Switch to StatefulSet (#12757)
This commit is contained in:
parent
46b4b0851d
commit
f2d71cd1c7
9 changed files with 158 additions and 99 deletions
|
@ -17,7 +17,7 @@
|
||||||
package org.keycloak.operator.controllers;
|
package org.keycloak.operator.controllers;
|
||||||
|
|
||||||
import io.fabric8.kubernetes.api.model.Service;
|
import io.fabric8.kubernetes.api.model.Service;
|
||||||
import io.fabric8.kubernetes.api.model.apps.Deployment;
|
import io.fabric8.kubernetes.api.model.apps.StatefulSet;
|
||||||
import io.fabric8.kubernetes.api.model.networking.v1.Ingress;
|
import io.fabric8.kubernetes.api.model.networking.v1.Ingress;
|
||||||
import io.fabric8.kubernetes.client.KubernetesClient;
|
import io.fabric8.kubernetes.client.KubernetesClient;
|
||||||
import io.fabric8.kubernetes.client.informers.SharedIndexInformer;
|
import io.fabric8.kubernetes.client.informers.SharedIndexInformer;
|
||||||
|
@ -61,8 +61,8 @@ public class KeycloakController implements Reconciler<Keycloak>, EventSourceInit
|
||||||
public List<EventSource> prepareEventSources(EventSourceContext<Keycloak> context) {
|
public List<EventSource> prepareEventSources(EventSourceContext<Keycloak> context) {
|
||||||
String namespace = context.getConfigurationService().getClientConfiguration().getNamespace();
|
String namespace = context.getConfigurationService().getClientConfiguration().getNamespace();
|
||||||
|
|
||||||
SharedIndexInformer<Deployment> deploymentInformer =
|
SharedIndexInformer<StatefulSet> deploymentInformer =
|
||||||
client.apps().deployments().inNamespace(namespace)
|
client.apps().statefulSets().inNamespace(namespace)
|
||||||
.withLabels(Constants.DEFAULT_LABELS)
|
.withLabels(Constants.DEFAULT_LABELS)
|
||||||
.runnableInformer(0);
|
.runnableInformer(0);
|
||||||
|
|
||||||
|
|
|
@ -22,13 +22,12 @@ import io.fabric8.kubernetes.api.model.EnvVarBuilder;
|
||||||
import io.fabric8.kubernetes.api.model.EnvVarSourceBuilder;
|
import io.fabric8.kubernetes.api.model.EnvVarSourceBuilder;
|
||||||
import io.fabric8.kubernetes.api.model.ExecActionBuilder;
|
import io.fabric8.kubernetes.api.model.ExecActionBuilder;
|
||||||
import io.fabric8.kubernetes.api.model.HasMetadata;
|
import io.fabric8.kubernetes.api.model.HasMetadata;
|
||||||
import io.fabric8.kubernetes.api.model.IntOrString;
|
|
||||||
import io.fabric8.kubernetes.api.model.PodTemplateSpec;
|
import io.fabric8.kubernetes.api.model.PodTemplateSpec;
|
||||||
import io.fabric8.kubernetes.api.model.ResourceRequirements;
|
import io.fabric8.kubernetes.api.model.ResourceRequirements;
|
||||||
import io.fabric8.kubernetes.api.model.VolumeBuilder;
|
import io.fabric8.kubernetes.api.model.VolumeBuilder;
|
||||||
import io.fabric8.kubernetes.api.model.VolumeMountBuilder;
|
import io.fabric8.kubernetes.api.model.VolumeMountBuilder;
|
||||||
import io.fabric8.kubernetes.api.model.apps.Deployment;
|
import io.fabric8.kubernetes.api.model.apps.StatefulSet;
|
||||||
import io.fabric8.kubernetes.api.model.apps.DeploymentBuilder;
|
import io.fabric8.kubernetes.api.model.apps.StatefulSetBuilder;
|
||||||
import io.fabric8.kubernetes.client.KubernetesClient;
|
import io.fabric8.kubernetes.client.KubernetesClient;
|
||||||
import io.quarkus.logging.Log;
|
import io.quarkus.logging.Log;
|
||||||
import org.keycloak.operator.Config;
|
import org.keycloak.operator.Config;
|
||||||
|
@ -55,13 +54,13 @@ public class KeycloakDeployment extends OperatorManagedResource implements Statu
|
||||||
|
|
||||||
private final Config config;
|
private final Config config;
|
||||||
private final Keycloak keycloakCR;
|
private final Keycloak keycloakCR;
|
||||||
private final Deployment existingDeployment;
|
private final StatefulSet existingDeployment;
|
||||||
private final Deployment baseDeployment;
|
private final StatefulSet baseDeployment;
|
||||||
private final String adminSecretName;
|
private final String adminSecretName;
|
||||||
|
|
||||||
private Set<String> serverConfigSecretsNames;
|
private Set<String> serverConfigSecretsNames;
|
||||||
|
|
||||||
public KeycloakDeployment(KubernetesClient client, Config config, Keycloak keycloakCR, Deployment existingDeployment, String adminSecretName) {
|
public KeycloakDeployment(KubernetesClient client, Config config, Keycloak keycloakCR, StatefulSet existingDeployment, String adminSecretName) {
|
||||||
super(client, keycloakCR);
|
super(client, keycloakCR);
|
||||||
this.config = config;
|
this.config = config;
|
||||||
this.keycloakCR = keycloakCR;
|
this.keycloakCR = keycloakCR;
|
||||||
|
@ -81,15 +80,15 @@ public class KeycloakDeployment extends OperatorManagedResource implements Statu
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Optional<HasMetadata> getReconciledResource() {
|
public Optional<HasMetadata> getReconciledResource() {
|
||||||
Deployment baseDeployment = new DeploymentBuilder(this.baseDeployment).build(); // clone not to change the base template
|
StatefulSet baseDeployment = new StatefulSetBuilder(this.baseDeployment).build(); // clone not to change the base template
|
||||||
Deployment reconciledDeployment;
|
StatefulSet reconciledDeployment;
|
||||||
if (existingDeployment == null) {
|
if (existingDeployment == null) {
|
||||||
Log.info("No existing Deployment found, using the default");
|
Log.info("No existing Deployment found, using the default");
|
||||||
reconciledDeployment = baseDeployment;
|
reconciledDeployment = baseDeployment;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
Log.info("Existing Deployment found, updating specs");
|
Log.info("Existing Deployment found, updating specs");
|
||||||
reconciledDeployment = new DeploymentBuilder(existingDeployment).build();
|
reconciledDeployment = new StatefulSetBuilder(existingDeployment).build();
|
||||||
|
|
||||||
// don't overwrite metadata, just specs
|
// don't overwrite metadata, just specs
|
||||||
reconciledDeployment.setSpec(baseDeployment.getSpec());
|
reconciledDeployment.setSpec(baseDeployment.getSpec());
|
||||||
|
@ -106,10 +105,10 @@ public class KeycloakDeployment extends OperatorManagedResource implements Statu
|
||||||
return Optional.of(reconciledDeployment);
|
return Optional.of(reconciledDeployment);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Deployment fetchExistingDeployment() {
|
private StatefulSet fetchExistingDeployment() {
|
||||||
return client
|
return client
|
||||||
.apps()
|
.apps()
|
||||||
.deployments()
|
.statefulSets()
|
||||||
.inNamespace(getNamespace())
|
.inNamespace(getNamespace())
|
||||||
.withName(getName())
|
.withName(getName())
|
||||||
.get();
|
.get();
|
||||||
|
@ -319,7 +318,7 @@ public class KeycloakDeployment extends OperatorManagedResource implements Statu
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void configureHostname(Deployment deployment) {
|
private void configureHostname(StatefulSet deployment) {
|
||||||
var kcContainer = deployment.getSpec().getTemplate().getSpec().getContainers().get(0);
|
var kcContainer = deployment.getSpec().getTemplate().getSpec().getContainers().get(0);
|
||||||
var hostname = this.keycloakCR.getSpec().getHostname();
|
var hostname = this.keycloakCR.getSpec().getHostname();
|
||||||
var envVars = kcContainer.getEnv();
|
var envVars = kcContainer.getEnv();
|
||||||
|
@ -346,7 +345,7 @@ public class KeycloakDeployment extends OperatorManagedResource implements Statu
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void configureTLS(Deployment deployment) {
|
private void configureTLS(StatefulSet deployment) {
|
||||||
var kcContainer = deployment.getSpec().getTemplate().getSpec().getContainers().get(0);
|
var kcContainer = deployment.getSpec().getTemplate().getSpec().getContainers().get(0);
|
||||||
var tlsSecret = this.keycloakCR.getSpec().getTlsSecret();
|
var tlsSecret = this.keycloakCR.getSpec().getTlsSecret();
|
||||||
var envVars = kcContainer.getEnv();
|
var envVars = kcContainer.getEnv();
|
||||||
|
@ -462,8 +461,8 @@ public class KeycloakDeployment extends OperatorManagedResource implements Statu
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Deployment createBaseDeployment() {
|
private StatefulSet createBaseDeployment() {
|
||||||
Deployment baseDeployment = new DeploymentBuilder()
|
StatefulSet baseDeployment = new StatefulSetBuilder()
|
||||||
.withNewMetadata()
|
.withNewMetadata()
|
||||||
.endMetadata()
|
.endMetadata()
|
||||||
.withNewSpec()
|
.withNewSpec()
|
||||||
|
@ -502,13 +501,6 @@ public class KeycloakDeployment extends OperatorManagedResource implements Statu
|
||||||
.endContainer()
|
.endContainer()
|
||||||
.endSpec()
|
.endSpec()
|
||||||
.endTemplate()
|
.endTemplate()
|
||||||
.withNewStrategy()
|
|
||||||
.withNewRollingUpdate()
|
|
||||||
// Same defaults as for a StatefulSet: https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/#maximum-unavailable-pods
|
|
||||||
.withNewMaxSurge(1) // maximum number of Pods that can be created over the desired number of Pods
|
|
||||||
.withNewMaxUnavailable(1) // maximum number of Pods that can be unavailable during the update process
|
|
||||||
.endRollingUpdate()
|
|
||||||
.endStrategy()
|
|
||||||
.endSpec()
|
.endSpec()
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
@ -601,15 +593,7 @@ public class KeycloakDeployment extends OperatorManagedResource implements Statu
|
||||||
public void updateStatus(KeycloakStatusBuilder status) {
|
public void updateStatus(KeycloakStatusBuilder status) {
|
||||||
validatePodTemplate(status);
|
validatePodTemplate(status);
|
||||||
if (existingDeployment == null) {
|
if (existingDeployment == null) {
|
||||||
status.addNotReadyMessage("No existing Deployment found, waiting for creating a new one");
|
status.addNotReadyMessage("No existing StatefulSet found, waiting for creating a new one");
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var replicaFailure = existingDeployment.getStatus().getConditions().stream()
|
|
||||||
.filter(d -> d.getType().equals("ReplicaFailure")).findFirst();
|
|
||||||
if (replicaFailure.isPresent()) {
|
|
||||||
status.addNotReadyMessage("Deployment failures");
|
|
||||||
status.addErrorMessage("Deployment failure: " + replicaFailure.get());
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -619,16 +603,12 @@ public class KeycloakDeployment extends OperatorManagedResource implements Statu
|
||||||
status.addNotReadyMessage("Waiting for more replicas");
|
status.addNotReadyMessage("Waiting for more replicas");
|
||||||
}
|
}
|
||||||
|
|
||||||
var progressing = existingDeployment.getStatus().getConditions().stream()
|
if (existingDeployment.getStatus() != null
|
||||||
.filter(c -> c.getType().equals("Progressing")).findFirst();
|
&& existingDeployment.getStatus().getCurrentRevision() != null
|
||||||
progressing.ifPresent(p -> {
|
&& existingDeployment.getStatus().getUpdateRevision() != null
|
||||||
String reason = p.getReason();
|
&& !existingDeployment.getStatus().getCurrentRevision().equals(existingDeployment.getStatus().getUpdateRevision())) {
|
||||||
// https://kubernetes.io/docs/concepts/workloads/controllers/deployment/#progressing-deployment
|
|
||||||
if (p.getStatus().equals("True") &&
|
|
||||||
(reason.equals("NewReplicaSetCreated") || reason.equals("FoundNewReplicaSet") || reason.equals("ReplicaSetUpdated"))) {
|
|
||||||
status.addRollingUpdateMessage("Rolling out deployment update");
|
status.addRollingUpdateMessage("Rolling out deployment update");
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Set<String> getConfigSecretsNames() {
|
public Set<String> getConfigSecretsNames() {
|
||||||
|
@ -645,7 +625,7 @@ public class KeycloakDeployment extends OperatorManagedResource implements Statu
|
||||||
}
|
}
|
||||||
|
|
||||||
public void rollingRestart() {
|
public void rollingRestart() {
|
||||||
client.apps().deployments()
|
client.apps().statefulSets()
|
||||||
.inNamespace(getNamespace())
|
.inNamespace(getNamespace())
|
||||||
.withName(getName())
|
.withName(getName())
|
||||||
.rolling().restart();
|
.rolling().restart();
|
||||||
|
|
|
@ -17,13 +17,14 @@
|
||||||
package org.keycloak.operator.controllers;
|
package org.keycloak.operator.controllers;
|
||||||
|
|
||||||
import io.fabric8.kubernetes.api.model.Container;
|
import io.fabric8.kubernetes.api.model.Container;
|
||||||
|
import io.fabric8.kubernetes.api.model.EnvVarBuilder;
|
||||||
import io.fabric8.kubernetes.api.model.HasMetadata;
|
import io.fabric8.kubernetes.api.model.HasMetadata;
|
||||||
import io.fabric8.kubernetes.api.model.PodTemplateSpec;
|
import io.fabric8.kubernetes.api.model.PodTemplateSpec;
|
||||||
import io.fabric8.kubernetes.api.model.SecretVolumeSourceBuilder;
|
import io.fabric8.kubernetes.api.model.SecretVolumeSourceBuilder;
|
||||||
import io.fabric8.kubernetes.api.model.Volume;
|
import io.fabric8.kubernetes.api.model.Volume;
|
||||||
import io.fabric8.kubernetes.api.model.VolumeBuilder;
|
import io.fabric8.kubernetes.api.model.VolumeBuilder;
|
||||||
import io.fabric8.kubernetes.api.model.VolumeMountBuilder;
|
import io.fabric8.kubernetes.api.model.VolumeMountBuilder;
|
||||||
import io.fabric8.kubernetes.api.model.apps.Deployment;
|
import io.fabric8.kubernetes.api.model.apps.StatefulSet;
|
||||||
import io.fabric8.kubernetes.api.model.batch.v1.Job;
|
import io.fabric8.kubernetes.api.model.batch.v1.Job;
|
||||||
import io.fabric8.kubernetes.api.model.batch.v1.JobBuilder;
|
import io.fabric8.kubernetes.api.model.batch.v1.JobBuilder;
|
||||||
import io.fabric8.kubernetes.client.KubernetesClient;
|
import io.fabric8.kubernetes.client.KubernetesClient;
|
||||||
|
@ -37,11 +38,14 @@ import org.keycloak.operator.crds.v2alpha1.realmimport.KeycloakRealmImportStatus
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import static org.keycloak.operator.Constants.DEFAULT_DIST_CONFIG;
|
||||||
|
import static org.keycloak.operator.controllers.KeycloakDeployment.getEnvVarName;
|
||||||
|
|
||||||
public class KeycloakRealmImportJob extends OperatorManagedResource {
|
public class KeycloakRealmImportJob extends OperatorManagedResource {
|
||||||
|
|
||||||
private final Keycloak keycloak;
|
private final Keycloak keycloak;
|
||||||
private final KeycloakRealmImport realmCR;
|
private final KeycloakRealmImport realmCR;
|
||||||
private final Deployment existingDeployment;
|
private final StatefulSet existingDeployment;
|
||||||
private final Job existingJob;
|
private final Job existingJob;
|
||||||
private final String secretName;
|
private final String secretName;
|
||||||
private final String volumeName;
|
private final String volumeName;
|
||||||
|
@ -80,10 +84,10 @@ public class KeycloakRealmImportJob extends OperatorManagedResource {
|
||||||
.get();
|
.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
private Deployment fetchExistingDeployment() {
|
private StatefulSet fetchExistingDeployment() {
|
||||||
return client
|
return client
|
||||||
.apps()
|
.apps()
|
||||||
.deployments()
|
.statefulSets()
|
||||||
.inNamespace(getNamespace())
|
.inNamespace(getNamespace())
|
||||||
.withName(getKeycloakName())
|
.withName(getKeycloakName())
|
||||||
.get();
|
.get();
|
||||||
|
@ -129,6 +133,26 @@ public class KeycloakRealmImportJob extends OperatorManagedResource {
|
||||||
buildKeycloakJobContainer(keycloakPodTemplate.getSpec().getContainers().get(0));
|
buildKeycloakJobContainer(keycloakPodTemplate.getSpec().getContainers().get(0));
|
||||||
keycloakPodTemplate.getSpec().getVolumes().add(buildSecretVolume());
|
keycloakPodTemplate.getSpec().getVolumes().add(buildSecretVolume());
|
||||||
|
|
||||||
|
var labels = keycloakPodTemplate.getMetadata().getLabels();
|
||||||
|
|
||||||
|
// The Job should not be selected with app=keycloak
|
||||||
|
labels.put("app", "keycloak-realm-import");
|
||||||
|
|
||||||
|
var envvars = keycloakPodTemplate
|
||||||
|
.getSpec()
|
||||||
|
.getContainers()
|
||||||
|
.get(0)
|
||||||
|
.getEnv();
|
||||||
|
|
||||||
|
var cacheEnvVarName = getEnvVarName("cache");
|
||||||
|
var healthEnvVarName = getEnvVarName("health-enabled");
|
||||||
|
envvars.removeIf(e -> e.getName().equals(cacheEnvVarName) || e.getName().equals(healthEnvVarName));
|
||||||
|
|
||||||
|
// The Job should not connect to the cache
|
||||||
|
envvars.add(new EnvVarBuilder().withName(cacheEnvVarName).withValue("local").build());
|
||||||
|
// The Job doesn't need health to be enabled
|
||||||
|
envvars.add(new EnvVarBuilder().withName(healthEnvVarName).withValue("false").build());
|
||||||
|
|
||||||
return buildJob(keycloakPodTemplate);
|
return buildJob(keycloakPodTemplate);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -205,7 +229,7 @@ public class KeycloakRealmImportJob extends OperatorManagedResource {
|
||||||
private String getRealmName() { return realmCR.getSpec().getRealm().getRealm(); }
|
private String getRealmName() { return realmCR.getSpec().getRealm().getRealm(); }
|
||||||
|
|
||||||
private void rollingRestart() {
|
private void rollingRestart() {
|
||||||
client.apps().deployments()
|
client.apps().statefulSets()
|
||||||
.inNamespace(getNamespace())
|
.inNamespace(getNamespace())
|
||||||
.withName(getKeycloakName())
|
.withName(getKeycloakName())
|
||||||
.rolling().restart();
|
.rolling().restart();
|
||||||
|
|
|
@ -9,7 +9,7 @@ rules:
|
||||||
# https://github.com/fabric8io/kubernetes-client/issues/3996
|
# https://github.com/fabric8io/kubernetes-client/issues/3996
|
||||||
- extensions
|
- extensions
|
||||||
resources:
|
resources:
|
||||||
- deployments
|
- statefulsets
|
||||||
verbs:
|
verbs:
|
||||||
- get
|
- get
|
||||||
- list
|
- list
|
||||||
|
|
|
@ -20,7 +20,6 @@ package org.keycloak.operator.testsuite.integration;
|
||||||
import io.fabric8.kubernetes.api.model.HasMetadata;
|
import io.fabric8.kubernetes.api.model.HasMetadata;
|
||||||
import io.fabric8.kubernetes.api.model.NamespaceBuilder;
|
import io.fabric8.kubernetes.api.model.NamespaceBuilder;
|
||||||
import io.fabric8.kubernetes.api.model.Secret;
|
import io.fabric8.kubernetes.api.model.Secret;
|
||||||
import io.fabric8.kubernetes.api.model.apps.Deployment;
|
|
||||||
import io.fabric8.kubernetes.client.Config;
|
import io.fabric8.kubernetes.client.Config;
|
||||||
import io.fabric8.kubernetes.client.ConfigBuilder;
|
import io.fabric8.kubernetes.client.ConfigBuilder;
|
||||||
import io.fabric8.kubernetes.client.DefaultKubernetesClient;
|
import io.fabric8.kubernetes.client.DefaultKubernetesClient;
|
||||||
|
@ -186,6 +185,17 @@ public abstract class BaseOperatorTest {
|
||||||
protected static void deleteDB() {
|
protected static void deleteDB() {
|
||||||
// Delete the Postgres StatefulSet
|
// Delete the Postgres StatefulSet
|
||||||
k8sclient.apps().statefulSets().inNamespace(namespace).withName("postgresql-db").delete();
|
k8sclient.apps().statefulSets().inNamespace(namespace).withName("postgresql-db").delete();
|
||||||
|
Awaitility.await()
|
||||||
|
.ignoreExceptions()
|
||||||
|
.untilAsserted(() -> {
|
||||||
|
Log.infof("Waiting for postgres to be deleted");
|
||||||
|
assertThat(k8sclient
|
||||||
|
.apps()
|
||||||
|
.statefulSets()
|
||||||
|
.inNamespace(namespace)
|
||||||
|
.withName("postgresql-db")
|
||||||
|
.get()).isNull();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO improve this (preferably move to JOSDK)
|
// TODO improve this (preferably move to JOSDK)
|
||||||
|
@ -220,7 +230,7 @@ public abstract class BaseOperatorTest {
|
||||||
.untilAsserted(() -> {
|
.untilAsserted(() -> {
|
||||||
var kcDeployments = k8sclient
|
var kcDeployments = k8sclient
|
||||||
.apps()
|
.apps()
|
||||||
.deployments()
|
.statefulSets()
|
||||||
.inNamespace(namespace)
|
.inNamespace(namespace)
|
||||||
.withLabels(Constants.DEFAULT_LABELS)
|
.withLabels(Constants.DEFAULT_LABELS)
|
||||||
.list()
|
.list()
|
||||||
|
|
|
@ -20,7 +20,7 @@ package org.keycloak.operator.testsuite.integration;
|
||||||
import io.fabric8.kubernetes.api.model.EnvVarBuilder;
|
import io.fabric8.kubernetes.api.model.EnvVarBuilder;
|
||||||
import io.fabric8.kubernetes.api.model.SecretBuilder;
|
import io.fabric8.kubernetes.api.model.SecretBuilder;
|
||||||
import io.fabric8.kubernetes.api.model.SecretKeySelectorBuilder;
|
import io.fabric8.kubernetes.api.model.SecretKeySelectorBuilder;
|
||||||
import io.fabric8.kubernetes.api.model.apps.DeploymentSpecBuilder;
|
import io.fabric8.kubernetes.api.model.apps.StatefulSetSpecBuilder;
|
||||||
import io.quarkus.logging.Log;
|
import io.quarkus.logging.Log;
|
||||||
import io.quarkus.test.junit.QuarkusTest;
|
import io.quarkus.test.junit.QuarkusTest;
|
||||||
import org.awaitility.Awaitility;
|
import org.awaitility.Awaitility;
|
||||||
|
@ -65,17 +65,17 @@ public class KeycloakDeploymentTest extends BaseOperatorTest {
|
||||||
|
|
||||||
// Check Operator has deployed Keycloak
|
// Check Operator has deployed Keycloak
|
||||||
Log.info("Checking Operator has deployed Keycloak deployment");
|
Log.info("Checking Operator has deployed Keycloak deployment");
|
||||||
assertThat(k8sclient.apps().deployments().inNamespace(namespace).withName(deploymentName).get()).isNotNull();
|
assertThat(k8sclient.apps().statefulSets().inNamespace(namespace).withName(deploymentName).get()).isNotNull();
|
||||||
|
|
||||||
// Check Keycloak has correct replicas
|
// Check Keycloak has correct replicas
|
||||||
Log.info("Checking Keycloak pod has ready replicas == 1");
|
Log.info("Checking Keycloak pod has ready replicas == 1");
|
||||||
assertThat(k8sclient.apps().deployments().inNamespace(namespace).withName(deploymentName).get().getStatus().getReadyReplicas()).isEqualTo(1);
|
assertThat(k8sclient.apps().statefulSets().inNamespace(namespace).withName(deploymentName).get().getStatus().getReadyReplicas()).isEqualTo(1);
|
||||||
|
|
||||||
// Delete CR
|
// Delete CR
|
||||||
Log.info("Deleting Keycloak CR and watching cleanup");
|
Log.info("Deleting Keycloak CR and watching cleanup");
|
||||||
k8sclient.resources(Keycloak.class).delete(kc);
|
k8sclient.resources(Keycloak.class).delete(kc);
|
||||||
Awaitility.await()
|
Awaitility.await()
|
||||||
.untilAsserted(() -> assertThat(k8sclient.apps().deployments().inNamespace(namespace).withName(deploymentName).get()).isNull());
|
.untilAsserted(() -> assertThat(k8sclient.apps().statefulSets().inNamespace(namespace).withName(deploymentName).get()).isNull());
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
savePodLogs();
|
savePodLogs();
|
||||||
throw e;
|
throw e;
|
||||||
|
@ -99,7 +99,7 @@ public class KeycloakDeploymentTest extends BaseOperatorTest {
|
||||||
Awaitility.await()
|
Awaitility.await()
|
||||||
.during(Duration.ofSeconds(15)) // check if the Deployment is stable
|
.during(Duration.ofSeconds(15)) // check if the Deployment is stable
|
||||||
.untilAsserted(() -> {
|
.untilAsserted(() -> {
|
||||||
var c = k8sclient.apps().deployments().inNamespace(namespace).withName(deploymentName).get()
|
var c = k8sclient.apps().statefulSets().inNamespace(namespace).withName(deploymentName).get()
|
||||||
.getSpec().getTemplate().getSpec().getContainers().get(0);
|
.getSpec().getTemplate().getSpec().getContainers().get(0);
|
||||||
assertThat(c.getImage()).isEqualTo("quay.io/keycloak/non-existing-keycloak");
|
assertThat(c.getImage()).isEqualTo("quay.io/keycloak/non-existing-keycloak");
|
||||||
assertThat(c.getEnv().stream()
|
assertThat(c.getEnv().stream()
|
||||||
|
@ -132,7 +132,7 @@ public class KeycloakDeploymentTest extends BaseOperatorTest {
|
||||||
.ignoreExceptions()
|
.ignoreExceptions()
|
||||||
.untilAsserted(() -> {
|
.untilAsserted(() -> {
|
||||||
Log.info("Asserting default value was overwritten by CR value");
|
Log.info("Asserting default value was overwritten by CR value");
|
||||||
var c = k8sclient.apps().deployments().inNamespace(namespace).withName(kc.getMetadata().getName()).get()
|
var c = k8sclient.apps().statefulSets().inNamespace(namespace).withName(kc.getMetadata().getName()).get()
|
||||||
.getSpec().getTemplate().getSpec().getContainers().get(0);
|
.getSpec().getTemplate().getSpec().getContainers().get(0);
|
||||||
|
|
||||||
assertThat(c.getEnv()).contains(e);
|
assertThat(c.getEnv()).contains(e);
|
||||||
|
@ -151,29 +151,29 @@ public class KeycloakDeploymentTest extends BaseOperatorTest {
|
||||||
deployKeycloak(k8sclient, kc, true);
|
deployKeycloak(k8sclient, kc, true);
|
||||||
|
|
||||||
Log.info("Trying to delete deployment");
|
Log.info("Trying to delete deployment");
|
||||||
assertThat(k8sclient.apps().deployments().withName(deploymentName).delete()).isTrue();
|
assertThat(k8sclient.apps().statefulSets().withName(deploymentName).delete()).isTrue();
|
||||||
Awaitility.await()
|
Awaitility.await()
|
||||||
.untilAsserted(() -> assertThat(k8sclient.apps().deployments().withName(deploymentName).get()).isNotNull());
|
.untilAsserted(() -> assertThat(k8sclient.apps().statefulSets().withName(deploymentName).get()).isNotNull());
|
||||||
|
|
||||||
waitForKeycloakToBeReady(k8sclient, kc); // wait for reconciler to calm down to avoid race condititon
|
waitForKeycloakToBeReady(k8sclient, kc); // wait for reconciler to calm down to avoid race condititon
|
||||||
|
|
||||||
Log.info("Trying to modify deployment");
|
Log.info("Trying to modify deployment");
|
||||||
|
|
||||||
var deployment = k8sclient.apps().deployments().withName(deploymentName).get();
|
var deployment = k8sclient.apps().statefulSets().withName(deploymentName).get();
|
||||||
var labels = Map.of("address", "EvergreenTerrace742");
|
var labels = Map.of("address", "EvergreenTerrace742");
|
||||||
var flandersEnvVar = new EnvVarBuilder().withName("NEIGHBOR").withValue("Stupid Flanders!").build();
|
var flandersEnvVar = new EnvVarBuilder().withName("NEIGHBOR").withValue("Stupid Flanders!").build();
|
||||||
var origSpecs = new DeploymentSpecBuilder(deployment.getSpec()).build(); // deep copy
|
var origSpecs = new StatefulSetSpecBuilder(deployment.getSpec()).build(); // deep copy
|
||||||
|
|
||||||
deployment.getMetadata().getLabels().putAll(labels);
|
deployment.getMetadata().getLabels().putAll(labels);
|
||||||
deployment.getSpec().getTemplate().getSpec().getContainers().get(0).setEnv(List.of(flandersEnvVar));
|
deployment.getSpec().getTemplate().getSpec().getContainers().get(0).setEnv(List.of(flandersEnvVar));
|
||||||
k8sclient.apps().deployments().createOrReplace(deployment);
|
k8sclient.apps().statefulSets().createOrReplace(deployment);
|
||||||
|
|
||||||
Awaitility.await()
|
Awaitility.await()
|
||||||
.atMost(5, MINUTES)
|
.atMost(5, MINUTES)
|
||||||
.pollDelay(1, SECONDS)
|
.pollDelay(1, SECONDS)
|
||||||
.ignoreExceptions()
|
.ignoreExceptions()
|
||||||
.untilAsserted(() -> {
|
.untilAsserted(() -> {
|
||||||
var d = k8sclient.apps().deployments().withName(deploymentName).get();
|
var d = k8sclient.apps().statefulSets().withName(deploymentName).get();
|
||||||
assertThat(d.getMetadata().getLabels().entrySet().containsAll(labels.entrySet())).isTrue(); // additional labels should not be overwritten
|
assertThat(d.getMetadata().getLabels().entrySet().containsAll(labels.entrySet())).isTrue(); // additional labels should not be overwritten
|
||||||
assertThat(d.getSpec()).isEqualTo(origSpecs); // specs should be reconciled back to original values
|
assertThat(d.getSpec()).isEqualTo(origSpecs); // specs should be reconciled back to original values
|
||||||
});
|
});
|
||||||
|
@ -286,15 +286,36 @@ public class KeycloakDeploymentTest extends BaseOperatorTest {
|
||||||
@Test
|
@Test
|
||||||
public void testInitialAdminUser() {
|
public void testInitialAdminUser() {
|
||||||
try {
|
try {
|
||||||
|
var kc = getDefaultKeycloakDeployment();
|
||||||
|
var kcAdminSecret = new KeycloakAdminSecret(k8sclient, kc);
|
||||||
|
|
||||||
|
k8sclient
|
||||||
|
.resources(Keycloak.class)
|
||||||
|
.inNamespace(namespace)
|
||||||
|
.delete();
|
||||||
|
k8sclient
|
||||||
|
.secrets()
|
||||||
|
.inNamespace(namespace)
|
||||||
|
.withName(kcAdminSecret.getName())
|
||||||
|
.delete();
|
||||||
|
|
||||||
|
// Making sure no other Keycloak pod is still around
|
||||||
|
Awaitility.await()
|
||||||
|
.ignoreExceptions()
|
||||||
|
.untilAsserted(() ->
|
||||||
|
assertThat(k8sclient
|
||||||
|
.pods()
|
||||||
|
.inNamespace(namespace)
|
||||||
|
.withLabel("app", "keycloak")
|
||||||
|
.list()
|
||||||
|
.getItems()
|
||||||
|
.size()).isZero());
|
||||||
// Recreating the database to keep this test isolated
|
// Recreating the database to keep this test isolated
|
||||||
deleteDB();
|
deleteDB();
|
||||||
deployDB();
|
deployDB();
|
||||||
var kc = getDefaultKeycloakDeployment();
|
|
||||||
deployKeycloak(k8sclient, kc, true);
|
deployKeycloak(k8sclient, kc, true);
|
||||||
|
|
||||||
var decoder = Base64.getDecoder();
|
var decoder = Base64.getDecoder();
|
||||||
var service = new KeycloakService(k8sclient, kc);
|
var service = new KeycloakService(k8sclient, kc);
|
||||||
var kcAdminSecret = new KeycloakAdminSecret(k8sclient, kc);
|
|
||||||
|
|
||||||
AtomicReference<String> adminUsername = new AtomicReference<>();
|
AtomicReference<String> adminUsername = new AtomicReference<>();
|
||||||
AtomicReference<String> adminPassword = new AtomicReference<>();
|
AtomicReference<String> adminPassword = new AtomicReference<>();
|
||||||
|
|
|
@ -36,6 +36,7 @@ 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;
|
||||||
import static org.keycloak.operator.Constants.KEYCLOAK_HTTPS_PORT;
|
import static org.keycloak.operator.Constants.KEYCLOAK_HTTPS_PORT;
|
||||||
|
import static org.keycloak.operator.controllers.KeycloakDeployment.getEnvVarName;
|
||||||
import static org.keycloak.operator.testsuite.utils.K8sUtils.deployKeycloak;
|
import static org.keycloak.operator.testsuite.utils.K8sUtils.deployKeycloak;
|
||||||
import static org.keycloak.operator.testsuite.utils.K8sUtils.getDefaultKeycloakDeployment;
|
import static org.keycloak.operator.testsuite.utils.K8sUtils.getDefaultKeycloakDeployment;
|
||||||
import static org.keycloak.operator.testsuite.utils.K8sUtils.inClusterCurl;
|
import static org.keycloak.operator.testsuite.utils.K8sUtils.inClusterCurl;
|
||||||
|
@ -115,6 +116,10 @@ public class RealmImportTest extends BaseOperatorTest {
|
||||||
CRAssert.assertKeycloakRealmImportStatusCondition(crSelector.get(), HAS_ERRORS, false);
|
CRAssert.assertKeycloakRealmImportStatusCondition(crSelector.get(), HAS_ERRORS, false);
|
||||||
});
|
});
|
||||||
var job = k8sclient.batch().v1().jobs().inNamespace(namespace).withName("example-count0-kc").get();
|
var job = k8sclient.batch().v1().jobs().inNamespace(namespace).withName("example-count0-kc").get();
|
||||||
|
assertThat(job.getSpec().getTemplate().getMetadata().getLabels().get("app")).isEqualTo("keycloak-realm-import");
|
||||||
|
var envvars = job.getSpec().getTemplate().getSpec().getContainers().get(0).getEnv();
|
||||||
|
assertThat(envvars.stream().filter(e -> e.getName().equals(getEnvVarName("cache"))).findAny().get().getValue()).isEqualTo("local");
|
||||||
|
assertThat(envvars.stream().filter(e -> e.getName().equals(getEnvVarName("health-enabled"))).findAny().get().getValue()).isEqualTo("false");
|
||||||
assertThat(job.getSpec().getTemplate().getSpec().getImagePullSecrets().size()).isEqualTo(1);
|
assertThat(job.getSpec().getTemplate().getSpec().getImagePullSecrets().size()).isEqualTo(1);
|
||||||
assertThat(job.getSpec().getTemplate().getSpec().getImagePullSecrets().get(0).getName()).isEqualTo("my-empty-secret");
|
assertThat(job.getSpec().getTemplate().getSpec().getImagePullSecrets().get(0).getName()).isEqualTo("my-empty-secret");
|
||||||
|
|
||||||
|
|
|
@ -102,7 +102,21 @@ public class WatchedSecretsTest extends BaseOperatorTest {
|
||||||
Log.info("Checking pod logs for DB auth failures");
|
Log.info("Checking pod logs for DB auth failures");
|
||||||
var podlogs = getPodNamesForCrs(Set.of(kc)).stream()
|
var podlogs = getPodNamesForCrs(Set.of(kc)).stream()
|
||||||
.filter(n -> !prevPodNames.contains(n)) // checking just new pods
|
.filter(n -> !prevPodNames.contains(n)) // checking just new pods
|
||||||
.map(n -> k8sclient.pods().inNamespace(namespace).withName(n).getLog())
|
.map(n -> {
|
||||||
|
var name = k8sclient
|
||||||
|
.pods()
|
||||||
|
.inNamespace(namespace)
|
||||||
|
.list()
|
||||||
|
.getItems()
|
||||||
|
.stream()
|
||||||
|
.filter(p -> (p.getMetadata().getName() + p.getMetadata().getCreationTimestamp()).equals(n))
|
||||||
|
.findAny()
|
||||||
|
.get()
|
||||||
|
.getMetadata()
|
||||||
|
.getName();
|
||||||
|
|
||||||
|
return k8sclient.pods().inNamespace(namespace).withName(name).getLog();
|
||||||
|
})
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
assertThat(podlogs).anyMatch(l -> l.contains("password authentication failed for user \"" + username + "\""));
|
assertThat(podlogs).anyMatch(l -> l.contains("password authentication failed for user \"" + username + "\""));
|
||||||
});
|
});
|
||||||
|
@ -220,8 +234,13 @@ public class WatchedSecretsTest extends BaseOperatorTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<String> getPodNamesForCrs(Set<Keycloak> crs) {
|
private List<String> getPodNamesForCrs(Set<Keycloak> crs) {
|
||||||
return k8sclient.pods().inNamespace(namespace).list().getItems().stream()
|
return k8sclient
|
||||||
.map(pod -> pod.getMetadata().getName())
|
.pods()
|
||||||
|
.inNamespace(namespace)
|
||||||
|
.list()
|
||||||
|
.getItems()
|
||||||
|
.stream()
|
||||||
|
.map(pod -> pod.getMetadata().getName() + pod.getMetadata().getCreationTimestamp())
|
||||||
.filter(pod -> crs.stream().map(c -> c.getMetadata().getName()).anyMatch(pod::startsWith))
|
.filter(pod -> crs.stream().map(c -> c.getMetadata().getName()).anyMatch(pod::startsWith))
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,7 @@ import io.fabric8.kubernetes.api.model.IntOrString;
|
||||||
import io.fabric8.kubernetes.api.model.PodTemplateSpec;
|
import io.fabric8.kubernetes.api.model.PodTemplateSpec;
|
||||||
import io.fabric8.kubernetes.api.model.PodTemplateSpecBuilder;
|
import io.fabric8.kubernetes.api.model.PodTemplateSpecBuilder;
|
||||||
import io.fabric8.kubernetes.api.model.ProbeBuilder;
|
import io.fabric8.kubernetes.api.model.ProbeBuilder;
|
||||||
import io.fabric8.kubernetes.api.model.apps.Deployment;
|
import io.fabric8.kubernetes.api.model.apps.StatefulSet;
|
||||||
import io.quarkus.test.junit.QuarkusTest;
|
import io.quarkus.test.junit.QuarkusTest;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.keycloak.operator.Config;
|
import org.keycloak.operator.Config;
|
||||||
|
@ -36,7 +36,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
@QuarkusTest
|
@QuarkusTest
|
||||||
public class PodTemplateTest {
|
public class PodTemplateTest {
|
||||||
|
|
||||||
Deployment getDeployment(PodTemplateSpec podTemplate) {
|
StatefulSet getDeployment(PodTemplateSpec podTemplate) {
|
||||||
var config = new Config(){
|
var config = new Config(){
|
||||||
@Override
|
@Override
|
||||||
public Keycloak keycloak() {
|
public Keycloak keycloak() {
|
||||||
|
@ -58,8 +58,8 @@ public class PodTemplateTest {
|
||||||
spec.setHostname("example.com");
|
spec.setHostname("example.com");
|
||||||
spec.setTlsSecret("example-tls-secret");
|
spec.setTlsSecret("example-tls-secret");
|
||||||
kc.setSpec(spec);
|
kc.setSpec(spec);
|
||||||
var deployment = new KeycloakDeployment(null, config, kc, new Deployment(), "dummy-admin");
|
var deployment = new KeycloakDeployment(null, config, kc, new StatefulSet(), "dummy-admin");
|
||||||
return (Deployment) deployment.getReconciledResource().get();
|
return (StatefulSet) deployment.getReconciledResource().get();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
Loading…
Reference in a new issue