Add Pod-Template to the Keycloak Deployment Spec (#10098)
This commit is contained in:
parent
e2f8e9a4c8
commit
f20cdd6d2a
9 changed files with 714 additions and 6 deletions
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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));
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
34
operator/src/test/resources/correct-podtemplate-keycloak.yml
Normal file
34
operator/src/test/resources/correct-podtemplate-keycloak.yml
Normal 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
|
13
operator/src/test/resources/empty-podtemplate-keycloak.yml
Normal file
13
operator/src/test/resources/empty-podtemplate-keycloak.yml
Normal 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:
|
Loading…
Reference in a new issue