Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions operator/src/main/java/org/keycloak/operator/Constants.java
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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<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 = 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<ValueOrSecret> DEFAULT_DIST_CONFIG_LIST = List.of(
new ValueOrSecret("health-enabled", "true"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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().getClientConfiguration().getNamespace();
Expand Down Expand Up @@ -86,9 +89,7 @@ public Map<String, EventSource> prepareEventSources(EventSourceContext<Keycloak>

return EventSourceInitializer.nameEventSources(statefulSetEvent,
servicesEvent,
ingressesEvent,
WatchedSecretsStore.getStoreEventSource(client, namespace),
WatchedSecretsStore.getWatchedSecretsEventSource(client, namespace));
ingressesEvent, watchedSecrets.getWatchedSecretsEventSource());
}

@Override
Expand All @@ -104,14 +105,9 @@ public UpdateControl<Keycloak> reconcile(Keycloak kc, Context<Keycloak> 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -62,6 +63,7 @@ public class KeycloakDeployment extends OperatorManagedResource implements Statu
private final String adminSecretName;

private Set<String> serverConfigSecretsNames;
private WatchedSecrets watchedSecrets;

private boolean migrationInProgress;

Expand All @@ -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<HasMetadata> getReconciledResource() {
StatefulSet baseDeployment = new StatefulSetBuilder(this.baseDeployment).build(); // clone not to change the base template
Expand Down Expand Up @@ -111,6 +117,11 @@ public Optional<HasMetadata> getReconciledResource() {
migrateDeployment(existingDeployment, reconciledDeployment);
}

var configSecretsNames = getConfigSecretsNames();
if (!configSecretsNames.isEmpty() && watchedSecrets != null) {
watchedSecrets.processWatched(configSecretsNames, keycloakCR, reconciledDeployment);
}

return Optional.of(reconciledDeployment);
}

Expand Down Expand Up @@ -495,7 +506,8 @@ private List<EnvVar> getEnvVars() {

return envVars;
}


@Override
public void updateStatus(KeycloakStatusAggregator status) {
status.apply(b -> b.withSelector(Constants.DEFAULT_LABELS_AS_STRING));
validatePodTemplate(status);
Expand All @@ -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
Expand All @@ -525,24 +537,17 @@ public void updateStatus(KeycloakStatusAggregator status) {
distConfigurator.validateOptions(status);
}

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
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
Expand Down
Original file line number Diff line number Diff line change
@@ -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 <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 processWatched(List<String> desiredWatchedSecretsNames, Keycloak keycloakCR, StatefulSet deployment);

EventSource getWatchedSecretsEventSource();

}
Original file line number Diff line number Diff line change
@@ -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<Secret>, EventSourceInitializer<Secret> {

@Inject
KubernetesClient client;

@Inject
WatchedSecretsStatefulSetController watchedSecretsStatefulSetController;

@Override
public Map<String, EventSource> prepareEventSources(EventSourceContext<Secret> context) {
watchedSecretsStatefulSetController.setSecrets(context.getPrimaryCache());
return Map.of();
}

@Override
public UpdateControl<Secret> reconcile(Secret resource, Context<Secret> context) throws Exception {
return watchedSecretsStatefulSetController.reconcileSecret(resource, context);
}

}
Loading