From 61d90f08f82f202bc2d866b61a6f6fb8bdc10345 Mon Sep 17 00:00:00 2001 From: Steve Hawkins Date: Thu, 22 Jun 2023 16:09:20 -0400 Subject: [PATCH] Removes the watched secret store by directly using the statefulsets closes #21125 --- .../java/org/keycloak/operator/Constants.java | 7 +- .../controllers/KeycloakController.java | 14 +- .../controllers/KeycloakDeployment.java | 29 ++- .../operator/controllers/WatchedSecrets.java | 42 ++++ .../controllers/WatchedSecretsController.java | 60 +++++ .../WatchedSecretsStatefulSetController.java | 158 ++++++++++++ .../controllers/WatchedSecretsStore.java | 230 ------------------ .../integration/WatchedSecretsTest.java | 6 +- 8 files changed, 288 insertions(+), 258 deletions(-) create mode 100644 operator/src/main/java/org/keycloak/operator/controllers/WatchedSecrets.java create mode 100644 operator/src/main/java/org/keycloak/operator/controllers/WatchedSecretsController.java create mode 100644 operator/src/main/java/org/keycloak/operator/controllers/WatchedSecretsStatefulSetController.java delete mode 100644 operator/src/main/java/org/keycloak/operator/controllers/WatchedSecretsStore.java diff --git a/operator/src/main/java/org/keycloak/operator/Constants.java b/operator/src/main/java/org/keycloak/operator/Constants.java index 3db139b608cc..4ca7bb51363f 100644 --- a/operator/src/main/java/org/keycloak/operator/Constants.java +++ b/operator/src/main/java/org/keycloak/operator/Constants.java @@ -22,7 +22,6 @@ import java.util.List; import java.util.Map; import java.util.TreeMap; -import java.util.stream.Collectors; public final class Constants { public static final String CRDS_GROUP = "k8s.keycloak.org"; @@ -34,15 +33,15 @@ 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 = "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 DEFAULT_LABELS = Collections.unmodifiableMap(new TreeMap<>(Map.of( "app", NAME, MANAGED_BY_LABEL, MANAGED_BY_VALUE ))); - public static final String DEFAULT_LABELS_AS_STRING = DEFAULT_LABELS.entrySet().stream() - .map(e -> e.getKey() + "=" + e.getValue()) - .collect(Collectors.joining(",")); + public static final String DEFAULT_LABELS_AS_STRING = "app=keycloak,app.kubernetes.io/managed-by=keycloak-operator"; public static final List DEFAULT_DIST_CONFIG_LIST = List.of( new ValueOrSecret("health-enabled", "true"), diff --git a/operator/src/main/java/org/keycloak/operator/controllers/KeycloakController.java b/operator/src/main/java/org/keycloak/operator/controllers/KeycloakController.java index 94d308bb6560..ad5db0fa9cbd 100644 --- a/operator/src/main/java/org/keycloak/operator/controllers/KeycloakController.java +++ b/operator/src/main/java/org/keycloak/operator/controllers/KeycloakController.java @@ -55,6 +55,9 @@ public class KeycloakController implements Reconciler, EventSourceInit @Inject Config config; + @Inject + WatchedSecrets watchedSecrets; + @Override public Map prepareEventSources(EventSourceContext context) { String namespace = context.getControllerConfiguration().getConfigurationService().getClientConfiguration().getNamespace(); @@ -86,9 +89,7 @@ public Map prepareEventSources(EventSourceContext return EventSourceInitializer.nameEventSources(statefulSetEvent, servicesEvent, - ingressesEvent, - WatchedSecretsStore.getStoreEventSource(client, namespace), - WatchedSecretsStore.getWatchedSecretsEventSource(client, namespace)); + ingressesEvent, watchedSecrets.getWatchedSecretsEventSource()); } @Override @@ -104,14 +105,9 @@ public UpdateControl reconcile(Keycloak kc, Context context) 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); diff --git a/operator/src/main/java/org/keycloak/operator/controllers/KeycloakDeployment.java b/operator/src/main/java/org/keycloak/operator/controllers/KeycloakDeployment.java index f5b8985555d8..76312a60bf1d 100644 --- a/operator/src/main/java/org/keycloak/operator/controllers/KeycloakDeployment.java +++ b/operator/src/main/java/org/keycloak/operator/controllers/KeycloakDeployment.java @@ -46,6 +46,7 @@ import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.TreeSet; import java.util.function.Consumer; import java.util.stream.Collectors; @@ -62,6 +63,7 @@ public class KeycloakDeployment extends OperatorManagedResource implements Statu private final String adminSecretName; private Set serverConfigSecretsNames; + private WatchedSecrets watchedSecrets; private boolean migrationInProgress; @@ -84,6 +86,10 @@ public KeycloakDeployment(KubernetesClient client, Config config, Keycloak keycl mergePodTemplate(this.baseDeployment.getSpec().getTemplate()); } + public void setWatchedSecrets(WatchedSecrets watchedSecrets) { + this.watchedSecrets = watchedSecrets; + } + @Override public Optional getReconciledResource() { StatefulSet baseDeployment = new StatefulSetBuilder(this.baseDeployment).build(); // clone not to change the base template @@ -111,6 +117,11 @@ public Optional getReconciledResource() { migrateDeployment(existingDeployment, reconciledDeployment); } + var configSecretsNames = getConfigSecretsNames(); + if (!configSecretsNames.isEmpty() && watchedSecrets != null) { + watchedSecrets.processWatched(configSecretsNames, keycloakCR, reconciledDeployment); + } + return Optional.of(reconciledDeployment); } @@ -495,7 +506,8 @@ private List getEnvVars() { return envVars; } - + + @Override public void updateStatus(KeycloakStatusAggregator status) { status.apply(b -> b.withSelector(Constants.DEFAULT_LABELS_AS_STRING)); validatePodTemplate(status); @@ -512,7 +524,7 @@ public void updateStatus(KeycloakStatusAggregator status) { status.addNotReadyMessage("Waiting for more replicas"); } } - + if (migrationInProgress) { status.addNotReadyMessage("Performing Keycloak upgrade, scaling down the deployment"); } else if (existingDeployment.getStatus() != null @@ -525,10 +537,10 @@ public void updateStatus(KeycloakStatusAggregator status) { distConfigurator.validateOptions(status); } - public Set getConfigSecretsNames() { - Set ret = new HashSet<>(serverConfigSecretsNames); + public List getConfigSecretsNames() { + TreeSet ret = new TreeSet<>(serverConfigSecretsNames); ret.addAll(distConfigurator.getSecretNames()); - return ret; + return new ArrayList<>(ret); } @Override @@ -536,13 +548,6 @@ public String getName() { 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 diff --git a/operator/src/main/java/org/keycloak/operator/controllers/WatchedSecrets.java b/operator/src/main/java/org/keycloak/operator/controllers/WatchedSecrets.java new file mode 100644 index 000000000000..0641f03118d3 --- /dev/null +++ b/operator/src/main/java/org/keycloak/operator/controllers/WatchedSecrets.java @@ -0,0 +1,42 @@ +/* + * 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 + */ +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 processWatched(List desiredWatchedSecretsNames, Keycloak keycloakCR, StatefulSet deployment); + + EventSource getWatchedSecretsEventSource(); + +} \ No newline at end of file diff --git a/operator/src/main/java/org/keycloak/operator/controllers/WatchedSecretsController.java b/operator/src/main/java/org/keycloak/operator/controllers/WatchedSecretsController.java new file mode 100644 index 000000000000..5f884f0c7273 --- /dev/null +++ b/operator/src/main/java/org/keycloak/operator/controllers/WatchedSecretsController.java @@ -0,0 +1,60 @@ +/* + * 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.client.KubernetesClient; +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.source.EventSource; + +import org.keycloak.operator.Constants; + +import java.util.Map; + +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, EventSourceInitializer { + + @Inject + KubernetesClient client; + + @Inject + WatchedSecretsStatefulSetController watchedSecretsStatefulSetController; + + @Override + public Map prepareEventSources(EventSourceContext context) { + watchedSecretsStatefulSetController.setSecrets(context.getPrimaryCache()); + return Map.of(); + } + + @Override + public UpdateControl reconcile(Secret resource, Context context) throws Exception { + return watchedSecretsStatefulSetController.reconcileSecret(resource, context); + } + +} diff --git a/operator/src/main/java/org/keycloak/operator/controllers/WatchedSecretsStatefulSetController.java b/operator/src/main/java/org/keycloak/operator/controllers/WatchedSecretsStatefulSetController.java new file mode 100644 index 000000000000..fb991d5eddcf --- /dev/null +++ b/operator/src/main/java/org/keycloak/operator/controllers/WatchedSecretsStatefulSetController.java @@ -0,0 +1,158 @@ +/* + * 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.DEFAULT_LABELS_AS_STRING) +public class WatchedSecretsStatefulSetController implements Reconciler, EventSourceInitializer, WatchedSecrets { + + @Inject + KubernetesClient client; + + private final SimpleInboundEventSource eventSource = new SimpleInboundEventSource(); + + private volatile IndexerResourceCache cache; + private volatile IndexerResourceCache secrets; + + @Override + public void processWatched(List desiredWatchedSecretsNames, Keycloak keycloakCR, StatefulSet deployment) { + List 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)); + } + + @Override + public Map prepareEventSources(EventSourceContext context) { + cache = context.getPrimaryCache(); + return Map.of(); + } + + @Override + public UpdateControl reconcile(StatefulSet resource, Context context) throws Exception { + for (Secret secret : fetchSecrets(getSecretNames(resource), resource.getMetadata().getNamespace())) { + if (!secret.getMetadata().getLabels().containsKey(Constants.KEYCLOAK_COMPONENT_LABEL)) { + + Log.infof("Adding label to Secret \"%s\"", secret.getMetadata().getName()); + + client.resource(secret).edit(s -> new SecretBuilder(s) + .editMetadata() + .withResourceVersion(null) + .addToLabels(Constants.KEYCLOAK_COMPONENT_LABEL, WatchedSecrets.WATCHED_SECRETS_LABEL_VALUE) + .endMetadata() + .build()); + } + } + + return UpdateControl.noUpdate(); + } + + private List fetchSecrets(List 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 currentSecrets) { + try { + // using hashes as it's more robust than resource versions that can change e.g. just when adding a label + var messageDigest = MessageDigest.getInstance("MD5"); + + currentSecrets.stream() + .map(s -> Serialization.asYaml(s.getData()).getBytes(StandardCharsets.UTF_8)) + .forEach(s -> messageDigest.update(s)); + + return new BigInteger(1, messageDigest.digest()).toString(16); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + } + + public void setSecrets(IndexerResourceCache secrets) { + this.secrets = secrets; + } + + public List getSecretNames(StatefulSet deployment) { + return Optional + .ofNullable(deployment.getMetadata().getAnnotations().get(Constants.KEYCLOAK_WATCHING_ANNOTATION)) + .map(watching -> watching.split(";")).map(Arrays::asList).orElse(List.of()); + } + + public UpdateControl reconcileSecret(Secret resource, Context context) { + // find all statefulsets to notify + // - this could detect whether the reconciliation is even necessary if we track individual hashes + var ret = cache.list() + .filter(statefulSet -> getSecretNames(statefulSet).contains(resource.getMetadata().getName())) + .map(statefulSet -> new ResourceID(statefulSet.getMetadata().getName(), + resource.getMetadata().getNamespace())) + .collect(Collectors.toSet()); + + if (ret.isEmpty()) { + return UpdateControl.updateResource(new SecretBuilder(resource) + .editMetadata() + .removeFromLabels(Constants.KEYCLOAK_COMPONENT_LABEL) + .endMetadata() + .build()); + } + + ret.forEach(eventSource::propagateEvent); + + return UpdateControl.noUpdate(); + } + + @Override + public EventSource getWatchedSecretsEventSource() { + return eventSource; + } + +} diff --git a/operator/src/main/java/org/keycloak/operator/controllers/WatchedSecretsStore.java b/operator/src/main/java/org/keycloak/operator/controllers/WatchedSecretsStore.java deleted file mode 100644 index c46958f4c55d..000000000000 --- a/operator/src/main/java/org/keycloak/operator/controllers/WatchedSecretsStore.java +++ /dev/null @@ -1,230 +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.crds.v2alpha1.deployment.Keycloak; - -import java.math.BigInteger; -import java.nio.charset.StandardCharsets; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.util.Base64; -import java.util.Collections; -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 - */ -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 lastObservedVersions; - private final Map currentVersions; - private final Set currentSecrets; - - public WatchedSecretsStore(Set 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 getReconciledResource() { - Secret secret = existingStore != null ? existingStore : getNewStore(); - secret.setData(null); - secret.setStringData(currentVersions); - - return Optional.of(secret); - } - - @Override - protected void setDefaultLabels(HasMetadata resource) { - super.setDefaultLabels(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 getNewLastObservedVersions() { - if (existingStore != null && existingStore.getData() != null) { - return existingStore.getData().entrySet().stream() - .collect(Collectors.toMap( - Map.Entry::getKey, - e -> new String(Base64.getDecoder().decode(e.getValue())) - )); - } - else { - return Collections.emptyMap(); - } - } - - private Map 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 new BigInteger(1, bytes).toString(16); - } - catch (NoSuchAlgorithmException e) { - throw new RuntimeException(e); - } - } - - private Set fetchCurrentSecrets(Set 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 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 informerConfiguration = InformerConfiguration - .from(Secret.class) - .withLabelSelector(Constants.KEYCLOAK_COMPONENT_LABEL + "=" + WATCHED_SECRETS_LABEL_VALUE) - .withNamespaces(namespace) - .withSecondaryToPrimaryMapper(secret -> { - // get all stores - List 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); - } -} diff --git a/operator/src/test/java/org/keycloak/operator/testsuite/integration/WatchedSecretsTest.java b/operator/src/test/java/org/keycloak/operator/testsuite/integration/WatchedSecretsTest.java index 1789f0051ae5..4fd528798f15 100644 --- a/operator/src/test/java/org/keycloak/operator/testsuite/integration/WatchedSecretsTest.java +++ b/operator/src/test/java/org/keycloak/operator/testsuite/integration/WatchedSecretsTest.java @@ -25,7 +25,7 @@ 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 void testSecretsAreWatched() { 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(), () -> {