mirror of
https://github.com/keycloak/keycloak.git
synced 2026-01-10 15:32:05 -03:30
enhance: adding the ability to set truststores via configmaps (#41796)
closes: #34114 Signed-off-by: Steve Hawkins <shawkins@redhat.com>
This commit is contained in:
parent
856df9ea3d
commit
183a96d6a1
@ -323,7 +323,7 @@ For more details, see <@links.server id="management-interface" />.
|
||||
|
||||
If you need to provide trusted certificates, the Keycloak CR provides a top level feature for configuring the server's truststore as discussed in <@links.server id="keycloak-truststore"/>.
|
||||
|
||||
Use the truststores stanza of the Keycloak spec to specify Secrets containing PEM encoded files, or PKCS12 files with extension `.p12`, `.pfx`, or `.pkcs12`, for example:
|
||||
Use the truststores stanza of the Keycloak spec to specify Secrets or ConfigMaps containing PEM encoded files, or PKCS12 files with extension `.p12`, `.pfx`, or `.pkcs12`, for example:
|
||||
|
||||
[source,yaml]
|
||||
----
|
||||
|
||||
@ -30,7 +30,6 @@ 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.StatefulSetBuilder;
|
||||
import io.fabric8.kubernetes.client.KubernetesClient;
|
||||
import io.javaoperatorsdk.operator.api.config.informer.Informer;
|
||||
import io.javaoperatorsdk.operator.api.reconciler.Context;
|
||||
import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource;
|
||||
@ -134,20 +133,25 @@ public class KeycloakDeploymentDependentResource extends CRUDKubernetesDependent
|
||||
|
||||
StatefulSet baseDeployment = createBaseDeployment(primary, context, operatorConfig);
|
||||
TreeSet<String> allSecrets = new TreeSet<>();
|
||||
TreeSet<String> allConfigMaps = new TreeSet<>();
|
||||
if (isTlsConfigured(primary)) {
|
||||
configureTLS(primary, baseDeployment, allSecrets);
|
||||
}
|
||||
Container kcContainer = baseDeployment.getSpec().getTemplate().getSpec().getContainers().get(0);
|
||||
addTruststores(primary, baseDeployment, kcContainer, allSecrets);
|
||||
addTruststores(primary, baseDeployment, kcContainer, allSecrets, allConfigMaps);
|
||||
addEnvVars(baseDeployment, primary, allSecrets, context);
|
||||
addResources(primary.getSpec().getResourceRequirements(), operatorConfig, kcContainer);
|
||||
Optional.ofNullable(primary.getSpec().getCacheSpec())
|
||||
.ifPresent(c -> configureCache(baseDeployment, kcContainer, c, context.getClient(), watchedResources));
|
||||
.ifPresent(c -> configureCache(baseDeployment, kcContainer, c, allConfigMaps));
|
||||
|
||||
if (!allSecrets.isEmpty()) {
|
||||
watchedResources.annotateDeployment(new ArrayList<>(allSecrets), Secret.class, baseDeployment, context.getClient());
|
||||
}
|
||||
|
||||
if (!allConfigMaps.isEmpty()) {
|
||||
watchedResources.annotateDeployment(new ArrayList<>(allConfigMaps), ConfigMap.class, baseDeployment, context.getClient());
|
||||
}
|
||||
|
||||
// default to the new revision - will be overriden to the old one if needed
|
||||
UpdateSpec.getRevision(primary).ifPresent(rev -> addUpdateRevisionAnnotation(rev, baseDeployment));
|
||||
|
||||
@ -185,7 +189,7 @@ public class KeycloakDeploymentDependentResource extends CRUDKubernetesDependent
|
||||
};
|
||||
}
|
||||
|
||||
private void configureCache(StatefulSet deployment, Container kcContainer, CacheSpec spec, KubernetesClient client, WatchedResources watchedResources) {
|
||||
private void configureCache(StatefulSet deployment, Container kcContainer, CacheSpec spec, TreeSet<String> allConfigMaps) {
|
||||
Optional.ofNullable(spec.getConfigMapFile()).ifPresent(configFile -> {
|
||||
if (configFile.getName() == null || configFile.getKey() == null) {
|
||||
throw new IllegalStateException("Cache file ConfigMap requires both a name and a key");
|
||||
@ -206,33 +210,54 @@ public class KeycloakDeploymentDependentResource extends CRUDKubernetesDependent
|
||||
|
||||
deployment.getSpec().getTemplate().getSpec().getVolumes().add(0, volume);
|
||||
kcContainer.getVolumeMounts().add(0, volumeMount);
|
||||
|
||||
// currently the only configmap we're watching
|
||||
watchedResources.annotateDeployment(List.of(configFile.getName()), ConfigMap.class, deployment, client);
|
||||
allConfigMaps.add(configFile.getName());
|
||||
});
|
||||
}
|
||||
|
||||
private void addTruststores(Keycloak keycloakCR, StatefulSet deployment, Container kcContainer, TreeSet<String> allSecrets) {
|
||||
private void addTruststores(Keycloak keycloakCR, StatefulSet deployment, Container kcContainer, TreeSet<String> allSecrets, TreeSet<String> allConfigMaps) {
|
||||
for (Truststore truststore : keycloakCR.getSpec().getTruststores().values()) {
|
||||
// for now we'll assume only secrets, later we can support configmaps
|
||||
TruststoreSource source = truststore.getSecret();
|
||||
String secretName = source.getName();
|
||||
var volume = new VolumeBuilder()
|
||||
.withName("truststore-secret-" + secretName)
|
||||
.withNewSecret()
|
||||
.withSecretName(secretName)
|
||||
.withOptional(source.getOptional())
|
||||
.endSecret()
|
||||
.build();
|
||||
if (source != null) {
|
||||
String secretName = source.getName();
|
||||
var volume = new VolumeBuilder()
|
||||
.withName("truststore-secret-" + secretName)
|
||||
.withNewSecret()
|
||||
.withSecretName(secretName)
|
||||
.withOptional(source.getOptional())
|
||||
.endSecret()
|
||||
.build();
|
||||
|
||||
var volumeMount = new VolumeMountBuilder()
|
||||
.withName(volume.getName())
|
||||
.withMountPath(Constants.TRUSTSTORES_FOLDER + "/secret-" + secretName)
|
||||
.build();
|
||||
var volumeMount = new VolumeMountBuilder()
|
||||
.withName(volume.getName())
|
||||
.withMountPath(Constants.TRUSTSTORES_FOLDER + "/secret-" + secretName)
|
||||
.build();
|
||||
|
||||
deployment.getSpec().getTemplate().getSpec().getVolumes().add(0, volume);
|
||||
kcContainer.getVolumeMounts().add(0, volumeMount);
|
||||
allSecrets.add(secretName);
|
||||
deployment.getSpec().getTemplate().getSpec().getVolumes().add(0, volume);
|
||||
kcContainer.getVolumeMounts().add(0, volumeMount);
|
||||
allSecrets.add(secretName);
|
||||
} else {
|
||||
source = truststore.getConfigMap();
|
||||
if (source != null) {
|
||||
String name = source.getName();
|
||||
var volume = new VolumeBuilder()
|
||||
.withName("truststore-configmap-" + name)
|
||||
.withNewConfigMap()
|
||||
.withName(name)
|
||||
.withOptional(source.getOptional())
|
||||
.endConfigMap()
|
||||
.build();
|
||||
|
||||
var volumeMount = new VolumeMountBuilder()
|
||||
.withName(volume.getName())
|
||||
.withMountPath(Constants.TRUSTSTORES_FOLDER + "/configmap-" + name)
|
||||
.build();
|
||||
|
||||
deployment.getSpec().getTemplate().getSpec().getVolumes().add(0, volume);
|
||||
kcContainer.getVolumeMounts().add(0, volumeMount);
|
||||
allConfigMaps.add(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -471,7 +496,7 @@ public class KeycloakDeploymentDependentResource extends CRUDKubernetesDependent
|
||||
var envVars = new ArrayList<>(varMap.values());
|
||||
baseDeployment.getSpec().getTemplate().getSpec().getContainers().get(0).setEnv(envVars);
|
||||
|
||||
// watch the secrets used by secret key - we don't currently expect configmaps, optional refs, or watch the initial-admin
|
||||
// watch the secrets used by secret key - we don't currently expect configmaps or watch the initial-admin
|
||||
TreeSet<String> serverConfigSecretsNames = envVars.stream().map(EnvVar::getValueFrom).filter(Objects::nonNull)
|
||||
.map(EnvVarSource::getSecretKeyRef).filter(Objects::nonNull).map(SecretKeySelector::getName).collect(Collectors.toCollection(TreeSet::new));
|
||||
|
||||
|
||||
@ -17,20 +17,22 @@
|
||||
|
||||
package org.keycloak.operator.crds.v2alpha1.deployment.spec;
|
||||
|
||||
import io.fabric8.generator.annotation.Required;
|
||||
import io.sundr.builder.annotations.Buildable;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
|
||||
|
||||
import io.sundr.builder.annotations.Buildable;
|
||||
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
@Buildable(editableEnabled = false, builderPackage = "io.fabric8.kubernetes.api.builder")
|
||||
public class Truststore {
|
||||
|
||||
@JsonPropertyDescription("Not used. To be removed in later versions.")
|
||||
private String name;
|
||||
@Required
|
||||
|
||||
@JsonPropertyDescription("The Secret containing the trust material - only set one of the other secret or configMap")
|
||||
private TruststoreSource secret;
|
||||
@JsonPropertyDescription("The ConfigMap containing the trust material - only set one of the other secret or configMap")
|
||||
private TruststoreSource configMap;
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
@ -48,4 +50,12 @@ public class Truststore {
|
||||
this.secret = secret;
|
||||
}
|
||||
|
||||
public TruststoreSource getConfigMap() {
|
||||
return configMap;
|
||||
}
|
||||
|
||||
public void setConfigMap(TruststoreSource configMap) {
|
||||
this.configMap = configMap;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -17,6 +17,7 @@
|
||||
|
||||
package org.keycloak.operator.testsuite.integration;
|
||||
|
||||
import io.fabric8.kubernetes.api.model.ConfigMapBuilder;
|
||||
import io.fabric8.kubernetes.api.model.Secret;
|
||||
import io.fabric8.kubernetes.api.model.apps.StatefulSet;
|
||||
import io.fabric8.kubernetes.client.dsl.Resource;
|
||||
@ -40,6 +41,7 @@ public class KeycloakTruststoresTests extends BaseOperatorTest {
|
||||
var kc = getTestKeycloakDeployment(true);
|
||||
var deploymentName = kc.getMetadata().getName();
|
||||
kc.getSpec().getTruststores().put("xyz", new TruststoreBuilder().withNewSecret().withName("xyz").endSecret().build());
|
||||
kc.getSpec().getTruststores().put("abc", new TruststoreBuilder().withNewConfigMap().withName("abc").endConfigMap().build());
|
||||
|
||||
deployKeycloak(k8sclient, kc, false);
|
||||
Resource<StatefulSet> stsResource = k8sclient.resources(StatefulSet.class).withName(deploymentName);
|
||||
@ -49,6 +51,10 @@ public class KeycloakTruststoresTests extends BaseOperatorTest {
|
||||
statefulSet.getMetadata().getAnnotations().get(WatchedResourcesTest.KEYCLOAK_MISSING_SECRETS_ANNOTATION));
|
||||
assertTrue(statefulSet.getMetadata().getAnnotations().get(WatchedResourcesTest.KEYCLOAK_WATCHING_ANNOTATION)
|
||||
.contains("xyz"));
|
||||
assertEquals("true",
|
||||
statefulSet.getMetadata().getAnnotations().get(WatchedResourcesTest.KEYCLOAK_MISSING_CONFIGMAPS_ANNOTATION));
|
||||
assertTrue(statefulSet.getMetadata().getAnnotations().get(WatchedResourcesTest.KEYCLOAK_WATCHING_CONFIGMAPS_ANNOTATION)
|
||||
.contains("abc"));
|
||||
});
|
||||
}
|
||||
|
||||
@ -59,6 +65,9 @@ public class KeycloakTruststoresTests extends BaseOperatorTest {
|
||||
|
||||
K8sUtils.set(k8sclient, getResourceFromFile("example-truststore-secret.yaml", Secret.class));
|
||||
kc.getSpec().getTruststores().put("example", new TruststoreBuilder().withNewSecret().withName("example-truststore-secret").endSecret().build());
|
||||
kc.getSpec().getTruststores().put("abc", new TruststoreBuilder().withNewConfigMap().withName("abc").endConfigMap().build());
|
||||
|
||||
k8sclient.configMaps().resource(new ConfigMapBuilder().withNewMetadata().withName("abc").endMetadata().build()).create();
|
||||
|
||||
deployKeycloak(k8sclient, kc, true);
|
||||
Resource<StatefulSet> stsResource = k8sclient.resources(StatefulSet.class).withName(deploymentName);
|
||||
@ -68,6 +77,11 @@ public class KeycloakTruststoresTests extends BaseOperatorTest {
|
||||
assertTrue(statefulSet.getSpec().getTemplate().getSpec().getContainers().get(0).getVolumeMounts().stream()
|
||||
.anyMatch(v -> v.getMountPath()
|
||||
.equals("/opt/keycloak/conf/truststores/secret-example-truststore-secret")));
|
||||
assertEquals("false",
|
||||
statefulSet.getMetadata().getAnnotations().get(WatchedResourcesTest.KEYCLOAK_MISSING_CONFIGMAPS_ANNOTATION));
|
||||
assertTrue(statefulSet.getSpec().getTemplate().getSpec().getContainers().get(0).getVolumeMounts().stream()
|
||||
.anyMatch(v -> v.getMountPath()
|
||||
.equals("/opt/keycloak/conf/truststores/configmap-abc")));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -19,6 +19,7 @@ package org.keycloak.operator.testsuite.unit;
|
||||
|
||||
import io.fabric8.kubernetes.api.model.Affinity;
|
||||
import io.fabric8.kubernetes.api.model.AffinityBuilder;
|
||||
import io.fabric8.kubernetes.api.model.ConfigMap;
|
||||
import io.fabric8.kubernetes.api.model.Container;
|
||||
import io.fabric8.kubernetes.api.model.EnvVar;
|
||||
import io.fabric8.kubernetes.api.model.IntOrString;
|
||||
@ -26,6 +27,7 @@ import io.fabric8.kubernetes.api.model.LocalObjectReference;
|
||||
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.Secret;
|
||||
import io.fabric8.kubernetes.api.model.SecretKeySelector;
|
||||
import io.fabric8.kubernetes.api.model.Toleration;
|
||||
import io.fabric8.kubernetes.api.model.TopologySpreadConstraint;
|
||||
@ -591,6 +593,8 @@ public class PodTemplateTest {
|
||||
.filter(v -> v.getName().equals(KeycloakDeploymentDependentResource.CACHE_CONFIG_FILE_MOUNT_NAME))
|
||||
.findFirst().orElseThrow();
|
||||
assertThat(volume.getConfigMap().getName()).isEqualTo("cm");
|
||||
|
||||
Mockito.verify(this.watchedResources).annotateDeployment(Mockito.eq(List.of("cm")), Mockito.eq(ConfigMap.class), Mockito.any(), Mockito.any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -794,7 +798,7 @@ public class PodTemplateTest {
|
||||
envVar = env.get("SECRET");
|
||||
assertEquals("key", envVar.getValueFrom().getSecretKeyRef().getKey());
|
||||
|
||||
Mockito.verify(this.watchedResources).annotateDeployment(Mockito.eq(List.of("example-tls-secret", "instance-initial-admin", "my-secret")), Mockito.any(), Mockito.any(), Mockito.any());
|
||||
Mockito.verify(this.watchedResources).annotateDeployment(Mockito.eq(List.of("example-tls-secret", "instance-initial-admin", "my-secret")), Mockito.eq(Secret.class), Mockito.any(), Mockito.any());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -39,7 +39,10 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
public class WatchedResourcesTest {
|
||||
|
||||
public static final String KEYCLOAK_WATCHING_ANNOTATION = "operator.keycloak.org/watching-secrets";
|
||||
public static final String KEYCLOAK_WATCHING_CONFIGMAPS_ANNOTATION = "operator.keycloak.org/watching-configmaps";
|
||||
public static final String KEYCLOAK_MISSING_SECRETS_ANNOTATION = "operator.keycloak.org/missing-secrets";
|
||||
public static final String KEYCLOAK_MISSING_CONFIGMAPS_ANNOTATION = "operator.keycloak.org/missing-configmaps";
|
||||
|
||||
|
||||
@Inject
|
||||
WatchedResources watchedResources;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user