diff --git a/docs/guides/operator/advanced-configuration.adoc b/docs/guides/operator/advanced-configuration.adoc index 2076b76d5a1..709bbea8ee6 100644 --- a/docs/guides/operator/advanced-configuration.adoc +++ b/docs/guides/operator/advanced-configuration.adoc @@ -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] ---- diff --git a/operator/src/main/java/org/keycloak/operator/controllers/KeycloakDeploymentDependentResource.java b/operator/src/main/java/org/keycloak/operator/controllers/KeycloakDeploymentDependentResource.java index 15b341009e8..248fa7c08fb 100644 --- a/operator/src/main/java/org/keycloak/operator/controllers/KeycloakDeploymentDependentResource.java +++ b/operator/src/main/java/org/keycloak/operator/controllers/KeycloakDeploymentDependentResource.java @@ -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 allSecrets = new TreeSet<>(); + TreeSet 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 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 allSecrets) { + private void addTruststores(Keycloak keycloakCR, StatefulSet deployment, Container kcContainer, TreeSet allSecrets, TreeSet 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 serverConfigSecretsNames = envVars.stream().map(EnvVar::getValueFrom).filter(Objects::nonNull) .map(EnvVarSource::getSecretKeyRef).filter(Objects::nonNull).map(SecretKeySelector::getName).collect(Collectors.toCollection(TreeSet::new)); diff --git a/operator/src/main/java/org/keycloak/operator/crds/v2alpha1/deployment/spec/Truststore.java b/operator/src/main/java/org/keycloak/operator/crds/v2alpha1/deployment/spec/Truststore.java index 761189351d6..4f00fb62b69 100644 --- a/operator/src/main/java/org/keycloak/operator/crds/v2alpha1/deployment/spec/Truststore.java +++ b/operator/src/main/java/org/keycloak/operator/crds/v2alpha1/deployment/spec/Truststore.java @@ -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; + } + } diff --git a/operator/src/test/java/org/keycloak/operator/testsuite/integration/KeycloakTruststoresTests.java b/operator/src/test/java/org/keycloak/operator/testsuite/integration/KeycloakTruststoresTests.java index 890e939f096..bf8bdce3016 100644 --- a/operator/src/test/java/org/keycloak/operator/testsuite/integration/KeycloakTruststoresTests.java +++ b/operator/src/test/java/org/keycloak/operator/testsuite/integration/KeycloakTruststoresTests.java @@ -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 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 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"))); } } diff --git a/operator/src/test/java/org/keycloak/operator/testsuite/unit/PodTemplateTest.java b/operator/src/test/java/org/keycloak/operator/testsuite/unit/PodTemplateTest.java index 5b33ef23a88..63621b358b6 100644 --- a/operator/src/test/java/org/keycloak/operator/testsuite/unit/PodTemplateTest.java +++ b/operator/src/test/java/org/keycloak/operator/testsuite/unit/PodTemplateTest.java @@ -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()); } } diff --git a/operator/src/test/java/org/keycloak/operator/testsuite/unit/WatchedResourcesTest.java b/operator/src/test/java/org/keycloak/operator/testsuite/unit/WatchedResourcesTest.java index 22fe9c0ca50..03264a58316 100644 --- a/operator/src/test/java/org/keycloak/operator/testsuite/unit/WatchedResourcesTest.java +++ b/operator/src/test/java/org/keycloak/operator/testsuite/unit/WatchedResourcesTest.java @@ -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;