From 89addeaea06624bf16863c97a2cde1d8b06d1474 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Thu, 5 Nov 2026 13:34:47 -0800 Subject: [PATCH 1/9] ci: run integration tests in parallel forks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Run integration test classes in it.fork.count (default 3) forked JVMs in the integration-tests profile. CRDs are no longer deleted after each class since test classes share them. Tests tagged SequentialIT (ShardSelectorIT, which rewrites the shared kubeconfig) run afterwards in a separate execution. OwnerRefMultiVersionIT now restores its mutated CRD itself. Signed-off-by: Attila Mészáros --- AGENTS.md | 11 +++++- .../OwnerRefMultiVersionIT.java | 10 ++++++ .../shardselector/ShardSelectorIT.java | 3 ++ pom.xml | 36 +++++++++++++++++++ 4 files changed, 59 insertions(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index c1b0494034..01636b9a05 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -72,9 +72,12 @@ java-operator-sdk/ # Run unit tests only ./mvnw test -# Run integration tests +# Run integration tests (test classes run in parallel in 3 forked JVMs) ./mvnw verify -Pintegration-tests +# Run integration tests with a different parallelism (1 = sequential) +./mvnw verify -Pintegration-tests -Dit.fork.count=1 + # Run specific test class ./mvnw test -Dtest=ClassName @@ -125,6 +128,12 @@ java-operator-sdk/ - Test against real Kubernetes API (typically via test cluster like minikube or kind) - Suffix with `IT` (e.g., `MyReconcilerIT`) - Located in `src/test/java` +- Integration test classes run in parallel against the same cluster, so they must not depend on + cluster-wide state: use the namespace provided by the extension, unique names for cluster-scoped + resources, and do not rely on a CRD being absent or deleted after the test (CRDs are not deleted + in the `integration-tests` profile, since test classes share them). A test that cannot run next + to other tests (e.g. it modifies the kubeconfig) is annotated with `@Tag("SequentialIT")` and is + executed alone, after the parallel tests. ### Test Resources diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/ownerreferencemultiversion/OwnerRefMultiVersionIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/ownerreferencemultiversion/OwnerRefMultiVersionIT.java index 7be254ad13..00b70a1361 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/ownerreferencemultiversion/OwnerRefMultiVersionIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/ownerreferencemultiversion/OwnerRefMultiVersionIT.java @@ -16,7 +16,10 @@ package io.javaoperatorsdk.operator.baseapi.ownerreferencemultiversion; import java.time.Duration; +import java.util.Objects; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.slf4j.Logger; @@ -70,6 +73,13 @@ class OwnerRefMultiVersionIT { }) .build(); + @AfterEach + void deleteModifiedCrd() { + var crdClient = operator.getKubernetesClient().apiextensions().v1().customResourceDefinitions(); + crdClient.withName(CRD_NAME).delete(); + crdClient.withName(CRD_NAME).waitUntilCondition(Objects::isNull, 60, TimeUnit.SECONDS); + } + @Test void mapperWorksAcrossVersionChange() { var reconciler = operator.getReconcilerOfType(OwnerRefMultiVersionReconciler.class); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/shardselector/ShardSelectorIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/shardselector/ShardSelectorIT.java index 35d9f9fc2c..f36710de76 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/shardselector/ShardSelectorIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/shardselector/ShardSelectorIT.java @@ -19,6 +19,7 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import io.fabric8.kubeapitest.junit.EnableKubeAPIServer; @@ -42,6 +43,8 @@ given custom resource is reconciled by exactly one instance, never by both and never by \ neither. Sharding relies on the Kubernetes API server 'ShardedListAndWatch' alpha feature. """) +// rewrites the shared kubeconfig while running, so no other test may build a client concurrently +@Tag("SequentialIT") @EnableKubeAPIServer( kubeAPIVersion = "1.36.*", apiServerFlags = {"--feature-gates=ShardedListAndWatch=true"}, diff --git a/pom.xml b/pom.xml index de5bf92895..8f465038c9 100644 --- a/pom.xml +++ b/pom.xml @@ -461,8 +461,19 @@ integration-tests + + 3 + + org.apache.maven.plugins maven-surefire-plugin @@ -474,7 +485,32 @@ **/*Test.java **/*E2E.java + false + + + default-test + + ${it.fork.count} + true + WatchPermissionAwareTest,SequentialIT + + false + + + + + sequential-integration-tests + + test + + test + + 1 + SequentialIT + + + From deb6ddf5104723b1186d3b7f2f9823a5d85d53f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Thu, 5 Nov 2026 13:50:56 -0800 Subject: [PATCH 2/9] fix: address review comments on parallel integration tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Give CRDs with clashing short names unique ones, since CRDs are no longer deleted between test classes. Wait for the dependent CRD deletion in CRDPresentActivationConditionIT before creating the primary. Limit failIfNoSpecifiedTests=false to the sequential execution so mistyped test selectors still fail. Signed-off-by: Attila Mészáros --- .../CleanerForReconcilerCustomResource.java | 2 +- .../CleanerForManagedDependentCustomResource.java | 2 +- ...DependentAnnotationSecondaryMapperResource.java | 2 +- .../CRDPresentActivationConditionIT.java | 14 +++++++++----- pom.xml | 2 +- 5 files changed, 13 insertions(+), 9 deletions(-) diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/cleanerforreconciler/CleanerForReconcilerCustomResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/cleanerforreconciler/CleanerForReconcilerCustomResource.java index 5d204275a7..cdf7dbb56a 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/cleanerforreconciler/CleanerForReconcilerCustomResource.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/cleanerforreconciler/CleanerForReconcilerCustomResource.java @@ -25,6 +25,6 @@ @Group("sample.javaoperatorsdk") @Version("v1") @Kind("CleanerForReconcilerCustomResource") -@ShortNames("cfr") +@ShortNames("cfrc") public class CleanerForReconcilerCustomResource extends CustomResource implements Namespaced {} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/cleanermanageddependent/CleanerForManagedDependentCustomResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/cleanermanageddependent/CleanerForManagedDependentCustomResource.java index a545446508..4e3572341a 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/cleanermanageddependent/CleanerForManagedDependentCustomResource.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/cleanermanageddependent/CleanerForManagedDependentCustomResource.java @@ -25,6 +25,6 @@ @Group("sample.javaoperatorsdk") @Version("v1") @Kind("CleanerForReconcilerCustomResource") -@ShortNames("cfr") +@ShortNames("cfmd") public class CleanerForManagedDependentCustomResource extends CustomResource implements Namespaced {} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/dependentannotationsecondarymapper/DependentAnnotationSecondaryMapperResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/dependentannotationsecondarymapper/DependentAnnotationSecondaryMapperResource.java index 520d42ed1a..062bceeb4b 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/dependentannotationsecondarymapper/DependentAnnotationSecondaryMapperResource.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/dependentannotationsecondarymapper/DependentAnnotationSecondaryMapperResource.java @@ -25,6 +25,6 @@ @Group("sample.javaoperatorsdk") @Version("v1") @Kind("MaxIntervalTestCustomResource") -@ShortNames("mit") +@ShortNames("dasm") public class DependentAnnotationSecondaryMapperResource extends CustomResource implements Namespaced {} 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 5f447a7249..2379dd3591 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 @@ -17,6 +17,8 @@ import java.time.Duration; import java.util.Map; +import java.util.Objects; +import java.util.concurrent.TimeUnit; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; @@ -53,11 +55,13 @@ public class CRDPresentActivationConditionIT { @Test void resourceCreatedOnlyIfCRDPresent() { // deleted so test can be repeated - extension - .getKubernetesClient() - .resources(CustomResourceDefinition.class) - .withName(CRD_NAME) - .delete(); + var crdResource = + extension + .getKubernetesClient() + .resources(CustomResourceDefinition.class) + .withName(CRD_NAME); + crdResource.delete(); + crdResource.waitUntilCondition(Objects::isNull, 60, TimeUnit.SECONDS); var resource = extension.create(testResource()); diff --git a/pom.xml b/pom.xml index 8f465038c9..a78c640f3b 100644 --- a/pom.xml +++ b/pom.xml @@ -485,7 +485,6 @@ **/*Test.java **/*E2E.java - false @@ -508,6 +507,7 @@ 1 SequentialIT + false From 3b3e3a1cd15029ab4ff291c316689e6a170dd09f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Thu, 5 Nov 2026 14:59:08 -0800 Subject: [PATCH 3/9] fix: make CRDPresentActivationConditionIT independent of reconcile timing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wait for the first reconciliation before applying the dependent CRD, so the cached 'CRD absent' result is guaranteed to exist. Under parallel load the primary's first reconcile could run after the CRD was applied, activating the dependent. Signed-off-by: Attila Mészáros --- .../CRDPresentActivationConditionIT.java | 10 ++++++++++ .../CRDPresentActivationReconciler.java | 10 +++++++++- 2 files changed, 19 insertions(+), 1 deletion(-) 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 2379dd3591..ced06d6412 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 @@ -81,6 +81,16 @@ void resourceCreatedOnlyIfCRDPresent() { assertThat(dr).isNull(); }); + // the workflow, thus the CRD presence check, runs before the reconcile method + await() + .untilAsserted( + () -> + assertThat( + extension + .getReconcilerOfType(CRDPresentActivationReconciler.class) + .getNumberOfExecutions()) + .isPositive()); + LocallyRunOperatorExtension.applyCrd( CRDPresentActivationDependentCustomResource.class, extension.getKubernetesClient()); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/crdpresentactivation/CRDPresentActivationReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/crdpresentactivation/CRDPresentActivationReconciler.java index 0cc773097a..fb107cd645 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/crdpresentactivation/CRDPresentActivationReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/crdpresentactivation/CRDPresentActivationReconciler.java @@ -15,6 +15,8 @@ */ package io.javaoperatorsdk.operator.workflow.crdpresentactivation; +import java.util.concurrent.atomic.AtomicInteger; + import io.javaoperatorsdk.operator.api.reconciler.*; import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent; import io.javaoperatorsdk.operator.processing.dependent.workflow.CRDPresentActivationCondition; @@ -31,14 +33,20 @@ public class CRDPresentActivationReconciler implements Reconciler, Cleaner { + private final AtomicInteger numberOfExecutions = new AtomicInteger(0); + @Override public UpdateControl reconcile( CRDPresentActivationCustomResource resource, Context context) { - + numberOfExecutions.incrementAndGet(); return UpdateControl.noUpdate(); } + public int getNumberOfExecutions() { + return numberOfExecutions.get(); + } + @Override public DeleteControl cleanup( CRDPresentActivationCustomResource resource, From dca01c30a73012f619ff914294b69acf62127209 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Thu, 5 Nov 2026 15:24:37 -0800 Subject: [PATCH 4/9] refactor: use a constant for the SequentialIT tag MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- AGENTS.md | 4 +-- .../operator/IntegrationTestTags.java | 28 +++++++++++++++++++ .../shardselector/ShardSelectorIT.java | 3 +- pom.xml | 3 +- 4 files changed, 34 insertions(+), 4 deletions(-) create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/IntegrationTestTags.java diff --git a/AGENTS.md b/AGENTS.md index 01636b9a05..e710f8efa8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -132,8 +132,8 @@ java-operator-sdk/ cluster-wide state: use the namespace provided by the extension, unique names for cluster-scoped resources, and do not rely on a CRD being absent or deleted after the test (CRDs are not deleted in the `integration-tests` profile, since test classes share them). A test that cannot run next - to other tests (e.g. it modifies the kubeconfig) is annotated with `@Tag("SequentialIT")` and is - executed alone, after the parallel tests. + to other tests (e.g. it modifies the kubeconfig) is annotated with + `@Tag(IntegrationTestTags.SEQUENTIAL_IT)` and is executed alone, after the parallel tests. ### Test Resources diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/IntegrationTestTags.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/IntegrationTestTags.java new file mode 100644 index 0000000000..7508486317 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/IntegrationTestTags.java @@ -0,0 +1,28 @@ +/* + * Copyright Java Operator SDK Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.javaoperatorsdk.operator; + +public final class IntegrationTestTags { + + /** + * Marks integration tests that cannot run concurrently with other integration tests. Such tests + * are excluded from the parallel execution and run on their own afterwards. The same value is + * used in the {@code integration-tests} profile in the root pom.xml. + */ + public static final String SEQUENTIAL_IT = "SequentialIT"; + + private IntegrationTestTags() {} +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/shardselector/ShardSelectorIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/shardselector/ShardSelectorIT.java index f36710de76..35f7f9a8d8 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/shardselector/ShardSelectorIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/shardselector/ShardSelectorIT.java @@ -30,6 +30,7 @@ import io.javaoperatorsdk.operator.Operator; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; +import static io.javaoperatorsdk.operator.IntegrationTestTags.SEQUENTIAL_IT; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; @@ -44,7 +45,7 @@ neither. Sharding relies on the Kubernetes API server 'ShardedListAndWatch' alpha feature. """) // rewrites the shared kubeconfig while running, so no other test may build a client concurrently -@Tag("SequentialIT") +@Tag(SEQUENTIAL_IT) @EnableKubeAPIServer( kubeAPIVersion = "1.36.*", apiServerFlags = {"--feature-gates=ShardedListAndWatch=true"}, diff --git a/pom.xml b/pom.xml index a78c640f3b..4ee2be324c 100644 --- a/pom.xml +++ b/pom.xml @@ -470,7 +470,8 @@ Integration tests run in parallel: each test class gets its own namespace, and test classes are spread over it.fork.count JVMs (e.g. -Dit.fork.count=1 to run sequentially). Tests that cannot share the cluster with other tests are tagged with - @Tag("SequentialIT") and run on their own, after the parallel execution. + @Tag(IntegrationTestTags.SEQUENTIAL_IT) and run on their own, after the parallel + execution. CRDs are not deleted after a test class (testsuite.deleteCRDs=false) since multiple test classes share the same CRDs and one deleting it would break the others. --> From 60ad8cf986e03af118b4aef0a77449ba2447292b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Thu, 5 Nov 2026 15:42:05 -0800 Subject: [PATCH 5/9] refactor: do not modify the shared kubeconfig in ShardSelectorIT MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Inject the kube config of the test API server instead of updating the kubeconfig file, so the test is safe to run in parallel with other tests, including ones in other modules. This makes the SequentialIT tag and the separate sequential execution unnecessary, so they are removed. Signed-off-by: Attila Mészáros --- AGENTS.md | 7 ++-- .../operator/IntegrationTestTags.java | 28 ---------------- .../shardselector/ShardSelectorIT.java | 20 ++++++----- pom.xml | 33 +++---------------- 4 files changed, 20 insertions(+), 68 deletions(-) delete mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/IntegrationTestTags.java diff --git a/AGENTS.md b/AGENTS.md index e710f8efa8..2c468aa616 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -130,10 +130,9 @@ java-operator-sdk/ - Located in `src/test/java` - Integration test classes run in parallel against the same cluster, so they must not depend on cluster-wide state: use the namespace provided by the extension, unique names for cluster-scoped - resources, and do not rely on a CRD being absent or deleted after the test (CRDs are not deleted - in the `integration-tests` profile, since test classes share them). A test that cannot run next - to other tests (e.g. it modifies the kubeconfig) is annotated with - `@Tag(IntegrationTestTags.SEQUENTIAL_IT)` and is executed alone, after the parallel tests. + resources, do not modify shared files such as the kubeconfig, and do not rely on a CRD being + absent or deleted after the test (CRDs are not deleted in the `integration-tests` profile, since + test classes share them). Distinct CRDs must not declare the same short name. ### Test Resources diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/IntegrationTestTags.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/IntegrationTestTags.java deleted file mode 100644 index 7508486317..0000000000 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/IntegrationTestTags.java +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright Java Operator SDK Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.javaoperatorsdk.operator; - -public final class IntegrationTestTags { - - /** - * Marks integration tests that cannot run concurrently with other integration tests. Such tests - * are excluded from the parallel execution and run on their own afterwards. The same value is - * used in the {@code integration-tests} profile in the root pom.xml. - */ - public static final String SEQUENTIAL_IT = "SequentialIT"; - - private IntegrationTestTags() {} -} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/shardselector/ShardSelectorIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/shardselector/ShardSelectorIT.java index 35f7f9a8d8..b548678360 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/shardselector/ShardSelectorIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/shardselector/ShardSelectorIT.java @@ -19,18 +19,18 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import io.fabric8.kubeapitest.junit.EnableKubeAPIServer; +import io.fabric8.kubeapitest.junit.KubeConfig; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.fabric8.kubernetes.client.Config; 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; -import static io.javaoperatorsdk.operator.IntegrationTestTags.SEQUENTIAL_IT; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; @@ -44,14 +44,14 @@ given custom resource is reconciled by exactly one instance, never by both and never by \ neither. Sharding relies on the Kubernetes API server 'ShardedListAndWatch' alpha feature. """) -// rewrites the shared kubeconfig while running, so no other test may build a client concurrently -@Tag(SEQUENTIAL_IT) @EnableKubeAPIServer( kubeAPIVersion = "1.36.*", - apiServerFlags = {"--feature-gates=ShardedListAndWatch=true"}, - updateKubeConfigFile = true) + apiServerFlags = {"--feature-gates=ShardedListAndWatch=true"}) class ShardSelectorIT { + // injected by Kube API Test, the shared kubeconfig file is intentionally not modified + @KubeConfig static String kubeConfigYaml; + // The two selectors split the 64-bit UID hash space in half: [0x0, 0x8000000000000000) and // [0x8000000000000000, 0x10000000000000000). Together they cover the whole space with no overlap, // so every resource is owned by exactly one shard. @@ -63,7 +63,7 @@ class ShardSelectorIT { private static final String TEST_RESOURCE_NAME = "shard-test1"; private static final Duration EVENT_SETTLE_WINDOW = Duration.ofMillis(500); - private final KubernetesClient adminClient = new KubernetesClientBuilder().build(); + private final KubernetesClient adminClient = newClient(); private Operator operator1; private Operator operator2; @@ -108,7 +108,7 @@ void onlyOneShardReconcilesTheResource() { private ShardSelectorTestReconciler startOperatorForShard(String shardSelector) { var reconciler = new ShardSelectorTestReconciler(); - var operator = new Operator(o -> o.withKubernetesClient(new KubernetesClientBuilder().build())); + var operator = new Operator(o -> o.withKubernetesClient(newClient())); operator.register(reconciler, o -> o.withShardSelector(shardSelector)); operator.start(); if (operator1 == null) { @@ -119,6 +119,10 @@ private ShardSelectorTestReconciler startOperatorForShard(String shardSelector) return reconciler; } + private static KubernetesClient newClient() { + return new KubernetesClientBuilder().withConfig(Config.fromKubeconfig(kubeConfigYaml)).build(); + } + private ShardSelectorTestCustomResource testCustomResource() { var resource = new ShardSelectorTestCustomResource(); resource.setMetadata(new ObjectMetaBuilder().withName(TEST_RESOURCE_NAME).build()); diff --git a/pom.xml b/pom.xml index 4ee2be324c..ea077a629e 100644 --- a/pom.xml +++ b/pom.xml @@ -469,9 +469,6 @@ @@ -486,32 +483,12 @@ **/*Test.java **/*E2E.java + ${it.fork.count} + true + + false + - - - default-test - - ${it.fork.count} - true - WatchPermissionAwareTest,SequentialIT - - false - - - - - sequential-integration-tests - - test - - test - - 1 - SequentialIT - false - - - From d37e2f11de60f1aca3e8de3f01d08d67f9356ac5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Fri, 6 Nov 2026 08:47:40 -0800 Subject: [PATCH 6/9] wip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index ea077a629e..aa22eddf28 100644 --- a/pom.xml +++ b/pom.xml @@ -462,7 +462,7 @@ integration-tests - 3 + 5 From 8cb10faf7e29e512c4c626d3f5d8b2e884184156 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Fri, 6 Nov 2026 09:22:19 -0800 Subject: [PATCH 7/9] wip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index aa22eddf28..ea077a629e 100644 --- a/pom.xml +++ b/pom.xml @@ -462,7 +462,7 @@ integration-tests - 5 + 3 From 36cd6df2793186e673bf8a3967acefd52d8fe1b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Fri, 6 Nov 2026 09:55:14 -0800 Subject: [PATCH 8/9] wip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index ea077a629e..aa22eddf28 100644 --- a/pom.xml +++ b/pom.xml @@ -462,7 +462,7 @@ integration-tests - 3 + 5 From 1ff7a5f0cac4da9caab432bad5991df6424a125c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Fri, 6 Nov 2026 10:16:18 -0800 Subject: [PATCH 9/9] wip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- AGENTS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index 2c468aa616..04775c1050 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -72,7 +72,7 @@ java-operator-sdk/ # Run unit tests only ./mvnw test -# Run integration tests (test classes run in parallel in 3 forked JVMs) +# Run integration tests (test classes run in parallel in 5 forked JVMs) ./mvnw verify -Pintegration-tests # Run integration tests with a different parallelism (1 = sequential)