Also replaces md5 usage with sha-256 (#21162)

closes #21125
This commit is contained in:
Steven Hawkins 2023-07-17 04:48:04 -04:00 committed by GitHub
parent d015fa392c
commit fa83034474
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 314 additions and 257 deletions

View file

@ -22,6 +22,8 @@ import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public final class Constants {
public static final String CRDS_GROUP = "k8s.keycloak.org";
@ -34,13 +36,14 @@ public final class Constants {
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 Map<String, String> DEFAULT_LABELS = Collections.unmodifiableMap(new TreeMap<>(Map.of(
"app", NAME,
MANAGED_BY_LABEL, MANAGED_BY_VALUE
)));
public static final String DEFAULT_LABELS_AS_STRING = "app=keycloak,app.kubernetes.io/managed-by=keycloak-operator";
public static final String DEFAULT_LABELS_AS_STRING = Utils.toSelectorString(DEFAULT_LABELS);
public static final Map<String, String> DEFAULT_LABELS = Collections
.unmodifiableMap(Stream.of(DEFAULT_LABELS_AS_STRING.split(",")).map(s -> s.split("="))
.collect(Collectors.toMap(e -> e[0], e -> e[1], (u1, u2) -> u1, TreeMap::new)));
public static final List<ValueOrSecret> DEFAULT_DIST_CONFIG_LIST = List.of(
new ValueOrSecret("health-enabled", "true"),

View file

@ -55,6 +55,9 @@ public class KeycloakController implements Reconciler<Keycloak>, EventSourceInit
@Inject
Config config;
@Inject
WatchedSecrets watchedSecrets;
@Override
public Map<String, EventSource> prepareEventSources(EventSourceContext<Keycloak> context) {
String namespace = context.getControllerConfiguration().getConfigurationService().getKubernetesClient().getNamespace();
@ -89,9 +92,7 @@ public class KeycloakController implements Reconciler<Keycloak>, EventSourceInit
return EventSourceInitializer.nameEventSources(statefulSetEvent,
servicesEvent,
ingressesEvent,
WatchedSecretsStore.getStoreEventSource(client, namespace),
WatchedSecretsStore.getWatchedSecretsEventSource(client, namespace));
ingressesEvent, watchedSecrets.getWatchedSecretsEventSource());
}
@Override
@ -107,14 +108,9 @@ public class KeycloakController implements Reconciler<Keycloak>, EventSourceInit
kcAdminSecret.createOrUpdateReconciled();
var kcDeployment = new KeycloakDeployment(client, config, kc, context.getSecondaryResource(StatefulSet.class).orElse(null), kcAdminSecret.getName());
var watchedSecrets = new WatchedSecretsStore(kcDeployment.getConfigSecretsNames(), client, kc);
kcDeployment.setWatchedSecrets(watchedSecrets);
kcDeployment.createOrUpdateReconciled();
if (watchedSecrets.changesDetected()) {
Log.info("Config Secrets modified, restarting deployment");
kcDeployment.rollingRestart();
}
kcDeployment.updateStatus(statusAggregator);
watchedSecrets.createOrUpdateReconciled();
var kcService = new KeycloakService(client, kc);
kcService.updateStatus(statusAggregator);

View file

@ -21,7 +21,6 @@ import io.fabric8.kubernetes.api.model.ContainerStateWaiting;
import io.fabric8.kubernetes.api.model.EnvVar;
import io.fabric8.kubernetes.api.model.EnvVarBuilder;
import io.fabric8.kubernetes.api.model.EnvVarSourceBuilder;
import io.fabric8.kubernetes.api.model.HasMetadata;
import io.fabric8.kubernetes.api.model.PodSpec;
import io.fabric8.kubernetes.api.model.PodSpecFluent.ContainersNested;
import io.fabric8.kubernetes.api.model.PodStatus;
@ -54,13 +53,14 @@ import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.TreeSet;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static org.keycloak.operator.crds.v2alpha1.CRDUtils.isTlsConfigured;
public class KeycloakDeployment extends OperatorManagedResource implements StatusUpdater<KeycloakStatusAggregator> {
public class KeycloakDeployment extends OperatorManagedResource<StatefulSet> implements StatusUpdater<KeycloakStatusAggregator> {
private final Config operatorConfig;
private final KeycloakDistConfigurator distConfigurator;
@ -71,6 +71,7 @@ public class KeycloakDeployment extends OperatorManagedResource implements Statu
private final String adminSecretName;
private Set<String> serverConfigSecretsNames;
private WatchedSecrets watchedSecrets;
private boolean migrationInProgress;
@ -87,8 +88,13 @@ public class KeycloakDeployment extends OperatorManagedResource implements Statu
addRemainingEnvVars();
}
public void setWatchedSecrets(WatchedSecrets watchedSecrets) {
this.watchedSecrets = watchedSecrets;
}
@Override
public Optional<HasMetadata> getReconciledResource() {
public Optional<StatefulSet> getReconciledResource() {
StatefulSet baseDeployment = new StatefulSetBuilder(this.baseDeployment).build(); // clone not to change the base template
if (existingDeployment == null) {
Log.info("No existing Deployment found, using the default");
}
@ -102,6 +108,12 @@ public class KeycloakDeployment extends OperatorManagedResource implements Statu
migrateDeployment(existingDeployment, baseDeployment);
}
var configSecretsNames = getConfigSecretsNames();
if (!configSecretsNames.isEmpty() && watchedSecrets != null) {
watchedSecrets.annotateDeployment(configSecretsNames, keycloakCR, baseDeployment);
}
return Optional.of(baseDeployment);
}
@ -109,6 +121,16 @@ public class KeycloakDeployment extends OperatorManagedResource implements Statu
return Optional.ofNullable(statefulSet).map(s -> getInstanceLabels().equals(s.getSpec().getSelector().getMatchLabels())).orElse(true);
}
@Override
public Optional<StatefulSet> createOrUpdateReconciled() {
var ret = super.createOrUpdateReconciled();
// after the change to the statefulset has been "committed", start watching
if (watchedSecrets != null) {
ret.map(StatefulSet.class::cast).ifPresent(watchedSecrets::addLabelsToWatchedSecrets);
}
return ret;
}
public void validatePodTemplate(KeycloakStatusAggregator status) {
var spec = getPodTemplateSpec();
if (spec.isEmpty()) {
@ -391,10 +413,10 @@ public class KeycloakDeployment extends OperatorManagedResource implements Statu
});
}
public Set<String> getConfigSecretsNames() {
Set<String> ret = new HashSet<>(serverConfigSecretsNames);
public List<String> getConfigSecretsNames() {
TreeSet<String> ret = new TreeSet<>(serverConfigSecretsNames);
ret.addAll(distConfigurator.getSecretNames());
return ret;
return new ArrayList<>(ret);
}
@Override
@ -402,13 +424,6 @@ public class KeycloakDeployment extends OperatorManagedResource implements Statu
return keycloakCR.getMetadata().getName();
}
public void rollingRestart() {
client.apps().statefulSets()
.inNamespace(getNamespace())
.withName(getName())
.rolling().restart();
}
public void migrateDeployment(StatefulSet previousDeployment, StatefulSet reconciledDeployment) {
if (previousDeployment == null
|| previousDeployment.getSpec() == null

View file

@ -20,7 +20,6 @@ package org.keycloak.operator.controllers;
import io.fabric8.kubernetes.api.model.HasMetadata;
import io.fabric8.kubernetes.api.model.OwnerReference;
import io.fabric8.kubernetes.api.model.OwnerReferenceBuilder;
import io.fabric8.kubernetes.client.CustomResource;
import io.fabric8.kubernetes.client.KubernetesClient;
import io.fabric8.kubernetes.client.KubernetesClientException;
import io.fabric8.kubernetes.client.dsl.base.PatchContext;
@ -40,20 +39,20 @@ import java.util.Optional;
*
* @author Vaclav Muzikar <vmuzikar@redhat.com>
*/
public abstract class OperatorManagedResource {
public abstract class OperatorManagedResource<T extends HasMetadata> {
private static final String KEYCLOAK_OPERATOR_FIELD_MANAGER = "keycloak-operator";
protected KubernetesClient client;
protected CustomResource<?, ?> cr;
protected HasMetadata cr;
public OperatorManagedResource(KubernetesClient client, CustomResource<?, ?> cr) {
public OperatorManagedResource(KubernetesClient client, HasMetadata cr) {
this.client = client;
this.cr = cr;
}
protected abstract Optional<HasMetadata> getReconciledResource();
protected abstract Optional<T> getReconciledResource();
public void createOrUpdateReconciled() {
getReconciledResource().ifPresent(resource -> {
public Optional<T> createOrUpdateReconciled() {
return getReconciledResource().map(resource -> {
try {
setInstanceLabels(resource);
setOwnerReferences(resource);
@ -80,6 +79,7 @@ public abstract class OperatorManagedResource {
}
}
Log.debugf("Successfully created or updated resource: %s", resource);
return resource;
} catch (Exception e) {
Log.error("Failed to create or update resource");
Log.error(Serialization.asYaml(resource));

View file

@ -0,0 +1,44 @@
/*
* 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);
}

View file

@ -0,0 +1,155 @@
/*
* 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.Optional;
import java.util.stream.Collectors;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import static io.javaoperatorsdk.operator.api.reconciler.Constants.WATCH_CURRENT_NAMESPACE;
@ApplicationScoped
@ControllerConfiguration(namespaces = WATCH_CURRENT_NAMESPACE, labelSelector = Constants.KEYCLOAK_COMPONENT_LABEL + "=" + WatchedSecrets.WATCHED_SECRETS_LABEL_VALUE)
public class WatchedSecretsController implements Reconciler<Secret>, EventSourceInitializer<Secret>, WatchedSecrets {
@Inject
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();
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_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).require()))
.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());
}
}

View file

@ -1,219 +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.HasMetadata;
import io.fabric8.kubernetes.api.model.Secret;
import io.fabric8.kubernetes.api.model.SecretBuilder;
import io.fabric8.kubernetes.client.KubernetesClient;
import io.fabric8.kubernetes.client.utils.Serialization;
import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration;
import io.javaoperatorsdk.operator.processing.event.ResourceID;
import io.javaoperatorsdk.operator.processing.event.source.EventSource;
import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource;
import io.javaoperatorsdk.operator.processing.event.source.informer.Mappers;
import io.quarkus.logging.Log;
import org.keycloak.operator.Constants;
import org.keycloak.operator.Utils;
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.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
/**
* Represents a version store of Secrets that are watched by a CR but is not owned by it. E.g. Secrets with
* credentials provided by user.
*
* It is backed by a Secret which holds a list of watched Secrets together with their last observed version. It marks
* all the watched Secrets with a label indicating which CRs are watching that resource.
*
* @author Vaclav Muzikar <vmuzikar@redhat.com>
*/
public class WatchedSecretsStore extends OperatorManagedResource {
public static final String COMPONENT = "secrets-store";
public static final String WATCHED_SECRETS_LABEL_VALUE = "watched-secret";
public static final String STORE_SUFFIX = "-" + COMPONENT;
private final Secret existingStore; // a Secret to store the last observed versions
// key is name of the secret
private final Map<String, String> lastObservedVersions;
private final Map<String, String> currentVersions;
private final Set<Secret> currentSecrets;
public WatchedSecretsStore(Set<String> desiredWatchedSecretsNames, KubernetesClient client, Keycloak kc) {
super(client, kc);
existingStore = fetchExistingStore();
lastObservedVersions = getNewLastObservedVersions();
currentSecrets = fetchCurrentSecrets(desiredWatchedSecretsNames);
currentVersions = getNewCurrentVersions();
}
/**
* @return true if any of the watched Secrets was changed, false otherwise (incl. if it's a newly watched Secret)
*/
public boolean changesDetected() {
return currentVersions.entrySet().stream().anyMatch(e -> {
String prevVersion = lastObservedVersions.get(e.getKey());
return prevVersion != null && !prevVersion.equals(e.getValue());
});
}
@Override
protected Optional<HasMetadata> getReconciledResource() {
Secret secret = getNewStore();
secret.setData(currentVersions);
return Optional.of(secret);
}
@Override
protected void setInstanceLabels(HasMetadata resource) {
super.setInstanceLabels(resource);
resource.getMetadata().getLabels().put(Constants.COMPONENT_LABEL, COMPONENT);
}
@Override
public void createOrUpdateReconciled() {
super.createOrUpdateReconciled();
addLabelsToWatchedSecrets();
}
public void addLabelsToWatchedSecrets() {
for (Secret secret : currentSecrets) {
if (secret.getMetadata() == null
|| secret.getMetadata().getLabels() == null
|| !secret.getMetadata().getLabels().containsKey(Constants.KEYCLOAK_COMPONENT_LABEL)) {
Log.infof("Adding label to Secret \"%s\"", secret.getMetadata().getName());
client.secrets().inNamespace(secret.getMetadata().getNamespace()).withName(secret.getMetadata().getName())
.edit(s -> new SecretBuilder(s)
.editMetadata()
.addToLabels(Constants.KEYCLOAK_COMPONENT_LABEL, WATCHED_SECRETS_LABEL_VALUE)
.endMetadata()
.build());
}
}
}
private Secret fetchExistingStore() {
return client.secrets().inNamespace(getNamespace()).withName(getName()).get();
}
private Secret getNewStore() {
return new SecretBuilder()
.withNewMetadata()
.withName(getName())
.withNamespace(getNamespace())
.endMetadata()
.build();
}
private Map<String, String> getNewLastObservedVersions() {
return Optional.ofNullable(existingStore).map(Secret::getData).orElse(Map.of());
}
private Map<String, String> getNewCurrentVersions() {
return currentSecrets.stream()
.collect(Collectors.toMap(s -> s.getMetadata().getName(), this::getSecretVersion));
}
private String getSecretVersion(Secret secret) {
String serializedData = Serialization.asYaml(secret.getData());
try {
// using hashes as it's more robust than resource versions that can change e.g. just when adding a label
byte[] bytes = MessageDigest.getInstance("MD5").digest(serializedData.getBytes(StandardCharsets.UTF_8));
return Utils.asBase64(new BigInteger(1, bytes).toString(16));
}
catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
private Set<Secret> fetchCurrentSecrets(Set<String> secretsNames) {
return secretsNames.stream()
.map(n -> client.secrets().inNamespace(getNamespace()).withName(n).require())
.collect(Collectors.toSet());
}
@Override
public String getName() {
return cr.getMetadata().getName() + STORE_SUFFIX;
}
public static EventSource getStoreEventSource(KubernetesClient client, String namespace) {
InformerConfiguration<Secret> informerConfiguration = InformerConfiguration
.from(Secret.class)
.withLabelSelector(Constants.COMPONENT_LABEL + "=" + COMPONENT)
.withNamespaces(namespace)
.withSecondaryToPrimaryMapper(Mappers.fromOwnerReference())
.build();
return new InformerEventSource<>(informerConfiguration, client);
}
private static void cleanObsoleteLabelFromSecret(KubernetesClient client, Secret secret) {
client.secrets().inNamespace(secret.getMetadata().getNamespace()).withName(secret.getMetadata().getName())
.edit(s -> new SecretBuilder(s)
.editMetadata()
.removeFromLabels(Constants.KEYCLOAK_COMPONENT_LABEL)
.endMetadata()
.build()
);
}
public static EventSource getWatchedSecretsEventSource(KubernetesClient client, String namespace) {
InformerConfiguration<Secret> informerConfiguration = InformerConfiguration
.from(Secret.class)
.withLabelSelector(Constants.KEYCLOAK_COMPONENT_LABEL + "=" + WATCHED_SECRETS_LABEL_VALUE)
.withNamespaces(namespace)
.withSecondaryToPrimaryMapper(secret -> {
// get all stores
List<Secret> stores = client.secrets().inNamespace(namespace).withLabel(Constants.COMPONENT_LABEL, COMPONENT).list().getItems();
// find all CR names that are watching this Secret
var ret = stores.stream()
// check if any of the stores tracks this secret
.filter(store -> store.getData().containsKey(secret.getMetadata().getName()))
.map(store -> {
String crName = store.getMetadata().getName().split(STORE_SUFFIX)[0];
return new ResourceID(crName, namespace);
})
.collect(Collectors.toSet());
if (ret.isEmpty()) {
Log.infof("No CRs watching \"%s\" Secret, cleaning up labels", secret.getMetadata().getName());
cleanObsoleteLabelFromSecret(client, secret);
Log.debug("Labels removed");
}
return ret;
})
.build();
return new InformerEventSource<>(informerConfiguration, client);
}
}

View file

@ -84,7 +84,7 @@ public class ClusteringTest extends BaseOperatorTest {
// the main resources are ready, check for the expected dependents
checkInstanceCount(1, StatefulSet.class, kc, kc1);
checkInstanceCount(2, Secret.class, kc, kc1);
checkInstanceCount(1, Secret.class, kc, kc1);
checkInstanceCount(1, Ingress.class, kc, kc1);
checkInstanceCount(2, Service.class, kc, kc1);

View file

@ -25,7 +25,7 @@ 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.WatchedSecretsStore;
import org.keycloak.operator.controllers.WatchedSecrets;
import org.keycloak.operator.crds.v2alpha1.deployment.Keycloak;
import org.keycloak.operator.crds.v2alpha1.deployment.KeycloakStatusCondition;
import org.keycloak.operator.crds.v2alpha1.deployment.ValueOrSecret;
@ -58,8 +58,8 @@ public class WatchedSecretsTest extends BaseOperatorTest {
Secret dbSecret = getDbSecret();
Secret tlsSecret = getTlsSecret();
assertThat(dbSecret.getMetadata().getLabels()).containsEntry(Constants.KEYCLOAK_COMPONENT_LABEL, WatchedSecretsStore.WATCHED_SECRETS_LABEL_VALUE);
assertThat(tlsSecret.getMetadata().getLabels()).containsEntry(Constants.KEYCLOAK_COMPONENT_LABEL, WatchedSecretsStore.WATCHED_SECRETS_LABEL_VALUE);
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(), () -> {

View file

@ -0,0 +1,63 @@
/*
* 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.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 java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import jakarta.inject.Inject;
import static org.junit.jupiter.api.Assertions.assertEquals;
@QuarkusTest
public class WatchedSecretsControllerTest {
@Inject
WatchedSecretsController watchedSecretsController;
@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")))));
}
@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()));
}
private Secret newSecret(Map<String, String> data) {
return new SecretBuilder().withNewMetadata().withName(UUID.randomUUID().toString())
.withLabels(Map.of(UUID.randomUUID().toString(), UUID.randomUUID().toString())).endMetadata()
.withData(data).build();
}
}