diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b81fa41b6d..ebc3588a2e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -11,7 +11,10 @@ jobs: strategy: matrix: java: [ 17, 21, 24 ] - kubernetes: [ '1.30.12', '1.31.8', '1.32.4','1.33.0' ] + # Use the latest versions supported by minikube, otherwise GitHub it will + # end up in a throttling requests from minikube and workflow will fail. + # Minikube does such requests only if a version is not officially supported. + kubernetes: [ '1.30.12', '1.31.8', '1.32.4','1.33.1' ] uses: ./.github/workflows/integration-tests.yml with: java-version: ${{ matrix.java }} @@ -35,7 +38,7 @@ jobs: matrix: java: [ 17, 21, 24 ] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Set up Java and Maven uses: actions/setup-java@v4 with: diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml index e06b427960..c7e91915b6 100644 --- a/.github/workflows/e2e-test.yml +++ b/.github/workflows/e2e-test.yml @@ -1,6 +1,6 @@ # Integration and end to end tests which runs locally and deploys the Operator to a Kubernetes # (Minikube) cluster and creates custom resources to verify the operator's functionality -name: Integration & End to End tests +name: End to End tests on: pull_request: paths-ignore: @@ -27,13 +27,16 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Setup Minikube-Kubernetes uses: manusa/actions-setup-minikube@v2.14.0 with: - minikube version: v1.34.0 - kubernetes version: v1.33.0 + minikube version: v1.36.0 + # Use the latest versions supported by minikube, otherwise GitHub it will + # end up in a throttling requests from minikube and workflow will fail. + # Minikube does such requests only if a version is not officially supported. + kubernetes version: v1.33.1 github token: ${{ secrets.GITHUB_TOKEN }} driver: docker diff --git a/.github/workflows/hugo.yaml b/.github/workflows/hugo.yaml index 511f10a8e0..c1b967616d 100644 --- a/.github/workflows/hugo.yaml +++ b/.github/workflows/hugo.yaml @@ -41,7 +41,7 @@ jobs: - name: Install Dart Sass run: sudo snap install dart-sass - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: submodules: recursive fetch-depth: 0 diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 75a6093371..fb4196be9c 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -29,7 +29,7 @@ jobs: continue-on-error: ${{ inputs.experimental }} timeout-minutes: 40 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: ref: ${{ inputs.checkout-ref }} - name: Set up Java and Maven @@ -41,10 +41,10 @@ jobs: - name: Set up Minikube uses: manusa/actions-setup-minikube@v2.14.0 with: - minikube version: 'v1.34.0' + minikube version: 'v1.36.0' kubernetes version: '${{ inputs.kube-version }}' - driver: 'docker' - github token: ${{ secrets.GITHUB_TOKEN }} + github token: ${{ github.token }} + - name: "${{inputs.it-category}} integration tests (kube: ${{ inputs.kube-version }} / java: ${{ inputs.java-version }} / client: ${{ inputs.http-client }})" run: | if [ -z "${{inputs.it-category}}" ]; then diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 27742bf7c2..b4f045a2ef 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -17,7 +17,7 @@ jobs: check_format_and_unit_tests: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Set up Java and Maven uses: actions/setup-java@v4 with: diff --git a/.github/workflows/release-project-in-dir.yml b/.github/workflows/release-project-in-dir.yml index dc79b6f6c2..7b0d732a56 100644 --- a/.github/workflows/release-project-in-dir.yml +++ b/.github/workflows/release-project-in-dir.yml @@ -19,7 +19,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout "${{inputs.version_branch}}" branch - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: ref: "${{inputs.version_branch}}" @@ -29,6 +29,11 @@ jobs: java-version: 17 distribution: temurin cache: 'maven' + server-id: central + server-username: MAVEN_USERNAME + server-password: MAVEN_CENTRAL_TOKEN + gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }} + gpg-passphrase: MAVEN_GPG_PASSPHRASE - name: Change version to release version # Assume that RELEASE_VERSION will have form like: "v1.0.1". So we cut the "v" @@ -37,15 +42,12 @@ jobs: env: RELEASE_VERSION: ${{ github.event.release.tag_name }} - - name: Release Maven package - uses: samuelmeuli/action-maven-publish@v1 - with: - maven_profiles: "release" - maven_args: ${{ env.MAVEN_ARGS }} - gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} - gpg_passphrase: ${{ secrets.GPG_PASSPHRASE }} - nexus_username: ${{ secrets.OSSRH_USERNAME }} - nexus_password: ${{ secrets.OSSRH_TOKEN }} + - name: Publish to Apache Maven Central + run: mvn package deploy -Prelease + env: + MAVEN_USERNAME: ${{ secrets.NEXUS_USERNAME }} + MAVEN_CENTRAL_TOKEN: ${{ secrets.NEXUS_PASSWORD }} + MAVEN_GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} # This is separate job because there were issues with git after release step, was not able to commit changes. update-working-version: @@ -54,7 +56,7 @@ jobs: if: "!contains(github.event.release.tag_name, 'RC')" # not sure we should keep this the RC part steps: - name: Checkout "${{inputs.version_branch}}" branch - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: ref: "${{inputs.version_branch}}" diff --git a/.github/workflows/snapshot-releases.yml b/.github/workflows/snapshot-releases.yml index 66fe9d25a3..be2a219d37 100644 --- a/.github/workflows/snapshot-releases.yml +++ b/.github/workflows/snapshot-releases.yml @@ -16,7 +16,7 @@ jobs: test: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Set up Java and Maven uses: actions/setup-java@v4 with: @@ -29,18 +29,22 @@ jobs: runs-on: ubuntu-latest needs: test steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Set up Java and Maven uses: actions/setup-java@v4 with: - distribution: temurin java-version: 17 + distribution: temurin cache: 'maven' - - name: Release Maven package - uses: samuelmeuli/action-maven-publish@v1 - with: - maven_profiles: "release" - gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} - gpg_passphrase: ${{ secrets.GPG_PASSPHRASE }} - nexus_username: ${{ secrets.OSSRH_USERNAME }} - nexus_password: ${{ secrets.OSSRH_TOKEN }} + server-id: central + server-username: MAVEN_USERNAME + server-password: MAVEN_CENTRAL_TOKEN + gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }} + gpg-passphrase: MAVEN_GPG_PASSPHRASE + + - name: Publish to Apache Maven Central + run: mvn package deploy -Prelease + env: + MAVEN_USERNAME: ${{ secrets.NEXUS_USERNAME }} + MAVEN_CENTRAL_TOKEN: ${{ secrets.NEXUS_PASSWORD }} + MAVEN_GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} diff --git a/.github/workflows/sonar.yml b/.github/workflows/sonar.yml index b7a96edef6..b95d5175c2 100644 --- a/.github/workflows/sonar.yml +++ b/.github/workflows/sonar.yml @@ -23,7 +23,7 @@ jobs: runs-on: ubuntu-latest if: ${{ ( github.event_name == 'push' ) || ( github.event_name == 'pull_request' && github.event.pull_request.head.repo.owner.login == 'java-operator-sdk' ) }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Set up Java and Maven uses: actions/setup-java@v4 with: diff --git a/README.md b/README.md index 86edf232b0..72eb2f7974 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,10 @@ conversion hooks and dynamic admission controllers are supported as a separate p Under the hood it uses the excellent [Fabric8 Kubernetes Client](https://github.com/fabric8io/kubernetes-client), which provides additional facilities, like generating CRD from source code (and vice versa). +Icon + +Java Operator SDK is a CNCF project as part of [Operator Framework](https://github.com/operator-framework). + ## Documentation Documentation can be found on the **[JOSDK WebSite](https://javaoperatorsdk.io/)**. @@ -63,6 +67,7 @@ projects want to advertise that fact here. For this reason, we ask that if you'd to be featured in this section, please open a PR, adding a link to and short description of your project, as shown below: +- [kroxylicious](https://github.com/kroxylicious/kroxylicious/tree/main/kroxylicious-operator) Kafka proxy operator - [ExposedApp operator](https://github.com/halkyonio/exposedapp-rhdblog): a sample operator written to illustrate JOSDK concepts and its Quarkus extension in the ["Write Kubernetes Operators in Java with the Java Operator SDK" blog series](https://developers.redhat.com/articles/2022/02/15/write-kubernetes-java-java-operator-sdk#). diff --git a/bootstrapper-maven-plugin/pom.xml b/bootstrapper-maven-plugin/pom.xml index eaa32eb009..3e292e810b 100644 --- a/bootstrapper-maven-plugin/pom.xml +++ b/bootstrapper-maven-plugin/pom.xml @@ -5,7 +5,7 @@ io.javaoperatorsdk java-operator-sdk - 5.1.2-SNAPSHOT + 5.1.3-SNAPSHOT bootstrapper @@ -15,7 +15,7 @@ 3.15.1 - 3.9.9 + 3.9.11 3.0.0 3.15.1 @@ -58,7 +58,7 @@ commons-io commons-io - 2.19.0 + 2.20.0 com.github.spullara.mustache.java diff --git a/caffeine-bounded-cache-support/pom.xml b/caffeine-bounded-cache-support/pom.xml index c45e56386d..14e19dd85e 100644 --- a/caffeine-bounded-cache-support/pom.xml +++ b/caffeine-bounded-cache-support/pom.xml @@ -4,7 +4,7 @@ io.javaoperatorsdk java-operator-sdk - 5.1.2-SNAPSHOT + 5.1.3-SNAPSHOT caffeine-bounded-cache-support diff --git a/docs/content/en/docs/faq/_index.md b/docs/content/en/docs/faq/_index.md index 9308ce4cfa..1c3d82fe35 100644 --- a/docs/content/en/docs/faq/_index.md +++ b/docs/content/en/docs/faq/_index.md @@ -90,8 +90,38 @@ reconciliation. For example if you patch the status at the end of the reconcilia it is not guaranteed that during the next reconciliation you will see the fresh resource. Therefore, controllers which do this, usually cache the updated status in memory to make sure it is present for next reconciliation. -Dependent Resources feature supports the [first approach](../dependent-resources/_index.md#external-state-tracking-dependent-resources). +From version 5.1 you can use [this utility](../documentation/reconciler.md#making-sure-the-primary-resource-is-up-to-date-for-the-next-reconciliation) +to make sure an updated status is present for the next reconciliation. + +Dependent Resources feature supports the [first approach](../documentation/dependent-resource-and-workflows/dependent-resources.md#external-state-tracking-dependent-resources). +### How can I make the status update of my custom resource trigger a reconciliation? + +The framework checks, by default, when an event occurs, that could trigger a reconciliation, if the event increased the +`generation` field of the primary resource's metadata and filters out the event if it did not. `generation` is typically +only increased when the `.spec` field of a resource is changed. As a result, a change in the `.status` field would not +normally trigger a reconciliation. + +To change this behavior, you can set the [ +`generationAwareEventProcessing`](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ControllerConfiguration.java#L43) +to `false`: + +```java + +@ControllerConfiguration(generationAwareEventProcessing = false) +static class TestCustomReconciler implements Reconciler { + + @Override + public UpdateControl reconcile(TestCustomResource resource, Context context) { + // code omitted + } +} +``` + +For secondary resources, every change should trigger a reconciliation by default, except when you add explicit filters +or use dependent resource implementations that filter out changes they trigger themselves by default, +see [related docs](../documentation/dependent-resource-and-workflows/dependent-resources.md#caching-and-event-handling-in-kubernetesdependentresource). + ### How can I skip the reconciliation of a dependent resource? Skipping workflow reconciliation altogether is possible with the explicit invocation feature since v5. diff --git a/docs/static/images/cncf_logo2.png b/docs/static/images/cncf_logo2.png new file mode 100644 index 0000000000..e1236b7e87 Binary files /dev/null and b/docs/static/images/cncf_logo2.png differ diff --git a/micrometer-support/pom.xml b/micrometer-support/pom.xml index 73ed6ff77e..ea18d07ce7 100644 --- a/micrometer-support/pom.xml +++ b/micrometer-support/pom.xml @@ -4,7 +4,7 @@ io.javaoperatorsdk java-operator-sdk - 5.1.2-SNAPSHOT + 5.1.3-SNAPSHOT micrometer-support diff --git a/operator-framework-bom/pom.xml b/operator-framework-bom/pom.xml index ccbb4ca4e2..4b314d5719 100644 --- a/operator-framework-bom/pom.xml +++ b/operator-framework-bom/pom.xml @@ -4,7 +4,7 @@ io.javaoperatorsdk operator-framework-bom - 5.1.2-SNAPSHOT + 5.1.3-SNAPSHOT pom Operator SDK - Bill of Materials Java SDK for implementing Kubernetes operators @@ -33,19 +33,12 @@ https://github.com/operator-framework/java-operator-sdk/tree/master - - - ossrh - https://oss.sonatype.org/content/repositories/snapshots - - - - 1.7.0 - 3.2.7 + 3.2.8 3.3.1 3.11.2 2.44.3 + 0.8.0 @@ -115,6 +108,17 @@ release + + org.apache.maven.plugins + maven-surefire-plugin + + + **/*IT.java + **/*E2E.java + **/InformerRelatedBehaviorTest.java + + + org.apache.maven.plugins maven-javadoc-plugin @@ -138,13 +142,13 @@ jar + verify org.apache.maven.plugins maven-gpg-plugin - ${maven-gpg-plugin.version} sign-artifacts @@ -162,14 +166,15 @@ - org.sonatype.plugins - nexus-staging-maven-plugin - ${nexus-staging-maven-plugin.version} + org.sonatype.central + central-publishing-maven-plugin + ${central-publishing-maven-plugin.version} true - ossrh - https://oss.sonatype.org/ - true + central + true + true + published diff --git a/operator-framework-core/pom.xml b/operator-framework-core/pom.xml index 69c02d6f01..c99b609113 100644 --- a/operator-framework-core/pom.xml +++ b/operator-framework-core/pom.xml @@ -4,7 +4,7 @@ io.javaoperatorsdk java-operator-sdk - 5.1.2-SNAPSHOT + 5.1.3-SNAPSHOT ../pom.xml diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/RuntimeInfo.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/RuntimeInfo.java index b7fbce7f07..0495131d79 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/RuntimeInfo.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/RuntimeInfo.java @@ -5,6 +5,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.health.EventSourceHealthIndicator; import io.javaoperatorsdk.operator.health.InformerWrappingEventSourceHealthIndicator; import io.javaoperatorsdk.operator.processing.event.source.controller.ControllerEventSource; @@ -22,7 +23,7 @@ public class RuntimeInfo { private final Operator operator; public RuntimeInfo(Operator operator) { - this.registeredControllers = operator.getRegisteredControllers(); + this.registeredControllers = Collections.unmodifiableSet(operator.getRegisteredControllers()); this.operator = operator; } @@ -30,6 +31,7 @@ public boolean isStarted() { return operator.isStarted(); } + @SuppressWarnings("unused") public Set getRegisteredControllers() { checkIfStarted(); return registeredControllers; @@ -80,4 +82,23 @@ public Map> unhealthyEventSource } return res; } + + /** + * Retrieves the {@link RegisteredController} associated with the specified controller name or + * {@code null} if no such controller is registered. + * + * @param controllerName the name of the {@link RegisteredController} to retrieve + * @return the {@link RegisteredController} associated with the specified controller name or + * {@code null} if no such controller is registered + * @since 5.1.2 + */ + @SuppressWarnings({"unchecked", "unused"}) + public RegisteredController getRegisteredController( + String controllerName) { + checkIfStarted(); + return registeredControllers.stream() + .filter(rc -> rc.getConfiguration().getName().equals(controllerName)) + .findFirst() + .orElse(null); + } } 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 438f7d91a9..891f199dbe 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 @@ -30,6 +30,12 @@ import static io.javaoperatorsdk.operator.api.config.ControllerConfiguration.CONTROLLER_NAME_AS_FIELD_MANAGER; +/** + * A default {@link ConfigurationService} implementation, resolving {@link Reconciler}s + * configuration when it has already been resolved before. If this behavior is not adequate, please + * use {@link AbstractConfigurationService} instead as a base for your {@code ConfigurationService} + * implementation. + */ public class BaseConfigurationService extends AbstractConfigurationService { private static final String LOGGER_NAME = "Default ConfigurationService implementation"; @@ -149,10 +155,12 @@ private static void configureFromAnnotatedReconciler( @Override protected void logMissingReconcilerWarning(String reconcilerKey, String reconcilersNameMessage) { - logger.warn( - "Configuration for reconciler '{}' was not found. {}", - reconcilerKey, - reconcilersNameMessage); + if (!createIfNeeded()) { + logger.warn( + "Configuration for reconciler '{}' was not found. {}", + reconcilerKey, + reconcilersNameMessage); + } } @SuppressWarnings("unused") @@ -318,6 +326,13 @@ private

