diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml index df96f28d90..1007878ea9 100644 --- a/.github/workflows/e2e-test.yml +++ b/.github/workflows/e2e-test.yml @@ -21,6 +21,7 @@ jobs: - "sample-operators/mysql-schema" - "sample-operators/tomcat-operator" - "sample-operators/webpage" + - "sample-operators/leader-election" runs-on: ubuntu-latest steps: - name: Checkout diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 27669c45a1..429e49cd5b 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -25,8 +25,8 @@ jobs: cache: 'maven' - name: Check code format run: | - ./mvnw ${MAVEN_ARGS} formatter:validate -Dconfigfile=$PWD/contributing/eclipse-google-style.xml --file pom.xml - ./mvnw ${MAVEN_ARGS} impsort:check --file pom.xml + ./mvnw ${MAVEN_ARGS} formatter:validate -Dconfigfile=$PWD/contributing/eclipse-google-style.xml -pl '!operator-framework-bom' --file pom.xml + ./mvnw ${MAVEN_ARGS} impsort:check -pl '!operator-framework-bom' --file pom.xml - name: Run unit tests run: ./mvnw ${MAVEN_ARGS} -B test --file pom.xml diff --git a/docs/documentation/dependent-resources.md b/docs/documentation/dependent-resources.md index e1d073d1ee..720e78cd7b 100644 --- a/docs/documentation/dependent-resources.md +++ b/docs/documentation/dependent-resources.md @@ -86,6 +86,35 @@ possible to not implement any of these traits and therefore create read-only dep that will trigger your reconciler whenever a user interacts with them but that are never modified by your reconciler itself. +[`AbstractSimpleDependentResource`](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractSimpleDependentResource.java) +and [`KubernetesDependentResource`](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java) +sub-classes can also implement +the [`Matcher`](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/Matcher.java) +interface to customize how the SDK decides whether or not the actual state of the dependent +matches the desired state. This makes it convenient to use these abstract base classes for your +implementation, only customizing the matching logic. Note that in many cases, there is no need +to customize that logic as the SDK already provides convenient default implementations in the +form +of [`DesiredEqualsMatcher`](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DesiredEqualsMatcher.java) +and +[`GenericKubernetesResourceMatcher`](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcher.java) +implementations, respectively. If you want to provide custom logic, you only need your +`DependentResource` implementation to implement the `Matcher` interface as below, which shows +how to customize the default matching logic for Kubernetes resource to also consider annotations +and labels, which are ignored by default: + +```java +public class MyDependentResource extends KubernetesDependentResource + implements Matcher { + // your implementation + + public Result match(MyDependent actualResource, MyPrimary primary, + Context context) { + return GenericKubernetesResourceMatcher.match(this, actualResource, primary, context, true); + } +} +``` + ### Batteries included: convenient DependentResource implementations! JOSDK also offers several other convenient implementations building on top of @@ -116,7 +145,7 @@ Deleted (or set to be garbage collected). The following example shows how to cre @KubernetesDependent(labelSelector = WebPageManagedDependentsReconciler.SELECTOR) class DeploymentDependentResource extends CRUDKubernetesDependentResource { - public DeploymentDependentResource() { + public DeploymentDependentResource() { super(Deployment.class); } @@ -169,26 +198,26 @@ instances are managed by JOSDK, an example of which can be seen below: ```java @ControllerConfiguration( - labelSelector = SELECTOR, - dependents = { - @Dependent(type = ConfigMapDependentResource.class), - @Dependent(type = DeploymentDependentResource.class), - @Dependent(type = ServiceDependentResource.class) - }) + labelSelector = SELECTOR, + dependents = { + @Dependent(type = ConfigMapDependentResource.class), + @Dependent(type = DeploymentDependentResource.class), + @Dependent(type = ServiceDependentResource.class) + }) public class WebPageManagedDependentsReconciler - implements Reconciler, ErrorStatusHandler { + implements Reconciler, ErrorStatusHandler { - // omitted code + // omitted code - @Override - public UpdateControl reconcile(WebPage webPage, Context context) - throws Exception { + @Override + public UpdateControl reconcile(WebPage webPage, Context context) + throws Exception { - final var name = context.getSecondaryResource(ConfigMap.class).orElseThrow() - .getMetadata().getName(); - webPage.setStatus(createStatus(name)); - return UpdateControl.patchStatus(webPage); - } + final var name = context.getSecondaryResource(ConfigMap.class).orElseThrow() + .getMetadata().getName(); + webPage.setStatus(createStatus(name)); + return UpdateControl.patchStatus(webPage); + } } ``` @@ -215,69 +244,69 @@ an `Ingress`: @ControllerConfiguration public class WebPageStandaloneDependentsReconciler - implements Reconciler, ErrorStatusHandler, - EventSourceInitializer { - - private KubernetesDependentResource configMapDR; - private KubernetesDependentResource deploymentDR; - private KubernetesDependentResource serviceDR; - private KubernetesDependentResource ingressDR; - - public WebPageStandaloneDependentsReconciler(KubernetesClient kubernetesClient) { - // 1. - createDependentResources(kubernetesClient); - } - - @Override - public List prepareEventSources(EventSourceContext context) { - // 2. - return List.of( - configMapDR.initEventSource(context), - deploymentDR.initEventSource(context), - serviceDR.initEventSource(context)); - } - - @Override - public UpdateControl reconcile(WebPage webPage, Context context) - throws Exception { - - // 3. - if (!isValidHtml(webPage.getHtml())) { - return UpdateControl.patchStatus(setInvalidHtmlErrorMessage(webPage)); - } - - // 4. - configMapDR.reconcile(webPage, context); - deploymentDR.reconcile(webPage, context); - serviceDR.reconcile(webPage, context); - - // 5. - if (Boolean.TRUE.equals(webPage.getSpec().getExposed())) { - ingressDR.reconcile(webPage, context); - } else { - ingressDR.delete(webPage, context); - } - - // 6. - webPage.setStatus( - createStatus(configMapDR.getResource(webPage).orElseThrow().getMetadata().getName())); - return UpdateControl.patchStatus(webPage); - } - - private void createDependentResources(KubernetesClient client) { - this.configMapDR = new ConfigMapDependentResource(); - this.deploymentDR = new DeploymentDependentResource(); - this.serviceDR = new ServiceDependentResource(); - this.ingressDR = new IngressDependentResource(); - - Arrays.asList(configMapDR, deploymentDR, serviceDR, ingressDR).forEach(dr -> { - dr.setKubernetesClient(client); - dr.configureWith(new KubernetesDependentResourceConfig() - .setLabelSelector(DEPENDENT_RESOURCE_LABEL_SELECTOR)); - }); - } - - // omitted code + implements Reconciler, ErrorStatusHandler, + EventSourceInitializer { + + private KubernetesDependentResource configMapDR; + private KubernetesDependentResource deploymentDR; + private KubernetesDependentResource serviceDR; + private KubernetesDependentResource ingressDR; + + public WebPageStandaloneDependentsReconciler(KubernetesClient kubernetesClient) { + // 1. + createDependentResources(kubernetesClient); + } + + @Override + public List prepareEventSources(EventSourceContext context) { + // 2. + return List.of( + configMapDR.initEventSource(context), + deploymentDR.initEventSource(context), + serviceDR.initEventSource(context)); + } + + @Override + public UpdateControl reconcile(WebPage webPage, Context context) + throws Exception { + + // 3. + if (!isValidHtml(webPage.getHtml())) { + return UpdateControl.patchStatus(setInvalidHtmlErrorMessage(webPage)); + } + + // 4. + configMapDR.reconcile(webPage, context); + deploymentDR.reconcile(webPage, context); + serviceDR.reconcile(webPage, context); + + // 5. + if (Boolean.TRUE.equals(webPage.getSpec().getExposed())) { + ingressDR.reconcile(webPage, context); + } else { + ingressDR.delete(webPage, context); + } + + // 6. + webPage.setStatus( + createStatus(configMapDR.getResource(webPage).orElseThrow().getMetadata().getName())); + return UpdateControl.patchStatus(webPage); + } + + private void createDependentResources(KubernetesClient client) { + this.configMapDR = new ConfigMapDependentResource(); + this.deploymentDR = new DeploymentDependentResource(); + this.serviceDR = new ServiceDependentResource(); + this.ingressDR = new IngressDependentResource(); + + Arrays.asList(configMapDR, deploymentDR, serviceDR, ingressDR).forEach(dr -> { + dr.setKubernetesClient(client); + dr.configureWith(new KubernetesDependentResourceConfig() + .setLabelSelector(DEPENDENT_RESOURCE_LABEL_SELECTOR)); + }); + } + + // omitted code } ``` diff --git a/docs/documentation/features.md b/docs/documentation/features.md index 727349ddd3..06fa0698c0 100644 --- a/docs/documentation/features.md +++ b/docs/documentation/features.md @@ -683,6 +683,19 @@ See also the [integration test](https://github.com/java-operator-sdk/java-operator-sdk/blob/ec37025a15046d8f409c77616110024bf32c3416/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/changenamespace/ChangeNamespaceTestReconciler.java) for this feature. +## Leader Election + +Operators are generally deployed with a single running or active instance. However, it is +possible to deploy multiple instances in such a way that only one, called the "leader", processes the +events. This is achieved via a mechanism called "leader election". While all the instances are +running, and even start their event sources to populate the caches, only the leader will process +the events. This means that should the leader change for any reason, for example because it +crashed, the other instances are already warmed up and ready to pick up where the previous +leader left off should one of them become elected leader. + +See sample configuration in the [E2E test](https://github.com/java-operator-sdk/java-operator-sdk/blob/144947d89323f1c65de6e86bd8b9a6a8ffe714ff/sample-operators/leader-election/src/main/java/io/javaoperatorsdk/operator/sample/LeaderElectionTestOperator.java#L26-L30) +. + ## Monitoring with Micrometer ## Automatic Generation of CRDs diff --git a/micrometer-support/pom.xml b/micrometer-support/pom.xml index 79f052fba8..0bb4e646ae 100644 --- a/micrometer-support/pom.xml +++ b/micrometer-support/pom.xml @@ -5,7 +5,7 @@ java-operator-sdk io.javaoperatorsdk - 3.1.2-SNAPSHOT + 3.2.0-SNAPSHOT 4.0.0 diff --git a/operator-framework-bom/pom.xml b/operator-framework-bom/pom.xml new file mode 100644 index 0000000000..ed9912accd --- /dev/null +++ b/operator-framework-bom/pom.xml @@ -0,0 +1,43 @@ + + + 4.0.0 + + io.javaoperatorsdk + operator-framework-bom + 3.2.0-SNAPSHOT + Operator SDK - Bill of Materials + pom + + + + + io.javaoperatorsdk + operator-framework-core + ${project.version} + + + io.javaoperatorsdk + operator-framework + ${project.version} + + + io.javaoperatorsdk + micrometer-support + ${project.version} + + + io.javaoperatorsdk + operator-framework-junit-5 + ${project.version} + + + + + + + ossrh + https://oss.sonatype.org/content/repositories/snapshots + + + diff --git a/operator-framework-core/pom.xml b/operator-framework-core/pom.xml index 0bb8286d8c..81183dd2b2 100644 --- a/operator-framework-core/pom.xml +++ b/operator-framework-core/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk - 3.1.2-SNAPSHOT + 3.2.0-SNAPSHOT ../pom.xml @@ -52,12 +52,10 @@ - io.fabric8 kubernetes-client - org.slf4j slf4j-api diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/ControllerManager.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/ControllerManager.java new file mode 100644 index 0000000000..44aa430ec2 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/ControllerManager.java @@ -0,0 +1,81 @@ +package io.javaoperatorsdk.operator; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.javaoperatorsdk.operator.processing.Controller; + +/** + * Not to be confused with the controller manager concept from Go's controller-runtime project. In + * JOSDK, the equivalent concept is {@link Operator}. + */ +class ControllerManager { + + private static final Logger log = LoggerFactory.getLogger(ControllerManager.class); + + @SuppressWarnings("rawtypes") + private final Map controllers = new HashMap<>(); + private boolean started = false; + + public synchronized void shouldStart() { + if (started) { + return; + } + if (controllers.isEmpty()) { + throw new OperatorException("No Controller exists. Exiting!"); + } + } + + public synchronized void start(boolean startEventProcessor) { + controllers().parallelStream().forEach(c -> c.start(startEventProcessor)); + started = true; + } + + public synchronized void stop() { + controllers().parallelStream().forEach(closeable -> { + log.debug("closing {}", closeable); + closeable.stop(); + }); + started = false; + } + + public synchronized void startEventProcessing() { + controllers().parallelStream().forEach(Controller::startEventProcessing); + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + synchronized void add(Controller controller) { + final var configuration = controller.getConfiguration(); + final var resourceTypeName = ReconcilerUtils + .getResourceTypeNameWithVersion(configuration.getResourceClass()); + 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 resource '" + resourceTypeName + "'"); + } + controllers.put(resourceTypeName, controller); + } + + @SuppressWarnings("rawtypes") + synchronized Optional get(String name) { + return controllers().stream() + .filter(c -> name.equals(c.getConfiguration().getName())) + .findFirst(); + } + + @SuppressWarnings("rawtypes") + synchronized Collection controllers() { + return controllers.values(); + } + + synchronized int size() { + return controllers.size(); + } +} + diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/LeaderElectionManager.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/LeaderElectionManager.java new file mode 100644 index 0000000000..94d201dce9 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/LeaderElectionManager.java @@ -0,0 +1,85 @@ +package io.javaoperatorsdk.operator; + +import java.util.UUID; +import java.util.concurrent.CompletableFuture; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.extended.leaderelection.LeaderCallbacks; +import io.fabric8.kubernetes.client.extended.leaderelection.LeaderElectionConfig; +import io.fabric8.kubernetes.client.extended.leaderelection.LeaderElector; +import io.fabric8.kubernetes.client.extended.leaderelection.LeaderElectorBuilder; +import io.fabric8.kubernetes.client.extended.leaderelection.resourcelock.LeaseLock; +import io.fabric8.kubernetes.client.extended.leaderelection.resourcelock.Lock; +import io.javaoperatorsdk.operator.api.config.ConfigurationServiceProvider; +import io.javaoperatorsdk.operator.api.config.LeaderElectionConfiguration; + +public class LeaderElectionManager { + + private static final Logger log = LoggerFactory.getLogger(LeaderElectionManager.class); + + private LeaderElector leaderElector = null; + private final ControllerManager controllerManager; + private String identity; + private CompletableFuture leaderElectionFuture; + + public LeaderElectionManager(ControllerManager controllerManager) { + this.controllerManager = controllerManager; + } + + public void init(LeaderElectionConfiguration config, KubernetesClient client) { + this.identity = identity(config); + Lock lock = new LeaseLock(config.getLeaseNamespace(), config.getLeaseName(), identity); + // releaseOnCancel is not used in the underlying implementation + leaderElector = new LeaderElectorBuilder(client, + ConfigurationServiceProvider.instance().getExecutorService()) + .withConfig( + new LeaderElectionConfig(lock, config.getLeaseDuration(), config.getRenewDeadline(), + config.getRetryPeriod(), leaderCallbacks(), true, config.getLeaseName())) + .build(); + } + + public boolean isLeaderElectionEnabled() { + return leaderElector != null; + } + + private LeaderCallbacks leaderCallbacks() { + return new LeaderCallbacks(this::startLeading, this::stopLeading, leader -> { + log.info("New leader with identity: {}", leader); + }); + } + + private void startLeading() { + controllerManager.startEventProcessing(); + } + + private void stopLeading() { + log.info("Stopped leading for identity: {}. Exiting.", identity); + // When leader stops leading the process ends immediately to prevent multiple reconciliations + // running parallel. + // Note that some reconciliations might run for a very long time. + System.exit(1); + } + + private String identity(LeaderElectionConfiguration config) { + String id = config.getIdentity().orElse(System.getenv("HOSTNAME")); + if (id == null || id.isBlank()) { + id = UUID.randomUUID().toString(); + } + return id; + } + + public void start() { + if (isLeaderElectionEnabled()) { + leaderElectionFuture = leaderElector.start(); + } + } + + public void stop() { + if (leaderElectionFuture != null) { + leaderElectionFuture.cancel(false); + } + } +} 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 a17e86d8cc..57faef696a 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,6 @@ package io.javaoperatorsdk.operator; -import java.util.Collection; -import java.util.HashMap; import java.util.HashSet; -import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.function.Consumer; @@ -12,15 +9,10 @@ import org.slf4j.LoggerFactory; import io.fabric8.kubernetes.api.model.HasMetadata; -import io.fabric8.kubernetes.client.DefaultKubernetesClient; import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.KubernetesClientBuilder; 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; +import io.javaoperatorsdk.operator.api.config.*; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; import io.javaoperatorsdk.operator.processing.Controller; import io.javaoperatorsdk.operator.processing.LifecycleAware; @@ -29,14 +21,17 @@ public class Operator implements LifecycleAware { private static final Logger log = LoggerFactory.getLogger(Operator.class); private final KubernetesClient kubernetesClient; - private final ControllerManager controllers = new ControllerManager(); + private final ControllerManager controllerManager = new ControllerManager(); + private final LeaderElectionManager leaderElectionManager = + new LeaderElectionManager(controllerManager); + private volatile boolean started = false; public Operator() { - this(new DefaultKubernetesClient(), ConfigurationServiceProvider.instance()); + this((KubernetesClient) null); } public Operator(KubernetesClient kubernetesClient) { - this(kubernetesClient, ConfigurationServiceProvider.instance()); + this(kubernetesClient, ConfigurationServiceProvider.instance(), null); } /** @@ -44,16 +39,15 @@ public Operator(KubernetesClient kubernetesClient) { */ @Deprecated public Operator(ConfigurationService configurationService) { - this(new DefaultKubernetesClient(), configurationService); + this(null, configurationService, null); } public Operator(Consumer overrider) { - this(new DefaultKubernetesClient(), overrider); + this(null, overrider); } public Operator(KubernetesClient client, Consumer overrider) { - this(client); - ConfigurationServiceProvider.overrideCurrent(overrider); + this(client, ConfigurationServiceProvider.instance(), overrider); } /** @@ -64,8 +58,19 @@ public Operator(KubernetesClient client, Consumer * @param configurationService provides configuration */ public Operator(KubernetesClient kubernetesClient, ConfigurationService configurationService) { - this.kubernetesClient = kubernetesClient; + this(kubernetesClient, configurationService, null); + } + + private Operator(KubernetesClient kubernetesClient, ConfigurationService configurationService, + Consumer overrider) { + this.kubernetesClient = + kubernetesClient != null ? kubernetesClient : new KubernetesClientBuilder().build(); ConfigurationServiceProvider.set(configurationService); + if (overrider != null) { + ConfigurationServiceProvider.overrideCurrent(overrider); + } + ConfigurationServiceProvider.instance().getLeaderElectionConfiguration() + .ifPresent(c -> leaderElectionManager.init(c, this.kubernetesClient)); } /** Adds a shutdown hook that automatically calls {@link #stop()} when the app shuts down. */ @@ -84,8 +89,11 @@ public KubernetesClient getKubernetesClient() { */ public void start() { try { - controllers.shouldStart(); - + if (started) { + return; + } + started = true; + controllerManager.shouldStart(); final var version = ConfigurationServiceProvider.instance().getVersion(); log.info( "Operator SDK {} (commit: {}) built on {} starting...", @@ -95,9 +103,11 @@ public void start() { final var clientVersion = Version.clientVersion(); log.info("Client version: {}", clientVersion); - ExecutorServiceManager.init(); - controllers.start(); + // first start the controller manager before leader election, + // the leader election would start subsequently the processor if on + controllerManager.start(!leaderElectionManager.isLeaderElectionEnabled()); + leaderElectionManager.start(); } catch (Exception e) { log.error("Error starting operator", e); stop(); @@ -111,9 +121,9 @@ public void stop() throws OperatorException { log.info( "Operator SDK {} is shutting down...", configurationService.getVersion().getSdkVersion()); - controllers.stop(); - + controllerManager.stop(); ExecutorServiceManager.stop(); + leaderElectionManager.stop(); if (configurationService.closeClientOnStop()) { kubernetesClient.close(); } @@ -149,6 +159,9 @@ public

