From 075d91303715c109711458ee3674f622e62b5359 Mon Sep 17 00:00:00 2001 From: Steven Hawkins Date: Wed, 7 Jun 2023 11:57:25 -0400 Subject: [PATCH] enabling keycloak to be a scalable resource (#20828) Closes #20825 --- .../java/org/keycloak/operator/Constants.java | 6 +- .../controllers/KeycloakController.java | 18 +++--- .../controllers/KeycloakDeployment.java | 27 ++++---- .../controllers/KeycloakDiscoveryService.java | 6 +- .../controllers/KeycloakDistConfigurator.java | 6 +- .../operator/controllers/KeycloakIngress.java | 6 +- .../operator/controllers/KeycloakService.java | 6 +- .../v2alpha1/deployment/KeycloakSpec.java | 2 + .../v2alpha1/deployment/KeycloakStatus.java | 33 +++++++++- ...der.java => KeycloakStatusAggregator.java} | 33 +++++++--- .../testsuite/integration/ClusteringTest.java | 61 ++++++++++++++----- .../unit/KeycloakDistConfiguratorTest.java | 4 +- .../testsuite/unit/KeycloakStatusTest.java | 37 +++++++++++ 13 files changed, 181 insertions(+), 64 deletions(-) rename operator/src/main/java/org/keycloak/operator/crds/v2alpha1/deployment/{KeycloakStatusBuilder.java => KeycloakStatusAggregator.java} (69%) create mode 100644 operator/src/test/java/org/keycloak/operator/testsuite/unit/KeycloakStatusTest.java diff --git a/operator/src/main/java/org/keycloak/operator/Constants.java b/operator/src/main/java/org/keycloak/operator/Constants.java index c8d38a899e..3db139b608 100644 --- a/operator/src/main/java/org/keycloak/operator/Constants.java +++ b/operator/src/main/java/org/keycloak/operator/Constants.java @@ -18,8 +18,10 @@ package org.keycloak.operator; import org.keycloak.operator.crds.v2alpha1.deployment.ValueOrSecret; +import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.TreeMap; import java.util.stream.Collectors; public final class Constants { @@ -33,10 +35,10 @@ public final class Constants { public static final String COMPONENT_LABEL = "app.kubernetes.io/component"; public static final String KEYCLOAK_COMPONENT_LABEL = "keycloak.org/component"; - public static final Map DEFAULT_LABELS = Map.of( + public static final Map DEFAULT_LABELS = Collections.unmodifiableMap(new TreeMap<>(Map.of( "app", NAME, MANAGED_BY_LABEL, MANAGED_BY_VALUE - ); + ))); public static final String DEFAULT_LABELS_AS_STRING = DEFAULT_LABELS.entrySet().stream() .map(e -> e.getKey() + "=" + e.getValue()) diff --git a/operator/src/main/java/org/keycloak/operator/controllers/KeycloakController.java b/operator/src/main/java/org/keycloak/operator/controllers/KeycloakController.java index c11920ea84..c3bdf08df2 100644 --- a/operator/src/main/java/org/keycloak/operator/controllers/KeycloakController.java +++ b/operator/src/main/java/org/keycloak/operator/controllers/KeycloakController.java @@ -37,7 +37,7 @@ import org.keycloak.operator.Config; import org.keycloak.operator.Constants; import org.keycloak.operator.crds.v2alpha1.deployment.Keycloak; import org.keycloak.operator.crds.v2alpha1.deployment.KeycloakStatus; -import org.keycloak.operator.crds.v2alpha1.deployment.KeycloakStatusBuilder; +import org.keycloak.operator.crds.v2alpha1.deployment.KeycloakStatusAggregator; import org.keycloak.operator.crds.v2alpha1.deployment.KeycloakStatusCondition; import jakarta.inject.Inject; @@ -92,13 +92,13 @@ public class KeycloakController implements Reconciler, EventSourceInit } @Override - public UpdateControl reconcile(Keycloak kc, Context context) { + public UpdateControl reconcile(Keycloak kc, Context context) { String kcName = kc.getMetadata().getName(); String namespace = kc.getMetadata().getNamespace(); Log.infof("--- Reconciling Keycloak: %s in namespace: %s", kcName, namespace); - var statusBuilder = new KeycloakStatusBuilder(); + var statusAggregator = new KeycloakStatusAggregator(); var kcAdminSecret = new KeycloakAdminSecret(client, kc); kcAdminSecret.createOrUpdateReconciled(); @@ -112,21 +112,21 @@ public class KeycloakController implements Reconciler, EventSourceInit Log.info("Config Secrets modified, restarting deployment"); kcDeployment.rollingRestart(); } - kcDeployment.updateStatus(statusBuilder); + kcDeployment.updateStatus(statusAggregator); watchedSecrets.createOrUpdateReconciled(); var kcService = new KeycloakService(client, kc); - kcService.updateStatus(statusBuilder); + kcService.updateStatus(statusAggregator); kcService.createOrUpdateReconciled(); var kcDiscoveryService = new KeycloakDiscoveryService(client, kc); - kcDiscoveryService.updateStatus(statusBuilder); + kcDiscoveryService.updateStatus(statusAggregator); kcDiscoveryService.createOrUpdateReconciled(); var kcIngress = new KeycloakIngress(client, kc); - kcIngress.updateStatus(statusBuilder); + kcIngress.updateStatus(statusAggregator); kcIngress.createOrUpdateReconciled(); - var status = statusBuilder.build(); + var status = statusAggregator.build(); Log.info("--- Reconciliation finished successfully"); @@ -152,7 +152,7 @@ public class KeycloakController implements Reconciler, EventSourceInit @Override public ErrorStatusUpdateControl updateErrorStatus(Keycloak kc, Context context, Exception e) { Log.error("--- Error reconciling", e); - KeycloakStatus status = new KeycloakStatusBuilder() + KeycloakStatus status = new KeycloakStatusAggregator() .addErrorMessage("Error performing operations:\n" + e.getMessage()) .build(); diff --git a/operator/src/main/java/org/keycloak/operator/controllers/KeycloakDeployment.java b/operator/src/main/java/org/keycloak/operator/controllers/KeycloakDeployment.java index 80a748bd50..d527008ee0 100644 --- a/operator/src/main/java/org/keycloak/operator/controllers/KeycloakDeployment.java +++ b/operator/src/main/java/org/keycloak/operator/controllers/KeycloakDeployment.java @@ -33,7 +33,7 @@ import org.keycloak.common.util.CollectionUtil; import org.keycloak.operator.Config; import org.keycloak.operator.Constants; import org.keycloak.operator.crds.v2alpha1.deployment.Keycloak; -import org.keycloak.operator.crds.v2alpha1.deployment.KeycloakStatusBuilder; +import org.keycloak.operator.crds.v2alpha1.deployment.KeycloakStatusAggregator; import org.keycloak.operator.crds.v2alpha1.deployment.ValueOrSecret; import java.nio.charset.StandardCharsets; @@ -41,6 +41,7 @@ import java.util.ArrayList; import java.util.Base64; import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Optional; @@ -50,7 +51,7 @@ import java.util.stream.Collectors; import static org.keycloak.operator.crds.v2alpha1.CRDUtils.isTlsConfigured; -public class KeycloakDeployment extends OperatorManagedResource implements StatusUpdater { +public class KeycloakDeployment extends OperatorManagedResource implements StatusUpdater { private final Config operatorConfig; private final KeycloakDistConfigurator distConfigurator; @@ -122,7 +123,7 @@ public class KeycloakDeployment extends OperatorManagedResource implements Statu .get(); } - public void validatePodTemplate(KeycloakStatusBuilder status) { + public void validatePodTemplate(KeycloakStatusAggregator status) { if (keycloakCR.getSpec() == null || keycloakCR.getSpec().getUnsupported() == null || keycloakCR.getSpec().getUnsupported().getPodTemplate() == null) { @@ -379,7 +380,7 @@ public class KeycloakDeployment extends OperatorManagedResource implements Statu baseDeployment.getSpec().getSelector().setMatchLabels(Constants.DEFAULT_LABELS); baseDeployment.getSpec().setReplicas(keycloakCR.getSpec().getInstances()); - Map labels = new HashMap<>(Constants.DEFAULT_LABELS); + Map labels = new LinkedHashMap<>(Constants.DEFAULT_LABELS); if (operatorConfig.keycloak().podLabels() != null) { labels.putAll(operatorConfig.keycloak().podLabels()); } @@ -491,20 +492,24 @@ public class KeycloakDeployment extends OperatorManagedResource implements Statu return envVars; } - - public void updateStatus(KeycloakStatusBuilder status) { + + public void updateStatus(KeycloakStatusAggregator status) { + status.apply(b -> b.withSelector(Constants.DEFAULT_LABELS_AS_STRING)); validatePodTemplate(status); if (existingDeployment == null) { status.addNotReadyMessage("No existing StatefulSet found, waiting for creating a new one"); return; } - if (existingDeployment.getStatus() == null - || existingDeployment.getStatus().getReadyReplicas() == null - || existingDeployment.getStatus().getReadyReplicas() < keycloakCR.getSpec().getInstances()) { - status.addNotReadyMessage("Waiting for more replicas"); + if (existingDeployment.getStatus() == null) { + status.addNotReadyMessage("Waiting for deployment status"); + } else { + status.apply(b -> b.withInstances(existingDeployment.getStatus().getReadyReplicas())); + if (Optional.ofNullable(existingDeployment.getStatus().getReadyReplicas()).orElse(0) < keycloakCR.getSpec().getInstances()) { + status.addNotReadyMessage("Waiting for more replicas"); + } } - + if (migrationInProgress) { status.addNotReadyMessage("Performing Keycloak upgrade, scaling down the deployment"); } else if (existingDeployment.getStatus() != null diff --git a/operator/src/main/java/org/keycloak/operator/controllers/KeycloakDiscoveryService.java b/operator/src/main/java/org/keycloak/operator/controllers/KeycloakDiscoveryService.java index 7e7346760f..eca99804b2 100644 --- a/operator/src/main/java/org/keycloak/operator/controllers/KeycloakDiscoveryService.java +++ b/operator/src/main/java/org/keycloak/operator/controllers/KeycloakDiscoveryService.java @@ -24,11 +24,11 @@ import io.fabric8.kubernetes.api.model.ServiceSpecBuilder; import io.fabric8.kubernetes.client.KubernetesClient; import org.keycloak.operator.Constants; import org.keycloak.operator.crds.v2alpha1.deployment.Keycloak; -import org.keycloak.operator.crds.v2alpha1.deployment.KeycloakStatusBuilder; +import org.keycloak.operator.crds.v2alpha1.deployment.KeycloakStatusAggregator; import java.util.Optional; -public class KeycloakDiscoveryService extends OperatorManagedResource implements StatusUpdater { +public class KeycloakDiscoveryService extends OperatorManagedResource implements StatusUpdater { private Service existingService; @@ -78,7 +78,7 @@ public class KeycloakDiscoveryService extends OperatorManagedResource implements .get(); } - public void updateStatus(KeycloakStatusBuilder status) { + public void updateStatus(KeycloakStatusAggregator status) { if (existingService == null) { status.addNotReadyMessage("No existing Discovery Service found, waiting for creating a new one"); return; diff --git a/operator/src/main/java/org/keycloak/operator/controllers/KeycloakDistConfigurator.java b/operator/src/main/java/org/keycloak/operator/controllers/KeycloakDistConfigurator.java index 27e2ae6696..eea21e9030 100644 --- a/operator/src/main/java/org/keycloak/operator/controllers/KeycloakDistConfigurator.java +++ b/operator/src/main/java/org/keycloak/operator/controllers/KeycloakDistConfigurator.java @@ -29,7 +29,7 @@ import io.quarkus.logging.Log; import org.keycloak.common.util.CollectionUtil; import org.keycloak.operator.Constants; import org.keycloak.operator.crds.v2alpha1.deployment.Keycloak; -import org.keycloak.operator.crds.v2alpha1.deployment.KeycloakStatusBuilder; +import org.keycloak.operator.crds.v2alpha1.deployment.KeycloakStatusAggregator; import org.keycloak.operator.crds.v2alpha1.deployment.ValueOrSecret; import org.keycloak.operator.crds.v2alpha1.deployment.spec.DatabaseSpec; import org.keycloak.operator.crds.v2alpha1.deployment.spec.FeatureSpec; @@ -85,7 +85,7 @@ public class KeycloakDistConfigurator { * * @param status Keycloak Status builder */ - public void validateOptions(KeycloakStatusBuilder status) { + public void validateOptions(KeycloakStatusAggregator status) { assumeFirstClassCitizens(status); } @@ -175,7 +175,7 @@ public class KeycloakDistConfigurator { * * @param status Status of the deployment */ - protected void assumeFirstClassCitizens(KeycloakStatusBuilder status) { + protected void assumeFirstClassCitizens(KeycloakStatusAggregator status) { final var serverConfigNames = keycloakCR .getSpec() .getAdditionalOptions() diff --git a/operator/src/main/java/org/keycloak/operator/controllers/KeycloakIngress.java b/operator/src/main/java/org/keycloak/operator/controllers/KeycloakIngress.java index 6d8377ae5f..4aed1f58cc 100644 --- a/operator/src/main/java/org/keycloak/operator/controllers/KeycloakIngress.java +++ b/operator/src/main/java/org/keycloak/operator/controllers/KeycloakIngress.java @@ -23,14 +23,14 @@ import io.fabric8.kubernetes.api.model.networking.v1.Ingress; import org.keycloak.operator.Constants; import org.keycloak.operator.crds.v2alpha1.deployment.spec.IngressSpec; import org.keycloak.operator.crds.v2alpha1.deployment.Keycloak; -import org.keycloak.operator.crds.v2alpha1.deployment.KeycloakStatusBuilder; +import org.keycloak.operator.crds.v2alpha1.deployment.KeycloakStatusAggregator; import java.util.HashMap; import java.util.Optional; import static org.keycloak.operator.crds.v2alpha1.CRDUtils.isTlsConfigured; -public class KeycloakIngress extends OperatorManagedResource implements StatusUpdater { +public class KeycloakIngress extends OperatorManagedResource implements StatusUpdater { private final Ingress existingIngress; private final Keycloak keycloak; @@ -142,7 +142,7 @@ public class KeycloakIngress extends OperatorManagedResource implements StatusUp .get(); } - public void updateStatus(KeycloakStatusBuilder status) { + public void updateStatus(KeycloakStatusAggregator status) { IngressSpec ingressSpec = keycloak.getSpec().getIngressSpec(); if (ingressSpec == null) { ingressSpec = new IngressSpec(); diff --git a/operator/src/main/java/org/keycloak/operator/controllers/KeycloakService.java b/operator/src/main/java/org/keycloak/operator/controllers/KeycloakService.java index 3cf24e6b5f..7946c0e920 100644 --- a/operator/src/main/java/org/keycloak/operator/controllers/KeycloakService.java +++ b/operator/src/main/java/org/keycloak/operator/controllers/KeycloakService.java @@ -24,7 +24,7 @@ import io.fabric8.kubernetes.api.model.ServiceSpecBuilder; import io.fabric8.kubernetes.client.KubernetesClient; import org.keycloak.operator.Constants; import org.keycloak.operator.crds.v2alpha1.deployment.Keycloak; -import org.keycloak.operator.crds.v2alpha1.deployment.KeycloakStatusBuilder; +import org.keycloak.operator.crds.v2alpha1.deployment.KeycloakStatusAggregator; import org.keycloak.operator.crds.v2alpha1.deployment.spec.HttpSpec; import java.util.Optional; @@ -32,7 +32,7 @@ import java.util.Optional; import static org.keycloak.operator.crds.v2alpha1.CRDUtils.getValueFromSubSpec; import static org.keycloak.operator.crds.v2alpha1.CRDUtils.isTlsConfigured; -public class KeycloakService extends OperatorManagedResource implements StatusUpdater { +public class KeycloakService extends OperatorManagedResource implements StatusUpdater { private Service existingService; private final Keycloak keycloak; @@ -84,7 +84,7 @@ public class KeycloakService extends OperatorManagedResource implements StatusUp .get(); } - public void updateStatus(KeycloakStatusBuilder status) { + public void updateStatus(KeycloakStatusAggregator status) { if (existingService == null) { status.addNotReadyMessage("No existing Keycloak Service found, waiting for creating a new one"); return; diff --git a/operator/src/main/java/org/keycloak/operator/crds/v2alpha1/deployment/KeycloakSpec.java b/operator/src/main/java/org/keycloak/operator/crds/v2alpha1/deployment/KeycloakSpec.java index 4c7cd5c1bb..56f79134cb 100644 --- a/operator/src/main/java/org/keycloak/operator/crds/v2alpha1/deployment/KeycloakSpec.java +++ b/operator/src/main/java/org/keycloak/operator/crds/v2alpha1/deployment/KeycloakSpec.java @@ -21,6 +21,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonPropertyDescription; import io.fabric8.kubernetes.api.model.LocalObjectReference; +import io.fabric8.kubernetes.model.annotation.SpecReplicas; import org.keycloak.operator.crds.v2alpha1.deployment.spec.DatabaseSpec; import org.keycloak.operator.crds.v2alpha1.deployment.spec.FeatureSpec; @@ -36,6 +37,7 @@ import java.util.List; @JsonInclude(JsonInclude.Include.NON_NULL) public class KeycloakSpec { + @SpecReplicas @JsonPropertyDescription("Number of Keycloak instances in HA mode. Default is 1.") private int instances = 1; diff --git a/operator/src/main/java/org/keycloak/operator/crds/v2alpha1/deployment/KeycloakStatus.java b/operator/src/main/java/org/keycloak/operator/crds/v2alpha1/deployment/KeycloakStatus.java index b606d0726a..8d8e97ff5d 100644 --- a/operator/src/main/java/org/keycloak/operator/crds/v2alpha1/deployment/KeycloakStatus.java +++ b/operator/src/main/java/org/keycloak/operator/crds/v2alpha1/deployment/KeycloakStatus.java @@ -19,11 +19,38 @@ package org.keycloak.operator.crds.v2alpha1.deployment; import java.util.List; import java.util.Objects; +import io.fabric8.kubernetes.model.annotation.LabelSelector; +import io.fabric8.kubernetes.model.annotation.StatusReplicas; +import io.sundr.builder.annotations.Buildable; + /** * @author Vaclav Muzikar */ +@Buildable(editableEnabled = false, builderPackage = "io.fabric8.kubernetes.api.builder", lazyCollectionInitEnabled = false) public class KeycloakStatus { + + @LabelSelector + private String selector; + @StatusReplicas + private Integer instances; + private List conditions; + + public String getSelector() { + return selector; + } + + public void setSelector(String selector) { + this.selector = selector; + } + + public Integer getInstances() { + return instances; + } + + public void setInstances(Integer instances) { + this.instances = instances; + } public List getConditions() { return conditions; @@ -38,11 +65,13 @@ public class KeycloakStatus { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; KeycloakStatus status = (KeycloakStatus) o; - return Objects.equals(getConditions(), status.getConditions()); + return Objects.equals(getConditions(), status.getConditions()) + && Objects.equals(getInstances(), status.getInstances()) + && Objects.equals(getSelector(), status.getSelector()); } @Override public int hashCode() { - return Objects.hash(getConditions()); + return Objects.hash(getConditions(), getInstances(), getSelector()); } } diff --git a/operator/src/main/java/org/keycloak/operator/crds/v2alpha1/deployment/KeycloakStatusBuilder.java b/operator/src/main/java/org/keycloak/operator/crds/v2alpha1/deployment/KeycloakStatusAggregator.java similarity index 69% rename from operator/src/main/java/org/keycloak/operator/crds/v2alpha1/deployment/KeycloakStatusBuilder.java rename to operator/src/main/java/org/keycloak/operator/crds/v2alpha1/deployment/KeycloakStatusAggregator.java index 3599d80975..2b788c0d13 100644 --- a/operator/src/main/java/org/keycloak/operator/crds/v2alpha1/deployment/KeycloakStatusBuilder.java +++ b/operator/src/main/java/org/keycloak/operator/crds/v2alpha1/deployment/KeycloakStatusAggregator.java @@ -19,11 +19,12 @@ package org.keycloak.operator.crds.v2alpha1.deployment; import java.util.ArrayList; import java.util.List; +import java.util.function.Consumer; /** * @author Vaclav Muzikar */ -public class KeycloakStatusBuilder { +public class KeycloakStatusAggregator { private final KeycloakStatusCondition readyCondition; private final KeycloakStatusCondition hasErrorsCondition; private final KeycloakStatusCondition rollingUpdate; @@ -31,8 +32,10 @@ public class KeycloakStatusBuilder { private final List notReadyMessages = new ArrayList<>(); private final List errorMessages = new ArrayList<>(); private final List rollingUpdateMessages = new ArrayList<>(); + + private final KeycloakStatusBuilder statusBuilder = new KeycloakStatusBuilder(); - public KeycloakStatusBuilder() { + public KeycloakStatusAggregator() { readyCondition = new KeycloakStatusCondition(); readyCondition.setType(KeycloakStatusCondition.READY); readyCondition.setStatus(true); @@ -46,36 +49,46 @@ public class KeycloakStatusBuilder { rollingUpdate.setStatus(false); } - public KeycloakStatusBuilder addNotReadyMessage(String message) { + public KeycloakStatusAggregator addNotReadyMessage(String message) { readyCondition.setStatus(false); notReadyMessages.add(message); return this; } - public KeycloakStatusBuilder addErrorMessage(String message) { + public KeycloakStatusAggregator addErrorMessage(String message) { hasErrorsCondition.setStatus(true); errorMessages.add(message); return this; } - public KeycloakStatusBuilder addWarningMessage(String message) { + public KeycloakStatusAggregator addWarningMessage(String message) { errorMessages.add("warning: " + message); return this; } - public KeycloakStatusBuilder addRollingUpdateMessage(String message) { + public KeycloakStatusAggregator addRollingUpdateMessage(String message) { rollingUpdate.setStatus(true); rollingUpdateMessages.add(message); return this; } + + /** + * Apply non-condition changes to the status + */ + public KeycloakStatusAggregator apply(Consumer toApply) { + statusBuilder.withConditions(List.of()); + toApply.accept(statusBuilder); + if (!statusBuilder.getConditions().isEmpty()) { + throw new AssertionError("use addXXXMessage methods to modify conditions"); + } + return this; + } public KeycloakStatus build() { readyCondition.setMessage(String.join("\n", notReadyMessages)); hasErrorsCondition.setMessage(String.join("\n", errorMessages)); rollingUpdate.setMessage(String.join("\n", rollingUpdateMessages)); - - KeycloakStatus status = new KeycloakStatus(); - status.setConditions(List.of(readyCondition, hasErrorsCondition, rollingUpdate)); - return status; + + return statusBuilder.withConditions(List.of(readyCondition, hasErrorsCondition, rollingUpdate)).build(); } } diff --git a/operator/src/test/java/org/keycloak/operator/testsuite/integration/ClusteringTest.java b/operator/src/test/java/org/keycloak/operator/testsuite/integration/ClusteringTest.java index 970d0d1db8..8167df15d8 100644 --- a/operator/src/test/java/org/keycloak/operator/testsuite/integration/ClusteringTest.java +++ b/operator/src/test/java/org/keycloak/operator/testsuite/integration/ClusteringTest.java @@ -17,29 +17,31 @@ package org.keycloak.operator.testsuite.integration; -import com.fasterxml.jackson.databind.JsonNode; import io.fabric8.kubernetes.client.utils.Serialization; import io.quarkus.logging.Log; import io.quarkus.test.junit.QuarkusTest; import io.restassured.RestAssured; + import org.awaitility.Awaitility; import org.junit.jupiter.api.Test; import org.keycloak.operator.Constants; -import org.keycloak.operator.testsuite.utils.CRAssert; import org.keycloak.operator.controllers.KeycloakService; import org.keycloak.operator.crds.v2alpha1.deployment.Keycloak; -import org.keycloak.operator.testsuite.utils.K8sUtils; +import org.keycloak.operator.crds.v2alpha1.deployment.KeycloakStatusCondition; import org.keycloak.operator.crds.v2alpha1.realmimport.KeycloakRealmImport; import org.keycloak.operator.crds.v2alpha1.realmimport.KeycloakRealmImportStatusCondition; -import org.keycloak.operator.crds.v2alpha1.deployment.KeycloakStatusCondition; +import org.keycloak.operator.testsuite.utils.CRAssert; +import org.keycloak.operator.testsuite.utils.K8sUtils; import java.time.Duration; +import java.util.Optional; + +import com.fasterxml.jackson.databind.JsonNode; import static java.util.concurrent.TimeUnit.MINUTES; import static java.util.concurrent.TimeUnit.SECONDS; import static org.assertj.core.api.Assertions.assertThat; - @QuarkusTest public class ClusteringTest extends BaseOperatorTest { @@ -55,11 +57,29 @@ public class ClusteringTest extends BaseOperatorTest { var kcPodsSelector = k8sclient.pods().inNamespace(namespace).withLabel("app", "keycloak"); + var scale = crSelector.scale(); + assertThat(scale.getSpec().getReplicas()).isEqualTo(1); + assertThat(scale.getStatus().getReplicas()).isEqualTo(1); + assertThat(scale.getStatus().getSelector()).isEqualTo(Constants.DEFAULT_LABELS_AS_STRING); + + // when scale it to 0 + Keycloak scaled = crSelector.scale(0); + assertThat(scaled.getSpec().getInstances()).isEqualTo(0); + + Awaitility.await() + .atMost(Duration.ofSeconds(60)) + .ignoreExceptions() + .untilAsserted(() -> assertThat(Optional.ofNullable(crSelector.scale().getStatus().getReplicas()).orElse(0)).isEqualTo(0)); + + Awaitility.await() + .atMost(1, MINUTES) + .pollDelay(1, SECONDS) + .ignoreExceptions() + .untilAsserted(() -> CRAssert.assertKeycloakStatusCondition(crSelector.get(), KeycloakStatusCondition.READY, true)); + // when scale it to 3 - crSelector.accept(keycloak -> { - keycloak.getMetadata().setResourceVersion(null); - keycloak.getSpec().setInstances(3); - }); + crSelector.scale(3); + assertThat(crSelector.scale().getSpec().getReplicas()).isEqualTo(3); Awaitility.await() .atMost(1, MINUTES) @@ -68,15 +88,19 @@ public class ClusteringTest extends BaseOperatorTest { .untilAsserted(() -> CRAssert.assertKeycloakStatusCondition(crSelector.get(), KeycloakStatusCondition.READY, false)); Awaitility.await() - .atMost(Duration.ofSeconds(60)) + .atMost(Duration.ofSeconds(180)) .ignoreExceptions() .untilAsserted(() -> assertThat(kcPodsSelector.list().getItems().size()).isEqualTo(3)); + Awaitility.await() + .atMost(Duration.ofSeconds(60)) + .ignoreExceptions() + .untilAsserted(() -> assertThat(crSelector.scale().getStatus().getReplicas()).isEqualTo(3)); + // when scale it down to 2 - crSelector.accept(keycloak -> { - keycloak.getMetadata().setResourceVersion(null); - keycloak.getSpec().setInstances(2); - }); + crSelector.scale(2); + assertThat(crSelector.scale().getSpec().getReplicas()).isEqualTo(2); + Awaitility.await() .atMost(Duration.ofSeconds(180)) .ignoreExceptions() @@ -88,6 +112,11 @@ public class ClusteringTest extends BaseOperatorTest { .ignoreExceptions() .untilAsserted(() -> CRAssert.assertKeycloakStatusCondition(crSelector.get(), KeycloakStatusCondition.READY, true)); + Awaitility.await() + .atMost(Duration.ofSeconds(60)) + .ignoreExceptions() + .untilAsserted(() -> assertThat(crSelector.scale().getStatus().getReplicas()).isEqualTo(2)); + // get the service var service = new KeycloakService(k8sclient, kc); String url = "https://" + service.getName() + "." + namespace + ":" + Constants.KEYCLOAK_HTTPS_PORT; @@ -122,8 +151,8 @@ public class ClusteringTest extends BaseOperatorTest { K8sUtils.deployKeycloak(k8sclient, kc, false); var targetInstances = 3; crSelector.accept(keycloak -> { - keycloak.getMetadata().setResourceVersion(null); - keycloak.getSpec().setInstances(targetInstances); + keycloak.getMetadata().setResourceVersion(null); + keycloak.getSpec().setInstances(targetInstances); }); var realm = k8sclient.load(getClass().getResourceAsStream("/token-test-realm.yaml")).inNamespace(namespace); var realmImportSelector = k8sclient.resources(KeycloakRealmImport.class).inNamespace(namespace).withName("example-token-test-kc"); diff --git a/operator/src/test/java/org/keycloak/operator/testsuite/unit/KeycloakDistConfiguratorTest.java b/operator/src/test/java/org/keycloak/operator/testsuite/unit/KeycloakDistConfiguratorTest.java index ee809cf09a..e6125ce82f 100644 --- a/operator/src/test/java/org/keycloak/operator/testsuite/unit/KeycloakDistConfiguratorTest.java +++ b/operator/src/test/java/org/keycloak/operator/testsuite/unit/KeycloakDistConfiguratorTest.java @@ -28,7 +28,7 @@ import org.keycloak.operator.Constants; import org.keycloak.operator.controllers.KeycloakDistConfigurator; import org.keycloak.operator.crds.v2alpha1.deployment.Keycloak; import org.keycloak.operator.crds.v2alpha1.deployment.KeycloakStatus; -import org.keycloak.operator.crds.v2alpha1.deployment.KeycloakStatusBuilder; +import org.keycloak.operator.crds.v2alpha1.deployment.KeycloakStatusAggregator; import org.keycloak.operator.crds.v2alpha1.deployment.KeycloakStatusCondition; import org.keycloak.operator.crds.v2alpha1.deployment.ValueOrSecret; import org.keycloak.operator.testsuite.utils.K8sUtils; @@ -208,7 +208,7 @@ public class KeycloakDistConfiguratorTest { private void assertWarningStatusFirstClassFields(KeycloakDistConfigurator distConfig, boolean expectWarning, Collection firstClassFields) { final String message = "warning: You need to specify these fields as the first-class citizen of the CR: "; - final KeycloakStatusBuilder statusBuilder = new KeycloakStatusBuilder(); + final KeycloakStatusAggregator statusBuilder = new KeycloakStatusAggregator(); distConfig.validateOptions(statusBuilder); final KeycloakStatus status = statusBuilder.build(); diff --git a/operator/src/test/java/org/keycloak/operator/testsuite/unit/KeycloakStatusTest.java b/operator/src/test/java/org/keycloak/operator/testsuite/unit/KeycloakStatusTest.java new file mode 100644 index 0000000000..4e973ae5b3 --- /dev/null +++ b/operator/src/test/java/org/keycloak/operator/testsuite/unit/KeycloakStatusTest.java @@ -0,0 +1,37 @@ +/* + * Copyright 2022 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * 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 org.keycloak.operator.testsuite.unit; + +import static org.junit.jupiter.api.Assertions.assertNotEquals; + +import org.junit.jupiter.api.Test; +import org.keycloak.operator.crds.v2alpha1.deployment.KeycloakStatus; +import org.keycloak.operator.crds.v2alpha1.deployment.KeycloakStatusAggregator; + +public class KeycloakStatusTest { + + @Test + public void testEqualityWithScale() { + KeycloakStatus status1 = new KeycloakStatusAggregator().apply(b -> b.withInstances(1)).build(); + + KeycloakStatus status2 = new KeycloakStatusAggregator().apply(b -> b.withInstances(2)).build(); + + assertNotEquals(status1, status2); + } + +}