enabling keycloak to be a scalable resource (#20828)

Closes #20825
This commit is contained in:
Steven Hawkins 2023-06-07 11:57:25 -04:00 committed by GitHub
parent 31db84e924
commit 075d913037
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 181 additions and 64 deletions

View file

@ -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<String, String> DEFAULT_LABELS = Map.of(
public static final Map<String, String> 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())

View file

@ -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<Keycloak>, EventSourceInit
}
@Override
public UpdateControl<Keycloak> reconcile(Keycloak kc, Context context) {
public UpdateControl<Keycloak> reconcile(Keycloak kc, Context<Keycloak> 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<Keycloak>, 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<Keycloak>, EventSourceInit
@Override
public ErrorStatusUpdateControl<Keycloak> updateErrorStatus(Keycloak kc, Context<Keycloak> context, Exception e) {
Log.error("--- Error reconciling", e);
KeycloakStatus status = new KeycloakStatusBuilder()
KeycloakStatus status = new KeycloakStatusAggregator()
.addErrorMessage("Error performing operations:\n" + e.getMessage())
.build();

View file

@ -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<KeycloakStatusBuilder> {
public class KeycloakDeployment extends OperatorManagedResource implements StatusUpdater<KeycloakStatusAggregator> {
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<String, String> labels = new HashMap<>(Constants.DEFAULT_LABELS);
Map<String, String> labels = new LinkedHashMap<>(Constants.DEFAULT_LABELS);
if (operatorConfig.keycloak().podLabels() != null) {
labels.putAll(operatorConfig.keycloak().podLabels());
}
@ -492,17 +493,21 @@ 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) {

View file

@ -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<KeycloakStatusBuilder> {
public class KeycloakDiscoveryService extends OperatorManagedResource implements StatusUpdater<KeycloakStatusAggregator> {
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;

View file

@ -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()

View file

@ -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<KeycloakStatusBuilder> {
public class KeycloakIngress extends OperatorManagedResource implements StatusUpdater<KeycloakStatusAggregator> {
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();

View file

@ -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<KeycloakStatusBuilder> {
public class KeycloakService extends OperatorManagedResource implements StatusUpdater<KeycloakStatusAggregator> {
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;

View file

@ -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;

View file

@ -19,12 +19,39 @@ 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 <vmuzikar@redhat.com>
*/
@Buildable(editableEnabled = false, builderPackage = "io.fabric8.kubernetes.api.builder", lazyCollectionInitEnabled = false)
public class KeycloakStatus {
@LabelSelector
private String selector;
@StatusReplicas
private Integer instances;
private List<KeycloakStatusCondition> 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<KeycloakStatusCondition> 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());
}
}

View file

@ -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 <vmuzikar@redhat.com>
*/
public class KeycloakStatusBuilder {
public class KeycloakStatusAggregator {
private final KeycloakStatusCondition readyCondition;
private final KeycloakStatusCondition hasErrorsCondition;
private final KeycloakStatusCondition rollingUpdate;
@ -32,7 +33,9 @@ public class KeycloakStatusBuilder {
private final List<String> errorMessages = new ArrayList<>();
private final List<String> rollingUpdateMessages = new ArrayList<>();
public KeycloakStatusBuilder() {
private final KeycloakStatusBuilder statusBuilder = new 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<KeycloakStatusBuilder> 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();
}
}

View file

@ -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;

View file

@ -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<String> 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();

View file

@ -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);
}
}