ResolvedControllerConfiguration

controllerCon informerConfig); } + /** + * @deprecated This method was meant to allow subclasses to prevent automatic creation of the + * configuration when not found. This functionality is now removed, if you want to be able to + * prevent automated, on-demand creation of a reconciler's configuration, please use the + * {@link AbstractConfigurationService} implementation instead as base for your extension. + */ + @Deprecated(forRemoval = true) protected boolean createIfNeeded() { return true; } 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 18e74d29a9..41134e64ac 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 @@ -28,7 +28,6 @@ import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent; import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource; import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResourceConfig; -import io.javaoperatorsdk.operator.processing.dependent.kubernetes.ResourceUpdaterMatcher; import io.javaoperatorsdk.operator.processing.dependent.workflow.ManagedWorkflowFactory; import io.javaoperatorsdk.operator.processing.event.source.controller.ControllerEventSource; @@ -396,9 +395,6 @@ default boolean shouldUseSSA( Class dependentResourceType, Class resourceType, KubernetesDependentResourceConfig config) { - if (ResourceUpdaterMatcher.class.isAssignableFrom(dependentResourceType)) { - return false; - } Boolean useSSAConfig = Optional.ofNullable(config).map(KubernetesDependentResourceConfig::useSSA).orElse(null); // don't use SSA for certain resources by default, only if explicitly overridden @@ -489,7 +485,6 @@ default Set> withPreviousAnnotationForDependentReso * * @return if resource version should be parsed (as integer) * @since 4.5.0 - * @return if resource version should be parsed (as integer) */ default boolean parseResourceVersionsForEventFilteringAndCaching() { return false; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/UpdateControl.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/UpdateControl.java index 1b5eefd7ff..1bd98c12d6 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/UpdateControl.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/UpdateControl.java @@ -21,8 +21,7 @@ private UpdateControl(P resource, boolean patchResource, boolean patchStatus) { } /** - * Preferred way to update the status. It does not do optimistic locking. Uses JSON Patch to patch - * the resource. + * Preferred way to update the status. Uses JSON Patch to patch the resource. * *

Note that this does not work, if the {@link CustomResource#initStatus()} is implemented, * since it breaks the diffing process. Don't implement it if using this method. There is also an diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/ResourceUpdaterMatcher.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/ResourceUpdaterMatcher.java deleted file mode 100644 index d893ff3e86..0000000000 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/ResourceUpdaterMatcher.java +++ /dev/null @@ -1,11 +0,0 @@ -package io.javaoperatorsdk.operator.processing.dependent.kubernetes; - -import io.fabric8.kubernetes.api.model.HasMetadata; -import io.javaoperatorsdk.operator.api.reconciler.Context; - -public interface ResourceUpdaterMatcher { - - R updateResource(R actual, R desired, Context context); - - boolean matches(R actual, R desired, Context context); -} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/WorkflowReconcileExecutor.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/WorkflowReconcileExecutor.java index 065e790ba4..9e29305b51 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/WorkflowReconcileExecutor.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/WorkflowReconcileExecutor.java @@ -144,9 +144,11 @@ protected void doRun(DependentResourceNode dependentResourceNode) { log.debug("Reconciling for primary: {} node: {} ", primaryID, dependentResourceNode); ReconcileResult reconcileResult = dependentResource.reconcile(primary, context); final var detailBuilder = createOrGetResultFor(dependentResourceNode); - detailBuilder.withReconcileResult(reconcileResult).markAsVisited(); - if (isConditionMet(dependentResourceNode.getReadyPostcondition(), dependentResourceNode)) { + boolean isReadyPostconditionMet = + isConditionMet(dependentResourceNode.getReadyPostcondition(), dependentResourceNode); + detailBuilder.withReconcileResult(reconcileResult).markAsVisited(); + if (isReadyPostconditionMet) { log.debug( "Setting already reconciled for: {} primaryID: {}", dependentResourceNode, primaryID); handleDependentsReconcile(dependentResourceNode); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java index b52dc278f2..c029a54170 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java @@ -95,7 +95,7 @@ private InformerEventSource( parseResourceVersions); // If there is a primary to secondary mapper there is no need for primary to secondary index. primaryToSecondaryMapper = configuration.getPrimaryToSecondaryMapper(); - if (primaryToSecondaryMapper == null) { + if (useSecondaryToPrimaryIndex()) { primaryToSecondaryIndex = // The index uses the secondary to primary mapper (always present) to build the index new DefaultPrimaryToSecondaryIndex<>(configuration.getSecondaryToPrimaryMapper()); @@ -157,6 +157,14 @@ public void onDelete(R resource, boolean b) { } } + @Override + public synchronized void start() { + super.start(); + // this makes sure that on first reconciliation all resources are + // present on the index + manager().list().forEach(primaryToSecondaryIndex::onAddOrUpdate); + } + private synchronized void onAddOrUpdate( Operation operation, R newObject, R oldObject, Runnable superOnOp) { var resourceID = ResourceID.fromResource(newObject); 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 3205bca523..a08989c8ce 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 @@ -57,7 +57,12 @@ void setup() { .thenReturn(mock(SecondaryToPrimaryMapper.class)); when(informerEventSourceConfiguration.getResourceClass()).thenReturn(Deployment.class); - informerEventSource = new InformerEventSource<>(informerEventSourceConfiguration, clientMock); + informerEventSource = + new InformerEventSource<>(informerEventSourceConfiguration, clientMock) { + // mocking start + @Override + public synchronized void start() {} + }; var mockControllerConfig = mock(ControllerConfiguration.class); when(mockControllerConfig.getConfigurationService()).thenReturn(new BaseConfigurationService()); diff --git a/operator-framework-junit5/pom.xml b/operator-framework-junit5/pom.xml index 07bfa9cd1c..8c8a349af0 100644 --- a/operator-framework-junit5/pom.xml +++ b/operator-framework-junit5/pom.xml @@ -4,7 +4,7 @@ io.javaoperatorsdk java-operator-sdk - 5.1.2-SNAPSHOT + 5.1.3-SNAPSHOT operator-framework-junit-5 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 00bf7e8380..794bc11d9a 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 @@ -111,6 +111,10 @@ public T create(T resource) { return kubernetesClient.resource(resource).inNamespace(namespace).create(); } + public T serverSideApply(T resource) { + return kubernetesClient.resource(resource).inNamespace(namespace).serverSideApply(); + } + public T replace(T resource) { return kubernetesClient.resource(resource).inNamespace(namespace).replace(); } diff --git a/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/LocallyRunOperatorExtension.java b/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/LocallyRunOperatorExtension.java index 3e6ad35e52..54cb57544d 100644 --- a/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/LocallyRunOperatorExtension.java +++ b/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/LocallyRunOperatorExtension.java @@ -54,6 +54,7 @@ public class LocallyRunOperatorExtension extends AbstractOperatorExtension { private final List> additionalCustomResourceDefinitions; private final Map registeredControllers; private final Map crdMappings; + private final Consumer beforeStartHook; private LocallyRunOperatorExtension( List reconcilers, @@ -68,7 +69,8 @@ private LocallyRunOperatorExtension( Consumer configurationServiceOverrider, Function namespaceNameSupplier, Function perClassNamespaceNameSupplier, - List additionalCrds) { + List additionalCrds, + Consumer beforeStartHook) { super( infrastructure, infrastructureTimeout, @@ -82,6 +84,7 @@ private LocallyRunOperatorExtension( this.portForwards = portForwards; this.localPortForwards = new ArrayList<>(portForwards.size()); this.additionalCustomResourceDefinitions = additionalCustomResourceDefinitions; + this.beforeStartHook = beforeStartHook; configurationServiceOverrider = configurationServiceOverrider != null ? configurationServiceOverrider.andThen( @@ -298,6 +301,10 @@ protected void before(ExtensionContext context) { }); crdMappings.clear(); + if (beforeStartHook != null) { + beforeStartHook.accept(this); + } + LOGGER.debug("Starting the operator locally"); this.operator.start(); } @@ -356,6 +363,7 @@ public static class Builder extends AbstractBuilder { private final List portForwards; private final List> additionalCustomResourceDefinitions; private final List additionalCRDs = new ArrayList<>(); + private Consumer beforeStartHook; private KubernetesClient kubernetesClient; protected Builder() { @@ -424,6 +432,15 @@ public Builder withAdditionalCRD(String... paths) { return this; } + /** + * Used to initialize resources when the namespace is generated but the operator is not started + * yet. + */ + public Builder withBeforeStartHook(Consumer beforeStartHook) { + this.beforeStartHook = beforeStartHook; + return this; + } + public LocallyRunOperatorExtension build() { return new LocallyRunOperatorExtension( reconcilers, @@ -438,7 +455,8 @@ public LocallyRunOperatorExtension build() { configurationServiceOverrider, namespaceNameSupplier, perClassNamespaceNameSupplier, - additionalCRDs); + additionalCRDs, + beforeStartHook); } } diff --git a/operator-framework/pom.xml b/operator-framework/pom.xml index 18cbda43cf..9324f16835 100644 --- a/operator-framework/pom.xml +++ b/operator-framework/pom.xml @@ -4,7 +4,7 @@ io.javaoperatorsdk java-operator-sdk - 5.1.2-SNAPSHOT + 5.1.3-SNAPSHOT operator-framework diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/startsecondaryaccess/StartupSecondaryAccessCustomResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/startsecondaryaccess/StartupSecondaryAccessCustomResource.java new file mode 100644 index 0000000000..b9701c94bd --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/startsecondaryaccess/StartupSecondaryAccessCustomResource.java @@ -0,0 +1,13 @@ +package io.javaoperatorsdk.operator.baseapi.startsecondaryaccess; + +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("ssac") +public class StartupSecondaryAccessCustomResource extends CustomResource + implements Namespaced {} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/startsecondaryaccess/StartupSecondaryAccessIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/startsecondaryaccess/StartupSecondaryAccessIT.java new file mode 100644 index 0000000000..61fc40803c --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/startsecondaryaccess/StartupSecondaryAccessIT.java @@ -0,0 +1,53 @@ +package io.javaoperatorsdk.operator.baseapi.startsecondaryaccess; + +import java.util.Map; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; + +import static io.javaoperatorsdk.operator.baseapi.startsecondaryaccess.StartupSecondaryAccessReconciler.LABEL_KEY; +import static io.javaoperatorsdk.operator.baseapi.startsecondaryaccess.StartupSecondaryAccessReconciler.LABEL_VALUE; +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +class StartupSecondaryAccessIT { + + public static final int SECONDARY_NUMBER = 200; + + @RegisterExtension + static LocallyRunOperatorExtension extension = + LocallyRunOperatorExtension.builder() + .withReconciler(new StartupSecondaryAccessReconciler()) + .withBeforeStartHook( + ex -> { + var primary = new StartupSecondaryAccessCustomResource(); + primary.setMetadata(new ObjectMetaBuilder().withName("test1").build()); + primary = ex.serverSideApply(primary); + + for (int i = 0; i < SECONDARY_NUMBER; i++) { + ConfigMap cm = new ConfigMap(); + cm.setMetadata( + new ObjectMetaBuilder() + .withLabels(Map.of(LABEL_KEY, LABEL_VALUE)) + .withNamespace(ex.getNamespace()) + .withName("cm" + i) + .build()); + cm.addOwnerReference(primary); + ex.serverSideApply(cm); + } + }) + .build(); + + @Test + void reconcilerSeeAllSecondaryResources() { + var reconciler = extension.getReconcilerOfType(StartupSecondaryAccessReconciler.class); + + await().untilAsserted(() -> assertThat(reconciler.isReconciled()).isTrue()); + + assertThat(reconciler.isSecondaryAndCacheSameAmount()).isTrue(); + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/startsecondaryaccess/StartupSecondaryAccessReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/startsecondaryaccess/StartupSecondaryAccessReconciler.java new file mode 100644 index 0000000000..a2c51fdafd --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/startsecondaryaccess/StartupSecondaryAccessReconciler.java @@ -0,0 +1,75 @@ +package io.javaoperatorsdk.operator.baseapi.startsecondaryaccess; + +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.javaoperatorsdk.operator.api.config.informer.InformerEventSourceConfiguration; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; +import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; +import io.javaoperatorsdk.operator.api.reconciler.Reconciler; +import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; +import io.javaoperatorsdk.operator.processing.event.source.EventSource; +import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource; + +import static io.javaoperatorsdk.operator.baseapi.startsecondaryaccess.StartupSecondaryAccessIT.SECONDARY_NUMBER; + +@ControllerConfiguration +public class StartupSecondaryAccessReconciler + implements Reconciler { + + private static final Logger log = LoggerFactory.getLogger(StartupSecondaryAccessReconciler.class); + + public static final String LABEL_KEY = "app"; + public static final String LABEL_VALUE = "secondary-test"; + + private InformerEventSource cmInformer; + + private boolean secondaryAndCacheSameAmount = true; + private boolean reconciled = false; + + @Override + public UpdateControl reconcile( + StartupSecondaryAccessCustomResource resource, + Context context) { + + var secondary = context.getSecondaryResources(ConfigMap.class); + var cached = cmInformer.list().toList(); + + log.info( + "Secondary number: {}, cached: {}, expected: {}", + secondary.size(), + cached.size(), + SECONDARY_NUMBER); + + if (secondary.size() != cached.size()) { + secondaryAndCacheSameAmount = false; + } + reconciled = true; + return UpdateControl.noUpdate(); + } + + @Override + public List> prepareEventSources( + EventSourceContext context) { + cmInformer = + new InformerEventSource<>( + InformerEventSourceConfiguration.from( + ConfigMap.class, StartupSecondaryAccessCustomResource.class) + .withLabelSelector(LABEL_KEY + "=" + LABEL_VALUE) + .build(), + context); + return List.of(cmInformer); + } + + public boolean isSecondaryAndCacheSameAmount() { + return secondaryAndCacheSameAmount; + } + + public boolean isReconciled() { + return reconciled; + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/dependentresourcecrossref/DependentResourceCrossRefIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/dependentresourcecrossref/DependentResourceCrossRefIT.java index ae5cd25895..1b71c79448 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/dependentresourcecrossref/DependentResourceCrossRefIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/dependentresourcecrossref/DependentResourceCrossRefIT.java @@ -44,6 +44,22 @@ void dependentResourceCanReferenceEachOther() { assertThat(operator.get(Secret.class, TEST_RESOURCE_NAME + i)).isNotNull(); } }); + + for (int i = 0; i < EXECUTION_NUMBER; i++) { + operator.delete(testResource(i)); + } + await() + .timeout(Duration.ofSeconds(30)) + .untilAsserted( + () -> { + for (int i = 0; i < EXECUTION_NUMBER; i++) { + assertThat( + operator.get( + DependentResourceCrossRefResource.class, + testResource(i).getMetadata().getName())) + .isNull(); + } + }); } DependentResourceCrossRefResource testResource(int n) { diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/dependentresourcecrossref/DependentResourceCrossRefReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/dependentresourcecrossref/DependentResourceCrossRefReconciler.java index 5d54ecdabe..247174838c 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/dependentresourcecrossref/DependentResourceCrossRefReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/dependentresourcecrossref/DependentResourceCrossRefReconciler.java @@ -5,6 +5,9 @@ import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import io.fabric8.kubernetes.api.model.ConfigMap; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; import io.fabric8.kubernetes.api.model.Secret; @@ -26,6 +29,8 @@ @ControllerConfiguration public class DependentResourceCrossRefReconciler implements Reconciler { + private static final Logger log = + LoggerFactory.getLogger(DependentResourceCrossRefReconciler.class); public static final String SECRET_NAME = "secret"; private final AtomicInteger numberOfExecutions = new AtomicInteger(0); @@ -48,6 +53,7 @@ public ErrorStatusUpdateControl updateErrorSt DependentResourceCrossRefResource resource, Context context, Exception e) { + log.error("Status update on error", e); errorHappened = true; return ErrorStatusUpdateControl.noStatusUpdate(); } diff --git a/pom.xml b/pom.xml index f4dd22edbc..0dbd0879d8 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ io.javaoperatorsdk java-operator-sdk - 5.1.2-SNAPSHOT + 5.1.3-SNAPSHOT pom Operator SDK for Java Java SDK for implementing Kubernetes operators @@ -44,13 +44,6 @@ https://github.com/operator-framework/java-operator-sdk/tree/main - - - ossrh - https://oss.sonatype.org/content/repositories/snapshots - - - UTF-8 17 @@ -59,38 +52,39 @@ java-operator-sdk https://sonarcloud.io jdk - 5.13.0 + 5.13.4 7.3.1 2.0.17 - 2.24.3 + 2.25.1 5.18.0 - 3.17.0 + 3.18.0 0.21.0 1.13.0 - 3.27.3 + 3.27.4 4.3.0 2.7.3 - 1.15.1 - 3.2.0 + 1.15.3 + 3.2.2 0.9.14 - 2.19.0 - 4.15 + 2.20.0 + 4.16 2.11 3.14.0 3.5.3 + 0.8.0 3.11.2 3.3.1 3.3.1 3.4.2 3.5.0 - 3.2.7 + 3.2.8 1.7.0 3.0.0 3.1.4 9.0.2 - 3.4.5 - 2.44.5 + 3.4.6 + 2.46.1 @@ -293,6 +287,15 @@ org.apache.maven.plugins maven-source-plugin ${maven-source-plugin.version} + + + attach-sources + + jar + + verify + + org.apache.maven.plugins @@ -515,14 +518,6 @@ org.apache.maven.plugins maven-source-plugin - - - attach-sources - - jar - - - org.apache.maven.plugins @@ -544,14 +539,15 @@ - org.sonatype.plugins - nexus-staging-maven-plugin - ${nexus-staging-maven-plugin.version} + org.sonatype.central + central-publishing-maven-plugin + ${central-publishing-maven-plugin.version} true - ossrh - https://oss.sonatype.org/ - true + central + true + true + published diff --git a/sample-operators/controller-namespace-deletion/pom.xml b/sample-operators/controller-namespace-deletion/pom.xml index 9a87338da5..312e2fb199 100644 --- a/sample-operators/controller-namespace-deletion/pom.xml +++ b/sample-operators/controller-namespace-deletion/pom.xml @@ -5,7 +5,7 @@ io.javaoperatorsdk sample-operators - 5.1.2-SNAPSHOT + 5.1.3-SNAPSHOT sample-controller-namespace-deletion diff --git a/sample-operators/leader-election/pom.xml b/sample-operators/leader-election/pom.xml index ca74158ae6..f01406b132 100644 --- a/sample-operators/leader-election/pom.xml +++ b/sample-operators/leader-election/pom.xml @@ -5,7 +5,7 @@ io.javaoperatorsdk sample-operators - 5.1.2-SNAPSHOT + 5.1.3-SNAPSHOT sample-leader-election diff --git a/sample-operators/mysql-schema/pom.xml b/sample-operators/mysql-schema/pom.xml index 94b2f93769..cf1be19cbb 100644 --- a/sample-operators/mysql-schema/pom.xml +++ b/sample-operators/mysql-schema/pom.xml @@ -5,7 +5,7 @@ io.javaoperatorsdk sample-operators - 5.1.2-SNAPSHOT + 5.1.3-SNAPSHOT sample-mysql-schema-operator diff --git a/sample-operators/pom.xml b/sample-operators/pom.xml index 2f1c9c1645..7763767a1f 100644 --- a/sample-operators/pom.xml +++ b/sample-operators/pom.xml @@ -5,7 +5,7 @@ io.javaoperatorsdk java-operator-sdk - 5.1.2-SNAPSHOT + 5.1.3-SNAPSHOT sample-operators diff --git a/sample-operators/tomcat-operator/pom.xml b/sample-operators/tomcat-operator/pom.xml index 38d6b4ec0c..3a9b640db8 100644 --- a/sample-operators/tomcat-operator/pom.xml +++ b/sample-operators/tomcat-operator/pom.xml @@ -5,7 +5,7 @@ io.javaoperatorsdk sample-operators - 5.1.2-SNAPSHOT + 5.1.3-SNAPSHOT sample-tomcat-operator diff --git a/sample-operators/webpage/pom.xml b/sample-operators/webpage/pom.xml index 7f118be1bb..d3a691a93a 100644 --- a/sample-operators/webpage/pom.xml +++ b/sample-operators/webpage/pom.xml @@ -5,7 +5,7 @@ io.javaoperatorsdk sample-operators - 5.1.2-SNAPSHOT + 5.1.3-SNAPSHOT sample-webpage-operator