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 org.keycloak.operator.crds.v2alpha1.deployment.ValueOrSecret;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.TreeMap;
import java.util.stream.Collectors; import java.util.stream.Collectors;
public final class Constants { 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 COMPONENT_LABEL = "app.kubernetes.io/component";
public static final String KEYCLOAK_COMPONENT_LABEL = "keycloak.org/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, "app", NAME,
MANAGED_BY_LABEL, MANAGED_BY_VALUE MANAGED_BY_LABEL, MANAGED_BY_VALUE
); )));
public static final String DEFAULT_LABELS_AS_STRING = DEFAULT_LABELS.entrySet().stream() public static final String DEFAULT_LABELS_AS_STRING = DEFAULT_LABELS.entrySet().stream()
.map(e -> e.getKey() + "=" + e.getValue()) .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.Constants;
import org.keycloak.operator.crds.v2alpha1.deployment.Keycloak; import org.keycloak.operator.crds.v2alpha1.deployment.Keycloak;
import org.keycloak.operator.crds.v2alpha1.deployment.KeycloakStatus; 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.KeycloakStatusCondition;
import jakarta.inject.Inject; import jakarta.inject.Inject;
@ -92,13 +92,13 @@ public class KeycloakController implements Reconciler<Keycloak>, EventSourceInit
} }
@Override @Override
public UpdateControl<Keycloak> reconcile(Keycloak kc, Context context) { public UpdateControl<Keycloak> reconcile(Keycloak kc, Context<Keycloak> context) {
String kcName = kc.getMetadata().getName(); String kcName = kc.getMetadata().getName();
String namespace = kc.getMetadata().getNamespace(); String namespace = kc.getMetadata().getNamespace();
Log.infof("--- Reconciling Keycloak: %s in namespace: %s", kcName, namespace); Log.infof("--- Reconciling Keycloak: %s in namespace: %s", kcName, namespace);
var statusBuilder = new KeycloakStatusBuilder(); var statusAggregator = new KeycloakStatusAggregator();
var kcAdminSecret = new KeycloakAdminSecret(client, kc); var kcAdminSecret = new KeycloakAdminSecret(client, kc);
kcAdminSecret.createOrUpdateReconciled(); kcAdminSecret.createOrUpdateReconciled();
@ -112,21 +112,21 @@ public class KeycloakController implements Reconciler<Keycloak>, EventSourceInit
Log.info("Config Secrets modified, restarting deployment"); Log.info("Config Secrets modified, restarting deployment");
kcDeployment.rollingRestart(); kcDeployment.rollingRestart();
} }
kcDeployment.updateStatus(statusBuilder); kcDeployment.updateStatus(statusAggregator);
watchedSecrets.createOrUpdateReconciled(); watchedSecrets.createOrUpdateReconciled();
var kcService = new KeycloakService(client, kc); var kcService = new KeycloakService(client, kc);
kcService.updateStatus(statusBuilder); kcService.updateStatus(statusAggregator);
kcService.createOrUpdateReconciled(); kcService.createOrUpdateReconciled();
var kcDiscoveryService = new KeycloakDiscoveryService(client, kc); var kcDiscoveryService = new KeycloakDiscoveryService(client, kc);
kcDiscoveryService.updateStatus(statusBuilder); kcDiscoveryService.updateStatus(statusAggregator);
kcDiscoveryService.createOrUpdateReconciled(); kcDiscoveryService.createOrUpdateReconciled();
var kcIngress = new KeycloakIngress(client, kc); var kcIngress = new KeycloakIngress(client, kc);
kcIngress.updateStatus(statusBuilder); kcIngress.updateStatus(statusAggregator);
kcIngress.createOrUpdateReconciled(); kcIngress.createOrUpdateReconciled();
var status = statusBuilder.build(); var status = statusAggregator.build();
Log.info("--- Reconciliation finished successfully"); Log.info("--- Reconciliation finished successfully");
@ -152,7 +152,7 @@ public class KeycloakController implements Reconciler<Keycloak>, EventSourceInit
@Override @Override
public ErrorStatusUpdateControl<Keycloak> updateErrorStatus(Keycloak kc, Context<Keycloak> context, Exception e) { public ErrorStatusUpdateControl<Keycloak> updateErrorStatus(Keycloak kc, Context<Keycloak> context, Exception e) {
Log.error("--- Error reconciling", e); Log.error("--- Error reconciling", e);
KeycloakStatus status = new KeycloakStatusBuilder() KeycloakStatus status = new KeycloakStatusAggregator()
.addErrorMessage("Error performing operations:\n" + e.getMessage()) .addErrorMessage("Error performing operations:\n" + e.getMessage())
.build(); .build();

View file

@ -33,7 +33,7 @@ import org.keycloak.common.util.CollectionUtil;
import org.keycloak.operator.Config; import org.keycloak.operator.Config;
import org.keycloak.operator.Constants; import org.keycloak.operator.Constants;
import org.keycloak.operator.crds.v2alpha1.deployment.Keycloak; 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.ValueOrSecret;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
@ -41,6 +41,7 @@ import java.util.ArrayList;
import java.util.Base64; import java.util.Base64;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
@ -50,7 +51,7 @@ import java.util.stream.Collectors;
import static org.keycloak.operator.crds.v2alpha1.CRDUtils.isTlsConfigured; 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 Config operatorConfig;
private final KeycloakDistConfigurator distConfigurator; private final KeycloakDistConfigurator distConfigurator;
@ -122,7 +123,7 @@ public class KeycloakDeployment extends OperatorManagedResource implements Statu
.get(); .get();
} }
public void validatePodTemplate(KeycloakStatusBuilder status) { public void validatePodTemplate(KeycloakStatusAggregator status) {
if (keycloakCR.getSpec() == null || if (keycloakCR.getSpec() == null ||
keycloakCR.getSpec().getUnsupported() == null || keycloakCR.getSpec().getUnsupported() == null ||
keycloakCR.getSpec().getUnsupported().getPodTemplate() == 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().getSelector().setMatchLabels(Constants.DEFAULT_LABELS);
baseDeployment.getSpec().setReplicas(keycloakCR.getSpec().getInstances()); 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) { if (operatorConfig.keycloak().podLabels() != null) {
labels.putAll(operatorConfig.keycloak().podLabels()); labels.putAll(operatorConfig.keycloak().podLabels());
} }
@ -491,20 +492,24 @@ public class KeycloakDeployment extends OperatorManagedResource implements Statu
return envVars; return envVars;
} }
public void updateStatus(KeycloakStatusBuilder status) { public void updateStatus(KeycloakStatusAggregator status) {
status.apply(b -> b.withSelector(Constants.DEFAULT_LABELS_AS_STRING));
validatePodTemplate(status); validatePodTemplate(status);
if (existingDeployment == null) { if (existingDeployment == null) {
status.addNotReadyMessage("No existing StatefulSet found, waiting for creating a new one"); status.addNotReadyMessage("No existing StatefulSet found, waiting for creating a new one");
return; return;
} }
if (existingDeployment.getStatus() == null if (existingDeployment.getStatus() == null) {
|| existingDeployment.getStatus().getReadyReplicas() == null status.addNotReadyMessage("Waiting for deployment status");
|| existingDeployment.getStatus().getReadyReplicas() < keycloakCR.getSpec().getInstances()) { } else {
status.addNotReadyMessage("Waiting for more replicas"); 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) { if (migrationInProgress) {
status.addNotReadyMessage("Performing Keycloak upgrade, scaling down the deployment"); status.addNotReadyMessage("Performing Keycloak upgrade, scaling down the deployment");
} else if (existingDeployment.getStatus() != null } else if (existingDeployment.getStatus() != null

View file

@ -24,11 +24,11 @@ import io.fabric8.kubernetes.api.model.ServiceSpecBuilder;
import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.KubernetesClient;
import org.keycloak.operator.Constants; import org.keycloak.operator.Constants;
import org.keycloak.operator.crds.v2alpha1.deployment.Keycloak; 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; import java.util.Optional;
public class KeycloakDiscoveryService extends OperatorManagedResource implements StatusUpdater<KeycloakStatusBuilder> { public class KeycloakDiscoveryService extends OperatorManagedResource implements StatusUpdater<KeycloakStatusAggregator> {
private Service existingService; private Service existingService;
@ -78,7 +78,7 @@ public class KeycloakDiscoveryService extends OperatorManagedResource implements
.get(); .get();
} }
public void updateStatus(KeycloakStatusBuilder status) { public void updateStatus(KeycloakStatusAggregator status) {
if (existingService == null) { if (existingService == null) {
status.addNotReadyMessage("No existing Discovery Service found, waiting for creating a new one"); status.addNotReadyMessage("No existing Discovery Service found, waiting for creating a new one");
return; return;

View file

@ -29,7 +29,7 @@ import io.quarkus.logging.Log;
import org.keycloak.common.util.CollectionUtil; import org.keycloak.common.util.CollectionUtil;
import org.keycloak.operator.Constants; import org.keycloak.operator.Constants;
import org.keycloak.operator.crds.v2alpha1.deployment.Keycloak; 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.ValueOrSecret;
import org.keycloak.operator.crds.v2alpha1.deployment.spec.DatabaseSpec; import org.keycloak.operator.crds.v2alpha1.deployment.spec.DatabaseSpec;
import org.keycloak.operator.crds.v2alpha1.deployment.spec.FeatureSpec; import org.keycloak.operator.crds.v2alpha1.deployment.spec.FeatureSpec;
@ -85,7 +85,7 @@ public class KeycloakDistConfigurator {
* *
* @param status Keycloak Status builder * @param status Keycloak Status builder
*/ */
public void validateOptions(KeycloakStatusBuilder status) { public void validateOptions(KeycloakStatusAggregator status) {
assumeFirstClassCitizens(status); assumeFirstClassCitizens(status);
} }
@ -175,7 +175,7 @@ public class KeycloakDistConfigurator {
* *
* @param status Status of the deployment * @param status Status of the deployment
*/ */
protected void assumeFirstClassCitizens(KeycloakStatusBuilder status) { protected void assumeFirstClassCitizens(KeycloakStatusAggregator status) {
final var serverConfigNames = keycloakCR final var serverConfigNames = keycloakCR
.getSpec() .getSpec()
.getAdditionalOptions() .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.Constants;
import org.keycloak.operator.crds.v2alpha1.deployment.spec.IngressSpec; import org.keycloak.operator.crds.v2alpha1.deployment.spec.IngressSpec;
import org.keycloak.operator.crds.v2alpha1.deployment.Keycloak; 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.HashMap;
import java.util.Optional; import java.util.Optional;
import static org.keycloak.operator.crds.v2alpha1.CRDUtils.isTlsConfigured; 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 Ingress existingIngress;
private final Keycloak keycloak; private final Keycloak keycloak;
@ -142,7 +142,7 @@ public class KeycloakIngress extends OperatorManagedResource implements StatusUp
.get(); .get();
} }
public void updateStatus(KeycloakStatusBuilder status) { public void updateStatus(KeycloakStatusAggregator status) {
IngressSpec ingressSpec = keycloak.getSpec().getIngressSpec(); IngressSpec ingressSpec = keycloak.getSpec().getIngressSpec();
if (ingressSpec == null) { if (ingressSpec == null) {
ingressSpec = new IngressSpec(); ingressSpec = new IngressSpec();

View file

@ -24,7 +24,7 @@ import io.fabric8.kubernetes.api.model.ServiceSpecBuilder;
import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.KubernetesClient;
import org.keycloak.operator.Constants; import org.keycloak.operator.Constants;
import org.keycloak.operator.crds.v2alpha1.deployment.Keycloak; 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 org.keycloak.operator.crds.v2alpha1.deployment.spec.HttpSpec;
import java.util.Optional; 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.getValueFromSubSpec;
import static org.keycloak.operator.crds.v2alpha1.CRDUtils.isTlsConfigured; 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 Service existingService;
private final Keycloak keycloak; private final Keycloak keycloak;
@ -84,7 +84,7 @@ public class KeycloakService extends OperatorManagedResource implements StatusUp
.get(); .get();
} }
public void updateStatus(KeycloakStatusBuilder status) { public void updateStatus(KeycloakStatusAggregator status) {
if (existingService == null) { if (existingService == null) {
status.addNotReadyMessage("No existing Keycloak Service found, waiting for creating a new one"); status.addNotReadyMessage("No existing Keycloak Service found, waiting for creating a new one");
return; return;

View file

@ -21,6 +21,7 @@ import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyDescription; import com.fasterxml.jackson.annotation.JsonPropertyDescription;
import io.fabric8.kubernetes.api.model.LocalObjectReference; 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.DatabaseSpec;
import org.keycloak.operator.crds.v2alpha1.deployment.spec.FeatureSpec; import org.keycloak.operator.crds.v2alpha1.deployment.spec.FeatureSpec;
@ -36,6 +37,7 @@ import java.util.List;
@JsonInclude(JsonInclude.Include.NON_NULL) @JsonInclude(JsonInclude.Include.NON_NULL)
public class KeycloakSpec { public class KeycloakSpec {
@SpecReplicas
@JsonPropertyDescription("Number of Keycloak instances in HA mode. Default is 1.") @JsonPropertyDescription("Number of Keycloak instances in HA mode. Default is 1.")
private int instances = 1; private int instances = 1;

View file

@ -19,11 +19,38 @@ package org.keycloak.operator.crds.v2alpha1.deployment;
import java.util.List; import java.util.List;
import java.util.Objects; 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> * @author Vaclav Muzikar <vmuzikar@redhat.com>
*/ */
@Buildable(editableEnabled = false, builderPackage = "io.fabric8.kubernetes.api.builder", lazyCollectionInitEnabled = false)
public class KeycloakStatus { public class KeycloakStatus {
@LabelSelector
private String selector;
@StatusReplicas
private Integer instances;
private List<KeycloakStatusCondition> conditions; 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() { public List<KeycloakStatusCondition> getConditions() {
return conditions; return conditions;
@ -38,11 +65,13 @@ public class KeycloakStatus {
if (this == o) return true; if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false; if (o == null || getClass() != o.getClass()) return false;
KeycloakStatus status = (KeycloakStatus) o; 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 @Override
public int hashCode() { 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.ArrayList;
import java.util.List; import java.util.List;
import java.util.function.Consumer;
/** /**
* @author Vaclav Muzikar <vmuzikar@redhat.com> * @author Vaclav Muzikar <vmuzikar@redhat.com>
*/ */
public class KeycloakStatusBuilder { public class KeycloakStatusAggregator {
private final KeycloakStatusCondition readyCondition; private final KeycloakStatusCondition readyCondition;
private final KeycloakStatusCondition hasErrorsCondition; private final KeycloakStatusCondition hasErrorsCondition;
private final KeycloakStatusCondition rollingUpdate; private final KeycloakStatusCondition rollingUpdate;
@ -31,8 +32,10 @@ public class KeycloakStatusBuilder {
private final List<String> notReadyMessages = new ArrayList<>(); private final List<String> notReadyMessages = new ArrayList<>();
private final List<String> errorMessages = new ArrayList<>(); private final List<String> errorMessages = new ArrayList<>();
private final List<String> rollingUpdateMessages = new ArrayList<>(); private final List<String> rollingUpdateMessages = new ArrayList<>();
private final KeycloakStatusBuilder statusBuilder = new KeycloakStatusBuilder();
public KeycloakStatusBuilder() { public KeycloakStatusAggregator() {
readyCondition = new KeycloakStatusCondition(); readyCondition = new KeycloakStatusCondition();
readyCondition.setType(KeycloakStatusCondition.READY); readyCondition.setType(KeycloakStatusCondition.READY);
readyCondition.setStatus(true); readyCondition.setStatus(true);
@ -46,36 +49,46 @@ public class KeycloakStatusBuilder {
rollingUpdate.setStatus(false); rollingUpdate.setStatus(false);
} }
public KeycloakStatusBuilder addNotReadyMessage(String message) { public KeycloakStatusAggregator addNotReadyMessage(String message) {
readyCondition.setStatus(false); readyCondition.setStatus(false);
notReadyMessages.add(message); notReadyMessages.add(message);
return this; return this;
} }
public KeycloakStatusBuilder addErrorMessage(String message) { public KeycloakStatusAggregator addErrorMessage(String message) {
hasErrorsCondition.setStatus(true); hasErrorsCondition.setStatus(true);
errorMessages.add(message); errorMessages.add(message);
return this; return this;
} }
public KeycloakStatusBuilder addWarningMessage(String message) { public KeycloakStatusAggregator addWarningMessage(String message) {
errorMessages.add("warning: " + message); errorMessages.add("warning: " + message);
return this; return this;
} }
public KeycloakStatusBuilder addRollingUpdateMessage(String message) { public KeycloakStatusAggregator addRollingUpdateMessage(String message) {
rollingUpdate.setStatus(true); rollingUpdate.setStatus(true);
rollingUpdateMessages.add(message); rollingUpdateMessages.add(message);
return this; 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() { public KeycloakStatus build() {
readyCondition.setMessage(String.join("\n", notReadyMessages)); readyCondition.setMessage(String.join("\n", notReadyMessages));
hasErrorsCondition.setMessage(String.join("\n", errorMessages)); hasErrorsCondition.setMessage(String.join("\n", errorMessages));
rollingUpdate.setMessage(String.join("\n", rollingUpdateMessages)); rollingUpdate.setMessage(String.join("\n", rollingUpdateMessages));
KeycloakStatus status = new KeycloakStatus(); return statusBuilder.withConditions(List.of(readyCondition, hasErrorsCondition, rollingUpdate)).build();
status.setConditions(List.of(readyCondition, hasErrorsCondition, rollingUpdate));
return status;
} }
} }

View file

@ -17,29 +17,31 @@
package org.keycloak.operator.testsuite.integration; package org.keycloak.operator.testsuite.integration;
import com.fasterxml.jackson.databind.JsonNode;
import io.fabric8.kubernetes.client.utils.Serialization; import io.fabric8.kubernetes.client.utils.Serialization;
import io.quarkus.logging.Log; import io.quarkus.logging.Log;
import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.junit.QuarkusTest;
import io.restassured.RestAssured; import io.restassured.RestAssured;
import org.awaitility.Awaitility; import org.awaitility.Awaitility;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.keycloak.operator.Constants; import org.keycloak.operator.Constants;
import org.keycloak.operator.testsuite.utils.CRAssert;
import org.keycloak.operator.controllers.KeycloakService; import org.keycloak.operator.controllers.KeycloakService;
import org.keycloak.operator.crds.v2alpha1.deployment.Keycloak; 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.KeycloakRealmImport;
import org.keycloak.operator.crds.v2alpha1.realmimport.KeycloakRealmImportStatusCondition; 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.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.MINUTES;
import static java.util.concurrent.TimeUnit.SECONDS; import static java.util.concurrent.TimeUnit.SECONDS;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
@QuarkusTest @QuarkusTest
public class ClusteringTest extends BaseOperatorTest { public class ClusteringTest extends BaseOperatorTest {
@ -55,11 +57,29 @@ public class ClusteringTest extends BaseOperatorTest {
var kcPodsSelector = k8sclient.pods().inNamespace(namespace).withLabel("app", "keycloak"); 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 // when scale it to 3
crSelector.accept(keycloak -> { crSelector.scale(3);
keycloak.getMetadata().setResourceVersion(null); assertThat(crSelector.scale().getSpec().getReplicas()).isEqualTo(3);
keycloak.getSpec().setInstances(3);
});
Awaitility.await() Awaitility.await()
.atMost(1, MINUTES) .atMost(1, MINUTES)
@ -68,15 +88,19 @@ public class ClusteringTest extends BaseOperatorTest {
.untilAsserted(() -> CRAssert.assertKeycloakStatusCondition(crSelector.get(), KeycloakStatusCondition.READY, false)); .untilAsserted(() -> CRAssert.assertKeycloakStatusCondition(crSelector.get(), KeycloakStatusCondition.READY, false));
Awaitility.await() Awaitility.await()
.atMost(Duration.ofSeconds(60)) .atMost(Duration.ofSeconds(180))
.ignoreExceptions() .ignoreExceptions()
.untilAsserted(() -> assertThat(kcPodsSelector.list().getItems().size()).isEqualTo(3)); .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 // when scale it down to 2
crSelector.accept(keycloak -> { crSelector.scale(2);
keycloak.getMetadata().setResourceVersion(null); assertThat(crSelector.scale().getSpec().getReplicas()).isEqualTo(2);
keycloak.getSpec().setInstances(2);
});
Awaitility.await() Awaitility.await()
.atMost(Duration.ofSeconds(180)) .atMost(Duration.ofSeconds(180))
.ignoreExceptions() .ignoreExceptions()
@ -88,6 +112,11 @@ public class ClusteringTest extends BaseOperatorTest {
.ignoreExceptions() .ignoreExceptions()
.untilAsserted(() -> CRAssert.assertKeycloakStatusCondition(crSelector.get(), KeycloakStatusCondition.READY, true)); .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 // get the service
var service = new KeycloakService(k8sclient, kc); var service = new KeycloakService(k8sclient, kc);
String url = "https://" + service.getName() + "." + namespace + ":" + Constants.KEYCLOAK_HTTPS_PORT; String url = "https://" + service.getName() + "." + namespace + ":" + Constants.KEYCLOAK_HTTPS_PORT;
@ -122,8 +151,8 @@ public class ClusteringTest extends BaseOperatorTest {
K8sUtils.deployKeycloak(k8sclient, kc, false); K8sUtils.deployKeycloak(k8sclient, kc, false);
var targetInstances = 3; var targetInstances = 3;
crSelector.accept(keycloak -> { crSelector.accept(keycloak -> {
keycloak.getMetadata().setResourceVersion(null); keycloak.getMetadata().setResourceVersion(null);
keycloak.getSpec().setInstances(targetInstances); keycloak.getSpec().setInstances(targetInstances);
}); });
var realm = k8sclient.load(getClass().getResourceAsStream("/token-test-realm.yaml")).inNamespace(namespace); 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"); var realmImportSelector = k8sclient.resources(KeycloakRealmImport.class).inNamespace(namespace).withName("example-token-test-kc");

View file

@ -28,7 +28,7 @@ import org.keycloak.operator.Constants;
import org.keycloak.operator.controllers.KeycloakDistConfigurator; import org.keycloak.operator.controllers.KeycloakDistConfigurator;
import org.keycloak.operator.crds.v2alpha1.deployment.Keycloak; import org.keycloak.operator.crds.v2alpha1.deployment.Keycloak;
import org.keycloak.operator.crds.v2alpha1.deployment.KeycloakStatus; 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.KeycloakStatusCondition;
import org.keycloak.operator.crds.v2alpha1.deployment.ValueOrSecret; import org.keycloak.operator.crds.v2alpha1.deployment.ValueOrSecret;
import org.keycloak.operator.testsuite.utils.K8sUtils; import org.keycloak.operator.testsuite.utils.K8sUtils;
@ -208,7 +208,7 @@ public class KeycloakDistConfiguratorTest {
private void assertWarningStatusFirstClassFields(KeycloakDistConfigurator distConfig, boolean expectWarning, Collection<String> firstClassFields) { 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 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); distConfig.validateOptions(statusBuilder);
final KeycloakStatus status = statusBuilder.build(); 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);
}
}