diff --git a/AGENTS.md b/AGENTS.md index c1b0494034..04775c1050 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 5 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,11 @@ 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, 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/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/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..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 @@ -22,7 +22,9 @@ 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; @@ -44,10 +46,12 @@ """) @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. @@ -59,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; @@ -104,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) { @@ -115,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/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..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 @@ -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()); @@ -77,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, diff --git a/pom.xml b/pom.xml index de5bf92895..aa22eddf28 100644 --- a/pom.xml +++ b/pom.xml @@ -461,8 +461,17 @@ integration-tests + + 5 + + org.apache.maven.plugins maven-surefire-plugin @@ -474,6 +483,11 @@ **/*Test.java **/*E2E.java + ${it.fork.count} + true + + false +