Tests for Keycloak Deployment
This commit is contained in:
parent
d9f1a9b207
commit
cfddcad3c5
5 changed files with 274 additions and 68 deletions
|
@ -12,15 +12,20 @@ import io.javaoperatorsdk.operator.api.reconciler.Reconciler;
|
|||
import io.quarkiverse.operatorsdk.runtime.OperatorProducer;
|
||||
import io.quarkiverse.operatorsdk.runtime.QuarkusConfigurationService;
|
||||
import io.quarkus.logging.Log;
|
||||
import org.awaitility.Awaitility;
|
||||
import org.eclipse.microprofile.config.ConfigProvider;
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
|
||||
import javax.enterprise.inject.Instance;
|
||||
import javax.enterprise.inject.spi.CDI;
|
||||
import javax.enterprise.util.TypeLiteral;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileWriter;
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
|
@ -32,6 +37,9 @@ public abstract class ClusterOperatorTest {
|
|||
public static final String OPERATOR_DEPLOYMENT_PROP = "test.operator.deployment";
|
||||
public static final String TARGET_KUBERNETES_GENERATED_YML_FOLDER = "target/kubernetes/";
|
||||
|
||||
public static final String TEST_RESULTS_DIR = "target/operator-test-results/";
|
||||
public static final String POD_LOGS_DIR = TEST_RESULTS_DIR + "pod-logs/";
|
||||
|
||||
public enum OperatorDeployment {local,remote}
|
||||
|
||||
protected static OperatorDeployment operatorDeployment;
|
||||
|
@ -50,6 +58,7 @@ public abstract class ClusterOperatorTest {
|
|||
operatorDeployment = ConfigProvider.getConfig().getOptionalValue(OPERATOR_DEPLOYMENT_PROP, OperatorDeployment.class).orElse(OperatorDeployment.local);
|
||||
deploymentTarget = ConfigProvider.getConfig().getOptionalValue(QUARKUS_KUBERNETES_DEPLOYMENT_TARGET, String.class).orElse("kubernetes");
|
||||
|
||||
setDefaultAwaitilityTimings();
|
||||
calculateNamespace();
|
||||
createK8sClient();
|
||||
createNamespace();
|
||||
|
@ -63,6 +72,12 @@ public abstract class ClusterOperatorTest {
|
|||
operator.start();
|
||||
}
|
||||
|
||||
deployDB();
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
public void beforeEach() {
|
||||
Log.info(((operatorDeployment == OperatorDeployment.remote) ? "Remote " : "Local ") + "Run Test :" + namespace);
|
||||
}
|
||||
|
||||
private static void createK8sClient() {
|
||||
|
@ -122,6 +137,41 @@ public abstract class ClusterOperatorTest {
|
|||
namespace = "keycloak-test-" + UUID.randomUUID();
|
||||
}
|
||||
|
||||
protected static void deployDB() {
|
||||
// DB
|
||||
Log.info("Creating new PostgreSQL deployment");
|
||||
k8sclient.load(KeycloakDeploymentE2EIT.class.getResourceAsStream("/example-postgres.yaml")).inNamespace(namespace).createOrReplace();
|
||||
|
||||
// Check DB has deployed and ready
|
||||
Log.info("Checking Postgres is running");
|
||||
Awaitility.await()
|
||||
.untilAsserted(() -> assertThat(k8sclient.apps().statefulSets().inNamespace(namespace).withName("postgresql-db").get().getStatus().getReadyReplicas()).isEqualTo(1));
|
||||
}
|
||||
|
||||
// TODO improve this (preferably move to JOSDK)
|
||||
protected void savePodLogs() {
|
||||
Log.infof("Saving pod logs to %s", POD_LOGS_DIR);
|
||||
for (var pod : k8sclient.pods().inNamespace(namespace).list().getItems()) {
|
||||
try {
|
||||
String podName = pod.getMetadata().getName();
|
||||
Log.infof("Processing %s", podName);
|
||||
String podLog = k8sclient.pods().inNamespace(namespace).withName(podName).getLog();
|
||||
File file = new File(POD_LOGS_DIR + String.format("%s-%s.txt", namespace, podName)); // using namespace for now, if more tests fail, the log might get overwritten
|
||||
file.getAbsoluteFile().getParentFile().mkdirs();
|
||||
try (var fw = new FileWriter(file, false)) {
|
||||
fw.write(podLog);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.error(e.getStackTrace());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void setDefaultAwaitilityTimings() {
|
||||
Awaitility.setDefaultPollInterval(Duration.ofSeconds(1));
|
||||
Awaitility.setDefaultTimeout(Duration.ofSeconds(180));
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
public static void after() throws FileNotFoundException {
|
||||
|
||||
|
|
|
@ -0,0 +1,122 @@
|
|||
package org.keycloak.operator;
|
||||
|
||||
import io.fabric8.kubernetes.api.model.EnvVarBuilder;
|
||||
import io.fabric8.kubernetes.api.model.apps.DeploymentSpecBuilder;
|
||||
import io.quarkus.logging.Log;
|
||||
import io.quarkus.test.junit.QuarkusTest;
|
||||
import org.awaitility.Awaitility;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.keycloak.operator.v2alpha1.crds.Keycloak;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.keycloak.operator.utils.K8sUtils.deployKeycloak;
|
||||
import static org.keycloak.operator.utils.K8sUtils.getDefaultKeycloakDeployment;
|
||||
import static org.keycloak.operator.utils.K8sUtils.waitForKeycloakToBeReady;
|
||||
|
||||
@QuarkusTest
|
||||
public class KeycloakDeploymentE2EIT extends ClusterOperatorTest {
|
||||
@Test
|
||||
public void testBasicKeycloakDeploymentAndDeletion() {
|
||||
try {
|
||||
// CR
|
||||
Log.info("Creating new Keycloak CR example");
|
||||
var kc = getDefaultKeycloakDeployment();
|
||||
var deploymentName = kc.getMetadata().getName();
|
||||
deployKeycloak(k8sclient, kc, true);
|
||||
|
||||
// Check Operator has deployed Keycloak
|
||||
Log.info("Checking Operator has deployed Keycloak deployment");
|
||||
assertThat(k8sclient.apps().deployments().inNamespace(namespace).withName(deploymentName).get()).isNotNull();
|
||||
|
||||
// Check Keycloak has correct replicas
|
||||
Log.info("Checking Keycloak pod has ready replicas == 1");
|
||||
assertThat(k8sclient.apps().deployments().inNamespace(namespace).withName(deploymentName).get().getStatus().getReadyReplicas()).isEqualTo(1);
|
||||
|
||||
// Delete CR
|
||||
Log.info("Deleting Keycloak CR and watching cleanup");
|
||||
k8sclient.resources(Keycloak.class).delete(kc);
|
||||
Awaitility.await()
|
||||
.untilAsserted(() -> assertThat(k8sclient.apps().deployments().inNamespace(namespace).withName(deploymentName).get()).isNull());
|
||||
} catch (Exception e) {
|
||||
savePodLogs();
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCRFields() {
|
||||
try {
|
||||
var kc = getDefaultKeycloakDeployment();
|
||||
var deploymentName = kc.getMetadata().getName();
|
||||
deployKeycloak(k8sclient, kc, true);
|
||||
|
||||
kc.getSpec().setImage("quay.io/keycloak/non-existing-keycloak");
|
||||
kc.getSpec().getServerConfiguration().put("KC_DB_PASSWORD", "Ay Caramba!");
|
||||
deployKeycloak(k8sclient, kc, false);
|
||||
|
||||
Awaitility.await()
|
||||
.during(Duration.ofSeconds(15)) // check if the Deployment is stable
|
||||
.untilAsserted(() -> {
|
||||
var c = k8sclient.apps().deployments().inNamespace(namespace).withName(deploymentName).get()
|
||||
.getSpec().getTemplate().getSpec().getContainers().get(0);
|
||||
assertThat(c.getImage()).isEqualTo("quay.io/keycloak/non-existing-keycloak");
|
||||
assertThat(c.getEnv().stream()
|
||||
.anyMatch(e -> e.getName().equals("KC_DB_PASSWORD") && e.getValue().equals("Ay Caramba!")))
|
||||
.isTrue();
|
||||
});
|
||||
|
||||
} catch (Exception e) {
|
||||
savePodLogs();
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDeploymentDurability() {
|
||||
try {
|
||||
var kc = getDefaultKeycloakDeployment();
|
||||
var deploymentName = kc.getMetadata().getName();
|
||||
deployKeycloak(k8sclient, kc, true);
|
||||
|
||||
Log.info("Trying to delete deployment");
|
||||
assertThat(k8sclient.apps().deployments().withName(deploymentName).delete()).isTrue();
|
||||
Awaitility.await()
|
||||
.untilAsserted(() -> assertThat(k8sclient.apps().deployments().withName(deploymentName).get()).isNotNull());
|
||||
|
||||
waitForKeycloakToBeReady(k8sclient, kc); // wait for reconciler to calm down to avoid race condititon
|
||||
|
||||
Log.info("Trying to modify deployment");
|
||||
|
||||
var deployment = k8sclient.apps().deployments().withName(deploymentName).get();
|
||||
var labels = Map.of("address", "EvergreenTerrace742");
|
||||
var flandersEnvVar = new EnvVarBuilder().withName("NEIGHBOR").withValue("Stupid Flanders!").build();
|
||||
var origSpecs = new DeploymentSpecBuilder(deployment.getSpec()).build(); // deep copy
|
||||
|
||||
deployment.getMetadata().getLabels().putAll(labels);
|
||||
deployment.getSpec().getTemplate().getSpec().getContainers().get(0).setEnv(List.of(flandersEnvVar));
|
||||
k8sclient.apps().deployments().createOrReplace(deployment);
|
||||
|
||||
Awaitility.await()
|
||||
.untilAsserted(() -> {
|
||||
var d = k8sclient.apps().deployments().withName(deploymentName).get();
|
||||
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
|
||||
});
|
||||
} catch (Exception e) {
|
||||
savePodLogs();
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void cleanup() {
|
||||
Log.info("Deleting Keycloak CR");
|
||||
k8sclient.resources(Keycloak.class).delete(getDefaultKeycloakDeployment());
|
||||
}
|
||||
|
||||
}
|
|
@ -1,68 +0,0 @@
|
|||
package org.keycloak.operator;
|
||||
|
||||
import io.quarkus.logging.Log;
|
||||
import io.quarkus.test.junit.QuarkusTest;
|
||||
import org.awaitility.Awaitility;
|
||||
import org.awaitility.core.ConditionTimeoutException;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.Duration;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@QuarkusTest
|
||||
public class OperatorE2EIT extends ClusterOperatorTest {
|
||||
@Test
|
||||
public void given_ClusterAndOperatorRunning_when_KeycloakCRCreated_Then_KeycloakStructureIsDeployedAndStatusIsOK() throws IOException {
|
||||
Log.info(((operatorDeployment == OperatorDeployment.remote) ? "Remote " : "Local ") + "Run Test :" + namespace);
|
||||
|
||||
// DB
|
||||
Log.info("Creating new PostgreSQL deployment");
|
||||
k8sclient.load(OperatorE2EIT.class.getResourceAsStream("/example-postgres.yaml")).inNamespace(namespace).createOrReplace();
|
||||
|
||||
// Check DB has deployed and ready
|
||||
Log.info("Checking Postgres is running");
|
||||
Awaitility.await()
|
||||
.atMost(Duration.ofSeconds(60))
|
||||
.pollDelay(Duration.ofSeconds(2))
|
||||
.untilAsserted(() -> assertThat(k8sclient.apps().statefulSets().inNamespace(namespace).withName("postgresql-db").get().getStatus().getReadyReplicas()).isEqualTo(1));
|
||||
// CR
|
||||
Log.info("Creating new Keycloak CR example");
|
||||
k8sclient.load(OperatorE2EIT.class.getResourceAsStream("/example-keycloak.yml")).inNamespace(namespace).createOrReplace();
|
||||
|
||||
// Check Operator has deployed Keycloak
|
||||
Log.info("Checking Operator has deployed Keycloak deployment");
|
||||
Awaitility.await()
|
||||
.atMost(Duration.ofSeconds(60))
|
||||
.pollDelay(Duration.ofSeconds(2))
|
||||
.untilAsserted(() -> assertThat(k8sclient.apps().deployments().inNamespace(namespace).withName("example-kc").get()).isNotNull());
|
||||
|
||||
// Check Keycloak has status ready
|
||||
StringBuffer podlog = new StringBuffer();
|
||||
try {
|
||||
Log.info("Checking Keycloak pod has ready replicas == 1");
|
||||
Awaitility.await()
|
||||
.atMost(Duration.ofSeconds(180))
|
||||
.pollDelay(Duration.ofSeconds(5))
|
||||
.untilAsserted(() -> {
|
||||
podlog.delete(0, podlog.length());
|
||||
try {
|
||||
k8sclient.pods().inNamespace(namespace).list().getItems().stream()
|
||||
.filter(a -> a.getMetadata().getName().startsWith("example-kc"))
|
||||
.forEach(a -> podlog.append(a.getMetadata().getName()).append(" : ")
|
||||
.append(k8sclient.pods().inNamespace(namespace).withName(a.getMetadata().getName()).getLog(true)));
|
||||
} catch (Exception e) {
|
||||
// swallowing exception bc the pod is not ready to give logs yet
|
||||
}
|
||||
assertThat(k8sclient.apps().deployments().inNamespace(namespace).withName("example-kc").get().getStatus().getReadyReplicas()).isEqualTo(1);
|
||||
});
|
||||
} catch (ConditionTimeoutException e) {
|
||||
Log.error("On error POD LOG " + podlog, e);
|
||||
throw e;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* 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.utils;
|
||||
|
||||
import org.keycloak.operator.v2alpha1.crds.Keycloak;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* @author Vaclav Muzikar <vmuzikar@redhat.com>
|
||||
*/
|
||||
public final class CRAssert {
|
||||
public static void assertKeycloakStatusCondition(Keycloak kc, String condition, boolean status) {
|
||||
assertThat(kc.getStatus().getConditions().stream()
|
||||
.anyMatch(c -> c.getType().equals(condition) && c.getStatus() == status)).isTrue();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* 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.utils;
|
||||
|
||||
import io.fabric8.kubernetes.client.KubernetesClient;
|
||||
import io.fabric8.kubernetes.client.utils.Serialization;
|
||||
import io.quarkus.logging.Log;
|
||||
import org.awaitility.Awaitility;
|
||||
import org.keycloak.operator.v2alpha1.crds.Keycloak;
|
||||
import org.keycloak.operator.v2alpha1.crds.KeycloakStatusCondition;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* @author Vaclav Muzikar <vmuzikar@redhat.com>
|
||||
*/
|
||||
public final class K8sUtils {
|
||||
public static <T> T getResourceFromFile(String fileName) {
|
||||
return Serialization.unmarshal(Objects.requireNonNull(K8sUtils.class.getResourceAsStream("/" + fileName)), Collections.emptyMap());
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static <T> T getResourceFromMultiResourceFile(String fileName, int index) {
|
||||
return ((List<T>) getResourceFromFile(fileName)).get(index);
|
||||
}
|
||||
|
||||
public static Keycloak getDefaultKeycloakDeployment() {
|
||||
return getResourceFromMultiResourceFile("example-keycloak.yml", 0);
|
||||
}
|
||||
|
||||
public static void deployKeycloak(KubernetesClient client, Keycloak kc, boolean waitUntilReady) {
|
||||
client.resources(Keycloak.class).createOrReplace(kc);
|
||||
|
||||
if (waitUntilReady) {
|
||||
waitForKeycloakToBeReady(client, kc);
|
||||
}
|
||||
}
|
||||
|
||||
public static void deployDefaultKeycloak(KubernetesClient client) {
|
||||
deployKeycloak(client, getDefaultKeycloakDeployment(), true);
|
||||
}
|
||||
|
||||
public static void waitForKeycloakToBeReady(KubernetesClient client, Keycloak kc) {
|
||||
Log.infof("Waiting for Keycloak \"%s\"", kc.getMetadata().getName());
|
||||
Awaitility.await()
|
||||
.ignoreExceptions()
|
||||
.untilAsserted(() -> {
|
||||
var currentKc = client.resources(Keycloak.class).withName(kc.getMetadata().getName()).get();
|
||||
CRAssert.assertKeycloakStatusCondition(currentKc, KeycloakStatusCondition.READY, true);
|
||||
CRAssert.assertKeycloakStatusCondition(currentKc, KeycloakStatusCondition.HAS_ERRORS, false);
|
||||
});
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue