Tests for Keycloak Deployment

This commit is contained in:
Václav Muzikář 2022-02-09 12:23:45 +01:00 committed by Bruno Oliveira da Silva
parent d9f1a9b207
commit cfddcad3c5
5 changed files with 274 additions and 68 deletions

View file

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

View file

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

View file

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

View file

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

View file

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