diff --git a/operator/src/test/java/org/keycloak/operator/testsuite/integration/BaseOperatorTest.java b/operator/src/test/java/org/keycloak/operator/testsuite/integration/BaseOperatorTest.java index 8e302f59bc5..78fa8dc9c9b 100644 --- a/operator/src/test/java/org/keycloak/operator/testsuite/integration/BaseOperatorTest.java +++ b/operator/src/test/java/org/keycloak/operator/testsuite/integration/BaseOperatorTest.java @@ -41,6 +41,7 @@ import io.fabric8.kubernetes.client.utils.Serialization; import io.fabric8.openshift.api.model.monitoring.v1.ServiceMonitor; import io.javaoperatorsdk.operator.Operator; import io.javaoperatorsdk.operator.api.config.BaseConfigurationService; +import io.javaoperatorsdk.operator.api.config.InformerStoppedHandler; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; import io.quarkiverse.operatorsdk.runtime.QuarkusConfigurationService; import io.quarkus.logging.Log; @@ -96,6 +97,7 @@ import jakarta.enterprise.inject.spi.CDI; import jakarta.enterprise.util.TypeLiteral; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.keycloak.operator.Utils.isOpenShift; import static org.keycloak.operator.testsuite.utils.K8sUtils.getResourceFromFile; @@ -113,7 +115,36 @@ public class BaseOperatorTest implements QuarkusTestAfterEachCallback { public static final String TEST_RESULTS_DIR = "target/operator-test-results/"; public static final String POD_LOGS_DIR = TEST_RESULTS_DIR + "pod-logs/"; - public enum OperatorDeployment {local_apiserver,local,remote} + private static final class BaseOperatorTestConfigurationService extends BaseConfigurationService { + + private final KubernetesClient client; + private boolean stopped; + + private BaseOperatorTestConfigurationService(KubernetesClient client) { + this.client = client; + } + + @Override + public KubernetesClient getKubernetesClient() { + return client; + } + + @Override + public Optional getInformerStoppedHandler() { + return Optional.of((informer, ex) -> { + if (ex != null && informer.hasSynced()) { + if (!stopped) { + log.error("Fatal error in informer: {}.", informer, ex); + stopped = true; + } + } else { + super.getInformerStoppedHandler().ifPresent(handler -> handler.onStop(informer, ex)); + } + }); + } +} + +public enum OperatorDeployment {local_apiserver,local,remote} protected static OperatorDeployment operatorDeployment; protected static QuarkusConfigurationService configuration; @@ -123,6 +154,7 @@ public class BaseOperatorTest implements QuarkusTestAfterEachCallback { protected static String kubernetesIp; protected static String customImage; private static Operator operator; + private static BaseOperatorTestConfigurationService config; protected static boolean isOpenShift; private static ApiServerHelper kubeApi; @@ -143,8 +175,7 @@ public class BaseOperatorTest implements QuarkusTestAfterEachCallback { kubeApi = new ApiServerHelper(); } - createK8sClient(); - createNamespace(); + createK8sClientForRandomNamespace(); kubernetesIp = ConfigProvider.getConfig().getOptionalValue(OPERATOR_KUBERNETES_IP, String.class).orElseGet(() -> { try { return new URL(k8sclient.getConfiguration().getMasterUrl()).getHost(); @@ -180,13 +211,16 @@ public class BaseOperatorTest implements QuarkusTestAfterEachCallback { + "------- Mode: " + operatorDeployment.name()); } - private static void createK8sClient() { + private static void createK8sClientForRandomNamespace() { namespace = getNewRandomNamespaceName(); + Log.infof("Creating new K8s Client for namespace %s", namespace); if (operatorDeployment == OperatorDeployment.local_apiserver) { k8sclient = kubeApi.createClient(namespace); } else { k8sclient = new KubernetesClientBuilder().withConfig(new ConfigBuilder(Config.autoConfigure(null)).withNamespace(namespace).build()).build(); } + Log.info("Creating Namespace " + namespace); + k8sclient.resource(new NamespaceBuilder().withNewMetadata().addToLabels("app","keycloak-test").withName(namespace).endMetadata().build()).create(); } private static void createRBACresourcesAndOperatorDeployment() throws FileNotFoundException { @@ -223,12 +257,8 @@ public class BaseOperatorTest implements QuarkusTestAfterEachCallback { private static void createOperator() { // create the operator to use the current client / namespace and injected dependent resources // to be replaced later with full cdi construction or test mechanics from quarkus operator sdk - operator = new Operator(new BaseConfigurationService() { - @Override - public KubernetesClient getKubernetesClient() { - return k8sclient; - } - }); + config = new BaseOperatorTestConfigurationService(k8sclient); + operator = new Operator(config); Log.info("Registering reconcilers for operator : " + operator + " [" + operatorDeployment + "]"); Instance> reconcilers = CDI.current().select(new TypeLiteral<>() {}); @@ -240,11 +270,6 @@ public class BaseOperatorTest implements QuarkusTestAfterEachCallback { operator.start(); } - private static void createNamespace() { - Log.info("Creating Namespace " + namespace); - k8sclient.resource(new NamespaceBuilder().withNewMetadata().addToLabels("app","keycloak-test").withName(namespace).endMetadata().build()).create(); - } - public static String getNewRandomNamespaceName() { return "keycloak-test-" + UUID.randomUUID(); } @@ -305,9 +330,11 @@ public class BaseOperatorTest implements QuarkusTestAfterEachCallback { if (operatorDeployment == OperatorDeployment.local_apiserver) { // by default garbage collection is not supported by envtest // so might as well do a namespace per test - k8sclient.namespaces().withName(namespace).delete(); + String oldNamespace = namespace; stopOperator(); - createNamespace(); + // create a new client bc operator has closed the old one + createK8sClientForRandomNamespace(); + k8sclient.namespaces().withName(oldNamespace).delete(); createOperator(); deployDBSecret(); return; @@ -515,16 +542,14 @@ public class BaseOperatorTest implements QuarkusTestAfterEachCallback { private static void stopOperator() { Log.info("Stopping Operator"); + assertFalse(config.stopped, "An informer unexpected stopped, check log for an ERROR"); + config.stopped = true; operator.stop(); // Avoid issues with Event Informers between tests Log.info("Removing Controllers and application scoped DRs from CDI"); Stream.of(KeycloakController.class, KeycloakRealmImportController.class, KeycloakUpdateJobDependentResource.class) .forEach(c -> CDI.current().destroy(CDI.current().select(c).get())); - - Log.info("Creating new K8s Client"); - // create a new client bc operator has closed the old one - createK8sClient(); } public static String getCurrentNamespace() {