parent
6416db2d67
commit
7311e12066
8 changed files with 541 additions and 164 deletions
|
@ -18,9 +18,12 @@
|
||||||
package org.keycloak.common.util;
|
package org.keycloak.common.util;
|
||||||
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:jeroen.rosenberg@gmail.com">Jeroen Rosenberg</a>
|
* @author <a href="mailto:jeroen.rosenberg@gmail.com">Jeroen Rosenberg</a>
|
||||||
|
@ -75,4 +78,15 @@ public class CollectionUtil {
|
||||||
public static boolean isNotEmpty(Collection<?> collection) {
|
public static boolean isNotEmpty(Collection<?> collection) {
|
||||||
return !isEmpty(collection);
|
return !isEmpty(collection);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static <T> Set<T> intersection(Collection<T> col1, Collection<T> col2) {
|
||||||
|
if (isEmpty(col1) || isEmpty(col2)) return Collections.emptySet();
|
||||||
|
|
||||||
|
final Collection<T> iteratorCollection = col1.size() <= col2.size() ? col1 : col2;
|
||||||
|
final Collection<T> searchCollection = iteratorCollection.equals(col1) ? col2 : col1;
|
||||||
|
|
||||||
|
return iteratorCollection.stream()
|
||||||
|
.filter(searchCollection::contains)
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,12 +20,9 @@ import io.fabric8.kubernetes.api.model.Container;
|
||||||
import io.fabric8.kubernetes.api.model.EnvVar;
|
import io.fabric8.kubernetes.api.model.EnvVar;
|
||||||
import io.fabric8.kubernetes.api.model.EnvVarBuilder;
|
import io.fabric8.kubernetes.api.model.EnvVarBuilder;
|
||||||
import io.fabric8.kubernetes.api.model.EnvVarSourceBuilder;
|
import io.fabric8.kubernetes.api.model.EnvVarSourceBuilder;
|
||||||
import io.fabric8.kubernetes.api.model.ExecActionBuilder;
|
|
||||||
import io.fabric8.kubernetes.api.model.HasMetadata;
|
import io.fabric8.kubernetes.api.model.HasMetadata;
|
||||||
import io.fabric8.kubernetes.api.model.PodTemplateSpec;
|
import io.fabric8.kubernetes.api.model.PodTemplateSpec;
|
||||||
import io.fabric8.kubernetes.api.model.ResourceRequirements;
|
import io.fabric8.kubernetes.api.model.ResourceRequirements;
|
||||||
import io.fabric8.kubernetes.api.model.VolumeBuilder;
|
|
||||||
import io.fabric8.kubernetes.api.model.VolumeMountBuilder;
|
|
||||||
import io.fabric8.kubernetes.api.model.apps.StatefulSet;
|
import io.fabric8.kubernetes.api.model.apps.StatefulSet;
|
||||||
import io.fabric8.kubernetes.api.model.apps.StatefulSetBuilder;
|
import io.fabric8.kubernetes.api.model.apps.StatefulSetBuilder;
|
||||||
import io.fabric8.kubernetes.client.KubernetesClient;
|
import io.fabric8.kubernetes.client.KubernetesClient;
|
||||||
|
@ -37,9 +34,7 @@ import org.keycloak.operator.crds.v2alpha1.deployment.Keycloak;
|
||||||
import org.keycloak.operator.crds.v2alpha1.deployment.KeycloakStatusBuilder;
|
import org.keycloak.operator.crds.v2alpha1.deployment.KeycloakStatusBuilder;
|
||||||
import org.keycloak.operator.crds.v2alpha1.deployment.ValueOrSecret;
|
import org.keycloak.operator.crds.v2alpha1.deployment.ValueOrSecret;
|
||||||
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Base64;
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
@ -53,7 +48,9 @@ import static io.smallrye.config.common.utils.StringUtil.replaceNonAlphanumericB
|
||||||
|
|
||||||
public class KeycloakDeployment extends OperatorManagedResource implements StatusUpdater<KeycloakStatusBuilder> {
|
public class KeycloakDeployment extends OperatorManagedResource implements StatusUpdater<KeycloakStatusBuilder> {
|
||||||
|
|
||||||
private final Config config;
|
private final Config operatorConfig;
|
||||||
|
private final KeycloakDeploymentConfig deploymentConfig;
|
||||||
|
|
||||||
private final Keycloak keycloakCR;
|
private final Keycloak keycloakCR;
|
||||||
private final StatefulSet existingDeployment;
|
private final StatefulSet existingDeployment;
|
||||||
private final StatefulSet baseDeployment;
|
private final StatefulSet baseDeployment;
|
||||||
|
@ -65,20 +62,20 @@ public class KeycloakDeployment extends OperatorManagedResource implements Statu
|
||||||
|
|
||||||
public KeycloakDeployment(KubernetesClient client, Config config, Keycloak keycloakCR, StatefulSet existingDeployment, String adminSecretName) {
|
public KeycloakDeployment(KubernetesClient client, Config config, Keycloak keycloakCR, StatefulSet existingDeployment, String adminSecretName) {
|
||||||
super(client, keycloakCR);
|
super(client, keycloakCR);
|
||||||
this.config = config;
|
this.operatorConfig = config;
|
||||||
this.keycloakCR = keycloakCR;
|
this.keycloakCR = keycloakCR;
|
||||||
this.adminSecretName = adminSecretName;
|
this.adminSecretName = adminSecretName;
|
||||||
|
|
||||||
if (existingDeployment != null) {
|
if (existingDeployment != null) {
|
||||||
Log.info("Existing Deployment provided by controller");
|
Log.info("Existing Deployment provided by controller");
|
||||||
this.existingDeployment = existingDeployment;
|
this.existingDeployment = existingDeployment;
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
Log.info("Trying to fetch existing Deployment from the API");
|
Log.info("Trying to fetch existing Deployment from the API");
|
||||||
this.existingDeployment = fetchExistingDeployment();
|
this.existingDeployment = fetchExistingDeployment();
|
||||||
}
|
}
|
||||||
|
|
||||||
baseDeployment = createBaseDeployment();
|
this.baseDeployment = createBaseDeployment();
|
||||||
|
this.deploymentConfig = createDeploymentConfig();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -329,149 +326,6 @@ public class KeycloakDeployment extends OperatorManagedResource implements Statu
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void configureHostname(StatefulSet deployment) {
|
|
||||||
var kcContainer = deployment.getSpec().getTemplate().getSpec().getContainers().get(0);
|
|
||||||
var hostname = this.keycloakCR.getSpec().getHostname();
|
|
||||||
var envVars = kcContainer.getEnv();
|
|
||||||
if (this.keycloakCR.getSpec().isHostnameDisabled()) {
|
|
||||||
var disableStrictHostname = List.of(
|
|
||||||
new EnvVarBuilder()
|
|
||||||
.withName("KC_HOSTNAME_STRICT")
|
|
||||||
.withValue("false")
|
|
||||||
.build(),
|
|
||||||
new EnvVarBuilder()
|
|
||||||
.withName("KC_HOSTNAME_STRICT_BACKCHANNEL")
|
|
||||||
.withValue("false")
|
|
||||||
.build());
|
|
||||||
|
|
||||||
envVars.addAll(disableStrictHostname);
|
|
||||||
} else {
|
|
||||||
var enabledStrictHostname = List.of(
|
|
||||||
new EnvVarBuilder()
|
|
||||||
.withName("KC_HOSTNAME")
|
|
||||||
.withValue(hostname)
|
|
||||||
.build());
|
|
||||||
|
|
||||||
envVars.addAll(enabledStrictHostname);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void configureTLS(StatefulSet deployment) {
|
|
||||||
var kcContainer = deployment.getSpec().getTemplate().getSpec().getContainers().get(0);
|
|
||||||
var tlsSecret = this.keycloakCR.getSpec().getTlsSecret();
|
|
||||||
var envVars = kcContainer.getEnv();
|
|
||||||
|
|
||||||
if (this.keycloakCR.getSpec().isHttp()) {
|
|
||||||
var disableTls = List.of(
|
|
||||||
new EnvVarBuilder()
|
|
||||||
.withName("KC_HTTP_ENABLED")
|
|
||||||
.withValue("true")
|
|
||||||
.build(),
|
|
||||||
new EnvVarBuilder()
|
|
||||||
.withName("KC_HOSTNAME_STRICT_HTTPS")
|
|
||||||
.withValue("false")
|
|
||||||
.build(),
|
|
||||||
new EnvVarBuilder()
|
|
||||||
.withName("KC_PROXY")
|
|
||||||
.withValue("edge")
|
|
||||||
.build());
|
|
||||||
|
|
||||||
envVars.addAll(disableTls);
|
|
||||||
} else {
|
|
||||||
var enabledTls = List.of(
|
|
||||||
new EnvVarBuilder()
|
|
||||||
.withName("KC_HTTPS_CERTIFICATE_FILE")
|
|
||||||
.withValue(Constants.CERTIFICATES_FOLDER + "/tls.crt")
|
|
||||||
.build(),
|
|
||||||
new EnvVarBuilder()
|
|
||||||
.withName("KC_HTTPS_CERTIFICATE_KEY_FILE")
|
|
||||||
.withValue(Constants.CERTIFICATES_FOLDER + "/tls.key")
|
|
||||||
.build(),
|
|
||||||
new EnvVarBuilder()
|
|
||||||
.withName("KC_PROXY")
|
|
||||||
.withValue("passthrough")
|
|
||||||
.build());
|
|
||||||
|
|
||||||
envVars.addAll(enabledTls);
|
|
||||||
|
|
||||||
var volume = new VolumeBuilder()
|
|
||||||
.withName("keycloak-tls-certificates")
|
|
||||||
.withNewSecret()
|
|
||||||
.withSecretName(tlsSecret)
|
|
||||||
.withOptional(false)
|
|
||||||
.endSecret()
|
|
||||||
.build();
|
|
||||||
|
|
||||||
var volumeMount = new VolumeMountBuilder()
|
|
||||||
.withName(volume.getName())
|
|
||||||
.withMountPath(Constants.CERTIFICATES_FOLDER)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
deployment.getSpec().getTemplate().getSpec().getVolumes().add(volume);
|
|
||||||
kcContainer.getVolumeMounts().add(volumeMount);
|
|
||||||
}
|
|
||||||
|
|
||||||
var userRelativePath = readConfigurationValue(Constants.KEYCLOAK_HTTP_RELATIVE_PATH_KEY);
|
|
||||||
var kcRelativePath = (userRelativePath == null) ? "" : userRelativePath;
|
|
||||||
var protocol = (this.keycloakCR.getSpec().isHttp()) ? "http" : "https";
|
|
||||||
var kcPort = (this.keycloakCR.getSpec().isHttp()) ? Constants.KEYCLOAK_HTTP_PORT : Constants.KEYCLOAK_HTTPS_PORT;
|
|
||||||
|
|
||||||
var baseProbe = new ArrayList<>(List.of("curl", "--head", "--fail", "--silent"));
|
|
||||||
|
|
||||||
if (!this.keycloakCR.getSpec().isHttp()) {
|
|
||||||
baseProbe.add("--insecure");
|
|
||||||
}
|
|
||||||
|
|
||||||
var readyProbe = new ArrayList<>(baseProbe);
|
|
||||||
readyProbe.add(protocol + "://127.0.0.1:" + kcPort + kcRelativePath + "/health/ready");
|
|
||||||
var liveProbe = new ArrayList<>(baseProbe);
|
|
||||||
liveProbe.add(protocol + "://127.0.0.1:" + kcPort + kcRelativePath + "/health/live");
|
|
||||||
|
|
||||||
kcContainer
|
|
||||||
.getReadinessProbe()
|
|
||||||
.setExec(new ExecActionBuilder().withCommand(readyProbe).build());
|
|
||||||
kcContainer
|
|
||||||
.getLivenessProbe()
|
|
||||||
.setExec(new ExecActionBuilder().withCommand(liveProbe).build());
|
|
||||||
}
|
|
||||||
|
|
||||||
public String readConfigurationValue(String key) {
|
|
||||||
if (this.keycloakCR != null &&
|
|
||||||
this.keycloakCR.getSpec() != null &&
|
|
||||||
this.keycloakCR.getSpec().getServerConfiguration() != null
|
|
||||||
) {
|
|
||||||
var serverConfigValue = this.keycloakCR
|
|
||||||
.getSpec()
|
|
||||||
.getServerConfiguration()
|
|
||||||
.stream()
|
|
||||||
.filter(sc -> sc.getName().equals(key))
|
|
||||||
.findFirst();
|
|
||||||
if (serverConfigValue.isPresent()) {
|
|
||||||
if (serverConfigValue.get().getValue() != null) {
|
|
||||||
return serverConfigValue.get().getValue();
|
|
||||||
} else {
|
|
||||||
var secretSelector = serverConfigValue.get().getSecret();
|
|
||||||
if (secretSelector == null) {
|
|
||||||
throw new IllegalStateException("Secret " + serverConfigValue.get().getName() + " not defined");
|
|
||||||
}
|
|
||||||
var secret = client.secrets().inNamespace(getNamespace()).withName(secretSelector.getName()).get();
|
|
||||||
if (secret == null) {
|
|
||||||
throw new IllegalStateException("Secret " + secretSelector.getName() + " not found in cluster");
|
|
||||||
}
|
|
||||||
if (secret.getData().containsKey(secretSelector.getKey())) {
|
|
||||||
return new String(Base64.getDecoder().decode(secret.getData().get(secretSelector.getKey())), StandardCharsets.UTF_8);
|
|
||||||
} else {
|
|
||||||
throw new IllegalStateException("Secret " + secretSelector.getName() + " doesn't contain the expected key " + secretSelector.getKey());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private StatefulSet createBaseDeployment() {
|
private StatefulSet createBaseDeployment() {
|
||||||
StatefulSet baseDeployment = new StatefulSetBuilder()
|
StatefulSet baseDeployment = new StatefulSetBuilder()
|
||||||
.withNewMetadata()
|
.withNewMetadata()
|
||||||
|
@ -523,7 +377,7 @@ public class KeycloakDeployment extends OperatorManagedResource implements Statu
|
||||||
|
|
||||||
Container container = baseDeployment.getSpec().getTemplate().getSpec().getContainers().get(0);
|
Container container = baseDeployment.getSpec().getTemplate().getSpec().getContainers().get(0);
|
||||||
var customImage = Optional.ofNullable(keycloakCR.getSpec().getImage());
|
var customImage = Optional.ofNullable(keycloakCR.getSpec().getImage());
|
||||||
container.setImage(customImage.orElse(config.keycloak().image()));
|
container.setImage(customImage.orElse(operatorConfig.keycloak().image()));
|
||||||
|
|
||||||
if (customImage.isPresent()) {
|
if (customImage.isPresent()) {
|
||||||
container.getArgs().add("--optimized");
|
container.getArgs().add("--optimized");
|
||||||
|
@ -533,17 +387,20 @@ public class KeycloakDeployment extends OperatorManagedResource implements Statu
|
||||||
baseDeployment.getSpec().getTemplate().getSpec().setImagePullSecrets(keycloakCR.getSpec().getImagePullSecrets());
|
baseDeployment.getSpec().getTemplate().getSpec().setImagePullSecrets(keycloakCR.getSpec().getImagePullSecrets());
|
||||||
}
|
}
|
||||||
|
|
||||||
container.setImagePullPolicy(config.keycloak().imagePullPolicy());
|
container.setImagePullPolicy(operatorConfig.keycloak().imagePullPolicy());
|
||||||
|
|
||||||
container.setEnv(getEnvVars());
|
container.setEnv(getEnvVars());
|
||||||
|
|
||||||
configureHostname(baseDeployment);
|
|
||||||
configureTLS(baseDeployment);
|
|
||||||
mergePodTemplate(baseDeployment.getSpec().getTemplate());
|
|
||||||
|
|
||||||
return baseDeployment;
|
return baseDeployment;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private KeycloakDeploymentConfig createDeploymentConfig() {
|
||||||
|
final KeycloakDeploymentConfig config = new KeycloakDeploymentConfig(keycloakCR, baseDeployment, client);
|
||||||
|
config.configureProperties();
|
||||||
|
mergePodTemplate(baseDeployment.getSpec().getTemplate());
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
private List<EnvVar> getEnvVars() {
|
private List<EnvVar> getEnvVars() {
|
||||||
// default config values
|
// default config values
|
||||||
List<ValueOrSecret> serverConfig = Constants.DEFAULT_DIST_CONFIG.entrySet().stream()
|
List<ValueOrSecret> serverConfig = Constants.DEFAULT_DIST_CONFIG.entrySet().stream()
|
||||||
|
@ -627,6 +484,8 @@ public class KeycloakDeployment extends OperatorManagedResource implements Statu
|
||||||
&& !existingDeployment.getStatus().getCurrentRevision().equals(existingDeployment.getStatus().getUpdateRevision())) {
|
&& !existingDeployment.getStatus().getCurrentRevision().equals(existingDeployment.getStatus().getUpdateRevision())) {
|
||||||
status.addRollingUpdateMessage("Rolling out deployment update");
|
status.addRollingUpdateMessage("Rolling out deployment update");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
deploymentConfig.validateProperties(status);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Set<String> getConfigSecretsNames() {
|
public Set<String> getConfigSecretsNames() {
|
||||||
|
|
|
@ -0,0 +1,272 @@
|
||||||
|
/*
|
||||||
|
* 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.controllers;
|
||||||
|
|
||||||
|
import io.fabric8.kubernetes.api.model.EnvVarBuilder;
|
||||||
|
import io.fabric8.kubernetes.api.model.ExecActionBuilder;
|
||||||
|
import io.fabric8.kubernetes.api.model.VolumeBuilder;
|
||||||
|
import io.fabric8.kubernetes.api.model.VolumeMountBuilder;
|
||||||
|
import io.fabric8.kubernetes.api.model.apps.StatefulSet;
|
||||||
|
import io.fabric8.kubernetes.client.KubernetesClient;
|
||||||
|
import org.keycloak.common.util.CollectionUtil;
|
||||||
|
import org.keycloak.operator.Constants;
|
||||||
|
import org.keycloak.operator.crds.v2alpha1.deployment.Keycloak;
|
||||||
|
import org.keycloak.operator.crds.v2alpha1.deployment.KeycloakStatusBuilder;
|
||||||
|
import org.keycloak.operator.crds.v2alpha1.deployment.ValueOrSecret;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Base64;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration for the KeycloakDeployment
|
||||||
|
*/
|
||||||
|
public class KeycloakDeploymentConfig {
|
||||||
|
private final Keycloak keycloakCR;
|
||||||
|
private final StatefulSet deployment;
|
||||||
|
private final KubernetesClient client;
|
||||||
|
|
||||||
|
public KeycloakDeploymentConfig(Keycloak keycloakCR, StatefulSet deployment, KubernetesClient client) {
|
||||||
|
this.keycloakCR = keycloakCR;
|
||||||
|
this.deployment = deployment;
|
||||||
|
this.client = client;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specify first-class citizens fields which should not be added as general server configuration property
|
||||||
|
*/
|
||||||
|
private final static List<String> FIRST_CLASS_FIELDS = List.of(
|
||||||
|
"hostname",
|
||||||
|
"tlsSecret",
|
||||||
|
"features",
|
||||||
|
"features-disabled"
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure configuration properties for the KeycloakDeployment
|
||||||
|
*/
|
||||||
|
protected void configureProperties() {
|
||||||
|
configureHostname();
|
||||||
|
configureTLS();
|
||||||
|
configureFeatures();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate all deployment configuration properties and update status of the Keycloak deployment
|
||||||
|
*
|
||||||
|
* @param status Keycloak Status builder
|
||||||
|
*/
|
||||||
|
protected void validateProperties(KeycloakStatusBuilder status) {
|
||||||
|
assumeFirstClassCitizens(status);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- Configuration of first-class citizen fields ---------- */
|
||||||
|
|
||||||
|
public void configureHostname() {
|
||||||
|
var kcContainer = deployment.getSpec().getTemplate().getSpec().getContainers().get(0);
|
||||||
|
var hostname = keycloakCR.getSpec().getHostname();
|
||||||
|
var envVars = kcContainer.getEnv();
|
||||||
|
if (keycloakCR.getSpec().isHostnameDisabled()) {
|
||||||
|
var disableStrictHostname = List.of(
|
||||||
|
new EnvVarBuilder()
|
||||||
|
.withName("KC_HOSTNAME_STRICT")
|
||||||
|
.withValue("false")
|
||||||
|
.build(),
|
||||||
|
new EnvVarBuilder()
|
||||||
|
.withName("KC_HOSTNAME_STRICT_BACKCHANNEL")
|
||||||
|
.withValue("false")
|
||||||
|
.build());
|
||||||
|
|
||||||
|
envVars.addAll(disableStrictHostname);
|
||||||
|
} else {
|
||||||
|
var enabledStrictHostname = List.of(
|
||||||
|
new EnvVarBuilder()
|
||||||
|
.withName("KC_HOSTNAME")
|
||||||
|
.withValue(hostname)
|
||||||
|
.build());
|
||||||
|
|
||||||
|
envVars.addAll(enabledStrictHostname);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void configureTLS() {
|
||||||
|
var kcContainer = deployment.getSpec().getTemplate().getSpec().getContainers().get(0);
|
||||||
|
var tlsSecret = keycloakCR.getSpec().getTlsSecret();
|
||||||
|
var envVars = kcContainer.getEnv();
|
||||||
|
|
||||||
|
if (keycloakCR.getSpec().isHttp()) {
|
||||||
|
var disableTls = List.of(
|
||||||
|
new EnvVarBuilder()
|
||||||
|
.withName("KC_HTTP_ENABLED")
|
||||||
|
.withValue("true")
|
||||||
|
.build(),
|
||||||
|
new EnvVarBuilder()
|
||||||
|
.withName("KC_HOSTNAME_STRICT_HTTPS")
|
||||||
|
.withValue("false")
|
||||||
|
.build(),
|
||||||
|
new EnvVarBuilder()
|
||||||
|
.withName("KC_PROXY")
|
||||||
|
.withValue("edge")
|
||||||
|
.build());
|
||||||
|
|
||||||
|
envVars.addAll(disableTls);
|
||||||
|
} else {
|
||||||
|
var enabledTls = List.of(
|
||||||
|
new EnvVarBuilder()
|
||||||
|
.withName("KC_HTTPS_CERTIFICATE_FILE")
|
||||||
|
.withValue(Constants.CERTIFICATES_FOLDER + "/tls.crt")
|
||||||
|
.build(),
|
||||||
|
new EnvVarBuilder()
|
||||||
|
.withName("KC_HTTPS_CERTIFICATE_KEY_FILE")
|
||||||
|
.withValue(Constants.CERTIFICATES_FOLDER + "/tls.key")
|
||||||
|
.build(),
|
||||||
|
new EnvVarBuilder()
|
||||||
|
.withName("KC_PROXY")
|
||||||
|
.withValue("passthrough")
|
||||||
|
.build());
|
||||||
|
|
||||||
|
envVars.addAll(enabledTls);
|
||||||
|
|
||||||
|
var volume = new VolumeBuilder()
|
||||||
|
.withName("keycloak-tls-certificates")
|
||||||
|
.withNewSecret()
|
||||||
|
.withSecretName(tlsSecret)
|
||||||
|
.withOptional(false)
|
||||||
|
.endSecret()
|
||||||
|
.build();
|
||||||
|
|
||||||
|
var volumeMount = new VolumeMountBuilder()
|
||||||
|
.withName(volume.getName())
|
||||||
|
.withMountPath(Constants.CERTIFICATES_FOLDER)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
deployment.getSpec().getTemplate().getSpec().getVolumes().add(volume);
|
||||||
|
kcContainer.getVolumeMounts().add(volumeMount);
|
||||||
|
}
|
||||||
|
|
||||||
|
var userRelativePath = readConfigurationValue(Constants.KEYCLOAK_HTTP_RELATIVE_PATH_KEY);
|
||||||
|
var kcRelativePath = (userRelativePath == null) ? "" : userRelativePath;
|
||||||
|
var protocol = (keycloakCR.getSpec().isHttp()) ? "http" : "https";
|
||||||
|
var kcPort = (keycloakCR.getSpec().isHttp()) ? Constants.KEYCLOAK_HTTP_PORT : Constants.KEYCLOAK_HTTPS_PORT;
|
||||||
|
|
||||||
|
var baseProbe = new ArrayList<>(List.of("curl", "--head", "--fail", "--silent"));
|
||||||
|
|
||||||
|
if (!keycloakCR.getSpec().isHttp()) {
|
||||||
|
baseProbe.add("--insecure");
|
||||||
|
}
|
||||||
|
|
||||||
|
var readyProbe = new ArrayList<>(baseProbe);
|
||||||
|
readyProbe.add(protocol + "://127.0.0.1:" + kcPort + kcRelativePath + "/health/ready");
|
||||||
|
var liveProbe = new ArrayList<>(baseProbe);
|
||||||
|
liveProbe.add(protocol + "://127.0.0.1:" + kcPort + kcRelativePath + "/health/live");
|
||||||
|
|
||||||
|
kcContainer
|
||||||
|
.getReadinessProbe()
|
||||||
|
.setExec(new ExecActionBuilder().withCommand(readyProbe).build());
|
||||||
|
kcContainer
|
||||||
|
.getLivenessProbe()
|
||||||
|
.setExec(new ExecActionBuilder().withCommand(liveProbe).build());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void configureFeatures() {
|
||||||
|
var featureSpec = keycloakCR.getSpec().getFeatureSpec();
|
||||||
|
if (featureSpec == null) return;
|
||||||
|
|
||||||
|
var kcContainer = deployment.getSpec().getTemplate().getSpec().getContainers().get(0);
|
||||||
|
var envVars = kcContainer.getEnv();
|
||||||
|
var enabledFeatures = featureSpec.getEnabledFeatures();
|
||||||
|
var disabledFeatures = featureSpec.getDisabledFeatures();
|
||||||
|
|
||||||
|
if (CollectionUtil.isNotEmpty(enabledFeatures)) {
|
||||||
|
envVars.add(new EnvVarBuilder()
|
||||||
|
.withName("KC_FEATURES")
|
||||||
|
.withValue(CollectionUtil.join(enabledFeatures, ","))
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (CollectionUtil.isNotEmpty(disabledFeatures)) {
|
||||||
|
envVars.add(new EnvVarBuilder()
|
||||||
|
.withName("KC_FEATURES_DISABLED")
|
||||||
|
.withValue(CollectionUtil.join(disabledFeatures, ","))
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- END of configuration of first-class citizen fields ---------- */
|
||||||
|
|
||||||
|
protected String readConfigurationValue(String key) {
|
||||||
|
if (keycloakCR != null &&
|
||||||
|
keycloakCR.getSpec() != null &&
|
||||||
|
keycloakCR.getSpec().getServerConfiguration() != null
|
||||||
|
) {
|
||||||
|
|
||||||
|
var serverConfigValue = keycloakCR
|
||||||
|
.getSpec()
|
||||||
|
.getServerConfiguration()
|
||||||
|
.stream()
|
||||||
|
.filter(sc -> sc.getName().equals(key))
|
||||||
|
.findFirst();
|
||||||
|
if (serverConfigValue.isPresent()) {
|
||||||
|
if (serverConfigValue.get().getValue() != null) {
|
||||||
|
return serverConfigValue.get().getValue();
|
||||||
|
} else {
|
||||||
|
var secretSelector = serverConfigValue.get().getSecret();
|
||||||
|
if (secretSelector == null) {
|
||||||
|
throw new IllegalStateException("Secret " + serverConfigValue.get().getName() + " not defined");
|
||||||
|
}
|
||||||
|
var secret = client.secrets().inNamespace(keycloakCR.getMetadata().getNamespace()).withName(secretSelector.getName()).get();
|
||||||
|
if (secret == null) {
|
||||||
|
throw new IllegalStateException("Secret " + secretSelector.getName() + " not found in cluster");
|
||||||
|
}
|
||||||
|
if (secret.getData().containsKey(secretSelector.getKey())) {
|
||||||
|
return new String(Base64.getDecoder().decode(secret.getData().get(secretSelector.getKey())), StandardCharsets.UTF_8);
|
||||||
|
} else {
|
||||||
|
throw new IllegalStateException("Secret " + secretSelector.getName() + " doesn't contain the expected key " + secretSelector.getKey());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assume the specified first-class citizens are not included in the general server configuration
|
||||||
|
*
|
||||||
|
* @param status Status of the deployment
|
||||||
|
*/
|
||||||
|
protected void assumeFirstClassCitizens(KeycloakStatusBuilder status) {
|
||||||
|
final var serverConfigNames = keycloakCR
|
||||||
|
.getSpec()
|
||||||
|
.getServerConfiguration()
|
||||||
|
.stream()
|
||||||
|
.map(ValueOrSecret::getName)
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
|
||||||
|
final var sameItems = CollectionUtil.intersection(serverConfigNames, FIRST_CLASS_FIELDS);
|
||||||
|
if (CollectionUtil.isNotEmpty(sameItems)) {
|
||||||
|
status.addWarningMessage("You need to specify these fields as the first-class citizen of the CR: "
|
||||||
|
+ CollectionUtil.join(sameItems, ","));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,10 +17,12 @@
|
||||||
package org.keycloak.operator.crds.v2alpha1.deployment;
|
package org.keycloak.operator.crds.v2alpha1.deployment;
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
|
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
|
||||||
|
|
||||||
import io.fabric8.kubernetes.api.model.LocalObjectReference;
|
import io.fabric8.kubernetes.api.model.LocalObjectReference;
|
||||||
import org.keycloak.operator.Constants;
|
import org.keycloak.operator.Constants;
|
||||||
|
import org.keycloak.operator.crds.v2alpha1.deployment.spec.FeatureSpec;
|
||||||
|
|
||||||
import javax.validation.constraints.NotNull;
|
import javax.validation.constraints.NotNull;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
@ -29,10 +31,13 @@ public class KeycloakSpec {
|
||||||
|
|
||||||
@JsonPropertyDescription("Number of Keycloak instances in HA mode. Default is 1.")
|
@JsonPropertyDescription("Number of Keycloak instances in HA mode. Default is 1.")
|
||||||
private int instances = 1;
|
private int instances = 1;
|
||||||
|
|
||||||
@JsonPropertyDescription("Custom Keycloak image to be used.")
|
@JsonPropertyDescription("Custom Keycloak image to be used.")
|
||||||
private String image;
|
private String image;
|
||||||
|
|
||||||
@JsonPropertyDescription("Secret(s) that might be used when pulling an image from a private container image registry or repository.")
|
@JsonPropertyDescription("Secret(s) that might be used when pulling an image from a private container image registry or repository.")
|
||||||
private List<LocalObjectReference> imagePullSecrets;
|
private List<LocalObjectReference> imagePullSecrets;
|
||||||
|
|
||||||
@JsonPropertyDescription("Configuration of the Keycloak server.\n" +
|
@JsonPropertyDescription("Configuration of the Keycloak server.\n" +
|
||||||
"expressed as a keys (reference: https://www.keycloak.org/server/all-config) and values that can be either direct values or references to secrets.")
|
"expressed as a keys (reference: https://www.keycloak.org/server/all-config) and values that can be either direct values or references to secrets.")
|
||||||
private List<ValueOrSecret> serverConfiguration; // can't use Set due to a bug in Sundrio https://github.com/sundrio/sundrio/issues/316
|
private List<ValueOrSecret> serverConfiguration; // can't use Set due to a bug in Sundrio https://github.com/sundrio/sundrio/issues/316
|
||||||
|
@ -44,17 +49,24 @@ public class KeycloakSpec {
|
||||||
@JsonPropertyDescription("Hostname for the Keycloak server.\n" +
|
@JsonPropertyDescription("Hostname for the Keycloak server.\n" +
|
||||||
"The special value `" + Constants.INSECURE_DISABLE + "` disables the hostname strict resolution.")
|
"The special value `" + Constants.INSECURE_DISABLE + "` disables the hostname strict resolution.")
|
||||||
private String hostname;
|
private String hostname;
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
@JsonPropertyDescription("A secret containing the TLS configuration for HTTPS. Reference: https://kubernetes.io/docs/concepts/configuration/secret/#tls-secrets.\n" +
|
@JsonPropertyDescription("A secret containing the TLS configuration for HTTPS. Reference: https://kubernetes.io/docs/concepts/configuration/secret/#tls-secrets.\n" +
|
||||||
"The special value `" + Constants.INSECURE_DISABLE + "` disables https.")
|
"The special value `" + Constants.INSECURE_DISABLE + "` disables https.")
|
||||||
private String tlsSecret;
|
private String tlsSecret;
|
||||||
|
|
||||||
@JsonPropertyDescription("Disable the default ingress.")
|
@JsonPropertyDescription("Disable the default ingress.")
|
||||||
private boolean disableDefaultIngress;
|
private boolean disableDefaultIngress;
|
||||||
|
|
||||||
@JsonPropertyDescription(
|
@JsonPropertyDescription(
|
||||||
"In this section you can configure podTemplate advanced features, not production-ready, and not supported settings.\n" +
|
"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.")
|
"Use at your own risk and open an issue with your use-case if you don't find an alternative way.")
|
||||||
private KeycloakSpecUnsupported unsupported;
|
private KeycloakSpecUnsupported unsupported;
|
||||||
|
|
||||||
|
@JsonProperty("features")
|
||||||
|
@JsonPropertyDescription("In this section you can configure Keycloak features, which should be enabled/disabled.")
|
||||||
|
private FeatureSpec featureSpec;
|
||||||
|
|
||||||
public String getHostname() {
|
public String getHostname() {
|
||||||
return hostname;
|
return hostname;
|
||||||
}
|
}
|
||||||
|
@ -97,6 +109,14 @@ public class KeycloakSpec {
|
||||||
this.unsupported = unsupported;
|
this.unsupported = unsupported;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public FeatureSpec getFeatureSpec() {
|
||||||
|
return featureSpec;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFeatureSpec(FeatureSpec featureSpec) {
|
||||||
|
this.featureSpec = featureSpec;
|
||||||
|
}
|
||||||
|
|
||||||
public int getInstances() {
|
public int getInstances() {
|
||||||
return instances;
|
return instances;
|
||||||
}
|
}
|
||||||
|
@ -128,4 +148,4 @@ public class KeycloakSpec {
|
||||||
public void setServerConfiguration(List<ValueOrSecret> serverConfiguration) {
|
public void setServerConfiguration(List<ValueOrSecret> serverConfiguration) {
|
||||||
this.serverConfiguration = serverConfiguration;
|
this.serverConfiguration = serverConfiguration;
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,55 @@
|
||||||
|
/*
|
||||||
|
* 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.crds.v2alpha1.deployment.spec;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
|
||||||
|
import io.sundr.builder.annotations.Buildable;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Buildable(editableEnabled = false, builderPackage = "io.fabric8.kubernetes.api.builder")
|
||||||
|
@JsonPropertyOrder({"enabled", "disabled"})
|
||||||
|
public class FeatureSpec implements Serializable {
|
||||||
|
|
||||||
|
@JsonProperty("enabled")
|
||||||
|
@JsonPropertyDescription("Enabled Keycloak features")
|
||||||
|
private List<String> enabledFeatures;
|
||||||
|
|
||||||
|
@JsonProperty("disabled")
|
||||||
|
@JsonPropertyDescription("Disabled Keycloak features")
|
||||||
|
private List<String> disabledFeatures;
|
||||||
|
|
||||||
|
public List<String> getEnabledFeatures() {
|
||||||
|
return enabledFeatures;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setEnabledFeatures(List<String> enabledFeatures) {
|
||||||
|
this.enabledFeatures = enabledFeatures;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> getDisabledFeatures() {
|
||||||
|
return disabledFeatures;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDisabledFeatures(List<String> disabledFeatures) {
|
||||||
|
this.disabledFeatures = disabledFeatures;
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,9 +18,15 @@
|
||||||
package org.keycloak.operator.testsuite.unit;
|
package org.keycloak.operator.testsuite.unit;
|
||||||
|
|
||||||
import io.fabric8.kubernetes.client.utils.Serialization;
|
import io.fabric8.kubernetes.client.utils.Serialization;
|
||||||
|
import org.hamcrest.CoreMatchers;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.keycloak.operator.crds.v2alpha1.deployment.Keycloak;
|
import org.keycloak.operator.crds.v2alpha1.deployment.Keycloak;
|
||||||
|
import org.keycloak.operator.crds.v2alpha1.deployment.spec.FeatureSpec;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
|
import static org.hamcrest.Matchers.notNullValue;
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
@ -36,4 +42,22 @@ public class CRSerializationTest {
|
||||||
assertTrue(keycloak.getSpec().isDisableDefaultIngress());
|
assertTrue(keycloak.getSpec().isDisableDefaultIngress());
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
@Test
|
||||||
|
public void featureSpecificationDeserialization(){
|
||||||
|
Keycloak keycloak = Serialization.unmarshal(this.getClass().getResourceAsStream("/test-serialization-keycloak-cr.yml"), Keycloak.class);
|
||||||
|
|
||||||
|
final FeatureSpec featureSpec = keycloak.getSpec().getFeatureSpec();
|
||||||
|
assertThat(featureSpec, notNullValue());
|
||||||
|
|
||||||
|
final List<String> enabledFeatures = featureSpec.getEnabledFeatures();
|
||||||
|
assertThat(enabledFeatures.size(), CoreMatchers.is(2));
|
||||||
|
assertThat(enabledFeatures.get(0), CoreMatchers.is("docker"));
|
||||||
|
assertThat(enabledFeatures.get(1), CoreMatchers.is("authorization"));
|
||||||
|
|
||||||
|
final List<String> disabledFeatures = featureSpec.getDisabledFeatures();
|
||||||
|
assertThat(disabledFeatures.size(), CoreMatchers.is(2));
|
||||||
|
assertThat(disabledFeatures.get(0), CoreMatchers.is("admin"));
|
||||||
|
assertThat(disabledFeatures.get(1), CoreMatchers.is("step-up-authentication"));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,124 @@
|
||||||
|
/*
|
||||||
|
* 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.testsuite.unit;
|
||||||
|
|
||||||
|
import io.fabric8.kubernetes.api.model.Container;
|
||||||
|
import io.fabric8.kubernetes.api.model.EnvVar;
|
||||||
|
import io.fabric8.kubernetes.api.model.apps.StatefulSet;
|
||||||
|
import io.fabric8.kubernetes.api.model.apps.StatefulSetBuilder;
|
||||||
|
import io.quarkus.test.junit.QuarkusTest;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.keycloak.common.util.CollectionUtil;
|
||||||
|
import org.keycloak.common.util.ObjectUtil;
|
||||||
|
import org.keycloak.operator.controllers.KeycloakDeploymentConfig;
|
||||||
|
import org.keycloak.operator.crds.v2alpha1.deployment.Keycloak;
|
||||||
|
import org.keycloak.operator.testsuite.utils.K8sUtils;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
@QuarkusTest
|
||||||
|
public class KeycloakDeploymentConfigTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void enabledFeatures() {
|
||||||
|
testFeatures(true, "docker", "authorization");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void disabledFeatures() {
|
||||||
|
testFeatures(false, "admin", "step-up-authentication");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void testFeatures(boolean enabledFeatures, String... features) {
|
||||||
|
final String featureEnvVar = enabledFeatures ? "KC_FEATURES" : "KC_FEATURES_DISABLED";
|
||||||
|
|
||||||
|
final Keycloak keycloak = K8sUtils.getResourceFromFile("/test-serialization-keycloak-cr.yml", Keycloak.class);
|
||||||
|
final StatefulSet deployment = getBasicKcDeployment();
|
||||||
|
final KeycloakDeploymentConfig deploymentConfig = new KeycloakDeploymentConfig(keycloak, deployment, null);
|
||||||
|
|
||||||
|
final Container container = deployment.getSpec().getTemplate().getSpec().getContainers().get(0);
|
||||||
|
assertThat(container).isNotNull();
|
||||||
|
|
||||||
|
assertEnvVarNotPresent(container.getEnv(), featureEnvVar);
|
||||||
|
|
||||||
|
deploymentConfig.configureFeatures();
|
||||||
|
|
||||||
|
assertContainerEnvVar(container.getEnv(), featureEnvVar, features);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* assertContainerEnvVar(container.getEnv(), "KC_FEATURES", "admin,authorization");
|
||||||
|
* assertContainerEnvVar(container.getEnv(), "KC_HOSTNAME", "someHostname");
|
||||||
|
*/
|
||||||
|
private void assertContainerEnvVar(List<EnvVar> envVars, String varName, String... expectedValue) {
|
||||||
|
assertThat(envVars).isNotNull();
|
||||||
|
assertEnvVarPresent(envVars, varName);
|
||||||
|
|
||||||
|
final List<String> foundValues = getValuesFromEnvVar(envVars, varName);
|
||||||
|
assertThat(CollectionUtil.isNotEmpty(foundValues)).isTrue();
|
||||||
|
for (String val : expectedValue) {
|
||||||
|
assertThat(foundValues.contains(val)).isTrue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertEnvVarPresent(List<EnvVar> envVars, String varName) {
|
||||||
|
assertThat(containsEnvironmentVariable(envVars, varName)).isTrue();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertEnvVarNotPresent(List<EnvVar> envVars, String varName) {
|
||||||
|
assertThat(containsEnvironmentVariable(envVars, varName)).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
private StatefulSet getBasicKcDeployment() {
|
||||||
|
return new StatefulSetBuilder()
|
||||||
|
.withNewSpec()
|
||||||
|
.withNewTemplate()
|
||||||
|
.withNewSpec()
|
||||||
|
.addNewContainer()
|
||||||
|
.withName("keycloak")
|
||||||
|
.withArgs("start")
|
||||||
|
.endContainer()
|
||||||
|
.endSpec()
|
||||||
|
.endTemplate()
|
||||||
|
.endSpec()
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean containsEnvironmentVariable(List<EnvVar> envVars, String varName) {
|
||||||
|
if (CollectionUtil.isEmpty(envVars) || ObjectUtil.isBlank(varName)) return false;
|
||||||
|
return envVars.stream().anyMatch(f -> varName.equals(f.getName()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns values of environment variable separated by comma (f.e KC_FEATURES=admin2,ciba)
|
||||||
|
*/
|
||||||
|
private List<String> getValuesFromEnvVar(List<EnvVar> envVars, String varName) {
|
||||||
|
if (CollectionUtil.isEmpty(envVars) || ObjectUtil.isBlank(varName)) return Collections.emptyList();
|
||||||
|
|
||||||
|
return envVars.stream().filter(f -> varName.equals(f.getName()))
|
||||||
|
.findFirst()
|
||||||
|
.map(EnvVar::getValue)
|
||||||
|
.map(f -> f.split(","))
|
||||||
|
.map(List::of)
|
||||||
|
.orElseGet(Collections::emptyList);
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,11 +8,20 @@ spec:
|
||||||
serverConfiguration:
|
serverConfiguration:
|
||||||
- name: key1
|
- name: key1
|
||||||
value: value1
|
value: value1
|
||||||
|
- name: features
|
||||||
|
value: docker
|
||||||
hostname: my-hostname
|
hostname: my-hostname
|
||||||
tlsSecret: my-tls-secret
|
tlsSecret: my-tls-secret
|
||||||
|
features:
|
||||||
|
enabled:
|
||||||
|
- docker
|
||||||
|
- authorization
|
||||||
|
disabled:
|
||||||
|
- admin
|
||||||
|
- step-up-authentication
|
||||||
disableDefaultIngress: true
|
disableDefaultIngress: true
|
||||||
unsupported:
|
unsupported:
|
||||||
podTemplate:
|
podTemplate:
|
||||||
metadata:
|
metadata:
|
||||||
labels:
|
labels:
|
||||||
my-label: "foo"
|
my-label: "foo"
|
Loading…
Reference in a new issue