Add Pod-Template to the Keycloak Deployment Spec (#10098)

This commit is contained in:
Andrea Peruffo 2022-03-02 07:13:57 +00:00 committed by GitHub
parent e2f8e9a4c8
commit f20cdd6d2a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 714 additions and 6 deletions

View file

@ -22,9 +22,12 @@ 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.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.PodTemplateSpec;
import io.fabric8.kubernetes.api.model.ResourceRequirements;
import io.fabric8.kubernetes.api.model.apps.Deployment; import io.fabric8.kubernetes.api.model.apps.Deployment;
import io.fabric8.kubernetes.api.model.apps.DeploymentBuilder; import io.fabric8.kubernetes.api.model.apps.DeploymentBuilder;
import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.KubernetesClient;
import io.fabric8.kubernetes.client.utils.Serialization;
import io.quarkus.logging.Log; import io.quarkus.logging.Log;
import org.keycloak.operator.Config; import org.keycloak.operator.Config;
import org.keycloak.operator.Constants; import org.keycloak.operator.Constants;
@ -32,11 +35,13 @@ import org.keycloak.operator.OperatorManagedResource;
import org.keycloak.operator.v2alpha1.crds.Keycloak; import org.keycloak.operator.v2alpha1.crds.Keycloak;
import org.keycloak.operator.v2alpha1.crds.KeycloakStatusBuilder; import org.keycloak.operator.v2alpha1.crds.KeycloakStatusBuilder;
import java.net.URL;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.function.Consumer;
import java.util.stream.Collectors; import java.util.stream.Collectors;
public class KeycloakDeployment extends OperatorManagedResource { public class KeycloakDeployment extends OperatorManagedResource {
@ -66,7 +71,7 @@ public class KeycloakDeployment extends OperatorManagedResource {
} }
@Override @Override
protected Optional<HasMetadata> getReconciledResource() { public Optional<HasMetadata> getReconciledResource() {
Deployment baseDeployment = new DeploymentBuilder(this.baseDeployment).build(); // clone not to change the base template Deployment baseDeployment = new DeploymentBuilder(this.baseDeployment).build(); // clone not to change the base template
Deployment reconciledDeployment; Deployment reconciledDeployment;
if (existingDeployment == null) { if (existingDeployment == null) {
@ -146,9 +151,211 @@ public class KeycloakDeployment extends OperatorManagedResource {
baseDeployment.getSpec().getTemplate().getSpec().setInitContainers(Collections.singletonList(initContainer)); baseDeployment.getSpec().getTemplate().getSpec().setInitContainers(Collections.singletonList(initContainer));
} }
public void validatePodTemplate(KeycloakStatusBuilder status) {
if (keycloakCR.getSpec() == null ||
keycloakCR.getSpec().getUnsupported() == null ||
keycloakCR.getSpec().getUnsupported().getPodTemplate() == null) {
return;
}
var overlayTemplate = this.keycloakCR.getSpec().getUnsupported().getPodTemplate();
if (overlayTemplate.getMetadata() != null &&
overlayTemplate.getMetadata().getName() != null) {
status.addWarningMessage("The name of the podTemplate cannot be modified");
}
if (overlayTemplate.getMetadata() != null &&
overlayTemplate.getMetadata().getNamespace() != null) {
status.addWarningMessage("The namespace of the podTemplate cannot be modified");
}
if (overlayTemplate.getSpec() != null &&
overlayTemplate.getSpec().getContainers() != null &&
overlayTemplate.getSpec().getContainers().get(0) != null &&
overlayTemplate.getSpec().getContainers().get(0).getName() != null) {
status.addWarningMessage("The name of the keycloak container cannot be modified");
}
if (overlayTemplate.getSpec() != null &&
overlayTemplate.getSpec().getContainers() != null &&
overlayTemplate.getSpec().getContainers().get(0) != null &&
overlayTemplate.getSpec().getContainers().get(0).getImage() != null) {
status.addWarningMessage("The image of the keycloak container cannot be modified using podTemplate");
}
}
private <T, V> void mergeMaps(Map<T, V> map1, Map<T, V> map2, Consumer<Map<T, V>> consumer) {
var map = new HashMap<T, V>();
Optional.ofNullable(map1).ifPresent(e -> map.putAll(e));
Optional.ofNullable(map2).ifPresent(e -> map.putAll(e));
consumer.accept(map);
}
private <T> void mergeLists(List<T> list1, List<T> list2, Consumer<List<T>> consumer) {
var list = new ArrayList<T>();
Optional.ofNullable(list1).ifPresent(e -> list.addAll(e));
Optional.ofNullable(list2).ifPresent(e -> list.addAll(e));
consumer.accept(list);
}
private <T> void mergeField(T value, Consumer<T> consumer) {
if (value != null && (!(value instanceof List) || ((List<?>) value).size() > 0)) {
consumer.accept(value);
}
}
private void mergePodTemplate(PodTemplateSpec baseTemplate) {
if (keycloakCR.getSpec() == null ||
keycloakCR.getSpec().getUnsupported() == null ||
keycloakCR.getSpec().getUnsupported().getPodTemplate() == null) {
return;
}
var overlayTemplate = keycloakCR.getSpec().getUnsupported().getPodTemplate();
mergeMaps(
Optional.ofNullable(baseTemplate.getMetadata()).map(m -> m.getLabels()).orElse(null),
Optional.ofNullable(overlayTemplate.getMetadata()).map(m -> m.getLabels()).orElse(null),
labels -> baseTemplate.getMetadata().setLabels(labels));
mergeMaps(
Optional.ofNullable(baseTemplate.getMetadata()).map(m -> m.getAnnotations()).orElse(null),
Optional.ofNullable(overlayTemplate.getMetadata()).map(m -> m.getAnnotations()).orElse(null),
annotations -> baseTemplate.getMetadata().setAnnotations(annotations));
var baseSpec = baseTemplate.getSpec();
var overlaySpec = overlayTemplate.getSpec();
var containers = new ArrayList<Container>();
var overlayContainers =
(overlaySpec == null || overlaySpec.getContainers() == null) ?
new ArrayList<Container>() : overlaySpec.getContainers();
if (overlayContainers.size() >= 1) {
var keycloakBaseContainer = baseSpec.getContainers().get(0);
var keycloakOverlayContainer = overlayContainers.get(0);
mergeField(keycloakOverlayContainer.getCommand(), v -> keycloakBaseContainer.setCommand(v));
mergeField(keycloakOverlayContainer.getReadinessProbe(), v -> keycloakBaseContainer.setReadinessProbe(v));
mergeField(keycloakOverlayContainer.getLivenessProbe(), v -> keycloakBaseContainer.setLivenessProbe(v));
mergeField(keycloakOverlayContainer.getStartupProbe(), v -> keycloakBaseContainer.setStartupProbe(v));
mergeField(keycloakOverlayContainer.getArgs(), v -> keycloakBaseContainer.setArgs(v));
mergeField(keycloakOverlayContainer.getImagePullPolicy(), v -> keycloakBaseContainer.setImagePullPolicy(v));
mergeField(keycloakOverlayContainer.getLifecycle(), v -> keycloakBaseContainer.setLifecycle(v));
mergeField(keycloakOverlayContainer.getSecurityContext(), v -> keycloakBaseContainer.setSecurityContext(v));
mergeField(keycloakOverlayContainer.getWorkingDir(), v -> keycloakBaseContainer.setWorkingDir(v));
var resources = new ResourceRequirements();
mergeMaps(
Optional.ofNullable(keycloakBaseContainer.getResources()).map(r -> r.getRequests()).orElse(null),
Optional.ofNullable(keycloakOverlayContainer.getResources()).map(r -> r.getRequests()).orElse(null),
requests -> resources.setRequests(requests));
mergeMaps(
Optional.ofNullable(keycloakBaseContainer.getResources()).map(l -> l.getLimits()).orElse(null),
Optional.ofNullable(keycloakOverlayContainer.getResources()).map(l -> l.getLimits()).orElse(null),
limits -> resources.setLimits(limits));
keycloakBaseContainer.setResources(resources);
mergeLists(
keycloakBaseContainer.getPorts(),
keycloakOverlayContainer.getPorts(),
p -> keycloakBaseContainer.setPorts(p));
mergeLists(
keycloakBaseContainer.getEnvFrom(),
keycloakOverlayContainer.getEnvFrom(),
e -> keycloakBaseContainer.setEnvFrom(e));
mergeLists(
keycloakBaseContainer.getEnv(),
keycloakOverlayContainer.getEnv(),
e -> keycloakBaseContainer.setEnv(e));
mergeLists(
keycloakBaseContainer.getVolumeMounts(),
keycloakOverlayContainer.getVolumeMounts(),
vm -> keycloakBaseContainer.setVolumeMounts(vm));
mergeLists(
keycloakBaseContainer.getVolumeDevices(),
keycloakOverlayContainer.getVolumeDevices(),
vd -> keycloakBaseContainer.setVolumeDevices(vd));
containers.add(keycloakBaseContainer);
// Skip keycloak container and add the rest
for (int i = 1; i < overlayContainers.size(); i++) {
containers.add(overlayContainers.get(i));
}
baseSpec.setContainers(containers);
}
if (overlaySpec != null) {
mergeField(overlaySpec.getActiveDeadlineSeconds(), ads -> baseSpec.setActiveDeadlineSeconds(ads));
mergeField(overlaySpec.getAffinity(), a -> baseSpec.setAffinity(a));
mergeField(overlaySpec.getAutomountServiceAccountToken(), a -> baseSpec.setAutomountServiceAccountToken(a));
mergeField(overlaySpec.getDnsConfig(), dc -> baseSpec.setDnsConfig(dc));
mergeField(overlaySpec.getDnsPolicy(), dp -> baseSpec.setDnsPolicy(dp));
mergeField(overlaySpec.getEnableServiceLinks(), esl -> baseSpec.setEnableServiceLinks(esl));
mergeField(overlaySpec.getHostIPC(), h -> baseSpec.setHostIPC(h));
mergeField(overlaySpec.getHostname(), h -> baseSpec.setHostname(h));
mergeField(overlaySpec.getHostNetwork(), h -> baseSpec.setHostNetwork(h));
mergeField(overlaySpec.getHostPID(), h -> baseSpec.setHostPID(h));
mergeField(overlaySpec.getNodeName(), n -> baseSpec.setNodeName(n));
mergeField(overlaySpec.getNodeSelector(), ns -> baseSpec.setNodeSelector(ns));
mergeField(overlaySpec.getPreemptionPolicy(), pp -> baseSpec.setPreemptionPolicy(pp));
mergeField(overlaySpec.getPriority(), p -> baseSpec.setPriority(p));
mergeField(overlaySpec.getPriorityClassName(), pcn -> baseSpec.setPriorityClassName(pcn));
mergeField(overlaySpec.getRestartPolicy(), rp -> baseSpec.setRestartPolicy(rp));
mergeField(overlaySpec.getRuntimeClassName(), rcn -> baseSpec.setRuntimeClassName(rcn));
mergeField(overlaySpec.getSchedulerName(), sn -> baseSpec.setSchedulerName(sn));
mergeField(overlaySpec.getSecurityContext(), sc -> baseSpec.setSecurityContext(sc));
mergeField(overlaySpec.getServiceAccount(), sa -> baseSpec.setServiceAccount(sa));
mergeField(overlaySpec.getServiceAccountName(), san -> baseSpec.setServiceAccountName(san));
mergeField(overlaySpec.getSetHostnameAsFQDN(), h -> baseSpec.setSetHostnameAsFQDN(h));
mergeField(overlaySpec.getShareProcessNamespace(), spn -> baseSpec.setShareProcessNamespace(spn));
mergeField(overlaySpec.getSubdomain(), s -> baseSpec.setSubdomain(s));
mergeField(overlaySpec.getTerminationGracePeriodSeconds(), t -> baseSpec.setTerminationGracePeriodSeconds(t));
mergeLists(
baseSpec.getImagePullSecrets(),
overlaySpec.getImagePullSecrets(),
ips -> baseSpec.setImagePullSecrets(ips));
mergeLists(
baseSpec.getHostAliases(),
overlaySpec.getHostAliases(),
ha -> baseSpec.setHostAliases(ha));
mergeLists(
baseSpec.getEphemeralContainers(),
overlaySpec.getEphemeralContainers(),
ec -> baseSpec.setEphemeralContainers(ec));
mergeLists(
baseSpec.getInitContainers(),
overlaySpec.getInitContainers(),
ic -> baseSpec.setInitContainers(ic));
mergeLists(
baseSpec.getReadinessGates(),
overlaySpec.getReadinessGates(),
rg -> baseSpec.setReadinessGates(rg));
mergeLists(
baseSpec.getTolerations(),
overlaySpec.getTolerations(),
t -> baseSpec.setTolerations(t));
mergeLists(
baseSpec.getTopologySpreadConstraints(),
overlaySpec.getTopologySpreadConstraints(),
tpc -> baseSpec.setTopologySpreadConstraints(tpc));
mergeLists(
baseSpec.getVolumes(),
overlaySpec.getVolumes(),
v -> baseSpec.setVolumes(v));
mergeMaps(
baseSpec.getOverhead(),
overlaySpec.getOverhead(),
o -> baseSpec.setOverhead(o));
}
}
private Deployment createBaseDeployment() { private Deployment createBaseDeployment() {
URL url = this.getClass().getResource("/base-keycloak-deployment.yaml"); var is = this.getClass().getResourceAsStream("/base-keycloak-deployment.yaml");
Deployment baseDeployment = client.apps().deployments().load(url).get(); Deployment baseDeployment = Serialization.unmarshal(is, Deployment.class);
baseDeployment.getMetadata().setName(getName()); baseDeployment.getMetadata().setName(getName());
baseDeployment.getMetadata().setNamespace(getNamespace()); baseDeployment.getMetadata().setNamespace(getNamespace());
@ -171,6 +378,7 @@ public class KeycloakDeployment extends OperatorManagedResource {
.collect(Collectors.toList())); .collect(Collectors.toList()));
addInitContainer(baseDeployment, keycloakCR.getSpec().getExtensions()); addInitContainer(baseDeployment, keycloakCR.getSpec().getExtensions());
mergePodTemplate(baseDeployment.getSpec().getTemplate());
// Set<String> configSecretsNames = new HashSet<>(); // Set<String> configSecretsNames = new HashSet<>();
// List<EnvVar> configEnvVars = serverConfig.entrySet().stream() // List<EnvVar> configEnvVars = serverConfig.entrySet().stream()
@ -199,6 +407,7 @@ public class KeycloakDeployment extends OperatorManagedResource {
} }
public void updateStatus(KeycloakStatusBuilder status) { public void updateStatus(KeycloakStatusBuilder 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 Deployment found, waiting for creating a new one");
return; return;

View file

@ -18,6 +18,8 @@ package org.keycloak.operator.v2alpha1.crds;
import com.fasterxml.jackson.annotation.JsonPropertyDescription; import com.fasterxml.jackson.annotation.JsonPropertyDescription;
import org.keycloak.operator.v2alpha1.crds.keycloakspec.Unsupported;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -26,9 +28,12 @@ public class KeycloakSpec {
private int instances = 1; private int instances = 1;
private String image; private String image;
private Map<String, String> serverConfiguration; private Map<String, String> serverConfiguration;
@JsonPropertyDescription("List of URLs to download Keycloak extensions.") @JsonPropertyDescription("List of URLs to download Keycloak extensions.")
private List<String> extensions; private List<String> extensions;
@JsonPropertyDescription(
"In this section you can configure podTemplate advanced features, not production-ready, and not supported settings.\n" +
"Use at your own risk and open an issue with your use-case if you don't find an alternative way.")
private Unsupported unsupported;
public List<String> getExtensions() { public List<String> getExtensions() {
return extensions; return extensions;
@ -38,6 +43,14 @@ public class KeycloakSpec {
this.extensions = extensions; this.extensions = extensions;
} }
public Unsupported getUnsupported() {
return unsupported;
}
public void setUnsupported(Unsupported unsupported) {
this.unsupported = unsupported;
}
public int getInstances() { public int getInstances() {
return instances; return instances;
} }

View file

@ -52,6 +52,11 @@ public class KeycloakStatusBuilder {
return this; return this;
} }
public KeycloakStatusBuilder addWarningMessage(String message) {
errorMessages.add("warning: " + message);
return this;
}
public KeycloakStatus build() { public KeycloakStatus build() {
readyCondition.setMessage(String.join("\n", notReadyMessages)); readyCondition.setMessage(String.join("\n", notReadyMessages));
hasErrorsCondition.setMessage(String.join("\n", errorMessages)); hasErrorsCondition.setMessage(String.join("\n", errorMessages));

View file

@ -0,0 +1,34 @@
package org.keycloak.operator.v2alpha1.crds.keycloakspec;
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
import io.fabric8.kubernetes.api.model.PodTemplateSpec;
import io.sundr.builder.annotations.Buildable;
import io.sundr.builder.annotations.BuildableReference;
@Buildable(editableEnabled = false, builderPackage = "io.fabric8.kubernetes.api.builder",
lazyCollectionInitEnabled = false, refs = {
@BuildableReference(io.fabric8.kubernetes.api.model.ObjectMeta.class),
@BuildableReference(io.fabric8.kubernetes.api.model.PodTemplateSpec.class)
})
public class Unsupported {
@JsonPropertyDescription("You can configure that will be merged with the one configured by default by the operator.\n" +
"Use at your own risk, we reserve the possibility to remove/change the way any field gets merged in future releases without notice.\n" +
"Reference: https://kubernetes.io/docs/concepts/workloads/pods/#pod-templates")
private PodTemplateSpec podTemplate;
public Unsupported() {}
public Unsupported(PodTemplateSpec podTemplate) {
this.podTemplate = podTemplate;
}
public PodTemplateSpec getPodTemplate() {
return podTemplate;
}
public void setPodTeplate(PodTemplateSpec podTemplate) {
this.podTemplate = podTemplate;
}
}

View file

@ -0,0 +1,167 @@
package org.keycloak.operator;
import io.fabric8.kubernetes.api.model.PodTemplateSpecBuilder;
import io.fabric8.kubernetes.client.dsl.Resource;
import io.fabric8.kubernetes.client.utils.Serialization;
import io.quarkus.logging.Log;
import io.quarkus.test.junit.QuarkusTest;
import org.awaitility.Awaitility;
import org.junit.jupiter.api.Test;
import org.keycloak.operator.utils.CRAssert;
import org.keycloak.operator.v2alpha1.crds.Keycloak;
import static java.util.concurrent.TimeUnit.MINUTES;
import static org.assertj.core.api.Assertions.assertThat;
import static org.keycloak.operator.v2alpha1.crds.KeycloakStatusCondition.HAS_ERRORS;
@QuarkusTest
public class PodTemplateE2EIT extends ClusterOperatorTest {
private Keycloak getEmptyPodTemplateKeycloak() {
return Serialization.unmarshal(getClass().getResourceAsStream("/empty-podtemplate-keycloak.yml"), Keycloak.class);
}
private Resource<Keycloak> getCrSelector() {
return k8sclient
.resources(Keycloak.class)
.inNamespace(namespace)
.withName("example-podtemplate");
}
@Test
public void testPodTemplateIsMerged() {
// Arrange
var keycloakWithPodTemplate = k8sclient
.load(getClass().getResourceAsStream("/correct-podtemplate-keycloak.yml"));
// Act
keycloakWithPodTemplate.createOrReplace();
// Assert
Awaitility
.await()
.ignoreExceptions()
.atMost(3, MINUTES).untilAsserted(() -> {
Log.info("Getting logs from Keycloak");
var keycloakPod = k8sclient
.pods()
.inNamespace(namespace)
.withLabel("app", "keycloak")
.list()
.getItems()
.get(0);
var logs = k8sclient
.pods()
.inNamespace(namespace)
.withName(keycloakPod.getMetadata().getName())
.getLog();
Log.info("Full logs are:\n" + logs);
assertThat(logs).contains("Hello World");
assertThat(keycloakPod.getMetadata().getLabels().get("foo")).isEqualTo("bar");
});
}
@Test
public void testPodTemplateIncorrectName() {
// Arrange
var plainKc = getEmptyPodTemplateKeycloak();
var podTemplate = new PodTemplateSpecBuilder()
.withNewMetadata()
.withName("foo")
.endMetadata()
.build();
plainKc.getSpec().getUnsupported().setPodTeplate(podTemplate);
// Act
k8sclient.resource(plainKc).createOrReplace();
// Assert
Log.info("Getting status of Keycloak");
Awaitility
.await()
.ignoreExceptions()
.atMost(3, MINUTES).untilAsserted(() -> {
CRAssert.assertKeycloakStatusCondition(getCrSelector().get(), HAS_ERRORS, false, "cannot be modified");
});
}
@Test
public void testPodTemplateIncorrectNamespace() {
// Arrange
var plainKc = getEmptyPodTemplateKeycloak();
var podTemplate = new PodTemplateSpecBuilder()
.withNewMetadata()
.withNamespace("bar")
.endMetadata()
.build();
plainKc.getSpec().getUnsupported().setPodTeplate(podTemplate);
// Act
k8sclient.resource(plainKc).createOrReplace();
// Assert
Log.info("Getting status of Keycloak");
Awaitility
.await()
.ignoreExceptions()
.atMost(3, MINUTES).untilAsserted(() -> {
CRAssert.assertKeycloakStatusCondition(getCrSelector().get(), HAS_ERRORS, false, "cannot be modified");
});
}
@Test
public void testPodTemplateIncorrectContainerName() {
// Arrange
var plainKc = getEmptyPodTemplateKeycloak();
var podTemplate = new PodTemplateSpecBuilder()
.withNewSpec()
.addNewContainer()
.withName("baz")
.endContainer()
.endSpec()
.build();
plainKc.getSpec().getUnsupported().setPodTeplate(podTemplate);
// Act
k8sclient.resource(plainKc).createOrReplace();
// Assert
Log.info("Getting status of Keycloak");
Awaitility
.await()
.ignoreExceptions()
.atMost(3, MINUTES).untilAsserted(() -> {
CRAssert.assertKeycloakStatusCondition(getCrSelector().get(), HAS_ERRORS, false, "cannot be modified");
});
}
@Test
public void testPodTemplateIncorrectDockerImage() {
// Arrange
var plainKc = getEmptyPodTemplateKeycloak();
var podTemplate = new PodTemplateSpecBuilder()
.withNewSpec()
.addNewContainer()
.withImage("foo")
.endContainer()
.endSpec()
.build();
plainKc.getSpec().getUnsupported().setPodTeplate(podTemplate);
// Act
k8sclient.resource(plainKc).createOrReplace();
// Assert
Log.info("Getting status of Keycloak");
Awaitility
.await()
.ignoreExceptions()
.atMost(3, MINUTES).untilAsserted(() -> {
CRAssert.assertKeycloakStatusCondition(getCrSelector().get(), HAS_ERRORS, false, "cannot be modified");
});
}
}

View file

@ -0,0 +1,224 @@
package org.keycloak.operator;
import io.fabric8.kubernetes.api.model.IntOrString;
import io.fabric8.kubernetes.api.model.PodTemplateSpec;
import io.fabric8.kubernetes.api.model.PodTemplateSpecBuilder;
import io.fabric8.kubernetes.api.model.ProbeBuilder;
import io.fabric8.kubernetes.api.model.apps.Deployment;
import io.quarkus.test.junit.QuarkusTest;
import org.junit.jupiter.api.Test;
import org.keycloak.operator.v2alpha1.KeycloakDeployment;
import org.keycloak.operator.v2alpha1.crds.Keycloak;
import org.keycloak.operator.v2alpha1.crds.KeycloakSpec;
import org.keycloak.operator.v2alpha1.crds.keycloakspec.Unsupported;
import java.net.URL;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
@QuarkusTest
public class PodTemplateTest {
Deployment getDeployment(PodTemplateSpec podTemplate) {
var config = new Config(){
@Override
public Keycloak keycloak() {
return new Keycloak() {
@Override
public String image() {
return "dummy-image";
}
@Override
public String imagePullPolicy() {
return "Never";
}
@Override
public String initContainerImage() { return "quay.io/keycloak/keycloak-init-container:legacy"; }
@Override
public String initContainerImagePullPolicy() { return "Always"; }
};
}
};
var kc = new Keycloak();
var spec = new KeycloakSpec();
spec.setUnsupported(new Unsupported(podTemplate));
kc.setSpec(spec);
var deployment = new KeycloakDeployment(null, config, kc, new Deployment());
return (Deployment) deployment.getReconciledResource().get();
}
@Test
public void testEmpty() {
// Arrange
PodTemplateSpec additionalPodTemplate = null;
// Act
var podTemplate = getDeployment(additionalPodTemplate).getSpec().getTemplate();
// Assert
assertEquals("keycloak", podTemplate.getSpec().getContainers().get(0).getName());
}
@Test
public void testMetadataIsMerged() {
// Arrange
var additionalPodTemplate = new PodTemplateSpecBuilder()
.withNewMetadata()
.addToLabels("one", "1")
.addToAnnotations("two", "2")
.endMetadata()
.build();
// Act
var podTemplate = getDeployment(additionalPodTemplate).getSpec().getTemplate();
// Assert
assertTrue(podTemplate.getMetadata().getLabels().containsKey("one"));
assertTrue(podTemplate.getMetadata().getLabels().containsValue("1"));
assertTrue(podTemplate.getMetadata().getAnnotations().containsKey("two"));
assertTrue(podTemplate.getMetadata().getAnnotations().containsValue("2"));
}
@Test
public void testVolumesAreMerged() {
// Arrange
var volumeName = "foo-volume";
var additionalPodTemplate = new PodTemplateSpecBuilder()
.withNewSpec()
.addNewVolume()
.withName("foo-volume")
.withNewEmptyDir()
.endEmptyDir()
.endVolume()
.endSpec()
.build();
// Act
var podTemplate = getDeployment(additionalPodTemplate).getSpec().getTemplate();
// Assert
assertEquals(volumeName, podTemplate.getSpec().getVolumes().get(0).getName());
}
@Test
public void testVolumeMountsAreMerged() {
// Arrange
var volumeMountName = "foo";
var volumeMountPath = "/mnt/path";
var additionalPodTemplate = new PodTemplateSpecBuilder()
.withNewSpec()
.addNewContainer()
.addNewVolumeMount()
.withName(volumeMountName)
.withMountPath(volumeMountPath)
.endVolumeMount()
.endContainer()
.endSpec()
.build();
// Act
var podTemplate = getDeployment(additionalPodTemplate).getSpec().getTemplate();
// Assert
assertEquals(volumeMountName, podTemplate.getSpec().getContainers().get(0).getVolumeMounts().get(0).getName());
assertEquals(volumeMountPath, podTemplate.getSpec().getContainers().get(0).getVolumeMounts().get(0).getMountPath());
}
@Test
public void testCommandsAndArgsAreMerged() {
// Arrange
var command = "foo";
var arg = "bar";
var additionalPodTemplate = new PodTemplateSpecBuilder()
.withNewSpec()
.addNewContainer()
.withCommand(command)
.withArgs(arg)
.endContainer()
.endSpec()
.build();
// Act
var podTemplate = getDeployment(additionalPodTemplate).getSpec().getTemplate();
// Assert
assertEquals(1, podTemplate.getSpec().getContainers().get(0).getCommand().size());
assertEquals(command, podTemplate.getSpec().getContainers().get(0).getCommand().get(0));
assertEquals(1, podTemplate.getSpec().getContainers().get(0).getArgs().size());
assertEquals(arg, podTemplate.getSpec().getContainers().get(0).getArgs().get(0));
}
@Test
public void testProbesAreMerged() {
// Arrange
var ready = new ProbeBuilder()
.withNewExec()
.withCommand("foo")
.endExec()
.withFailureThreshold(1)
.withInitialDelaySeconds(2)
.withTimeoutSeconds(3)
.build();
var live = new ProbeBuilder()
.withNewHttpGet()
.withPort(new IntOrString(1000))
.withScheme("UDP")
.withPath("/foo")
.endHttpGet()
.withFailureThreshold(4)
.withInitialDelaySeconds(5)
.withTimeoutSeconds(6)
.build();
var additionalPodTemplate = new PodTemplateSpecBuilder()
.withNewSpec()
.addNewContainer()
.withReadinessProbe(ready)
.withLivenessProbe(live)
.endContainer()
.endSpec()
.build();
// Act
var podTemplate = getDeployment(additionalPodTemplate).getSpec().getTemplate();
// Assert
var readyProbe = podTemplate.getSpec().getContainers().get(0).getReadinessProbe();
var liveProbe = podTemplate.getSpec().getContainers().get(0).getLivenessProbe();
assertEquals("foo", ready.getExec().getCommand().get(0));
assertEquals(1, readyProbe.getFailureThreshold());
assertEquals(2, readyProbe.getInitialDelaySeconds());
assertEquals(3, readyProbe.getTimeoutSeconds());
assertEquals(1000, liveProbe.getHttpGet().getPort().getIntVal());
assertEquals("UDP", liveProbe.getHttpGet().getScheme());
assertEquals("/foo", liveProbe.getHttpGet().getPath());
assertEquals(4, liveProbe.getFailureThreshold());
assertEquals(5, liveProbe.getInitialDelaySeconds());
assertEquals(6, liveProbe.getTimeoutSeconds());
}
@Test
public void testEnvVarsAreMerged() {
// Arrange
var env = "KC_SOMETHING";
var value = "some-value";
var additionalPodTemplate = new PodTemplateSpecBuilder()
.withNewSpec()
.addNewContainer()
.addNewEnv()
.withName(env)
.withValue(value)
.endEnv()
.endContainer()
.endSpec()
.build();
// Act
var podTemplate = getDeployment(additionalPodTemplate).getSpec().getTemplate();
// Assert
var envVar = podTemplate.getSpec().getContainers().get(0).getEnv().stream().filter(e -> e.getName().equals(env)).findFirst().get();
assertEquals(env, envVar.getName());
assertEquals(value, envVar.getValue());
}
}

View file

@ -18,6 +18,7 @@
package org.keycloak.operator.utils; package org.keycloak.operator.utils;
import org.keycloak.operator.v2alpha1.crds.Keycloak; import org.keycloak.operator.v2alpha1.crds.Keycloak;
import org.keycloak.operator.v2alpha1.crds.KeycloakRealmImport;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
@ -25,8 +26,16 @@ import static org.assertj.core.api.Assertions.assertThat;
* @author Vaclav Muzikar <vmuzikar@redhat.com> * @author Vaclav Muzikar <vmuzikar@redhat.com>
*/ */
public final class CRAssert { public final class CRAssert {
public static void assertKeycloakStatusCondition(Keycloak kc, String condition, boolean status) { public static void assertKeycloakStatusCondition(Keycloak kc, String condition, boolean status) {
assertKeycloakStatusCondition(kc, condition, status, null);
}
public static void assertKeycloakStatusCondition(Keycloak kc, String condition, boolean status, String containedMessage) {
assertThat(kc.getStatus().getConditions().stream() assertThat(kc.getStatus().getConditions().stream()
.anyMatch(c -> c.getType().equals(condition) && c.getStatus() == status)).isTrue(); .anyMatch(c ->
c.getType().equals(condition) &&
c.getStatus() == status &&
(containedMessage == null || c.getMessage().contains(containedMessage)))
).isTrue();
} }
} }

View file

@ -0,0 +1,34 @@
apiVersion: keycloak.org/v2alpha1
kind: Keycloak
metadata:
name: example-podtemplate-kc
spec:
instances: 1
serverConfiguration:
KC_DB: postgres
KC_DB_URL_HOST: postgres-db
KC_DB_USERNAME: postgres
KC_DB_PASSWORD: testpassword
unsupported:
podTemplate:
metadata:
labels:
foo: "bar"
spec:
containers:
- volumeMounts:
- name: test-volume
mountPath: /mnt/test
command: [ "/bin/bash", "-c", "cat /mnt/test/test.txt && /opt/keycloak/bin/kc.sh start-dev" ]
volumes:
- name: test-volume
secret:
secretName: keycloak-podtemplate-secret
---
apiVersion: v1
kind: Secret
metadata:
name: keycloak-podtemplate-secret
data:
test.txt: "SGVsbG8gV29ybGQK" # Hello World
type: Opaque

View file

@ -0,0 +1,13 @@
apiVersion: keycloak.org/v2alpha1
kind: Keycloak
metadata:
name: example-podtemplate
spec:
instances: 1
serverConfiguration:
KC_DB: postgres
KC_DB_URL_HOST: postgres-db
KC_DB_USERNAME: postgres
KC_DB_PASSWORD: testpassword
unsupported:
podTemplate: