From b351b520dcec51f1cb3bede60292389640e9f5d0 Mon Sep 17 00:00:00 2001 From: Steven Hawkins Date: Fri, 17 Oct 2025 03:38:56 -0400 Subject: [PATCH 01/75] fix: reducing log level of failure to patch in handleErrorStatusHandler (#2983) closes: #2981 Signed-off-by: Steve Hawkins --- .../event/ReconciliationDispatcher.java | 37 ++++++++++++++++--- 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcher.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcher.java index 8e0293f3b4..ea79bb8483 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,10 +1,12 @@ package io.javaoperatorsdk.operator.processing.event; import java.lang.reflect.InvocationTargetException; +import java.net.HttpURLConnection; import java.util.function.Function; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.slf4j.event.Level; import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.api.model.KubernetesResourceList; @@ -216,12 +218,35 @@ public boolean isLastAttempt() { customResourceFacade.patchStatus( errorStatusUpdateControl.getResource().orElseThrow(), originalResource); } catch (Exception ex) { - log.error( - "updateErrorStatus failed for resource: {} with version: {} for error {}", - getUID(resource), - getVersion(resource), - e.getMessage(), - ex); + int code = ex instanceof KubernetesClientException kcex ? kcex.getCode() : -1; + Level exceptionLevel = Level.ERROR; + String failedMessage = ""; + if (context.isNextReconciliationImminent() + || !(errorStatusUpdateControl.isNoRetry() || retryInfo.isLastAttempt())) { + if (code == HttpURLConnection.HTTP_CONFLICT + || (originalResource.getMetadata().getResourceVersion() != null && code == 422)) { + exceptionLevel = Level.DEBUG; + failedMessage = " due to conflict"; + log.info( + "ErrorStatusUpdateControl.patchStatus of {} failed due to a conflict, but the next" + + " reconiliation is imminent.", + ResourceID.fromResource(originalResource)); + } else { + exceptionLevel = Level.WARN; + failedMessage = ", but will be retried soon,"; + } + } + + log.atLevel(exceptionLevel) + .log( + "ErrorStatusUpdateControl.patchStatus failed{} for {} with UID: {} and version: {}" + + " for error {}", + failedMessage, + ResourceID.fromResource(originalResource), + getUID(resource), + getVersion(resource), + e.getMessage(), + ex); } } if (errorStatusUpdateControl.isNoRetry()) { From 3db519a67f75ad60b13a419cc94aa7c77ecce5fb Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Fri, 17 Oct 2025 15:06:37 +0200 Subject: [PATCH 02/75] fix: typo (#3005) Signed-off-by: Chris Laprun --- .../operator/processing/event/ReconciliationDispatcher.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcher.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcher.java index ea79bb8483..90bdc93979 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 @@ -229,7 +229,7 @@ public boolean isLastAttempt() { failedMessage = " due to conflict"; log.info( "ErrorStatusUpdateControl.patchStatus of {} failed due to a conflict, but the next" - + " reconiliation is imminent.", + + " reconciliation is imminent.", ResourceID.fromResource(originalResource)); } else { exceptionLevel = Level.WARN; @@ -503,7 +503,6 @@ public R patchStatus(R resource, R originalResource) { } } - @SuppressWarnings("unchecked") private R editStatus(R resource, R originalResource) { String resourceVersion = resource.getMetadata().getResourceVersion(); // the cached resource should not be changed in any circumstances From d6e76d20f7ff84e00ca644f53f39514ecc970cb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Fri, 17 Oct 2025 15:52:18 +0200 Subject: [PATCH 03/75] feat: CI for java 25 (#2947) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: java 25 support Signed-off-by: Attila Mészáros * use zulu distribution Signed-off-by: Attila Mészáros * fix Signed-off-by: Attila Mészáros * latest google format Signed-off-by: Attila Mészáros * chore: use temurin distribution again, adjust versions Signed-off-by: Chris Laprun --------- Signed-off-by: Attila Mészáros Signed-off-by: Chris Laprun Co-authored-by: Chris Laprun --- .github/workflows/build.yml | 6 +++--- .github/workflows/e2e-test.yml | 2 +- .github/workflows/pr.yml | 2 +- .github/workflows/release-project-in-dir.yml | 2 +- .github/workflows/snapshot-releases.yml | 4 ++-- .github/workflows/sonar.yml | 2 +- pom.xml | 1 + 7 files changed, 10 insertions(+), 9 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e37ab5d8d5..25b234846a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -10,7 +10,7 @@ jobs: integration_tests: strategy: matrix: - java: [ 17, 21, 24 ] + java: [ 17, 21, 25 ] # 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. @@ -26,7 +26,7 @@ jobs: httpclient: [ 'vertx', 'jdk', 'jetty' ] uses: ./.github/workflows/integration-tests.yml with: - java-version: 24 + java-version: 25 kube-version: '1.32.0' http-client: ${{ matrix.httpclient }} experimental: true @@ -36,7 +36,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - java: [ 17, 21, 24 ] + java: [ 17, 21, 25 ] steps: - uses: actions/checkout@v5 - name: Set up Java and Maven diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml index f72a0af43b..7aa92a409c 100644 --- a/.github/workflows/e2e-test.yml +++ b/.github/workflows/e2e-test.yml @@ -43,7 +43,7 @@ jobs: - name: Set up Java and Maven uses: actions/setup-java@v5 with: - java-version: 17 + java-version: 25 distribution: temurin cache: 'maven' diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index df5819f33d..79660cfb1b 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -22,7 +22,7 @@ jobs: uses: actions/setup-java@v5 with: distribution: temurin - java-version: 21 + java-version: 25 cache: 'maven' - name: Check code format run: | diff --git a/.github/workflows/release-project-in-dir.yml b/.github/workflows/release-project-in-dir.yml index 0683b01b1f..0313aebe4d 100644 --- a/.github/workflows/release-project-in-dir.yml +++ b/.github/workflows/release-project-in-dir.yml @@ -80,4 +80,4 @@ jobs: uses: ad-m/github-push-action@master with: branch: "${{inputs.version_branch}}" - github_token: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file + github_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/snapshot-releases.yml b/.github/workflows/snapshot-releases.yml index a12d9aaed5..0f560dd2cb 100644 --- a/.github/workflows/snapshot-releases.yml +++ b/.github/workflows/snapshot-releases.yml @@ -21,7 +21,7 @@ jobs: uses: actions/setup-java@v5 with: distribution: temurin - java-version: 17 + java-version: 21 cache: 'maven' - name: Build and test project run: ./mvnw ${MAVEN_ARGS} clean install --file pom.xml @@ -33,7 +33,7 @@ jobs: - name: Set up Java and Maven uses: actions/setup-java@v5 with: - java-version: 17 + java-version: 21 distribution: temurin cache: 'maven' server-id: central diff --git a/.github/workflows/sonar.yml b/.github/workflows/sonar.yml index 370f36303c..132575edaa 100644 --- a/.github/workflows/sonar.yml +++ b/.github/workflows/sonar.yml @@ -28,7 +28,7 @@ jobs: uses: actions/setup-java@v5 with: distribution: temurin - java-version: 17 + java-version: 25 cache: 'maven' - name: Cache SonarCloud packages uses: actions/cache@v4 diff --git a/pom.xml b/pom.xml index 6a76c1c7a0..303c11f79b 100644 --- a/pom.xml +++ b/pom.xml @@ -344,6 +344,7 @@ + 1.28.0 true From fa288d168ffd2f37b23214aaac8b92a8ba8615d0 Mon Sep 17 00:00:00 2001 From: Donnerbart Date: Tue, 21 Oct 2025 16:01:08 +0200 Subject: [PATCH 04/75] Improve PodTemplateSpec sanitizer for GKE Autopilot compatibility (#3012) * test: update type parameters in SSABasedGenericKubernetesResourceMatcherTest for better type safety * fix: improve PodTemplateSpec sanitizer for GKE Autopilot compatibility Signed-off-by: David Sondermann --- .../kubernetes/PodTemplateSpecSanitizer.java | 1 - .../PodTemplateSpecSanitizerTest.java | 69 +++++++++++++++---- ...dGenericKubernetesResourceMatcherTest.java | 18 ++--- 3 files changed, 62 insertions(+), 26 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/PodTemplateSpecSanitizer.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/PodTemplateSpecSanitizer.java index 962059961e..fd1dcff49c 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/PodTemplateSpecSanitizer.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/PodTemplateSpecSanitizer.java @@ -124,7 +124,6 @@ private static void sanitizeQuantities( "resources", quantityPath)) .map(Map.class::cast) - .filter(m -> m.size() == desiredResource.size()) .ifPresent( m -> actualResource.forEach( diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/PodTemplateSpecSanitizerTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/PodTemplateSpecSanitizerTest.java index 091a1a666c..e7756b45bc 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/PodTemplateSpecSanitizerTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/PodTemplateSpecSanitizerTest.java @@ -132,19 +132,6 @@ void testSanitizePodTemplateSpec_whenResourceIsNull_doNothing() { verifyNoInteractions(actualMap); } - @Test - void testSanitizeResourceRequirements_whenResourceSizeMismatch_doNothing() { - final var actualMap = - sanitizeRequestsAndLimits( - ContainerType.CONTAINER, - Map.of("cpu", new Quantity("2")), - Map.of(), - Map.of("cpu", new Quantity("4")), - Map.of("cpu", new Quantity("4"), "memory", new Quantity("4Gi"))); - assertContainerResources(actualMap, "requests").hasSize(1).containsEntry("cpu", "2"); - assertContainerResources(actualMap, "limits").hasSize(1).containsEntry("cpu", "4"); - } - @Test void testSanitizeResourceRequirements_whenResourceKeyMismatch_doNothing() { final var actualMap = @@ -187,6 +174,34 @@ void testSanitizePodTemplateSpec_whenResourcesHaveNumericalAmountMismatch_doNoth assertInitContainerResources(actualMap, "limits").hasSize(1).containsEntry("cpu", "2"); } + @Test + void + testSanitizePodTemplateSpec_whenResourcesHaveNumericalAmountMismatch_withEphemeralStorageAddedByOtherOperator_doNothing() { + // mimics an environment like GKE Autopilot that enforces ephemeral-storage requests and limits + final var actualMap = + sanitizeRequestsAndLimits( + ContainerType.INIT_CONTAINER, + Map.of( + "cpu", + new Quantity("2"), + "memory", + new Quantity("4Gi"), + "ephemeral-storage", + new Quantity("1Gi")), + Map.of("cpu", new Quantity("4"), "memory", new Quantity("4Ti")), + Map.of("cpu", new Quantity("2"), "ephemeral-storage", new Quantity("1Gi")), + Map.of("cpu", new Quantity("4000m"))); + assertInitContainerResources(actualMap, "requests") + .hasSize(3) + .containsEntry("cpu", "2") + .containsEntry("memory", "4Gi") + .containsEntry("ephemeral-storage", "1Gi"); + assertInitContainerResources(actualMap, "limits") + .hasSize(2) + .containsEntry("cpu", "2") + .containsEntry("ephemeral-storage", "1Gi"); + } + @Test void testSanitizeResourceRequirements_whenResourcesHaveAmountAndFormatMismatchWithSameNumericalAmount_thenSanitizeActualMap() { @@ -204,6 +219,34 @@ void testSanitizePodTemplateSpec_whenResourcesHaveNumericalAmountMismatch_doNoth assertContainerResources(actualMap, "limits").hasSize(1).containsEntry("cpu", "4000m"); } + @Test + void + testSanitizeResourceRequirements_whenResourcesHaveAmountAndFormatMismatchWithSameNumericalAmount_withEphemeralStorageAddedByOtherOperator_thenSanitizeActualMap() { + // mimics an environment like GKE Autopilot that enforces ephemeral-storage requests and limits + final var actualMap = + sanitizeRequestsAndLimits( + ContainerType.CONTAINER, + Map.of( + "cpu", + new Quantity("2"), + "memory", + new Quantity("4Gi"), + "ephemeral-storage", + new Quantity("1Gi")), + Map.of("cpu", new Quantity("2000m"), "memory", new Quantity("4096Mi")), + Map.of("cpu", new Quantity("4"), "ephemeral-storage", new Quantity("1Gi")), + Map.of("cpu", new Quantity("4000m"))); + assertContainerResources(actualMap, "requests") + .hasSize(3) + .containsEntry("cpu", "2000m") + .containsEntry("memory", "4096Mi") + .containsEntry("ephemeral-storage", "1Gi"); + assertContainerResources(actualMap, "limits") + .hasSize(2) + .containsEntry("cpu", "4000m") + .containsEntry("ephemeral-storage", "1Gi"); + } + @Test void testSanitizePodTemplateSpec_whenEnvVarsIsEmpty_doNothing() { final var template = diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/SSABasedGenericKubernetesResourceMatcherTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/SSABasedGenericKubernetesResourceMatcherTest.java index c339e5ebf6..e441516d46 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/SSABasedGenericKubernetesResourceMatcherTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/SSABasedGenericKubernetesResourceMatcherTest.java @@ -30,7 +30,7 @@ class SSABasedGenericKubernetesResourceMatcherTest { - private final Context mockedContext = mock(); + private final Context mockedContext = mock(); private final SSABasedGenericKubernetesResourceMatcher matcher = SSABasedGenericKubernetesResourceMatcher.getInstance(); @@ -325,8 +325,8 @@ void testSanitizeState_daemonSet_withResourceTypeMismatch() { void testCustomMatcher_returnsExpectedMatchBasedOnReadOnlyLabel(boolean readOnly) { var dr = new ConfigMapDR(); dr.configureWith( - new KubernetesDependentResourceConfigBuilder() - .withSSAMatcher(new ReadOnlyAwareMatcher()) + new KubernetesDependentResourceConfigBuilder() + .withSSAMatcher(new ReadOnlyAwareMatcher<>()) .build()); var desiredConfigMap = loadResource("configmap.empty-owner-reference-desired.yaml", ConfigMap.class); @@ -334,14 +334,8 @@ void testCustomMatcher_returnsExpectedMatchBasedOnReadOnlyLabel(boolean readOnly var actualConfigMap = loadResource("configmap.empty-owner-reference.yaml", ConfigMap.class); actualConfigMap.getMetadata().getLabels().put("readonly", Boolean.toString(readOnly)); - ConfigMap ignoredPrimary = null; - assertThat( - dr.match( - actualConfigMap, - desiredConfigMap, - ignoredPrimary, - (Context) mockedContext) - .matched()) + HasMetadata primary = mock(); + assertThat(dr.match(actualConfigMap, desiredConfigMap, primary, mockedContext).matched()) .isEqualTo(readOnly); } @@ -391,7 +385,7 @@ private static R loadResource(String fileName, Class clazz) { clazz, SSABasedGenericKubernetesResourceMatcherTest.class, fileName); } - private static class ConfigMapDR extends KubernetesDependentResource { + private static class ConfigMapDR extends KubernetesDependentResource { public ConfigMapDR() { super(ConfigMap.class); } From a25ddb132c23b6e30de37c524c31f6e885083696 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Wed, 22 Oct 2025 12:57:35 +0200 Subject: [PATCH 05/75] improve: showcase issue where updating the spec with SSA removes the finalizer (#2848) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../SSASpecUpdateCustomResource.java | 14 ++++++ .../SSASpecUpdateCustomResourceSpec.java | 14 ++++++ .../SSASpecUpdateCustomResourceStatus.java | 15 ++++++ .../ssaissue/specupdate/SSASpecUpdateIT.java | 41 ++++++++++++++++ .../specupdate/SSASpecUpdateReconciler.java | 47 +++++++++++++++++++ 5 files changed, 131 insertions(+) create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/ssaissue/specupdate/SSASpecUpdateCustomResource.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/ssaissue/specupdate/SSASpecUpdateCustomResourceSpec.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/ssaissue/specupdate/SSASpecUpdateCustomResourceStatus.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/ssaissue/specupdate/SSASpecUpdateIT.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/ssaissue/specupdate/SSASpecUpdateReconciler.java diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/ssaissue/specupdate/SSASpecUpdateCustomResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/ssaissue/specupdate/SSASpecUpdateCustomResource.java new file mode 100644 index 0000000000..c4ee2dd1a5 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/ssaissue/specupdate/SSASpecUpdateCustomResource.java @@ -0,0 +1,14 @@ +package io.javaoperatorsdk.operator.baseapi.ssaissue.specupdate; + +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("ssul") +public class SSASpecUpdateCustomResource + extends CustomResource + implements Namespaced {} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/ssaissue/specupdate/SSASpecUpdateCustomResourceSpec.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/ssaissue/specupdate/SSASpecUpdateCustomResourceSpec.java new file mode 100644 index 0000000000..47953ccce0 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/ssaissue/specupdate/SSASpecUpdateCustomResourceSpec.java @@ -0,0 +1,14 @@ +package io.javaoperatorsdk.operator.baseapi.ssaissue.specupdate; + +public class SSASpecUpdateCustomResourceSpec { + + private String value; + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/ssaissue/specupdate/SSASpecUpdateCustomResourceStatus.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/ssaissue/specupdate/SSASpecUpdateCustomResourceStatus.java new file mode 100644 index 0000000000..e5f103dc69 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/ssaissue/specupdate/SSASpecUpdateCustomResourceStatus.java @@ -0,0 +1,15 @@ +package io.javaoperatorsdk.operator.baseapi.ssaissue.specupdate; + +public class SSASpecUpdateCustomResourceStatus { + + private Integer value = 0; + + public Integer getValue() { + return value; + } + + public SSASpecUpdateCustomResourceStatus setValue(Integer value) { + this.value = value; + return this; + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/ssaissue/specupdate/SSASpecUpdateIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/ssaissue/specupdate/SSASpecUpdateIT.java new file mode 100644 index 0000000000..9cccd32e3c --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/ssaissue/specupdate/SSASpecUpdateIT.java @@ -0,0 +1,41 @@ +package io.javaoperatorsdk.operator.baseapi.ssaissue.specupdate; + +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 static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +class SSASpecUpdateIT { + + public static final String TEST_RESOURCE_NAME = "test"; + + @RegisterExtension + LocallyRunOperatorExtension operator = + LocallyRunOperatorExtension.builder().withReconciler(SSASpecUpdateReconciler.class).build(); + + // showcases that if the spec of the resources is updated with SSA, but the finalizer + // is not explicitly added to the fresh resource, the update removes the finalizer + @Test + void showFinalizerRemovalWhenSpecUpdated() { + SSASpecUpdateCustomResource res = createResource(); + operator.create(res); + + await() + .untilAsserted( + () -> { + var actual = operator.get(SSASpecUpdateCustomResource.class, TEST_RESOURCE_NAME); + assertThat(actual.getSpec()).isNotNull(); + assertThat(actual.getFinalizers()).isEmpty(); + }); + } + + SSASpecUpdateCustomResource createResource() { + SSASpecUpdateCustomResource res = new SSASpecUpdateCustomResource(); + res.setMetadata(new ObjectMetaBuilder().withName(TEST_RESOURCE_NAME).build()); + return res; + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/ssaissue/specupdate/SSASpecUpdateReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/ssaissue/specupdate/SSASpecUpdateReconciler.java new file mode 100644 index 0000000000..483060ad59 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/ssaissue/specupdate/SSASpecUpdateReconciler.java @@ -0,0 +1,47 @@ +package io.javaoperatorsdk.operator.baseapi.ssaissue.specupdate; + +import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.javaoperatorsdk.operator.api.reconciler.Cleaner; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; +import io.javaoperatorsdk.operator.api.reconciler.DeleteControl; +import io.javaoperatorsdk.operator.api.reconciler.Reconciler; +import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; + +@ControllerConfiguration +public class SSASpecUpdateReconciler + implements Reconciler, Cleaner { + + @Override + public UpdateControl reconcile( + SSASpecUpdateCustomResource resource, Context context) { + + var copy = createFreshCopy(resource); + copy.getSpec().setValue("value"); + context + .getClient() + .resource(copy) + .fieldManager(context.getControllerConfiguration().fieldManager()) + .serverSideApply(); + + return UpdateControl.noUpdate(); + } + + SSASpecUpdateCustomResource createFreshCopy(SSASpecUpdateCustomResource resource) { + var res = new SSASpecUpdateCustomResource(); + res.setMetadata( + new ObjectMetaBuilder() + .withName(resource.getMetadata().getName()) + .withNamespace(resource.getMetadata().getNamespace()) + .build()); + res.setSpec(new SSASpecUpdateCustomResourceSpec()); + return res; + } + + @Override + public DeleteControl cleanup( + SSASpecUpdateCustomResource resource, Context context) { + + return DeleteControl.defaultDelete(); + } +} From 61ed0ef0ab00afd4aaf06d47f50f7e94699bdf37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Wed, 22 Oct 2025 16:57:25 +0200 Subject: [PATCH 06/75] improve: showcase initialized spec deleted when adding finalizer with SSA with same fieldManager (#2847) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../SSAFinalizerIssueCustomResource.java | 13 ++++ .../finalizer/SSAFinalizerIssueIT.java | 66 +++++++++++++++++++ .../SSAFinalizerIssueReconciler.java | 26 ++++++++ .../finalizer/SSAFinalizerIssueSpec.java | 26 ++++++++ .../finalizer/SSAFinalizerIssueStatus.java | 19 ++++++ 5 files changed, 150 insertions(+) create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/ssaissue/finalizer/SSAFinalizerIssueCustomResource.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/ssaissue/finalizer/SSAFinalizerIssueIT.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/ssaissue/finalizer/SSAFinalizerIssueReconciler.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/ssaissue/finalizer/SSAFinalizerIssueSpec.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/ssaissue/finalizer/SSAFinalizerIssueStatus.java diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/ssaissue/finalizer/SSAFinalizerIssueCustomResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/ssaissue/finalizer/SSAFinalizerIssueCustomResource.java new file mode 100644 index 0000000000..0e31be2dfb --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/ssaissue/finalizer/SSAFinalizerIssueCustomResource.java @@ -0,0 +1,13 @@ +package io.javaoperatorsdk.operator.baseapi.ssaissue.finalizer; + +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("ssfi") +public class SSAFinalizerIssueCustomResource + extends CustomResource implements Namespaced {} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/ssaissue/finalizer/SSAFinalizerIssueIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/ssaissue/finalizer/SSAFinalizerIssueIT.java new file mode 100644 index 0000000000..a830675518 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/ssaissue/finalizer/SSAFinalizerIssueIT.java @@ -0,0 +1,66 @@ +package io.javaoperatorsdk.operator.baseapi.ssaissue.finalizer; + +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.fabric8.kubernetes.client.dsl.base.PatchContext; +import io.fabric8.kubernetes.client.dsl.base.PatchType; +import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +class SSAFinalizerIssueIT { + + public static final String TEST_1 = "test1"; + + @RegisterExtension + LocallyRunOperatorExtension extension = + LocallyRunOperatorExtension.builder() + .withReconciler(new SSAFinalizerIssueReconciler()) + .build(); + + /** + * Showcases possibly a case with SSA: When the resource is created with the same field manager + * that used by the controller, when adding a finalizer, it deletes other parts of the spec. + */ + @Test + void addingFinalizerRemoveListValues() { + var fieldManager = + extension + .getRegisteredControllerForReconcile(SSAFinalizerIssueReconciler.class) + .getConfiguration() + .fieldManager(); + + extension + .getKubernetesClient() + .resource(testResource()) + .inNamespace(extension.getNamespace()) + .patch( + new PatchContext.Builder() + .withFieldManager(fieldManager) + .withForce(true) + .withPatchType(PatchType.SERVER_SIDE_APPLY) + .build()); + + await() + .untilAsserted( + () -> { + var actual = extension.get(SSAFinalizerIssueCustomResource.class, TEST_1); + assertThat(actual.getFinalizers()).hasSize(1); + assertThat(actual.getSpec()).isNull(); + }); + } + + SSAFinalizerIssueCustomResource testResource() { + var res = new SSAFinalizerIssueCustomResource(); + res.setMetadata(new ObjectMetaBuilder().withName(TEST_1).build()); + res.setSpec(new SSAFinalizerIssueSpec()); + res.getSpec().setValue("val"); + res.getSpec().setList(List.of("val1", "val2")); + return res; + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/ssaissue/finalizer/SSAFinalizerIssueReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/ssaissue/finalizer/SSAFinalizerIssueReconciler.java new file mode 100644 index 0000000000..441819834a --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/ssaissue/finalizer/SSAFinalizerIssueReconciler.java @@ -0,0 +1,26 @@ +package io.javaoperatorsdk.operator.baseapi.ssaissue.finalizer; + +import io.javaoperatorsdk.operator.api.reconciler.Cleaner; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; +import io.javaoperatorsdk.operator.api.reconciler.DeleteControl; +import io.javaoperatorsdk.operator.api.reconciler.Reconciler; +import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; + +@ControllerConfiguration +public class SSAFinalizerIssueReconciler + implements Reconciler, + Cleaner { + + @Override + public DeleteControl cleanup( + SSAFinalizerIssueCustomResource resource, Context context) { + return DeleteControl.defaultDelete(); + } + + @Override + public UpdateControl reconcile( + SSAFinalizerIssueCustomResource resource, Context context) { + return UpdateControl.noUpdate(); + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/ssaissue/finalizer/SSAFinalizerIssueSpec.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/ssaissue/finalizer/SSAFinalizerIssueSpec.java new file mode 100644 index 0000000000..d1c9dd5966 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/ssaissue/finalizer/SSAFinalizerIssueSpec.java @@ -0,0 +1,26 @@ +package io.javaoperatorsdk.operator.baseapi.ssaissue.finalizer; + +import java.util.List; + +public class SSAFinalizerIssueSpec { + + private String value; + + private List list = null; + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public List getList() { + return list; + } + + public void setList(List list) { + this.list = list; + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/ssaissue/finalizer/SSAFinalizerIssueStatus.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/ssaissue/finalizer/SSAFinalizerIssueStatus.java new file mode 100644 index 0000000000..471372af40 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/ssaissue/finalizer/SSAFinalizerIssueStatus.java @@ -0,0 +1,19 @@ +package io.javaoperatorsdk.operator.baseapi.ssaissue.finalizer; + +public class SSAFinalizerIssueStatus { + + private String configMapStatus; + + public String getConfigMapStatus() { + return configMapStatus; + } + + public void setConfigMapStatus(String configMapStatus) { + this.configMapStatus = configMapStatus; + } + + @Override + public String toString() { + return "TestCustomResourceStatus{" + "configMapStatus='" + configMapStatus + '\'' + '}'; + } +} From 385f1a302381e20113eb7d50a7f07f6e1f69a4fe Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Oct 2025 08:24:41 +0100 Subject: [PATCH 07/75] chore(deps): bump org.apache.maven.plugin-tools:maven-plugin-annotations (#3023) Bumps [org.apache.maven.plugin-tools:maven-plugin-annotations](https://github.com/apache/maven-plugin-tools) from 3.15.1 to 3.15.2. - [Release notes](https://github.com/apache/maven-plugin-tools/releases) - [Commits](https://github.com/apache/maven-plugin-tools/compare/maven-plugin-tools-3.15.1...maven-plugin-tools-3.15.2) --- updated-dependencies: - dependency-name: org.apache.maven.plugin-tools:maven-plugin-annotations dependency-version: 3.15.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- bootstrapper-maven-plugin/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bootstrapper-maven-plugin/pom.xml b/bootstrapper-maven-plugin/pom.xml index c306dcea35..659c52796e 100644 --- a/bootstrapper-maven-plugin/pom.xml +++ b/bootstrapper-maven-plugin/pom.xml @@ -14,7 +14,7 @@ Operator SDK - Bootstrapper Maven Plugin - 3.15.1 + 3.15.2 3.9.11 3.0.0 3.15.1 From 358ed4ee4e839f6ca5ba57e65f953e504df87fe5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Oct 2025 08:24:49 +0100 Subject: [PATCH 08/75] chore(deps): bump org.apache.maven.plugins:maven-plugin-plugin (#3022) Bumps [org.apache.maven.plugins:maven-plugin-plugin](https://github.com/apache/maven-plugin-tools) from 3.15.1 to 3.15.2. - [Release notes](https://github.com/apache/maven-plugin-tools/releases) - [Commits](https://github.com/apache/maven-plugin-tools/compare/maven-plugin-tools-3.15.1...maven-plugin-tools-3.15.2) --- updated-dependencies: - dependency-name: org.apache.maven.plugins:maven-plugin-plugin dependency-version: 3.15.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- bootstrapper-maven-plugin/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bootstrapper-maven-plugin/pom.xml b/bootstrapper-maven-plugin/pom.xml index 659c52796e..5952379112 100644 --- a/bootstrapper-maven-plugin/pom.xml +++ b/bootstrapper-maven-plugin/pom.xml @@ -17,7 +17,7 @@ 3.15.2 3.9.11 3.0.0 - 3.15.1 + 3.15.2 From 6127066bf52afd9dcd41f10cf239c89f6871b1ae Mon Sep 17 00:00:00 2001 From: Donnerbart Date: Mon, 27 Oct 2025 09:54:15 +0100 Subject: [PATCH 09/75] improve: Small health check improvements (#3021) Signed-off-by: David Sondermann --- ...nformerWrappingEventSourceHealthIndicator.java | 9 +++------ .../event/source/informer/InformerWrapper.java | 4 ++-- .../event/source/timer/TimerEventSource.java | 6 ++++++ .../source/polling/PollingEventSourceTest.java | 2 +- .../event/source/timer/TimerEventSourceTest.java | 15 ++++++++++++--- 5 files changed, 24 insertions(+), 12 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/health/InformerWrappingEventSourceHealthIndicator.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/health/InformerWrappingEventSourceHealthIndicator.java index 2c337f3cd7..720ce0227c 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/health/InformerWrappingEventSourceHealthIndicator.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/health/InformerWrappingEventSourceHealthIndicator.java @@ -11,11 +11,8 @@ public interface InformerWrappingEventSourceHealthIndicator i.getStatus() != Status.HEALTHY) - .findAny(); - - return nonUp.isPresent() ? Status.UNHEALTHY : Status.HEALTHY; + var hasNonHealthy = + informerHealthIndicators().values().stream().anyMatch(i -> i.getStatus() != Status.HEALTHY); + return hasNonHealthy ? Status.UNHEALTHY : Status.HEALTHY; } } 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 c07ffdbf46..2bb6dcbc75 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 @@ -204,8 +204,8 @@ public boolean isRunning() { public Status getStatus() { var status = isRunning() && hasSynced() && isWatching() ? Status.HEALTHY : Status.UNHEALTHY; log.debug( - "Informer status: {} for for type: {}, namespace: {}, details[ is running: {}, has synced:" - + " {}, is watching: {} ]", + "Informer status: {} for type: {}, namespace: {}, details [is running: {}, has synced: {}," + + " is watching: {}]", status, informer.getApiTypeClass().getSimpleName(), namespaceIdentifier, diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/timer/TimerEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/timer/TimerEventSource.java index 53c0d328a8..a6801b5f39 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/timer/TimerEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/timer/TimerEventSource.java @@ -10,6 +10,7 @@ import org.slf4j.LoggerFactory; import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.health.Status; import io.javaoperatorsdk.operator.processing.event.Event; import io.javaoperatorsdk.operator.processing.event.ResourceID; import io.javaoperatorsdk.operator.processing.event.source.AbstractEventSource; @@ -77,6 +78,11 @@ public void stop() { } } + @Override + public Status getStatus() { + return isRunning() ? Status.HEALTHY : Status.UNHEALTHY; + } + @Override public Set getSecondaryResources(HasMetadata primary) { return Set.of(); diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/polling/PollingEventSourceTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/polling/PollingEventSourceTest.java index 0f7d26446d..448537e9aa 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/polling/PollingEventSourceTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/polling/PollingEventSourceTest.java @@ -87,7 +87,7 @@ void propagatesEventOnNewResourceForPrimary() throws InterruptedException { } @Test - void updatesHealthIndicatorBasedOnExceptionsInFetcher() throws InterruptedException { + void updatesHealthIndicatorBasedOnExceptionsInFetcher() { when(resourceFetcher.fetchResources()).thenReturn(testResponseWithOneValue()); pollingEventSource.start(); assertThat(pollingEventSource.getStatus()).isEqualTo(Status.HEALTHY); diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/timer/TimerEventSourceTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/timer/TimerEventSourceTest.java index 9396411777..825bc6adab 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/timer/TimerEventSourceTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/timer/TimerEventSourceTest.java @@ -1,6 +1,5 @@ package io.javaoperatorsdk.operator.processing.event.source.timer; -import java.io.IOException; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.TimeUnit; @@ -12,6 +11,7 @@ import org.junit.jupiter.api.Test; import io.javaoperatorsdk.operator.TestUtils; +import io.javaoperatorsdk.operator.health.Status; import io.javaoperatorsdk.operator.processing.event.Event; import io.javaoperatorsdk.operator.processing.event.EventHandler; import io.javaoperatorsdk.operator.processing.event.ResourceID; @@ -42,6 +42,7 @@ public void schedulesOnce() { untilAsserted(() -> assertThat(eventHandler.events).hasSize(1)); untilAsserted(PERIOD * 2, 0, () -> assertThat(eventHandler.events).hasSize(1)); + assertThat(source.getStatus()).isEqualTo(Status.HEALTHY); } @Test @@ -52,6 +53,7 @@ public void canCancelOnce() { source.cancelOnceSchedule(resourceID); untilAsserted(() -> assertThat(eventHandler.events).isEmpty()); + assertThat(source.getStatus()).isEqualTo(Status.HEALTHY); } @Test @@ -62,6 +64,7 @@ public void canRescheduleOnceEvent() { source.scheduleOnce(resourceID, 2 * PERIOD); untilAsserted(PERIOD * 2, PERIOD, () -> assertThat(eventHandler.events).hasSize(1)); + assertThat(source.getStatus()).isEqualTo(Status.HEALTHY); } @Test @@ -72,23 +75,29 @@ public void deRegistersOnceEventSources() { source.onResourceDeleted(customResource); untilAsserted(() -> assertThat(eventHandler.events).isEmpty()); + assertThat(source.getStatus()).isEqualTo(Status.HEALTHY); } @Test - public void eventNotRegisteredIfStopped() throws IOException { + public void eventNotRegisteredIfStopped() { var resourceID = ResourceID.fromResource(TestUtils.testCustomResource()); + assertThat(source.getStatus()).isEqualTo(Status.HEALTHY); source.stop(); assertThatExceptionOfType(IllegalStateException.class) .isThrownBy(() -> source.scheduleOnce(resourceID, PERIOD)); + assertThat(source.getStatus()).isEqualTo(Status.UNHEALTHY); } @Test - public void eventNotFiredIfStopped() throws IOException { + public void eventNotFiredIfStopped() { source.scheduleOnce(ResourceID.fromResource(TestUtils.testCustomResource()), PERIOD); + assertThat(source.getStatus()).isEqualTo(Status.HEALTHY); + source.stop(); untilAsserted(() -> assertThat(eventHandler.events).isEmpty()); + assertThat(source.getStatus()).isEqualTo(Status.UNHEALTHY); } private void untilAsserted(ThrowingRunnable assertion) { From 1c881f55757fa768b085bbff234ecf0dc80e9a87 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 29 Oct 2025 08:45:51 +0100 Subject: [PATCH 10/75] chore(deps): bump com.github.ben-manes.caffeine:caffeine (#3025) Bumps [com.github.ben-manes.caffeine:caffeine](https://github.com/ben-manes/caffeine) from 3.2.2 to 3.2.3. - [Release notes](https://github.com/ben-manes/caffeine/releases) - [Commits](https://github.com/ben-manes/caffeine/compare/v3.2.2...v3.2.3) --- updated-dependencies: - dependency-name: com.github.ben-manes.caffeine:caffeine dependency-version: 3.2.3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 303c11f79b..ca4dd282d6 100644 --- a/pom.xml +++ b/pom.xml @@ -64,7 +64,7 @@ 4.3.0 2.7.3 1.15.5 - 3.2.2 + 3.2.3 0.9.14 2.20.0 4.16 From ad55db8fafe59a053b9636549a7fc3d099c87dc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Mon, 3 Nov 2025 15:29:36 +0100 Subject: [PATCH 11/75] improve: marking long running tests as integration test (#3028) --- .../{BootstrapperTest.java => BootstrapperIT.java} | 4 ++-- .../operator/{OperatorTest.java => OperatorIT.java} | 3 ++- ...rExtensionTest.java => LocallyRunOperatorExtensionIT.java} | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) rename bootstrapper-maven-plugin/src/test/java/io/javaoperatorsdk/bootstrapper/{BootstrapperTest.java => BootstrapperIT.java} (96%) rename operator-framework-core/src/test/java/io/javaoperatorsdk/operator/{OperatorTest.java => OperatorIT.java} (99%) rename operator-framework-junit5/src/test/java/io/javaoperatorsdk/operator/junit/{LocallyRunOperatorExtensionTest.java => LocallyRunOperatorExtensionIT.java} (95%) diff --git a/bootstrapper-maven-plugin/src/test/java/io/javaoperatorsdk/bootstrapper/BootstrapperTest.java b/bootstrapper-maven-plugin/src/test/java/io/javaoperatorsdk/bootstrapper/BootstrapperIT.java similarity index 96% rename from bootstrapper-maven-plugin/src/test/java/io/javaoperatorsdk/bootstrapper/BootstrapperTest.java rename to bootstrapper-maven-plugin/src/test/java/io/javaoperatorsdk/bootstrapper/BootstrapperIT.java index f7840c1585..ec1399fcf8 100644 --- a/bootstrapper-maven-plugin/src/test/java/io/javaoperatorsdk/bootstrapper/BootstrapperTest.java +++ b/bootstrapper-maven-plugin/src/test/java/io/javaoperatorsdk/bootstrapper/BootstrapperIT.java @@ -13,9 +13,9 @@ import static org.assertj.core.api.Assertions.assertThat; -class BootstrapperTest { +class BootstrapperIT { - private static final Logger log = LoggerFactory.getLogger(BootstrapperTest.class); + private static final Logger log = LoggerFactory.getLogger(BootstrapperIT.class); Bootstrapper bootstrapper = new Bootstrapper(); diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/OperatorTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/OperatorIT.java similarity index 99% rename from operator-framework-core/src/test/java/io/javaoperatorsdk/operator/OperatorTest.java rename to operator-framework-core/src/test/java/io/javaoperatorsdk/operator/OperatorIT.java index 39fc98f6b0..42309d5c44 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/OperatorTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/OperatorIT.java @@ -15,7 +15,8 @@ import static org.junit.jupiter.api.Assertions.*; @SuppressWarnings("rawtypes") -class OperatorTest { +class OperatorIT { + @Test void shouldBePossibleToRetrieveNumberOfRegisteredControllers() { final var operator = new Operator(); diff --git a/operator-framework-junit5/src/test/java/io/javaoperatorsdk/operator/junit/LocallyRunOperatorExtensionTest.java b/operator-framework-junit5/src/test/java/io/javaoperatorsdk/operator/junit/LocallyRunOperatorExtensionIT.java similarity index 95% rename from operator-framework-junit5/src/test/java/io/javaoperatorsdk/operator/junit/LocallyRunOperatorExtensionTest.java rename to operator-framework-junit5/src/test/java/io/javaoperatorsdk/operator/junit/LocallyRunOperatorExtensionIT.java index 04ac7a91ae..e195ce8406 100644 --- a/operator-framework-junit5/src/test/java/io/javaoperatorsdk/operator/junit/LocallyRunOperatorExtensionTest.java +++ b/operator-framework-junit5/src/test/java/io/javaoperatorsdk/operator/junit/LocallyRunOperatorExtensionIT.java @@ -9,7 +9,7 @@ import static org.junit.jupiter.api.Assertions.*; -class LocallyRunOperatorExtensionTest { +class LocallyRunOperatorExtensionIT { @Test void getAdditionalCRDsFromFiles() { From f350562b455278756d9ad8402096686e1db472cf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 7 Nov 2025 14:03:47 +0100 Subject: [PATCH 12/75] chore(deps): bump io.micrometer:micrometer-core from 1.15.5 to 1.16.0 (#3033) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index ca4dd282d6..12613ea0c5 100644 --- a/pom.xml +++ b/pom.xml @@ -63,7 +63,7 @@ 3.27.6 4.3.0 2.7.3 - 1.15.5 + 1.16.0 3.2.3 0.9.14 2.20.0 From c27b0c6b868095b18caaf68a6e887d9e6b096075 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Nov 2025 04:37:46 +0100 Subject: [PATCH 13/75] chore(deps): bump commons-io:commons-io from 2.20.0 to 2.21.0 (#3035) Bumps [commons-io:commons-io](https://github.com/apache/commons-io) from 2.20.0 to 2.21.0. - [Changelog](https://github.com/apache/commons-io/blob/master/RELEASE-NOTES.txt) - [Commits](https://github.com/apache/commons-io/compare/rel/commons-io-2.20.0...rel/commons-io-2.21.0) --- updated-dependencies: - dependency-name: commons-io:commons-io dependency-version: 2.21.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- bootstrapper-maven-plugin/pom.xml | 2 +- pom.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bootstrapper-maven-plugin/pom.xml b/bootstrapper-maven-plugin/pom.xml index 5952379112..a55636840b 100644 --- a/bootstrapper-maven-plugin/pom.xml +++ b/bootstrapper-maven-plugin/pom.xml @@ -58,7 +58,7 @@ commons-io commons-io - 2.20.0 + 2.21.0 com.github.spullara.mustache.java diff --git a/pom.xml b/pom.xml index 12613ea0c5..6391b298dc 100644 --- a/pom.xml +++ b/pom.xml @@ -66,7 +66,7 @@ 1.16.0 3.2.3 0.9.14 - 2.20.0 + 2.21.0 4.16 2.11 From 6eb248689aec7ef542dab002a4077b90b1151793 Mon Sep 17 00:00:00 2001 From: Steven Hawkins Date: Wed, 12 Nov 2025 14:53:04 -0500 Subject: [PATCH 14/75] fix: checking for null to avoid NPE with SSA matching (#3037) closes: #3034 Signed-off-by: Steve Hawkins --- ...BasedGenericKubernetesResourceMatcher.java | 9 + ...dGenericKubernetesResourceMatcherTest.java | 23 + ...atefulset-with-managed-fields-missing.yaml | 435 ++++++++++++++++++ 3 files changed, 467 insertions(+) create mode 100644 operator-framework-core/src/test/resources/io/javaoperatorsdk/operator/processing/dependent/kubernetes/statefulset-with-managed-fields-missing.yaml diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/SSABasedGenericKubernetesResourceMatcher.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/SSABasedGenericKubernetesResourceMatcher.java index 4954dfd17a..6b8c829908 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/SSABasedGenericKubernetesResourceMatcher.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/SSABasedGenericKubernetesResourceMatcher.java @@ -320,6 +320,10 @@ private static void handleListKeyEntrySet( result.put(keyInActual, valueList); var actualValueList = (List>) actualMap.get(keyInActual); + if (actualValueList == null) { + return; + } + var targetValuesByIndex = new TreeMap>(); var managedEntryByIndex = new HashMap>(); @@ -396,6 +400,11 @@ private static void handleSetValues( continue; } var values = (List) actualMap.get(keyInActual); + + if (values == null || values.isEmpty()) { + continue; + } + var targetClass = (values.get(0) instanceof Map) ? null : values.get(0).getClass(); var value = parseKeyValue(keyWithoutPrefix(valueEntry.getKey()), targetClass, objectMapper); valueList.add(value); diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/SSABasedGenericKubernetesResourceMatcherTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/SSABasedGenericKubernetesResourceMatcherTest.java index e441516d46..185ff73e93 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/SSABasedGenericKubernetesResourceMatcherTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/SSABasedGenericKubernetesResourceMatcherTest.java @@ -49,6 +49,29 @@ void setup() { when(mockedContext.getControllerConfiguration()).thenReturn(controllerConfiguration); } + @Test + void statefulSetWithMissingManagedField() { + var actual = loadResource("statefulset-with-managed-fields-missing.yaml", StatefulSet.class); + var desired = + actual + .edit() + .editMetadata() + .withManagedFields(List.of()) + .endMetadata() + .editSpec() + .editTemplate() + .editSpec() + .editFirstContainer() + .withImage("new") + .endContainer() + .endSpec() + .endTemplate() + .endSpec() + .build(); + + assertThat(matcher.matches(actual, desired, mockedContext)).isFalse(); + } + @Test void noMatchWhenNoMatchingController() { var desired = loadResource("nginx-deployment.yaml", Deployment.class); diff --git a/operator-framework-core/src/test/resources/io/javaoperatorsdk/operator/processing/dependent/kubernetes/statefulset-with-managed-fields-missing.yaml b/operator-framework-core/src/test/resources/io/javaoperatorsdk/operator/processing/dependent/kubernetes/statefulset-with-managed-fields-missing.yaml new file mode 100644 index 0000000000..47dd31b0c1 --- /dev/null +++ b/operator-framework-core/src/test/resources/io/javaoperatorsdk/operator/processing/dependent/kubernetes/statefulset-with-managed-fields-missing.yaml @@ -0,0 +1,435 @@ +apiVersion: apps/v1 +kind: StatefulSet +metadata: + annotations: + operator.keycloak.org/migrating: "false" + operator.keycloak.org/missing-secrets: "false" + operator.keycloak.org/watching-secrets: companyname-keycloak-the-keycloak-name-initial-admin;companyname-keycloak-the-keycloak-name-postgres-app;ldap-ca-certificates + creationTimestamp: "2025-10-17T16:13:16Z" + generation: 1 + labels: + app: keycloak + app.kubernetes.io/instance: companyname-keycloak-the-keycloak-name + app.kubernetes.io/managed-by: keycloak-operator + name: companyname-keycloak-the-keycloak-name + namespace: ns + ownerReferences: + - apiVersion: k8s.keycloak.org/v2alpha1 + kind: Keycloak + name: companyname-keycloak-the-keycloak-name + uid: 7b72f87b-c6d4-46a0-a7ca-7e10e0b47c0e + resourceVersion: "1722442060" + uid: 03f0d845-92d2-4c4a-8a68-bb495f5e55c0 + managedFields: + - apiVersion: apps/v1 + fieldsType: FieldsV1 + fieldsV1: + f:metadata: + f:annotations: + f:operator.keycloak.org/migrating: {} + f:operator.keycloak.org/missing-secrets: {} + f:operator.keycloak.org/watching-secrets: {} + f:labels: + f:app: {} + f:app.kubernetes.io/instance: {} + f:app.kubernetes.io/managed-by: {} + f:ownerReferences: + k:{"uid":"7b72f87b-c6d4-46a0-a7ca-7e10e0b47c0e"}: {} + f:spec: + f:replicas: {} + f:selector: {} + f:serviceName: {} + f:template: + f:metadata: + f:annotations: + f:operator.keycloak.org/watched-secret-hash: {} + f:labels: + f:app: {} + f:app.kubernetes.io/component: {} + f:app.kubernetes.io/instance: {} + f:app.kubernetes.io/managed-by: {} + f:spec: + f:containers: + k:{"name":"keycloak"}: + .: {} + f:args: {} + f:env: + k:{"name":"KC_BOOTSTRAP_ADMIN_PASSWORD"}: + .: {} + f:name: {} + f:valueFrom: + f:secretKeyRef: {} + k:{"name":"KC_BOOTSTRAP_ADMIN_USERNAME"}: + .: {} + f:name: {} + f:valueFrom: + f:secretKeyRef: {} + k:{"name":"KC_CACHE"}: + .: {} + f:name: {} + f:value: {} + k:{"name":"KC_DB"}: + .: {} + f:name: {} + f:value: {} + k:{"name":"KC_DB_PASSWORD"}: + .: {} + f:name: {} + f:valueFrom: + f:secretKeyRef: {} + k:{"name":"KC_DB_SCHEMA"}: + .: {} + f:name: {} + f:value: {} + k:{"name":"KC_DB_URL_DATABASE"}: + .: {} + f:name: {} + f:value: {} + k:{"name":"KC_DB_URL_HOST"}: + .: {} + f:name: {} + f:value: {} + k:{"name":"KC_DB_USERNAME"}: + .: {} + f:name: {} + f:valueFrom: + f:secretKeyRef: {} + k:{"name":"KC_HEALTH_ENABLED"}: + .: {} + f:name: {} + f:value: {} + k:{"name":"KC_HOSTNAME"}: + .: {} + f:name: {} + f:value: {} + k:{"name":"KC_HTTP_ENABLED"}: + .: {} + f:name: {} + f:value: {} + k:{"name":"KC_HTTP_PORT"}: + .: {} + f:name: {} + f:value: {} + k:{"name":"KC_HTTPS_PORT"}: + .: {} + f:name: {} + f:value: {} + k:{"name":"KC_METRICS_ENABLED"}: + .: {} + f:name: {} + f:value: {} + k:{"name":"KC_PROXY_HEADERS"}: + .: {} + f:name: {} + f:value: {} + k:{"name":"KC_SPI_CACHE_EMBEDDED_DEFAULT_MACHINE_NAME"}: + .: {} + f:name: {} + f:valueFrom: + f:fieldRef: {} + k:{"name":"KC_TRACING_RESOURCE_ATTRIBUTES"}: + .: {} + f:name: {} + f:value: {} + k:{"name":"KC_TRACING_SERVICE_NAME"}: + .: {} + f:name: {} + f:value: {} + k:{"name":"KC_TRUSTSTORE_PATHS"}: + .: {} + f:name: {} + f:value: {} + k:{"name":"KCKEY_METRICS_ENABLED"}: + .: {} + f:name: {} + f:value: {} + k:{"name":"POD_IP"}: + .: {} + f:name: {} + f:valueFrom: + f:fieldRef: {} + f:image: {} + f:imagePullPolicy: {} + f:livenessProbe: + f:failureThreshold: {} + f:httpGet: + f:path: {} + f:port: {} + f:scheme: {} + f:periodSeconds: {} + f:name: {} + f:ports: + k:{"containerPort":8080,"protocol":"TCP"}: + .: {} + f:containerPort: {} + f:name: {} + f:protocol: {} + k:{"containerPort":8443,"protocol":"TCP"}: + .: {} + f:containerPort: {} + f:name: {} + f:protocol: {} + k:{"containerPort":9000,"protocol":"TCP"}: + .: {} + f:containerPort: {} + f:name: {} + f:protocol: {} + f:readinessProbe: + f:failureThreshold: {} + f:httpGet: + f:path: {} + f:port: {} + f:scheme: {} + f:periodSeconds: {} + f:resources: + f:claims: + k:{"name":"keycloak"}: + .: {} + f:name: {} + f:request: {} + f:limits: + f:memory: {} + f:requests: + f:cpu: {} + f:memory: {} + f:startupProbe: + f:failureThreshold: {} + f:httpGet: + f:path: {} + f:port: {} + f:scheme: {} + f:periodSeconds: {} + f:volumeMounts: + k:{"mountPath":"/opt/keycloak/conf/truststores/secret-ldap-ca-certificates"}: + .: {} + f:mountPath: {} + f:name: {} + f:dnsPolicy: {} + f:restartPolicy: {} + f:terminationGracePeriodSeconds: {} + f:topologySpreadConstraints: + k:{"topologyKey":"kubernetes.io/hostname","whenUnsatisfiable":"ScheduleAnyway"}: + .: {} + f:labelSelector: {} + f:maxSkew: {} + f:topologyKey: {} + f:whenUnsatisfiable: {} + k:{"topologyKey":"topology.kubernetes.io/zone","whenUnsatisfiable":"ScheduleAnyway"}: + .: {} + f:labelSelector: {} + f:maxSkew: {} + f:topologyKey: {} + f:whenUnsatisfiable: {} + f:volumes: + k:{"name":"truststore-secret-ldap-ca-certificates"}: + .: {} + f:name: {} + f:secret: + f:secretName: {} + manager: controller + operation: Apply + time: "2025-10-17T16:13:16Z" + - apiVersion: apps/v1 + fieldsType: FieldsV1 + fieldsV1: + f:status: + f:availableReplicas: {} + f:collisionCount: {} + f:currentReplicas: {} + f:currentRevision: {} + f:observedGeneration: {} + f:readyReplicas: {} + f:replicas: {} + f:updateRevision: {} + f:updatedReplicas: {} + manager: kube-controller-manager + operation: Update + subresource: status + time: "2025-11-01T22:35:47Z" +spec: + persistentVolumeClaimRetentionPolicy: + whenDeleted: Retain + whenScaled: Retain + podManagementPolicy: OrderedReady + replicas: 1 + revisionHistoryLimit: 10 + selector: + matchLabels: + app: keycloak + app.kubernetes.io/instance: companyname-keycloak-the-keycloak-name + app.kubernetes.io/managed-by: keycloak-operator + serviceName: companyname-keycloak-the-keycloak-name-discovery + template: + metadata: + annotations: + operator.keycloak.org/watched-secret-hash: 57e61d6eea030b446d392972aca00088408828a69aecfb201c57d5e1a9dd01bc + creationTimestamp: null + labels: + app: keycloak + app.kubernetes.io/component: server + app.kubernetes.io/instance: companyname-keycloak-the-keycloak-name + app.kubernetes.io/managed-by: keycloak-operator + spec: + containers: + - args: + - -Djgroups.bind.address=$(POD_IP) + - --verbose + - start + - --optimized + env: + - name: KC_HOSTNAME + value: host + - name: KC_HTTP_ENABLED + value: "true" + - name: KC_HTTP_PORT + value: "8080" + - name: KC_HTTPS_PORT + value: "8443" + - name: KC_DB + value: postgres + - name: KC_DB_USERNAME + valueFrom: + secretKeyRef: + key: username + name: companyname-keycloak-the-keycloak-name-postgres-app + - name: KC_DB_PASSWORD + valueFrom: + secretKeyRef: + key: password + name: companyname-keycloak-the-keycloak-name-postgres-app + - name: KC_DB_URL_DATABASE + value: keycloak + - name: KC_DB_URL_HOST + value: companyname-keycloak-the-keycloak-name-postgres-rw + - name: KC_DB_SCHEMA + value: public + - name: KC_PROXY_HEADERS + value: forwarded + - name: KC_BOOTSTRAP_ADMIN_USERNAME + valueFrom: + secretKeyRef: + key: username + name: companyname-keycloak-the-keycloak-name-initial-admin + - name: KC_BOOTSTRAP_ADMIN_PASSWORD + valueFrom: + secretKeyRef: + key: password + name: companyname-keycloak-the-keycloak-name-initial-admin + - name: KC_CACHE + value: ispn + - name: KC_HEALTH_ENABLED + value: "true" + - name: KC_METRICS_ENABLED + value: "true" + - name: KCKEY_METRICS_ENABLED + value: metrics-enabled + - name: POD_IP + valueFrom: + fieldRef: + apiVersion: v1 + fieldPath: status.podIP + - name: KC_SPI_CACHE_EMBEDDED_DEFAULT_MACHINE_NAME + valueFrom: + fieldRef: + apiVersion: v1 + fieldPath: spec.nodeName + - name: KC_TRUSTSTORE_PATHS + value: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt,/var/run/secrets/kubernetes.io/serviceaccount/service-ca.crt + - name: KC_TRACING_SERVICE_NAME + value: companyname-keycloak-the-keycloak-name + - name: KC_TRACING_RESOURCE_ATTRIBUTES + value: k8s.namespace.name=prj-sso-operators + image: companyname-docker.*********/release/keycloak:26.4.1-0 + imagePullPolicy: Always + livenessProbe: + failureThreshold: 5 + httpGet: + path: /health/live + port: 9000 + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 + name: keycloak + ports: + - containerPort: 8443 + name: https + protocol: TCP + - containerPort: 8080 + name: http + protocol: TCP + - containerPort: 9000 + name: management + protocol: TCP + readinessProbe: + failureThreshold: 5 + httpGet: + path: /health/ready + port: 9000 + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 + resources: + limits: + memory: 3Gi + requests: + cpu: 10m + memory: 256Mi + startupProbe: + failureThreshold: 600 + httpGet: + path: /health/started + port: 9000 + scheme: HTTP + periodSeconds: 1 + successThreshold: 1 + timeoutSeconds: 1 + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + volumeMounts: + - mountPath: /opt/keycloak/conf/truststores/secret-ldap-ca-certificates + name: truststore-secret-ldap-ca-certificates + dnsPolicy: ClusterFirst + restartPolicy: Always + schedulerName: default-scheduler + securityContext: {} + terminationGracePeriodSeconds: 30 + topologySpreadConstraints: + - labelSelector: + matchLabels: + app: keycloak + app.kubernetes.io/component: server + app.kubernetes.io/instance: companyname-keycloak-the-keycloak-name + app.kubernetes.io/managed-by: keycloak-operator + maxSkew: 1 + topologyKey: topology.kubernetes.io/zone + whenUnsatisfiable: ScheduleAnyway + - labelSelector: + matchLabels: + app: keycloak + app.kubernetes.io/component: server + app.kubernetes.io/instance: companyname-keycloak-the-keycloak-name + app.kubernetes.io/managed-by: keycloak-operator + maxSkew: 1 + topologyKey: kubernetes.io/hostname + whenUnsatisfiable: ScheduleAnyway + volumes: + - name: truststore-secret-ldap-ca-certificates + secret: + defaultMode: 420 + secretName: ldap-ca-certificates + updateStrategy: + rollingUpdate: + partition: 0 + type: RollingUpdate +status: + availableReplicas: 1 + collisionCount: 0 + currentReplicas: 1 + currentRevision: companyname-keycloak-the-keycloak-name-75d6c885bb + observedGeneration: 1 + readyReplicas: 1 + replicas: 1 + updateRevision: companyname-keycloak-the-keycloak-name-75d6c885bb + updatedReplicas: 1 \ No newline at end of file From d8f95fd54795ce4cee508ac9cc32262739d11e75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Fri, 14 Nov 2025 04:37:35 +0100 Subject: [PATCH 15/75] chore: update version to 5.1.6-SNAPSHOT (#3040) --- bootstrapper-maven-plugin/pom.xml | 2 +- caffeine-bounded-cache-support/pom.xml | 2 +- micrometer-support/pom.xml | 2 +- operator-framework-bom/pom.xml | 2 +- operator-framework-core/pom.xml | 2 +- operator-framework-junit5/pom.xml | 2 +- operator-framework/pom.xml | 2 +- pom.xml | 2 +- sample-operators/controller-namespace-deletion/pom.xml | 2 +- sample-operators/leader-election/pom.xml | 2 +- sample-operators/mysql-schema/pom.xml | 2 +- sample-operators/pom.xml | 2 +- sample-operators/tomcat-operator/pom.xml | 2 +- sample-operators/webpage/pom.xml | 2 +- 14 files changed, 14 insertions(+), 14 deletions(-) diff --git a/bootstrapper-maven-plugin/pom.xml b/bootstrapper-maven-plugin/pom.xml index a55636840b..324d0f9692 100644 --- a/bootstrapper-maven-plugin/pom.xml +++ b/bootstrapper-maven-plugin/pom.xml @@ -5,7 +5,7 @@ io.javaoperatorsdk java-operator-sdk - 5.1.5-SNAPSHOT + 5.1.6-SNAPSHOT bootstrapper diff --git a/caffeine-bounded-cache-support/pom.xml b/caffeine-bounded-cache-support/pom.xml index 76f3db9abc..a77ee12101 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.5-SNAPSHOT + 5.1.6-SNAPSHOT caffeine-bounded-cache-support diff --git a/micrometer-support/pom.xml b/micrometer-support/pom.xml index c66a2d339f..af02906180 100644 --- a/micrometer-support/pom.xml +++ b/micrometer-support/pom.xml @@ -4,7 +4,7 @@ io.javaoperatorsdk java-operator-sdk - 5.1.5-SNAPSHOT + 5.1.6-SNAPSHOT micrometer-support diff --git a/operator-framework-bom/pom.xml b/operator-framework-bom/pom.xml index 7770b05ab8..92b747828b 100644 --- a/operator-framework-bom/pom.xml +++ b/operator-framework-bom/pom.xml @@ -4,7 +4,7 @@ io.javaoperatorsdk operator-framework-bom - 5.1.5-SNAPSHOT + 5.1.6-SNAPSHOT pom Operator SDK - Bill of Materials Java SDK for implementing Kubernetes operators diff --git a/operator-framework-core/pom.xml b/operator-framework-core/pom.xml index 5b4281a1ec..d3b9b4129f 100644 --- a/operator-framework-core/pom.xml +++ b/operator-framework-core/pom.xml @@ -4,7 +4,7 @@ io.javaoperatorsdk java-operator-sdk - 5.1.5-SNAPSHOT + 5.1.6-SNAPSHOT ../pom.xml diff --git a/operator-framework-junit5/pom.xml b/operator-framework-junit5/pom.xml index 5aeeb92d45..eee08fc31c 100644 --- a/operator-framework-junit5/pom.xml +++ b/operator-framework-junit5/pom.xml @@ -4,7 +4,7 @@ io.javaoperatorsdk java-operator-sdk - 5.1.5-SNAPSHOT + 5.1.6-SNAPSHOT operator-framework-junit-5 diff --git a/operator-framework/pom.xml b/operator-framework/pom.xml index f1a100eb75..1cf2a0aa9e 100644 --- a/operator-framework/pom.xml +++ b/operator-framework/pom.xml @@ -4,7 +4,7 @@ io.javaoperatorsdk java-operator-sdk - 5.1.5-SNAPSHOT + 5.1.6-SNAPSHOT operator-framework diff --git a/pom.xml b/pom.xml index 6391b298dc..5c04bdb8e1 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ io.javaoperatorsdk java-operator-sdk - 5.1.5-SNAPSHOT + 5.1.6-SNAPSHOT pom Operator SDK for Java Java SDK for implementing Kubernetes operators diff --git a/sample-operators/controller-namespace-deletion/pom.xml b/sample-operators/controller-namespace-deletion/pom.xml index bb2a8fe099..3c04058c74 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.5-SNAPSHOT + 5.1.6-SNAPSHOT sample-controller-namespace-deletion diff --git a/sample-operators/leader-election/pom.xml b/sample-operators/leader-election/pom.xml index 23873c5d45..6becc69448 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.5-SNAPSHOT + 5.1.6-SNAPSHOT sample-leader-election diff --git a/sample-operators/mysql-schema/pom.xml b/sample-operators/mysql-schema/pom.xml index 8201c1148e..d5e01ca206 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.5-SNAPSHOT + 5.1.6-SNAPSHOT sample-mysql-schema-operator diff --git a/sample-operators/pom.xml b/sample-operators/pom.xml index c19bb7f3f6..914e11d901 100644 --- a/sample-operators/pom.xml +++ b/sample-operators/pom.xml @@ -5,7 +5,7 @@ io.javaoperatorsdk java-operator-sdk - 5.1.5-SNAPSHOT + 5.1.6-SNAPSHOT sample-operators diff --git a/sample-operators/tomcat-operator/pom.xml b/sample-operators/tomcat-operator/pom.xml index 0c43071f16..2f925bba5f 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.5-SNAPSHOT + 5.1.6-SNAPSHOT sample-tomcat-operator diff --git a/sample-operators/webpage/pom.xml b/sample-operators/webpage/pom.xml index 55eafa8490..5f237bbd29 100644 --- a/sample-operators/webpage/pom.xml +++ b/sample-operators/webpage/pom.xml @@ -5,7 +5,7 @@ io.javaoperatorsdk sample-operators - 5.1.5-SNAPSHOT + 5.1.6-SNAPSHOT sample-webpage-operator From 2b789401694cdf7493d8f7f8185b9176177bfb70 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 18 Nov 2025 08:21:59 +0100 Subject: [PATCH 16/75] chore(deps): bump org.apache.maven.plugins:maven-jar-plugin (#3045) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 5c04bdb8e1..dbd8f2c1e4 100644 --- a/pom.xml +++ b/pom.xml @@ -76,7 +76,7 @@ 3.12.0 3.3.1 3.3.1 - 3.4.2 + 3.5.0 3.5.0 3.2.8 1.7.0 From 683071ad77b6a8cb7e1e5717d76077f42667ffa1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 18 Nov 2025 08:40:18 +0100 Subject: [PATCH 17/75] chore(deps): bump com.google.cloud.tools:jib-maven-plugin (#3044) Bumps [com.google.cloud.tools:jib-maven-plugin](https://github.com/GoogleContainerTools/jib) from 3.4.6 to 3.5.0. - [Release notes](https://github.com/GoogleContainerTools/jib/releases) - [Commits](https://github.com/GoogleContainerTools/jib/commits) --- updated-dependencies: - dependency-name: com.google.cloud.tools:jib-maven-plugin dependency-version: 3.5.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index dbd8f2c1e4..7ec473efcf 100644 --- a/pom.xml +++ b/pom.xml @@ -83,7 +83,7 @@ 3.0.0 3.1.4 9.0.2 - 3.4.6 + 3.5.0 3.0.0 From ba6b6768d287294d16804d9d3168d6f7a7fbcc40 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 18 Nov 2025 15:18:00 +0100 Subject: [PATCH 18/75] chore(deps): bump org.apache.commons:commons-lang3 from 3.19.0 to 3.20.0 (#3043) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 7ec473efcf..1af31eb989 100644 --- a/pom.xml +++ b/pom.xml @@ -57,7 +57,7 @@ 2.0.17 2.25.2 5.20.0 - 3.19.0 + 3.20.0 0.23.0 1.13.0 3.27.6 From a01b4cd10d44c84394ca86ad91e5cab294fce5ad Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 19 Nov 2025 09:22:37 +0100 Subject: [PATCH 19/75] chore(deps): bump com.diffplug.spotless:spotless-maven-plugin (#3048) Bumps [com.diffplug.spotless:spotless-maven-plugin](https://github.com/diffplug/spotless) from 3.0.0 to 3.1.0. - [Release notes](https://github.com/diffplug/spotless/releases) - [Changelog](https://github.com/diffplug/spotless/blob/main/CHANGES.md) - [Commits](https://github.com/diffplug/spotless/compare/lib/3.0.0...lib/3.1.0) --- updated-dependencies: - dependency-name: com.diffplug.spotless:spotless-maven-plugin dependency-version: 3.1.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- operator-framework-bom/pom.xml | 2 +- pom.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/operator-framework-bom/pom.xml b/operator-framework-bom/pom.xml index 92b747828b..0c6eac1cd8 100644 --- a/operator-framework-bom/pom.xml +++ b/operator-framework-bom/pom.xml @@ -37,7 +37,7 @@ 3.2.8 3.3.1 3.12.0 - 3.0.0 + 3.1.0 0.9.0 diff --git a/pom.xml b/pom.xml index 1af31eb989..2119f250fb 100644 --- a/pom.xml +++ b/pom.xml @@ -84,7 +84,7 @@ 3.1.4 9.0.2 3.5.0 - 3.0.0 + 3.1.0 From 0441c333c5bbeb19b5f012e2d7b7f53c61dc1828 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 19 Nov 2025 09:55:19 +0100 Subject: [PATCH 20/75] chore(deps): bump com.google.cloud.tools:jib-maven-plugin (#3047) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [com.google.cloud.tools:jib-maven-plugin](https://github.com/GoogleContainerTools/jib) from 3.5.0 to 3.5.1. - [Release notes](https://github.com/GoogleContainerTools/jib/releases) - [Commits](https://github.com/GoogleContainerTools/jib/commits) --- updated-dependencies: - dependency-name: com.google.cloud.tools:jib-maven-plugin dependency-version: 3.5.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Attila Mészáros --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 2119f250fb..9931eefb7f 100644 --- a/pom.xml +++ b/pom.xml @@ -83,7 +83,7 @@ 3.0.0 3.1.4 9.0.2 - 3.5.0 + 3.5.1 3.1.0 From 32810726feac9ce1c6de6f674c2432d43aa0f01d Mon Sep 17 00:00:00 2001 From: Marcin Peck Date: Thu, 20 Nov 2025 13:13:23 +0100 Subject: [PATCH 21/75] fix: add unknown action to MicrometerMetrics for prometheus compatibility (#3038) (#3049) Signed-off-by: Marcin Peck --- .../operator/monitoring/micrometer/MicrometerMetrics.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/micrometer-support/src/main/java/io/javaoperatorsdk/operator/monitoring/micrometer/MicrometerMetrics.java b/micrometer-support/src/main/java/io/javaoperatorsdk/operator/monitoring/micrometer/MicrometerMetrics.java index 26f149249b..1bf376e68d 100644 --- a/micrometer-support/src/main/java/io/javaoperatorsdk/operator/monitoring/micrometer/MicrometerMetrics.java +++ b/micrometer-support/src/main/java/io/javaoperatorsdk/operator/monitoring/micrometer/MicrometerMetrics.java @@ -54,6 +54,7 @@ public class MicrometerMetrics implements Metrics { private static final String EVENTS_DELETE = "events.delete"; private static final String CLUSTER = "cluster"; private static final String SIZE_SUFFIX = ".size"; + private static final String UNKNOWN_ACTION = "UNKNOWN"; private final boolean collectPerResourceMetrics; private final MeterRegistry registry; private final Map gauges = new ConcurrentHashMap<>(); @@ -179,7 +180,8 @@ public void receivedEvent(Event event, Map metadata) { event.getRelatedCustomResourceID(), EVENTS_RECEIVED, metadata, - Tag.of(EVENT, event.getClass().getSimpleName())); + Tag.of(EVENT, event.getClass().getSimpleName()), + Tag.of(ACTION, UNKNOWN_ACTION)); } } From 680cfd7876a89aa6d2d020e728894f2291cffe20 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 21 Nov 2025 09:17:52 +0100 Subject: [PATCH 22/75] chore(deps): bump actions/checkout from 5 to 6 (#3053) --- .github/workflows/build.yml | 2 +- .github/workflows/e2e-test.yml | 2 +- .github/workflows/hugo.yaml | 2 +- .github/workflows/integration-tests.yml | 2 +- .github/workflows/pr.yml | 2 +- .github/workflows/release-project-in-dir.yml | 4 ++-- .github/workflows/snapshot-releases.yml | 4 ++-- .github/workflows/sonar.yml | 2 +- 8 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 25b234846a..b6b7485a6e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -38,7 +38,7 @@ jobs: matrix: java: [ 17, 21, 25 ] steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Set up Java and Maven uses: actions/setup-java@v5 with: diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml index 7aa92a409c..a637c72927 100644 --- a/.github/workflows/e2e-test.yml +++ b/.github/workflows/e2e-test.yml @@ -27,7 +27,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Setup Minikube-Kubernetes uses: manusa/actions-setup-minikube@v2.14.0 diff --git a/.github/workflows/hugo.yaml b/.github/workflows/hugo.yaml index 2c0a63d50d..224bd8db2c 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@v5 + uses: actions/checkout@v6 with: submodules: recursive fetch-depth: 0 diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index fdb8897c07..77c268d6bc 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@v5 + - uses: actions/checkout@v6 with: ref: ${{ inputs.checkout-ref }} - name: Set up Java and Maven diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 79660cfb1b..488ab09518 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@v5 + - uses: actions/checkout@v6 - name: Set up Java and Maven uses: actions/setup-java@v5 with: diff --git a/.github/workflows/release-project-in-dir.yml b/.github/workflows/release-project-in-dir.yml index 0313aebe4d..1a77feb5f1 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@v5 + uses: actions/checkout@v6 with: ref: "${{inputs.version_branch}}" @@ -56,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@v5 + uses: actions/checkout@v6 with: ref: "${{inputs.version_branch}}" diff --git a/.github/workflows/snapshot-releases.yml b/.github/workflows/snapshot-releases.yml index 0f560dd2cb..0d829bba12 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@v5 + - uses: actions/checkout@v6 - name: Set up Java and Maven uses: actions/setup-java@v5 with: @@ -29,7 +29,7 @@ jobs: runs-on: ubuntu-latest needs: test steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Set up Java and Maven uses: actions/setup-java@v5 with: diff --git a/.github/workflows/sonar.yml b/.github/workflows/sonar.yml index 132575edaa..5557d71968 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@v5 + - uses: actions/checkout@v6 - name: Set up Java and Maven uses: actions/setup-java@v5 with: From 5677abe6659a6a9ee69ca65f017d6caab199e6d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Fri, 21 Nov 2025 16:09:16 +0100 Subject: [PATCH 23/75] feat(docs): generating test index (#3052) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .github/workflows/hugo.yaml | 10 ++ operator-framework/pom.xml | 68 +++++++++ .../operator/CRDMappingInTestExtensionIT.java | 10 ++ .../operator/baseapi/ConcurrencyIT.java | 10 ++ .../baseapi/InformerErrorHandlerStartIT.java | 10 ++ .../baseapi/LeaderElectionPermissionIT.java | 10 ++ .../BuiltInResourceCleanerIT.java | 10 ++ .../changenamespace/ChangeNamespaceIT.java | 11 ++ .../CleanerForReconcilerIT.java | 12 ++ .../cleanupconflict/CleanupConflictIT.java | 9 ++ .../ClusterScopedResourceIT.java | 10 ++ .../ConcurrentFinalizerRemovalIT.java | 10 ++ ...pdateInformerEventSourceEventFilterIT.java | 10 ++ .../PreviousAnnotationDisabledIT.java | 9 ++ .../KubernetesResourceStatusUpdateIT.java | 11 ++ ...namicGenericEventSourceRegistrationIT.java | 10 ++ .../ErrorStatusHandlerIT.java | 10 ++ .../operator/baseapi/event/EventSourceIT.java | 10 ++ .../operator/baseapi/filter/FilterIT.java | 10 ++ .../GenericKubernetesResourceHandlingIT.java | 11 ++ .../baseapi/gracefulstop/GracefulStopIT.java | 10 ++ .../InformerEventSourceIT.java | 10 ++ .../InformerRemoteClusterIT.java | 9 ++ .../labelselector/LabelSelectorIT.java | 10 ++ .../LeaderElectionChangeNamespaceIT.java | 10 ++ .../ManualObservedGenerationIT.java | 9 ++ .../baseapi/maxinterval/MaxIntervalIT.java | 10 ++ .../MaxIntervalAfterRetryIT.java | 9 ++ .../MultipleReconcilerSameTypeIT.java | 10 ++ .../MultipleSecondaryEventSourceIT.java | 10 ++ .../multiversioncrd/MultiVersionCRDIT.java | 11 ++ .../NextReconciliationImminentIT.java | 9 ++ .../PatchResourceAndStatusNoSSAIT.java | 9 ++ .../PatchResourceAndStatusWithSSAIT.java | 10 ++ .../PatchResourceWithSSAIT.java | 10 ++ .../PerResourcePollingEventSourceIT.java | 9 ++ .../primaryindexer/PrimaryIndexerIT.java | 11 ++ .../PrimaryToSecondaryIT.java | 10 ++ .../PrimaryToSecondaryMissingIT.java | 10 ++ .../baseapi/ratelimit/RateLimitIT.java | 9 ++ .../operator/baseapi/retry/RetryIT.java | 10 ++ .../baseapi/retry/RetryMaxAttemptIT.java | 10 ++ .../baseapi/simple/ReconcilerExecutorIT.java | 9 ++ .../finalizer/SSAFinalizerIssueIT.java | 10 ++ .../ssaissue/specupdate/SSASpecUpdateIT.java | 10 ++ .../StartupSecondaryAccessIT.java | 10 ++ .../statuscache/StatusPatchCacheIT.java | 10 ++ .../StatusPatchNotLockingForNonSSAIT.java | 10 ++ .../StatusPatchSSAMigrationIT.java | 10 ++ .../StatusUpdateLockingIT.java | 10 ++ .../subresource/SubResourceUpdateIT.java | 10 ++ .../UnmodifiableDependentPartIT.java | 10 ++ .../UpdateStatusInCleanupAndRescheduleIT.java | 10 ++ .../bulkdependent/BulkDependentDeleterIT.java | 10 ++ .../BulkDependentWithConditionIT.java | 10 ++ .../external/BulkExternalDependentIT.java | 10 ++ .../managed/ManagedBulkDependentIT.java | 11 ++ .../readonly/ReadOnlyBulkDependentIT.java | 10 ++ .../standalone/StandaloneBulkDependentIT.java | 10 ++ ...nerForManagedDependentResourcesOnlyIT.java | 10 ++ ...teOnlyIfNotExistingDependentWithSSAIT.java | 10 ++ .../DependentAnnotationSecondaryMapperIT.java | 10 ++ .../DependentCustomMappingAnnotationIT.java | 10 ++ .../DependentDifferentNamespaceIT.java | 11 ++ .../dependentfilter/DependentFilterIT.java | 12 ++ .../DependentOperationEventFilterIT.java | 10 ++ .../DependentReInitializationIT.java | 10 ++ .../DependentResourceCrossRefIT.java | 10 ++ .../dependentssa/DependentSSAMatchingIT.java | 12 ++ .../dependentssa/DependentSSAMigrationIT.java | 10 ++ .../ExternalStateDependentIT.java | 10 ++ .../externalstate/ExternalStateIT.java | 12 ++ .../ExternalStateBulkIT.java | 10 ++ .../GenericKubernetesDependentManagedIT.java | 10 ++ ...enericKubernetesDependentStandaloneIT.java | 10 ++ ...ubernetesDependentGarbageCollectionIT.java | 12 ++ .../MultipleDependentResourceIT.java | 11 ++ ...ependentResourceWithNoDiscriminatorIT.java | 9 ++ ...tipleDependentSameTypeMultiInformerIT.java | 10 ++ ...ipleManagedDependentNoDiscriminatorIT.java | 10 ++ .../MultipleManagedDependentSameTypeIT.java | 10 ++ ...pleManagedExternalDependentSameTypeIT.java | 9 ++ .../MultiOwnerDependentTriggeringIT.java | 10 ++ .../PrevAnnotationBlockReconcilerIT.java | 10 ++ .../DependentPrimaryIndexerIT.java | 10 ++ .../PrimaryToSecondaryDependentIT.java | 11 ++ .../dependent/restart/OperatorRestartIT.java | 9 ++ .../ServiceStrictMatcherIT.java | 9 ++ .../SpecialResourcesDependentIT.java | 10 ++ .../SSAWithLegacyMatcherIT.java | 10 ++ .../StandaloneDependentResourceIT.java | 12 ++ .../StatefulSetDesiredSanitizerIT.java | 9 ++ .../complexdependent/ComplexWorkflowIT.java | 9 ++ .../CRDPresentActivationConditionIT.java | 10 ++ .../WorkflowActivationConditionIT.java | 10 ++ .../ManagedDependentDeleteConditionIT.java | 10 ++ .../MultipleDependentWithActivationIT.java | 10 ++ .../OrderedManagedDependentIT.java | 9 ++ .../WorkflowActivationCleanupIT.java | 9 ++ .../WorkflowActivationConditionIT.java | 10 ++ .../WorkflowAllFeatureIT.java | 10 ++ .../WorkflowExplicitCleanupIT.java | 10 ++ .../WorkflowExplicitInvocationIT.java | 10 ++ .../WorkflowMultipleActivationIT.java | 10 ++ .../WorkflowSilentExceptionHandlingIT.java | 10 ++ pom.xml | 1 + test-index-processor/pom.xml | 31 ++++ .../io/javaoperatorsdk/annotation/Sample.java | 29 ++++ .../processor/SampleProcessor.java | 143 ++++++++++++++++++ .../javax.annotation.processing.Processor | 1 + 110 files changed, 1316 insertions(+) create mode 100644 test-index-processor/pom.xml create mode 100644 test-index-processor/src/main/java/io/javaoperatorsdk/annotation/Sample.java create mode 100644 test-index-processor/src/main/java/io/javaoperatorsdk/processor/SampleProcessor.java create mode 100644 test-index-processor/src/main/resources/META-INF/services/javax.annotation.processing.Processor diff --git a/.github/workflows/hugo.yaml b/.github/workflows/hugo.yaml index 224bd8db2c..320cfb177a 100644 --- a/.github/workflows/hugo.yaml +++ b/.github/workflows/hugo.yaml @@ -45,6 +45,16 @@ jobs: with: submodules: recursive fetch-depth: 0 + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + cache: maven + - name: Build test-index-processor and generate test documentation + run: | + ./mvnw clean install -DskipTests -pl test-index-processor + ./mvnw process-test-classes -DskipTests -pl operator-framework - name: Setup Pages id: pages uses: actions/configure-pages@v5 diff --git a/operator-framework/pom.xml b/operator-framework/pom.xml index 1cf2a0aa9e..6dc9549be0 100644 --- a/operator-framework/pom.xml +++ b/operator-framework/pom.xml @@ -84,6 +84,13 @@ kube-api-test-client-inject test + + io.javaoperatorsdk + test-index-processor + ${project.version} + test + true + @@ -106,6 +113,23 @@ + + + default-testCompile + + testCompile + + test-compile + + + + io.javaoperatorsdk + test-index-processor + ${project.version} + + + + @@ -138,6 +162,50 @@ org.apache.maven.plugins maven-surefire-plugin + + org.apache.maven.plugins + maven-resources-plugin + + + copy-samples-to-docs + + copy-resources + + process-test-classes + + ${project.basedir}/../docs/content/en/docs/testindex + + + ${project.build.directory}/generated-test-sources/test-annotations + + samples.md + + false + + + + + + + + org.apache.maven.plugins + maven-antrun-plugin + 3.1.0 + + + rename-samples-to-index + + run + + process-test-classes + + + + + + + + diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/CRDMappingInTestExtensionIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/CRDMappingInTestExtensionIT.java index 9153ae4ff5..62f5508731 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/CRDMappingInTestExtensionIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/CRDMappingInTestExtensionIT.java @@ -12,6 +12,7 @@ import io.fabric8.kubernetes.model.annotation.Group; import io.fabric8.kubernetes.model.annotation.Kind; import io.fabric8.kubernetes.model.annotation.Version; +import io.javaoperatorsdk.annotation.Sample; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; @@ -21,6 +22,15 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; +@Sample( + tldr = "Custom CRD Mapping in Test Extension", + description = + """ + Demonstrates how to manually specify and apply Custom Resource Definitions (CRDs) in \ + integration tests using the LocallyRunOperatorExtension. This test verifies that CRDs \ + can be loaded from specified file paths and properly registered with the Kubernetes API \ + server during test execution. + """) public class CRDMappingInTestExtensionIT { private final KubernetesClient client = new KubernetesClientBuilder().build(); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/ConcurrencyIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/ConcurrencyIT.java index 80fb09095d..3a336bfb6b 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/ConcurrencyIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/ConcurrencyIT.java @@ -10,6 +10,7 @@ import org.slf4j.LoggerFactory; import io.fabric8.kubernetes.api.model.ConfigMap; +import io.javaoperatorsdk.annotation.Sample; import io.javaoperatorsdk.operator.baseapi.simple.TestCustomResource; import io.javaoperatorsdk.operator.baseapi.simple.TestReconciler; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; @@ -18,6 +19,15 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; +@Sample( + tldr = "Concurrent Reconciliation of Multiple Resources", + description = + """ + Demonstrates the operator's ability to handle concurrent reconciliation of multiple \ + resources. The test creates, updates, and deletes many resources simultaneously to \ + verify proper handling of concurrent operations, ensuring thread safety and correct \ + resource state management under load. + """) class ConcurrencyIT { public static final int NUMBER_OF_RESOURCES_CREATED = 50; public static final int NUMBER_OF_RESOURCES_DELETED = 30; diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/InformerErrorHandlerStartIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/InformerErrorHandlerStartIT.java index 18a107d9b2..f018ba44e7 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/InformerErrorHandlerStartIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/InformerErrorHandlerStartIT.java @@ -9,12 +9,22 @@ import io.fabric8.kubernetes.client.ConfigBuilder; import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.KubernetesClientBuilder; +import io.javaoperatorsdk.annotation.Sample; import io.javaoperatorsdk.operator.Operator; 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; +@Sample( + tldr = "Operator Startup with Informer Errors", + description = + """ + Demonstrates that the operator can start successfully even when informers encounter \ + errors during startup, such as insufficient access rights. By setting \ + stopOnInformerErrorDuringStartup to false, the operator gracefully handles permission \ + errors and continues initialization, allowing it to operate with partial access. + """) class InformerErrorHandlerStartIT { /** Test showcases that the operator starts even if there is no access right for some resource. */ @Test diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/LeaderElectionPermissionIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/LeaderElectionPermissionIT.java index 51b1f4b3d5..4ff7e9c295 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/LeaderElectionPermissionIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/LeaderElectionPermissionIT.java @@ -8,6 +8,7 @@ import io.fabric8.kubernetes.client.ConfigBuilder; import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.KubernetesClientBuilder; +import io.javaoperatorsdk.annotation.Sample; import io.javaoperatorsdk.operator.Operator; import io.javaoperatorsdk.operator.OperatorException; import io.javaoperatorsdk.operator.ReconcilerUtils; @@ -21,6 +22,15 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; +@Sample( + tldr = "Leader Election with Insufficient Permissions", + description = + """ + Verifies that the operator fails gracefully when leader election is configured but \ + the service account lacks permissions to access lease resources. This test ensures \ + proper error handling and messaging when RBAC permissions are insufficient for \ + leader election functionality. + """) class LeaderElectionPermissionIT { KubernetesClient adminClient = new KubernetesClientBuilder().build(); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/builtinresourcecleaner/BuiltInResourceCleanerIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/builtinresourcecleaner/BuiltInResourceCleanerIT.java index 9d914d080c..94ee5325e3 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/builtinresourcecleaner/BuiltInResourceCleanerIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/builtinresourcecleaner/BuiltInResourceCleanerIT.java @@ -8,6 +8,7 @@ import org.slf4j.LoggerFactory; import io.fabric8.kubernetes.api.model.Service; +import io.javaoperatorsdk.annotation.Sample; import io.javaoperatorsdk.operator.ReconcilerUtils; import io.javaoperatorsdk.operator.dependent.standalonedependent.StandaloneDependentResourceIT; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; @@ -15,6 +16,15 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; +@Sample( + tldr = "Cleanup handler for built-in Kubernetes resources", + description = + """ + Demonstrates how to implement cleanup handlers (finalizers) for built-in Kubernetes \ + resources like Service and Pod. These resources don't use generation the same way \ + as custom resources, so this sample shows the proper approach to handle their \ + lifecycle and cleanup logic. + """) class BuiltInResourceCleanerIT { private static final Logger log = LoggerFactory.getLogger(BuiltInResourceCleanerIT.class); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/changenamespace/ChangeNamespaceIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/changenamespace/ChangeNamespaceIT.java index 67f65c64ca..478f351d94 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/changenamespace/ChangeNamespaceIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/changenamespace/ChangeNamespaceIT.java @@ -14,6 +14,7 @@ import io.fabric8.kubernetes.api.model.NamespaceBuilder; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; import io.fabric8.kubernetes.client.KubernetesClient; +import io.javaoperatorsdk.annotation.Sample; import io.javaoperatorsdk.operator.RegisteredController; import io.javaoperatorsdk.operator.api.reconciler.Constants; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; @@ -21,6 +22,16 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; +@Sample( + tldr = "Dynamically Changing Watched Namespaces", + description = + """ + Demonstrates how to dynamically change the set of namespaces that an operator watches at \ + runtime. This feature allows operators to add or remove namespaces from their watch \ + list, including switching between specific namespaces and watching all namespaces. \ + The test verifies that resources in newly added namespaces are reconciled and \ + resources in removed namespaces are no longer watched. + """) class ChangeNamespaceIT { public static final String TEST_RESOURCE_NAME_1 = "test1"; diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/cleanerforreconciler/CleanerForReconcilerIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/cleanerforreconciler/CleanerForReconcilerIT.java index 6ccb34f0e3..03fa506d07 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/cleanerforreconciler/CleanerForReconcilerIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/cleanerforreconciler/CleanerForReconcilerIT.java @@ -4,11 +4,23 @@ import org.junit.jupiter.api.extension.RegisterExtension; import io.fabric8.kubernetes.api.model.ObjectMeta; +import io.javaoperatorsdk.annotation.Sample; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; +@Sample( + tldr = "Implementing Cleanup Logic with Cleaner Interface", + description = + """ + Demonstrates how to implement cleanup logic for custom resources using the Cleaner \ + interface. When a reconciler implements Cleaner, the framework automatically adds a \ + finalizer to resources and calls the cleanup method when the resource is deleted. \ + This pattern is useful for cleaning up external resources or performing custom \ + deletion logic. The test verifies finalizer handling, cleanup execution, and the \ + ability to reschedule cleanup operations. + """) class CleanerForReconcilerIT { public static final String TEST_RESOURCE_NAME = "cleaner-for-reconciler-test1"; diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/cleanupconflict/CleanupConflictIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/cleanupconflict/CleanupConflictIT.java index 19ef6df9a0..0993f76c8f 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/cleanupconflict/CleanupConflictIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/cleanupconflict/CleanupConflictIT.java @@ -6,12 +6,21 @@ import org.junit.jupiter.api.extension.RegisterExtension; import io.fabric8.kubernetes.api.model.ObjectMeta; +import io.javaoperatorsdk.annotation.Sample; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; import static io.javaoperatorsdk.operator.baseapi.cleanupconflict.CleanupConflictReconciler.WAIT_TIME; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; +@Sample( + tldr = "Cleanup Finalizer Removal Without Conflicts", + description = + """ + Tests that finalizers are removed correctly during cleanup without causing conflicts, \ + even when multiple finalizers are present and removed concurrently. This verifies the \ + operator's ability to handle finalizer updates safely during resource deletion. + """) class CleanupConflictIT { private static final String ADDITIONAL_FINALIZER = "javaoperatorsdk.io/additionalfinalizer"; diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/clusterscopedresource/ClusterScopedResourceIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/clusterscopedresource/ClusterScopedResourceIT.java index 4c92fbada7..2a690035da 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/clusterscopedresource/ClusterScopedResourceIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/clusterscopedresource/ClusterScopedResourceIT.java @@ -7,12 +7,22 @@ import io.fabric8.kubernetes.api.model.ConfigMap; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.javaoperatorsdk.annotation.Sample; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; import static io.javaoperatorsdk.operator.IntegrationTestConstants.GARBAGE_COLLECTION_TIMEOUT_SECONDS; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; +@Sample( + tldr = "Cluster-scoped resource reconciliation", + description = + """ + Demonstrates how to reconcile cluster-scoped custom resources (non-namespaced). This \ + test shows CRUD operations on cluster-scoped resources and verifies that \ + dependent resources are created, updated, and properly cleaned up when the \ + primary resource is deleted. + """) class ClusterScopedResourceIT { public static final String TEST_NAME = "test1"; diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/concurrentfinalizerremoval/ConcurrentFinalizerRemovalIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/concurrentfinalizerremoval/ConcurrentFinalizerRemovalIT.java index da166ce2c1..d83c1b1b79 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/concurrentfinalizerremoval/ConcurrentFinalizerRemovalIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/concurrentfinalizerremoval/ConcurrentFinalizerRemovalIT.java @@ -6,12 +6,22 @@ import org.slf4j.LoggerFactory; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.javaoperatorsdk.annotation.Sample; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; import io.javaoperatorsdk.operator.processing.retry.GenericRetry; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; +@Sample( + tldr = "Concurrent Finalizer Removal by Multiple Reconcilers", + description = + """ + Demonstrates safe concurrent finalizer removal when multiple reconcilers manage the \ + same resource with different finalizers. Tests that finalizers can be removed \ + concurrently without conflicts or race conditions, ensuring proper cleanup even when \ + multiple controllers are involved. + """) class ConcurrentFinalizerRemovalIT { private static final Logger log = LoggerFactory.getLogger(ConcurrentFinalizerRemovalIT.class); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/createupdateeventfilter/CreateUpdateInformerEventSourceEventFilterIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/createupdateeventfilter/CreateUpdateInformerEventSourceEventFilterIT.java index 2d9a7db573..38cdb8726b 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/createupdateeventfilter/CreateUpdateInformerEventSourceEventFilterIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/createupdateeventfilter/CreateUpdateInformerEventSourceEventFilterIT.java @@ -7,12 +7,22 @@ import io.fabric8.kubernetes.api.model.ConfigMap; import io.fabric8.kubernetes.api.model.ObjectMeta; +import io.javaoperatorsdk.annotation.Sample; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; import static io.javaoperatorsdk.operator.baseapi.createupdateeventfilter.CreateUpdateEventFilterTestReconciler.CONFIG_MAP_TEST_DATA_KEY; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; +@Sample( + tldr = "Event filtering for create and update operations", + description = + """ + Shows how to configure event filters on informer event sources to control which create and \ + update events trigger reconciliation. This is useful for preventing unnecessary \ + reconciliation loops when dependent resources are modified by the controller \ + itself. + """) class CreateUpdateInformerEventSourceEventFilterIT { @RegisterExtension diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/createupdateeventfilter/PreviousAnnotationDisabledIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/createupdateeventfilter/PreviousAnnotationDisabledIT.java index b5554493ee..90ba1d9cb1 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/createupdateeventfilter/PreviousAnnotationDisabledIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/createupdateeventfilter/PreviousAnnotationDisabledIT.java @@ -3,8 +3,17 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; +import io.javaoperatorsdk.annotation.Sample; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; +@Sample( + tldr = "Event Filtering with Previous Annotation Disabled", + description = + """ + Tests event filtering behavior when the previous annotation feature for dependent \ + resources is disabled. Verifies that update events are properly received and handled \ + even without the annotation tracking mechanism that compares previous resource states. + """) class PreviousAnnotationDisabledIT { @RegisterExtension diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/deployment/KubernetesResourceStatusUpdateIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/deployment/KubernetesResourceStatusUpdateIT.java index 5fe9bfa939..fc6ee36387 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/deployment/KubernetesResourceStatusUpdateIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/deployment/KubernetesResourceStatusUpdateIT.java @@ -17,12 +17,23 @@ import io.fabric8.kubernetes.api.model.PodTemplateSpec; import io.fabric8.kubernetes.api.model.apps.Deployment; import io.fabric8.kubernetes.api.model.apps.DeploymentSpec; +import io.javaoperatorsdk.annotation.Sample; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; import static io.javaoperatorsdk.operator.baseapi.deployment.DeploymentReconciler.STATUS_MESSAGE; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; +@Sample( + tldr = "Reconciling Non-Custom Kubernetes Resources with Status Updates", + description = + """ + Demonstrates how to reconcile standard Kubernetes resources (like Deployments) instead of \ + custom resources, and how to update their status subresource. This pattern is useful when \ + building operators that manage native Kubernetes resources rather than custom resource \ + definitions. The test verifies that the operator can watch, reconcile, and update the \ + status of a Deployment resource. + """) class KubernetesResourceStatusUpdateIT { @RegisterExtension diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/dynamicgenericeventsourceregistration/DynamicGenericEventSourceRegistrationIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/dynamicgenericeventsourceregistration/DynamicGenericEventSourceRegistrationIT.java index 5c05850b4a..c9e1fbc713 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/dynamicgenericeventsourceregistration/DynamicGenericEventSourceRegistrationIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/dynamicgenericeventsourceregistration/DynamicGenericEventSourceRegistrationIT.java @@ -8,11 +8,21 @@ import io.fabric8.kubernetes.api.model.ConfigMap; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; import io.fabric8.kubernetes.api.model.Secret; +import io.javaoperatorsdk.annotation.Sample; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; +@Sample( + tldr = "Dynamic Generic Event Source Registration", + description = + """ + Demonstrates dynamic registration of generic event sources during runtime. The test \ + verifies that event sources can be dynamically added to a reconciler and properly \ + trigger reconciliation when the associated resources change, enabling flexible event \ + source management. + """) class DynamicGenericEventSourceRegistrationIT { public static final String TEST_RESOURCE_NAME = "test1"; diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/errorstatushandler/ErrorStatusHandlerIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/errorstatushandler/ErrorStatusHandlerIT.java index b888143d2d..9ee3df10e1 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/errorstatushandler/ErrorStatusHandlerIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/errorstatushandler/ErrorStatusHandlerIT.java @@ -6,12 +6,22 @@ import org.junit.jupiter.api.extension.RegisterExtension; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.javaoperatorsdk.annotation.Sample; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; import io.javaoperatorsdk.operator.processing.retry.GenericRetry; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; +@Sample( + tldr = "Error Status Handler for Failed Reconciliations", + description = + """ + Demonstrates how to implement error status handlers that update resource status when \ + reconciliations fail. The test verifies that error messages are properly recorded in the \ + resource status after each failed retry attempt. This provides visibility into \ + reconciliation failures and helps with debugging operator issues. + """) class ErrorStatusHandlerIT { public static final int MAX_RETRY_ATTEMPTS = 3; diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/event/EventSourceIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/event/EventSourceIT.java index eaa881709b..185b6c08f0 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/event/EventSourceIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/event/EventSourceIT.java @@ -6,12 +6,22 @@ import org.junit.jupiter.api.extension.RegisterExtension; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.javaoperatorsdk.annotation.Sample; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; import io.javaoperatorsdk.operator.support.TestUtils; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; +@Sample( + tldr = "Custom Event Source for Periodic Reconciliation", + description = + """ + Demonstrates how to implement custom event sources that trigger reconciliation on a \ + periodic basis. The test verifies that reconciliations are triggered at regular intervals \ + by a timer-based event source. This enables operators to perform periodic checks or \ + updates independent of resource changes. + """) class EventSourceIT { @RegisterExtension diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/filter/FilterIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/filter/FilterIT.java index 1e51ad5ab0..6cb24473ab 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/filter/FilterIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/filter/FilterIT.java @@ -6,12 +6,22 @@ import org.junit.jupiter.api.extension.RegisterExtension; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.javaoperatorsdk.annotation.Sample; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; import static io.javaoperatorsdk.operator.baseapi.filter.FilterTestReconciler.CONFIG_MAP_FILTER_VALUE; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; +@Sample( + tldr = "Filtering Events for Primary and Secondary Resources", + description = + """ + Demonstrates how to implement event filters for both primary custom resources and \ + secondary dependent resources. The test verifies that resource updates matching specific \ + filter criteria are ignored and don't trigger reconciliation. This helps reduce \ + unnecessary reconciliation executions and improve operator efficiency. + """) class FilterIT { public static final String RESOURCE_NAME = "test1"; diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/generickubernetesresourcehandling/GenericKubernetesResourceHandlingIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/generickubernetesresourcehandling/GenericKubernetesResourceHandlingIT.java index 3b2fccfe59..44d8c7fabb 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/generickubernetesresourcehandling/GenericKubernetesResourceHandlingIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/generickubernetesresourcehandling/GenericKubernetesResourceHandlingIT.java @@ -3,10 +3,21 @@ import org.junit.jupiter.api.extension.RegisterExtension; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.javaoperatorsdk.annotation.Sample; import io.javaoperatorsdk.operator.dependent.generickubernetesresource.GenericKubernetesDependentSpec; import io.javaoperatorsdk.operator.dependent.generickubernetesresource.GenericKubernetesDependentTestBase; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; +@Sample( + tldr = "Working with GenericKubernetesResource for Dynamic Resource Types", + description = + """ + Demonstrates how to use GenericKubernetesResource to work with Kubernetes resources \ + dynamically without requiring compile-time type definitions. This approach is useful when \ + building operators that need to manage arbitrary Kubernetes resources or when the resource \ + types are not known at compile time. The test shows how to handle generic resources as \ + dependent resources in a reconciler. + """) public class GenericKubernetesResourceHandlingIT extends GenericKubernetesDependentTestBase { diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/gracefulstop/GracefulStopIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/gracefulstop/GracefulStopIT.java index 4f499b256b..a16a6cafc5 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/gracefulstop/GracefulStopIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/gracefulstop/GracefulStopIT.java @@ -6,12 +6,22 @@ import org.junit.jupiter.api.extension.RegisterExtension; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.javaoperatorsdk.annotation.Sample; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; import static io.javaoperatorsdk.operator.baseapi.gracefulstop.GracefulStopTestReconciler.RECONCILER_SLEEP; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; +@Sample( + tldr = "Graceful Operator Shutdown with Reconciliation Timeout", + description = + """ + Demonstrates how to configure graceful shutdown behavior with reconciliation termination \ + timeouts. The test verifies that in-progress reconciliations are allowed to complete when \ + the operator stops. This ensures clean shutdown without interrupting ongoing \ + reconciliation work. + """) public class GracefulStopIT { public static final String TEST_1 = "test1"; diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/informereventsource/InformerEventSourceIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/informereventsource/InformerEventSourceIT.java index c1a857c22c..3b442666d5 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/informereventsource/InformerEventSourceIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/informereventsource/InformerEventSourceIT.java @@ -8,6 +8,7 @@ import io.fabric8.kubernetes.api.model.ConfigMap; import io.fabric8.kubernetes.api.model.ObjectMeta; +import io.javaoperatorsdk.annotation.Sample; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; import static io.javaoperatorsdk.operator.baseapi.informereventsource.InformerEventSourceTestCustomReconciler.MISSING_CONFIG_MAP; @@ -17,6 +18,15 @@ import static org.assertj.core.api.Assertions.fail; import static org.awaitility.Awaitility.await; +@Sample( + tldr = "Using Informer Event Source to Watch Secondary Resources", + description = + """ + Demonstrates how to use InformerEventSource to watch changes in secondary resources \ + (ConfigMaps) and trigger reconciliation when those resources are created, updated, or \ + deleted. The test verifies that the reconciler responds to ConfigMap changes and updates \ + the primary resource status accordingly. + """) class InformerEventSourceIT { public static final String RESOURCE_NAME = "informertestcr"; diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/informerremotecluster/InformerRemoteClusterIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/informerremotecluster/InformerRemoteClusterIT.java index 3cd5ddccbc..9d27c5d2d9 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/informerremotecluster/InformerRemoteClusterIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/informerremotecluster/InformerRemoteClusterIT.java @@ -10,6 +10,7 @@ import io.fabric8.kubernetes.api.model.ConfigMapBuilder; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; import io.fabric8.kubernetes.client.KubernetesClient; +import io.javaoperatorsdk.annotation.Sample; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; import io.javaoperatorsdk.operator.processing.event.source.informer.Mappers; @@ -17,6 +18,14 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; +@Sample( + tldr = "Watching resources in a remote Kubernetes cluster", + description = + """ + Demonstrates how to configure an informer event source to watch resources in a different \ + Kubernetes cluster from where the operator is running. This enables multi-cluster \ + scenarios where an operator in one cluster manages resources in another cluster. + """) @EnableKubeAPIServer class InformerRemoteClusterIT { diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/labelselector/LabelSelectorIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/labelselector/LabelSelectorIT.java index b073bee248..b6820459f2 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/labelselector/LabelSelectorIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/labelselector/LabelSelectorIT.java @@ -8,6 +8,7 @@ import org.junit.jupiter.api.extension.RegisterExtension; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.javaoperatorsdk.annotation.Sample; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; import static io.javaoperatorsdk.operator.baseapi.labelselector.LabelSelectorTestReconciler.LABEL_KEY; @@ -15,6 +16,15 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; +@Sample( + tldr = "Label Selector for Custom Resource Filtering", + description = + """ + Demonstrates how to configure label selectors to filter which custom resources an \ + operator watches. The test verifies that only resources with matching labels trigger \ + reconciliation. This allows operators to selectively manage a subset of custom resources \ + based on their labels. + """) class LabelSelectorIT { @RegisterExtension diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/leaderelectionchangenamespace/LeaderElectionChangeNamespaceIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/leaderelectionchangenamespace/LeaderElectionChangeNamespaceIT.java index f6194439e2..f35b2380a4 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/leaderelectionchangenamespace/LeaderElectionChangeNamespaceIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/leaderelectionchangenamespace/LeaderElectionChangeNamespaceIT.java @@ -14,12 +14,22 @@ import io.fabric8.kubernetes.api.model.coordination.v1.LeaseSpecBuilder; import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.KubernetesClientBuilder; +import io.javaoperatorsdk.annotation.Sample; import io.javaoperatorsdk.operator.api.config.LeaderElectionConfiguration; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; +@Sample( + tldr = "Leader election with namespace change handling", + description = + """ + Tests that when an operator is not elected as leader, changing the watched namespaces does \ + not start processing. This ensures that only the leader operator actively \ + reconciles resources, preventing conflicts in multi-instance deployments with \ + leader election. + """) public class LeaderElectionChangeNamespaceIT { public static final String LEASE_NAME = "nschangelease"; diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/manualobservedgeneration/ManualObservedGenerationIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/manualobservedgeneration/ManualObservedGenerationIT.java index f90fda5c5e..55836c5617 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/manualobservedgeneration/ManualObservedGenerationIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/manualobservedgeneration/ManualObservedGenerationIT.java @@ -4,11 +4,20 @@ import org.junit.jupiter.api.extension.RegisterExtension; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.javaoperatorsdk.annotation.Sample; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; +@Sample( + tldr = "Manually managing observedGeneration in status", + description = + """ + Shows how to manually track and update the observedGeneration field in status to indicate \ + which generation of the resource spec has been successfully processed. This is useful for \ + providing clear feedback to users about reconciliation progress. + """) public class ManualObservedGenerationIT { public static final String RESOURCE_NAME = "test1"; diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/maxinterval/MaxIntervalIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/maxinterval/MaxIntervalIT.java index af7cdbe8ec..9ecf50741c 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/maxinterval/MaxIntervalIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/maxinterval/MaxIntervalIT.java @@ -6,11 +6,21 @@ import org.junit.jupiter.api.extension.RegisterExtension; import io.fabric8.kubernetes.api.model.ObjectMeta; +import io.javaoperatorsdk.annotation.Sample; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; +@Sample( + tldr = "Maximum Reconciliation Interval Configuration", + description = + """ + Demonstrates how to configure a maximum interval for periodic reconciliation triggers. \ + The test verifies that reconciliation is automatically triggered at the configured \ + interval even when there are no resource changes, enabling periodic validation and drift \ + detection. + """) class MaxIntervalIT { @RegisterExtension diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/maxintervalafterretry/MaxIntervalAfterRetryIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/maxintervalafterretry/MaxIntervalAfterRetryIT.java index bc55fa6035..40659a5793 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/maxintervalafterretry/MaxIntervalAfterRetryIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/maxintervalafterretry/MaxIntervalAfterRetryIT.java @@ -6,11 +6,20 @@ import org.junit.jupiter.api.extension.RegisterExtension; import io.fabric8.kubernetes.api.model.ObjectMeta; +import io.javaoperatorsdk.annotation.Sample; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; +@Sample( + tldr = "Maximum Reconciliation Interval After Retry", + description = + """ + Tests that reconciliation is repeatedly triggered based on the maximum interval setting \ + even after retries. This ensures periodic reconciliation continues at the configured \ + maximum interval, maintaining eventual consistency regardless of retry attempts. + """) class MaxIntervalAfterRetryIT { @RegisterExtension diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/multiplereconcilersametype/MultipleReconcilerSameTypeIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/multiplereconcilersametype/MultipleReconcilerSameTypeIT.java index 81cfa7fd07..23fe9c815a 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/multiplereconcilersametype/MultipleReconcilerSameTypeIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/multiplereconcilersametype/MultipleReconcilerSameTypeIT.java @@ -4,11 +4,21 @@ import org.junit.jupiter.api.extension.RegisterExtension; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.javaoperatorsdk.annotation.Sample; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; +@Sample( + tldr = "Multiple reconcilers for the same resource type", + description = + """ + Demonstrates how to register multiple reconcilers for the same custom resource type, with \ + each reconciler handling different resources based on label selectors or other \ + criteria. This enables different processing logic for different subsets of the same \ + resource type. + """) public class MultipleReconcilerSameTypeIT { public static final String TEST_RESOURCE_1 = "test1"; diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/multiplesecondaryeventsource/MultipleSecondaryEventSourceIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/multiplesecondaryeventsource/MultipleSecondaryEventSourceIT.java index 18a937040f..776054e893 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/multiplesecondaryeventsource/MultipleSecondaryEventSourceIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/multiplesecondaryeventsource/MultipleSecondaryEventSourceIT.java @@ -7,10 +7,20 @@ import io.fabric8.kubernetes.api.model.ConfigMap; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.javaoperatorsdk.annotation.Sample; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; import static org.awaitility.Awaitility.await; +@Sample( + tldr = "Managing Multiple Secondary Event Sources", + description = + """ + Demonstrates how to configure and use multiple secondary event sources for a single \ + reconciler. The test verifies that the reconciler is triggered by changes to different \ + secondary resources and handles events from multiple sources correctly, including periodic \ + event sources. + """) class MultipleSecondaryEventSourceIT { public static final String TEST_RESOURCE_NAME = "testresource"; diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/multiversioncrd/MultiVersionCRDIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/multiversioncrd/MultiVersionCRDIT.java index 9e0c6eb1fb..11b26e6ae3 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/multiversioncrd/MultiVersionCRDIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/multiversioncrd/MultiVersionCRDIT.java @@ -13,6 +13,7 @@ import io.fabric8.kubernetes.client.WatcherException; import io.fabric8.kubernetes.client.informers.SharedIndexInformer; import io.fabric8.kubernetes.client.utils.Serialization; +import io.javaoperatorsdk.annotation.Sample; import io.javaoperatorsdk.operator.api.config.InformerStoppedHandler; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; @@ -21,6 +22,16 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; +@Sample( + tldr = "Handling Multiple CRD Versions", + description = + """ + Demonstrates how to work with Custom Resource Definitions that have multiple API \ + versions. The test shows how to configure multiple reconcilers for different versions of \ + the same CRD, handle version-specific schemas, and deal with incompatible version \ + conversions. It also demonstrates error handling through InformerStoppedHandler when \ + deserialization fails due to schema incompatibilities between versions. + """) class MultiVersionCRDIT { private static final Logger log = LoggerFactory.getLogger(MultiVersionCRDIT.class); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/nextreconciliationimminent/NextReconciliationImminentIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/nextreconciliationimminent/NextReconciliationImminentIT.java index 03bd6b0a7a..ca21390259 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/nextreconciliationimminent/NextReconciliationImminentIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/nextreconciliationimminent/NextReconciliationImminentIT.java @@ -8,11 +8,20 @@ import org.slf4j.LoggerFactory; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.javaoperatorsdk.annotation.Sample; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; +@Sample( + tldr = "Skipping status updates when next reconciliation is imminent", + description = + """ + Shows how to use the nextReconciliationImminent flag to skip status updates when another \ + reconciliation event is already pending. This optimization prevents unnecessary \ + status patch operations when rapid consecutive reconciliations occur. + """) public class NextReconciliationImminentIT { private static final Logger log = LoggerFactory.getLogger(NextReconciliationImminentIT.class); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/patchresourceandstatusnossa/PatchResourceAndStatusNoSSAIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/patchresourceandstatusnossa/PatchResourceAndStatusNoSSAIT.java index a835dd2de6..301b91fea1 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/patchresourceandstatusnossa/PatchResourceAndStatusNoSSAIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/patchresourceandstatusnossa/PatchResourceAndStatusNoSSAIT.java @@ -7,6 +7,7 @@ import org.junit.jupiter.api.extension.RegisterExtension; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.javaoperatorsdk.annotation.Sample; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; import io.javaoperatorsdk.operator.support.TestUtils; @@ -15,6 +16,14 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; +@Sample( + tldr = "Patching resource and status without Server-Side Apply", + description = + """ + Demonstrates how to patch both the primary resource metadata/spec and status subresource \ + using traditional JSON merge patch instead of Server-Side Apply. This shows the \ + legacy approach for updating resources when SSA is disabled. + """) class PatchResourceAndStatusNoSSAIT { @RegisterExtension LocallyRunOperatorExtension extension = diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/patchresourcewithssa/PatchResourceAndStatusWithSSAIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/patchresourcewithssa/PatchResourceAndStatusWithSSAIT.java index f395706092..7c251c91fa 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/patchresourcewithssa/PatchResourceAndStatusWithSSAIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/patchresourcewithssa/PatchResourceAndStatusWithSSAIT.java @@ -1,7 +1,17 @@ package io.javaoperatorsdk.operator.baseapi.patchresourcewithssa; +import io.javaoperatorsdk.annotation.Sample; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; +@Sample( + tldr = "Patching resource and status with Server-Side Apply", + description = + """ + Demonstrates how to use Server-Side Apply (SSA) to patch both the primary resource and its \ + status subresource. SSA provides better conflict resolution and field management \ + tracking compared to traditional merge patches, making it the recommended approach \ + for resource updates. + """) public class PatchResourceAndStatusWithSSAIT extends PatchWithSSAITBase { @Override diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/patchresourcewithssa/PatchResourceWithSSAIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/patchresourcewithssa/PatchResourceWithSSAIT.java index d512665d06..e8ca8ffc57 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/patchresourcewithssa/PatchResourceWithSSAIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/patchresourcewithssa/PatchResourceWithSSAIT.java @@ -1,7 +1,17 @@ package io.javaoperatorsdk.operator.baseapi.patchresourcewithssa; +import io.javaoperatorsdk.annotation.Sample; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; +@Sample( + tldr = "Patching Resources with Server-Side Apply (SSA)", + description = + """ + Demonstrates how to use Server-Side Apply (SSA) for patching primary resources in \ + Kubernetes. The test verifies that the reconciler can patch resources using SSA, which \ + provides better conflict resolution and field management compared to traditional update \ + approaches, including proper handling of managed fields. + """) public class PatchResourceWithSSAIT extends PatchWithSSAITBase { @Override diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/perresourceeventsource/PerResourcePollingEventSourceIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/perresourceeventsource/PerResourcePollingEventSourceIT.java index 20f89e0d39..f2fa6f0064 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/perresourceeventsource/PerResourcePollingEventSourceIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/perresourceeventsource/PerResourcePollingEventSourceIT.java @@ -4,11 +4,20 @@ import org.junit.jupiter.api.extension.RegisterExtension; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.javaoperatorsdk.annotation.Sample; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; +@Sample( + tldr = "Per-resource polling event source implementation", + description = + """ + Shows how to implement a per-resource polling event source where each primary resource has \ + its own polling schedule to fetch external state. This is useful for integrating \ + with external systems that don't support event-driven notifications. + """) class PerResourcePollingEventSourceIT { public static final String NAME_1 = "name1"; diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/primaryindexer/PrimaryIndexerIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/primaryindexer/PrimaryIndexerIT.java index 9063ef0fcb..784fe0a579 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/primaryindexer/PrimaryIndexerIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/primaryindexer/PrimaryIndexerIT.java @@ -7,12 +7,23 @@ import io.fabric8.kubernetes.api.model.ConfigMap; import io.fabric8.kubernetes.api.model.ObjectMeta; +import io.javaoperatorsdk.annotation.Sample; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; import static io.javaoperatorsdk.operator.baseapi.primaryindexer.AbstractPrimaryIndexerTestReconciler.CONFIG_MAP_NAME; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; +@Sample( + tldr = "Using Primary Indexer for Secondary Resource Mapping", + description = + """ + Demonstrates how to use primary indexers to efficiently map secondary resources back to \ + their primary resources. When a secondary resource (like a ConfigMap) changes, the primary \ + indexer allows the framework to determine which primary resources should be reconciled. \ + This pattern enables efficient one-to-many and many-to-many relationships between primary \ + and secondary resources without polling or full scans. + """) public class PrimaryIndexerIT { public static final String RESOURCE_NAME1 = "test1"; diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/primarytosecondary/PrimaryToSecondaryIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/primarytosecondary/PrimaryToSecondaryIT.java index d82c24a55f..778745cca2 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/primarytosecondary/PrimaryToSecondaryIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/primarytosecondary/PrimaryToSecondaryIT.java @@ -6,11 +6,21 @@ import org.junit.jupiter.api.extension.RegisterExtension; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.javaoperatorsdk.annotation.Sample; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; +@Sample( + tldr = "Primary to Secondary Resource Mapping", + description = + """ + Demonstrates many-to-one mapping between primary and secondary resources where multiple \ + primary resources can reference the same secondary resource. The test verifies that \ + changes in the secondary resource trigger reconciliation of all related primary resources, \ + enabling shared resource patterns. + """) class PrimaryToSecondaryIT { public static final String CLUSTER_NAME = "cluster1"; diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/primarytosecondary/PrimaryToSecondaryMissingIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/primarytosecondary/PrimaryToSecondaryMissingIT.java index 84e6910b35..c27ebeb75f 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/primarytosecondary/PrimaryToSecondaryMissingIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/primarytosecondary/PrimaryToSecondaryMissingIT.java @@ -3,6 +3,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; +import io.javaoperatorsdk.annotation.Sample; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; import static io.javaoperatorsdk.operator.baseapi.primarytosecondary.PrimaryToSecondaryIT.cluster; @@ -14,6 +15,15 @@ * The intention with this IT is to show the use cases why the PrimaryToSecondary Mapper is needed, * and the situation when it is not working. */ +@Sample( + tldr = "Issues When Primary-to-Secondary Mapper Is Missing", + description = + """ + Demonstrates the problems that occur when accessing secondary resources without a \ + proper PrimaryToSecondaryMapper configured. The test shows that accessing secondary \ + resources through the context fails without the mapper, while direct cache access works \ + as a workaround, highlighting the importance of proper mapper configuration. + """) class PrimaryToSecondaryMissingIT { @RegisterExtension diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/ratelimit/RateLimitIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/ratelimit/RateLimitIT.java index 8b04fd0095..ed0a595a96 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/ratelimit/RateLimitIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/ratelimit/RateLimitIT.java @@ -9,12 +9,21 @@ import org.slf4j.LoggerFactory; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.javaoperatorsdk.annotation.Sample; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; import static io.javaoperatorsdk.operator.baseapi.ratelimit.RateLimitReconciler.REFRESH_PERIOD; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; +@Sample( + tldr = "Rate Limiting Reconciliation Executions", + description = + """ + Demonstrates how to implement rate limiting to control how frequently reconciliations \ + execute. The test shows that multiple rapid resource updates are batched and executed at a \ + controlled rate. This prevents overwhelming the system when resources change frequently. + """) class RateLimitIT { private static final Logger log = LoggerFactory.getLogger(RateLimitIT.class); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/retry/RetryIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/retry/RetryIT.java index 410d34a390..ef91272f70 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/retry/RetryIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/retry/RetryIT.java @@ -6,6 +6,7 @@ import org.junit.jupiter.api.extension.RegisterExtension; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.javaoperatorsdk.annotation.Sample; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; import io.javaoperatorsdk.operator.processing.retry.GenericRetry; import io.javaoperatorsdk.operator.support.TestUtils; @@ -13,6 +14,15 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; +@Sample( + tldr = "Automatic Retry for Failed Reconciliations", + description = + """ + Demonstrates how to configure automatic retry logic for reconciliations that fail \ + temporarily. The test shows that failed executions are automatically retried with \ + configurable intervals and max attempts. After a specified number of retries, the \ + reconciliation succeeds and updates the resource status accordingly. + """) class RetryIT { public static final int RETRY_INTERVAL = 150; public static final int MAX_RETRY_ATTEMPTS = 5; diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/retry/RetryMaxAttemptIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/retry/RetryMaxAttemptIT.java index f57a70201f..07cc458713 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/retry/RetryMaxAttemptIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/retry/RetryMaxAttemptIT.java @@ -3,12 +3,22 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; +import io.javaoperatorsdk.annotation.Sample; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; import io.javaoperatorsdk.operator.processing.retry.GenericRetry; import static io.javaoperatorsdk.operator.baseapi.retry.RetryIT.createTestCustomResource; import static org.assertj.core.api.Assertions.assertThat; +@Sample( + tldr = "Maximum Retry Attempts Configuration", + description = + """ + Demonstrates how to configure a maximum number of retry attempts for failed \ + reconciliations. The test verifies that the operator stops retrying after reaching the \ + configured maximum attempts. This prevents infinite retry loops when reconciliations \ + consistently fail. + """) class RetryMaxAttemptIT { public static final int MAX_RETRY_ATTEMPTS = 3; diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/simple/ReconcilerExecutorIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/simple/ReconcilerExecutorIT.java index cbd8de4459..c744830f5d 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/simple/ReconcilerExecutorIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/simple/ReconcilerExecutorIT.java @@ -7,12 +7,21 @@ import org.junit.jupiter.api.extension.RegisterExtension; import io.fabric8.kubernetes.api.model.ConfigMap; +import io.javaoperatorsdk.annotation.Sample; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; import io.javaoperatorsdk.operator.support.TestUtils; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; +@Sample( + tldr = "Basic reconciler execution", + description = + """ + Demonstrates the basic reconciler execution flow including resource creation, status \ + updates, and cleanup. This test verifies that a reconciler can create dependent resources \ + (ConfigMap), update status, and properly handle cleanup when resources are deleted. + """) class ReconcilerExecutorIT { @RegisterExtension diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/ssaissue/finalizer/SSAFinalizerIssueIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/ssaissue/finalizer/SSAFinalizerIssueIT.java index a830675518..25cd6fbd91 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/ssaissue/finalizer/SSAFinalizerIssueIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/ssaissue/finalizer/SSAFinalizerIssueIT.java @@ -8,11 +8,21 @@ import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; import io.fabric8.kubernetes.client.dsl.base.PatchContext; import io.fabric8.kubernetes.client.dsl.base.PatchType; +import io.javaoperatorsdk.annotation.Sample; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; +@Sample( + tldr = "Server-Side Apply Finalizer Field Manager Issue", + description = + """ + Demonstrates a potential issue with Server-Side Apply (SSA) when adding finalizers. \ + When a resource is created with the same field manager used by the controller, adding \ + a finalizer can unexpectedly remove other spec fields, showcasing field manager \ + ownership conflicts in SSA. + """) class SSAFinalizerIssueIT { public static final String TEST_1 = "test1"; diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/ssaissue/specupdate/SSASpecUpdateIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/ssaissue/specupdate/SSASpecUpdateIT.java index 9cccd32e3c..af4c668884 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/ssaissue/specupdate/SSASpecUpdateIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/ssaissue/specupdate/SSASpecUpdateIT.java @@ -4,11 +4,21 @@ import org.junit.jupiter.api.extension.RegisterExtension; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.javaoperatorsdk.annotation.Sample; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; +@Sample( + tldr = "Server-Side Apply Finalizer Removal on Spec Update", + description = + """ + Demonstrates an issue with Server-Side Apply (SSA) where updating the resource spec \ + without explicitly including the finalizer causes the finalizer to be removed. This \ + highlights the importance of including all desired fields when using SSA to avoid \ + unintended field removal. + """) class SSASpecUpdateIT { public static final String TEST_RESOURCE_NAME = "test"; 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 index 61fc40803c..157569530d 100644 --- 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 @@ -7,6 +7,7 @@ import io.fabric8.kubernetes.api.model.ConfigMap; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.javaoperatorsdk.annotation.Sample; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; import static io.javaoperatorsdk.operator.baseapi.startsecondaryaccess.StartupSecondaryAccessReconciler.LABEL_KEY; @@ -14,6 +15,15 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; +@Sample( + tldr = "Accessing Secondary Resources During Operator Startup", + description = + """ + Verifies that reconcilers can properly access all secondary resources during operator \ + startup, even when a large number of secondary resources exist. The test ensures that \ + the informer cache is fully synchronized before reconciliation begins, allowing access \ + to all related resources. + """) class StartupSecondaryAccessIT { public static final int SECONDARY_NUMBER = 200; diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/statuscache/StatusPatchCacheIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/statuscache/StatusPatchCacheIT.java index 9d0b923056..f6e91f80dc 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/statuscache/StatusPatchCacheIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/statuscache/StatusPatchCacheIT.java @@ -6,11 +6,21 @@ import org.junit.jupiter.api.extension.RegisterExtension; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.javaoperatorsdk.annotation.Sample; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; +@Sample( + tldr = "Status patch caching for consistency", + description = + """ + Demonstrates how the framework caches status patches to ensure consistency when status is \ + updated frequently. The cache guarantees that status values are monotonically \ + increasing and always reflect the most recent state, even with rapid successive \ + updates. + """) public class StatusPatchCacheIT { public static final String TEST_1 = "test1"; diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/statuspatchnonlocking/StatusPatchNotLockingForNonSSAIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/statuspatchnonlocking/StatusPatchNotLockingForNonSSAIT.java index 4913b900a0..8cb06adbed 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/statuspatchnonlocking/StatusPatchNotLockingForNonSSAIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/statuspatchnonlocking/StatusPatchNotLockingForNonSSAIT.java @@ -7,6 +7,7 @@ import org.junit.jupiter.api.extension.RegisterExtension; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.javaoperatorsdk.annotation.Sample; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; import static io.javaoperatorsdk.operator.baseapi.statuspatchnonlocking.StatusPatchLockingReconciler.MESSAGE; @@ -14,6 +15,15 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; +@Sample( + tldr = "Status Patching Without Optimistic Locking for Non-SSA", + description = + """ + Tests status update behavior when not using Server-Side Apply (SSA), verifying that \ + optimistic locking is not enforced on status patches. The test also demonstrates proper \ + field deletion when values are set to null, ensuring correct status management without \ + SSA optimistic locking. + """) class StatusPatchNotLockingForNonSSAIT { public static final String TEST_RESOURCE_NAME = "test"; diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/statuspatchnonlocking/StatusPatchSSAMigrationIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/statuspatchnonlocking/StatusPatchSSAMigrationIT.java index a301e9f61a..44158fdd8e 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/statuspatchnonlocking/StatusPatchSSAMigrationIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/statuspatchnonlocking/StatusPatchSSAMigrationIT.java @@ -11,12 +11,22 @@ import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.KubernetesClientBuilder; import io.fabric8.kubernetes.client.utils.KubernetesResourceUtil; +import io.javaoperatorsdk.annotation.Sample; import io.javaoperatorsdk.operator.Operator; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; +@Sample( + tldr = "Migrating Status Patching from Non-SSA to SSA", + description = + """ + Demonstrates the process and challenges of migrating status patching from traditional \ + update methods to Server-Side Apply (SSA). Tests show a known Kubernetes issue where \ + field deletion doesn't work correctly during migration, and provides a workaround by \ + removing managed field entries from the previous update method. + """) public class StatusPatchSSAMigrationIT { public static final String TEST_RESOURCE_NAME = "test"; diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/statusupdatelocking/StatusUpdateLockingIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/statusupdatelocking/StatusUpdateLockingIT.java index ad51d82059..b745f82bc6 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/statusupdatelocking/StatusUpdateLockingIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/statusupdatelocking/StatusUpdateLockingIT.java @@ -7,12 +7,22 @@ import org.junit.jupiter.api.extension.RegisterExtension; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.javaoperatorsdk.annotation.Sample; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; import static io.javaoperatorsdk.operator.baseapi.statusupdatelocking.StatusUpdateLockingReconciler.WAIT_TIME; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; +@Sample( + tldr = "Status Update Locking and Concurrency Control", + description = + """ + Demonstrates how the framework handles concurrent status updates and ensures no \ + optimistic locking conflicts occur when updating status subresources. The test verifies \ + that status updates can proceed independently of spec updates without causing version \ + conflicts or requiring retries. + """) class StatusUpdateLockingIT { public static final String TEST_RESOURCE_NAME = "test"; diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/subresource/SubResourceUpdateIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/subresource/SubResourceUpdateIT.java index 7544e3b791..b74c6dc78d 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/subresource/SubResourceUpdateIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/subresource/SubResourceUpdateIT.java @@ -7,6 +7,7 @@ import org.junit.jupiter.api.extension.RegisterExtension; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.javaoperatorsdk.annotation.Sample; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; import io.javaoperatorsdk.operator.support.TestUtils; @@ -14,6 +15,15 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; +@Sample( + tldr = "Status Subresource Updates", + description = + """ + Demonstrates how to properly update the status subresource of custom resources. The test \ + verifies that status updates are handled correctly without triggering unnecessary \ + reconciliations, and that concurrent spec and status updates are managed properly with \ + optimistic locking and retry mechanisms. + """) class SubResourceUpdateIT { public static final int WAIT_AFTER_EXECUTION = 500; diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/unmodifiabledependentpart/UnmodifiableDependentPartIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/unmodifiabledependentpart/UnmodifiableDependentPartIT.java index 735255c54c..2435e71fdd 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/unmodifiabledependentpart/UnmodifiableDependentPartIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/unmodifiabledependentpart/UnmodifiableDependentPartIT.java @@ -5,6 +5,7 @@ import io.fabric8.kubernetes.api.model.ConfigMap; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.javaoperatorsdk.annotation.Sample; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; import static io.javaoperatorsdk.operator.baseapi.unmodifiabledependentpart.UnmodifiablePartConfigMapDependent.ACTUAL_DATA_KEY; @@ -12,6 +13,15 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; +@Sample( + tldr = "Unmodifiable Parts in Dependent Resources", + description = + """ + Demonstrates how to preserve certain parts of a dependent resource from being modified \ + during updates while allowing other parts to change. This test shows that initial data \ + can be marked as unmodifiable and will remain unchanged even when the primary resource \ + spec is updated, enabling partial update control. + """) public class UnmodifiableDependentPartIT { public static final String TEST_RESOURCE_NAME = "test1"; diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/updatestatusincleanupandreschedule/UpdateStatusInCleanupAndRescheduleIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/updatestatusincleanupandreschedule/UpdateStatusInCleanupAndRescheduleIT.java index b2a2b463ba..34cbedf310 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/updatestatusincleanupandreschedule/UpdateStatusInCleanupAndRescheduleIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/updatestatusincleanupandreschedule/UpdateStatusInCleanupAndRescheduleIT.java @@ -4,11 +4,21 @@ import org.junit.jupiter.api.extension.RegisterExtension; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.javaoperatorsdk.annotation.Sample; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; +@Sample( + tldr = "Update Status in Cleanup and Reschedule", + description = + """ + Tests the ability to update resource status during cleanup and reschedule the cleanup \ + operation. This demonstrates that cleanup methods can perform status updates and request \ + to be called again after a delay, enabling multi-step cleanup processes with status \ + tracking. + """) public class UpdateStatusInCleanupAndRescheduleIT { public static final String TEST_RESOURCE = "test1"; diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/bulkdependent/BulkDependentDeleterIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/bulkdependent/BulkDependentDeleterIT.java index 108607283b..5952396fc1 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/bulkdependent/BulkDependentDeleterIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/bulkdependent/BulkDependentDeleterIT.java @@ -2,9 +2,19 @@ import org.junit.jupiter.api.extension.RegisterExtension; +import io.javaoperatorsdk.annotation.Sample; import io.javaoperatorsdk.operator.dependent.bulkdependent.managed.ManagedDeleterBulkReconciler; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; +@Sample( + tldr = "Bulk Dependent Resource Deleter Implementation", + description = + """ + Demonstrates implementation of a bulk dependent resource with custom deleter logic. \ + This test extends BulkDependentTestBase to verify that bulk dependent resources can \ + implement custom deletion strategies, managing multiple resources efficiently during \ + cleanup operations. + """) public class BulkDependentDeleterIT extends BulkDependentTestBase { @RegisterExtension diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/bulkdependent/condition/BulkDependentWithConditionIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/bulkdependent/condition/BulkDependentWithConditionIT.java index eb3a6e7368..f9027c089d 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/bulkdependent/condition/BulkDependentWithConditionIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/bulkdependent/condition/BulkDependentWithConditionIT.java @@ -3,6 +3,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; +import io.javaoperatorsdk.annotation.Sample; import io.javaoperatorsdk.operator.dependent.bulkdependent.BulkDependentTestCustomResource; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; @@ -13,6 +14,15 @@ import static org.assertj.core.api.Assertions.*; import static org.awaitility.Awaitility.await; +@Sample( + tldr = "Bulk Dependent Resources with Ready Conditions", + description = + """ + Tests bulk dependent resources with preconditions that control when reconciliation \ + occurs. This demonstrates using ready conditions to ensure bulk operations only execute \ + when the primary resource is in the appropriate state, coordinating complex multi-resource \ + management. + """) class BulkDependentWithConditionIT { @RegisterExtension diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/bulkdependent/external/BulkExternalDependentIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/bulkdependent/external/BulkExternalDependentIT.java index 4f7d425729..3080beb001 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/bulkdependent/external/BulkExternalDependentIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/bulkdependent/external/BulkExternalDependentIT.java @@ -3,12 +3,22 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; +import io.javaoperatorsdk.annotation.Sample; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; import static io.javaoperatorsdk.operator.dependent.bulkdependent.BulkDependentTestBase.*; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; +@Sample( + tldr = "Managing External Bulk Resources", + description = + """ + Demonstrates managing multiple external resources (non-Kubernetes) using bulk dependent \ + resources. This pattern allows operators to manage a variable number of external resources \ + based on primary resource specifications, handling creation, updates, and deletion of \ + external resources at scale. + """) class BulkExternalDependentIT { @RegisterExtension diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/bulkdependent/managed/ManagedBulkDependentIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/bulkdependent/managed/ManagedBulkDependentIT.java index 61b7f62a24..7353887ef4 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/bulkdependent/managed/ManagedBulkDependentIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/bulkdependent/managed/ManagedBulkDependentIT.java @@ -2,9 +2,20 @@ import org.junit.jupiter.api.extension.RegisterExtension; +import io.javaoperatorsdk.annotation.Sample; import io.javaoperatorsdk.operator.dependent.bulkdependent.BulkDependentTestBase; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; +@Sample( + tldr = "Bulk Dependent Resources with Managed Workflow", + description = + """ + Demonstrates how to manage bulk dependent resources using the managed workflow approach. \ + This test extends the base bulk dependent test to show how multiple instances of \ + the same type of dependent resource can be created and managed together. The \ + managed workflow handles the orchestration of creating, updating, and deleting \ + multiple dependent resources based on the primary resource specification. + """) public class ManagedBulkDependentIT extends BulkDependentTestBase { @RegisterExtension diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/bulkdependent/readonly/ReadOnlyBulkDependentIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/bulkdependent/readonly/ReadOnlyBulkDependentIT.java index 422360b7b2..c2524f635c 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/bulkdependent/readonly/ReadOnlyBulkDependentIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/bulkdependent/readonly/ReadOnlyBulkDependentIT.java @@ -7,6 +7,7 @@ import io.fabric8.kubernetes.api.model.ConfigMap; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.javaoperatorsdk.annotation.Sample; import io.javaoperatorsdk.operator.dependent.bulkdependent.BulkDependentTestCustomResource; import io.javaoperatorsdk.operator.dependent.bulkdependent.BulkDependentTestSpec; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; @@ -14,6 +15,15 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; +@Sample( + tldr = "Read-Only Bulk Dependent Resources", + description = + """ + Demonstrates how to use read-only bulk dependent resources to observe and react to \ + multiple existing resources without managing them. This test shows how an operator \ + can monitor a collection of resources created externally and update the custom \ + resource status based on their state, without creating or modifying them. + """) public class ReadOnlyBulkDependentIT { public static final int EXPECTED_NUMBER_OF_RESOURCES = 2; diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/bulkdependent/standalone/StandaloneBulkDependentIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/bulkdependent/standalone/StandaloneBulkDependentIT.java index a74102db0d..3c2a12a680 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/bulkdependent/standalone/StandaloneBulkDependentIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/bulkdependent/standalone/StandaloneBulkDependentIT.java @@ -2,9 +2,19 @@ import org.junit.jupiter.api.extension.RegisterExtension; +import io.javaoperatorsdk.annotation.Sample; import io.javaoperatorsdk.operator.dependent.bulkdependent.BulkDependentTestBase; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; +@Sample( + tldr = "Standalone Bulk Dependent Resources", + description = + """ + Demonstrates how to use standalone bulk dependent resources to manage multiple \ + resources of the same type efficiently. This test shows how bulk operations can be \ + performed on a collection of resources without individual reconciliation cycles, \ + improving performance when managing many similar resources. + """) class StandaloneBulkDependentIT extends BulkDependentTestBase { @RegisterExtension diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/cleanermanageddependent/CleanerForManagedDependentResourcesOnlyIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/cleanermanageddependent/CleanerForManagedDependentResourcesOnlyIT.java index b0b716e8e2..37d82c313c 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/cleanermanageddependent/CleanerForManagedDependentResourcesOnlyIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/cleanermanageddependent/CleanerForManagedDependentResourcesOnlyIT.java @@ -4,11 +4,21 @@ import org.junit.jupiter.api.extension.RegisterExtension; import io.fabric8.kubernetes.api.model.ObjectMeta; +import io.javaoperatorsdk.annotation.Sample; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; +@Sample( + tldr = "Cleanup handlers for managed dependent resources", + description = + """ + Shows how to implement cleanup logic for managed dependent resources using the Cleaner \ + interface. The framework automatically adds finalizers and invokes the cleanup \ + method when the primary resource is deleted, ensuring proper cleanup of dependent \ + resources. + """) class CleanerForManagedDependentResourcesOnlyIT { public static final String TEST_RESOURCE_NAME = "cleaner-for-reconciler-test1"; diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/createonlyifnotexistsdependentwithssa/CreateOnlyIfNotExistingDependentWithSSAIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/createonlyifnotexistsdependentwithssa/CreateOnlyIfNotExistingDependentWithSSAIT.java index 3c41bae977..1d405a788d 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/createonlyifnotexistsdependentwithssa/CreateOnlyIfNotExistingDependentWithSSAIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/createonlyifnotexistsdependentwithssa/CreateOnlyIfNotExistingDependentWithSSAIT.java @@ -10,11 +10,21 @@ import io.fabric8.kubernetes.api.model.ConfigMap; import io.fabric8.kubernetes.api.model.ConfigMapBuilder; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.javaoperatorsdk.annotation.Sample; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; +@Sample( + tldr = "Create-Only Dependent Resources with Server-Side Apply", + description = + """ + Demonstrates how to configure a dependent resource that is only created if it doesn't \ + exist, using Server-Side Apply (SSA). This test shows that when a resource already \ + exists, the dependent resource implementation will not modify it, preserving any \ + external changes. + """) class CreateOnlyIfNotExistingDependentWithSSAIT { public static final String TEST_RESOURCE_NAME = "test1"; diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/dependentannotationsecondarymapper/DependentAnnotationSecondaryMapperIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/dependentannotationsecondarymapper/DependentAnnotationSecondaryMapperIT.java index 466837de4e..5a4862bebf 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/dependentannotationsecondarymapper/DependentAnnotationSecondaryMapperIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/dependentannotationsecondarymapper/DependentAnnotationSecondaryMapperIT.java @@ -7,6 +7,7 @@ import io.fabric8.kubernetes.api.model.ConfigMap; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.javaoperatorsdk.annotation.Sample; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; import static io.javaoperatorsdk.operator.processing.event.source.informer.Mappers.DEFAULT_ANNOTATION_FOR_NAME; @@ -14,6 +15,15 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; +@Sample( + tldr = "Annotation-Based Secondary Resource Mapping for Dependents", + description = + """ + Demonstrates using annotations instead of owner references to map secondary resources \ + to primary resources in dependent resources. This approach is useful when owner references \ + cannot be used (e.g., cross-namespace or cluster-scoped relationships), using special \ + annotations to establish the relationship. + """) class DependentAnnotationSecondaryMapperIT { public static final String TEST_RESOURCE_NAME = "test1"; diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/dependentcustommappingannotation/DependentCustomMappingAnnotationIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/dependentcustommappingannotation/DependentCustomMappingAnnotationIT.java index 42f365884f..d9a1525d0c 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/dependentcustommappingannotation/DependentCustomMappingAnnotationIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/dependentcustommappingannotation/DependentCustomMappingAnnotationIT.java @@ -5,6 +5,7 @@ import io.fabric8.kubernetes.api.model.ConfigMap; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.javaoperatorsdk.annotation.Sample; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; import static io.javaoperatorsdk.operator.dependent.dependentcustommappingannotation.CustomMappingConfigMapDependentResource.CUSTOM_NAMESPACE_KEY; @@ -12,6 +13,15 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; +@Sample( + tldr = "Custom Annotation Keys for Resource Mapping", + description = + """ + Tests custom annotation-based mapping for dependent resources using configurable \ + annotation keys instead of the default ones. This allows developers to customize which \ + annotations are used to establish relationships between primary and secondary resources, \ + providing flexibility for different naming conventions or avoiding conflicts. + """) class DependentCustomMappingAnnotationIT { public static final String INITIAL_VALUE = "initial value"; diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/dependentdifferentnamespace/DependentDifferentNamespaceIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/dependentdifferentnamespace/DependentDifferentNamespaceIT.java index c02bac5a5d..fbddee518f 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/dependentdifferentnamespace/DependentDifferentNamespaceIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/dependentdifferentnamespace/DependentDifferentNamespaceIT.java @@ -5,12 +5,23 @@ import io.fabric8.kubernetes.api.model.ConfigMap; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.javaoperatorsdk.annotation.Sample; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; import static io.javaoperatorsdk.operator.dependent.dependentdifferentnamespace.ConfigMapDependentResource.KEY; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; +@Sample( + tldr = "Dependent Resources in Different Namespaces", + description = + """ + Demonstrates how to manage dependent resources in a namespace different from the primary \ + resource. This test shows how to configure dependent resources to be created in a \ + specific namespace rather than inheriting the namespace from the primary resource. \ + The test verifies full CRUD operations for a ConfigMap that lives in a different \ + namespace than the custom resource that manages it. + """) class DependentDifferentNamespaceIT { public static final String TEST_1 = "different-ns-test1"; diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/dependentfilter/DependentFilterIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/dependentfilter/DependentFilterIT.java index bc7b578d7f..c988ef2741 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/dependentfilter/DependentFilterIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/dependentfilter/DependentFilterIT.java @@ -8,6 +8,7 @@ import io.fabric8.kubernetes.api.model.ConfigMap; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.javaoperatorsdk.annotation.Sample; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; import static io.javaoperatorsdk.operator.dependent.dependentfilter.DependentFilterTestReconciler.CM_VALUE_KEY; @@ -15,6 +16,17 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; +@Sample( + tldr = "Filtering Reconciliation Triggers from Dependent Resources", + description = + """ + Demonstrates how to filter events from dependent resources to prevent unnecessary \ + reconciliation triggers. This test shows how to configure filters on dependent \ + resources so that only specific changes trigger a reconciliation of the primary \ + resource. The test verifies that updates to filtered fields in the dependent \ + resource do not cause the reconciler to execute, improving efficiency and avoiding \ + reconciliation loops. + """) class DependentFilterIT { public static final String RESOURCE_NAME = "test1"; diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/dependentoperationeventfiltering/DependentOperationEventFilterIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/dependentoperationeventfiltering/DependentOperationEventFilterIT.java index e9ddc6cd6e..60e2670099 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/dependentoperationeventfiltering/DependentOperationEventFilterIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/dependentoperationeventfiltering/DependentOperationEventFilterIT.java @@ -7,11 +7,21 @@ import io.fabric8.kubernetes.api.model.ConfigMap; import io.fabric8.kubernetes.api.model.ObjectMeta; +import io.javaoperatorsdk.annotation.Sample; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; +@Sample( + tldr = "Event filtering for dependent resource operations", + description = + """ + Demonstrates how to configure event filters on dependent resources to prevent \ + reconciliation loops. When a dependent resource is created or updated by the \ + controller, the filter prevents those events from triggering unnecessary \ + reconciliations. + """) class DependentOperationEventFilterIT { public static final String TEST = "test"; diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/dependentreinitialization/DependentReInitializationIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/dependentreinitialization/DependentReInitializationIT.java index 270b89a6a5..760f354007 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/dependentreinitialization/DependentReInitializationIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/dependentreinitialization/DependentReInitializationIT.java @@ -4,9 +4,19 @@ import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.KubernetesClientBuilder; +import io.javaoperatorsdk.annotation.Sample; import io.javaoperatorsdk.operator.Operator; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; +@Sample( + tldr = "Reusing Dependent Resource Instances Across Tests", + description = + """ + Demonstrates that dependent resource instances can be safely reused across multiple \ + operator start/stop cycles. This is particularly useful in CDI-managed environments \ + like Quarkus, where dependent resources are managed as beans and should be reusable \ + across test executions. + """) class DependentReInitializationIT { /** 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 1b71c79448..740cd81e1a 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 @@ -8,11 +8,21 @@ import io.fabric8.kubernetes.api.model.ConfigMap; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; import io.fabric8.kubernetes.api.model.Secret; +import io.javaoperatorsdk.annotation.Sample; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; +@Sample( + tldr = "Dependent Resources with Cross-References", + description = + """ + Tests dependent resources that reference each other, creating interdependencies between \ + multiple secondary resources. The test verifies that resources with circular or \ + cross-references can be safely created, managed, and deleted without causing issues, \ + even under concurrent operations with multiple primary resources. + """) class DependentResourceCrossRefIT { public static final String TEST_RESOURCE_NAME = "test"; diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/dependentssa/DependentSSAMatchingIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/dependentssa/DependentSSAMatchingIT.java index 8ab686b1b8..3fab17116f 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/dependentssa/DependentSSAMatchingIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/dependentssa/DependentSSAMatchingIT.java @@ -11,11 +11,23 @@ import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; import io.fabric8.kubernetes.client.dsl.base.PatchContext; import io.fabric8.kubernetes.client.dsl.base.PatchType; +import io.javaoperatorsdk.annotation.Sample; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; +@Sample( + tldr = "Server-Side Apply (SSA) with Dependent Resources", + description = + """ + Demonstrates how to use Server-Side Apply (SSA) with dependent resources and field manager \ + matching. This test shows how SSA allows multiple controllers to manage different \ + fields of the same resource without conflicts. The test verifies that changes made \ + by different field managers are properly isolated, and that the operator only \ + updates its own fields when changes occur, preserving fields managed by other \ + controllers. + """) public class DependentSSAMatchingIT { public static final String TEST_RESOURCE_NAME = "test1"; diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/dependentssa/DependentSSAMigrationIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/dependentssa/DependentSSAMigrationIT.java index 0d354febdf..18f7421293 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/dependentssa/DependentSSAMigrationIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/dependentssa/DependentSSAMigrationIT.java @@ -11,12 +11,22 @@ import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.KubernetesClientBuilder; import io.fabric8.kubernetes.client.utils.KubernetesResourceUtil; +import io.javaoperatorsdk.annotation.Sample; import io.javaoperatorsdk.operator.Operator; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; +@Sample( + tldr = "Migrating Dependent Resources from Legacy to SSA", + description = + """ + Demonstrates migrating dependent resource management from legacy update methods to \ + Server-Side Apply (SSA). Tests show bidirectional migration scenarios and field manager \ + handling, including using the default fabric8 field manager to avoid creating duplicate \ + managed field entries during migration. + """) class DependentSSAMigrationIT { public static final String FABRIC8_CLIENT_DEFAULT_FIELD_MANAGER = "fabric8-kubernetes-client"; diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/externalstate/ExternalStateDependentIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/externalstate/ExternalStateDependentIT.java index 87e588673d..23f19a2143 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/externalstate/ExternalStateDependentIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/externalstate/ExternalStateDependentIT.java @@ -2,8 +2,18 @@ import org.junit.jupiter.api.extension.RegisterExtension; +import io.javaoperatorsdk.annotation.Sample; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; +@Sample( + tldr = "External State Tracking in Dependent Resources", + description = + """ + Demonstrates managing dependent resources with external state that needs to be tracked \ + independently of Kubernetes resources. This pattern allows operators to maintain state \ + information for external systems or resources, ensuring proper reconciliation even when \ + the external state differs from the desired Kubernetes resource state. + """) public class ExternalStateDependentIT extends ExternalStateTestBase { @RegisterExtension diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/externalstate/ExternalStateIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/externalstate/ExternalStateIT.java index bae36431b5..e4c8bab47c 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/externalstate/ExternalStateIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/externalstate/ExternalStateIT.java @@ -5,6 +5,7 @@ import io.fabric8.kubernetes.api.model.ConfigMap; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.javaoperatorsdk.annotation.Sample; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; import io.javaoperatorsdk.operator.support.ExternalIDGenServiceMock; @@ -12,6 +13,17 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; +@Sample( + tldr = "Managing External Resources with Persistent State", + description = + """ + Demonstrates how to manage external resources (outside of Kubernetes) while maintaining \ + their state in Kubernetes resources. This test shows a pattern for reconciling \ + external systems by storing external resource identifiers in a ConfigMap. The test \ + verifies that external resources can be created, updated, and deleted in \ + coordination with Kubernetes resources, with the ConfigMap serving as a state store \ + for external resource IDs. + """) class ExternalStateIT { private static final String TEST_RESOURCE_NAME = "test1"; diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/externalstate/externalstatebulkdependent/ExternalStateBulkIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/externalstate/externalstatebulkdependent/ExternalStateBulkIT.java index b2714fab47..d9c7693d70 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/externalstate/externalstatebulkdependent/ExternalStateBulkIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/externalstate/externalstatebulkdependent/ExternalStateBulkIT.java @@ -6,12 +6,22 @@ import org.junit.jupiter.api.extension.RegisterExtension; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.javaoperatorsdk.annotation.Sample; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; import io.javaoperatorsdk.operator.support.ExternalIDGenServiceMock; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; +@Sample( + tldr = "Bulk External State Management with Persistent State", + description = + """ + Demonstrates managing multiple external resources with persistent state tracking using \ + bulk dependent resources. This combines external state management with bulk operations, \ + allowing operators to track and reconcile a variable number of external resources with \ + persistent state that survives operator restarts. + """) class ExternalStateBulkIT { private static final String TEST_RESOURCE_NAME = "test1"; diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/generickubernetesresource/generickubernetesdependentresourcemanaged/GenericKubernetesDependentManagedIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/generickubernetesresource/generickubernetesdependentresourcemanaged/GenericKubernetesDependentManagedIT.java index 93fc34fbf0..416843c723 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/generickubernetesresource/generickubernetesdependentresourcemanaged/GenericKubernetesDependentManagedIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/generickubernetesresource/generickubernetesdependentresourcemanaged/GenericKubernetesDependentManagedIT.java @@ -3,10 +3,20 @@ import org.junit.jupiter.api.extension.RegisterExtension; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.javaoperatorsdk.annotation.Sample; import io.javaoperatorsdk.operator.dependent.generickubernetesresource.GenericKubernetesDependentSpec; import io.javaoperatorsdk.operator.dependent.generickubernetesresource.GenericKubernetesDependentTestBase; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; +@Sample( + tldr = "Generic Kubernetes Dependent Resource (Managed)", + description = + """ + Demonstrates how to use GenericKubernetesResource as a managed dependent resource. This \ + test shows how to work with generic Kubernetes resources that don't have a specific \ + Java model class, allowing the operator to manage any Kubernetes resource type \ + dynamically. + """) public class GenericKubernetesDependentManagedIT extends GenericKubernetesDependentTestBase { diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/generickubernetesresource/generickubernetesdependentstandalone/GenericKubernetesDependentStandaloneIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/generickubernetesresource/generickubernetesdependentstandalone/GenericKubernetesDependentStandaloneIT.java index 9afdec5474..97ae13ae2a 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/generickubernetesresource/generickubernetesdependentstandalone/GenericKubernetesDependentStandaloneIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/generickubernetesresource/generickubernetesdependentstandalone/GenericKubernetesDependentStandaloneIT.java @@ -3,10 +3,20 @@ import org.junit.jupiter.api.extension.RegisterExtension; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.javaoperatorsdk.annotation.Sample; import io.javaoperatorsdk.operator.dependent.generickubernetesresource.GenericKubernetesDependentSpec; import io.javaoperatorsdk.operator.dependent.generickubernetesresource.GenericKubernetesDependentTestBase; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; +@Sample( + tldr = "Generic Kubernetes Resource as Standalone Dependent", + description = + """ + Tests using GenericKubernetesResource as a standalone dependent resource. This approach \ + allows operators to manage arbitrary Kubernetes resources without requiring specific Java \ + classes for each resource type, providing flexibility for managing various resource types \ + dynamically. + """) public class GenericKubernetesDependentStandaloneIT extends GenericKubernetesDependentTestBase { diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/kubernetesdependentgarbagecollection/KubernetesDependentGarbageCollectionIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/kubernetesdependentgarbagecollection/KubernetesDependentGarbageCollectionIT.java index 8e08c4e739..0ee286a65a 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/kubernetesdependentgarbagecollection/KubernetesDependentGarbageCollectionIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/kubernetesdependentgarbagecollection/KubernetesDependentGarbageCollectionIT.java @@ -7,12 +7,24 @@ import io.fabric8.kubernetes.api.model.ConfigMap; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.javaoperatorsdk.annotation.Sample; import io.javaoperatorsdk.operator.IntegrationTestConstants; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; +@Sample( + tldr = "Kubernetes Native Garbage Collection for Dependent Resources", + description = + """ + Demonstrates how to leverage Kubernetes native garbage collection for dependent resources \ + using owner references. This test shows how dependent resources are automatically \ + cleaned up by Kubernetes when the owner resource is deleted, and how to \ + conditionally create or delete dependent resources based on the primary resource \ + state. Owner references ensure that dependent resources don't outlive their \ + owners. + """) class KubernetesDependentGarbageCollectionIT { public static final String TEST_RESOURCE_NAME = "test1"; diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/multipledependentresource/MultipleDependentResourceIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/multipledependentresource/MultipleDependentResourceIT.java index a7dfefdaa8..962dfaa959 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/multipledependentresource/MultipleDependentResourceIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/multipledependentresource/MultipleDependentResourceIT.java @@ -7,6 +7,7 @@ import io.fabric8.kubernetes.api.model.ConfigMap; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.javaoperatorsdk.annotation.Sample; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; import static io.javaoperatorsdk.operator.dependent.multipledependentresource.MultipleDependentResourceConfigMap.DATA_KEY; @@ -16,6 +17,16 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; +@Sample( + tldr = "Managing Multiple Dependent Resources", + description = + """ + Demonstrates how to manage multiple dependent resources from a single reconciler. This \ + test shows how a single custom resource can create, update, and delete multiple \ + ConfigMaps (or other Kubernetes resources) as dependents. The test verifies that \ + all dependent resources are created together, updated together when the primary \ + resource changes, and properly cleaned up when the primary resource is deleted. + """) public class MultipleDependentResourceIT { public static final String CHANGED_VALUE = "changed value"; diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/multipledependentresourcewithsametype/MultipleDependentResourceWithNoDiscriminatorIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/multipledependentresourcewithsametype/MultipleDependentResourceWithNoDiscriminatorIT.java index 8be51ebf1a..4817078b7c 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/multipledependentresourcewithsametype/MultipleDependentResourceWithNoDiscriminatorIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/multipledependentresourcewithsametype/MultipleDependentResourceWithNoDiscriminatorIT.java @@ -8,11 +8,20 @@ import io.fabric8.kubernetes.api.model.ConfigMap; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.javaoperatorsdk.annotation.Sample; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; +@Sample( + tldr = "Multiple Dependents of Same Type Without Discriminator", + description = + """ + Demonstrates managing multiple dependent resources of the same type (ConfigMaps) without \ + using discriminators. The framework uses resource names to differentiate between them, \ + simplifying configuration when distinct names are sufficient for identification. + """) class MultipleDependentResourceWithNoDiscriminatorIT { public static final String TEST_RESOURCE_NAME = "multipledependentresource-testresource"; diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/multipledependentsametypemultiinformer/MultipleDependentSameTypeMultiInformerIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/multipledependentsametypemultiinformer/MultipleDependentSameTypeMultiInformerIT.java index 5ba6d56be3..0d4e40e5ee 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/multipledependentsametypemultiinformer/MultipleDependentSameTypeMultiInformerIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/multipledependentsametypemultiinformer/MultipleDependentSameTypeMultiInformerIT.java @@ -7,6 +7,7 @@ import org.junit.jupiter.api.extension.RegisterExtension; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.javaoperatorsdk.annotation.Sample; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; import static io.javaoperatorsdk.operator.IntegrationTestConstants.GARBAGE_COLLECTION_TIMEOUT_SECONDS; @@ -14,6 +15,15 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; +@Sample( + tldr = "Multiple Managed Dependents of Same Type with Multi-Informer", + description = + """ + Tests managing multiple dependent resources of the same type using separate informers \ + for each. This approach allows for independent event handling and caching for resources \ + of the same type, useful when different caching strategies or event filtering is needed \ + for different instances. + """) class MultipleDependentSameTypeMultiInformerIT { public static final String TEST_RESOURCE_NAME = "test1"; diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/multipledrsametypenodiscriminator/MultipleManagedDependentNoDiscriminatorIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/multipledrsametypenodiscriminator/MultipleManagedDependentNoDiscriminatorIT.java index 1ed1da56f9..715b4763b5 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/multipledrsametypenodiscriminator/MultipleManagedDependentNoDiscriminatorIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/multipledrsametypenodiscriminator/MultipleManagedDependentNoDiscriminatorIT.java @@ -7,12 +7,22 @@ import io.fabric8.kubernetes.api.model.ConfigMap; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.javaoperatorsdk.annotation.Sample; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; import static io.javaoperatorsdk.operator.dependent.multipledrsametypenodiscriminator.MultipleManagedDependentSameTypeNoDiscriminatorReconciler.DATA_KEY; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; +@Sample( + tldr = "Multiple Managed Dependents of Same Type Without Discriminator", + description = + """ + Demonstrates managing multiple managed dependent resources of the same type without \ + explicit discriminators. The test verifies complete CRUD operations on multiple ConfigMaps, \ + showing that resource names alone can differentiate between dependents when a \ + discriminator is not needed. + """) public class MultipleManagedDependentNoDiscriminatorIT { public static final String RESOURCE_NAME = "test1"; diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/multiplemanageddependentsametype/MultipleManagedDependentSameTypeIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/multiplemanageddependentsametype/MultipleManagedDependentSameTypeIT.java index a835b5a255..4958f84ff1 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/multiplemanageddependentsametype/MultipleManagedDependentSameTypeIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/multiplemanageddependentsametype/MultipleManagedDependentSameTypeIT.java @@ -7,6 +7,7 @@ import org.junit.jupiter.api.extension.RegisterExtension; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.javaoperatorsdk.annotation.Sample; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; import static io.javaoperatorsdk.operator.IntegrationTestConstants.GARBAGE_COLLECTION_TIMEOUT_SECONDS; @@ -14,6 +15,15 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; +@Sample( + tldr = "Managing Multiple Dependent Resources of the Same Type", + description = + """ + Demonstrates how to manage multiple dependent resources of the same type from a single \ + reconciler. This test shows how multiple ConfigMaps with the same type can be \ + created, updated, and deleted as dependent resources of a custom resource, \ + verifying proper CRUD operations and garbage collection. + """) class MultipleManagedDependentSameTypeIT { public static final String TEST_RESOURCE_NAME = "test1"; diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/multiplemanagedexternaldependenttype/MultipleManagedExternalDependentSameTypeIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/multiplemanagedexternaldependenttype/MultipleManagedExternalDependentSameTypeIT.java index a8c1f889d0..cf39c66cc7 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/multiplemanagedexternaldependenttype/MultipleManagedExternalDependentSameTypeIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/multiplemanagedexternaldependenttype/MultipleManagedExternalDependentSameTypeIT.java @@ -4,6 +4,7 @@ import org.junit.jupiter.api.extension.RegisterExtension; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.javaoperatorsdk.annotation.Sample; import io.javaoperatorsdk.operator.dependent.multiplemanageddependentsametype.MultipleManagedDependentResourceSpec; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; import io.javaoperatorsdk.operator.support.ExternalServiceMock; @@ -11,6 +12,14 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; +@Sample( + tldr = "Multiple Managed External Dependents of Same Type", + description = + """ + Tests managing multiple external (non-Kubernetes) dependent resources of the same type. \ + This demonstrates that operators can manage multiple instances of external resources \ + simultaneously, handling their lifecycle including creation, updates, and deletion. + """) class MultipleManagedExternalDependentSameTypeIT { @RegisterExtension diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/multipleupdateondependent/MultiOwnerDependentTriggeringIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/multipleupdateondependent/MultiOwnerDependentTriggeringIT.java index d5a704ca0c..975e30ecf3 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/multipleupdateondependent/MultiOwnerDependentTriggeringIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/multipleupdateondependent/MultiOwnerDependentTriggeringIT.java @@ -7,11 +7,21 @@ import io.fabric8.kubernetes.api.model.ConfigMap; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.javaoperatorsdk.annotation.Sample; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; +@Sample( + tldr = "Dependent Resource Shared by Multiple Owners", + description = + """ + Demonstrates a dependent resource (ConfigMap) that is managed by multiple primary \ + resources simultaneously. Tests verify that updates from any owner trigger proper \ + reconciliation, owner references are correctly maintained, and the shared resource \ + properly aggregates data from all owners. + """) class MultiOwnerDependentTriggeringIT { public static final String VALUE_1 = "value1"; diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/prevblocklist/PrevAnnotationBlockReconcilerIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/prevblocklist/PrevAnnotationBlockReconcilerIT.java index 137e2ba663..375327376c 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/prevblocklist/PrevAnnotationBlockReconcilerIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/prevblocklist/PrevAnnotationBlockReconcilerIT.java @@ -7,11 +7,21 @@ import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; import io.fabric8.kubernetes.api.model.apps.Deployment; +import io.javaoperatorsdk.annotation.Sample; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; +@Sample( + tldr = "Blocking Previous Annotation for Specific Resource Types", + description = + """ + Tests the previous annotation blocklist feature, which prevents storing previous resource \ + state annotations for specific resource types like Deployments. This optimization avoids \ + unnecessary reconciliation loops for resources that have server-side mutations, improving \ + performance and stability. + """) class PrevAnnotationBlockReconcilerIT { public static final String TEST_1 = "test1"; diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/primaryindexer/DependentPrimaryIndexerIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/primaryindexer/DependentPrimaryIndexerIT.java index 16bb0d46a7..774a446188 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/primaryindexer/DependentPrimaryIndexerIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/primaryindexer/DependentPrimaryIndexerIT.java @@ -1,8 +1,18 @@ package io.javaoperatorsdk.operator.dependent.primaryindexer; +import io.javaoperatorsdk.annotation.Sample; import io.javaoperatorsdk.operator.baseapi.primaryindexer.PrimaryIndexerIT; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; +@Sample( + tldr = "Primary Resource Indexer with Dependent Resources", + description = + """ + Extends PrimaryIndexerIT to test primary resource indexing functionality with dependent \ + resources. Demonstrates how custom indexes on primary resources can be used to efficiently \ + query and access resources within dependent resource implementations, enabling performant \ + lookups. + """) public class DependentPrimaryIndexerIT extends PrimaryIndexerIT { protected LocallyRunOperatorExtension buildOperator() { diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/primarytosecondaydependent/PrimaryToSecondaryDependentIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/primarytosecondaydependent/PrimaryToSecondaryDependentIT.java index 11a080a8bf..212fb1e3de 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/primarytosecondaydependent/PrimaryToSecondaryDependentIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/primarytosecondaydependent/PrimaryToSecondaryDependentIT.java @@ -9,6 +9,7 @@ import io.fabric8.kubernetes.api.model.ConfigMap; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; import io.fabric8.kubernetes.api.model.Secret; +import io.javaoperatorsdk.annotation.Sample; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; import static io.javaoperatorsdk.operator.dependent.primarytosecondaydependent.ConfigMapDependent.TEST_CONFIG_MAP_NAME; @@ -17,6 +18,16 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; +@Sample( + tldr = "Primary to Secondary Dependent Resource", + description = + """ + Demonstrates how to configure dependencies between dependent resources where one \ + dependent resource (secondary) depends on another dependent resource (primary). \ + This test shows how a Secret's creation can be conditioned on the state of a \ + ConfigMap, illustrating the use of reconcile preconditions and dependent resource \ + chaining. + """) class PrimaryToSecondaryDependentIT { public static final String TEST_CR_NAME = "test1"; diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/restart/OperatorRestartIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/restart/OperatorRestartIT.java index b8adb562dd..32923d0843 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/restart/OperatorRestartIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/restart/OperatorRestartIT.java @@ -3,12 +3,21 @@ import org.junit.jupiter.api.*; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.javaoperatorsdk.annotation.Sample; import io.javaoperatorsdk.operator.Operator; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; +@Sample( + tldr = "Operator restart and state recovery", + description = + """ + Tests that an operator can be stopped and restarted while maintaining correct behavior. \ + After restart, the operator should resume processing existing resources without \ + losing track of their state, demonstrating proper state recovery and persistence. + """) class OperatorRestartIT { private static final Operator operator = new Operator(o -> o.withCloseClientOnStop(false)); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/servicestrictmatcher/ServiceStrictMatcherIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/servicestrictmatcher/ServiceStrictMatcherIT.java index 72edd4ae40..4742a5d621 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/servicestrictmatcher/ServiceStrictMatcherIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/servicestrictmatcher/ServiceStrictMatcherIT.java @@ -6,11 +6,20 @@ import org.junit.jupiter.api.extension.RegisterExtension; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.javaoperatorsdk.annotation.Sample; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; +@Sample( + tldr = "Strict matching for Service resources", + description = + """ + Shows how to use a strict matcher for Service dependent resources that correctly handles \ + Service-specific fields. This prevents unnecessary updates when Kubernetes adds \ + default values or modifies certain fields, avoiding reconciliation loops. + """) public class ServiceStrictMatcherIT { @RegisterExtension diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/specialresourcesdependent/SpecialResourcesDependentIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/specialresourcesdependent/SpecialResourcesDependentIT.java index 3d62512bd7..58265b22b5 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/specialresourcesdependent/SpecialResourcesDependentIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/specialresourcesdependent/SpecialResourcesDependentIT.java @@ -5,6 +5,7 @@ import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; import io.fabric8.kubernetes.api.model.ServiceAccount; +import io.javaoperatorsdk.annotation.Sample; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; import static io.javaoperatorsdk.operator.dependent.specialresourcesdependent.SpecialResourceSpec.CHANGED_VALUE; @@ -16,6 +17,15 @@ * Test for resources that are somehow special, currently mostly to cover the approach to handle * resources without spec. Not all the resources added here. */ +@Sample( + tldr = "Handling special Kubernetes resources without spec", + description = + """ + Demonstrates how to handle special built-in Kubernetes resources like ServiceAccount that \ + don't have a spec field. These resources require different handling approaches \ + since their configuration is stored directly in the resource body rather than in a \ + spec section. + """) public class SpecialResourcesDependentIT { public static final String RESOURCE_NAME = "test1"; diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/ssalegacymatcher/SSAWithLegacyMatcherIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/ssalegacymatcher/SSAWithLegacyMatcherIT.java index 63afd73d1a..42cf9cf8e7 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/ssalegacymatcher/SSAWithLegacyMatcherIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/ssalegacymatcher/SSAWithLegacyMatcherIT.java @@ -5,11 +5,21 @@ import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; import io.fabric8.kubernetes.api.model.Service; +import io.javaoperatorsdk.annotation.Sample; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; +@Sample( + tldr = "Using Legacy Resource Matcher with SSA", + description = + """ + Demonstrates using the legacy resource matcher with Server-Side Apply (SSA). The legacy \ + matcher provides backward compatibility for matching logic while using SSA for updates, \ + ensuring that resource comparisons work correctly even when migrating from traditional \ + update methods to SSA. + """) public class SSAWithLegacyMatcherIT { public static final String TEST_RESOURCE_NAME = "test1"; diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/standalonedependent/StandaloneDependentResourceIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/standalonedependent/StandaloneDependentResourceIT.java index 91fbd64a19..9e8f383b84 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/standalonedependent/StandaloneDependentResourceIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/standalonedependent/StandaloneDependentResourceIT.java @@ -9,6 +9,7 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.api.model.ObjectMeta; import io.fabric8.kubernetes.api.model.apps.Deployment; +import io.javaoperatorsdk.annotation.Sample; import io.javaoperatorsdk.operator.api.config.*; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; @@ -16,6 +17,17 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; +@Sample( + tldr = "Standalone Dependent Resources", + description = + """ + Demonstrates how to use standalone dependent resources that are managed independently \ + without explicit workflow configuration. This test shows how dependent resources \ + can be created and managed programmatically, with the dependent resource handling \ + CRUD operations on a Kubernetes Deployment. The test verifies both creation and \ + update scenarios, including cache updates when the dependent resource state \ + changes. + """) public class StandaloneDependentResourceIT { public static final String DEPENDENT_TEST_NAME = "dependent-test1"; diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/statefulsetdesiredsanitizer/StatefulSetDesiredSanitizerIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/statefulsetdesiredsanitizer/StatefulSetDesiredSanitizerIT.java index f54708a9ea..fd51afb4e0 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/statefulsetdesiredsanitizer/StatefulSetDesiredSanitizerIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/statefulsetdesiredsanitizer/StatefulSetDesiredSanitizerIT.java @@ -7,11 +7,20 @@ import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; import io.fabric8.kubernetes.api.model.apps.StatefulSet; +import io.javaoperatorsdk.annotation.Sample; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; +@Sample( + tldr = "Sanitizing StatefulSet desired state for SSA", + description = + """ + Shows how to properly sanitize StatefulSet resources before using Server-Side Apply. \ + StatefulSets have immutable fields and server-managed fields that need to be \ + removed from the desired state to prevent conflicts and unnecessary updates. + """) public class StatefulSetDesiredSanitizerIT { public static final String TEST_1 = "test1"; diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/complexdependent/ComplexWorkflowIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/complexdependent/ComplexWorkflowIT.java index 4af8467c5c..8baed835b1 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/complexdependent/ComplexWorkflowIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/complexdependent/ComplexWorkflowIT.java @@ -9,6 +9,7 @@ import io.fabric8.kubernetes.api.model.Service; import io.fabric8.kubernetes.api.model.apps.StatefulSet; import io.fabric8.kubernetes.client.readiness.Readiness; +import io.javaoperatorsdk.annotation.Sample; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; import io.javaoperatorsdk.operator.workflow.complexdependent.dependent.FirstService; import io.javaoperatorsdk.operator.workflow.complexdependent.dependent.FirstStatefulSet; @@ -18,6 +19,14 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; +@Sample( + tldr = "Complex Workflow with Multiple Dependents", + description = + """ + Demonstrates a complex workflow with multiple dependent resources (StatefulSets and \ + Services) that have dependencies on each other. This test shows how to orchestrate \ + the reconciliation of interconnected dependent resources in a specific order. + """) class ComplexWorkflowIT { public static final String TEST_RESOURCE_NAME = "test1"; diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/crdpresentactivation/CRDPresentActivationConditionIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/crdpresentactivation/CRDPresentActivationConditionIT.java index 1793cceefa..a6a77d8a6e 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/crdpresentactivation/CRDPresentActivationConditionIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/crdpresentactivation/CRDPresentActivationConditionIT.java @@ -8,11 +8,21 @@ import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; import io.fabric8.kubernetes.api.model.apiextensions.v1.CustomResourceDefinition; +import io.javaoperatorsdk.annotation.Sample; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; +@Sample( + tldr = "Workflow Activation Based on CRD Presence", + description = + """ + Tests workflow activation conditions that depend on the presence of specific Custom \ + Resource Definitions (CRDs). Dependent resources are only created when their corresponding \ + CRDs exist in the cluster, allowing operators to gracefully handle optional dependencies \ + and multi-cluster scenarios. + """) public class CRDPresentActivationConditionIT { public static final String TEST_1 = "test1"; diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/getnonactivesecondary/WorkflowActivationConditionIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/getnonactivesecondary/WorkflowActivationConditionIT.java index 0de986ccf9..156ba5b7fd 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/getnonactivesecondary/WorkflowActivationConditionIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/getnonactivesecondary/WorkflowActivationConditionIT.java @@ -4,11 +4,21 @@ import org.junit.jupiter.api.extension.RegisterExtension; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.javaoperatorsdk.annotation.Sample; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; +@Sample( + tldr = "Workflow Functions on Vanilla Kubernetes Despite Inactive Resources", + description = + """ + Verifies that workflows function correctly on vanilla Kubernetes even when they include \ + resources that are not available on the platform (like OpenShift Routes). The operator \ + successfully reconciles by skipping inactive dependents based on activation conditions, \ + demonstrating platform-agnostic operator design. + """) public class WorkflowActivationConditionIT { public static final String TEST_RESOURCE_NAME = "test1"; diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/manageddependentdeletecondition/ManagedDependentDeleteConditionIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/manageddependentdeletecondition/ManagedDependentDeleteConditionIT.java index 482d2e5091..1c382247b8 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/manageddependentdeletecondition/ManagedDependentDeleteConditionIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/manageddependentdeletecondition/ManagedDependentDeleteConditionIT.java @@ -9,11 +9,21 @@ import io.fabric8.kubernetes.api.model.ConfigMap; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; import io.fabric8.kubernetes.api.model.Secret; +import io.javaoperatorsdk.annotation.Sample; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; +@Sample( + tldr = "Managed Dependent Delete Condition", + description = + """ + Demonstrates how to use delete conditions to control when dependent resources can be \ + deleted. This test shows how the primary resource deletion can be blocked until \ + dependent resources are properly cleaned up, ensuring graceful shutdown and \ + preventing orphaned resources. + """) public class ManagedDependentDeleteConditionIT { public static final String RESOURCE_NAME = "test1"; diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/multipledependentwithactivation/MultipleDependentWithActivationIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/multipledependentwithactivation/MultipleDependentWithActivationIT.java index 2360660adc..4845dfe476 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/multipledependentwithactivation/MultipleDependentWithActivationIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/multipledependentwithactivation/MultipleDependentWithActivationIT.java @@ -6,11 +6,21 @@ import io.fabric8.kubernetes.api.model.ConfigMap; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; import io.fabric8.kubernetes.api.model.Secret; +import io.javaoperatorsdk.annotation.Sample; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; +@Sample( + tldr = "Multiple Dependents with Activation Conditions", + description = + """ + Demonstrates how to use activation conditions with multiple dependent resources. This test \ + shows how different dependent resources can be dynamically enabled or disabled \ + based on runtime conditions, allowing flexible workflow behavior that adapts to \ + changing requirements. + """) public class MultipleDependentWithActivationIT { public static final String INITIAL_VALUE = "initial_value"; diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/orderedmanageddependent/OrderedManagedDependentIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/orderedmanageddependent/OrderedManagedDependentIT.java index ff25911a8c..56ff94d1f1 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/orderedmanageddependent/OrderedManagedDependentIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/orderedmanageddependent/OrderedManagedDependentIT.java @@ -6,11 +6,20 @@ import org.junit.jupiter.api.extension.RegisterExtension; import io.fabric8.kubernetes.api.model.ObjectMeta; +import io.javaoperatorsdk.annotation.Sample; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; +@Sample( + tldr = "Ordered Managed Dependent Resources", + description = + """ + Demonstrates how to control the order of reconciliation for managed dependent resources. \ + This test verifies that dependent resources are reconciled in a specific sequence, \ + ensuring proper orchestration when dependencies have ordering requirements. + """) class OrderedManagedDependentIT { @RegisterExtension diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/workflowactivationcleanup/WorkflowActivationCleanupIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/workflowactivationcleanup/WorkflowActivationCleanupIT.java index 80bbf22d40..3c366877fd 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/workflowactivationcleanup/WorkflowActivationCleanupIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/workflowactivationcleanup/WorkflowActivationCleanupIT.java @@ -8,12 +8,21 @@ import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.KubernetesClientBuilder; import io.fabric8.kubernetes.client.utils.KubernetesResourceUtil; +import io.javaoperatorsdk.annotation.Sample; import io.javaoperatorsdk.operator.Operator; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; +@Sample( + tldr = "Workflow Activation Cleanup", + description = + """ + Demonstrates how workflow cleanup is handled when activation conditions are involved. This \ + test verifies that resources are properly cleaned up on operator startup even when \ + marked for deletion, ensuring no orphaned resources remain after restarts. + """) public class WorkflowActivationCleanupIT { private final KubernetesClient client = new KubernetesClientBuilder().build(); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/workflowactivationcondition/WorkflowActivationConditionIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/workflowactivationcondition/WorkflowActivationConditionIT.java index a5b5b23fe3..108a7d4a54 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/workflowactivationcondition/WorkflowActivationConditionIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/workflowactivationcondition/WorkflowActivationConditionIT.java @@ -5,12 +5,22 @@ import io.fabric8.kubernetes.api.model.ConfigMap; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.javaoperatorsdk.annotation.Sample; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; import static io.javaoperatorsdk.operator.workflow.workflowactivationcondition.ConfigMapDependentResource.DATA_KEY; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; +@Sample( + tldr = "Workflow Activation Condition", + description = + """ + Demonstrates how to use activation conditions to conditionally enable or disable parts of \ + a workflow. This test shows how the workflow can adapt to different environments \ + (e.g., vanilla Kubernetes vs. OpenShift) by activating only the relevant dependent \ + resources based on runtime conditions. + """) public class WorkflowActivationConditionIT { public static final String TEST_RESOURCE_NAME = "test1"; diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/workflowallfeature/WorkflowAllFeatureIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/workflowallfeature/WorkflowAllFeatureIT.java index 93551dcf43..9cf798033a 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/workflowallfeature/WorkflowAllFeatureIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/workflowallfeature/WorkflowAllFeatureIT.java @@ -9,12 +9,22 @@ import io.fabric8.kubernetes.api.model.ConfigMap; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; import io.fabric8.kubernetes.api.model.apps.Deployment; +import io.javaoperatorsdk.annotation.Sample; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; import static io.javaoperatorsdk.operator.workflow.workflowallfeature.ConfigMapDependentResource.READY_TO_DELETE_ANNOTATION; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; +@Sample( + tldr = "Comprehensive workflow with reconcile and delete conditions", + description = + """ + Demonstrates a complete workflow implementation including reconcile conditions, delete \ + conditions, and ready conditions. Shows how to control when dependent resources are \ + created or deleted based on conditions, and how to coordinate dependencies that \ + must wait for others to be ready. + """) public class WorkflowAllFeatureIT { public static final String RESOURCE_NAME = "test"; diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/workflowexplicitcleanup/WorkflowExplicitCleanupIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/workflowexplicitcleanup/WorkflowExplicitCleanupIT.java index b6d4969fd3..a0227de13b 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/workflowexplicitcleanup/WorkflowExplicitCleanupIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/workflowexplicitcleanup/WorkflowExplicitCleanupIT.java @@ -5,11 +5,21 @@ import io.fabric8.kubernetes.api.model.ConfigMap; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.javaoperatorsdk.annotation.Sample; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; +@Sample( + tldr = "Explicit Workflow Cleanup Invocation", + description = + """ + Tests explicit workflow cleanup invocation, demonstrating that workflow cleanup is called \ + even when using explicit workflow invocation mode. This ensures that dependent resources \ + are properly cleaned up during deletion regardless of how the workflow is invoked, \ + maintaining consistent cleanup behavior. + """) public class WorkflowExplicitCleanupIT { public static final String RESOURCE_NAME = "test1"; diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/workflowexplicitinvocation/WorkflowExplicitInvocationIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/workflowexplicitinvocation/WorkflowExplicitInvocationIT.java index eaa799300f..8027750346 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/workflowexplicitinvocation/WorkflowExplicitInvocationIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/workflowexplicitinvocation/WorkflowExplicitInvocationIT.java @@ -7,11 +7,21 @@ import io.fabric8.kubernetes.api.model.ConfigMap; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.javaoperatorsdk.annotation.Sample; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; +@Sample( + tldr = "Workflow Explicit Invocation", + description = + """ + Demonstrates how to explicitly control when a workflow is invoked rather than having it \ + run automatically on every reconciliation. This test shows how to programmatically \ + trigger workflow execution and how cleanup is still performed even with explicit \ + invocation. + """) public class WorkflowExplicitInvocationIT { public static final String RESOURCE_NAME = "test1"; diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/workflowmultipleactivation/WorkflowMultipleActivationIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/workflowmultipleactivation/WorkflowMultipleActivationIT.java index 0d5f3b60f1..cd40d2c08a 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/workflowmultipleactivation/WorkflowMultipleActivationIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/workflowmultipleactivation/WorkflowMultipleActivationIT.java @@ -8,12 +8,22 @@ import io.fabric8.kubernetes.api.model.ConfigMap; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; import io.fabric8.kubernetes.api.model.Secret; +import io.javaoperatorsdk.annotation.Sample; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; import static io.javaoperatorsdk.operator.workflow.workflowactivationcondition.ConfigMapDependentResource.DATA_KEY; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; +@Sample( + tldr = "Dynamic Workflow Activation and Deactivation", + description = + """ + Tests dynamic activation and deactivation of workflow dependents based on changing \ + conditions. Demonstrates that dependents can be conditionally activated or deactivated \ + during the resource lifecycle, with proper cleanup and recreation, and verifies that \ + inactive dependents don't trigger reconciliation or maintain informers. + """) public class WorkflowMultipleActivationIT { public static final String INITIAL_DATA = "initial data"; diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/workflowsilentexceptionhandling/WorkflowSilentExceptionHandlingIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/workflowsilentexceptionhandling/WorkflowSilentExceptionHandlingIT.java index 2f5b7246c6..926d7d7622 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/workflowsilentexceptionhandling/WorkflowSilentExceptionHandlingIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/workflowsilentexceptionhandling/WorkflowSilentExceptionHandlingIT.java @@ -4,11 +4,21 @@ import org.junit.jupiter.api.extension.RegisterExtension; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.javaoperatorsdk.annotation.Sample; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; +@Sample( + tldr = "Silent Workflow Exception Handling in Reconciler", + description = + """ + Demonstrates handling workflow exceptions silently within the reconciler rather than \ + propagating them. Tests verify that exceptions from dependent resources during both \ + reconciliation and cleanup are captured in the result object, allowing custom error \ + handling logic without failing the entire reconciliation. + """) public class WorkflowSilentExceptionHandlingIT { @RegisterExtension diff --git a/pom.xml b/pom.xml index 9931eefb7f..c2e6ae8443 100644 --- a/pom.xml +++ b/pom.xml @@ -36,6 +36,7 @@ sample-operators caffeine-bounded-cache-support bootstrapper-maven-plugin + test-index-processor diff --git a/test-index-processor/pom.xml b/test-index-processor/pom.xml new file mode 100644 index 0000000000..dc3e84ab3e --- /dev/null +++ b/test-index-processor/pom.xml @@ -0,0 +1,31 @@ + + + 4.0.0 + + + io.javaoperatorsdk + java-operator-sdk + 5.1.6-SNAPSHOT + + + test-index-processor + Test Index Annotation Processor + Annotation processor for generating integration test index documentation + + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + -proc:none + + + + + diff --git a/test-index-processor/src/main/java/io/javaoperatorsdk/annotation/Sample.java b/test-index-processor/src/main/java/io/javaoperatorsdk/annotation/Sample.java new file mode 100644 index 0000000000..3bfb1a4e79 --- /dev/null +++ b/test-index-processor/src/main/java/io/javaoperatorsdk/annotation/Sample.java @@ -0,0 +1,29 @@ +package io.javaoperatorsdk.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation to mark integration tests as samples for documentation generation. Tests annotated + * with @Sample will be included in the generated samples.md documentation file. + */ +@Retention(RetentionPolicy.SOURCE) +@Target(ElementType.TYPE) +public @interface Sample { + + /** + * A short title describing the test sample. + * + * @return the short title + */ + String tldr(); + + /** + * A detailed description of what the test does and demonstrates. + * + * @return the detailed description + */ + String description(); +} diff --git a/test-index-processor/src/main/java/io/javaoperatorsdk/processor/SampleProcessor.java b/test-index-processor/src/main/java/io/javaoperatorsdk/processor/SampleProcessor.java new file mode 100644 index 0000000000..d59e7bf687 --- /dev/null +++ b/test-index-processor/src/main/java/io/javaoperatorsdk/processor/SampleProcessor.java @@ -0,0 +1,143 @@ +package io.javaoperatorsdk.processor; + +import java.io.BufferedWriter; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Set; + +import javax.annotation.processing.AbstractProcessor; +import javax.annotation.processing.RoundEnvironment; +import javax.annotation.processing.SupportedAnnotationTypes; +import javax.annotation.processing.SupportedSourceVersion; +import javax.lang.model.SourceVersion; +import javax.lang.model.element.Element; +import javax.lang.model.element.TypeElement; +import javax.tools.Diagnostic; +import javax.tools.FileObject; +import javax.tools.StandardLocation; + +import io.javaoperatorsdk.annotation.Sample; + +/** + * Annotation processor that generates a samples.md file containing documentation for all + * integration tests annotated with @Sample. + */ +@SupportedAnnotationTypes("io.javaoperatorsdk.annotation.Sample") +@SupportedSourceVersion(SourceVersion.RELEASE_17) +public class SampleProcessor extends AbstractProcessor { + + private final List samples = new ArrayList<>(); + private boolean processed = false; + + @Override + public boolean process(Set annotations, RoundEnvironment roundEnv) { + if (processed) { + return true; + } + + // Collect all @Sample annotated elements + for (Element element : roundEnv.getElementsAnnotatedWith(Sample.class)) { + if (element instanceof TypeElement) { + TypeElement typeElement = (TypeElement) element; + Sample sample = element.getAnnotation(Sample.class); + + String className = typeElement.getQualifiedName().toString(); + String simpleName = typeElement.getSimpleName().toString(); + String tldr = sample.tldr(); + String description = sample.description(); + + samples.add(new SampleInfo(className, simpleName, tldr, description)); + } + } + + // Generate the markdown file in the last round + if (roundEnv.processingOver() && !samples.isEmpty()) { + generateMarkdownFile(); + processed = true; + } + + return true; + } + + private void generateMarkdownFile() { + try { + // Sort samples by class name for consistent ordering + samples.sort(Comparator.comparing(s -> s.className)); + + // Create the samples.md file in the source output location + FileObject file = + processingEnv.getFiler().createResource(StandardLocation.SOURCE_OUTPUT, "", "samples.md"); + + try (BufferedWriter writer = new BufferedWriter(file.openWriter())) { + writer.write("---\n"); + writer.write("title: Integration Test Index\n"); + writer.write("weight: 105\n"); + writer.write("---\n\n"); + writer.write( + "This document provides an index of all integration tests annotated with @Sample.\n\n" + + "These server also as sample for various use cases. " + + "Your are encouraged to improve both the tests and/or descriptions.\n\n"); + + // Generate table of contents + writer.write("## Contents\n\n"); + for (SampleInfo sample : samples) { + String anchor = sample.simpleName.toLowerCase(); + writer.write("- [" + sample.tldr + "](#" + anchor + ")\n"); + } + writer.write("\n---\n\n"); + + // Generate individual test sections + for (SampleInfo sample : samples) { + writer.write("## " + sample.simpleName + "\n\n"); + writer.write("**" + sample.tldr + "**\n\n"); + writer.write(sample.description + "\n\n"); + writer.write("**Package:** " + getGitHubPackageLink(sample.className) + "\n\n"); + writer.write("---\n\n"); + } + } + + processingEnv + .getMessager() + .printMessage( + Diagnostic.Kind.NOTE, "Generated samples.md with " + samples.size() + " samples"); + } catch (IOException e) { + processingEnv + .getMessager() + .printMessage(Diagnostic.Kind.ERROR, "Failed to generate samples.md: " + e.getMessage()); + } + } + + private String getGitHubPackageLink(String className) { + // Extract package name by removing the simple class name + int lastDot = className.lastIndexOf('.'); + if (lastDot == -1) { + return "[root package]"; + } + + String packageName = className.substring(0, lastDot); + String packagePath = packageName.replace('.', '/'); + + // GitHub repository base URL + String baseUrl = "https://github.com/operator-framework/java-operator-sdk/tree/main"; + String sourcePath = "operator-framework/src/test/java"; + + String githubUrl = baseUrl + "/" + sourcePath + "/" + packagePath; + return "[" + packageName + "](" + githubUrl + ")"; + } + + private static class SampleInfo { + final String className; + final String simpleName; + final String tldr; + final String description; + + SampleInfo(String className, String simpleName, String tldr, String description) { + this.className = className; + this.simpleName = simpleName; + this.tldr = tldr; + this.description = description; + } + } +} diff --git a/test-index-processor/src/main/resources/META-INF/services/javax.annotation.processing.Processor b/test-index-processor/src/main/resources/META-INF/services/javax.annotation.processing.Processor new file mode 100644 index 0000000000..a9fb2eb851 --- /dev/null +++ b/test-index-processor/src/main/resources/META-INF/services/javax.annotation.processing.Processor @@ -0,0 +1 @@ +io.javaoperatorsdk.processor.SampleProcessor From 9eeaa23868a87140bcb933803cf009850ad41dcc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Fri, 21 Nov 2025 16:17:25 +0100 Subject: [PATCH 24/75] fix: hugo build for test index (#3054) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .github/workflows/hugo.yaml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/hugo.yaml b/.github/workflows/hugo.yaml index 320cfb177a..04a6edb790 100644 --- a/.github/workflows/hugo.yaml +++ b/.github/workflows/hugo.yaml @@ -53,8 +53,7 @@ jobs: cache: maven - name: Build test-index-processor and generate test documentation run: | - ./mvnw clean install -DskipTests -pl test-index-processor - ./mvnw process-test-classes -DskipTests -pl operator-framework + ./mvnw clean install -DskipTests - name: Setup Pages id: pages uses: actions/configure-pages@v5 From 1383c8cb00a55891ac5365437b3a5177727930ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Sat, 22 Nov 2025 10:20:16 +0100 Subject: [PATCH 25/75] docs: organize test index into categories (#3055) --- .../processor/SampleProcessor.java | 98 +++++++++++++++---- 1 file changed, 81 insertions(+), 17 deletions(-) diff --git a/test-index-processor/src/main/java/io/javaoperatorsdk/processor/SampleProcessor.java b/test-index-processor/src/main/java/io/javaoperatorsdk/processor/SampleProcessor.java index d59e7bf687..0caf73968f 100644 --- a/test-index-processor/src/main/java/io/javaoperatorsdk/processor/SampleProcessor.java +++ b/test-index-processor/src/main/java/io/javaoperatorsdk/processor/SampleProcessor.java @@ -63,8 +63,25 @@ public boolean process(Set annotations, RoundEnvironment private void generateMarkdownFile() { try { - // Sort samples by class name for consistent ordering - samples.sort(Comparator.comparing(s -> s.className)); + // Categorize samples by package + List baseApiSamples = new ArrayList<>(); + List dependentSamples = new ArrayList<>(); + List workflowSamples = new ArrayList<>(); + + for (SampleInfo sample : samples) { + if (sample.className.contains(".baseapi.")) { + baseApiSamples.add(sample); + } else if (sample.className.contains(".dependent.")) { + dependentSamples.add(sample); + } else if (sample.className.contains(".workflow.")) { + workflowSamples.add(sample); + } + } + + // Sort each category by class name + baseApiSamples.sort(Comparator.comparing(s -> s.className)); + dependentSamples.sort(Comparator.comparing(s -> s.className)); + workflowSamples.sort(Comparator.comparing(s -> s.className)); // Create the samples.md file in the source output location FileObject file = @@ -73,28 +90,67 @@ private void generateMarkdownFile() { try (BufferedWriter writer = new BufferedWriter(file.openWriter())) { writer.write("---\n"); writer.write("title: Integration Test Index\n"); - writer.write("weight: 105\n"); + writer.write("weight: 85\n"); writer.write("---\n\n"); writer.write( "This document provides an index of all integration tests annotated with @Sample.\n\n" - + "These server also as sample for various use cases. " - + "Your are encouraged to improve both the tests and/or descriptions.\n\n"); + + "These serve also as samples for various use cases. " + + "You are encouraged to improve both the tests and/or descriptions.\n\n"); // Generate table of contents writer.write("## Contents\n\n"); - for (SampleInfo sample : samples) { - String anchor = sample.simpleName.toLowerCase(); - writer.write("- [" + sample.tldr + "](#" + anchor + ")\n"); + + if (!baseApiSamples.isEmpty()) { + writer.write("### Base API\n\n"); + for (SampleInfo sample : baseApiSamples) { + String anchor = sample.simpleName.toLowerCase(); + writer.write("- [" + sample.tldr + "](#" + anchor + ")\n"); + } + writer.write("\n"); } - writer.write("\n---\n\n"); - - // Generate individual test sections - for (SampleInfo sample : samples) { - writer.write("## " + sample.simpleName + "\n\n"); - writer.write("**" + sample.tldr + "**\n\n"); - writer.write(sample.description + "\n\n"); - writer.write("**Package:** " + getGitHubPackageLink(sample.className) + "\n\n"); - writer.write("---\n\n"); + + if (!dependentSamples.isEmpty()) { + writer.write("### Dependent Resources\n\n"); + for (SampleInfo sample : dependentSamples) { + String anchor = sample.simpleName.toLowerCase(); + writer.write("- [" + sample.tldr + "](#" + anchor + ")\n"); + } + writer.write("\n"); + } + + if (!workflowSamples.isEmpty()) { + writer.write("### Workflows\n\n"); + for (SampleInfo sample : workflowSamples) { + String anchor = sample.simpleName.toLowerCase(); + writer.write("- [" + sample.tldr + "](#" + anchor + ")\n"); + } + writer.write("\n"); + } + + writer.write("---\n\n"); + + // Generate Base API section + if (!baseApiSamples.isEmpty()) { + writer.write("# Base API\n\n"); + for (SampleInfo sample : baseApiSamples) { + writeSampleSection(writer, sample); + } + } + + // Generate Dependent Resources section + if (!dependentSamples.isEmpty()) { + writer.write("# Dependent Resources\n\n"); + for (SampleInfo sample : dependentSamples) { + writeSampleSection(writer, sample); + } + } + + // Generate Workflows section + if (!workflowSamples.isEmpty()) { + writer.write("# Workflows\n\n"); + for (SampleInfo sample : workflowSamples) { + writeSampleSection(writer, sample); + } } } @@ -109,6 +165,14 @@ private void generateMarkdownFile() { } } + private void writeSampleSection(BufferedWriter writer, SampleInfo sample) throws IOException { + writer.write("## " + sample.simpleName + "\n\n"); + writer.write("**" + sample.tldr + "**\n\n"); + writer.write(sample.description + "\n\n"); + writer.write("**Package:** " + getGitHubPackageLink(sample.className) + "\n\n"); + writer.write("---\n\n"); + } + private String getGitHubPackageLink(String className) { // Extract package name by removing the simple class name int lastDot = className.lastIndexOf('.'); From ab0f1d64b4a48f8af2d15b3fa1b7eac2efbe32eb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Nov 2025 09:43:20 +0100 Subject: [PATCH 26/75] chore(deps): bump org.takes:takes from 1.24.6 to 1.25.0 (#3056) Bumps [org.takes:takes](https://github.com/yegor256/takes) from 1.24.6 to 1.25.0. - [Release notes](https://github.com/yegor256/takes/releases) - [Commits](https://github.com/yegor256/takes/compare/1.24.6...1.25.0) --- updated-dependencies: - dependency-name: org.takes:takes dependency-version: 1.25.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- sample-operators/controller-namespace-deletion/pom.xml | 2 +- sample-operators/leader-election/pom.xml | 2 +- sample-operators/mysql-schema/pom.xml | 2 +- sample-operators/tomcat-operator/pom.xml | 2 +- sample-operators/webpage/pom.xml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/sample-operators/controller-namespace-deletion/pom.xml b/sample-operators/controller-namespace-deletion/pom.xml index 3c04058c74..e5b2bbc985 100644 --- a/sample-operators/controller-namespace-deletion/pom.xml +++ b/sample-operators/controller-namespace-deletion/pom.xml @@ -43,7 +43,7 @@ org.takes takes - 1.24.6 + 1.25.0 org.awaitility diff --git a/sample-operators/leader-election/pom.xml b/sample-operators/leader-election/pom.xml index 6becc69448..f17c5ae1d4 100644 --- a/sample-operators/leader-election/pom.xml +++ b/sample-operators/leader-election/pom.xml @@ -43,7 +43,7 @@ org.takes takes - 1.24.6 + 1.25.0 org.awaitility diff --git a/sample-operators/mysql-schema/pom.xml b/sample-operators/mysql-schema/pom.xml index d5e01ca206..61b9108aec 100644 --- a/sample-operators/mysql-schema/pom.xml +++ b/sample-operators/mysql-schema/pom.xml @@ -37,7 +37,7 @@ org.takes takes - 1.24.6 + 1.25.0 mysql diff --git a/sample-operators/tomcat-operator/pom.xml b/sample-operators/tomcat-operator/pom.xml index 2f925bba5f..8b5b755543 100644 --- a/sample-operators/tomcat-operator/pom.xml +++ b/sample-operators/tomcat-operator/pom.xml @@ -52,7 +52,7 @@ org.takes takes - 1.24.6 + 1.25.0 org.junit.jupiter diff --git a/sample-operators/webpage/pom.xml b/sample-operators/webpage/pom.xml index 5f237bbd29..ed44583e59 100644 --- a/sample-operators/webpage/pom.xml +++ b/sample-operators/webpage/pom.xml @@ -42,7 +42,7 @@ org.takes takes - 1.24.6 + 1.25.0 org.awaitility From b35c29ff5da43219c63c92f7795c1877c5544685 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Nov 2025 09:50:11 +0100 Subject: [PATCH 27/75] chore(deps): bump org.apache.maven.plugins:maven-antrun-plugin (#3057) Bumps [org.apache.maven.plugins:maven-antrun-plugin](https://github.com/apache/maven-antrun-plugin) from 3.1.0 to 3.2.0. - [Release notes](https://github.com/apache/maven-antrun-plugin/releases) - [Commits](https://github.com/apache/maven-antrun-plugin/compare/maven-antrun-plugin-3.1.0...maven-antrun-plugin-3.2.0) --- updated-dependencies: - dependency-name: org.apache.maven.plugins:maven-antrun-plugin dependency-version: 3.2.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- operator-framework/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/operator-framework/pom.xml b/operator-framework/pom.xml index 6dc9549be0..01032ccc94 100644 --- a/operator-framework/pom.xml +++ b/operator-framework/pom.xml @@ -190,7 +190,7 @@ org.apache.maven.plugins maven-antrun-plugin - 3.1.0 + 3.2.0 rename-samples-to-index From 40e6cc833690825fecc4678ef62766cdff1998e5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Nov 2025 11:55:27 +0100 Subject: [PATCH 28/75] chore(deps): bump actions/setup-java from 4 to 5 (#3058) Bumps [actions/setup-java](https://github.com/actions/setup-java) from 4 to 5. - [Release notes](https://github.com/actions/setup-java/releases) - [Commits](https://github.com/actions/setup-java/compare/v4...v5) --- updated-dependencies: - dependency-name: actions/setup-java dependency-version: '5' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/hugo.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/hugo.yaml b/.github/workflows/hugo.yaml index 04a6edb790..7c5a1fa62a 100644 --- a/.github/workflows/hugo.yaml +++ b/.github/workflows/hugo.yaml @@ -46,7 +46,7 @@ jobs: submodules: recursive fetch-depth: 0 - name: Set up JDK 17 - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: java-version: '17' distribution: 'temurin' From b26c008f41747435987fad1f37a93cee3015cf12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Mon, 24 Nov 2025 18:43:03 +0100 Subject: [PATCH 29/75] docs: add missing favicons to the page (#3060) --- docs/assets/icons/logo-colored.svg | 17 +++++++++++++++++ docs/static/favicons/android-144x144.png | Bin 0 -> 4950 bytes docs/static/favicons/android-192x192.png | Bin 0 -> 6804 bytes docs/static/favicons/android-36x36.png | Bin 0 -> 1210 bytes docs/static/favicons/android-48x48.png | Bin 0 -> 1605 bytes docs/static/favicons/android-72x72.png | Bin 0 -> 2447 bytes docs/static/favicons/android-96x96.png | Bin 0 -> 3294 bytes .../favicons/apple-touch-icon-180x180.png | Bin 0 -> 6395 bytes docs/static/favicons/favicon-1024.png | Bin 0 -> 41992 bytes docs/static/favicons/favicon-16x16.png | Bin 0 -> 513 bytes docs/static/favicons/favicon-256.png | Bin 0 -> 9161 bytes docs/static/favicons/favicon-32x32.png | Bin 0 -> 1032 bytes docs/static/favicons/favicon.ico | Bin 1150 -> 9662 bytes docs/static/favicons/pwa-192x192.png | Bin 0 -> 6804 bytes docs/static/favicons/pwa-512x512.png | Bin 0 -> 19469 bytes docs/static/favicons/tile150x150.png | Bin 0 -> 5244 bytes docs/static/favicons/tile310x310.png | Bin 0 -> 11158 bytes docs/static/favicons/tile70x70.png | Bin 0 -> 2363 bytes 18 files changed, 17 insertions(+) create mode 100644 docs/assets/icons/logo-colored.svg create mode 100644 docs/static/favicons/android-144x144.png create mode 100644 docs/static/favicons/android-192x192.png create mode 100644 docs/static/favicons/android-36x36.png create mode 100644 docs/static/favicons/android-48x48.png create mode 100644 docs/static/favicons/android-72x72.png create mode 100644 docs/static/favicons/android-96x96.png create mode 100644 docs/static/favicons/apple-touch-icon-180x180.png create mode 100644 docs/static/favicons/favicon-1024.png create mode 100644 docs/static/favicons/favicon-16x16.png create mode 100644 docs/static/favicons/favicon-256.png create mode 100644 docs/static/favicons/favicon-32x32.png create mode 100644 docs/static/favicons/pwa-192x192.png create mode 100644 docs/static/favicons/pwa-512x512.png create mode 100644 docs/static/favicons/tile150x150.png create mode 100644 docs/static/favicons/tile310x310.png create mode 100644 docs/static/favicons/tile70x70.png diff --git a/docs/assets/icons/logo-colored.svg b/docs/assets/icons/logo-colored.svg new file mode 100644 index 0000000000..6f757c3188 --- /dev/null +++ b/docs/assets/icons/logo-colored.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + diff --git a/docs/static/favicons/android-144x144.png b/docs/static/favicons/android-144x144.png new file mode 100644 index 0000000000000000000000000000000000000000..584673895d15106a3d8581a9a20f6fb1237ad6c0 GIT binary patch literal 4950 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 literal 0 HcmV?d00001 diff --git a/docs/static/favicons/android-192x192.png b/docs/static/favicons/android-192x192.png new file mode 100644 index 0000000000000000000000000000000000000000..751a15b91a0e95c9eda3101040df9d923b16add2 GIT binary patch literal 6804 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 literal 0 HcmV?d00001 diff --git a/docs/static/favicons/android-36x36.png b/docs/static/favicons/android-36x36.png new file mode 100644 index 0000000000000000000000000000000000000000..881447eb47c1d4ac42233aeee2ac13a7e29ab409 GIT binary patch literal 1210 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 literal 0 HcmV?d00001 diff --git a/docs/static/favicons/android-72x72.png b/docs/static/favicons/android-72x72.png new file mode 100644 index 0000000000000000000000000000000000000000..475d2deed16f2ce43350a052202787592d5742dc GIT binary patch literal 2447 zcmV;A32^p_P)JRdvAi2@2}mN+4DPlcJADnGjkVCaQx93i(cJvXXNx_?ME({ zpam5KcqHU7FKF$l5O{#P*GHEFUs$yF&_9CKo(h2n$XcSn3)0q>30iw91R9{dch}7n zxL)8-LFyJlpaDvm)Q!NjP*M8DAax5N&;Y5hO3|I?x=+3rq;4Su7NA6|wi>ulFrS;t z3j*yyb~S(z`D1XJg%e1CI^2~-0yhC4E(*`v8HA=+1QH-;aQX^hHcFqkt_?!dD*_3S z8mR?9_+k)xULt-A^xU=j%ycNz3pfCdaVi&W*x2c}u2=ZpgECGg!r|ZmSZipEhQrGDyqiLU|bj;*>v;G8kVA%i@($G!P9KefHW*8o{`)dW@ZlgZFC zJ!{q!`>F2*z75hnYt|H}XAB$!PM)xlvQ;v2efa||ANl1+q4?H=k|iHka?7=+%G>0#kWgLLAqkAx#5`ML1xszN%?ntNK~|K)!bJVcgZcx~}O=^ zxuT+}_2pc|I^2~-COQ3mM1Nh;5U@{lM%oRsTYH-ojH=O|3ifv6%~_ zdeF|gx^T&2PY}8MTOS#qWQ0Rv7g>?cTJhVnYQ?(X9~4dxgp*I^Z;AbbxmTh_bZO! z7FD?!DEh2faj|nPP|!knXbe(YJ!-HK+5Aqp?6&2un=Zt7Hm1nTB%{CZdSQC_$NTFx z*L`(Psu$J6=Kuh!=Ey4|U;8k0ZW$PR+kIQmmW;~NRZXplLWTSy5v#2h(Hl@31(}mw zr$(--+|aVM5c|X3rZX081HLsuyZyjclLQSDRsFNTATIQ@f`gQn<% z{#E5eE8vUbKZrJ^B3N|_Quk1sAvh6^8`8HLb-wY3UkRi z|KkH0f&CWxM5GgRHj``Kf*%bpa20#5UIreJSBb2>X1E6>V$lmk zXaPxEt2M{-+-D*ZvD#|EZWb^ZG+Nadf1^;FBVAs&7i9^m%N+Pgp@xqIg8eCQfx=Qj zTrX^nWCfM*@Jso8JnS|O^FMnb9;K7BdXQwL5!91ZL697Nl5xau!IQoI;Q(3HT_eRZ zr@KB<@Io(0y6%ZoGdkPo$rFvk0a9!Jl3k%jqFx9Dg=R)p@c^o$zt2<|H4o~FMRzHD zc7}OC2`{Q_eEc0Rg#4s?UCkL*oi`ad6S=dZvGpP^Y>ytul!vn3O&3y+>hZO_I0AKD zlTqghvJQ-j$S=LJKPo_r_Y7Z5mgCFmtxvFJKfV_8L?;OHtZAGsR$@=3&^s|B9OE>rZ;@JBO# z;KzzNU*2*f(*{%gDm%Rjs3MWd#uiWa`zIVfky>TYYDNLBNuUE_bse>&IR zvJ7M5K=r5!<<5$x)}MQ=chmv6D;D{_A`de9Qp^AwLuqHj!Us0~$7_csqJ3R-UI@7d z)!MQCsOTM)4e>|((4F)Sm59}TU9hddsbi{=BKo9ccx-WV{B=K^1L&xaUSbTlqTIyT zRIu_MLQO?u>!1D7&*>JPh}F#zQjY>FCTQ|cL7x@N@Apqzu&a>#%{JE!oi^CFOoVR; zTnAK6(Dd0f&UgJjLX+1Vr)zEGYihWkpOI+$x$4B!=iUpHpXAd$M>m{_asn3y4OWlsOk?vF89N! zab=a4G5TP6L;QI!b}MqFpjmQveWcuq)F^yYpu#JshEY0Hcuq94rJ`xeo1WXx0ne5Z z9cydPbxh`Rh06q4szU+G=ZV5k5js`bWt6`;j(ug}hRu7u@^7m6_c+wbl}^t&Gv=$q zT(x?-8kr?l9V>c@QTb4fWW?%0F>*jM>|eO|qrT6}e+r%81Sjx}{{xDGMq*zf%|8GD N002ovPDHLkV1iGpt*-z8 literal 0 HcmV?d00001 diff --git a/docs/static/favicons/android-96x96.png b/docs/static/favicons/android-96x96.png new file mode 100644 index 0000000000000000000000000000000000000000..e39a382da61f8bb3e2980ed494af1cf9d9e7448d GIT binary patch literal 3294 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℘qKc0F;QUu)zkWoP~gL&A=fcW%;Id+mYW_j52 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�>2kds}P!Sj_l9?Qi$W z?OF$++n44|gQqPme{U|GAwWNFEEgaOQN0M>{)5mDmt|Qc^Fp(#Kh+lZ0_KkVk$45v zdJlzncjMBZkRrn(eh=m6AWleG4+Ks)Rlz@(&0$+k1N? z_))G{1-AW$?c8cLIL=y@00rkr1Nl7wWceAi=V{?X7{tlr8wU|p=O%EfvRxfaM^p)f z=4(9kW;UNZlcBt#i6KPN1}5SwOA2dw%e4zf5A!-ysc#&DAYe&n2(*?eUI{CDLW_Mk zzD=RyWlO;w=e+iIs#OS!{75@=ldna0VQ7cu9CpZjGqtDiIxIj!g8x26PQ zOG>glTogzt{GI-Ik`_(Cdr1KI-z~}jis7b$F~aVY(G6FS&H_=Srr(Sksne}%?-fxp zin8COv*Sf%UMiY_jXUra`uydl^20+mjeW zH;ra%4lwYh**gdcEBl+O4YX$v!yWhbx#A3OVJAV zrxUpleNXo_rVpMwy+I}4U;{I-Xzd;|^nSOz&?EEf2ZCka=21($%2?q3Ou6Gx1Pk0E z0H;698m%&EUx;rpXN%K9e`pn$t`=RnIseOwXYW$FBsc)#W`2=GRIJA@^a*VKkkNk) z>H$-Fyp(4x<42>atUL>35Z)+W;VsP{!xRMJu*{e5lS6BkrYV zG%eo>$1b&GXWg|WpG?W$l|o-EPB;?|)0)4P+gDw+hzefCf!ZN%53G{J8xT0)v#bG0 zbC5@wAwbqM*wvJ-nG-2V<)!=6qwtM#cinN#pTXOJF{xzEa$*L96ys1b1n(>rKI-U2 zFVr1Tk{#YX|IuLqz{7>6PvAfS($mj9iRw_y%bCvlJGfPdVx>}_8F#llG`Y3D*SFe5 z5Rj>a9XLIq-1m#C7QS}~K~dU@94OB(PT42yg=TypEh_=E1SU9CSipf06wG?7@BQNB z^t=ma4yfKzqVialuf}OmJrhDEMSBTXz_ndR5=$iS-Mkgf((jI$sHJiU9TFXw{Rx5t}X>gNDur#cFNVC<^hA$@rz@Uuw`d1Tsp2!Mi&@Wyb)7|EZFB5T-(Zo6E7>HO@h zU7pdrO;&|8ks366QRHn5+CAF!NBoVe(y$f28abGOOo$V!3h0HiGI1{eb6+5<9O+N5 zb_|2_im#s1QZ|zztM6@G{2UV%>?~6XWdi{HeT@ zrrS28C5>yILUrt>u#hC9SkC30#7wd9`G-T{74ftm-#bSlyXh^IUEa;@gUthH!%@5H-7!qUotAvDBX@HS+4BnS8%ODh{$~RQjfz* zjKW;s-Lq2XWvsRNU&xlV9s@2SF`wD-;PN-kG{F61d_cZXfIxc6_jn*ZFIgS*Q{j8O z2_7d6+Tdv+bMBus!MOLmsINeApKnYm2v{@}5c%R!4pG_gWGEvA8F?NeQo}Pn*SYF8 zIl3Mi{p@JJrC&Y00hC&Wfhj4pzw#e0h1=l*GCV}aM9svg&DXP!^VjjLXizkWI^t%2 zq{sCl0mE7RavOtj(qdl*@oQ9{V1SkKHnY``;*jo!Xs|}ms;HiXiUL{4SdiWV3qBde zEIIThh-+7&1YhfAP80W$_vW#@!f{LjU^Uq1VGMYV(a1<|?~kBl?IOK-$)m-BM#1xP zPXu`#@jDa!Jw=MoMwAWq0}hI2+tMrQkB5xvZH>1s{8~oMSkl`9MJdv#SDk5ZZt7<} z#A~stN@NYZsg+b17z90_Kgu4$b0rV)H`umHh$ZGmB2F76 z^ia@kP8R=N0ZQX8;>4ee=h@MvPqn!(-Pg8^6UeolZYI)P0$I&I(eI*n=dV)sAE+y^ z4PH@{!}B8_C#^bV9kiwL6Bd26OrxSh)j0*pq_ex0 zN5hTi`R6y}q(M0U1*=ue#m$i5We?t#+ z$Nr*XkwW-^VGt+8jj$5VxQ0-e^7mATU8pYV3W%3$%uDrMCm&FuoC?6`W06M?GDF~|c_uCS`v#&aB0qOq+aaz%N+@)g z^6mbHRA`k-i%6*-@2lL5{VisC_3QNB8faha)#ra{jd`{EFtac--acz5RNxi$T5s3ONoQ%{Z**n zV1|R{y-OiC!KYV^M`D3t}(o zAnYR9caB%q;Z6+4j{_;RD2qtEL1Pd3H4gx2#2yoQ0DA$KyA=L`V@hh^%&@YOV8g5R zaca}J&J-wf1DT<9xab5E%pWxO5)`n2pYFSTB?BHSRcS+vQEAsn(5s@HHo=7 z9dvpLRNug{vamB3%M5Mkcq#v%Nnf}u<81EorqS}VeA$+3JIsYX6rdB%MXwVAti$tbY{d;bTrfKi}mk57eakA7Kh1B zfq_>3@Q(DFVKHrhmfDZ*Kpj`t)>gBw1<7H+L8AX^0mw8?WA2^nvCu$?zc68dWcf%u zqLX2y{}W#SuQ34cGyt`L1%AE+$mg=UGndbtVK1CAw}Jcw@lHyQ&2xL(pOptsx}XPP z5~((GqyGTc2SK?6&~+!6CA;oQDbA0g8bm^)&fRkY9Ma)+l>^E~t?VW+&2wycQ-49C zJ|h_BFUICpD}dZK*uoac^5K>gJFTMW>=;GSO$?HC3b`GJqeR{oDEqzzKO z;0F$A%}0|@F{&oj{g-YAvGRBzE=qb3l<8RLlzzAzw}W^IH9z=o=dM$OU{ZU^k#y6a zUwCKwqIwU2X!I`7yLJp+JQG|0f&99JmIPz7*MALejN!~W<)7WQu?$GOC{+ME=!zhO z@aLFjq8Vh%_PoSgt3yAKwfRC1fTIp(R2hK56$}WOb)wigu84D}#_$6&C^A5l`2n<- zNH&R{$H4fyJ;{yg9!1rHVzd*u$;lV;%Bd9g%#wj_%{GPuoQvwXOqQ_