mirror of
https://github.com/keycloak/keycloak.git
synced 2026-01-10 15:32:05 -03:30
Convert watching to polling and adding infinispan config file support (#26510)
Signed-off-by: Steve Hawkins <shawkins@redhat.com>
This commit is contained in:
parent
01be4032d8
commit
f55e903092
@ -171,6 +171,23 @@ link:{upgradingguide_link}[{upgradingguide_name}].
|
||||
|
||||
In previous versions of Keycloak when the last member of a User, Group or Client policy was deleted then that policy would also be deleted. Unfortunately this could lead to an escalation of privileges if the policy was used in an aggregate policy. To avoid privilege escalation the effect policies are no longer deleted and an administrator will need to update those policies.
|
||||
|
||||
= Keycloak CR cache-config-file option
|
||||
|
||||
The Keycloak CR now allows for specifying the `cache-config-file` option via the `cache` spec `configMapFile` field, for example:
|
||||
|
||||
----
|
||||
apiVersion: k8s.keycloak.org/v2alpha1
|
||||
kind: Keycloak
|
||||
metadata:
|
||||
name: example-kc
|
||||
spec:
|
||||
...
|
||||
cache:
|
||||
configMapFile:
|
||||
name: my-configmap
|
||||
key: config.xml
|
||||
----
|
||||
|
||||
= Updates to cookies
|
||||
|
||||
Cookie handling code has been refactored and improved, including a new Cookie Provider. This provides better consistency
|
||||
|
||||
@ -270,6 +270,12 @@ realm password policy.
|
||||
* `Hashing Algorithm: pbkdf2-sha256`
|
||||
* `Hashing Iterations: 27500`
|
||||
|
||||
= Operator Referenced Resource Polling
|
||||
|
||||
Secrets and ConfigMaps referenced via the Keycloak CR will now be polled for changes, rather than watched via the api server. It may take around 1 minute for changes to be detected.
|
||||
|
||||
This was done so to not require label manipulation on those resources. After upgrading if any Secret still has the operator.keycloak.org/component label, it may be removed or ignored.
|
||||
|
||||
= Renaming JPA provider configuration options for migration
|
||||
|
||||
After removal of the Map Store the following configuration options were renamed:
|
||||
|
||||
@ -103,10 +103,11 @@ spec:
|
||||
|
||||
Secret References are used by some dedicated options in the Keycloak CR, such as `tlsSecret`, or as a value in `additionalOptions`.
|
||||
|
||||
When specifying a Secret Reference, make sure that a Secret containing the referenced keys is present in the same namespace as the CR referencing it.
|
||||
Along with the {project_name} Server Deployment, the Operator adds special labels to the referenced Secrets to watch for changes.
|
||||
Similarly ConfigMap References are used by options such as the `configMapFile`.
|
||||
|
||||
When a referenced Secret is modified, the Operator performs a rolling restart of the {project_name} Deployment to pick up the changes.
|
||||
When specifying a Secret or ConfigMap Reference, make sure that a Secret or ConfigMap containing the referenced keys is present in the same namespace as the CR referencing it.
|
||||
|
||||
The operator will poll approximately every minute for changes to referenced Secrets or ConfigMaps. When a meaningful change is detected, the Operator performs a rolling restart of the {project_name} Deployment to pick up the changes.
|
||||
|
||||
=== Unsupported features
|
||||
|
||||
|
||||
@ -32,6 +32,7 @@ public interface Config {
|
||||
String image();
|
||||
String imagePullPolicy();
|
||||
boolean startOptimized();
|
||||
int pollIntervalSeconds();
|
||||
|
||||
Map<String, String> podLabels();
|
||||
}
|
||||
|
||||
@ -35,10 +35,6 @@ public final class Constants {
|
||||
public static final String MANAGED_BY_LABEL = "app.kubernetes.io/managed-by";
|
||||
public static final String MANAGED_BY_VALUE = "keycloak-operator";
|
||||
public static final String COMPONENT_LABEL = "app.kubernetes.io/component";
|
||||
public static final String KEYCLOAK_COMPONENT_LABEL = "operator.keycloak.org/component";
|
||||
public static final String KEYCLOAK_WATCHED_SECRET_HASH_ANNOTATION = "operator.keycloak.org/watched-secret-hash";
|
||||
public static final String KEYCLOAK_WATCHING_ANNOTATION = "operator.keycloak.org/watching-secrets";
|
||||
public static final String KEYCLOAK_MISSING_SECRETS_ANNOTATION = "operator.keycloak.org/missing-secrets";
|
||||
public static final String KEYCLOAK_MIGRATING_ANNOTATION = "operator.keycloak.org/migrating";
|
||||
|
||||
public static final String DEFAULT_LABELS_AS_STRING = "app=keycloak,app.kubernetes.io/managed-by=keycloak-operator";
|
||||
@ -70,6 +66,7 @@ public final class Constants {
|
||||
public static final String CERTIFICATES_FOLDER = "/mnt/certificates";
|
||||
|
||||
public static final String TRUSTSTORES_FOLDER = "/opt/keycloak/conf/truststores";
|
||||
public static final String CACHE_CONFIG_FOLDER = "/opt/keycloak/conf/cache";
|
||||
|
||||
public static String KEYCLOAK_HTTP_RELATIVE_PATH_KEY = "http-relative-path";
|
||||
public static final String KEYCLOAK_HTTP_RELATIVE_PATH_KEY = "http-relative-path";
|
||||
}
|
||||
|
||||
@ -74,7 +74,7 @@ public class KeycloakController implements Reconciler<Keycloak>, EventSourceInit
|
||||
Config config;
|
||||
|
||||
@Inject
|
||||
WatchedSecrets watchedSecrets;
|
||||
WatchedResources watchedResources;
|
||||
|
||||
@Inject
|
||||
KeycloakDistConfigurator distConfigurator;
|
||||
@ -94,7 +94,6 @@ public class KeycloakController implements Reconciler<Keycloak>, EventSourceInit
|
||||
|
||||
Map<String, EventSource> sources = new HashMap<>();
|
||||
sources.put("serviceSource", servicesEvent);
|
||||
sources.putAll(EventSourceInitializer.nameEventSources(watchedSecrets.getWatchedSecretsEventSource()));
|
||||
return sources;
|
||||
}
|
||||
|
||||
@ -103,7 +102,7 @@ public class KeycloakController implements Reconciler<Keycloak>, EventSourceInit
|
||||
String kcName = kc.getMetadata().getName();
|
||||
String namespace = kc.getMetadata().getNamespace();
|
||||
|
||||
Log.infof("--- Reconciling Keycloak: %s in namespace: %s", kcName, namespace);
|
||||
Log.debugf("--- Reconciling Keycloak: %s in namespace: %s", kcName, namespace);
|
||||
|
||||
boolean modifiedSpec = false;
|
||||
if (kc.getSpec().getInstances() == null) {
|
||||
@ -132,7 +131,7 @@ public class KeycloakController implements Reconciler<Keycloak>, EventSourceInit
|
||||
updateStatus(kc, context.getSecondaryResource(StatefulSet.class).orElse(null), statusAggregator, context);
|
||||
var status = statusAggregator.build();
|
||||
|
||||
Log.info("--- Reconciliation finished successfully");
|
||||
Log.debug("--- Reconciliation finished successfully");
|
||||
|
||||
UpdateControl<Keycloak> updateControl;
|
||||
if (status.equals(kc.getStatus())) {
|
||||
@ -143,10 +142,12 @@ public class KeycloakController implements Reconciler<Keycloak>, EventSourceInit
|
||||
updateControl = UpdateControl.updateStatus(kc);
|
||||
}
|
||||
|
||||
if (!status.isReady() || context.getSecondaryResource(StatefulSet.class)
|
||||
.map(s -> s.getMetadata().getAnnotations().get(Constants.KEYCLOAK_MISSING_SECRETS_ANNOTATION))
|
||||
.filter(Boolean::valueOf).isPresent()) {
|
||||
var statefulSet = context.getSecondaryResource(StatefulSet.class);
|
||||
|
||||
if (!status.isReady() || statefulSet.filter(watchedResources::hasMissing).isPresent()) {
|
||||
updateControl.rescheduleAfter(10, TimeUnit.SECONDS);
|
||||
} else if (statefulSet.filter(watchedResources::isWatching).isPresent()) {
|
||||
updateControl.rescheduleAfter(config.keycloak().pollIntervalSeconds(), TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
return updateControl;
|
||||
|
||||
@ -16,6 +16,8 @@
|
||||
*/
|
||||
package org.keycloak.operator.controllers;
|
||||
|
||||
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.EnvVarBuilder;
|
||||
import io.fabric8.kubernetes.api.model.EnvVarSource;
|
||||
@ -24,6 +26,7 @@ import io.fabric8.kubernetes.api.model.PodSpec;
|
||||
import io.fabric8.kubernetes.api.model.PodSpecFluent.ContainersNested;
|
||||
import io.fabric8.kubernetes.api.model.PodTemplateSpec;
|
||||
import io.fabric8.kubernetes.api.model.PodTemplateSpecFluent.SpecNested;
|
||||
import io.fabric8.kubernetes.api.model.Secret;
|
||||
import io.fabric8.kubernetes.api.model.SecretKeySelector;
|
||||
import io.fabric8.kubernetes.api.model.VolumeBuilder;
|
||||
import io.fabric8.kubernetes.api.model.VolumeMountBuilder;
|
||||
@ -42,6 +45,7 @@ import org.keycloak.operator.Utils;
|
||||
import org.keycloak.operator.crds.v2alpha1.deployment.Keycloak;
|
||||
import org.keycloak.operator.crds.v2alpha1.deployment.KeycloakSpec;
|
||||
import org.keycloak.operator.crds.v2alpha1.deployment.ValueOrSecret;
|
||||
import org.keycloak.operator.crds.v2alpha1.deployment.spec.CacheSpec;
|
||||
import org.keycloak.operator.crds.v2alpha1.deployment.spec.Truststore;
|
||||
import org.keycloak.operator.crds.v2alpha1.deployment.spec.TruststoreSource;
|
||||
import org.keycloak.operator.crds.v2alpha1.deployment.spec.UnsupportedSpec;
|
||||
@ -67,6 +71,8 @@ import static org.keycloak.operator.crds.v2alpha1.CRDUtils.isTlsConfigured;
|
||||
@KubernetesDependent(labelSelector = Constants.DEFAULT_LABELS_AS_STRING)
|
||||
public class KeycloakDeploymentDependentResource extends CRUDKubernetesDependentResource<StatefulSet, Keycloak> {
|
||||
|
||||
public static final String CACHE_CONFIG_FILE_MOUNT_NAME = "cache-config-file-configmap";
|
||||
|
||||
public static final String KC_TRUSTSTORE_PATHS = "KC_TRUSTSTORE_PATHS";
|
||||
|
||||
static final String JGROUPS_DNS_QUERY_PARAM = "-Djgroups.dns.query=";
|
||||
@ -77,7 +83,7 @@ public class KeycloakDeploymentDependentResource extends CRUDKubernetesDependent
|
||||
Config operatorConfig;
|
||||
|
||||
@Inject
|
||||
WatchedSecrets watchedSecrets;
|
||||
WatchedResources watchedResources;
|
||||
|
||||
@Inject
|
||||
KeycloakDistConfigurator distConfigurator;
|
||||
@ -93,19 +99,22 @@ public class KeycloakDeploymentDependentResource extends CRUDKubernetesDependent
|
||||
if (isTlsConfigured(primary)) {
|
||||
configureTLS(primary, baseDeployment, allSecrets);
|
||||
}
|
||||
addTruststores(primary, baseDeployment, allSecrets);
|
||||
Container kcContainer = baseDeployment.getSpec().getTemplate().getSpec().getContainers().get(0);
|
||||
addTruststores(primary, baseDeployment, kcContainer, allSecrets);
|
||||
addEnvVars(baseDeployment, primary, allSecrets);
|
||||
Optional.ofNullable(primary.getSpec().getCacheSpec())
|
||||
.ifPresent(c -> configureCache(primary, baseDeployment, kcContainer, c));
|
||||
|
||||
if (!allSecrets.isEmpty()) {
|
||||
watchedSecrets.annotateDeployment(new ArrayList<>(allSecrets), primary, baseDeployment);
|
||||
watchedResources.annotateDeployment(new ArrayList<>(allSecrets), Secret.class, baseDeployment, this.client);
|
||||
}
|
||||
|
||||
StatefulSet existingDeployment = context.getSecondaryResource(StatefulSet.class).orElse(null);
|
||||
if (existingDeployment == null) {
|
||||
Log.info("No existing Deployment found, using the default");
|
||||
Log.debug("No existing Deployment found, using the default");
|
||||
}
|
||||
else {
|
||||
Log.info("Existing Deployment found, handling migration");
|
||||
Log.debug("Existing Deployment found, handling migration");
|
||||
|
||||
// version 22 changed the match labels, account for older versions
|
||||
if (!existingDeployment.isMarkedForDeletion() && !hasExpectedMatchLabels(existingDeployment, primary)) {
|
||||
@ -119,8 +128,34 @@ public class KeycloakDeploymentDependentResource extends CRUDKubernetesDependent
|
||||
return baseDeployment;
|
||||
}
|
||||
|
||||
private void addTruststores(Keycloak keycloakCR, StatefulSet deployment, TreeSet<String> allSecrets) {
|
||||
var kcContainer = deployment.getSpec().getTemplate().getSpec().getContainers().get(0);
|
||||
private void configureCache(Keycloak keycloakCR, StatefulSet deployment, Container kcContainer, CacheSpec spec) {
|
||||
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");
|
||||
}
|
||||
|
||||
var volume = new VolumeBuilder()
|
||||
.withName(CACHE_CONFIG_FILE_MOUNT_NAME)
|
||||
.withNewConfigMap()
|
||||
.withName(configFile.getName())
|
||||
.withOptional(configFile.getOptional())
|
||||
.endConfigMap()
|
||||
.build();
|
||||
|
||||
var volumeMount = new VolumeMountBuilder()
|
||||
.withName(volume.getName())
|
||||
.withMountPath(Constants.CACHE_CONFIG_FOLDER)
|
||||
.build();
|
||||
|
||||
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, this.client);
|
||||
});
|
||||
}
|
||||
|
||||
private void addTruststores(Keycloak keycloakCR, StatefulSet deployment, Container kcContainer, TreeSet<String> allSecrets) {
|
||||
for (Truststore truststore : keycloakCR.getSpec().getTruststores().values()) {
|
||||
// for now we'll assume only secrets, later we can support configmaps
|
||||
TruststoreSource source = truststore.getSecret();
|
||||
@ -165,18 +200,6 @@ public class KeycloakDeploymentDependentResource extends CRUDKubernetesDependent
|
||||
allSecrets.add(keycloakCR.getSpec().getHttpSpec().getTlsSecret());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreated(Keycloak primary, StatefulSet created, Context<Keycloak> context) {
|
||||
watchedSecrets.addLabelsToWatchedSecrets(created);
|
||||
super.onCreated(primary, created, context);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onUpdated(Keycloak primary, StatefulSet updated, StatefulSet actual, Context<Keycloak> context) {
|
||||
watchedSecrets.addLabelsToWatchedSecrets(updated);
|
||||
super.onUpdated(primary, updated, actual, context);
|
||||
}
|
||||
|
||||
private boolean hasExpectedMatchLabels(StatefulSet statefulSet, Keycloak keycloak) {
|
||||
return Optional.ofNullable(statefulSet).map(s -> Utils.allInstanceLabels(keycloak).equals(s.getSpec().getSelector().getMatchLabels())).orElse(true);
|
||||
}
|
||||
@ -339,7 +362,7 @@ public class KeycloakDeploymentDependentResource extends CRUDKubernetesDependent
|
||||
.map(EnvVarSource::getSecretKeyRef).filter(Objects::nonNull).map(SecretKeySelector::getName)
|
||||
.filter(n -> !n.equals(adminSecretName)).collect(Collectors.toCollection(TreeSet::new));
|
||||
|
||||
Log.infof("Found config secrets names: %s", serverConfigSecretsNames);
|
||||
Log.debugf("Found config secrets names: %s", serverConfigSecretsNames);
|
||||
|
||||
allSecrets.addAll(serverConfigSecretsNames);
|
||||
}
|
||||
|
||||
@ -40,6 +40,7 @@ import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@ -66,6 +67,7 @@ public class KeycloakDistConfigurator {
|
||||
configureTransactions();
|
||||
configureHttp();
|
||||
configureDatabase();
|
||||
configureCache();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -108,6 +110,11 @@ public class KeycloakDistConfigurator {
|
||||
.mapOption("https-certificate-key-file", http -> (http.getTlsSecret() != null && !http.getTlsSecret().isEmpty()) ? Constants.CERTIFICATES_FOLDER + "/tls.key" : null);
|
||||
}
|
||||
|
||||
void configureCache() {
|
||||
optionMapper(keycloakCR -> keycloakCR.getSpec().getCacheSpec())
|
||||
.mapOption("cache-config-file", cache -> Optional.ofNullable(cache.getConfigMapFile()).map(c -> Constants.CACHE_CONFIG_FOLDER + "/" + c.getKey()).orElse(null));
|
||||
}
|
||||
|
||||
void configureDatabase() {
|
||||
optionMapper(keycloakCR -> keycloakCR.getSpec().getDatabaseSpec())
|
||||
.mapOption("db", DatabaseSpec::getVendor)
|
||||
|
||||
@ -0,0 +1,112 @@
|
||||
/*
|
||||
* 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.ConfigMap;
|
||||
import io.fabric8.kubernetes.api.model.HasMetadata;
|
||||
import io.fabric8.kubernetes.api.model.Secret;
|
||||
import io.fabric8.kubernetes.api.model.apps.StatefulSet;
|
||||
import io.fabric8.kubernetes.client.KubernetesClient;
|
||||
import io.fabric8.kubernetes.client.utils.Serialization;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
|
||||
@ApplicationScoped
|
||||
public class WatchedResources {
|
||||
public static final String KEYCLOAK_WATCHED_HASH_ANNOTATION_PREFIX = "operator.keycloak.org/watched-";
|
||||
public static final String KEYCLOAK_WATCHING_ANNOTATION_PREFIX = "operator.keycloak.org/watching-";
|
||||
public static final String KEYCLOAK_MISSING_ANNOTATION_PREFIX = "operator.keycloak.org/missing-";
|
||||
|
||||
/**
|
||||
* @param deployment mutable resource being reconciled, it will be updated with
|
||||
* annotations
|
||||
*/
|
||||
public <T extends HasMetadata> void annotateDeployment(List<String> names, Class<T> type, StatefulSet deployment,
|
||||
KubernetesClient client) {
|
||||
List<T> current = fetch(names, type, deployment.getMetadata().getNamespace(), client);
|
||||
String plural = HasMetadata.getPlural(type);
|
||||
deployment.getMetadata().getAnnotations().put(WatchedResources.KEYCLOAK_MISSING_ANNOTATION_PREFIX + plural,
|
||||
Boolean.valueOf(current.size() < names.size()).toString());
|
||||
deployment.getMetadata().getAnnotations().put(WatchedResources.KEYCLOAK_WATCHING_ANNOTATION_PREFIX + plural,
|
||||
names.stream().collect(Collectors.joining(";")));
|
||||
deployment.getSpec().getTemplate().getMetadata().getAnnotations()
|
||||
.put(WatchedResources.KEYCLOAK_WATCHED_HASH_ANNOTATION_PREFIX + HasMetadata.getKind(type).toLowerCase() + "-hash", getHash(current));
|
||||
}
|
||||
|
||||
static Object getData(Object object) {
|
||||
if (object instanceof Secret) {
|
||||
return ((Secret) object).getData();
|
||||
}
|
||||
if (object instanceof ConfigMap) {
|
||||
return ((ConfigMap) object).getData();
|
||||
}
|
||||
return object;
|
||||
}
|
||||
|
||||
public boolean hasMissing(StatefulSet deployment) {
|
||||
return deployment.getMetadata().getAnnotations().entrySet().stream()
|
||||
.anyMatch(e -> e.getKey().startsWith(WatchedResources.KEYCLOAK_MISSING_ANNOTATION_PREFIX)
|
||||
&& Boolean.valueOf(e.getValue()));
|
||||
}
|
||||
|
||||
public boolean isWatching(StatefulSet deployment) {
|
||||
return deployment.getMetadata().getAnnotations().entrySet().stream()
|
||||
.anyMatch(e -> e.getKey().startsWith(WatchedResources.KEYCLOAK_WATCHING_ANNOTATION_PREFIX)
|
||||
&& e.getValue() != null && !e.getValue().isEmpty());
|
||||
}
|
||||
|
||||
public <T extends HasMetadata> String getHash(List<T> current) {
|
||||
try {
|
||||
// using hashes as it's more robust than resource versions that can change e.g.
|
||||
// just when adding a label
|
||||
// Uses a fips compliant hash
|
||||
var messageDigest = MessageDigest.getInstance("SHA-256");
|
||||
|
||||
current.stream().map(s -> Serialization.asYaml(getData(s)).getBytes(StandardCharsets.UTF_8))
|
||||
.forEachOrdered(s -> messageDigest.update(s));
|
||||
|
||||
return new BigInteger(1, messageDigest.digest()).toString(16);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private <T extends HasMetadata> List<T> fetch(List<String> names, Class<T> type, String namespace,
|
||||
KubernetesClient client) {
|
||||
return names.stream().map(n -> client.resources(type).inNamespace(namespace).withName(n).get())
|
||||
.filter(Objects::nonNull).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public List<String> getNames(StatefulSet deployment, Class<? extends HasMetadata> type) {
|
||||
return Optional
|
||||
.ofNullable(deployment.getMetadata().getAnnotations().get(WatchedResources.KEYCLOAK_WATCHING_ANNOTATION_PREFIX + HasMetadata.getPlural(type)))
|
||||
.filter(watching -> !watching.isEmpty())
|
||||
.map(watching -> watching.split(";")).map(Arrays::asList).orElse(List.of());
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,44 +0,0 @@
|
||||
/*
|
||||
* 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.apps.StatefulSet;
|
||||
import io.javaoperatorsdk.operator.processing.event.source.EventSource;
|
||||
|
||||
import org.keycloak.operator.crds.v2alpha1.deployment.Keycloak;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Provides a mechanism to track secrets
|
||||
*
|
||||
* @author Vaclav Muzikar <vmuzikar@redhat.com>
|
||||
*/
|
||||
public interface WatchedSecrets {
|
||||
public static final String WATCHED_SECRETS_LABEL_VALUE = "watched-secret";
|
||||
|
||||
/**
|
||||
* @param deployment mutable resource being reconciled, it will be updated with annotations
|
||||
*/
|
||||
void annotateDeployment(List<String> desiredWatchedSecretsNames, Keycloak keycloakCR, StatefulSet deployment);
|
||||
|
||||
EventSource getWatchedSecretsEventSource();
|
||||
|
||||
void addLabelsToWatchedSecrets(StatefulSet deployment);
|
||||
|
||||
}
|
||||
@ -1,156 +0,0 @@
|
||||
/*
|
||||
* Copyright 2021 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.Secret;
|
||||
import io.fabric8.kubernetes.api.model.SecretBuilder;
|
||||
import io.fabric8.kubernetes.api.model.apps.StatefulSet;
|
||||
import io.fabric8.kubernetes.client.KubernetesClient;
|
||||
import io.fabric8.kubernetes.client.utils.Serialization;
|
||||
import io.javaoperatorsdk.operator.api.reconciler.Context;
|
||||
import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration;
|
||||
import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext;
|
||||
import io.javaoperatorsdk.operator.api.reconciler.EventSourceInitializer;
|
||||
import io.javaoperatorsdk.operator.api.reconciler.Reconciler;
|
||||
import io.javaoperatorsdk.operator.api.reconciler.UpdateControl;
|
||||
import io.javaoperatorsdk.operator.processing.event.ResourceID;
|
||||
import io.javaoperatorsdk.operator.processing.event.source.EventSource;
|
||||
import io.javaoperatorsdk.operator.processing.event.source.IndexerResourceCache;
|
||||
import io.javaoperatorsdk.operator.processing.event.source.inbound.SimpleInboundEventSource;
|
||||
import io.quarkus.logging.Log;
|
||||
|
||||
import org.keycloak.operator.Constants;
|
||||
import org.keycloak.operator.crds.v2alpha1.deployment.Keycloak;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
|
||||
@ApplicationScoped
|
||||
@ControllerConfiguration(labelSelector = Constants.KEYCLOAK_COMPONENT_LABEL + "=" + WatchedSecrets.WATCHED_SECRETS_LABEL_VALUE)
|
||||
public class WatchedSecretsController implements Reconciler<Secret>, EventSourceInitializer<Secret>, WatchedSecrets {
|
||||
|
||||
private volatile KubernetesClient client;
|
||||
|
||||
private final SimpleInboundEventSource eventSource = new SimpleInboundEventSource();
|
||||
|
||||
private volatile IndexerResourceCache<Secret> secrets;
|
||||
|
||||
@Override
|
||||
public Map<String, EventSource> prepareEventSources(EventSourceContext<Secret> context) {
|
||||
this.secrets = context.getPrimaryCache();
|
||||
this.client = context.getClient();
|
||||
return Map.of();
|
||||
}
|
||||
|
||||
@Override
|
||||
public UpdateControl<Secret> reconcile(Secret resource, Context<Secret> context) throws Exception {
|
||||
// find all statefulsets to notify
|
||||
// - this could detect whether the reconciliation is even necessary if we track individual hashes
|
||||
var ret = client.apps().statefulSets().inNamespace(resource.getMetadata().getNamespace())
|
||||
.withLabels(Constants.DEFAULT_LABELS).list().getItems().stream()
|
||||
.filter(statefulSet -> getSecretNames(statefulSet).contains(resource.getMetadata().getName()))
|
||||
.map(statefulSet -> new ResourceID(statefulSet.getMetadata().getName(),
|
||||
resource.getMetadata().getNamespace()))
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
if (ret.isEmpty()) {
|
||||
Log.infof("Removing label from Secret \"%s\"", resource.getMetadata().getName());
|
||||
|
||||
return UpdateControl.updateResource(new SecretBuilder(resource)
|
||||
.editMetadata()
|
||||
.removeFromLabels(Constants.KEYCLOAK_COMPONENT_LABEL)
|
||||
.endMetadata()
|
||||
.build());
|
||||
} else {
|
||||
ret.forEach(eventSource::propagateEvent);
|
||||
}
|
||||
|
||||
return UpdateControl.noUpdate();
|
||||
}
|
||||
|
||||
@Override
|
||||
public EventSource getWatchedSecretsEventSource() {
|
||||
return eventSource;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void annotateDeployment(List<String> desiredWatchedSecretsNames, Keycloak keycloakCR, StatefulSet deployment) {
|
||||
List<Secret> currentSecrets = fetchSecrets(desiredWatchedSecretsNames, keycloakCR.getMetadata().getNamespace());
|
||||
deployment.getMetadata().getAnnotations().put(Constants.KEYCLOAK_MISSING_SECRETS_ANNOTATION,
|
||||
Boolean.valueOf(currentSecrets.size() < desiredWatchedSecretsNames.size()).toString());
|
||||
deployment.getMetadata().getAnnotations().put(Constants.KEYCLOAK_WATCHING_ANNOTATION, desiredWatchedSecretsNames.stream().collect(Collectors.joining(";")));
|
||||
deployment.getSpec().getTemplate().getMetadata().getAnnotations().put(Constants.KEYCLOAK_WATCHED_SECRET_HASH_ANNOTATION, getSecretHash(currentSecrets));
|
||||
}
|
||||
|
||||
private List<Secret> fetchSecrets(List<String> secretsNames, String namespace) {
|
||||
return secretsNames.stream()
|
||||
.map(n -> Optional.ofNullable(secrets).flatMap(cache -> cache.get(new ResourceID(n, namespace)))
|
||||
.orElseGet(() -> client.secrets().inNamespace(namespace).withName(n).get()))
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public String getSecretHash(List<Secret> currentSecrets) {
|
||||
try {
|
||||
// using hashes as it's more robust than resource versions that can change e.g. just when adding a label
|
||||
// Uses a fips compliant hash
|
||||
var messageDigest = MessageDigest.getInstance("SHA-256");
|
||||
|
||||
currentSecrets.stream()
|
||||
.map(s -> Serialization.asYaml(s.getData()).getBytes(StandardCharsets.UTF_8))
|
||||
.forEachOrdered(s -> messageDigest.update(s));
|
||||
|
||||
return new BigInteger(1, messageDigest.digest()).toString(16);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addLabelsToWatchedSecrets(StatefulSet deployment) {
|
||||
for (Secret secret : fetchSecrets(getSecretNames(deployment), deployment.getMetadata().getNamespace())) {
|
||||
if (!secret.getMetadata().getLabels().containsKey(Constants.KEYCLOAK_COMPONENT_LABEL)) {
|
||||
|
||||
Log.infof("Adding label to Secret \"%s\"", secret.getMetadata().getName());
|
||||
|
||||
client.resource(secret).accept(s -> {
|
||||
s.getMetadata().getLabels().put(Constants.KEYCLOAK_COMPONENT_LABEL, WatchedSecrets.WATCHED_SECRETS_LABEL_VALUE);
|
||||
s.getMetadata().setResourceVersion(null);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public List<String> getSecretNames(StatefulSet deployment) {
|
||||
return Optional
|
||||
.ofNullable(deployment.getMetadata().getAnnotations().get(Constants.KEYCLOAK_WATCHING_ANNOTATION))
|
||||
.filter(watching -> !watching.isEmpty())
|
||||
.map(watching -> watching.split(";")).map(Arrays::asList).orElse(List.of());
|
||||
}
|
||||
|
||||
}
|
||||
@ -19,6 +19,7 @@ package org.keycloak.operator.crds.v2alpha1.deployment;
|
||||
import io.fabric8.kubernetes.api.model.LocalObjectReference;
|
||||
import io.fabric8.kubernetes.model.annotation.SpecReplicas;
|
||||
|
||||
import org.keycloak.operator.crds.v2alpha1.deployment.spec.CacheSpec;
|
||||
import org.keycloak.operator.crds.v2alpha1.deployment.spec.DatabaseSpec;
|
||||
import org.keycloak.operator.crds.v2alpha1.deployment.spec.FeatureSpec;
|
||||
import org.keycloak.operator.crds.v2alpha1.deployment.spec.HostnameSpec;
|
||||
@ -90,6 +91,10 @@ public class KeycloakSpec {
|
||||
@JsonPropertyDescription("In this section you can configure Keycloak truststores.")
|
||||
private Map<String, Truststore> truststores = new LinkedHashMap<>();
|
||||
|
||||
@JsonProperty("cache")
|
||||
@JsonPropertyDescription("In this section you can configure Keycloak's cache")
|
||||
private CacheSpec cacheSpec;
|
||||
|
||||
public HttpSpec getHttpSpec() {
|
||||
return httpSpec;
|
||||
}
|
||||
@ -200,4 +205,12 @@ public class KeycloakSpec {
|
||||
this.truststores = truststores;
|
||||
}
|
||||
|
||||
public CacheSpec getCacheSpec() {
|
||||
return cacheSpec;
|
||||
}
|
||||
|
||||
public void setCacheSpec(CacheSpec cache) {
|
||||
this.cacheSpec = cache;
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,39 @@
|
||||
/*
|
||||
* 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 io.fabric8.kubernetes.api.model.ConfigMapKeySelector;
|
||||
import io.sundr.builder.annotations.Buildable;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
@Buildable(editableEnabled = false, builderPackage = "io.fabric8.kubernetes.api.builder")
|
||||
public class CacheSpec {
|
||||
|
||||
private ConfigMapKeySelector configMapFile;
|
||||
|
||||
public ConfigMapKeySelector getConfigMapFile() {
|
||||
return configMapFile;
|
||||
}
|
||||
|
||||
public void setConfigMapFile(ConfigMapKeySelector configMapFile) {
|
||||
this.configMapFile = configMapFile;
|
||||
}
|
||||
|
||||
}
|
||||
@ -8,6 +8,7 @@ quarkus.banner.enabled=false
|
||||
operator.keycloak.image=${RELATED_IMAGE_KEYCLOAK:quay.io/keycloak/keycloak:nightly}
|
||||
operator.keycloak.image-pull-policy=Always
|
||||
operator.keycloak.start-optimized=false
|
||||
operator.keycloak.poll-interval-seconds=60
|
||||
|
||||
# https://quarkus.io/guides/deploying-to-kubernetes#environment-variables-from-keyvalue-pairs
|
||||
quarkus.kubernetes.env.vars.related-image-keycloak=${operator.keycloak.image}
|
||||
|
||||
@ -44,6 +44,7 @@ import org.keycloak.operator.crds.v2alpha1.deployment.Keycloak;
|
||||
import org.keycloak.operator.crds.v2alpha1.deployment.KeycloakStatusCondition;
|
||||
import org.keycloak.operator.crds.v2alpha1.deployment.ValueOrSecret;
|
||||
import org.keycloak.operator.crds.v2alpha1.deployment.spec.HostnameSpecBuilder;
|
||||
import org.keycloak.operator.testsuite.unit.WatchedResourcesTest;
|
||||
import org.keycloak.operator.testsuite.utils.CRAssert;
|
||||
import org.keycloak.operator.testsuite.utils.K8sUtils;
|
||||
|
||||
@ -251,7 +252,7 @@ public class KeycloakDeploymentTest extends BaseOperatorTest {
|
||||
|
||||
// managed changes
|
||||
deployment.getSpec().getTemplate().getSpec().getContainers().get(0).setEnv(List.of(flandersEnvVar));
|
||||
String originalAnnotationValue = deployment.getMetadata().getAnnotations().put(Constants.KEYCLOAK_WATCHING_ANNOTATION, "not-right");
|
||||
String originalAnnotationValue = deployment.getMetadata().getAnnotations().put(WatchedResourcesTest.KEYCLOAK_WATCHING_ANNOTATION, "not-right");
|
||||
|
||||
deployment.getMetadata().setResourceVersion(null);
|
||||
k8sclient.resource(deployment).update();
|
||||
@ -266,7 +267,7 @@ public class KeycloakDeploymentTest extends BaseOperatorTest {
|
||||
assertThat(d.getMetadata().getLabels().entrySet().containsAll(labels.entrySet())).isTrue();
|
||||
// managed changes should get reverted
|
||||
assertThat(d.getSpec()).isEqualTo(expectedSpec); // specs should be reconciled expected merged state
|
||||
assertThat(d.getMetadata().getAnnotations().get(Constants.KEYCLOAK_WATCHING_ANNOTATION)).isEqualTo(originalAnnotationValue);
|
||||
assertThat(d.getMetadata().getAnnotations().get(WatchedResourcesTest.KEYCLOAK_WATCHING_ANNOTATION)).isEqualTo(originalAnnotationValue);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -24,8 +24,8 @@ import io.quarkus.test.junit.QuarkusTest;
|
||||
|
||||
import org.awaitility.Awaitility;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.keycloak.operator.Constants;
|
||||
import org.keycloak.operator.crds.v2alpha1.deployment.spec.TruststoreBuilder;
|
||||
import org.keycloak.operator.testsuite.unit.WatchedResourcesTest;
|
||||
import org.keycloak.operator.testsuite.utils.K8sUtils;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
@ -46,8 +46,8 @@ public class KeycloakTruststoresTests extends BaseOperatorTest {
|
||||
Awaitility.await().ignoreExceptions().untilAsserted(() -> {
|
||||
StatefulSet statefulSet = stsResource.get();
|
||||
assertEquals("true",
|
||||
statefulSet.getMetadata().getAnnotations().get(Constants.KEYCLOAK_MISSING_SECRETS_ANNOTATION));
|
||||
assertTrue(statefulSet.getMetadata().getAnnotations().get(Constants.KEYCLOAK_WATCHING_ANNOTATION)
|
||||
statefulSet.getMetadata().getAnnotations().get(WatchedResourcesTest.KEYCLOAK_MISSING_SECRETS_ANNOTATION));
|
||||
assertTrue(statefulSet.getMetadata().getAnnotations().get(WatchedResourcesTest.KEYCLOAK_WATCHING_ANNOTATION)
|
||||
.contains("xyz"));
|
||||
});
|
||||
}
|
||||
@ -64,7 +64,7 @@ public class KeycloakTruststoresTests extends BaseOperatorTest {
|
||||
Resource<StatefulSet> stsResource = k8sclient.resources(StatefulSet.class).withName(deploymentName);
|
||||
StatefulSet statefulSet = stsResource.get();
|
||||
assertEquals("false",
|
||||
statefulSet.getMetadata().getAnnotations().get(Constants.KEYCLOAK_MISSING_SECRETS_ANNOTATION));
|
||||
statefulSet.getMetadata().getAnnotations().get(WatchedResourcesTest.KEYCLOAK_MISSING_SECRETS_ANNOTATION));
|
||||
assertTrue(statefulSet.getSpec().getTemplate().getSpec().getContainers().get(0).getVolumeMounts().stream()
|
||||
.anyMatch(v -> v.getMountPath()
|
||||
.equals("/opt/keycloak/conf/truststores/secret-example-truststore-secret")));
|
||||
|
||||
@ -26,11 +26,10 @@ import io.quarkus.test.junit.QuarkusTest;
|
||||
import org.awaitility.Awaitility;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.keycloak.operator.Constants;
|
||||
import org.keycloak.operator.controllers.WatchedSecrets;
|
||||
import org.keycloak.operator.crds.v2alpha1.deployment.Keycloak;
|
||||
import org.keycloak.operator.crds.v2alpha1.deployment.ValueOrSecret;
|
||||
import org.keycloak.operator.crds.v2alpha1.deployment.spec.HostnameSpecBuilder;
|
||||
import org.keycloak.operator.testsuite.unit.WatchedResourcesTest;
|
||||
|
||||
import java.util.Base64;
|
||||
import java.util.HashSet;
|
||||
@ -41,6 +40,7 @@ import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.keycloak.operator.testsuite.utils.K8sUtils.deployKeycloak;
|
||||
|
||||
/**
|
||||
@ -56,9 +56,6 @@ public class WatchedSecretsTest extends BaseOperatorTest {
|
||||
Secret dbSecret = getDbSecret();
|
||||
Secret tlsSecret = getTlsSecret();
|
||||
|
||||
assertThat(dbSecret.getMetadata().getLabels()).containsEntry(Constants.KEYCLOAK_COMPONENT_LABEL, WatchedSecrets.WATCHED_SECRETS_LABEL_VALUE);
|
||||
assertThat(tlsSecret.getMetadata().getLabels()).containsEntry(Constants.KEYCLOAK_COMPONENT_LABEL, WatchedSecrets.WATCHED_SECRETS_LABEL_VALUE);
|
||||
|
||||
Log.info("Updating DB Secret, expecting restart");
|
||||
testDeploymentRestarted(Set.of(kc), Set.of(), () -> {
|
||||
dbSecret.getData().put(UUID.randomUUID().toString(), "YmxhaGJsYWg=");
|
||||
@ -105,22 +102,19 @@ public class WatchedSecretsTest extends BaseOperatorTest {
|
||||
var kc = getTestKeycloakDeployment(false);
|
||||
deployKeycloak(k8sclient, kc, true);
|
||||
|
||||
Secret dbSecret = getDbSecret();
|
||||
|
||||
Log.info("Updating KC to not to rely on DB Secret");
|
||||
hardcodeDBCredsInCR(kc);
|
||||
testDeploymentRestarted(Set.of(kc), Set.of(), () -> {
|
||||
deployKeycloak(k8sclient, kc, false, false);
|
||||
});
|
||||
|
||||
Log.info("Updating DB Secret to trigger clean-up process");
|
||||
testDeploymentRestarted(Set.of(), Set.of(kc), () -> {
|
||||
var dbSecret = getDbSecret();
|
||||
dbSecret.getMetadata().getLabels().put(UUID.randomUUID().toString(), "YmxhaGJsYWg");
|
||||
k8sclient.resource(dbSecret).update();
|
||||
});
|
||||
|
||||
Awaitility.await().untilAsserted(() -> {
|
||||
Log.info("Checking labels on DB Secret");
|
||||
assertThat(getDbSecret().getMetadata().getLabels()).doesNotContainKey(Constants.KEYCLOAK_COMPONENT_LABEL);
|
||||
Log.info("Checking StatefulSet annotations");
|
||||
assertFalse(k8sclient.resources(StatefulSet.class).withName(kc.getMetadata().getName()).get().getMetadata()
|
||||
.getAnnotations().get(WatchedResourcesTest.KEYCLOAK_WATCHING_ANNOTATION)
|
||||
.contains(dbSecret.getMetadata().getName()));
|
||||
});
|
||||
}
|
||||
|
||||
@ -223,4 +217,4 @@ public class WatchedSecretsTest extends BaseOperatorTest {
|
||||
public void restoreDBSecret() {
|
||||
deployDBSecret();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -64,6 +64,11 @@ public class KeycloakDistConfiguratorTest {
|
||||
testFirstClassCitizen(Map.of("transaction-xa-enabled", "false"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cache() {
|
||||
testFirstClassCitizen(Map.of("cache-config-file", "/opt/keycloak/conf/cache/file.xml"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void http() {
|
||||
final Map<String, String> expectedValues = Map.of(
|
||||
|
||||
@ -23,6 +23,8 @@ 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.Volume;
|
||||
import io.fabric8.kubernetes.api.model.VolumeMount;
|
||||
import io.fabric8.kubernetes.api.model.apps.StatefulSet;
|
||||
import io.fabric8.kubernetes.api.model.apps.StatefulSetBuilder;
|
||||
import io.javaoperatorsdk.operator.api.reconciler.Context;
|
||||
@ -31,9 +33,10 @@ import io.quarkus.test.junit.QuarkusTest;
|
||||
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.keycloak.operator.Constants;
|
||||
import org.keycloak.operator.Utils;
|
||||
import org.keycloak.operator.controllers.KeycloakDeploymentDependentResource;
|
||||
import org.keycloak.operator.controllers.WatchedSecretsController;
|
||||
import org.keycloak.operator.controllers.WatchedResources;
|
||||
import org.keycloak.operator.crds.v2alpha1.deployment.Keycloak;
|
||||
import org.keycloak.operator.crds.v2alpha1.deployment.KeycloakBuilder;
|
||||
import org.keycloak.operator.crds.v2alpha1.deployment.KeycloakSpecBuilder;
|
||||
@ -58,7 +61,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
public class PodTemplateTest {
|
||||
|
||||
@InjectMock
|
||||
WatchedSecretsController watchedSecrets;
|
||||
WatchedResources watchedResources;
|
||||
|
||||
@Inject
|
||||
KeycloakDeploymentDependentResource deployment;
|
||||
@ -419,4 +422,26 @@ public class PodTemplateTest {
|
||||
assertThat(podTemplate.getSpec().getContainers().get(0).getCommand().contains(KeycloakDeploymentDependentResource.OPTIMIZED_ARG));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCacheConfigFileMount() {
|
||||
// Arrange
|
||||
PodTemplateSpec additionalPodTemplate = null;
|
||||
|
||||
// Act
|
||||
var podTemplate = getDeployment(additionalPodTemplate, null,
|
||||
s -> s.withNewCacheSpec().withNewConfigMapFile("file.xml", "cm", null).endCacheSpec())
|
||||
.getSpec().getTemplate();
|
||||
|
||||
// Assert
|
||||
VolumeMount volumeMount = podTemplate.getSpec().getContainers().get(0).getVolumeMounts().stream()
|
||||
.filter(vm -> vm.getName().equals(KeycloakDeploymentDependentResource.CACHE_CONFIG_FILE_MOUNT_NAME))
|
||||
.findFirst().orElseThrow();
|
||||
assertThat(volumeMount.getMountPath()).isEqualTo(Constants.CACHE_CONFIG_FOLDER);
|
||||
|
||||
Volume volume = podTemplate.getSpec().getVolumes().stream()
|
||||
.filter(v -> v.getName().equals(KeycloakDeploymentDependentResource.CACHE_CONFIG_FILE_MOUNT_NAME))
|
||||
.findFirst().orElseThrow();
|
||||
assertThat(volume.getConfigMap().getName()).isEqualTo("cm");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -17,14 +17,14 @@
|
||||
|
||||
package org.keycloak.operator.testsuite.unit;
|
||||
|
||||
import io.fabric8.kubernetes.api.model.ConfigMapBuilder;
|
||||
import io.fabric8.kubernetes.api.model.Secret;
|
||||
import io.fabric8.kubernetes.api.model.SecretBuilder;
|
||||
import io.fabric8.kubernetes.api.model.apps.StatefulSetBuilder;
|
||||
import io.quarkus.test.junit.QuarkusTest;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.keycloak.operator.Constants;
|
||||
import org.keycloak.operator.controllers.WatchedSecretsController;
|
||||
import org.keycloak.operator.controllers.WatchedResources;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
@ -36,22 +36,29 @@ import jakarta.inject.Inject;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
@QuarkusTest
|
||||
public class WatchedSecretsControllerTest {
|
||||
public class WatchedResourcesTest {
|
||||
|
||||
public static final String KEYCLOAK_WATCHING_ANNOTATION = "operator.keycloak.org/watching-secrets";
|
||||
public static final String KEYCLOAK_MISSING_SECRETS_ANNOTATION = "operator.keycloak.org/missing-secrets";
|
||||
|
||||
@Inject
|
||||
WatchedSecretsController watchedSecretsController;
|
||||
WatchedResources watchedResources;
|
||||
|
||||
@Test
|
||||
public void testSecretHashing() {
|
||||
assertEquals("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", watchedSecretsController.getSecretHash(List.of()));
|
||||
assertEquals("b5655bfe4d4e130f5023a76a5de0906cf84eb5895bda5d44642673f9eb4024bf", watchedSecretsController.getSecretHash(List.of(newSecret(Map.of("a", "b")), newSecret(Map.of("c", "d")))));
|
||||
public void testHashing() {
|
||||
assertEquals("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", watchedResources.getHash(List.of()));
|
||||
assertEquals("b5655bfe4d4e130f5023a76a5de0906cf84eb5895bda5d44642673f9eb4024bf", watchedResources.getHash(List.of(newSecret(Map.of("a", "b")), newSecret(Map.of("c", "d")))));
|
||||
assertEquals("d526224334e65c71095be909b2d14c52f1589abb84a3c76fbe79dd75d7132fbb",
|
||||
watchedResources.getHash(List.of(new ConfigMapBuilder().withNewMetadata().withName("x")
|
||||
.withAnnotations(Map.of(UUID.randomUUID().toString(), UUID.randomUUID().toString()))
|
||||
.endMetadata().withData(Map.of("a", "b")).build())));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetSecretNames() {
|
||||
assertEquals(List.of(), watchedSecretsController.getSecretNames(new StatefulSetBuilder().withNewMetadata().addToAnnotations(Constants.KEYCLOAK_WATCHING_ANNOTATION, "").endMetadata().build()));
|
||||
assertEquals(Arrays.asList("something"), watchedSecretsController.getSecretNames(new StatefulSetBuilder().withNewMetadata().addToAnnotations(Constants.KEYCLOAK_WATCHING_ANNOTATION, "something").endMetadata().build()));
|
||||
assertEquals(Arrays.asList("x", "y"), watchedSecretsController.getSecretNames(new StatefulSetBuilder().withNewMetadata().addToAnnotations(Constants.KEYCLOAK_WATCHING_ANNOTATION, "x;y").endMetadata().build()));
|
||||
assertEquals(List.of(), watchedResources.getNames(new StatefulSetBuilder().withNewMetadata().addToAnnotations(WatchedResourcesTest.KEYCLOAK_WATCHING_ANNOTATION, "").endMetadata().build(), Secret.class));
|
||||
assertEquals(Arrays.asList("something"), watchedResources.getNames(new StatefulSetBuilder().withNewMetadata().addToAnnotations(WatchedResourcesTest.KEYCLOAK_WATCHING_ANNOTATION, "something").endMetadata().build(), Secret.class));
|
||||
assertEquals(Arrays.asList("x", "y"), watchedResources.getNames(new StatefulSetBuilder().withNewMetadata().addToAnnotations(WatchedResourcesTest.KEYCLOAK_WATCHING_ANNOTATION, "x;y").endMetadata().build(), Secret.class));
|
||||
}
|
||||
|
||||
private Secret newSecret(Map<String, String> data) {
|
||||
@ -6,4 +6,6 @@ quarkus.operator-sdk.start-operator=false
|
||||
quarkus.log.level=INFO
|
||||
|
||||
operator.keycloak.pod-labels."test.label"=foobar
|
||||
operator.keycloak.pod-labels."testLabelWithExpression"=${OPERATOR_TEST_LABEL_EXPRESSION}
|
||||
operator.keycloak.pod-labels."testLabelWithExpression"=${OPERATOR_TEST_LABEL_EXPRESSION}
|
||||
# allow the watching tests to complete more quickly
|
||||
operator.keycloak.poll-interval-seconds=10
|
||||
@ -43,6 +43,10 @@ spec:
|
||||
adminUrl: https://www.my-admin-hostname.org:8448/something
|
||||
strict: true
|
||||
strictBackchannel: true
|
||||
cache:
|
||||
configMapFile:
|
||||
name: my-config-map
|
||||
key: file.xml
|
||||
features:
|
||||
enabled:
|
||||
- docker
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user