RegisteredController

register(Reconciler

re public

RegisteredController

register(Reconciler

reconciler, ControllerConfiguration

configuration) throws OperatorException { + if (started) { + throw new OperatorException("Operator already started. Register all the controllers before."); + } if (configuration == null) { throw new OperatorException( @@ -161,7 +174,7 @@ public

RegisteredController

register(Reconciler

re final var controller = new Controller<>(reconciler, configuration, kubernetesClient); - controllers.add(controller); + controllerManager.add(controller); final var watchedNS = configuration.watchAllNamespaces() ? "[all namespaces]" : configuration.getEffectiveNamespaces(); @@ -191,73 +204,15 @@ public

RegisteredController

register(Reconciler

re } public Optional getRegisteredController(String name) { - return controllers.get(name).map(RegisteredController.class::cast); + return controllerManager.get(name).map(RegisteredController.class::cast); } public Set getRegisteredControllers() { - return new HashSet<>(controllers.controllers()); + return new HashSet<>(controllerManager.controllers()); } public int getRegisteredControllersNumber() { - return controllers.size(); + return controllerManager.size(); } - static class ControllerManager implements LifecycleAware { - private final Map controllers = new HashMap<>(); - private boolean started = false; - - public synchronized void shouldStart() { - if (started) { - return; - } - if (controllers.isEmpty()) { - throw new OperatorException("No Controller exists. Exiting!"); - } - } - - public synchronized void start() { - controllers().parallelStream().forEach(Controller::start); - started = true; - } - - public synchronized void stop() { - controllers().parallelStream().forEach(closeable -> { - log.debug("closing {}", closeable); - closeable.stop(); - }); - - started = false; - } - - @SuppressWarnings("unchecked") - synchronized void add(Controller controller) { - final var configuration = controller.getConfiguration(); - final var resourceTypeName = ReconcilerUtils - .getResourceTypeNameWithVersion(configuration.getResourceClass()); - 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 resource '" + resourceTypeName + "'"); - } - controllers.put(resourceTypeName, controller); - if (started) { - controller.start(); - } - } - - synchronized Optional get(String name) { - return controllers().stream() - .filter(c -> name.equals(c.getConfiguration().getName())) - .findFirst(); - } - - synchronized Collection controllers() { - return controllers.values(); - } - - synchronized int size() { - return controllers.size(); - } - } } 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 7dfc52e50a..1e4b535202 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 @@ -21,6 +21,10 @@ public BaseConfigurationService(Version version, Cloner cloner, ObjectMapper map super(version, cloner, mapper); } + public BaseConfigurationService(Version version, Cloner cloner) { + super(version, cloner); + } + public BaseConfigurationService() { this(Utils.loadFromProperties()); } 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 a425bda5bd..f0d1455dd7 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,5 +1,6 @@ package io.javaoperatorsdk.operator.api.config; +import java.util.Optional; import java.util.Set; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -132,4 +133,9 @@ default ObjectMapper getObjectMapper() { default DependentResourceFactory dependentResourceFactory() { return new DependentResourceFactory() {}; } + + default Optional getLeaderElectionConfiguration() { + return Optional.empty(); + } + } 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 6233b59fd5..d222024115 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,5 +1,6 @@ package io.javaoperatorsdk.operator.api.config; +import java.util.Optional; import java.util.Set; import java.util.concurrent.ExecutorService; import java.util.function.Consumer; @@ -21,6 +22,7 @@ public class ConfigurationServiceOverrider { private boolean closeClientOnStop; private ObjectMapper objectMapper; private ExecutorService executorService = null; + private LeaderElectionConfiguration leaderElectionConfiguration; ConfigurationServiceOverrider(ConfigurationService original) { this.original = original; @@ -80,6 +82,12 @@ public ConfigurationServiceOverrider withObjectMapper(ObjectMapper objectMapper) return this; } + public ConfigurationServiceOverrider withLeaderElectionConfiguration( + LeaderElectionConfiguration leaderElectionConfiguration) { + this.leaderElectionConfiguration = leaderElectionConfiguration; + return this; + } + public ConfigurationService build() { return new BaseConfigurationService(original.getVersion(), cloner, objectMapper) { @Override @@ -125,6 +133,16 @@ public ExecutorService getExecutorService() { return super.getExecutorService(); } } + + @Override + public ObjectMapper getObjectMapper() { + return objectMapper; + } + + @Override + public Optional getLeaderElectionConfiguration() { + return Optional.ofNullable(leaderElectionConfiguration); + } }; } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/LeaderElectionConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/LeaderElectionConfiguration.java new file mode 100644 index 0000000000..5146fa6a1e --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/LeaderElectionConfiguration.java @@ -0,0 +1,85 @@ +package io.javaoperatorsdk.operator.api.config; + +import java.time.Duration; +import java.util.Optional; + +public class LeaderElectionConfiguration { + + public static final Duration LEASE_DURATION_DEFAULT_VALUE = Duration.ofSeconds(15); + public static final Duration RENEW_DEADLINE_DEFAULT_VALUE = Duration.ofSeconds(10); + public static final Duration RETRY_PERIOD_DEFAULT_VALUE = Duration.ofSeconds(2); + + private final String leaseName; + private final String leaseNamespace; + private final String identity; + + private final Duration leaseDuration; + private final Duration renewDeadline; + private final Duration retryPeriod; + + public LeaderElectionConfiguration(String leaseName, String leaseNamespace, String identity) { + this( + leaseName, + leaseNamespace, + LEASE_DURATION_DEFAULT_VALUE, + RENEW_DEADLINE_DEFAULT_VALUE, + RETRY_PERIOD_DEFAULT_VALUE, identity); + } + + public LeaderElectionConfiguration(String leaseName, String leaseNamespace) { + this( + leaseName, + leaseNamespace, + LEASE_DURATION_DEFAULT_VALUE, + RENEW_DEADLINE_DEFAULT_VALUE, + RETRY_PERIOD_DEFAULT_VALUE, null); + } + + public LeaderElectionConfiguration( + String leaseName, + String leaseNamespace, + Duration leaseDuration, + Duration renewDeadline, + Duration retryPeriod) { + this(leaseName, leaseNamespace, leaseDuration, renewDeadline, retryPeriod, null); + } + + public LeaderElectionConfiguration( + String leaseName, + String leaseNamespace, + Duration leaseDuration, + Duration renewDeadline, + Duration retryPeriod, + String identity) { + this.leaseName = leaseName; + this.leaseNamespace = leaseNamespace; + this.leaseDuration = leaseDuration; + this.renewDeadline = renewDeadline; + this.retryPeriod = retryPeriod; + this.identity = identity; + } + + public String getLeaseNamespace() { + return leaseNamespace; + } + + public String getLeaseName() { + return leaseName; + } + + public Duration getLeaseDuration() { + return leaseDuration; + } + + public Duration getRenewDeadline() { + return renewDeadline; + } + + public Duration getRetryPeriod() { + return retryPeriod; + } + + public Optional getIdentity() { + return Optional.ofNullable(identity); + } +} 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 66439ab8b8..92b70e722d 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,6 +36,7 @@ import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.DefaultManagedDependentResourceContext; import io.javaoperatorsdk.operator.processing.dependent.workflow.ManagedWorkflow; import io.javaoperatorsdk.operator.processing.dependent.workflow.WorkflowCleanupResult; +import io.javaoperatorsdk.operator.processing.event.EventProcessor; import io.javaoperatorsdk.operator.processing.event.EventSourceManager; import io.javaoperatorsdk.operator.processing.event.ResourceID; @@ -44,7 +45,8 @@ @SuppressWarnings({"unchecked", "rawtypes"}) @Ignore public class Controller

- implements Reconciler

, Cleaner

, LifecycleAware, RegisteredController

{ + implements Reconciler

, LifecycleAware, Cleaner

, + RegisteredController

{ private static final Logger log = LoggerFactory.getLogger(Controller.class); @@ -58,6 +60,7 @@ public class Controller

private final ManagedWorkflow

managedWorkflow; private final GroupVersionKind associatedGVK; + private final EventProcessor

eventProcessor; public Controller(Reconciler

reconciler, ControllerConfiguration

configuration, @@ -75,6 +78,8 @@ public Controller(Reconciler

reconciler, managedWorkflow = ManagedWorkflow.workflowFor(kubernetesClient, configuration.getDependentResources()); eventSourceManager = new EventSourceManager<>(this); + eventProcessor = new EventProcessor<>(eventSourceManager); + eventSourceManager.postProcessDefaultEventSourcesAfterProcessorInitializer(); } @Override @@ -258,6 +263,10 @@ public MixedOperation, Resource

> getCRClient() { return kubernetesClient.resources(configuration.getResourceClass()); } + public void start() throws OperatorException { + start(true); + } + /** * Registers the specified controller with this operator, overriding its default configuration by * the specified one (usually created via @@ -266,7 +275,7 @@ public MixedOperation, Resource

> getCRClient() { * * @throws OperatorException if a problem occurred during the registration process */ - public void start() throws OperatorException { + public synchronized void start(boolean startEventProcessor) throws OperatorException { final Class

resClass = configuration.getResourceClass(); final String controllerName = configuration.getName(); final var crdName = configuration.getResourceTypeName(); @@ -284,6 +293,9 @@ public void start() throws OperatorException { initAndRegisterEventSources(context); eventSourceManager.start(); + if (startEventProcessor) { + eventProcessor.start(); + } log.info("'{}' controller started, pending event sources initialization", controllerName); } catch (MissingCRDException e) { stop(); @@ -291,6 +303,7 @@ public void start() throws OperatorException { } } + private void validateCRDWithLocalModelIfRequired(Class

resClass, String controllerName, String crdName, String specVersion) { final CustomResourceDefinition crd; @@ -311,7 +324,14 @@ public void changeNamespaces(Set namespaces) { || namespaces.contains(WATCH_CURRENT_NAMESPACE)) { throw new OperatorException("Unexpected value in target namespaces: " + namespaces); } + eventProcessor.stop(); eventSourceManager.changeNamespaces(namespaces); + eventProcessor.start(); + } + + public synchronized void startEventProcessing() { + log.info("Started event processing for controller: {}", configuration.getName()); + eventProcessor.start(); } private void throwMissingCRDException(String crdName, String specVersion, String controllerName) { @@ -346,7 +366,10 @@ public EventSourceManager

getEventSourceManager() { return eventSourceManager; } - public void stop() { + public synchronized void stop() { + if (eventProcessor != null) { + eventProcessor.stop(); + } if (eventSourceManager != null) { eventSourceManager.stop(); } @@ -359,4 +382,8 @@ public boolean useFinalizer() { public GroupVersionKind getAssociatedGroupVersionKind() { return associatedGVK; } + + public EventProcessor

getEventProcessor() { + return eventProcessor; + } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/Matcher.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/Matcher.java index 84290fe464..750fe89cbf 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/Matcher.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/Matcher.java @@ -5,18 +5,68 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.api.reconciler.Context; +/** + * Implement this interface to provide custom matching logic when determining whether secondary + * resources match their desired state. This is used by some default implementations of the + * {@link io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource} interface, notably + * {@link io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource}. + * + * @param the type associated with the secondary resources we want to match + * @param

the type associated with the primary resources with which the related + * {@link io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource} + * implementation is associated + */ public interface Matcher { + + /** + * Abstracts the matching result letting implementations also return the desired state if it has + * been computed as part of their logic. This allows the SDK to avoid re-computing it if not + * needed. + * + * @param the type associated with the secondary resources we want to match + */ interface Result { + + /** + * Whether or not the actual resource matched the desired state + * + * @return {@code true} if the observed resource matched the desired state, {@code false} + * otherwise + */ boolean matched(); + /** + * Retrieves the associated desired state if it has been computed during the matching process or + * empty if not. + * + * @return an {@link Optional} holding the desired state if it has been computed during the + * matching process or {@link Optional#empty()} if not + */ default Optional computedDesired() { return Optional.empty(); } + /** + * Creates a result stating only whether the resource matched the desired state without having + * computed the desired state. + * + * @param matched whether the actual resource matched the desired state + * @return a {@link Result} with an empty computed desired state + * @param the type of resources being matched + */ static Result nonComputed(boolean matched) { return () -> matched; } + /** + * Creates a result stating whether the resource matched and the associated computed desired + * state so that the SDK can use it downstream without having to recompute it. + * + * @param matched whether the actual resource matched the desired state + * @param computedDesired the associated desired state as computed during the matching process + * @return a {@link Result} with the associated desired state + * @param the type of resources being matched + */ static Result computed(boolean matched, T computedDesired) { return new Result<>() { @Override @@ -32,5 +82,17 @@ public Optional computedDesired() { } } + /** + * Determines whether the specified secondary resource matches the desired state as defined from + * the specified primary resource, given the specified {@link Context}. + * + * @param actualResource the resource we want to determine whether it's matching the desired state + * @param primary the primary resource from which the desired state is inferred + * @param context the context in which the resource is being matched + * @return a {@link Result} encapsulating whether the resource matched its desired state and this + * associated state if it was computed as part of the matching process. Use the static + * convenience methods ({@link Result#nonComputed(boolean)} and + * {@link Result#computed(boolean, Object)}) + */ Result match(R actualResource, P primary, 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 f2c789e267..e294b1c938 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 @@ -1,5 +1,7 @@ package io.javaoperatorsdk.operator.processing.dependent.kubernetes; +import java.util.Objects; + import io.fabric8.kubernetes.api.model.ConfigMap; import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.api.model.Secret; @@ -41,8 +43,44 @@ static Matcher matcherFor( @Override public Result match(R actualResource, P primary, Context

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

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

context, boolean considerMetadata) { final var desired = dependentResource.desired(primary, context); + if (considerMetadata) { + final var desiredMetadata = desired.getMetadata(); + final var actualMetadata = actualResource.getMetadata(); + final var matched = + Objects.equals(desiredMetadata.getAnnotations(), actualMetadata.getAnnotations()) && + Objects.equals(desiredMetadata.getLabels(), actualMetadata.getLabels()); + if (!matched) { + return Result.computed(false, desired); + } + } + + final var objectMapper = ConfigurationServiceProvider.instance().getObjectMapper(); // reflection will be replaced by this: // https://github.com/fabric8io/kubernetes-client/issues/3816 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 226a656abc..e46878f927 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,7 +31,7 @@ import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getName; -class EventProcessor implements EventHandler, LifecycleAware { +public class EventProcessor implements EventHandler, LifecycleAware { private static final Logger log = LoggerFactory.getLogger(EventProcessor.class); private static final long MINIMAL_RATE_LIMIT_RESCHEDULE_DURATION = 50; @@ -48,7 +48,7 @@ class EventProcessor implements EventHandler, LifecycleAw private final ResourceStateManager resourceStateManager = new ResourceStateManager(); private final Map metricsMetadata; - EventProcessor(EventSourceManager eventSourceManager) { + public EventProcessor(EventSourceManager eventSourceManager) { this( eventSourceManager.getControllerResourceEventSource(), ExecutorServiceManager.instance().executorService(), 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 3332364410..3f1e5e848d 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 @@ -23,31 +23,30 @@ import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceAction; import io.javaoperatorsdk.operator.processing.event.source.timer.TimerEventSource; -public class EventSourceManager implements LifecycleAware { +public class EventSourceManager

implements LifecycleAware { private static final Logger log = LoggerFactory.getLogger(EventSourceManager.class); - private final EventSources eventSources; - private final EventProcessor eventProcessor; - private final Controller controller; + private final EventSources

eventSources; + private final Controller

controller; - public EventSourceManager(Controller controller) { + public EventSourceManager(Controller

controller) { this(controller, new EventSources<>()); } - EventSourceManager(Controller controller, EventSources eventSources) { + EventSourceManager(Controller

controller, EventSources

eventSources) { this.eventSources = eventSources; this.controller = controller; // controller event source needs to be available before we create the event processor eventSources.initControllerEventSource(controller); - this.eventProcessor = new EventProcessor<>(this); - postProcessDefaultEventSources(); + + postProcessDefaultEventSourcesAfterProcessorInitializer(); } - private void postProcessDefaultEventSources() { - eventSources.controllerResourceEventSource().setEventHandler(eventProcessor); - eventSources.retryEventSource().setEventHandler(eventProcessor); + public void postProcessDefaultEventSourcesAfterProcessorInitializer() { + eventSources.controllerResourceEventSource().setEventHandler(controller.getEventProcessor()); + eventSources.retryEventSource().setEventHandler(controller.getEventProcessor()); } /** @@ -64,7 +63,6 @@ private void postProcessDefaultEventSources() { public synchronized void start() { startEventSource(eventSources.namedControllerResourceEventSource()); eventSources.additionalNamedEventSources().parallel().forEach(this::startEventSource); - eventProcessor.start(); } @Override @@ -72,7 +70,7 @@ public synchronized void stop() { stopEventSource(eventSources.namedControllerResourceEventSource()); eventSources.additionalNamedEventSources().parallel().forEach(this::stopEventSource); eventSources.clear(); - eventProcessor.stop(); + } @SuppressWarnings("rawtypes") @@ -122,7 +120,7 @@ public final synchronized void registerEventSource(String name, EventSource even name = EventSourceInitializer.generateNameFor(eventSource); } eventSources.add(name, eventSource); - eventSource.setEventHandler(eventProcessor); + eventSource.setEventHandler(controller.getEventProcessor()); } catch (IllegalStateException | MissingCRDException e) { throw e; // leave untouched } catch (Exception e) { @@ -132,10 +130,10 @@ public final synchronized void registerEventSource(String name, EventSource even } @SuppressWarnings("unchecked") - public void broadcastOnResourceEvent(ResourceAction action, R resource, R oldResource) { + public void broadcastOnResourceEvent(ResourceAction action, P resource, P oldResource) { eventSources.additionalNamedEventSources().forEach(eventSource -> { if (eventSource.original() instanceof ResourceEventAware) { - var lifecycleAwareES = ((ResourceEventAware) eventSource.original()); + var lifecycleAwareES = ((ResourceEventAware

) eventSource.original()); switch (action) { case ADDED: lifecycleAwareES.onResourceCreated(resource); @@ -152,7 +150,6 @@ public void broadcastOnResourceEvent(ResourceAction action, R resource, R oldRes } public void changeNamespaces(Set namespaces) { - eventProcessor.stop(); eventSources.controllerResourceEventSource() .changeNamespaces(namespaces); eventSources @@ -162,11 +159,6 @@ public void changeNamespaces(Set namespaces) { .filter(NamespaceChangeable::allowsNamespaceChanges) .parallel() .forEach(ies -> ies.changeNamespaces(namespaces)); - eventProcessor.start(); - } - - EventHandler getEventHandler() { - return eventProcessor; } public Set getRegisteredEventSources() { @@ -175,30 +167,30 @@ public Set getRegisteredEventSources() { .collect(Collectors.toCollection(LinkedHashSet::new)); } - public ControllerResourceEventSource getControllerResourceEventSource() { + public ControllerResourceEventSource

getControllerResourceEventSource() { return eventSources.controllerResourceEventSource(); } - ResourceEventSource getResourceEventSourceFor( + ResourceEventSource getResourceEventSourceFor( Class dependentType) { return getResourceEventSourceFor(dependentType, null); } - public List> getEventSourcesFor(Class dependentType) { + public List> getEventSourcesFor(Class dependentType) { return eventSources.getEventSources(dependentType); } - public ResourceEventSource getResourceEventSourceFor( + public ResourceEventSource getResourceEventSourceFor( Class dependentType, String qualifier) { Objects.requireNonNull(dependentType, "dependentType is Mandatory"); return eventSources.get(dependentType, qualifier); } - TimerEventSource retryEventSource() { + TimerEventSource

retryEventSource() { return eventSources.retryEventSource(); } - Controller getController() { + Controller

getController() { return controller; } } 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 db529c48a0..bb19e6b505 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcher.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcher.java @@ -1,9 +1,5 @@ package io.javaoperatorsdk.operator.processing.event; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.nio.charset.StandardCharsets; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -14,7 +10,6 @@ import io.fabric8.kubernetes.client.dsl.MixedOperation; import io.fabric8.kubernetes.client.dsl.Resource; import io.fabric8.kubernetes.client.dsl.internal.HasMetadataOperationsImpl; -import io.fabric8.kubernetes.client.utils.Serialization; import io.javaoperatorsdk.operator.OperatorException; import io.javaoperatorsdk.operator.api.ObservedGenerationAware; import io.javaoperatorsdk.operator.api.config.ConfigurationServiceProvider; @@ -35,26 +30,26 @@ /** * Handles calls and results of a Reconciler and finalizer related logic */ -class ReconciliationDispatcher { +class ReconciliationDispatcher

{ public static final int MAX_FINALIZER_REMOVAL_RETRY = 10; private static final Logger log = LoggerFactory.getLogger(ReconciliationDispatcher.class); - private final Controller controller; - private final CustomResourceFacade customResourceFacade; + private final Controller

controller; + private final CustomResourceFacade

customResourceFacade; - ReconciliationDispatcher(Controller controller, - CustomResourceFacade customResourceFacade) { + ReconciliationDispatcher(Controller

controller, + CustomResourceFacade

customResourceFacade) { this.controller = controller; this.customResourceFacade = customResourceFacade; } - public ReconciliationDispatcher(Controller controller) { + public ReconciliationDispatcher(Controller

controller) { this(controller, new CustomResourceFacade<>(controller.getCRClient())); } - public PostExecutionControl handleExecution(ExecutionScope executionScope) { + public PostExecutionControl

handleExecution(ExecutionScope

executionScope) { try { return handleDispatch(executionScope); } catch (Exception e) { @@ -63,9 +58,9 @@ public PostExecutionControl handleExecution(ExecutionScope executionScope) } } - private PostExecutionControl handleDispatch(ExecutionScope executionScope) + private PostExecutionControl

handleDispatch(ExecutionScope

executionScope) throws Exception { - R originalResource = executionScope.getResource(); + P originalResource = executionScope.getResource(); var resourceForExecution = cloneResource(originalResource); log.debug("Handling dispatch for resource {}", getName(originalResource)); @@ -78,7 +73,7 @@ private PostExecutionControl handleDispatch(ExecutionScope executionScope) return PostExecutionControl.defaultDispatch(); } - Context context = + Context

context = new DefaultContext<>(executionScope.getRetryInfo(), controller, originalResource); if (markedForDeletion) { return handleCleanup(resourceForExecution, context); @@ -87,7 +82,7 @@ private PostExecutionControl handleDispatch(ExecutionScope executionScope) } } - private boolean shouldNotDispatchToCleanupWhenMarkedForDeletion(R resource) { + private boolean shouldNotDispatchToCleanupWhenMarkedForDeletion(P resource) { var alreadyRemovedFinalizer = controller.useFinalizer() && !resource.hasFinalizer(configuration().getFinalizerName()); if (alreadyRemovedFinalizer) { @@ -97,9 +92,9 @@ private boolean shouldNotDispatchToCleanupWhenMarkedForDeletion(R resource) { return !controller.useFinalizer() || alreadyRemovedFinalizer; } - private PostExecutionControl handleReconcile( - ExecutionScope executionScope, R resourceForExecution, R originalResource, - Context context) throws Exception { + private PostExecutionControl

handleReconcile( + ExecutionScope

executionScope, P resourceForExecution, P originalResource, + Context

context) throws Exception { if (controller.useFinalizer() && !originalResource.hasFinalizer(configuration().getFinalizerName())) { /* @@ -119,21 +114,21 @@ private PostExecutionControl handleReconcile( } } - private R cloneResource(R resource) { + private P cloneResource(P resource) { final var cloner = ConfigurationServiceProvider.instance().getResourceCloner(); return cloner.clone(resource); } - private PostExecutionControl reconcileExecution(ExecutionScope executionScope, - R resourceForExecution, R originalResource, Context context) throws Exception { + private PostExecutionControl

reconcileExecution(ExecutionScope

executionScope, + P resourceForExecution, P originalResource, Context

context) throws Exception { log.debug( "Reconciling resource {} with version: {} with execution scope: {}", getName(resourceForExecution), getVersion(resourceForExecution), executionScope); - UpdateControl updateControl = controller.reconcile(resourceForExecution, context); - R updatedCustomResource = null; + UpdateControl

updateControl = controller.reconcile(resourceForExecution, context); + P updatedCustomResource = null; if (updateControl.isUpdateResourceAndStatus()) { updatedCustomResource = updateCustomResource(updateControl.getResource()); @@ -165,8 +160,8 @@ && shouldUpdateObservedGenerationAutomatically(resourceForExecution)) { } @SuppressWarnings("unchecked") - private PostExecutionControl handleErrorStatusHandler(R resource, R originalResource, - Context context, + private PostExecutionControl

handleErrorStatusHandler(P resource, P originalResource, + Context

context, Exception e) throws Exception { if (isErrorStatusHandlerPresent()) { try { @@ -181,11 +176,11 @@ public boolean isLastAttempt() { return controller.getConfiguration().getRetry() == null; } }); - ((DefaultContext) context).setRetryInfo(retryInfo); - var errorStatusUpdateControl = ((ErrorStatusHandler) controller.getReconciler()) + ((DefaultContext

) context).setRetryInfo(retryInfo); + var errorStatusUpdateControl = ((ErrorStatusHandler

) controller.getReconciler()) .updateErrorStatus(resource, context, e); - R updatedResource = null; + P updatedResource = null; if (errorStatusUpdateControl.getResource().isPresent()) { updatedResource = errorStatusUpdateControl.isPatch() ? customResourceFacade .patchStatus(errorStatusUpdateControl.getResource().orElseThrow(), originalResource) @@ -212,7 +207,7 @@ private boolean isErrorStatusHandlerPresent() { return controller.getReconciler() instanceof ErrorStatusHandler; } - private R updateStatusGenerationAware(R resource, R originalResource, boolean patch) { + private P updateStatusGenerationAware(P resource, P originalResource, boolean patch) { updateStatusObservedGenerationIfRequired(resource); if (patch) { return customResourceFacade.patchStatus(resource, originalResource); @@ -222,7 +217,7 @@ private R updateStatusGenerationAware(R resource, R originalResource, boolean pa } @SuppressWarnings("rawtypes") - private boolean shouldUpdateObservedGenerationAutomatically(R resource) { + private boolean shouldUpdateObservedGenerationAutomatically(P resource) { if (configuration().isGenerationAware() && resource instanceof CustomResource) { var customResource = (CustomResource) resource; var status = customResource.getStatus(); @@ -237,7 +232,7 @@ private boolean shouldUpdateObservedGenerationAutomatically(R resource) { } @SuppressWarnings("rawtypes") - private void updateStatusObservedGenerationIfRequired(R resource) { + private void updateStatusObservedGenerationIfRequired(P resource) { if (configuration().isGenerationAware() && resource instanceof CustomResource) { var customResource = (CustomResource) resource; var status = customResource.getStatus(); @@ -249,9 +244,9 @@ private void updateStatusObservedGenerationIfRequired(R resource) { } } - private PostExecutionControl createPostExecutionControl(R updatedCustomResource, - UpdateControl updateControl) { - PostExecutionControl postExecutionControl; + private PostExecutionControl

createPostExecutionControl(P updatedCustomResource, + UpdateControl

updateControl) { + PostExecutionControl

postExecutionControl; if (updatedCustomResource != null) { if (updateControl.isUpdateStatus() && updateControl.isPatch()) { postExecutionControl = @@ -267,7 +262,7 @@ private PostExecutionControl createPostExecutionControl(R updatedCustomResour } private void updatePostExecutionControlWithReschedule( - PostExecutionControl postExecutionControl, + PostExecutionControl

postExecutionControl, BaseControl baseControl) { baseControl.getScheduleDelay().ifPresentOrElse(postExecutionControl::withReSchedule, () -> controller.getConfiguration().maxReconciliationInterval() @@ -275,7 +270,7 @@ private void updatePostExecutionControlWithReschedule( } - private PostExecutionControl handleCleanup(R resource, Context context) { + private PostExecutionControl

handleCleanup(P resource, Context

context) { log.debug( "Executing delete for resource: {} with version: {}", getName(resource), @@ -288,7 +283,7 @@ private PostExecutionControl handleCleanup(R resource, Context context) { // cleanup is finished, nothing left to done final var finalizerName = configuration().getFinalizerName(); if (deleteControl.isRemoveFinalizer() && resource.hasFinalizer(finalizerName)) { - R customResource = removeFinalizer(resource, finalizerName); + P customResource = removeFinalizer(resource, finalizerName); return PostExecutionControl.customResourceFinalizerRemoved(customResource); } } @@ -298,29 +293,29 @@ private PostExecutionControl handleCleanup(R resource, Context context) { getVersion(resource), deleteControl, useFinalizer); - PostExecutionControl postExecutionControl = PostExecutionControl.defaultDispatch(); + PostExecutionControl

postExecutionControl = PostExecutionControl.defaultDispatch(); updatePostExecutionControlWithReschedule(postExecutionControl, deleteControl); return postExecutionControl; } - private R updateCustomResourceWithFinalizer(R resource) { + private P updateCustomResourceWithFinalizer(P resource) { log.debug( "Adding finalizer for resource: {} version: {}", getUID(resource), getVersion(resource)); resource.addFinalizer(configuration().getFinalizerName()); - return customResourceFacade.replaceResourceWithLock(resource); + return customResourceFacade.updateResource(resource); } - private R updateCustomResource(R resource) { + private P updateCustomResource(P resource) { log.debug("Updating resource: {} with version: {}", getUID(resource), getVersion(resource)); log.trace("Resource before update: {}", resource); - return customResourceFacade.replaceResourceWithLock(resource); + return customResourceFacade.updateResource(resource); } - ControllerConfiguration configuration() { + ControllerConfiguration

configuration() { return controller.getConfiguration(); } - public R removeFinalizer(R resource, String finalizer) { + public P removeFinalizer(P resource, String finalizer) { if (log.isDebugEnabled()) { log.debug("Removing finalizer on resource: {}", ResourceID.fromResource(resource)); } @@ -331,7 +326,7 @@ public R removeFinalizer(R resource, String finalizer) { if (!removed) { return resource; } - return customResourceFacade.replaceResourceWithLock(resource); + return customResourceFacade.updateResource(resource); } catch (KubernetesClientException e) { log.trace("Exception during finalizer removal for resource: {}", resource); retryIndex++; @@ -365,16 +360,16 @@ public R getResource(String namespace, String name) { return resourceOperation.inNamespace(namespace).withName(name).get(); } - public R replaceResourceWithLock(R resource) { + public R updateResource(R resource) { log.debug( "Trying to replace resource {}, version: {}", getName(resource), resource.getMetadata().getResourceVersion()); return resourceOperation .inNamespace(resource.getMetadata().getNamespace()) - .withName(getName(resource)) + .resource(resource) .lockResourceVersion(resource.getMetadata().getResourceVersion()) - .replace(resource); + .replace(); } @SuppressWarnings({"rawtypes", "unchecked"}) @@ -393,15 +388,11 @@ public R patchStatus(R resource, R originalResource) { // don't do optimistic locking on patch originalResource.getMetadata().setResourceVersion(null); resource.getMetadata().setResourceVersion(null); - try (var bis = new ByteArrayInputStream( - Serialization.asJson(originalResource).getBytes(StandardCharsets.UTF_8))) { + try { return resourceOperation .inNamespace(resource.getMetadata().getNamespace()) - // will be simplified in fabric8 v6 - .load(bis) + .resource(originalResource) .editStatus(r -> resource); - } catch (IOException e) { - throw new IllegalStateException(e); } finally { // restore initial resource version originalResource.getMetadata().setResourceVersion(resourceVersion); 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 da4631a24f..7ca398c615 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 @@ -24,10 +24,9 @@ import io.javaoperatorsdk.operator.processing.event.ResourceID; import io.javaoperatorsdk.operator.processing.event.source.Cache; import io.javaoperatorsdk.operator.processing.event.source.IndexerResourceCache; -import io.javaoperatorsdk.operator.processing.event.source.UpdatableCache; public class InformerManager> - implements LifecycleAware, IndexerResourceCache, UpdatableCache { + implements LifecycleAware, IndexerResourceCache { private static final String ALL_NAMESPACES_MAP_KEY = "allNamespaces"; private static final Logger log = LoggerFactory.getLogger(InformerManager.class); @@ -95,7 +94,7 @@ public void changeNamespaces(Set namespaces) { private InformerWrapper createEventSource( - FilterWatchListDeletable> filteredBySelectorClient, + FilterWatchListDeletable, Resource> filteredBySelectorClient, ResourceEventHandler eventHandler, String key) { var source = new InformerWrapper<>(filteredBySelectorClient.runnableInformer(0)); source.addEventHandler(eventHandler); @@ -157,22 +156,6 @@ private Optional> getSource(String namespace) { return Optional.ofNullable(sources.get(namespace)); } - @Override - public T remove(ResourceID key) { - return getSource(key.getNamespace().orElse(ALL_NAMESPACES_MAP_KEY)) - .map(c -> c.remove(key)) - .orElse(null); - } - - @Override - public void put(ResourceID key, T resource) { - getSource(key.getNamespace().orElse(ALL_NAMESPACES_MAP_KEY)) - .ifPresentOrElse(c -> c.put(key, resource), - () -> log.warn( - "Cannot put resource in the cache. No related cache found: {}. Resource: {}", - key, resource)); - } - @Override public void addIndexers(Map>> indexers) { this.indexers.putAll(indexers); 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 e0f3108660..24930b6c0c 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 @@ -2,7 +2,6 @@ import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.Optional; import java.util.function.Function; import java.util.function.Predicate; @@ -17,10 +16,9 @@ import io.javaoperatorsdk.operator.processing.LifecycleAware; import io.javaoperatorsdk.operator.processing.event.ResourceID; import io.javaoperatorsdk.operator.processing.event.source.IndexerResourceCache; -import io.javaoperatorsdk.operator.processing.event.source.UpdatableCache; class InformerWrapper - implements LifecycleAware, IndexerResourceCache, UpdatableCache { + implements LifecycleAware, IndexerResourceCache { private final SharedIndexInformer informer; private final Cache cache; @@ -72,22 +70,6 @@ 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); - } - public void addEventHandler(ResourceEventHandler eventHandler) { informer.addEventHandler(eventHandler); } 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 b328ace132..d788f61e4a 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,15 +4,10 @@ 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; 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.TestCustomReconcilerOtherV1; -import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; -import io.javaoperatorsdk.operator.sample.simple.TestCustomResourceOtherV1; +import io.javaoperatorsdk.operator.sample.simple.*; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; 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 index b282e11fe5..0527e9bd8f 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/MockKubernetesClient.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/MockKubernetesClient.java @@ -4,12 +4,7 @@ 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.dsl.*; import io.fabric8.kubernetes.client.informers.SharedIndexInformer; import io.fabric8.kubernetes.client.informers.cache.Indexer; @@ -28,9 +23,9 @@ public static KubernetesClient client(Class clazz) { mock(MixedOperation.class); NonNamespaceOperation, Resource> nonNamespaceOperation = mock(NonNamespaceOperation.class); - FilterWatchListMultiDeletable> inAnyNamespace = mock( - FilterWatchListMultiDeletable.class); - FilterWatchListDeletable> filterable = + AnyNamespaceOperation, Resource> inAnyNamespace = mock( + AnyNamespaceOperation.class); + FilterWatchListDeletable, Resource> filterable = mock(FilterWatchListDeletable.class); when(resources.inNamespace(anyString())).thenReturn(nonNamespaceOperation); when(nonNamespaceOperation.withLabelSelector(nullable(String.class))).thenReturn(filterable); 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 1bdad72c76..3cf2a4a6bc 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 @@ -1,7 +1,5 @@ package io.javaoperatorsdk.operator; -import java.net.URI; - import org.junit.jupiter.api.Test; import io.fabric8.kubernetes.api.model.ContainerBuilder; @@ -15,7 +13,6 @@ import io.fabric8.kubernetes.api.model.apps.DeploymentSpec; import io.fabric8.kubernetes.client.CustomResource; import io.fabric8.kubernetes.client.KubernetesClientException; -import io.fabric8.kubernetes.client.http.HttpRequest; import io.fabric8.kubernetes.model.annotation.Group; import io.fabric8.kubernetes.model.annotation.ShortNames; import io.fabric8.kubernetes.model.annotation.Version; @@ -31,8 +28,6 @@ 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.mockito.Mockito.mock; -import static org.mockito.Mockito.when; class ReconcilerUtilsTest { @@ -121,13 +116,10 @@ private Deployment createTestDeployment() { @Test void handleKubernetesExceptionShouldThrowMissingCRDExceptionWhenAppropriate() { - var request = mock(HttpRequest.class); - when(request.uri()).thenReturn(URI - .create(RESOURCE_URI)); assertThrows(MissingCRDException.class, () -> handleKubernetesClientException( new KubernetesClientException( - "Failure executing: GET at: " + RESOURCE_URI + ". Message: Not Found.", - null, 404, null, request), + "Failure executing: GET at: https://kubernetes.docker.internal:6443/apis/tomcatoperator.io/v1/tomcats. Message: Not Found.", + 404, null), HasMetadata.getFullResourceName(Tomcat.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 bad10d551d..d39db6ced6 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 @@ -31,21 +31,9 @@ static void setUp() { void checksIfDesiredValuesAreTheSame() { var actual = createDeployment(); final var desired = createDeployment(); - final var matcher = GenericKubernetesResourceMatcher.matcherFor(Deployment.class, - new KubernetesDependentResource<>(Deployment.class) { - @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; - } - }); + final var dependentResource = new TestDependentResource(desired); + final var matcher = + GenericKubernetesResourceMatcher.matcherFor(Deployment.class, dependentResource); 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); @@ -65,6 +53,21 @@ protected Deployment desired(HasMetadata primary, Context context) { assertThat(matcher.match(actual, null, context).matched()) .withFailMessage("Changed values are not ok") .isFalse(); + + actual = new DeploymentBuilder(createDeployment()) + .editOrNewMetadata() + .addToAnnotations("test", "value") + .endMetadata() + .build(); + assertThat(GenericKubernetesResourceMatcher + .match(dependentResource, actual, null, context, false).matched()) + .withFailMessage("Annotations shouldn't matter when metadata is not considered") + .isTrue(); + + assertThat(GenericKubernetesResourceMatcher + .match(dependentResource, actual, null, context, true).matched()) + .withFailMessage("Annotations should matter when metadata is considered") + .isFalse(); } Deployment createDeployment() { @@ -79,4 +82,27 @@ HasMetadata createPrimary(String caseName) { .endMetadata() .build(); } + + private class TestDependentResource extends KubernetesDependentResource { + + private final Deployment desired; + + public TestDependentResource(Deployment desired) { + super(Deployment.class); + this.desired = desired; + } + + @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; + } + } } 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 779fe032f9..2782ed52d1 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 @@ -39,7 +39,7 @@ public void registersEventSource() { Set registeredSources = eventSourceManager.getRegisteredEventSources(); assertThat(registeredSources).contains(eventSource); - verify(eventSource, times(1)).setEventHandler(eventSourceManager.getEventHandler()); + verify(eventSource, times(1)).setEventHandler(any()); } @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 ddd5ce583b..89dafa9fc4 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 @@ -133,7 +133,7 @@ void addFinalizerOnNewResource() { verify(reconciler, never()) .reconcile(ArgumentMatchers.eq(testCustomResource), any()); verify(customResourceFacade, times(1)) - .replaceResourceWithLock( + .updateResource( argThat(testCustomResource -> testCustomResource.hasFinalizer(DEFAULT_FINALIZER))); assertThat(testCustomResource.hasFinalizer(DEFAULT_FINALIZER)).isTrue(); } @@ -155,7 +155,7 @@ void updatesOnlyStatusSubResourceIfFinalizerSet() { reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); verify(customResourceFacade, times(1)).patchStatus(eq(testCustomResource), any()); - verify(customResourceFacade, never()).replaceResourceWithLock(any()); + verify(customResourceFacade, never()).updateResource(any()); } @Test @@ -163,12 +163,12 @@ void updatesBothResourceAndStatusIfFinalizerSet() { testCustomResource.addFinalizer(DEFAULT_FINALIZER); reconciler.reconcile = (r, c) -> UpdateControl.updateResourceAndStatus(testCustomResource); - when(customResourceFacade.replaceResourceWithLock(testCustomResource)) + when(customResourceFacade.updateResource(testCustomResource)) .thenReturn(testCustomResource); reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); - verify(customResourceFacade, times(1)).replaceResourceWithLock(testCustomResource); + verify(customResourceFacade, times(1)).updateResource(testCustomResource); verify(customResourceFacade, times(1)).updateStatus(testCustomResource); } @@ -182,7 +182,7 @@ void patchesStatus() { verify(customResourceFacade, times(1)).patchStatus(eq(testCustomResource), any()); verify(customResourceFacade, never()).updateStatus(any()); - verify(customResourceFacade, never()).replaceResourceWithLock(any()); + verify(customResourceFacade, never()).updateResource(any()); } @Test @@ -214,7 +214,7 @@ void removesDefaultFinalizerOnDeleteIfSet() { reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); assertThat(postExecControl.isFinalizerRemoved()).isTrue(); - verify(customResourceFacade, times(1)).replaceResourceWithLock(testCustomResource); + verify(customResourceFacade, times(1)).updateResource(testCustomResource); } @Test @@ -223,7 +223,7 @@ void retriesFinalizerRemovalWithFreshResource() { markForDeletion(testCustomResource); var resourceWithFinalizer = TestUtils.testCustomResource(); resourceWithFinalizer.addFinalizer(DEFAULT_FINALIZER); - when(customResourceFacade.replaceResourceWithLock(testCustomResource)) + when(customResourceFacade.updateResource(testCustomResource)) .thenThrow(new KubernetesClientException(null, 409, null)) .thenReturn(testCustomResource); when(customResourceFacade.getResource(any(), any())).thenReturn(resourceWithFinalizer); @@ -232,7 +232,7 @@ void retriesFinalizerRemovalWithFreshResource() { reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); assertThat(postExecControl.isFinalizerRemoved()).isTrue(); - verify(customResourceFacade, times(2)).replaceResourceWithLock(any()); + verify(customResourceFacade, times(2)).updateResource(any()); verify(customResourceFacade, times(1)).getResource(any(), any()); } @@ -240,7 +240,7 @@ void retriesFinalizerRemovalWithFreshResource() { void throwsExceptionIfFinalizerRemovalRetryExceeded() { testCustomResource.addFinalizer(DEFAULT_FINALIZER); markForDeletion(testCustomResource); - when(customResourceFacade.replaceResourceWithLock(any())) + when(customResourceFacade.updateResource(any())) .thenThrow(new KubernetesClientException(null, 409, null)); when(customResourceFacade.getResource(any(), any())) .thenAnswer((Answer) invocationOnMock -> createResourceWithFinalizer()); @@ -252,7 +252,7 @@ void throwsExceptionIfFinalizerRemovalRetryExceeded() { assertThat(postExecControl.getRuntimeException()).isPresent(); assertThat(postExecControl.getRuntimeException().get()) .isInstanceOf(OperatorException.class); - verify(customResourceFacade, times(MAX_FINALIZER_REMOVAL_RETRY)).replaceResourceWithLock(any()); + verify(customResourceFacade, times(MAX_FINALIZER_REMOVAL_RETRY)).updateResource(any()); verify(customResourceFacade, times(MAX_FINALIZER_REMOVAL_RETRY - 1)).getResource(any(), any()); } @@ -261,7 +261,7 @@ void throwsExceptionIfFinalizerRemovalRetryExceeded() { void throwsExceptionIfFinalizerRemovalClientExceptionIsNotConflict() { testCustomResource.addFinalizer(DEFAULT_FINALIZER); markForDeletion(testCustomResource); - when(customResourceFacade.replaceResourceWithLock(any())) + when(customResourceFacade.updateResource(any())) .thenThrow(new KubernetesClientException(null, 400, null)); var res = @@ -269,7 +269,7 @@ void throwsExceptionIfFinalizerRemovalClientExceptionIsNotConflict() { assertThat(res.getRuntimeException()).isPresent(); assertThat(res.getRuntimeException().get()).isInstanceOf(KubernetesClientException.class); - verify(customResourceFacade, times(1)).replaceResourceWithLock(any()); + verify(customResourceFacade, times(1)).updateResource(any()); verify(customResourceFacade, never()).getResource(any(), any()); } @@ -315,7 +315,7 @@ void doesNotRemovesTheSetFinalizerIfTheDeleteNotMethodInstructsIt() { reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); assertEquals(1, testCustomResource.getMetadata().getFinalizers().size()); - verify(customResourceFacade, never()).replaceResourceWithLock(any()); + verify(customResourceFacade, never()).updateResource(any()); } @Test @@ -325,7 +325,7 @@ void doesNotUpdateTheResourceIfNoUpdateUpdateControlIfFinalizerSet() { reconciler.reconcile = (r, c) -> UpdateControl.noUpdate(); reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); - verify(customResourceFacade, never()).replaceResourceWithLock(any()); + verify(customResourceFacade, never()).updateResource(any()); verify(customResourceFacade, never()).updateStatus(testCustomResource); } @@ -333,13 +333,13 @@ void doesNotUpdateTheResourceIfNoUpdateUpdateControlIfFinalizerSet() { void addsFinalizerIfNotMarkedForDeletionAndEmptyCustomResourceReturned() { removeFinalizers(testCustomResource); reconciler.reconcile = (r, c) -> UpdateControl.noUpdate(); - when(customResourceFacade.replaceResourceWithLock(any())).thenReturn(testCustomResource); + when(customResourceFacade.updateResource(any())).thenReturn(testCustomResource); var postExecControl = reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); assertEquals(1, testCustomResource.getMetadata().getFinalizers().size()); - verify(customResourceFacade, times(1)).replaceResourceWithLock(any()); + verify(customResourceFacade, times(1)).updateResource(any()); assertThat(postExecControl.updateIsStatusPatch()).isFalse(); assertThat(postExecControl.getUpdatedCustomResource()).isPresent(); } @@ -351,7 +351,7 @@ void doesNotCallDeleteIfMarkedForDeletionButNotOurFinalizer() { reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); - verify(customResourceFacade, never()).replaceResourceWithLock(any()); + verify(customResourceFacade, never()).updateResource(any()); verify(reconciler, never()).cleanup(eq(testCustomResource), any()); } @@ -475,7 +475,7 @@ void updateObservedGenerationOnCustomResourceUpdate() throws Exception { when(config.isGenerationAware()).thenReturn(true); when(reconciler.reconcile(any(), any())) .thenReturn(UpdateControl.updateResource(observedGenResource)); - when(facade.replaceResourceWithLock(any())).thenReturn(observedGenResource); + when(facade.updateResource(any())).thenReturn(observedGenResource); when(facade.updateStatus(observedGenResource)).thenReturn(observedGenResource); var dispatcher = init(observedGenResource, reconciler, config, facade, true); 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 deleted file mode 100644 index f0f0bb0e62..0000000000 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/CustomResourceSelectorTest.java +++ /dev/null @@ -1,167 +0,0 @@ -package io.javaoperatorsdk.operator.processing.event.source; - -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; -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.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; -import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; -import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; - -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) -class CustomResourceSelectorTest { - - private static final Logger LOGGER = LoggerFactory.getLogger(CustomResourceSelectorTest.class); - public static final String NAMESPACE = "test"; - - KubernetesMockServer server; - KubernetesClient client; - ConfigurationService configurationService; - - @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())); - // make sure not the same config instance is used for the controller, so rate limiter is not - // shared - when(configurationService.getConfigurationFor(any(MyController.class))) - .thenReturn(new MyConfiguration()) - .thenReturn(new MyConfiguration()); - } - - @Test - void resourceWatchedByLabel() { - assertThat(server).isNotNull(); - assertThat(client).isNotNull(); - - 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(); - 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(); - } - }), - (overrider) -> overrider.settingNamespace(NAMESPACE).withLabelSelector("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(); - } - }), - (overrider) -> overrider.settingNamespace(NAMESPACE).withLabelSelector("app=bar")); - o2.start(); - - 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) - .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)))); - } finally { - o1.stop(); - o2.stop(); - } - - } - - 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; - } - - public static class MyConfiguration extends DefaultControllerConfiguration { - - public MyConfiguration() { - super(MyController.class.getCanonicalName(), "mycontroller", null, Constants.NO_VALUE_SET, - false, null, - null, null, null, TestCustomResource.class, null, null, null, null, - null, null); - } - } - - @ControllerConfiguration(namespaces = NAMESPACE) - public static class MyController implements Reconciler { - - private final Consumer consumer; - - public MyController(Consumer consumer) { - this.consumer = consumer; - } - - @Override - public UpdateControl reconcile( - TestCustomResource resource, Context context) { - - LOGGER.info("Received event on: {}", resource); - - consumer.accept(resource); - // patch status now increases generation, this seems to be an issue with the mock server - return UpdateControl.updateStatus(resource); - } - } -} 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 623705b609..67e9d1fca0 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 @@ -9,8 +9,7 @@ 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.AnyNamespaceOperation; import io.fabric8.kubernetes.client.dsl.MixedOperation; import io.fabric8.kubernetes.client.informers.SharedIndexInformer; import io.fabric8.kubernetes.client.informers.cache.Indexer; @@ -37,10 +36,10 @@ class InformerEventSourceTest { mock(TemporaryResourceCache.class); private final EventHandler eventHandlerMock = mock(EventHandler.class); private final MixedOperation crClientMock = mock(MixedOperation.class); - private final FilterWatchListMultiDeletable specificResourceClientMock = - mock(FilterWatchListMultiDeletable.class); - private final FilterWatchListDeletable labeledResourceClientMock = - mock(FilterWatchListDeletable.class); + private final AnyNamespaceOperation specificResourceClientMock = + mock(AnyNamespaceOperation.class); + private final AnyNamespaceOperation labeledResourceClientMock = + mock(AnyNamespaceOperation.class); private final SharedIndexInformer informer = mock(SharedIndexInformer.class); private final InformerConfiguration informerConfiguration = mock(InformerConfiguration.class); 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 3413b620b3..5800c273e7 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 @@ -38,13 +38,13 @@ public TestCustomReconciler(KubernetesClient kubernetesClient, boolean updateSta @Override public DeleteControl cleanup( TestCustomResource resource, Context context) { - Boolean delete = + var statusDetails = kubernetesClient .configMaps() .inNamespace(resource.getMetadata().getNamespace()) .withName(resource.getSpec().getConfigMapName()) .delete(); - if (delete) { + if (statusDetails.size() == 1 && statusDetails.get(0).getCauses().isEmpty()) { log.info( "Deleted ConfigMap {} for resource: {}", resource.getSpec().getConfigMapName(), diff --git a/operator-framework-junit5/pom.xml b/operator-framework-junit5/pom.xml index 0389dcfa97..72f512f2b5 100644 --- a/operator-framework-junit5/pom.xml +++ b/operator-framework-junit5/pom.xml @@ -5,7 +5,7 @@ java-operator-sdk io.javaoperatorsdk - 3.1.2-SNAPSHOT + 3.2.0-SNAPSHOT 4.0.0 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 1a2ff991c2..0598cfc198 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 @@ -16,8 +16,8 @@ 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.KubernetesClientBuilder; import io.fabric8.kubernetes.client.dsl.NonNamespaceOperation; import io.fabric8.kubernetes.client.dsl.Resource; import io.fabric8.kubernetes.client.utils.KubernetesResourceUtil; @@ -52,7 +52,7 @@ protected AbstractOperatorExtension( boolean preserveNamespaceOnError, boolean waitForNamespaceDeletion) { - this.kubernetesClient = new DefaultKubernetesClient(); + this.kubernetesClient = new KubernetesClientBuilder().build(); this.configurationService = configurationService; this.infrastructure = infrastructure; this.infrastructureTimeout = infrastructureTimeout; @@ -100,17 +100,33 @@ public T get(Class type, String name) { return kubernetesClient.resources(type).inNamespace(namespace).withName(name).get(); } + public T create(T resource) { + return kubernetesClient.resource(resource).inNamespace(namespace).create(); + } + + @Deprecated(forRemoval = true) public T create(Class type, T resource) { - return kubernetesClient.resources(type).inNamespace(namespace).create(resource); + return create(resource); + } + + public T replace(T resource) { + return kubernetesClient.resource(resource).inNamespace(namespace).replace(); } + @Deprecated(forRemoval = true) public T replace(Class type, T resource) { - return kubernetesClient.resources(type).inNamespace(namespace).replace(resource); + return replace(resource); + } + + public boolean delete(T resource) { + var res = kubernetesClient.resource(resource).inNamespace(namespace).delete(); + return res.size() == 1 && res.get(0).getCauses().isEmpty(); } + @Deprecated(forRemoval = true) @SuppressWarnings("unchecked") public boolean delete(Class type, T resource) { - return kubernetesClient.resources(type).inNamespace(namespace).delete(resource); + return delete(resource); } protected void beforeAllImpl(ExtensionContext context) { @@ -144,7 +160,9 @@ protected void before(ExtensionContext context) { kubernetesClient .namespaces() - .create(new NamespaceBuilder().withNewMetadata().withName(namespace).endMetadata().build()); + .resource( + new NamespaceBuilder().withNewMetadata().withName(namespace).endMetadata().build()) + .create(); kubernetesClient .resourceList(infrastructure) diff --git a/operator-framework/pom.xml b/operator-framework/pom.xml index 68d4f25909..05e09d708e 100644 --- a/operator-framework/pom.xml +++ b/operator-framework/pom.xml @@ -5,7 +5,7 @@ java-operator-sdk io.javaoperatorsdk - 3.1.2-SNAPSHOT + 3.2.0-SNAPSHOT 4.0.0 @@ -61,6 +61,11 @@ crd-generator-apt test + + io.fabric8 + crd-generator-api + test + org.apache.logging.log4j log4j-slf4j-impl 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 e1e5e83fa7..47eee2a249 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,7 +1,5 @@ package io.javaoperatorsdk.operator.config.runtime; -import java.util.*; - import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/ChangeNamespaceIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/ChangeNamespaceIT.java index a6a228bc5e..56eda890a1 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/ChangeNamespaceIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/ChangeNamespaceIT.java @@ -35,8 +35,7 @@ class ChangeNamespaceIT { void addNewAndRemoveOldNamespaceTest() { try { var reconciler = operator.getReconcilerOfType(ChangeNamespaceTestReconciler.class); - var defaultNamespaceResource = operator.create(ChangeNamespaceTestCustomResource.class, - customResource(TEST_RESOURCE_NAME_1)); + var defaultNamespaceResource = operator.create(customResource(TEST_RESOURCE_NAME_1)); await().pollDelay(Duration.ofMillis(100)).untilAsserted(() -> assertThat( reconciler.numberOfResourceReconciliations(defaultNamespaceResource)).isEqualTo(2)); @@ -63,8 +62,7 @@ void addNewAndRemoveOldNamespaceTest() { // removing a namespace registeredController.changeNamespaces(Set.of(ADDITIONAL_TEST_NAMESPACE)); - var newResourceInDefaultNamespace = operator.create(ChangeNamespaceTestCustomResource.class, - customResource(TEST_RESOURCE_NAME_3)); + var newResourceInDefaultNamespace = operator.create(customResource(TEST_RESOURCE_NAME_3)); await().pollDelay(Duration.ofMillis(200)) .untilAsserted(() -> assertThat( reconciler.numberOfResourceReconciliations(newResourceInDefaultNamespace)).isZero()); @@ -72,7 +70,7 @@ void addNewAndRemoveOldNamespaceTest() { ConfigMap firstMap = operator.get(ConfigMap.class, TEST_RESOURCE_NAME_1); firstMap.setData(Map.of("data", "newdata")); - operator.replace(ConfigMap.class, firstMap); + operator.replace(firstMap); await().untilAsserted(() -> assertThat( reconciler.numberOfResourceReconciliations(defaultNamespaceResource)).isEqualTo(2)); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/CleanerForReconcilerIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/CleanerForReconcilerIT.java index 65a828fd92..a3cfde8207 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/CleanerForReconcilerIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/CleanerForReconcilerIT.java @@ -24,12 +24,12 @@ class CleanerForReconcilerIT { @Test void addsFinalizerAndCallsCleanupIfCleanerImplemented() { var testResource = createTestResource(); - operator.create(CleanerForReconcilerCustomResource.class, testResource); + operator.create(testResource); await().until(() -> !operator.get(CleanerForReconcilerCustomResource.class, TEST_RESOURCE_NAME) .getMetadata().getFinalizers().isEmpty()); - operator.delete(CleanerForReconcilerCustomResource.class, testResource); + operator.delete(testResource); await().until( () -> operator.get(CleanerForReconcilerCustomResource.class, TEST_RESOURCE_NAME) == null); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/CleanupConflictIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/CleanupConflictIT.java index d904698041..e1c571c4a5 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/CleanupConflictIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/CleanupConflictIT.java @@ -28,18 +28,18 @@ class CleanupConflictIT { void cleanupRemovesFinalizerWithoutConflict() throws InterruptedException { var testResource = createTestResource(); testResource.addFinalizer(ADDITIONAL_FINALIZER); - testResource = operator.create(CleanupConflictCustomResource.class, testResource); + testResource = operator.create(testResource); await().untilAsserted( () -> assertThat(operator.getReconcilerOfType(CleanupConflictReconciler.class) .getNumberReconcileExecutions()).isEqualTo(1)); - operator.delete(CleanupConflictCustomResource.class, testResource); + operator.delete(testResource); Thread.sleep(WAIT_TIME / 2); testResource = operator.get(CleanupConflictCustomResource.class, TEST_RESOURCE_NAME); testResource.getMetadata().getFinalizers().remove(ADDITIONAL_FINALIZER); testResource.getMetadata().setResourceVersion(null); - operator.replace(CleanupConflictCustomResource.class, testResource); + operator.replace(testResource); await().pollDelay(Duration.ofMillis(WAIT_TIME * 2)).untilAsserted( () -> assertThat(operator.getReconcilerOfType(CleanupConflictReconciler.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 9bcea57f58..31eabc0292 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/ControllerExecutionIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/ControllerExecutionIT.java @@ -26,7 +26,7 @@ void configMapGetsCreatedForTestCustomResource() { operator.getReconcilerOfType(TestReconciler.class).setUpdateStatus(true); TestCustomResource resource = TestUtils.testCustomResource(); - operator.create(TestCustomResource.class, resource); + operator.create(resource); awaitResourcesCreatedOrUpdated(); awaitStatusUpdated(); @@ -39,7 +39,7 @@ void patchesStatusForTestCustomResource() { operator.getReconcilerOfType(TestReconciler.class).setUpdateStatus(true); TestCustomResource resource = TestUtils.testCustomResource(); - operator.create(TestCustomResource.class, resource); + operator.create(resource); awaitStatusUpdated(); } @@ -49,7 +49,7 @@ void eventIsSkippedChangedOnMetadataOnlyUpdate() { operator.getReconcilerOfType(TestReconciler.class).setUpdateStatus(false); TestCustomResource resource = TestUtils.testCustomResource(); - operator.create(TestCustomResource.class, resource); + operator.create(resource); awaitResourcesCreatedOrUpdated(); assertThat(TestUtils.getNumberOfExecutions(operator)).isEqualTo(1); @@ -60,11 +60,11 @@ void cleanupExecuted() { operator.getReconcilerOfType(TestReconciler.class).setUpdateStatus(true); TestCustomResource resource = TestUtils.testCustomResource(); - resource = operator.create(TestCustomResource.class, resource); + resource = operator.create(resource); awaitResourcesCreatedOrUpdated(); awaitStatusUpdated(); - operator.delete(TestCustomResource.class, resource); + operator.delete(resource); await().atMost(Duration.ofSeconds(1)) .until(() -> ((TestReconciler) operator.getFirstReconciler()) diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/CreateUpdateInformerEventSourceEventFilterIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/CreateUpdateInformerEventSourceEventFilterIT.java index 75310157f4..f319509c47 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/CreateUpdateInformerEventSourceEventFilterIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/CreateUpdateInformerEventSourceEventFilterIT.java @@ -28,7 +28,7 @@ class CreateUpdateInformerEventSourceEventFilterIT { void updateEventNotReceivedAfterCreateOrUpdate() { CreateUpdateEventFilterTestCustomResource resource = prepareTestResource(); var createdResource = - operator.create(CreateUpdateEventFilterTestCustomResource.class, resource); + operator.create(resource); await() .atMost(Duration.ofSeconds(1)) @@ -53,7 +53,7 @@ void updateEventNotReceivedAfterCreateOrUpdate() { operator.get(CreateUpdateEventFilterTestCustomResource.class, resource.getMetadata().getName()); actualCreatedResource.getSpec().setValue("2"); - operator.replace(CreateUpdateEventFilterTestCustomResource.class, actualCreatedResource); + operator.replace(actualCreatedResource); await().atMost(Duration.ofSeconds(1)) 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 1d45c808ac..6733abaa47 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/CustomResourceFilterIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/CustomResourceFilterIT.java @@ -23,9 +23,9 @@ 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); + operator.create(filtered1); + operator.create(filtered2); + operator.create(notFiltered); Thread.sleep(300); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/DeleterForManagedDependentResourcesOnlyIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/DeleterForManagedDependentResourcesOnlyIT.java index 22e1b0f2be..66db8ecb5e 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/DeleterForManagedDependentResourcesOnlyIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/DeleterForManagedDependentResourcesOnlyIT.java @@ -26,13 +26,13 @@ class DeleterForManagedDependentResourcesOnlyIT { @Test void addsFinalizerAndCallsCleanupIfCleanerImplemented() { var testResource = createTestResource(); - operator.create(CleanerForManagedDependentCustomResource.class, testResource); + operator.create(testResource); await().until( () -> !operator.get(CleanerForManagedDependentCustomResource.class, TEST_RESOURCE_NAME) .getMetadata().getFinalizers().isEmpty()); - operator.delete(CleanerForManagedDependentCustomResource.class, testResource); + operator.delete(testResource); await().until( () -> operator.get(CleanerForManagedDependentCustomResource.class, diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/DependentAnnotationSecondaryMapperIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/DependentAnnotationSecondaryMapperIT.java index ddc61a8590..b5b01e1530 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/DependentAnnotationSecondaryMapperIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/DependentAnnotationSecondaryMapperIT.java @@ -28,7 +28,7 @@ class DependentAnnotationSecondaryMapperIT { @Test void mapsSecondaryByAnnotation() { - operator.create(DependentAnnotationSecondaryMapperResource.class, testResource()); + operator.create(testResource()); var reconciler = operator.getReconcilerOfType(DependentAnnotationSecondaryMapperReconciler.class); @@ -46,7 +46,7 @@ void mapsSecondaryByAnnotation() { assertThat(configMap.getMetadata().getOwnerReferences()).isEmpty(); configMap.getData().put("additional_data", "data"); - operator.replace(ConfigMap.class, configMap); + operator.replace(configMap); await().pollDelay(Duration.ofMillis(150)) .untilAsserted(() -> assertThat(reconciler.getNumberOfExecutions()).isEqualTo(2)); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/DependentFilterIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/DependentFilterIT.java index a900d0645d..19d6678a59 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/DependentFilterIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/DependentFilterIT.java @@ -30,7 +30,7 @@ class DependentFilterIT { @Test void filtersUpdateOnConfigMap() { var resource = createResource(); - operator.create(DependentFilterTestCustomResource.class, resource); + operator.create(resource); await().pollDelay(Duration.ofMillis(150)).untilAsserted(() -> { assertThat(operator.getReconcilerOfType(DependentFilterTestReconciler.class) @@ -39,7 +39,7 @@ void filtersUpdateOnConfigMap() { var configMap = operator.get(ConfigMap.class, RESOURCE_NAME); configMap.setData(Map.of(CM_VALUE_KEY, CONFIG_MAP_FILTER_VALUE)); - operator.replace(ConfigMap.class, configMap); + operator.replace(configMap); await().pollDelay(Duration.ofMillis(150)).untilAsserted(() -> { assertThat(operator.getReconcilerOfType(DependentFilterTestReconciler.class) diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/DependentOperationEventFilterIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/DependentOperationEventFilterIT.java index 6dd8164952..1d0c091e16 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/DependentOperationEventFilterIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/DependentOperationEventFilterIT.java @@ -31,7 +31,7 @@ class DependentOperationEventFilterIT { @Test void reconcileNotTriggeredWithDependentResourceCreateOrUpdate() { var resource = - operator.create(DependentOperationEventFilterCustomResource.class, createTestResource()); + operator.create(createTestResource()); await().pollDelay(Duration.ofSeconds(1)).atMost(Duration.ofSeconds(3)) .until( @@ -42,7 +42,7 @@ void reconcileNotTriggeredWithDependentResourceCreateOrUpdate() { .containsEntry(ConfigMapDependentResource.KEY, SPEC_VAL_1); resource.getSpec().setValue(SPEC_VAL_2); - operator.replace(DependentOperationEventFilterCustomResource.class, resource); + operator.replace(resource); await().pollDelay(Duration.ofSeconds(1)).atMost(Duration.ofSeconds(3)) .until( diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/DependentResourceCrossRefIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/DependentResourceCrossRefIT.java index fc4118f909..912b5b8ce7 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/DependentResourceCrossRefIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/DependentResourceCrossRefIT.java @@ -29,7 +29,7 @@ class DependentResourceCrossRefIT { @Test void dependentResourceCanReferenceEachOther() { for (int i = 0; i < EXECUTION_NUMBER; i++) { - operator.create(DependentResourceCrossRefResource.class, testResource(i)); + operator.create(testResource(i)); } await() .pollDelay(Duration.ofMillis(150)) 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 529b34a7f5..e66fd2dcce 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/ErrorStatusHandlerIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/ErrorStatusHandlerIT.java @@ -29,7 +29,7 @@ class ErrorStatusHandlerIT { @Test void testErrorMessageSetEventually() { ErrorStatusHandlerTestCustomResource resource = - operator.create(ErrorStatusHandlerTestCustomResource.class, createCustomResource()); + operator.create(createCustomResource()); await() .atMost(10, TimeUnit.SECONDS) 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 b4ff4fc04d..a85543030b 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/EventSourceIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/EventSourceIT.java @@ -25,7 +25,7 @@ class EventSourceIT { void receivingPeriodicEvents() { EventSourceTestCustomResource resource = createTestCustomResource("1"); - operator.create(EventSourceTestCustomResource.class, resource); + operator.create(resource); await() .atMost(5, TimeUnit.SECONDS) diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/FilterIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/FilterIT.java index 2dea399448..28ce794de9 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/FilterIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/FilterIT.java @@ -27,14 +27,14 @@ class FilterIT { @Test void filtersControllerResourceUpdate() { - var res = operator.create(FilterTestCustomResource.class, createResource()); + var res = operator.create(createResource()); // One for CR create event other for ConfigMap event await().pollDelay(Duration.ofMillis(POLL_DELAY)) .untilAsserted(() -> assertThat(operator.getReconcilerOfType(FilterTestReconciler.class) .getNumberOfExecutions()).isEqualTo(2)); res.getSpec().setValue(FilterTestReconciler.CUSTOM_RESOURCE_FILTER_VALUE); - operator.replace(FilterTestCustomResource.class, res); + operator.replace(res); // not more reconciliation with the filtered value await().pollDelay(Duration.ofMillis(POLL_DELAY)) @@ -44,14 +44,14 @@ void filtersControllerResourceUpdate() { @Test void filtersSecondaryResourceUpdate() { - var res = operator.create(FilterTestCustomResource.class, createResource()); + var res = operator.create(createResource()); // One for CR create event other for ConfigMap event await().pollDelay(Duration.ofMillis(POLL_DELAY)) .untilAsserted(() -> assertThat(operator.getReconcilerOfType(FilterTestReconciler.class) .getNumberOfExecutions()).isEqualTo(2)); res.getSpec().setValue(CONFIG_MAP_FILTER_VALUE); - operator.replace(FilterTestCustomResource.class, res); + operator.replace(res); // the CM event filtered out await().pollDelay(Duration.ofMillis(POLL_DELAY)) 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 8f05b4b124..45983145da 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/InformerEventSourceIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/InformerEventSourceIT.java @@ -34,13 +34,13 @@ class InformerEventSourceIT { @Test void testUsingInformerToWatchChangesOfConfigMap() { var customResource = initialCustomResource(); - customResource = operator.create(InformerEventSourceTestCustomResource.class, customResource); + customResource = operator.create(customResource); ConfigMap configMap = - operator.create(ConfigMap.class, relatedConfigMap(customResource.getMetadata().getName())); + operator.create(relatedConfigMap(customResource.getMetadata().getName())); waitForCRStatusValue(INITIAL_STATUS_MESSAGE); configMap.getData().put(TARGET_CONFIG_MAP_KEY, UPDATE_STATUS_MESSAGE); - operator.replace(ConfigMap.class, configMap); + operator.replace(configMap); waitForCRStatusValue(UPDATE_STATUS_MESSAGE); } @@ -48,13 +48,13 @@ void testUsingInformerToWatchChangesOfConfigMap() { @Test void deletingSecondaryResource() { var customResource = initialCustomResource(); - customResource = operator.create(InformerEventSourceTestCustomResource.class, customResource); + customResource = operator.create(customResource); waitForCRStatusValue(MISSING_CONFIG_MAP); ConfigMap configMap = - operator.create(ConfigMap.class, relatedConfigMap(customResource.getMetadata().getName())); + operator.create(relatedConfigMap(customResource.getMetadata().getName())); waitForCRStatusValue(INITIAL_STATUS_MESSAGE); - boolean res = operator.delete(ConfigMap.class, configMap); + boolean res = operator.delete(configMap); if (!res) { fail("Unable to delete configmap"); } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/KubernetesDependentGarbageCollectionIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/KubernetesDependentGarbageCollectionIT.java index a524089009..dc3f080ab7 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/KubernetesDependentGarbageCollectionIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/KubernetesDependentGarbageCollectionIT.java @@ -29,7 +29,7 @@ class KubernetesDependentGarbageCollectionIT { void resourceSecondaryResourceIsGarbageCollected() { var resource = customResource(); var createdResources = - operator.create(DependentGarbageCollectionTestCustomResource.class, resource); + operator.create(resource); await().untilAsserted(() -> { ConfigMap configMap = operator.get(ConfigMap.class, TEST_RESOURCE_NAME); @@ -41,7 +41,7 @@ void resourceSecondaryResourceIsGarbageCollected() { assertThat(configMap.getMetadata().getOwnerReferences().get(0).getName()) .isEqualTo(TEST_RESOURCE_NAME); - operator.delete(DependentGarbageCollectionTestCustomResource.class, createdResources); + operator.delete(createdResources); await().atMost(Duration.ofSeconds(30)).untilAsserted(() -> { ConfigMap cm = operator.get(ConfigMap.class, TEST_RESOURCE_NAME); @@ -53,7 +53,7 @@ void resourceSecondaryResourceIsGarbageCollected() { void deletesSecondaryResource() { var resource = customResource(); var createdResources = - operator.create(DependentGarbageCollectionTestCustomResource.class, resource); + operator.create(resource); await().untilAsserted(() -> { ConfigMap configMap = operator.get(ConfigMap.class, TEST_RESOURCE_NAME); @@ -61,7 +61,7 @@ void deletesSecondaryResource() { }); createdResources.getSpec().setCreateConfigMap(false); - operator.replace(DependentGarbageCollectionTestCustomResource.class, createdResources); + operator.replace(createdResources); await().untilAsserted(() -> { ConfigMap cm = operator.get(ConfigMap.class, TEST_RESOURCE_NAME); 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 10541437ae..02cd982af9 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/KubernetesResourceStatusUpdateIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/KubernetesResourceStatusUpdateIT.java @@ -32,7 +32,7 @@ class KubernetesResourceStatusUpdateIT { @Test void testReconciliationOfNonCustomResourceAndStatusUpdate() { - var deployment = operator.create(Deployment.class, testDeployment()); + var deployment = operator.create(testDeployment()); await().atMost(120, TimeUnit.SECONDS).untilAsserted(() -> { var d = operator.get(Deployment.class, deployment.getMetadata().getName()); assertThat(d.getStatus()).isNotNull(); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/LabelSelectorIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/LabelSelectorIT.java new file mode 100644 index 0000000000..8324a8c1d2 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/LabelSelectorIT.java @@ -0,0 +1,50 @@ +package io.javaoperatorsdk.operator; + +import java.time.Duration; +import java.util.Collections; +import java.util.Map; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; +import io.javaoperatorsdk.operator.sample.labelselector.LabelSelectorTestCustomResource; +import io.javaoperatorsdk.operator.sample.labelselector.LabelSelectorTestReconciler; + +import static io.javaoperatorsdk.operator.sample.labelselector.LabelSelectorTestReconciler.LABEL_KEY; +import static io.javaoperatorsdk.operator.sample.labelselector.LabelSelectorTestReconciler.LABEL_VALUE; +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +class LabelSelectorIT { + + @RegisterExtension + LocallyRunOperatorExtension operator = + LocallyRunOperatorExtension.builder().withReconciler(new LabelSelectorTestReconciler()) + .build(); + + + @Test + void filtersCustomResourceByLabel() { + operator.create(resource("r1", true)); + operator.create(resource("r2", false)); + + await().pollDelay(Duration.ofMillis(150)).untilAsserted(() -> { + assertThat( + operator.getReconcilerOfType(LabelSelectorTestReconciler.class).getNumberOfExecutions()) + .isEqualTo(1); + }); + } + + LabelSelectorTestCustomResource resource(String name, boolean addLabel) { + var res = new LabelSelectorTestCustomResource(); + res.setMetadata(new ObjectMetaBuilder() + .withName(name) + .withLabels(addLabel ? Map.of(LABEL_KEY, LABEL_VALUE) + : Collections.emptyMap()) + .build()); + return res; + } + +} 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 767219ca70..e817fa8d1f 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/MaxIntervalIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/MaxIntervalIT.java @@ -22,7 +22,7 @@ class MaxIntervalIT { void reconciliationTriggeredBasedOnMaxInterval() { MaxIntervalTestCustomResource cr = createTestResource(); - operator.create(MaxIntervalTestCustomResource.class, cr); + operator.create(cr); await() .pollInterval(50, TimeUnit.MILLISECONDS) 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 36533ae661..65bf839ff4 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/MultiVersionCRDIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/MultiVersionCRDIT.java @@ -15,7 +15,7 @@ import io.javaoperatorsdk.operator.sample.multiversioncrd.MultiVersionCRDTestReconciler1; import io.javaoperatorsdk.operator.sample.multiversioncrd.MultiVersionCRDTestReconciler2; -import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static com.google.common.truth.Truth.assertThat; import static org.awaitility.Awaitility.await; class MultiVersionCRDIT { @@ -32,22 +32,22 @@ class MultiVersionCRDIT { @Test void multipleCRDVersions() { - operator.create(MultiVersionCRDTestCustomResource1.class, createTestResourceV1WithoutLabel()); - operator.create(MultiVersionCRDTestCustomResource2.class, createTestResourceV2WithLabel()); + operator.create(createTestResourceV1WithoutLabel()); + operator.create(createTestResourceV2WithLabel()); await() .atMost(Duration.ofSeconds(2)) .pollInterval(Duration.ofMillis(50)) - .until( + .untilAsserted( () -> { 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()); + assertThat(crV1Now.getStatus()).isNotNull(); + assertThat(crV2Now.getStatus()).isNotNull(); + assertThat(crV1Now.getStatus().getReconciledBy()) + .containsExactly(MultiVersionCRDTestReconciler1.class.getSimpleName()); + assertThat(crV2Now.getStatus().getReconciledBy()) + .containsExactly(MultiVersionCRDTestReconciler2.class.getSimpleName()); }); } @@ -55,17 +55,18 @@ void multipleCRDVersions() { void invalidEventsDoesNotBreakEventHandling() { var v2res = createTestResourceV2WithLabel(); v2res.getMetadata().getLabels().clear(); - operator.create(MultiVersionCRDTestCustomResource2.class, v2res); + operator.create(v2res); var v1res = createTestResourceV1WithoutLabel(); - operator.create(MultiVersionCRDTestCustomResource1.class, v1res); + operator.create(v1res); await() .atMost(Duration.ofSeconds(2)) .pollInterval(Duration.ofMillis(50)) - .until(() -> { + .untilAsserted(() -> { var crV1Now = operator.get(MultiVersionCRDTestCustomResource1.class, CR_V1_NAME); - return crV1Now.getStatus().getReconciledBy() - .contains(MultiVersionCRDTestReconciler1.class.getSimpleName()); + assertThat(crV1Now.getStatus()).isNotNull(); + assertThat(crV1Now.getStatus().getReconciledBy()) + .containsExactly(MultiVersionCRDTestReconciler1.class.getSimpleName()); }); assertThat( operator diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/MultipleDependentResourceIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/MultipleDependentResourceIT.java index 8dbe73da68..d3e7f77fd5 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/MultipleDependentResourceIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/MultipleDependentResourceIT.java @@ -29,7 +29,7 @@ class MultipleDependentResourceIT { @Test void twoConfigMapsHaveBeenCreated() { MultipleDependentResourceCustomResource customResource = createTestCustomResource(); - operator.create(MultipleDependentResourceCustomResource.class, customResource); + operator.create(customResource); var reconciler = operator.getReconcilerOfType(MultipleDependentResourceReconciler.class); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/MultipleSecondaryEventSourceIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/MultipleSecondaryEventSourceIT.java index 278547d303..85be323379 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/MultipleSecondaryEventSourceIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/MultipleSecondaryEventSourceIT.java @@ -26,7 +26,7 @@ class MultipleSecondaryEventSourceIT { void receivingPeriodicEvents() { MultipleSecondaryEventSourceCustomResource resource = createTestCustomResource(); - operator.create(MultipleSecondaryEventSourceCustomResource.class, resource); + operator.create(resource); var reconciler = operator.getReconcilerOfType(MultipleSecondaryEventSourceReconciler.class); @@ -51,7 +51,7 @@ private void updateConfigMap(MultipleSecondaryEventSourceCustomResource resource number == 1 ? MultipleSecondaryEventSourceReconciler.getName1(resource) : MultipleSecondaryEventSourceReconciler.getName2(resource)); map1.getData().put("value2", "value2"); - operator.replace(ConfigMap.class, map1); + operator.replace(map1); } public MultipleSecondaryEventSourceCustomResource createTestCustomResource() { 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 edf89112f8..0e28ddec4c 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/ObservedGenerationHandlingIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/ObservedGenerationHandlingIT.java @@ -25,7 +25,7 @@ void testReconciliationOfNonCustomResourceAndStatusUpdate() { resource.setMetadata(new ObjectMeta()); resource.getMetadata().setName("observed-gen1"); - var createdResource = operator.create(ObservedGenerationTestCustomResource.class, resource); + var createdResource = operator.create(resource); await().atMost(10, TimeUnit.SECONDS).untilAsserted(() -> { var d = operator.get(ObservedGenerationTestCustomResource.class, diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/OrderedManagedDependentIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/OrderedManagedDependentIT.java index 3292dd3d92..e7bb8fdd14 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/OrderedManagedDependentIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/OrderedManagedDependentIT.java @@ -25,7 +25,7 @@ class OrderedManagedDependentIT { @Test void managedDependentsAreReconciledInOrder() { - operator.create(OrderedManagedDependentCustomResource.class, createTestResource()); + operator.create(createTestResource()); await().pollDelay(Duration.ofSeconds(1)).atMost(Duration.ofSeconds(5)) .until(() -> ((OrderedManagedDependentTestReconciler) operator.getFirstReconciler()) diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/PrimaryIndexerIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/PrimaryIndexerIT.java index e4bdd14d51..16f223ce38 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/PrimaryIndexerIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/PrimaryIndexerIT.java @@ -33,8 +33,8 @@ protected LocallyRunOperatorExtension buildOperator() { @Test void changesToSecondaryResourcesCorrectlyTriggerReconciler() { var reconciler = (AbstractPrimaryIndexerTestReconciler) operator.getFirstReconciler(); - operator.create(PrimaryIndexerTestCustomResource.class, createTestResource(RESOURCE_NAME1)); - operator.create(PrimaryIndexerTestCustomResource.class, createTestResource(RESOURCE_NAME2)); + operator.create(createTestResource(RESOURCE_NAME1)); + operator.create(createTestResource(RESOURCE_NAME2)); await() .pollDelay(Duration.ofMillis(500)) @@ -44,7 +44,7 @@ void changesToSecondaryResourcesCorrectlyTriggerReconciler() { assertThat(reconciler.getNumberOfExecutions().get(RESOURCE_NAME2).get()).isEqualTo(1); }); - operator.create(ConfigMap.class, configMap()); + operator.create(configMap()); await() .pollDelay(Duration.ofMillis(500)) diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/PrimaryToSecondaryIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/PrimaryToSecondaryIT.java index ebb7c3bee5..38cd613d5a 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/PrimaryToSecondaryIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/PrimaryToSecondaryIT.java @@ -29,9 +29,9 @@ class PrimaryToSecondaryIT { @Test void readsSecondaryInManyToOneCases() throws InterruptedException { - operator.create(Cluster.class, cluster()); + operator.create(cluster()); Thread.sleep(MIN_DELAY); - operator.create(Job.class, job()); + operator.create(job()); await().pollDelay(Duration.ofMillis(300)).untilAsserted( () -> assertThat(operator.getReconcilerOfType(JobReconciler.class).getNumberOfExecutions()) diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/RateLimitIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/RateLimitIT.java index 25509155de..f1d5bed6f1 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/RateLimitIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/RateLimitIT.java @@ -30,12 +30,12 @@ class RateLimitIT { @Test void rateLimitsExecution() { - var res = operator.create(RateLimitCustomResource.class, createResource()); + var res = operator.create(createResource()); IntStream.rangeClosed(1, 5).forEach(i -> { log.debug("replacing resource version: {}", i); var resource = createResource(); resource.getSpec().setNumber(i); - operator.replace(RateLimitCustomResource.class, resource); + operator.replace(resource); }); await().pollInterval(Duration.ofMillis(100)) .pollDelay(Duration.ofMillis(REFRESH_PERIOD / 2)) 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 9050601378..a54e24dc70 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/RetryIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/RetryIT.java @@ -37,7 +37,7 @@ class RetryIT { void retryFailedExecution() { RetryTestCustomResource resource = createTestCustomResource("1"); - operator.create(RetryTestCustomResource.class, resource); + operator.create(resource); await("cr status updated") .pollDelay( 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 fd2fa864f2..d09b14c4a9 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/RetryMaxAttemptIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/RetryMaxAttemptIT.java @@ -32,7 +32,7 @@ class RetryMaxAttemptIT { void retryFailedExecution() throws InterruptedException { RetryTestCustomResource resource = createTestCustomResource("max-retry"); - operator.create(RetryTestCustomResource.class, resource); + operator.create(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/StandaloneDependentResourceIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/StandaloneDependentResourceIT.java index f4e6624c4a..f9082ed481 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/StandaloneDependentResourceIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/StandaloneDependentResourceIT.java @@ -33,7 +33,7 @@ void dependentResourceManagesDeployment() { customResource.setMetadata(new ObjectMeta()); customResource.getMetadata().setName(DEPENDENT_TEST_NAME); - operator.create(StandaloneDependentTestCustomResource.class, customResource); + operator.create(customResource); awaitForDeploymentReadyReplicas(1); assertThat( @@ -48,13 +48,13 @@ void executeUpdateForTestingCacheUpdateForGetResource() { customResource.setSpec(new StandaloneDependentTestCustomResourceSpec()); customResource.setMetadata(new ObjectMeta()); customResource.getMetadata().setName(DEPENDENT_TEST_NAME); - var createdCR = operator.create(StandaloneDependentTestCustomResource.class, customResource); + var createdCR = operator.create(customResource); awaitForDeploymentReadyReplicas(1); var clonedCr = ConfigurationServiceProvider.instance().getResourceCloner().clone(createdCR); clonedCr.getSpec().setReplicaCount(2); - operator.replace(StandaloneDependentTestCustomResource.class, clonedCr); + operator.replace(clonedCr); awaitForDeploymentReadyReplicas(2); assertThat( diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/StatusPatchNotLockingIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/StatusPatchNotLockingIT.java index 5cc7e7f065..1746e5737d 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/StatusPatchNotLockingIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/StatusPatchNotLockingIT.java @@ -28,10 +28,10 @@ class StatusPatchNotLockingIT { @Test void noOptimisticLockingDoneOnStatusUpdate() throws InterruptedException { - var resource = operator.create(StatusPatchLockingCustomResource.class, createResource()); + var resource = operator.create(createResource()); Thread.sleep(WAIT_TIME / 2); resource.getMetadata().setAnnotations(Map.of("key", "value")); - operator.replace(StatusPatchLockingCustomResource.class, resource); + operator.replace(resource); await().pollDelay(Duration.ofMillis(WAIT_TIME)).untilAsserted(() -> { assertThat( @@ -49,7 +49,7 @@ void noOptimisticLockingDoneOnStatusUpdate() throws InterruptedException { // see https://github.com/fabric8io/kubernetes-client/issues/4158 @Test void valuesAreDeletedIfSetToNull() { - var resource = operator.create(StatusPatchLockingCustomResource.class, createResource()); + var resource = operator.create(createResource()); await().untilAsserted(() -> { var actual = operator.get(StatusPatchLockingCustomResource.class, @@ -59,7 +59,7 @@ void valuesAreDeletedIfSetToNull() { }); resource.getSpec().setMessageInStatus(false); - operator.replace(StatusPatchLockingCustomResource.class, resource); + operator.replace(resource); await().untilAsserted(() -> { var actual = operator.get(StatusPatchLockingCustomResource.class, diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/StatusUpdateLockingIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/StatusUpdateLockingIT.java index fa84469a09..147c0403c3 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/StatusUpdateLockingIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/StatusUpdateLockingIT.java @@ -26,10 +26,10 @@ class StatusUpdateLockingIT { @Test void optimisticLockingDoneOnStatusUpdate() throws InterruptedException { - var resource = operator.create(StatusUpdateLockingCustomResource.class, createResource()); + var resource = operator.create(createResource()); Thread.sleep(WAIT_TIME / 2); resource.getMetadata().setAnnotations(Map.of("key", "value")); - operator.replace(StatusUpdateLockingCustomResource.class, resource); + operator.replace(resource); await().pollDelay(Duration.ofMillis(WAIT_TIME)).untilAsserted(() -> { assertThat( 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 8e5fe0a1dd..77eff4812d 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/SubResourceUpdateIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/SubResourceUpdateIT.java @@ -30,7 +30,7 @@ class SubResourceUpdateIT { @Test void updatesSubResourceStatus() { SubResourceTestCustomResource resource = createTestCustomResource("1"); - operator.create(SubResourceTestCustomResource.class, resource); + operator.create(resource); awaitStatusUpdated(resource.getMetadata().getName()); // wait for sure, there are no more events @@ -45,7 +45,7 @@ void updatesSubResourceStatusNoFinalizer() { SubResourceTestCustomResource resource = createTestCustomResource("1"); resource.getMetadata().setFinalizers(Collections.emptyList()); - operator.create(SubResourceTestCustomResource.class, resource); + operator.create(resource); awaitStatusUpdated(resource.getMetadata().getName()); // wait for sure, there are no more events @@ -60,7 +60,7 @@ void updatesSubResourceStatusNoFinalizer() { void ifNoFinalizerPresentFirstAddsTheFinalizerThenExecutesControllerAgain() { SubResourceTestCustomResource resource = createTestCustomResource("1"); resource.getMetadata().getFinalizers().clear(); - operator.create(SubResourceTestCustomResource.class, resource); + operator.create(resource); awaitStatusUpdated(resource.getMetadata().getName()); // wait for sure, there are no more events @@ -77,7 +77,7 @@ void ifNoFinalizerPresentFirstAddsTheFinalizerThenExecutesControllerAgain() { @Test void updateCustomResourceAfterSubResourceChange() { SubResourceTestCustomResource resource = createTestCustomResource("1"); - resource = operator.create(SubResourceTestCustomResource.class, resource); + resource = operator.create(resource); // waits for the resource to start processing waitXms(EVENT_RECEIVE_WAIT); 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 b1d82d9b46..3d7d27b4c3 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/UpdatingResAndSubResIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/UpdatingResAndSubResIT.java @@ -25,7 +25,7 @@ class UpdatingResAndSubResIT { @Test void updatesSubResourceStatus() { DoubleUpdateTestCustomResource resource = createTestCustomResource("1"); - operator.create(DoubleUpdateTestCustomResource.class, resource); + operator.create(resource); awaitStatusUpdated(resource.getMetadata().getName()); // wait for sure, there are no more events diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/WorkflowAllFeatureIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/WorkflowAllFeatureIT.java index 2a8289101f..f60f2b6fde 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/WorkflowAllFeatureIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/WorkflowAllFeatureIT.java @@ -30,7 +30,7 @@ public class WorkflowAllFeatureIT { @Test void configMapNotReconciledUntilDeploymentReady() { - operator.create(WorkflowAllFeatureCustomResource.class, customResource(true)); + operator.create(customResource(true)); await().untilAsserted( () -> { assertThat(operator @@ -57,7 +57,7 @@ void configMapNotReconciledUntilDeploymentReady() { @Test void configMapNotReconciledIfReconcileConditionNotMet() { - var resource = operator.create(WorkflowAllFeatureCustomResource.class, customResource(false)); + var resource = operator.create(customResource(false)); await().atMost(ONE_MINUTE).untilAsserted(() -> { assertThat(operator.get(ConfigMap.class, RESOURCE_NAME)).isNull(); @@ -66,7 +66,7 @@ void configMapNotReconciledIfReconcileConditionNotMet() { }); resource.getSpec().setCreateConfigMap(true); - operator.replace(WorkflowAllFeatureCustomResource.class, resource); + operator.replace(resource); await().untilAsserted(() -> { assertThat(operator.get(ConfigMap.class, RESOURCE_NAME)).isNotNull(); @@ -78,7 +78,7 @@ void configMapNotReconciledIfReconcileConditionNotMet() { @Test void configMapNotDeletedUntilNotMarked() { - var resource = operator.create(WorkflowAllFeatureCustomResource.class, customResource(true)); + var resource = operator.create(customResource(true)); await().atMost(ONE_MINUTE).untilAsserted(() -> { assertThat(operator.get(WorkflowAllFeatureCustomResource.class, RESOURCE_NAME).getStatus()) @@ -88,7 +88,7 @@ void configMapNotDeletedUntilNotMarked() { assertThat(operator.get(ConfigMap.class, RESOURCE_NAME)).isNotNull(); }); - operator.delete(WorkflowAllFeatureCustomResource.class, resource); + operator.delete(resource); await().pollDelay(Duration.ofMillis(300)).untilAsserted(() -> { assertThat(operator.get(ConfigMap.class, RESOURCE_NAME)).isNotNull(); @@ -109,7 +109,7 @@ private void markConfigMapForDelete() { cm.getMetadata().setAnnotations(new HashMap<>()); } cm.getMetadata().getAnnotations().put(READY_TO_DELETE_ANNOTATION, "true"); - operator.replace(ConfigMap.class, cm); + operator.replace(cm); } private WorkflowAllFeatureCustomResource customResource(boolean createConfigMap) { diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/labelselector/LabelSelectorTestCustomResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/labelselector/LabelSelectorTestCustomResource.java new file mode 100644 index 0000000000..0b304aa9e7 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/labelselector/LabelSelectorTestCustomResource.java @@ -0,0 +1,15 @@ +package io.javaoperatorsdk.operator.sample.labelselector; + +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("lst") +public class LabelSelectorTestCustomResource + extends CustomResource + implements Namespaced { +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/labelselector/LabelSelectorTestReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/labelselector/LabelSelectorTestReconciler.java new file mode 100644 index 0000000000..3fc0a78630 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/labelselector/LabelSelectorTestReconciler.java @@ -0,0 +1,31 @@ +package io.javaoperatorsdk.operator.sample.labelselector; + +import java.util.concurrent.atomic.AtomicInteger; + +import io.javaoperatorsdk.operator.api.reconciler.*; +import io.javaoperatorsdk.operator.support.TestExecutionInfoProvider; + +import static io.javaoperatorsdk.operator.sample.labelselector.LabelSelectorTestReconciler.LABEL_KEY; +import static io.javaoperatorsdk.operator.sample.labelselector.LabelSelectorTestReconciler.LABEL_VALUE; + +@ControllerConfiguration(labelSelector = LABEL_KEY + "=" + LABEL_VALUE) +public class LabelSelectorTestReconciler + implements Reconciler, TestExecutionInfoProvider { + + public static final String LABEL_KEY = "app"; + public static final String LABEL_VALUE = "myapp"; + + private final AtomicInteger numberOfExecutions = new AtomicInteger(0); + + @Override + public UpdateControl reconcile( + LabelSelectorTestCustomResource 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/simple/TestReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestReconciler.java index ebf809799e..d39c3c8d15 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 @@ -66,13 +66,14 @@ public void setKubernetesClient(KubernetesClient kubernetesClient) { public DeleteControl cleanup( TestCustomResource resource, Context context) { numberOfCleanupExecutions.incrementAndGet(); - Boolean delete = - kubernetesClient - .configMaps() - .inNamespace(resource.getMetadata().getNamespace()) - .withName(resource.getSpec().getConfigMapName()) - .delete(); - if (delete) { + + var statusDetail = kubernetesClient + .configMaps() + .inNamespace(resource.getMetadata().getNamespace()) + .withName(resource.getSpec().getConfigMapName()) + .delete(); + + if (statusDetail.size() == 1 && statusDetail.get(0).getCauses().isEmpty()) { log.info( "Deleted ConfigMap {} for resource: {}", resource.getSpec().getConfigMapName(), diff --git a/pom.xml b/pom.xml index 66cd2feea8..d4fbf88297 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ io.javaoperatorsdk java-operator-sdk - 3.1.2-SNAPSHOT + 3.2.0-SNAPSHOT Operator SDK for Java Java SDK for implementing Kubernetes operators pom @@ -43,8 +43,8 @@ https://sonarcloud.io 5.9.0 - 5.12.3 - 1.7.36 + 6.0.0 + 2.0.0 2.18.0 4.7.0 3.12.0 @@ -75,10 +75,10 @@ + operator-framework-bom operator-framework-core operator-framework-junit5 operator-framework - smoke-test-samples micrometer-support sample-operators @@ -86,6 +86,13 @@ + + org.junit + junit-bom + ${junit.version} + pom + import + io.fabric8 kubernetes-client-bom @@ -94,13 +101,16 @@ import - org.junit - junit-bom - ${junit.version} - pom - import + io.fabric8 + kubernetes-server-mock + ${fabric8-client.version} + test + + + io.fabric8 + kubernetes-client + ${fabric8-client.version} - org.apache.commons commons-lang3 @@ -116,7 +126,6 @@ micrometer-core ${micrometer-core.version} - com.squareup javapoet diff --git a/sample-operators/leader-election/README.md b/sample-operators/leader-election/README.md new file mode 100644 index 0000000000..2a9a369a45 --- /dev/null +++ b/sample-operators/leader-election/README.md @@ -0,0 +1,6 @@ +# Leader Election E2E Test + +The purpose of this module is to e2e test leader election feature. + +The deployment is using directly pods in order to better control some aspects in test. +In real life this would be a Deployment. \ No newline at end of file diff --git a/sample-operators/leader-election/k8s/operator-instance-2.yaml b/sample-operators/leader-election/k8s/operator-instance-2.yaml new file mode 100644 index 0000000000..abde7eb56a --- /dev/null +++ b/sample-operators/leader-election/k8s/operator-instance-2.yaml @@ -0,0 +1,19 @@ +apiVersion: v1 +kind: Pod +metadata: + name: leader-election-operator-2 +spec: + serviceAccountName: leader-election-operator + containers: + - name: operator + image: leader-election-operator + imagePullPolicy: Never + env: + - name: POD_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name diff --git a/sample-operators/leader-election/k8s/operator.yaml b/sample-operators/leader-election/k8s/operator.yaml new file mode 100644 index 0000000000..00ed3e7273 --- /dev/null +++ b/sample-operators/leader-election/k8s/operator.yaml @@ -0,0 +1,65 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: leader-election-operator + +--- +apiVersion: v1 +kind: Pod +metadata: + name: leader-election-operator-1 +spec: + serviceAccountName: leader-election-operator + containers: + - name: operator + image: leader-election-operator + imagePullPolicy: Never + env: + - name: POD_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: operator-admin +subjects: + - kind: ServiceAccount + name: leader-election-operator +roleRef: + kind: ClusterRole + name: leader-election-operator + apiGroup: "" + +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: leader-election-operator +rules: + - apiGroups: + - "apiextensions.k8s.io" + resources: + - customresourcedefinitions + verbs: + - '*' + - apiGroups: + - "sample.javaoperatorsdk" + resources: + - leaderelectiontestcustomresources + - leaderelectiontestcustomresources/status + verbs: + - '*' + - apiGroups: + - "coordination.k8s.io" + resources: + - "leases" + verbs: + - '*' + diff --git a/sample-operators/leader-election/pom.xml b/sample-operators/leader-election/pom.xml new file mode 100644 index 0000000000..8ae0e3e4ba --- /dev/null +++ b/sample-operators/leader-election/pom.xml @@ -0,0 +1,82 @@ + + + 4.0.0 + + + io.javaoperatorsdk + sample-operators + 3.2.0-SNAPSHOT + + + sample-leader-election + Operator SDK - Samples - Leader Election + An E2E test for leader election + jar + + + 11 + 11 + 3.2.1 + + + + + io.javaoperatorsdk + operator-framework + + + org.apache.logging.log4j + log4j-slf4j-impl + + + org.takes + takes + 1.21.1 + + + io.fabric8 + crd-generator-apt + provided + + + io.fabric8 + crd-generator-api + + + org.awaitility + awaitility + compile + + + io.javaoperatorsdk + operator-framework-junit-5 + ${project.version} + test + + + + + + com.google.cloud.tools + jib-maven-plugin + ${jib-maven-plugin.version} + + + gcr.io/distroless/java:11 + + + leader-election-operator + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.10.0 + + + + + \ No newline at end of file diff --git a/smoke-test-samples/common/src/main/java/io/javaoperatorsdk/operator/sample/CustomService.java b/sample-operators/leader-election/src/main/java/io/javaoperatorsdk/operator/sample/LeaderElectionTestCustomResource.java similarity index 70% rename from smoke-test-samples/common/src/main/java/io/javaoperatorsdk/operator/sample/CustomService.java rename to sample-operators/leader-election/src/main/java/io/javaoperatorsdk/operator/sample/LeaderElectionTestCustomResource.java index d67c7e6955..b440be4d18 100644 --- a/smoke-test-samples/common/src/main/java/io/javaoperatorsdk/operator/sample/CustomService.java +++ b/sample-operators/leader-election/src/main/java/io/javaoperatorsdk/operator/sample/LeaderElectionTestCustomResource.java @@ -8,6 +8,8 @@ @Group("sample.javaoperatorsdk") @Version("v1") -@ShortNames("cs") -public class CustomService extends CustomResource implements Namespaced { +@ShortNames("le") +public class LeaderElectionTestCustomResource + extends CustomResource + implements Namespaced { } diff --git a/sample-operators/leader-election/src/main/java/io/javaoperatorsdk/operator/sample/LeaderElectionTestOperator.java b/sample-operators/leader-election/src/main/java/io/javaoperatorsdk/operator/sample/LeaderElectionTestOperator.java new file mode 100644 index 0000000000..47a5a0ed59 --- /dev/null +++ b/sample-operators/leader-election/src/main/java/io/javaoperatorsdk/operator/sample/LeaderElectionTestOperator.java @@ -0,0 +1,34 @@ +package io.javaoperatorsdk.operator.sample; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.fabric8.kubernetes.client.ConfigBuilder; +import io.fabric8.kubernetes.client.KubernetesClientBuilder; +import io.javaoperatorsdk.operator.Operator; +import io.javaoperatorsdk.operator.api.config.LeaderElectionConfiguration; + +public class LeaderElectionTestOperator { + + private static final Logger log = LoggerFactory.getLogger(LeaderElectionTestOperator.class); + + public static void main(String[] args) { + String identity = System.getenv("POD_NAME"); + String namespace = System.getenv("POD_NAMESPACE"); + + log.info("Starting operator with identity: {}", identity); + + var client = new KubernetesClientBuilder().withConfig(new ConfigBuilder() + .withNamespace(namespace) + .build()).build(); + + Operator operator = new Operator(client, + c -> c.withLeaderElectionConfiguration( + new LeaderElectionConfiguration("leader-election-test", namespace, identity))); + + operator.register(new LeaderElectionTestReconciler(identity)); + operator.installShutdownHook(); + operator.start(); + } + +} diff --git a/sample-operators/leader-election/src/main/java/io/javaoperatorsdk/operator/sample/LeaderElectionTestReconciler.java b/sample-operators/leader-election/src/main/java/io/javaoperatorsdk/operator/sample/LeaderElectionTestReconciler.java new file mode 100644 index 0000000000..f6231d0a52 --- /dev/null +++ b/sample-operators/leader-election/src/main/java/io/javaoperatorsdk/operator/sample/LeaderElectionTestReconciler.java @@ -0,0 +1,34 @@ +package io.javaoperatorsdk.operator.sample; + +import java.time.Duration; + +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 LeaderElectionTestReconciler + implements Reconciler { + + private final String reconcilerName; + + public LeaderElectionTestReconciler(String reconcilerName) { + this.reconcilerName = reconcilerName; + } + + @Override + public UpdateControl reconcile( + LeaderElectionTestCustomResource resource, + Context context) { + + if (resource.getStatus() == null) { + resource.setStatus(new LeaderElectionTestStatus()); + } + + resource.getStatus().getReconciledBy().add(reconcilerName); + // update status is with optimistic locking + return UpdateControl.updateStatus(resource).rescheduleAfter(Duration.ofSeconds(1)); + } + +} diff --git a/sample-operators/leader-election/src/main/java/io/javaoperatorsdk/operator/sample/LeaderElectionTestStatus.java b/sample-operators/leader-election/src/main/java/io/javaoperatorsdk/operator/sample/LeaderElectionTestStatus.java new file mode 100644 index 0000000000..5a9e66e270 --- /dev/null +++ b/sample-operators/leader-election/src/main/java/io/javaoperatorsdk/operator/sample/LeaderElectionTestStatus.java @@ -0,0 +1,21 @@ +package io.javaoperatorsdk.operator.sample; + +import java.util.ArrayList; +import java.util.List; + +public class LeaderElectionTestStatus { + + private List reconciledBy; + + public List getReconciledBy() { + if (reconciledBy == null) { + reconciledBy = new ArrayList<>(); + } + return reconciledBy; + } + + public LeaderElectionTestStatus setReconciledBy(List reconciledBy) { + this.reconciledBy = reconciledBy; + return this; + } +} diff --git a/smoke-test-samples/common/src/main/resources/log4j2.xml b/sample-operators/leader-election/src/main/resources/log4j2.xml similarity index 65% rename from smoke-test-samples/common/src/main/resources/log4j2.xml rename to sample-operators/leader-election/src/main/resources/log4j2.xml index d6869ee67c..0ec69bf713 100644 --- a/smoke-test-samples/common/src/main/resources/log4j2.xml +++ b/sample-operators/leader-election/src/main/resources/log4j2.xml @@ -2,12 +2,12 @@ - + - + - \ No newline at end of file + diff --git a/sample-operators/leader-election/src/test/java/io/javaoperatorsdk/operator/sample/LeaderElectionE2E.java b/sample-operators/leader-election/src/test/java/io/javaoperatorsdk/operator/sample/LeaderElectionE2E.java new file mode 100644 index 0000000000..60b3ad639a --- /dev/null +++ b/sample-operators/leader-election/src/test/java/io/javaoperatorsdk/operator/sample/LeaderElectionE2E.java @@ -0,0 +1,180 @@ +package io.javaoperatorsdk.operator.sample; + +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.time.Duration; +import java.util.List; +import java.util.Locale; +import java.util.OptionalInt; +import java.util.UUID; +import java.util.stream.IntStream; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledIfSystemProperty; +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.ObjectMetaBuilder; +import io.fabric8.kubernetes.api.model.rbac.ClusterRoleBinding; +import io.fabric8.kubernetes.client.ConfigBuilder; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.KubernetesClientBuilder; + +import static io.javaoperatorsdk.operator.junit.AbstractOperatorExtension.CRD_READY_WAIT; +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +class LeaderElectionE2E { + + private static final Logger log = LoggerFactory.getLogger(LeaderElectionE2E.class); + public static final int POD_STARTUP_TIMEOUT = 60; + + public static final String TEST_RESOURCE_NAME = "test1"; + public static final int MINIMAL_SECONDS_FOR_RENEWAL = 3; + public static final int MAX_WAIT_SECONDS = 30; + + private static final String OPERATOR_1_POD_NAME = "leader-election-operator-1"; + private static final String OPERATOR_2_POD_NAME = "leader-election-operator-2"; + public static final int MINIMAL_EXPECTED_RECONCILIATION = 3; + + private String namespace; + private KubernetesClient client; + + @Test + // not for local mode by design + @EnabledIfSystemProperty(named = "test.deployment", matches = "remote") + void otherInstancesTakesOverWhenSteppingDown() { + log.info("Deploying operator"); + deployOperatorsInOrder(); + log.info("Applying custom resource"); + applyCustomResource(); + + log.info("Awaiting custom resource reconciliations"); + await().pollDelay(Duration.ofSeconds(MINIMAL_SECONDS_FOR_RENEWAL)) + .atMost(Duration.ofSeconds(MAX_WAIT_SECONDS)) + .untilAsserted(() -> { + var actualStatus = client.resources(LeaderElectionTestCustomResource.class) + .inNamespace(namespace).withName(TEST_RESOURCE_NAME).get().getStatus(); + + assertThat(actualStatus).isNotNull(); + assertThat(actualStatus.getReconciledBy()) + .hasSizeGreaterThan(MINIMAL_EXPECTED_RECONCILIATION); + }); + + client.pods().inNamespace(namespace).withName(OPERATOR_1_POD_NAME).delete(); + + var actualListSize = client.resources(LeaderElectionTestCustomResource.class) + .inNamespace(namespace).withName(TEST_RESOURCE_NAME).get().getStatus().getReconciledBy() + .size(); + + await().pollDelay(Duration.ofSeconds(MINIMAL_SECONDS_FOR_RENEWAL)) + .atMost(Duration.ofSeconds(240)) + .untilAsserted(() -> { + var actualStatus = client.resources(LeaderElectionTestCustomResource.class) + .inNamespace(namespace).withName(TEST_RESOURCE_NAME).get().getStatus(); + + assertThat(actualStatus).isNotNull(); + assertThat(actualStatus.getReconciledBy()) + .hasSizeGreaterThan(actualListSize + MINIMAL_EXPECTED_RECONCILIATION); + }); + + assertReconciliations( + client.resources(LeaderElectionTestCustomResource.class).inNamespace(namespace) + .withName(TEST_RESOURCE_NAME).get().getStatus().getReconciledBy()); + } + + private void assertReconciliations(List reconciledBy) { + log.info("Reconciled by content: {}", reconciledBy); + OptionalInt firstO2StatusIndex = IntStream.range(0, reconciledBy.size()) + .filter(i -> reconciledBy.get(i).equals(OPERATOR_2_POD_NAME)) + .findFirst(); + assertThat(firstO2StatusIndex).isPresent(); + assertThat(reconciledBy.subList(0, firstO2StatusIndex.getAsInt() - 1)) + .allMatch(s -> s.equals(OPERATOR_1_POD_NAME)); + assertThat(reconciledBy.subList(firstO2StatusIndex.getAsInt(), reconciledBy.size())) + .allMatch(s -> s.equals(OPERATOR_2_POD_NAME)); + } + + private void applyCustomResource() { + var res = new LeaderElectionTestCustomResource(); + res.setMetadata(new ObjectMetaBuilder() + .withName(TEST_RESOURCE_NAME) + .withNamespace(namespace) + .build()); + client.resource(res).create(); + } + + @BeforeEach + void setup() { + namespace = "leader-election-it-" + UUID.randomUUID(); + client = new KubernetesClientBuilder().withConfig(new ConfigBuilder() + .withNamespace(namespace) + .build()).build(); + applyCRD(); + client.namespaces().resource(new NamespaceBuilder().withNewMetadata().withName(namespace) + .endMetadata().build()).create(); + } + + @AfterEach + void tearDown() { + client.namespaces().resource(new NamespaceBuilder().withNewMetadata().withName(namespace) + .endMetadata().build()).delete(); + await() + .atMost(Duration.ofSeconds(60)) + .untilAsserted(() -> assertThat(client.namespaces().withName(namespace).get()).isNull()); + } + + private void deployOperatorsInOrder() { + applyResources("k8s/operator.yaml"); + await().atMost(Duration.ofSeconds(POD_STARTUP_TIMEOUT)).untilAsserted(() -> { + var pod = client.pods().inNamespace(namespace).withName(OPERATOR_1_POD_NAME).get(); + assertThat(pod.getStatus().getContainerStatuses().get(0).getReady()).isTrue(); + }); + + applyResources("k8s/operator-instance-2.yaml"); + await().atMost(Duration.ofSeconds(POD_STARTUP_TIMEOUT)).untilAsserted(() -> { + var pod = client.pods().inNamespace(namespace).withName(OPERATOR_2_POD_NAME).get(); + assertThat(pod.getStatus().getContainerStatuses().get(0).getReady()).isTrue(); + }); + } + + void applyCRD() { + String path = + "./target/classes/META-INF/fabric8/leaderelectiontestcustomresources.sample.javaoperatorsdk-v1.yml"; + try (InputStream is = new FileInputStream(path)) { + final var crd = client.load(is); + crd.createOrReplace(); + Thread.sleep(CRD_READY_WAIT); + log.debug("Applied CRD with name: {}", crd.get().get(0).getMetadata().getName()); + } catch (InterruptedException | IOException e) { + throw new RuntimeException(e); + } + } + + void applyResources(String path) { + try { + List resources = client.load(new FileInputStream(path)).get(); + resources.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); + } + } + }); + client.resourceList(resources) + .inNamespace(namespace) + .createOrReplace(); + + } catch (FileNotFoundException e) { + throw new RuntimeException(e); + } + } +} diff --git a/sample-operators/leader-election/src/test/resources/log4j2.xml b/sample-operators/leader-election/src/test/resources/log4j2.xml new file mode 100644 index 0000000000..2b7fdd3479 --- /dev/null +++ b/sample-operators/leader-election/src/test/resources/log4j2.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/sample-operators/mysql-schema/pom.xml b/sample-operators/mysql-schema/pom.xml index 5814602b1a..dda3074ac6 100644 --- a/sample-operators/mysql-schema/pom.xml +++ b/sample-operators/mysql-schema/pom.xml @@ -7,7 +7,7 @@ io.javaoperatorsdk sample-operators - 3.1.2-SNAPSHOT + 3.2.0-SNAPSHOT sample-mysql-schema-operator @@ -47,6 +47,10 @@ crd-generator-apt provided + + io.fabric8 + crd-generator-api + org.apache.logging.log4j log4j-slf4j-impl 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 f1cd015c34..b2fb0aab65 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 @@ -9,10 +9,7 @@ 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.fabric8.kubernetes.client.*; import io.javaoperatorsdk.operator.Operator; import io.javaoperatorsdk.operator.monitoring.micrometer.MicrometerMetrics; import io.javaoperatorsdk.operator.sample.dependent.ResourcePollerConfig; @@ -26,8 +23,7 @@ public class MySQLSchemaOperator { 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); + KubernetesClient client = new KubernetesClientBuilder().build(); Operator operator = new Operator(client, overrider -> overrider.withMetrics(new MicrometerMetrics(new LoggingMeterRegistry()))); diff --git a/sample-operators/pom.xml b/sample-operators/pom.xml index d0f5ea74c6..82b0e8ba00 100644 --- a/sample-operators/pom.xml +++ b/sample-operators/pom.xml @@ -7,7 +7,7 @@ io.javaoperatorsdk java-operator-sdk - 3.1.2-SNAPSHOT + 3.2.0-SNAPSHOT sample-operators @@ -22,5 +22,6 @@ tomcat-operator webpage mysql-schema + leader-election - \ No newline at end of file + diff --git a/sample-operators/tomcat-operator/pom.xml b/sample-operators/tomcat-operator/pom.xml index 23c9e338e9..31dd8ff981 100644 --- a/sample-operators/tomcat-operator/pom.xml +++ b/sample-operators/tomcat-operator/pom.xml @@ -7,7 +7,7 @@ io.javaoperatorsdk sample-operators - 3.1.2-SNAPSHOT + 3.2.0-SNAPSHOT sample-tomcat-operator @@ -32,6 +32,10 @@ crd-generator-apt provided + + io.fabric8 + crd-generator-api + org.apache.logging.log4j log4j-slf4j-impl @@ -54,6 +58,7 @@ org.awaitility awaitility + 4.1.1 test 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 6b894a3348..3973379769 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 @@ -9,10 +9,7 @@ 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.fabric8.kubernetes.client.*; import io.javaoperatorsdk.operator.Operator; public class TomcatOperator { @@ -21,8 +18,7 @@ public class TomcatOperator { public static void main(String[] args) throws IOException { - Config config = new ConfigBuilder().withNamespace(null).build(); - KubernetesClient client = new DefaultKubernetesClient(config); + KubernetesClient client = new KubernetesClientBuilder().build(); Operator operator = new Operator(client); operator.register(new TomcatReconciler()); operator.register(new WebappReconciler(client)); diff --git a/sample-operators/webpage/pom.xml b/sample-operators/webpage/pom.xml index 642715d4f2..7a57b23827 100644 --- a/sample-operators/webpage/pom.xml +++ b/sample-operators/webpage/pom.xml @@ -7,7 +7,7 @@ io.javaoperatorsdk sample-operators - 3.1.2-SNAPSHOT + 3.2.0-SNAPSHOT sample-webpage-operator @@ -40,6 +40,10 @@ crd-generator-apt provided + + io.fabric8 + crd-generator-api + org.awaitility awaitility 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 661f6f3ba8..9b137649fc 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 @@ -9,10 +9,7 @@ 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.fabric8.kubernetes.client.*; import io.javaoperatorsdk.operator.Operator; public class WebPageOperator { @@ -25,8 +22,7 @@ public class WebPageOperator { 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); + KubernetesClient client = new KubernetesClientBuilder().build(); Operator operator = new Operator(client); String reconcilerEnvVar = System.getenv(WEBPAGE_RECONCILER_ENV); if (WEBPAGE_CLASSIC_RECONCILER_ENV_VALUE.equals(reconcilerEnvVar)) { 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 f80e681b7a..15c2ceef87 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 @@ -47,7 +47,7 @@ boolean isLocal() { void testAddingWebPage() { var webPage = createWebPage(TITLE1); - operator().create(WebPage.class, webPage); + operator().create(webPage); await() .atMost(Duration.ofSeconds(WAIT_SECONDS)) @@ -64,7 +64,7 @@ void testAddingWebPage() { assertThat(httpGetForWebPage(webPage)).contains(TITLE1); // update part: changing title - operator().replace(WebPage.class, createWebPage(TITLE2)); + operator().replace(createWebPage(TITLE2)); await().atMost(Duration.ofSeconds(WAIT_SECONDS)) .pollInterval(POLL_INTERVAL) @@ -74,7 +74,7 @@ void testAddingWebPage() { }); // delete part: deleting webpage - operator().delete(WebPage.class, createWebPage(TITLE2)); + operator().delete(createWebPage(TITLE2)); await().atMost(Duration.ofSeconds(WAIT_SECONDS)) .pollInterval(POLL_INTERVAL) diff --git a/smoke-test-samples/README.md b/smoke-test-samples/README.md deleted file mode 100644 index 12daaa70f6..0000000000 --- a/smoke-test-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 in the `sample-operators` directory. diff --git a/smoke-test-samples/common/crd/test_object.yaml b/smoke-test-samples/common/crd/test_object.yaml deleted file mode 100644 index d897b550ef..0000000000 --- a/smoke-test-samples/common/crd/test_object.yaml +++ /dev/null @@ -1,7 +0,0 @@ -apiVersion: "sample.javaoperatorsdk/v1" -kind: CustomService -metadata: - name: custom-service1 -spec: - name: testservice1 - label: testlabel diff --git a/smoke-test-samples/common/pom.xml b/smoke-test-samples/common/pom.xml deleted file mode 100644 index 8b339f669c..0000000000 --- a/smoke-test-samples/common/pom.xml +++ /dev/null @@ -1,39 +0,0 @@ - - - 4.0.0 - - - io.javaoperatorsdk - java-operator-sdk-smoke-test-samples - 3.1.2-SNAPSHOT - - - operator-framework-smoke-test-samples-common - Operator SDK - Smoke Test Samples - Common Files - Files shared between some of the samples - jar - - - - io.javaoperatorsdk - operator-framework - compile - - - io.fabric8 - crd-generator-apt - compile - - - - org.apache.logging.log4j - log4j-slf4j-impl - - - org.apache.logging.log4j - log4j-core - - - - 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 deleted file mode 100644 index 9a5e188cba..0000000000 --- a/smoke-test-samples/common/src/main/java/io/javaoperatorsdk/operator/sample/CustomServiceReconciler.java +++ /dev/null @@ -1,58 +0,0 @@ -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; -import io.fabric8.kubernetes.client.DefaultKubernetesClient; -import io.fabric8.kubernetes.client.KubernetesClient; -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. */ -@ControllerConfiguration -public class CustomServiceReconciler implements Reconciler { - - private static final Logger log = LoggerFactory.getLogger(CustomServiceReconciler.class); - - private final KubernetesClient kubernetesClient; - - public CustomServiceReconciler() { - this(new DefaultKubernetesClient()); - } - - public CustomServiceReconciler(KubernetesClient kubernetesClient) { - this.kubernetesClient = kubernetesClient; - } - - @Override - public UpdateControl reconcile( - CustomService resource, Context context) { - log.info("Reconciling: {}", resource.getMetadata().getName()); - - ServicePort servicePort = new ServicePort(); - servicePort.setPort(8080); - 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(service); - - return UpdateControl.updateResource(resource); - } -} diff --git a/smoke-test-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 deleted file mode 100644 index f4a3452b35..0000000000 --- a/smoke-test-samples/common/src/main/java/io/javaoperatorsdk/operator/sample/ServiceSpec.java +++ /dev/null @@ -1,23 +0,0 @@ -package io.javaoperatorsdk.operator.sample; - -public class ServiceSpec { - - private String name; - private String label; - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public String getLabel() { - return label; - } - - public void setLabel(String label) { - this.label = label; - } -} diff --git a/smoke-test-samples/pom.xml b/smoke-test-samples/pom.xml deleted file mode 100644 index 2c05124b27..0000000000 --- a/smoke-test-samples/pom.xml +++ /dev/null @@ -1,36 +0,0 @@ - - - 4.0.0 - - - io.javaoperatorsdk - java-operator-sdk - 3.1.2-SNAPSHOT - - - java-operator-sdk-smoke-test-samples - Operator SDK - Smoke Test Samples - Samples to manually smoke the sdk - pom - - - common - pure-java - spring-boot-plain - - - - - - org.apache.maven.plugins - maven-deploy-plugin - ${maven-deploy-plugin.version} - - true - - - - - - diff --git a/smoke-test-samples/pure-java/pom.xml b/smoke-test-samples/pure-java/pom.xml deleted file mode 100644 index 06d0d4b1e7..0000000000 --- a/smoke-test-samples/pure-java/pom.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - 4.0.0 - - - io.javaoperatorsdk - java-operator-sdk-smoke-test-samples - 3.1.2-SNAPSHOT - - - 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-smoke-test-samples-common - ${project.version} - - - - 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 deleted file mode 100644 index 0a3b59d290..0000000000 --- a/smoke-test-samples/pure-java/src/main/java/io/javaoperatorsdk/operator/sample/PureJavaApplicationRunner.java +++ /dev/null @@ -1,16 +0,0 @@ -package io.javaoperatorsdk.operator.sample; - -import java.util.concurrent.Executors; - -import io.javaoperatorsdk.operator.Operator; - -public class PureJavaApplicationRunner { - - public static void main(String[] args) { - Operator operator = - new Operator(overrider -> overrider.withExecutorService(Executors.newCachedThreadPool()) - .withConcurrentReconciliationThreads(2)); - operator.register(new CustomServiceReconciler()); - operator.start(); - } -} diff --git a/smoke-test-samples/spring-boot-plain/pom.xml b/smoke-test-samples/spring-boot-plain/pom.xml deleted file mode 100644 index 6dd835497b..0000000000 --- a/smoke-test-samples/spring-boot-plain/pom.xml +++ /dev/null @@ -1,76 +0,0 @@ - - - 4.0.0 - - - io.javaoperatorsdk - java-operator-sdk-smoke-test-samples - 3.1.2-SNAPSHOT - - - operator-framework-smoke-test-samples-spring-boot - Operator SDK - Smoke Test Samples - Spring Boot - Sample usage with Spring Boot - jar - - - - io.javaoperatorsdk - operator-framework-smoke-test-samples-common - ${project.version} - - - org.springframework.boot - spring-boot-starter-log4j2 - - - org.springframework.boot - spring-boot-starter - - - org.springframework.boot - spring-boot-starter-logging - - - - - org.springframework.boot - spring-boot-starter-test - test - - - junit - junit - - - - - org.apache.logging.log4j - log4j-api - ${log4j.version} - - - - - - - org.springframework.boot - spring-boot-dependencies - ${spring-boot.version} - pom - import - - - - - - - - org.springframework.boot - spring-boot-maven-plugin - ${spring-boot.version} - - - - 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 deleted file mode 100644 index 7eed511d18..0000000000 --- a/smoke-test-samples/spring-boot-plain/src/main/java/io/javaoperatorsdk/operator/sample/Config.java +++ /dev/null @@ -1,27 +0,0 @@ -package io.javaoperatorsdk.operator.sample; - -import java.util.List; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -import io.javaoperatorsdk.operator.Operator; -import io.javaoperatorsdk.operator.api.reconciler.Reconciler; - -@Configuration -public class Config { - - @Bean - public CustomServiceReconciler customServiceController() { - return new CustomServiceReconciler(); - } - - // Register all controller beans - @Bean(initMethod = "start", destroyMethod = "stop") - @SuppressWarnings("rawtypes") - public Operator operator(List controllers) { - Operator operator = new Operator(); - controllers.forEach(operator::register); - return operator; - } -} diff --git a/smoke-test-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 deleted file mode 100644 index 97533f858a..0000000000 --- a/smoke-test-samples/spring-boot-plain/src/main/java/io/javaoperatorsdk/operator/sample/SpringBootStarterSampleApplication.java +++ /dev/null @@ -1,12 +0,0 @@ -package io.javaoperatorsdk.operator.sample; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; - -@SpringBootApplication -public class SpringBootStarterSampleApplication { - - public static void main(String[] args) { - SpringApplication.run(SpringBootStarterSampleApplication.class, args); - } -} diff --git a/smoke-test-samples/spring-boot-plain/src/main/resources/application.yaml b/smoke-test-samples/spring-boot-plain/src/main/resources/application.yaml deleted file mode 100644 index e5ed78803c..0000000000 --- a/smoke-test-samples/spring-boot-plain/src/main/resources/application.yaml +++ /dev/null @@ -1,5 +0,0 @@ -javaoperatorsdk: - controllers: - customservicecontroller: - retry: - maxAttempts: 3 \ No newline at end of file