Switch to StatefulSet (#12757)

This commit is contained in:
Andrea Peruffo 2022-07-13 15:58:06 +01:00 committed by GitHub
parent 46b4b0851d
commit f2d71cd1c7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 158 additions and 99 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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