Converts keycloakdeployment to a dependent resource (#22591)
Closes #22225
This commit is contained in:
parent
dd37e02140
commit
a65af2d254
19 changed files with 698 additions and 959 deletions
|
@ -129,6 +129,11 @@
|
||||||
<artifactId>quarkus-junit5</artifactId>
|
<artifactId>quarkus-junit5</artifactId>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.quarkus</groupId>
|
||||||
|
<artifactId>quarkus-junit5-mockito</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.assertj</groupId>
|
<groupId>org.assertj</groupId>
|
||||||
<artifactId>assertj-core</artifactId>
|
<artifactId>assertj-core</artifactId>
|
||||||
|
|
|
@ -39,6 +39,7 @@ public final class Constants {
|
||||||
public static final String KEYCLOAK_WATCHED_SECRET_HASH_ANNOTATION = "operator.keycloak.org/watched-secret-hash";
|
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_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_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";
|
public static final String DEFAULT_LABELS_AS_STRING = "app=keycloak,app.kubernetes.io/managed-by=keycloak-operator";
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
|
|
||||||
package org.keycloak.operator;
|
package org.keycloak.operator;
|
||||||
|
|
||||||
|
import io.fabric8.kubernetes.api.model.HasMetadata;
|
||||||
import io.fabric8.kubernetes.client.KubernetesClient;
|
import io.fabric8.kubernetes.client.KubernetesClient;
|
||||||
|
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
@ -24,6 +25,7 @@ import java.time.ZoneOffset;
|
||||||
import java.time.ZonedDateTime;
|
import java.time.ZonedDateTime;
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
@ -55,4 +57,10 @@ public final class Utils {
|
||||||
.collect(Collectors.joining(","));
|
.collect(Collectors.joining(","));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static Map<String, String> allInstanceLabels(HasMetadata primary) {
|
||||||
|
var labels = new LinkedHashMap<>(Constants.DEFAULT_LABELS);
|
||||||
|
labels.put(Constants.INSTANCE_LABEL, primary.getMetadata().getName());
|
||||||
|
return labels;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,7 +27,7 @@ public class KeycloakAdminSecretDependentResource extends KubernetesDependentRes
|
||||||
return new SecretBuilder()
|
return new SecretBuilder()
|
||||||
.withNewMetadata()
|
.withNewMetadata()
|
||||||
.withName(getName(primary))
|
.withName(getName(primary))
|
||||||
.addToLabels(OperatorManagedResource.allInstanceLabels(primary))
|
.addToLabels(Utils.allInstanceLabels(primary))
|
||||||
.withNamespace(primary.getMetadata().getNamespace())
|
.withNamespace(primary.getMetadata().getNamespace())
|
||||||
.endMetadata()
|
.endMetadata()
|
||||||
.withType("kubernetes.io/basic-auth")
|
.withType("kubernetes.io/basic-auth")
|
||||||
|
|
|
@ -16,10 +16,15 @@
|
||||||
*/
|
*/
|
||||||
package org.keycloak.operator.controllers;
|
package org.keycloak.operator.controllers;
|
||||||
|
|
||||||
|
import io.fabric8.kubernetes.api.model.ContainerState;
|
||||||
|
import io.fabric8.kubernetes.api.model.ContainerStateWaiting;
|
||||||
|
import io.fabric8.kubernetes.api.model.PodSpec;
|
||||||
|
import io.fabric8.kubernetes.api.model.PodStatus;
|
||||||
import io.fabric8.kubernetes.api.model.Service;
|
import io.fabric8.kubernetes.api.model.Service;
|
||||||
import io.fabric8.kubernetes.api.model.apps.StatefulSet;
|
import io.fabric8.kubernetes.api.model.apps.StatefulSet;
|
||||||
import io.fabric8.kubernetes.client.KubernetesClient;
|
import io.fabric8.kubernetes.client.readiness.Readiness;
|
||||||
import io.fabric8.kubernetes.client.utils.KubernetesResourceUtil;
|
import io.fabric8.kubernetes.client.utils.KubernetesResourceUtil;
|
||||||
|
import io.fabric8.kubernetes.client.utils.Serialization;
|
||||||
import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration;
|
import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration;
|
||||||
import io.javaoperatorsdk.operator.api.reconciler.Context;
|
import io.javaoperatorsdk.operator.api.reconciler.Context;
|
||||||
import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration;
|
import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration;
|
||||||
|
@ -35,8 +40,10 @@ import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEven
|
||||||
import io.javaoperatorsdk.operator.processing.event.source.informer.Mappers;
|
import io.javaoperatorsdk.operator.processing.event.source.informer.Mappers;
|
||||||
import io.quarkus.logging.Log;
|
import io.quarkus.logging.Log;
|
||||||
|
|
||||||
|
import org.keycloak.common.util.CollectionUtil;
|
||||||
import org.keycloak.operator.Config;
|
import org.keycloak.operator.Config;
|
||||||
import org.keycloak.operator.Constants;
|
import org.keycloak.operator.Constants;
|
||||||
|
import org.keycloak.operator.Utils;
|
||||||
import org.keycloak.operator.crds.v2alpha1.deployment.Keycloak;
|
import org.keycloak.operator.crds.v2alpha1.deployment.Keycloak;
|
||||||
import org.keycloak.operator.crds.v2alpha1.deployment.KeycloakStatus;
|
import org.keycloak.operator.crds.v2alpha1.deployment.KeycloakStatus;
|
||||||
import org.keycloak.operator.crds.v2alpha1.deployment.KeycloakStatusAggregator;
|
import org.keycloak.operator.crds.v2alpha1.deployment.KeycloakStatusAggregator;
|
||||||
|
@ -44,6 +51,7 @@ import org.keycloak.operator.crds.v2alpha1.deployment.spec.HostnameSpec;
|
||||||
import org.keycloak.operator.crds.v2alpha1.deployment.spec.HostnameSpecBuilder;
|
import org.keycloak.operator.crds.v2alpha1.deployment.spec.HostnameSpecBuilder;
|
||||||
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
@ -52,6 +60,7 @@ import jakarta.inject.Inject;
|
||||||
|
|
||||||
@ControllerConfiguration(
|
@ControllerConfiguration(
|
||||||
dependents = {
|
dependents = {
|
||||||
|
@Dependent(type = KeycloakDeploymentDependentResource.class),
|
||||||
@Dependent(type = KeycloakAdminSecretDependentResource.class),
|
@Dependent(type = KeycloakAdminSecretDependentResource.class),
|
||||||
@Dependent(type = KeycloakIngressDependentResource.class, reconcilePrecondition = KeycloakIngressDependentResource.EnabledCondition.class),
|
@Dependent(type = KeycloakIngressDependentResource.class, reconcilePrecondition = KeycloakIngressDependentResource.EnabledCondition.class),
|
||||||
@Dependent(type = KeycloakServiceDependentResource.class, useEventSourceWithName = "serviceSource"),
|
@Dependent(type = KeycloakServiceDependentResource.class, useEventSourceWithName = "serviceSource"),
|
||||||
|
@ -61,42 +70,31 @@ public class KeycloakController implements Reconciler<Keycloak>, EventSourceInit
|
||||||
|
|
||||||
public static final String OPENSHIFT_DEFAULT = "openshift-default";
|
public static final String OPENSHIFT_DEFAULT = "openshift-default";
|
||||||
|
|
||||||
@Inject
|
|
||||||
KubernetesClient client;
|
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
Config config;
|
Config config;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
WatchedSecrets watchedSecrets;
|
WatchedSecrets watchedSecrets;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
KeycloakDistConfigurator distConfigurator;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Map<String, EventSource> prepareEventSources(EventSourceContext<Keycloak> context) {
|
public Map<String, EventSource> prepareEventSources(EventSourceContext<Keycloak> context) {
|
||||||
var namespaces = context.getControllerConfiguration().getNamespaces();
|
var namespaces = context.getControllerConfiguration().getNamespaces();
|
||||||
|
|
||||||
InformerConfiguration<StatefulSet> statefulSetIC = InformerConfiguration
|
|
||||||
.from(StatefulSet.class)
|
|
||||||
.withLabelSelector(Constants.DEFAULT_LABELS_AS_STRING)
|
|
||||||
.withNamespaces(namespaces)
|
|
||||||
.withSecondaryToPrimaryMapper(Mappers.fromOwnerReference())
|
|
||||||
.withOnUpdateFilter(new MetadataAwareOnUpdateFilter<>())
|
|
||||||
.build();
|
|
||||||
|
|
||||||
InformerConfiguration<Service> servicesIC = InformerConfiguration
|
InformerConfiguration<Service> servicesIC = InformerConfiguration
|
||||||
.from(Service.class)
|
.from(Service.class)
|
||||||
.withLabelSelector(Constants.DEFAULT_LABELS_AS_STRING)
|
.withLabelSelector(Constants.DEFAULT_LABELS_AS_STRING)
|
||||||
.withNamespaces(namespaces)
|
.withNamespaces(namespaces)
|
||||||
.withSecondaryToPrimaryMapper(Mappers.fromOwnerReference())
|
.withSecondaryToPrimaryMapper(Mappers.fromOwnerReference())
|
||||||
.withOnUpdateFilter(new MetadataAwareOnUpdateFilter<>())
|
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
EventSource statefulSetEvent = new InformerEventSource<>(statefulSetIC, context);
|
|
||||||
EventSource servicesEvent = new InformerEventSource<>(servicesIC, context);
|
EventSource servicesEvent = new InformerEventSource<>(servicesIC, context);
|
||||||
|
|
||||||
Map<String, EventSource> sources = new HashMap<>();
|
Map<String, EventSource> sources = new HashMap<>();
|
||||||
sources.put("serviceSource", servicesEvent);
|
sources.put("serviceSource", servicesEvent);
|
||||||
sources.putAll(EventSourceInitializer.nameEventSources(statefulSetEvent,
|
sources.putAll(EventSourceInitializer.nameEventSources(watchedSecrets.getWatchedSecretsEventSource()));
|
||||||
watchedSecrets.getWatchedSecretsEventSource()));
|
|
||||||
return sources;
|
return sources;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -131,11 +129,7 @@ public class KeycloakController implements Reconciler<Keycloak>, EventSourceInit
|
||||||
|
|
||||||
var statusAggregator = new KeycloakStatusAggregator(kc.getStatus(), kc.getMetadata().getGeneration());
|
var statusAggregator = new KeycloakStatusAggregator(kc.getStatus(), kc.getMetadata().getGeneration());
|
||||||
|
|
||||||
var kcDeployment = new KeycloakDeployment(context.getClient(), config, kc, context.getSecondaryResource(StatefulSet.class).orElse(null), KeycloakAdminSecretDependentResource.getName(kc));
|
updateStatus(kc, context.getSecondaryResource(StatefulSet.class).orElse(null), statusAggregator, context);
|
||||||
kcDeployment.setWatchedSecrets(watchedSecrets);
|
|
||||||
kcDeployment.createOrUpdateReconciled();
|
|
||||||
kcDeployment.updateStatus(statusAggregator);
|
|
||||||
|
|
||||||
var status = statusAggregator.build();
|
var status = statusAggregator.build();
|
||||||
|
|
||||||
Log.info("--- Reconciliation finished successfully");
|
Log.info("--- Reconciliation finished successfully");
|
||||||
|
@ -181,4 +175,97 @@ public class KeycloakController implements Reconciler<Keycloak>, EventSourceInit
|
||||||
.withName("cluster").get())
|
.withName("cluster").get())
|
||||||
.map(i -> Optional.ofNullable(i.getSpec().getAppsDomain()).orElse(i.getSpec().getDomain()));
|
.map(i -> Optional.ofNullable(i.getSpec().getAppsDomain()).orElse(i.getSpec().getDomain()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void updateStatus(Keycloak keycloakCR, StatefulSet existingDeployment, KeycloakStatusAggregator status, Context<Keycloak> context) {
|
||||||
|
status.apply(b -> b.withSelector(Utils.toSelectorString(Utils.allInstanceLabels(keycloakCR))));
|
||||||
|
validatePodTemplate(keycloakCR, status);
|
||||||
|
if (existingDeployment == null) {
|
||||||
|
status.addNotReadyMessage("No existing StatefulSet found, waiting for creating a new one");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingDeployment.getStatus() == null) {
|
||||||
|
status.addNotReadyMessage("Waiting for deployment status");
|
||||||
|
} else {
|
||||||
|
status.apply(b -> b.withInstances(existingDeployment.getStatus().getReadyReplicas()));
|
||||||
|
if (Optional.ofNullable(existingDeployment.getStatus().getReadyReplicas()).orElse(0) < keycloakCR.getSpec().getInstances()) {
|
||||||
|
checkForPodErrors(status, keycloakCR, existingDeployment, context);
|
||||||
|
status.addNotReadyMessage("Waiting for more replicas");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Optional
|
||||||
|
.ofNullable(existingDeployment.getMetadata().getAnnotations().get(Constants.KEYCLOAK_MIGRATING_ANNOTATION))
|
||||||
|
.map(Boolean::valueOf).orElse(false)) {
|
||||||
|
status.addNotReadyMessage("Performing Keycloak upgrade, scaling down the deployment");
|
||||||
|
} else if (existingDeployment.getStatus() != null
|
||||||
|
&& existingDeployment.getStatus().getCurrentRevision() != null
|
||||||
|
&& existingDeployment.getStatus().getUpdateRevision() != null
|
||||||
|
&& !existingDeployment.getStatus().getCurrentRevision().equals(existingDeployment.getStatus().getUpdateRevision())) {
|
||||||
|
status.addRollingUpdateMessage("Rolling out deployment update");
|
||||||
|
}
|
||||||
|
|
||||||
|
distConfigurator.validateOptions(keycloakCR, status);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void validatePodTemplate(Keycloak keycloakCR, KeycloakStatusAggregator status) {
|
||||||
|
var spec = KeycloakDeploymentDependentResource.getPodTemplateSpec(keycloakCR);
|
||||||
|
if (spec.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var overlayTemplate = spec.orElseThrow();
|
||||||
|
|
||||||
|
if (overlayTemplate.getMetadata() != null) {
|
||||||
|
if (overlayTemplate.getMetadata().getName() != null) {
|
||||||
|
status.addWarningMessage("The name of the podTemplate cannot be modified");
|
||||||
|
}
|
||||||
|
if (overlayTemplate.getMetadata().getNamespace() != null) {
|
||||||
|
status.addWarningMessage("The namespace of the podTemplate cannot be modified");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Optional.ofNullable(overlayTemplate.getSpec()).map(PodSpec::getContainers).flatMap(l -> l.stream().findFirst())
|
||||||
|
.ifPresent(container -> {
|
||||||
|
if (container.getName() != null) {
|
||||||
|
status.addWarningMessage("The name of the keycloak container cannot be modified");
|
||||||
|
}
|
||||||
|
if (container.getImage() != null) {
|
||||||
|
status.addWarningMessage(
|
||||||
|
"The image of the keycloak container cannot be modified using podTemplate");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (overlayTemplate.getSpec() != null &&
|
||||||
|
CollectionUtil.isNotEmpty(overlayTemplate.getSpec().getImagePullSecrets())) {
|
||||||
|
status.addWarningMessage("The imagePullSecrets of the keycloak container cannot be modified using podTemplate");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkForPodErrors(KeycloakStatusAggregator status, Keycloak keycloak, StatefulSet existingDeployment, Context<Keycloak> context) {
|
||||||
|
context.getClient().pods().inNamespace(existingDeployment.getMetadata().getNamespace())
|
||||||
|
.withLabel("controller-revision-hash", existingDeployment.getStatus().getUpdateRevision())
|
||||||
|
.withLabels(Utils.allInstanceLabels(keycloak))
|
||||||
|
.list().getItems().stream()
|
||||||
|
.filter(p -> !Readiness.isPodReady(p)
|
||||||
|
&& Optional.ofNullable(p.getStatus()).map(PodStatus::getContainerStatuses).isPresent())
|
||||||
|
.sorted((p1, p2) -> p1.getMetadata().getName().compareTo(p2.getMetadata().getName()))
|
||||||
|
.forEachOrdered(p -> {
|
||||||
|
Optional.of(p.getStatus()).map(s -> s.getContainerStatuses()).stream().flatMap(List::stream)
|
||||||
|
.filter(cs -> !Boolean.TRUE.equals(cs.getReady()))
|
||||||
|
.sorted((cs1, cs2) -> cs1.getName().compareTo(cs2.getName())).forEachOrdered(cs -> {
|
||||||
|
if (Optional.ofNullable(cs.getState()).map(ContainerState::getWaiting)
|
||||||
|
.map(ContainerStateWaiting::getReason).map(String::toLowerCase)
|
||||||
|
.filter(s -> s.contains("err") || s.equals("crashloopbackoff")).isPresent()) {
|
||||||
|
Log.infof("Found unhealthy container on pod %s/%s: %s",
|
||||||
|
p.getMetadata().getNamespace(), p.getMetadata().getName(),
|
||||||
|
Serialization.asYaml(cs));
|
||||||
|
status.addErrorMessage(
|
||||||
|
String.format("Waiting for %s/%s due to %s: %s", p.getMetadata().getNamespace(),
|
||||||
|
p.getMetadata().getName(), cs.getState().getWaiting().getReason(),
|
||||||
|
cs.getState().getWaiting().getMessage()));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,504 +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.ContainerState;
|
|
||||||
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.PodSpec;
|
|
||||||
import io.fabric8.kubernetes.api.model.PodSpecFluent.ContainersNested;
|
|
||||||
import io.fabric8.kubernetes.api.model.PodStatus;
|
|
||||||
import io.fabric8.kubernetes.api.model.PodTemplateSpec;
|
|
||||||
import io.fabric8.kubernetes.api.model.PodTemplateSpecFluent.SpecNested;
|
|
||||||
import io.fabric8.kubernetes.api.model.apps.StatefulSet;
|
|
||||||
import io.fabric8.kubernetes.api.model.apps.StatefulSetBuilder;
|
|
||||||
import io.fabric8.kubernetes.api.model.apps.StatefulSetSpecFluent.TemplateNested;
|
|
||||||
import io.fabric8.kubernetes.client.KubernetesClient;
|
|
||||||
import io.fabric8.kubernetes.client.readiness.Readiness;
|
|
||||||
import io.fabric8.kubernetes.client.utils.Serialization;
|
|
||||||
import io.quarkus.logging.Log;
|
|
||||||
|
|
||||||
import org.keycloak.common.util.CollectionUtil;
|
|
||||||
import org.keycloak.operator.Config;
|
|
||||||
import org.keycloak.operator.Constants;
|
|
||||||
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.KeycloakStatusAggregator;
|
|
||||||
import org.keycloak.operator.crds.v2alpha1.deployment.ValueOrSecret;
|
|
||||||
import org.keycloak.operator.crds.v2alpha1.deployment.spec.UnsupportedSpec;
|
|
||||||
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Base64;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.LinkedHashMap;
|
|
||||||
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<StatefulSet> {
|
|
||||||
|
|
||||||
public static final String OPTIMIZED_ARG = "--optimized";
|
|
||||||
private final Config operatorConfig;
|
|
||||||
private final KeycloakDistConfigurator distConfigurator;
|
|
||||||
|
|
||||||
private final Keycloak keycloakCR;
|
|
||||||
private final StatefulSet existingDeployment;
|
|
||||||
private final StatefulSet baseDeployment;
|
|
||||||
private final String adminSecretName;
|
|
||||||
|
|
||||||
private Set<String> serverConfigSecretsNames;
|
|
||||||
private WatchedSecrets watchedSecrets;
|
|
||||||
|
|
||||||
private boolean migrationInProgress;
|
|
||||||
|
|
||||||
public KeycloakDeployment(KubernetesClient client, Config config, Keycloak keycloakCR, StatefulSet existingDeployment, String adminSecretName) {
|
|
||||||
super(client, keycloakCR);
|
|
||||||
this.operatorConfig = config;
|
|
||||||
this.keycloakCR = keycloakCR;
|
|
||||||
this.adminSecretName = adminSecretName;
|
|
||||||
this.existingDeployment = existingDeployment;
|
|
||||||
this.baseDeployment = createBaseDeployment();
|
|
||||||
this.distConfigurator = new KeycloakDistConfigurator(keycloakCR, baseDeployment, client);
|
|
||||||
this.distConfigurator.configureDistOptions();
|
|
||||||
// after the distConfiguration, we can add the remaining default / additionalConfig
|
|
||||||
addRemainingEnvVars();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setWatchedSecrets(WatchedSecrets watchedSecrets) {
|
|
||||||
this.watchedSecrets = watchedSecrets;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
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");
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
Log.info("Existing Deployment found, handling migration");
|
|
||||||
|
|
||||||
if (!existingDeployment.isMarkedForDeletion() && !hasExpectedMatchLabels(existingDeployment)) {
|
|
||||||
client.resource(existingDeployment).lockResourceVersion().delete();
|
|
||||||
Log.info("Existing Deployment found with old label selector, it will be recreated");
|
|
||||||
}
|
|
||||||
|
|
||||||
migrateDeployment(existingDeployment, baseDeployment);
|
|
||||||
}
|
|
||||||
|
|
||||||
var configSecretsNames = getConfigSecretsNames();
|
|
||||||
if (!configSecretsNames.isEmpty() && watchedSecrets != null) {
|
|
||||||
watchedSecrets.annotateDeployment(configSecretsNames, keycloakCR, baseDeployment);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Optional.of(baseDeployment);
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean hasExpectedMatchLabels(StatefulSet statefulSet) {
|
|
||||||
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()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var overlayTemplate = spec.orElseThrow();
|
|
||||||
|
|
||||||
if (overlayTemplate.getMetadata() != null) {
|
|
||||||
if (overlayTemplate.getMetadata().getName() != null) {
|
|
||||||
status.addWarningMessage("The name of the podTemplate cannot be modified");
|
|
||||||
}
|
|
||||||
if (overlayTemplate.getMetadata().getNamespace() != null) {
|
|
||||||
status.addWarningMessage("The namespace of the podTemplate cannot be modified");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Optional.ofNullable(overlayTemplate.getSpec()).map(PodSpec::getContainers).flatMap(l -> l.stream().findFirst())
|
|
||||||
.ifPresent(container -> {
|
|
||||||
if (container.getName() != null) {
|
|
||||||
status.addWarningMessage("The name of the keycloak container cannot be modified");
|
|
||||||
}
|
|
||||||
if (container.getImage() != null) {
|
|
||||||
status.addWarningMessage(
|
|
||||||
"The image of the keycloak container cannot be modified using podTemplate");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (overlayTemplate.getSpec() != null &&
|
|
||||||
CollectionUtil.isNotEmpty(overlayTemplate.getSpec().getImagePullSecrets())) {
|
|
||||||
status.addWarningMessage("The imagePullSecrets of the keycloak container cannot be modified using podTemplate");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Optional<PodTemplateSpec> getPodTemplateSpec() {
|
|
||||||
return Optional.ofNullable(keycloakCR.getSpec()).map(KeycloakSpec::getUnsupported).map(UnsupportedSpec::getPodTemplate);
|
|
||||||
}
|
|
||||||
|
|
||||||
private StatefulSet createBaseDeployment() {
|
|
||||||
Map<String, String> labels = getInstanceLabels();
|
|
||||||
if (operatorConfig.keycloak().podLabels() != null) {
|
|
||||||
labels.putAll(operatorConfig.keycloak().podLabels());
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Create a builder for the statefulset, note that the pod template spec is used as the basis
|
|
||||||
* over that some values are forced, others will let the template override, others merge
|
|
||||||
*/
|
|
||||||
|
|
||||||
StatefulSetBuilder baseDeploymentBuilder = new StatefulSetBuilder()
|
|
||||||
.withNewMetadata()
|
|
||||||
.withName(getName())
|
|
||||||
.withNamespace(getNamespace())
|
|
||||||
.endMetadata()
|
|
||||||
.withNewSpec()
|
|
||||||
.withNewSelector()
|
|
||||||
.withMatchLabels(getInstanceLabels())
|
|
||||||
.endSelector()
|
|
||||||
.withNewTemplateLike(getPodTemplateSpec().orElseGet(PodTemplateSpec::new))
|
|
||||||
.editOrNewMetadata().addToLabels(labels).endMetadata()
|
|
||||||
.editOrNewSpec().withImagePullSecrets(keycloakCR.getSpec().getImagePullSecrets()).endSpec()
|
|
||||||
.endTemplate()
|
|
||||||
.withReplicas(keycloakCR.getSpec().getInstances())
|
|
||||||
.endSpec();
|
|
||||||
|
|
||||||
var specBuilder = baseDeploymentBuilder.editSpec().editTemplate().editOrNewSpec();
|
|
||||||
|
|
||||||
if (!specBuilder.hasRestartPolicy()) {
|
|
||||||
specBuilder.withRestartPolicy("Always");
|
|
||||||
}
|
|
||||||
if (!specBuilder.hasTerminationGracePeriodSeconds()) {
|
|
||||||
specBuilder.withTerminationGracePeriodSeconds(30L);
|
|
||||||
}
|
|
||||||
if (!specBuilder.hasDnsPolicy()) {
|
|
||||||
specBuilder.withDnsPolicy("ClusterFirst");
|
|
||||||
}
|
|
||||||
|
|
||||||
// there isn't currently an editOrNewFirstContainer, so we need to do this manually
|
|
||||||
ContainersNested<SpecNested<TemplateNested<io.fabric8.kubernetes.api.model.apps.StatefulSetFluent.SpecNested<StatefulSetBuilder>>>> containerBuilder = null;
|
|
||||||
if (specBuilder.buildContainers().isEmpty()) {
|
|
||||||
containerBuilder = specBuilder.addNewContainer();
|
|
||||||
} else {
|
|
||||||
containerBuilder = specBuilder.editFirstContainer();
|
|
||||||
}
|
|
||||||
|
|
||||||
containerBuilder.withName("keycloak");
|
|
||||||
|
|
||||||
var customImage = Optional.ofNullable(keycloakCR.getSpec().getImage());
|
|
||||||
containerBuilder.withImage(customImage.orElse(operatorConfig.keycloak().image()));
|
|
||||||
|
|
||||||
if (!containerBuilder.hasImagePullPolicy()) {
|
|
||||||
containerBuilder.withImagePullPolicy(operatorConfig.keycloak().imagePullPolicy());
|
|
||||||
}
|
|
||||||
if (Optional.ofNullable(containerBuilder.getArgs()).orElse(List.of()).isEmpty()) {
|
|
||||||
containerBuilder.withArgs("--verbose", "start");
|
|
||||||
}
|
|
||||||
if (customImage.isPresent()) {
|
|
||||||
containerBuilder.addToArgs(OPTIMIZED_ARG);
|
|
||||||
}
|
|
||||||
|
|
||||||
// probes
|
|
||||||
var tlsConfigured = isTlsConfigured(keycloakCR);
|
|
||||||
var protocol = !tlsConfigured ? "HTTP" : "HTTPS";
|
|
||||||
var kcPort = KeycloakServiceDependentResource.getServicePort(tlsConfigured, keycloakCR);
|
|
||||||
|
|
||||||
// Relative path ends with '/'
|
|
||||||
var kcRelativePath = Optional.ofNullable(readConfigurationValue(Constants.KEYCLOAK_HTTP_RELATIVE_PATH_KEY))
|
|
||||||
.map(path -> !path.endsWith("/") ? path + "/" : path)
|
|
||||||
.orElse("/");
|
|
||||||
|
|
||||||
if (!containerBuilder.hasReadinessProbe()) {
|
|
||||||
containerBuilder.withNewReadinessProbe()
|
|
||||||
.withPeriodSeconds(10)
|
|
||||||
.withFailureThreshold(3)
|
|
||||||
.withNewHttpGet()
|
|
||||||
.withScheme(protocol)
|
|
||||||
.withNewPort(kcPort)
|
|
||||||
.withPath(kcRelativePath + "health/ready")
|
|
||||||
.endHttpGet()
|
|
||||||
.endReadinessProbe();
|
|
||||||
}
|
|
||||||
if (!containerBuilder.hasLivenessProbe()) {
|
|
||||||
containerBuilder.withNewLivenessProbe()
|
|
||||||
.withPeriodSeconds(10)
|
|
||||||
.withFailureThreshold(3)
|
|
||||||
.withNewHttpGet()
|
|
||||||
.withScheme(protocol)
|
|
||||||
.withNewPort(kcPort)
|
|
||||||
.withPath(kcRelativePath + "health/live")
|
|
||||||
.endHttpGet()
|
|
||||||
.endLivenessProbe();
|
|
||||||
}
|
|
||||||
if (!containerBuilder.hasStartupProbe()) {
|
|
||||||
containerBuilder.withNewStartupProbe()
|
|
||||||
.withPeriodSeconds(1)
|
|
||||||
.withFailureThreshold(600)
|
|
||||||
.withNewHttpGet()
|
|
||||||
.withScheme(protocol)
|
|
||||||
.withNewPort(kcPort)
|
|
||||||
.withPath(kcRelativePath + "health/started")
|
|
||||||
.endHttpGet()
|
|
||||||
.endStartupProbe();
|
|
||||||
}
|
|
||||||
|
|
||||||
// add in ports - there's no merging being done here
|
|
||||||
StatefulSet baseDeployment = containerBuilder
|
|
||||||
.addNewPort()
|
|
||||||
.withName(Constants.KEYCLOAK_HTTPS_PORT_NAME)
|
|
||||||
.withContainerPort(Constants.KEYCLOAK_HTTPS_PORT)
|
|
||||||
.withProtocol(Constants.KEYCLOAK_SERVICE_PROTOCOL)
|
|
||||||
.endPort()
|
|
||||||
.addNewPort()
|
|
||||||
.withName(Constants.KEYCLOAK_HTTP_PORT_NAME)
|
|
||||||
.withContainerPort(Constants.KEYCLOAK_HTTP_PORT)
|
|
||||||
.withProtocol(Constants.KEYCLOAK_SERVICE_PROTOCOL)
|
|
||||||
.endPort()
|
|
||||||
.endContainer().endSpec().endTemplate().endSpec().build();
|
|
||||||
|
|
||||||
return baseDeployment;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void addRemainingEnvVars() {
|
|
||||||
// add in the remaining envVars, but only if they are not already set
|
|
||||||
var envVars = getEnvVars();
|
|
||||||
var env = baseDeployment.getSpec().getTemplate().getSpec().getContainers().get(0).getEnv();
|
|
||||||
if (env != null && !env.isEmpty()) {
|
|
||||||
// this is also a final rationalization of whatever was added first by any previous manipulation wins
|
|
||||||
envVars = new ArrayList<>(Stream.concat(env.stream(), envVars.stream())
|
|
||||||
.collect(Collectors.toMap(EnvVar::getName, Function.identity(), (e1, e2) -> e1, LinkedHashMap::new))
|
|
||||||
.values());
|
|
||||||
}
|
|
||||||
baseDeployment.getSpec().getTemplate().getSpec().getContainers().get(0).setEnv(envVars);
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<EnvVar> getEnvVars() {
|
|
||||||
// default config values
|
|
||||||
List<ValueOrSecret> serverConfigsList = new ArrayList<>(Constants.DEFAULT_DIST_CONFIG_LIST);
|
|
||||||
|
|
||||||
// merge with the CR; the values in CR take precedence
|
|
||||||
if (keycloakCR.getSpec().getAdditionalOptions() != null) {
|
|
||||||
Set<String> inCr = keycloakCR.getSpec().getAdditionalOptions().stream().map(v -> v.getName()).collect(Collectors.toSet());
|
|
||||||
serverConfigsList.removeIf(v -> inCr.contains(v.getName()));
|
|
||||||
serverConfigsList.addAll(keycloakCR.getSpec().getAdditionalOptions());
|
|
||||||
}
|
|
||||||
|
|
||||||
// set env vars
|
|
||||||
serverConfigSecretsNames = new HashSet<>();
|
|
||||||
List<EnvVar> envVars = serverConfigsList.stream()
|
|
||||||
.map(v -> {
|
|
||||||
var envBuilder = new EnvVarBuilder().withName(KeycloakDistConfigurator.getKeycloakOptionEnvVarName(v.getName()));
|
|
||||||
var secret = v.getSecret();
|
|
||||||
if (secret != null) {
|
|
||||||
envBuilder.withValueFrom(
|
|
||||||
new EnvVarSourceBuilder().withSecretKeyRef(secret).build());
|
|
||||||
serverConfigSecretsNames.add(secret.getName()); // for watching it later
|
|
||||||
} else {
|
|
||||||
envBuilder.withValue(v.getValue());
|
|
||||||
}
|
|
||||||
return envBuilder.build();
|
|
||||||
})
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
Log.infof("Found config secrets names: %s", serverConfigSecretsNames);
|
|
||||||
|
|
||||||
envVars.add(
|
|
||||||
new EnvVarBuilder()
|
|
||||||
.withName("KEYCLOAK_ADMIN")
|
|
||||||
.withNewValueFrom()
|
|
||||||
.withNewSecretKeyRef()
|
|
||||||
.withName(adminSecretName)
|
|
||||||
.withKey("username")
|
|
||||||
.withOptional(false)
|
|
||||||
.endSecretKeyRef()
|
|
||||||
.endValueFrom()
|
|
||||||
.build());
|
|
||||||
envVars.add(
|
|
||||||
new EnvVarBuilder()
|
|
||||||
.withName("KEYCLOAK_ADMIN_PASSWORD")
|
|
||||||
.withNewValueFrom()
|
|
||||||
.withNewSecretKeyRef()
|
|
||||||
.withName(adminSecretName)
|
|
||||||
.withKey("password")
|
|
||||||
.withOptional(false)
|
|
||||||
.endSecretKeyRef()
|
|
||||||
.endValueFrom()
|
|
||||||
.build());
|
|
||||||
|
|
||||||
envVars.add(
|
|
||||||
new EnvVarBuilder()
|
|
||||||
.withName("jgroups.dns.query")
|
|
||||||
.withValue(getName() + Constants.KEYCLOAK_DISCOVERY_SERVICE_SUFFIX +"." + getNamespace())
|
|
||||||
.build());
|
|
||||||
|
|
||||||
return envVars;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void updateStatus(KeycloakStatusAggregator status) {
|
|
||||||
status.apply(b -> b.withSelector(Utils.toSelectorString(getInstanceLabels())));
|
|
||||||
validatePodTemplate(status);
|
|
||||||
if (existingDeployment == null) {
|
|
||||||
status.addNotReadyMessage("No existing StatefulSet found, waiting for creating a new one");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (existingDeployment.getStatus() == null) {
|
|
||||||
status.addNotReadyMessage("Waiting for deployment status");
|
|
||||||
} else {
|
|
||||||
status.apply(b -> b.withInstances(existingDeployment.getStatus().getReadyReplicas()));
|
|
||||||
if (Optional.ofNullable(existingDeployment.getStatus().getReadyReplicas()).orElse(0) < keycloakCR.getSpec().getInstances()) {
|
|
||||||
checkForPodErrors(status);
|
|
||||||
status.addNotReadyMessage("Waiting for more replicas");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (migrationInProgress) {
|
|
||||||
status.addNotReadyMessage("Performing Keycloak upgrade, scaling down the deployment");
|
|
||||||
} else if (existingDeployment.getStatus() != null
|
|
||||||
&& existingDeployment.getStatus().getCurrentRevision() != null
|
|
||||||
&& existingDeployment.getStatus().getUpdateRevision() != null
|
|
||||||
&& !existingDeployment.getStatus().getCurrentRevision().equals(existingDeployment.getStatus().getUpdateRevision())) {
|
|
||||||
status.addRollingUpdateMessage("Rolling out deployment update");
|
|
||||||
}
|
|
||||||
|
|
||||||
distConfigurator.validateOptions(status);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void checkForPodErrors(KeycloakStatusAggregator status) {
|
|
||||||
client.pods().inNamespace(existingDeployment.getMetadata().getNamespace())
|
|
||||||
.withLabel("controller-revision-hash", existingDeployment.getStatus().getUpdateRevision())
|
|
||||||
.withLabels(getInstanceLabels())
|
|
||||||
.list().getItems().stream()
|
|
||||||
.filter(p -> !Readiness.isPodReady(p)
|
|
||||||
&& Optional.ofNullable(p.getStatus()).map(PodStatus::getContainerStatuses).isPresent())
|
|
||||||
.sorted((p1, p2) -> p1.getMetadata().getName().compareTo(p2.getMetadata().getName()))
|
|
||||||
.forEachOrdered(p -> {
|
|
||||||
Optional.of(p.getStatus()).map(s -> s.getContainerStatuses()).stream().flatMap(List::stream)
|
|
||||||
.filter(cs -> !Boolean.TRUE.equals(cs.getReady()))
|
|
||||||
.sorted((cs1, cs2) -> cs1.getName().compareTo(cs2.getName())).forEachOrdered(cs -> {
|
|
||||||
if (Optional.ofNullable(cs.getState()).map(ContainerState::getWaiting)
|
|
||||||
.map(ContainerStateWaiting::getReason).map(String::toLowerCase)
|
|
||||||
.filter(s -> s.contains("err") || s.equals("crashloopbackoff")).isPresent()) {
|
|
||||||
Log.infof("Found unhealthy container on pod %s/%s: %s",
|
|
||||||
p.getMetadata().getNamespace(), p.getMetadata().getName(),
|
|
||||||
Serialization.asYaml(cs));
|
|
||||||
status.addErrorMessage(
|
|
||||||
String.format("Waiting for %s/%s due to %s: %s", p.getMetadata().getNamespace(),
|
|
||||||
p.getMetadata().getName(), cs.getState().getWaiting().getReason(),
|
|
||||||
cs.getState().getWaiting().getMessage()));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<String> getConfigSecretsNames() {
|
|
||||||
TreeSet<String> ret = new TreeSet<>(serverConfigSecretsNames);
|
|
||||||
ret.addAll(distConfigurator.getSecretNames());
|
|
||||||
return new ArrayList<>(ret);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String getName(Keycloak keycloak) {
|
|
||||||
return keycloak.getMetadata().getName();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getName() {
|
|
||||||
return getName(keycloakCR);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void migrateDeployment(StatefulSet previousDeployment, StatefulSet reconciledDeployment) {
|
|
||||||
if (previousDeployment == null
|
|
||||||
|| previousDeployment.getSpec() == null
|
|
||||||
|| previousDeployment.getSpec().getTemplate() == null
|
|
||||||
|| previousDeployment.getSpec().getTemplate().getSpec() == null
|
|
||||||
|| previousDeployment.getSpec().getTemplate().getSpec().getContainers() == null
|
|
||||||
|| previousDeployment.getSpec().getTemplate().getSpec().getContainers().get(0) == null)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var previousContainer = previousDeployment.getSpec().getTemplate().getSpec().getContainers().get(0);
|
|
||||||
var reconciledContainer = reconciledDeployment.getSpec().getTemplate().getSpec().getContainers().get(0);
|
|
||||||
|
|
||||||
if (!previousContainer.getImage().equals(reconciledContainer.getImage())
|
|
||||||
&& previousDeployment.getStatus().getReplicas() > 1) {
|
|
||||||
// TODO Check if migration is really needed (e.g. based on actual KC version); https://github.com/keycloak/keycloak/issues/10441
|
|
||||||
Log.info("Detected changed Keycloak image, assuming Keycloak upgrade. Scaling down the deployment to one instance to perform a safe database migration");
|
|
||||||
Log.infof("original image: %s; new image: %s", previousContainer.getImage(), reconciledContainer.getImage());
|
|
||||||
|
|
||||||
reconciledContainer.setImage(previousContainer.getImage());
|
|
||||||
reconciledDeployment.getSpec().setReplicas(1);
|
|
||||||
|
|
||||||
migrationInProgress = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected String readConfigurationValue(String key) {
|
|
||||||
if (keycloakCR != null &&
|
|
||||||
keycloakCR.getSpec() != null &&
|
|
||||||
keycloakCR.getSpec().getAdditionalOptions() != null
|
|
||||||
) {
|
|
||||||
|
|
||||||
var serverConfigValue = keycloakCR
|
|
||||||
.getSpec()
|
|
||||||
.getAdditionalOptions()
|
|
||||||
.stream()
|
|
||||||
.filter(sc -> sc.getName().equals(key))
|
|
||||||
.findFirst();
|
|
||||||
if (serverConfigValue.isPresent()) {
|
|
||||||
if (serverConfigValue.get().getValue() != null) {
|
|
||||||
return serverConfigValue.get().getValue();
|
|
||||||
} else {
|
|
||||||
var secretSelector = serverConfigValue.get().getSecret();
|
|
||||||
if (secretSelector == null) {
|
|
||||||
throw new IllegalStateException("Secret " + serverConfigValue.get().getName() + " not defined");
|
|
||||||
}
|
|
||||||
var secret = client.secrets().inNamespace(keycloakCR.getMetadata().getNamespace()).withName(secretSelector.getName()).get();
|
|
||||||
if (secret == null) {
|
|
||||||
throw new IllegalStateException("Secret " + secretSelector.getName() + " not found in cluster");
|
|
||||||
}
|
|
||||||
if (secret.getData().containsKey(secretSelector.getKey())) {
|
|
||||||
return new String(Base64.getDecoder().decode(secret.getData().get(secretSelector.getKey())), StandardCharsets.UTF_8);
|
|
||||||
} else {
|
|
||||||
throw new IllegalStateException("Secret " + secretSelector.getName() + " doesn't contain the expected key " + secretSelector.getKey());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,410 @@
|
||||||
|
/*
|
||||||
|
* 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.EnvVar;
|
||||||
|
import io.fabric8.kubernetes.api.model.EnvVarBuilder;
|
||||||
|
import io.fabric8.kubernetes.api.model.EnvVarSource;
|
||||||
|
import io.fabric8.kubernetes.api.model.EnvVarSourceBuilder;
|
||||||
|
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.SecretKeySelector;
|
||||||
|
import io.fabric8.kubernetes.api.model.VolumeBuilder;
|
||||||
|
import io.fabric8.kubernetes.api.model.VolumeMountBuilder;
|
||||||
|
import io.fabric8.kubernetes.api.model.apps.StatefulSet;
|
||||||
|
import io.fabric8.kubernetes.api.model.apps.StatefulSetBuilder;
|
||||||
|
import io.fabric8.kubernetes.api.model.apps.StatefulSetSpec;
|
||||||
|
import io.fabric8.kubernetes.api.model.apps.StatefulSetSpecFluent.TemplateNested;
|
||||||
|
import io.javaoperatorsdk.operator.api.reconciler.Context;
|
||||||
|
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource;
|
||||||
|
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent;
|
||||||
|
import io.quarkus.logging.Log;
|
||||||
|
|
||||||
|
import org.keycloak.operator.Config;
|
||||||
|
import org.keycloak.operator.Constants;
|
||||||
|
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.UnsupportedSpec;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Base64;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
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 jakarta.inject.Inject;
|
||||||
|
|
||||||
|
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 OPTIMIZED_ARG = "--optimized";
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
Config operatorConfig;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
WatchedSecrets watchedSecrets;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
KeycloakDistConfigurator distConfigurator;
|
||||||
|
|
||||||
|
public KeycloakDeploymentDependentResource() {
|
||||||
|
super(StatefulSet.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public StatefulSet desired(Keycloak primary, Context<Keycloak> context) {
|
||||||
|
StatefulSet baseDeployment = createBaseDeployment(primary, context);
|
||||||
|
if (isTlsConfigured(primary)) {
|
||||||
|
configureTLS(primary, baseDeployment);
|
||||||
|
}
|
||||||
|
addEnvVarsAndWatchSecrets(baseDeployment, primary);
|
||||||
|
|
||||||
|
StatefulSet existingDeployment = context.getSecondaryResource(StatefulSet.class).orElse(null);
|
||||||
|
if (existingDeployment == null) {
|
||||||
|
Log.info("No existing Deployment found, using the default");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Log.info("Existing Deployment found, handling migration");
|
||||||
|
|
||||||
|
// version 22 changed the match labels, account for older versions
|
||||||
|
if (!existingDeployment.isMarkedForDeletion() && !hasExpectedMatchLabels(existingDeployment, primary)) {
|
||||||
|
context.getClient().resource(existingDeployment).lockResourceVersion().delete();
|
||||||
|
Log.info("Existing Deployment found with old label selector, it will be recreated");
|
||||||
|
}
|
||||||
|
|
||||||
|
migrateDeployment(existingDeployment, baseDeployment, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
return baseDeployment;
|
||||||
|
}
|
||||||
|
|
||||||
|
void configureTLS(Keycloak keycloakCR, StatefulSet deployment) {
|
||||||
|
var kcContainer = deployment.getSpec().getTemplate().getSpec().getContainers().get(0);
|
||||||
|
|
||||||
|
var volume = new VolumeBuilder()
|
||||||
|
.withName("keycloak-tls-certificates")
|
||||||
|
.withNewSecret()
|
||||||
|
.withSecretName(keycloakCR.getSpec().getHttpSpec().getTlsSecret())
|
||||||
|
.withOptional(false)
|
||||||
|
.endSecret()
|
||||||
|
.build();
|
||||||
|
|
||||||
|
var volumeMount = new VolumeMountBuilder()
|
||||||
|
.withName(volume.getName())
|
||||||
|
.withMountPath(Constants.CERTIFICATES_FOLDER)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
deployment.getSpec().getTemplate().getSpec().getVolumes().add(0, volume);
|
||||||
|
kcContainer.getVolumeMounts().add(0, volumeMount);
|
||||||
|
}
|
||||||
|
|
||||||
|
@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);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Optional<PodTemplateSpec> getPodTemplateSpec(Keycloak keycloakCR) {
|
||||||
|
return Optional.ofNullable(keycloakCR.getSpec()).map(KeycloakSpec::getUnsupported).map(UnsupportedSpec::getPodTemplate);
|
||||||
|
}
|
||||||
|
|
||||||
|
private StatefulSet createBaseDeployment(Keycloak keycloakCR, Context<Keycloak> context) {
|
||||||
|
Map<String, String> labels = Utils.allInstanceLabels(keycloakCR);
|
||||||
|
if (operatorConfig.keycloak().podLabels() != null) {
|
||||||
|
labels.putAll(operatorConfig.keycloak().podLabels());
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Create a builder for the statefulset, note that the pod template spec is used as the basis
|
||||||
|
* over that some values are forced, others will let the template override, others merge
|
||||||
|
*/
|
||||||
|
|
||||||
|
StatefulSetBuilder baseDeploymentBuilder = new StatefulSetBuilder()
|
||||||
|
.withNewMetadata()
|
||||||
|
.withName(getName(keycloakCR))
|
||||||
|
.withNamespace(keycloakCR.getMetadata().getNamespace())
|
||||||
|
.withLabels(Utils.allInstanceLabels(keycloakCR))
|
||||||
|
.addToAnnotations(Constants.KEYCLOAK_MIGRATING_ANNOTATION, Boolean.FALSE.toString())
|
||||||
|
.endMetadata()
|
||||||
|
.withNewSpec()
|
||||||
|
.withNewSelector()
|
||||||
|
.withMatchLabels(Utils.allInstanceLabels(keycloakCR))
|
||||||
|
.endSelector()
|
||||||
|
.withNewTemplateLike(getPodTemplateSpec(keycloakCR).orElseGet(PodTemplateSpec::new))
|
||||||
|
.editOrNewMetadata().addToLabels(labels).endMetadata()
|
||||||
|
.editOrNewSpec().withImagePullSecrets(keycloakCR.getSpec().getImagePullSecrets()).endSpec()
|
||||||
|
.endTemplate()
|
||||||
|
.withReplicas(keycloakCR.getSpec().getInstances())
|
||||||
|
.endSpec();
|
||||||
|
|
||||||
|
var specBuilder = baseDeploymentBuilder.editSpec().editTemplate().editOrNewSpec();
|
||||||
|
|
||||||
|
if (!specBuilder.hasRestartPolicy()) {
|
||||||
|
specBuilder.withRestartPolicy("Always");
|
||||||
|
}
|
||||||
|
if (!specBuilder.hasTerminationGracePeriodSeconds()) {
|
||||||
|
specBuilder.withTerminationGracePeriodSeconds(30L);
|
||||||
|
}
|
||||||
|
if (!specBuilder.hasDnsPolicy()) {
|
||||||
|
specBuilder.withDnsPolicy("ClusterFirst");
|
||||||
|
}
|
||||||
|
|
||||||
|
// there isn't currently an editOrNewFirstContainer, so we need to do this manually
|
||||||
|
ContainersNested<SpecNested<TemplateNested<io.fabric8.kubernetes.api.model.apps.StatefulSetFluent.SpecNested<StatefulSetBuilder>>>> containerBuilder = null;
|
||||||
|
if (specBuilder.buildContainers().isEmpty()) {
|
||||||
|
containerBuilder = specBuilder.addNewContainer();
|
||||||
|
} else {
|
||||||
|
containerBuilder = specBuilder.editFirstContainer();
|
||||||
|
}
|
||||||
|
|
||||||
|
containerBuilder.withName("keycloak");
|
||||||
|
|
||||||
|
var customImage = Optional.ofNullable(keycloakCR.getSpec().getImage());
|
||||||
|
containerBuilder.withImage(customImage.orElse(operatorConfig.keycloak().image()));
|
||||||
|
|
||||||
|
if (!containerBuilder.hasImagePullPolicy()) {
|
||||||
|
containerBuilder.withImagePullPolicy(operatorConfig.keycloak().imagePullPolicy());
|
||||||
|
}
|
||||||
|
if (Optional.ofNullable(containerBuilder.getArgs()).orElse(List.of()).isEmpty()) {
|
||||||
|
containerBuilder.withArgs("--verbose", "start");
|
||||||
|
}
|
||||||
|
if (customImage.isPresent()) {
|
||||||
|
containerBuilder.addToArgs(OPTIMIZED_ARG);
|
||||||
|
}
|
||||||
|
|
||||||
|
// probes
|
||||||
|
var tlsConfigured = isTlsConfigured(keycloakCR);
|
||||||
|
var protocol = !tlsConfigured ? "HTTP" : "HTTPS";
|
||||||
|
var kcPort = KeycloakServiceDependentResource.getServicePort(tlsConfigured, keycloakCR);
|
||||||
|
|
||||||
|
// Relative path ends with '/'
|
||||||
|
var kcRelativePath = readConfigurationValue(Constants.KEYCLOAK_HTTP_RELATIVE_PATH_KEY, keycloakCR, context)
|
||||||
|
.map(path -> !path.endsWith("/") ? path + "/" : path)
|
||||||
|
.orElse("/");
|
||||||
|
|
||||||
|
if (!containerBuilder.hasReadinessProbe()) {
|
||||||
|
containerBuilder.withNewReadinessProbe()
|
||||||
|
.withPeriodSeconds(10)
|
||||||
|
.withFailureThreshold(3)
|
||||||
|
.withNewHttpGet()
|
||||||
|
.withScheme(protocol)
|
||||||
|
.withNewPort(kcPort)
|
||||||
|
.withPath(kcRelativePath + "health/ready")
|
||||||
|
.endHttpGet()
|
||||||
|
.endReadinessProbe();
|
||||||
|
}
|
||||||
|
if (!containerBuilder.hasLivenessProbe()) {
|
||||||
|
containerBuilder.withNewLivenessProbe()
|
||||||
|
.withPeriodSeconds(10)
|
||||||
|
.withFailureThreshold(3)
|
||||||
|
.withNewHttpGet()
|
||||||
|
.withScheme(protocol)
|
||||||
|
.withNewPort(kcPort)
|
||||||
|
.withPath(kcRelativePath + "health/live")
|
||||||
|
.endHttpGet()
|
||||||
|
.endLivenessProbe();
|
||||||
|
}
|
||||||
|
if (!containerBuilder.hasStartupProbe()) {
|
||||||
|
containerBuilder.withNewStartupProbe()
|
||||||
|
.withPeriodSeconds(1)
|
||||||
|
.withFailureThreshold(600)
|
||||||
|
.withNewHttpGet()
|
||||||
|
.withScheme(protocol)
|
||||||
|
.withNewPort(kcPort)
|
||||||
|
.withPath(kcRelativePath + "health/started")
|
||||||
|
.endHttpGet()
|
||||||
|
.endStartupProbe();
|
||||||
|
}
|
||||||
|
|
||||||
|
// add in ports - there's no merging being done here
|
||||||
|
StatefulSet baseDeployment = containerBuilder
|
||||||
|
.addNewPort()
|
||||||
|
.withName(Constants.KEYCLOAK_HTTPS_PORT_NAME)
|
||||||
|
.withContainerPort(Constants.KEYCLOAK_HTTPS_PORT)
|
||||||
|
.withProtocol(Constants.KEYCLOAK_SERVICE_PROTOCOL)
|
||||||
|
.endPort()
|
||||||
|
.addNewPort()
|
||||||
|
.withName(Constants.KEYCLOAK_HTTP_PORT_NAME)
|
||||||
|
.withContainerPort(Constants.KEYCLOAK_HTTP_PORT)
|
||||||
|
.withProtocol(Constants.KEYCLOAK_SERVICE_PROTOCOL)
|
||||||
|
.endPort()
|
||||||
|
.endContainer().endSpec().endTemplate().endSpec().build();
|
||||||
|
|
||||||
|
return baseDeployment;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addEnvVarsAndWatchSecrets(StatefulSet baseDeployment, Keycloak keycloakCR) {
|
||||||
|
var firstClasssEnvVars = distConfigurator.configureDistOptions(keycloakCR);
|
||||||
|
|
||||||
|
String adminSecretName = KeycloakAdminSecretDependentResource.getName(keycloakCR);
|
||||||
|
var additionalEnvVars = getDefaultAndAdditionalEnvVars(keycloakCR, adminSecretName);
|
||||||
|
|
||||||
|
var env = Optional.ofNullable(baseDeployment.getSpec().getTemplate().getSpec().getContainers().get(0).getEnv()).orElse(List.of());
|
||||||
|
|
||||||
|
// accumulate the env vars in priority order - unsupported, first class, additional
|
||||||
|
var envVars = new ArrayList<>(Stream.concat(Stream.concat(env.stream(), firstClasssEnvVars.stream()), additionalEnvVars.stream())
|
||||||
|
.collect(Collectors.toMap(EnvVar::getName, Function.identity(), (e1, e2) -> e1, LinkedHashMap::new))
|
||||||
|
.values());
|
||||||
|
baseDeployment.getSpec().getTemplate().getSpec().getContainers().get(0).setEnv(envVars);
|
||||||
|
|
||||||
|
// watch the secrets used by secret key - we don't currently expect configmaps, optional refs, or watch the initial-admin
|
||||||
|
TreeSet<String> serverConfigSecretsNames = envVars.stream().map(EnvVar::getValueFrom).filter(Objects::nonNull)
|
||||||
|
.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);
|
||||||
|
|
||||||
|
// add secrets from volume mounts (currently just the tls secret)
|
||||||
|
if (isTlsConfigured(keycloakCR)) {
|
||||||
|
serverConfigSecretsNames.add(keycloakCR.getSpec().getHttpSpec().getTlsSecret());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!serverConfigSecretsNames.isEmpty()) {
|
||||||
|
watchedSecrets.annotateDeployment(new ArrayList<>(serverConfigSecretsNames), keycloakCR, baseDeployment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<EnvVar> getDefaultAndAdditionalEnvVars(Keycloak keycloakCR, String adminSecretName) {
|
||||||
|
// default config values
|
||||||
|
List<ValueOrSecret> serverConfigsList = new ArrayList<>(Constants.DEFAULT_DIST_CONFIG_LIST);
|
||||||
|
|
||||||
|
// merge with the CR; the values in CR take precedence
|
||||||
|
if (keycloakCR.getSpec().getAdditionalOptions() != null) {
|
||||||
|
Set<String> inCr = keycloakCR.getSpec().getAdditionalOptions().stream().map(v -> v.getName()).collect(Collectors.toSet());
|
||||||
|
serverConfigsList.removeIf(v -> inCr.contains(v.getName()));
|
||||||
|
serverConfigsList.addAll(keycloakCR.getSpec().getAdditionalOptions());
|
||||||
|
}
|
||||||
|
|
||||||
|
// set env vars
|
||||||
|
List<EnvVar> envVars = serverConfigsList.stream()
|
||||||
|
.map(v -> {
|
||||||
|
var envBuilder = new EnvVarBuilder().withName(KeycloakDistConfigurator.getKeycloakOptionEnvVarName(v.getName()));
|
||||||
|
var secret = v.getSecret();
|
||||||
|
if (secret != null) {
|
||||||
|
envBuilder.withValueFrom(
|
||||||
|
new EnvVarSourceBuilder().withSecretKeyRef(secret).build());
|
||||||
|
} else {
|
||||||
|
envBuilder.withValue(v.getValue());
|
||||||
|
}
|
||||||
|
return envBuilder.build();
|
||||||
|
})
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
envVars.add(
|
||||||
|
new EnvVarBuilder()
|
||||||
|
.withName("KEYCLOAK_ADMIN")
|
||||||
|
.withNewValueFrom()
|
||||||
|
.withNewSecretKeyRef()
|
||||||
|
.withName(adminSecretName)
|
||||||
|
.withKey("username")
|
||||||
|
.withOptional(false)
|
||||||
|
.endSecretKeyRef()
|
||||||
|
.endValueFrom()
|
||||||
|
.build());
|
||||||
|
envVars.add(
|
||||||
|
new EnvVarBuilder()
|
||||||
|
.withName("KEYCLOAK_ADMIN_PASSWORD")
|
||||||
|
.withNewValueFrom()
|
||||||
|
.withNewSecretKeyRef()
|
||||||
|
.withName(adminSecretName)
|
||||||
|
.withKey("password")
|
||||||
|
.withOptional(false)
|
||||||
|
.endSecretKeyRef()
|
||||||
|
.endValueFrom()
|
||||||
|
.build());
|
||||||
|
|
||||||
|
envVars.add(
|
||||||
|
new EnvVarBuilder()
|
||||||
|
.withName("jgroups.dns.query")
|
||||||
|
.withValue(getName(keycloakCR) + Constants.KEYCLOAK_DISCOVERY_SERVICE_SUFFIX +"." + keycloakCR.getMetadata().getNamespace())
|
||||||
|
.build());
|
||||||
|
|
||||||
|
return envVars;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getName(Keycloak keycloak) {
|
||||||
|
return keycloak.getMetadata().getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void migrateDeployment(StatefulSet previousDeployment, StatefulSet reconciledDeployment, Context<Keycloak> context) {
|
||||||
|
var previousContainer = Optional.ofNullable(previousDeployment).map(StatefulSet::getSpec)
|
||||||
|
.map(StatefulSetSpec::getTemplate).map(PodTemplateSpec::getSpec).map(PodSpec::getContainers)
|
||||||
|
.flatMap(c -> c.stream().findFirst()).orElse(null);
|
||||||
|
if (previousContainer == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var reconciledContainer = reconciledDeployment.getSpec().getTemplate().getSpec().getContainers().get(0);
|
||||||
|
|
||||||
|
if (!previousContainer.getImage().equals(reconciledContainer.getImage())
|
||||||
|
&& previousDeployment.getStatus().getReplicas() > 1) {
|
||||||
|
// TODO Check if migration is really needed (e.g. based on actual KC version); https://github.com/keycloak/keycloak/issues/10441
|
||||||
|
Log.info("Detected changed Keycloak image, assuming Keycloak upgrade. Scaling down the deployment to one instance to perform a safe database migration");
|
||||||
|
Log.infof("original image: %s; new image: %s", previousContainer.getImage(), reconciledContainer.getImage());
|
||||||
|
|
||||||
|
reconciledContainer.setImage(previousContainer.getImage());
|
||||||
|
reconciledDeployment.getSpec().setReplicas(1);
|
||||||
|
|
||||||
|
reconciledDeployment.getMetadata().getAnnotations().put(Constants.KEYCLOAK_MIGRATING_ANNOTATION, Boolean.TRUE.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Optional<String> readConfigurationValue(String key, Keycloak keycloakCR, Context<Keycloak> context) {
|
||||||
|
return Optional.ofNullable(keycloakCR.getSpec()).map(KeycloakSpec::getAdditionalOptions)
|
||||||
|
.flatMap(l -> l.stream().filter(sc -> sc.getName().equals(key)).findFirst().map(serverConfigValue -> {
|
||||||
|
if (serverConfigValue.getValue() != null) {
|
||||||
|
return serverConfigValue.getValue();
|
||||||
|
}
|
||||||
|
var secretSelector = serverConfigValue.getSecret();
|
||||||
|
if (secretSelector == null) {
|
||||||
|
throw new IllegalStateException("Secret " + serverConfigValue.getName() + " not defined");
|
||||||
|
}
|
||||||
|
var secret = context.getClient().secrets().inNamespace(keycloakCR.getMetadata().getNamespace()).withName(secretSelector.getName()).get();
|
||||||
|
if (secret == null) {
|
||||||
|
throw new IllegalStateException("Secret " + secretSelector.getName() + " not found in cluster");
|
||||||
|
}
|
||||||
|
if (secret.getData().containsKey(secretSelector.getKey())) {
|
||||||
|
return new String(Base64.getDecoder().decode(secret.getData().get(secretSelector.getKey())), StandardCharsets.UTF_8);
|
||||||
|
}
|
||||||
|
throw new IllegalStateException("Secret " + secretSelector.getName() + " doesn't contain the expected key " + secretSelector.getKey());
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -26,6 +26,7 @@ import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernete
|
||||||
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent;
|
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent;
|
||||||
|
|
||||||
import org.keycloak.operator.Constants;
|
import org.keycloak.operator.Constants;
|
||||||
|
import org.keycloak.operator.Utils;
|
||||||
import org.keycloak.operator.crds.v2alpha1.deployment.Keycloak;
|
import org.keycloak.operator.crds.v2alpha1.deployment.Keycloak;
|
||||||
|
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
@ -50,7 +51,7 @@ public class KeycloakDiscoveryServiceDependentResource extends CRUDKubernetesDep
|
||||||
.withProtocol("TCP")
|
.withProtocol("TCP")
|
||||||
.withPort(Constants.KEYCLOAK_DISCOVERY_SERVICE_PORT)
|
.withPort(Constants.KEYCLOAK_DISCOVERY_SERVICE_PORT)
|
||||||
.endPort()
|
.endPort()
|
||||||
.withSelector(OperatorManagedResource.allInstanceLabels(keycloak))
|
.withSelector(Utils.allInstanceLabels(keycloak))
|
||||||
.withClusterIP("None")
|
.withClusterIP("None")
|
||||||
.withPublishNotReadyAddresses(Boolean.TRUE)
|
.withPublishNotReadyAddresses(Boolean.TRUE)
|
||||||
.build();
|
.build();
|
||||||
|
@ -62,7 +63,7 @@ public class KeycloakDiscoveryServiceDependentResource extends CRUDKubernetesDep
|
||||||
.withNewMetadata()
|
.withNewMetadata()
|
||||||
.withName(getName(primary))
|
.withName(getName(primary))
|
||||||
.withNamespace(primary.getMetadata().getNamespace())
|
.withNamespace(primary.getMetadata().getNamespace())
|
||||||
.addToLabels(OperatorManagedResource.allInstanceLabels(primary))
|
.addToLabels(Utils.allInstanceLabels(primary))
|
||||||
.endMetadata()
|
.endMetadata()
|
||||||
.withSpec(getServiceSpec(primary))
|
.withSpec(getServiceSpec(primary))
|
||||||
.build();
|
.build();
|
||||||
|
|
|
@ -21,11 +21,8 @@ import io.fabric8.kubernetes.api.model.EnvVar;
|
||||||
import io.fabric8.kubernetes.api.model.EnvVarBuilder;
|
import io.fabric8.kubernetes.api.model.EnvVarBuilder;
|
||||||
import io.fabric8.kubernetes.api.model.EnvVarSourceBuilder;
|
import io.fabric8.kubernetes.api.model.EnvVarSourceBuilder;
|
||||||
import io.fabric8.kubernetes.api.model.SecretKeySelector;
|
import io.fabric8.kubernetes.api.model.SecretKeySelector;
|
||||||
import io.fabric8.kubernetes.api.model.VolumeBuilder;
|
|
||||||
import io.fabric8.kubernetes.api.model.VolumeMountBuilder;
|
|
||||||
import io.fabric8.kubernetes.api.model.apps.StatefulSet;
|
|
||||||
import io.fabric8.kubernetes.client.KubernetesClient;
|
|
||||||
import io.quarkus.logging.Log;
|
import io.quarkus.logging.Log;
|
||||||
|
|
||||||
import org.keycloak.common.util.CollectionUtil;
|
import org.keycloak.common.util.CollectionUtil;
|
||||||
import org.keycloak.operator.Constants;
|
import org.keycloak.operator.Constants;
|
||||||
import org.keycloak.operator.crds.v2alpha1.deployment.Keycloak;
|
import org.keycloak.operator.crds.v2alpha1.deployment.Keycloak;
|
||||||
|
@ -39,40 +36,31 @@ import org.keycloak.operator.crds.v2alpha1.deployment.spec.TransactionsSpec;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.HashSet;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
|
|
||||||
import static io.smallrye.config.common.utils.StringUtil.replaceNonAlphanumericByUnderscores;
|
import static io.smallrye.config.common.utils.StringUtil.replaceNonAlphanumericByUnderscores;
|
||||||
import static org.keycloak.operator.crds.v2alpha1.CRDUtils.isTlsConfigured;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configuration for the KeycloakDeployment
|
* Configuration for the Keycloak Statefulset
|
||||||
*/
|
*/
|
||||||
|
@ApplicationScoped
|
||||||
public class KeycloakDistConfigurator {
|
public class KeycloakDistConfigurator {
|
||||||
private final Keycloak keycloakCR;
|
|
||||||
private final StatefulSet deployment;
|
|
||||||
private final KubernetesClient client;
|
|
||||||
|
|
||||||
public KeycloakDistConfigurator(Keycloak keycloakCR, StatefulSet deployment, KubernetesClient client) {
|
|
||||||
this.keycloakCR = keycloakCR;
|
|
||||||
this.deployment = deployment;
|
|
||||||
this.client = client;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Specify first-class citizens fields which should not be added as general server configuration property
|
* Specify first-class citizens fields which should not be added as general server configuration property
|
||||||
*/
|
*/
|
||||||
private final Set<String> firstClassConfigOptions = new HashSet<>();
|
@SuppressWarnings("rawtypes")
|
||||||
|
private final Map<String, org.keycloak.operator.controllers.KeycloakDistConfigurator.OptionMapper.Mapper> firstClassConfigOptions = new LinkedHashMap<>();
|
||||||
|
|
||||||
/**
|
public KeycloakDistConfigurator() {
|
||||||
* Configure configuration properties for the KeycloakDeployment
|
// register the configuration mappers for the various parts of the keycloak cr
|
||||||
*/
|
|
||||||
public void configureDistOptions() {
|
|
||||||
configureHostname();
|
configureHostname();
|
||||||
configureFeatures();
|
configureFeatures();
|
||||||
configureTransactions();
|
configureTransactions();
|
||||||
|
@ -85,14 +73,14 @@ public class KeycloakDistConfigurator {
|
||||||
*
|
*
|
||||||
* @param status Keycloak Status builder
|
* @param status Keycloak Status builder
|
||||||
*/
|
*/
|
||||||
public void validateOptions(KeycloakStatusAggregator status) {
|
public void validateOptions(Keycloak keycloakCR, KeycloakStatusAggregator status) {
|
||||||
assumeFirstClassCitizens(status);
|
assumeFirstClassCitizens(keycloakCR, status);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------- Configuration of first-class citizen fields ---------- */
|
/* ---------- Configuration of first-class citizen fields ---------- */
|
||||||
|
|
||||||
public void configureHostname() {
|
void configureHostname() {
|
||||||
optionMapper(keycloakCR.getSpec().getHostnameSpec())
|
optionMapper(keycloakCR -> keycloakCR.getSpec().getHostnameSpec())
|
||||||
.mapOption("hostname", HostnameSpec::getHostname)
|
.mapOption("hostname", HostnameSpec::getHostname)
|
||||||
.mapOption("hostname-admin", HostnameSpec::getAdmin)
|
.mapOption("hostname-admin", HostnameSpec::getAdmin)
|
||||||
.mapOption("hostname-admin-url", HostnameSpec::getAdminUrl)
|
.mapOption("hostname-admin-url", HostnameSpec::getAdminUrl)
|
||||||
|
@ -100,61 +88,28 @@ public class KeycloakDistConfigurator {
|
||||||
.mapOption("hostname-strict-backchannel", HostnameSpec::isStrictBackchannel);
|
.mapOption("hostname-strict-backchannel", HostnameSpec::isStrictBackchannel);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void configureFeatures() {
|
void configureFeatures() {
|
||||||
optionMapper(keycloakCR.getSpec().getFeatureSpec())
|
optionMapper(keycloakCR -> keycloakCR.getSpec().getFeatureSpec())
|
||||||
.mapOptionFromCollection("features", FeatureSpec::getEnabledFeatures)
|
.mapOptionFromCollection("features", FeatureSpec::getEnabledFeatures)
|
||||||
.mapOptionFromCollection("features-disabled", FeatureSpec::getDisabledFeatures);
|
.mapOptionFromCollection("features-disabled", FeatureSpec::getDisabledFeatures);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void configureTransactions() {
|
void configureTransactions() {
|
||||||
optionMapper(keycloakCR.getSpec().getTransactionsSpec())
|
optionMapper(keycloakCR -> keycloakCR.getSpec().getTransactionsSpec())
|
||||||
.mapOption("transaction-xa-enabled", TransactionsSpec::isXaEnabled);
|
.mapOption("transaction-xa-enabled", TransactionsSpec::isXaEnabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void configureHttp() {
|
void configureHttp() {
|
||||||
var optionMapper = optionMapper(keycloakCR.getSpec().getHttpSpec())
|
optionMapper(keycloakCR -> keycloakCR.getSpec().getHttpSpec())
|
||||||
.mapOption("http-enabled", HttpSpec::getHttpEnabled)
|
.mapOption("http-enabled", HttpSpec::getHttpEnabled)
|
||||||
.mapOption("http-port", HttpSpec::getHttpPort)
|
.mapOption("http-port", HttpSpec::getHttpPort)
|
||||||
.mapOption("https-port", HttpSpec::getHttpsPort);
|
.mapOption("https-port", HttpSpec::getHttpsPort)
|
||||||
|
.mapOption("https-certificate-file", http -> (http.getTlsSecret() != null && !http.getTlsSecret().isEmpty()) ? Constants.CERTIFICATES_FOLDER + "/tls.crt" : null)
|
||||||
configureTLS(optionMapper);
|
.mapOption("https-certificate-key-file", http -> (http.getTlsSecret() != null && !http.getTlsSecret().isEmpty()) ? Constants.CERTIFICATES_FOLDER + "/tls.key" : null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void configureTLS(OptionMapper<HttpSpec> optionMapper) {
|
void configureDatabase() {
|
||||||
final String certFileOptionName = "https-certificate-file";
|
optionMapper(keycloakCR -> keycloakCR.getSpec().getDatabaseSpec())
|
||||||
final String keyFileOptionName = "https-certificate-key-file";
|
|
||||||
|
|
||||||
if (!isTlsConfigured(keycloakCR)) {
|
|
||||||
// for mapping and triggering warning in status if someone uses the fields directly
|
|
||||||
optionMapper.mapOption(certFileOptionName);
|
|
||||||
optionMapper.mapOption(keyFileOptionName);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
optionMapper.mapOption(certFileOptionName, Constants.CERTIFICATES_FOLDER + "/tls.crt");
|
|
||||||
optionMapper.mapOption(keyFileOptionName, Constants.CERTIFICATES_FOLDER + "/tls.key");
|
|
||||||
|
|
||||||
var kcContainer = deployment.getSpec().getTemplate().getSpec().getContainers().get(0);
|
|
||||||
|
|
||||||
var volume = new VolumeBuilder()
|
|
||||||
.withName("keycloak-tls-certificates")
|
|
||||||
.withNewSecret()
|
|
||||||
.withSecretName(keycloakCR.getSpec().getHttpSpec().getTlsSecret())
|
|
||||||
.withOptional(false)
|
|
||||||
.endSecret()
|
|
||||||
.build();
|
|
||||||
|
|
||||||
var volumeMount = new VolumeMountBuilder()
|
|
||||||
.withName(volume.getName())
|
|
||||||
.withMountPath(Constants.CERTIFICATES_FOLDER)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
deployment.getSpec().getTemplate().getSpec().getVolumes().add(0, volume);
|
|
||||||
kcContainer.getVolumeMounts().add(0, volumeMount);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void configureDatabase() {
|
|
||||||
optionMapper(keycloakCR.getSpec().getDatabaseSpec())
|
|
||||||
.mapOption("db", DatabaseSpec::getVendor)
|
.mapOption("db", DatabaseSpec::getVendor)
|
||||||
.mapOption("db-username", DatabaseSpec::getUsernameSecret)
|
.mapOption("db-username", DatabaseSpec::getUsernameSecret)
|
||||||
.mapOption("db-password", DatabaseSpec::getPasswordSecret)
|
.mapOption("db-password", DatabaseSpec::getPasswordSecret)
|
||||||
|
@ -175,7 +130,7 @@ public class KeycloakDistConfigurator {
|
||||||
*
|
*
|
||||||
* @param status Status of the deployment
|
* @param status Status of the deployment
|
||||||
*/
|
*/
|
||||||
protected void assumeFirstClassCitizens(KeycloakStatusAggregator status) {
|
protected void assumeFirstClassCitizens(Keycloak keycloakCR, KeycloakStatusAggregator status) {
|
||||||
final var serverConfigNames = keycloakCR
|
final var serverConfigNames = keycloakCR
|
||||||
.getSpec()
|
.getSpec()
|
||||||
.getAdditionalOptions()
|
.getAdditionalOptions()
|
||||||
|
@ -183,7 +138,7 @@ public class KeycloakDistConfigurator {
|
||||||
.map(ValueOrSecret::getName)
|
.map(ValueOrSecret::getName)
|
||||||
.collect(Collectors.toSet());
|
.collect(Collectors.toSet());
|
||||||
|
|
||||||
final var sameItems = CollectionUtil.intersection(serverConfigNames, firstClassConfigOptions);
|
final var sameItems = CollectionUtil.intersection(serverConfigNames, firstClassConfigOptions.keySet());
|
||||||
if (CollectionUtil.isNotEmpty(sameItems)) {
|
if (CollectionUtil.isNotEmpty(sameItems)) {
|
||||||
status.addWarningMessage("You need to specify these fields as the first-class citizen of the CR: "
|
status.addWarningMessage("You need to specify these fields as the first-class citizen of the CR: "
|
||||||
+ CollectionUtil.join(sameItems, ","));
|
+ CollectionUtil.join(sameItems, ","));
|
||||||
|
@ -195,79 +150,58 @@ public class KeycloakDistConfigurator {
|
||||||
return "KC_" + replaceNonAlphanumericByUnderscores(kcConfigName).toUpperCase();
|
return "KC_" + replaceNonAlphanumericByUnderscores(kcConfigName).toUpperCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
private <T> OptionMapper<T> optionMapper(T optionSpec) {
|
private <T> OptionMapper<T> optionMapper(Function<Keycloak, T> optionSpec) {
|
||||||
return new OptionMapper<>(optionSpec);
|
return new OptionMapper<>(optionSpec);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Collection<String> getSecretNames() {
|
private class OptionMapper<T> {
|
||||||
Set<String> names = new HashSet<>();
|
|
||||||
|
|
||||||
if (isTlsConfigured(keycloakCR)) {
|
private class Mapper<R> {
|
||||||
names.add(keycloakCR.getSpec().getHttpSpec().getTlsSecret());
|
Function<T, R> optionValueSupplier;
|
||||||
|
|
||||||
|
public Mapper(Function<T, R> optionValueSupplier) {
|
||||||
|
this.optionValueSupplier = optionValueSupplier;
|
||||||
|
}
|
||||||
|
|
||||||
|
void map(String optionName, Keycloak keycloak, List<EnvVar> variables) {
|
||||||
|
var categorySpec = optionSpec.apply(keycloak);
|
||||||
|
|
||||||
|
if (categorySpec == null) {
|
||||||
|
Log.debugf("No category spec provided for %s", optionName);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
R value = optionValueSupplier.apply(categorySpec);
|
||||||
|
|
||||||
|
if (value == null || value.toString().trim().isEmpty()) {
|
||||||
|
Log.debugf("No value provided for %s", optionName);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
EnvVarBuilder envVarBuilder = new EnvVarBuilder()
|
||||||
|
.withName(getKeycloakOptionEnvVarName(optionName));
|
||||||
|
|
||||||
|
if (value instanceof SecretKeySelector) {
|
||||||
|
envVarBuilder.withValueFrom(new EnvVarSourceBuilder().withSecretKeyRef((SecretKeySelector) value).build());
|
||||||
|
} else {
|
||||||
|
envVarBuilder.withValue(String.valueOf(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
variables.add(envVarBuilder.build());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Optional.ofNullable(keycloakCR.getSpec().getDatabaseSpec()).map(DatabaseSpec::getUsernameSecret).map(SecretKeySelector::getName).ifPresent(names::add);
|
private final Function<Keycloak, T> optionSpec;
|
||||||
Optional.ofNullable(keycloakCR.getSpec().getDatabaseSpec()).map(DatabaseSpec::getPasswordSecret).map(SecretKeySelector::getName).ifPresent(names::add);
|
|
||||||
|
|
||||||
return names;
|
public OptionMapper(Function<Keycloak, T> optionSpec) {
|
||||||
}
|
this.optionSpec = optionSpec;
|
||||||
|
|
||||||
private class OptionMapper<T> {
|
|
||||||
private final T categorySpec;
|
|
||||||
private final List<EnvVar> envVars;
|
|
||||||
|
|
||||||
public OptionMapper(T optionSpec) {
|
|
||||||
this.categorySpec = optionSpec;
|
|
||||||
|
|
||||||
var kcContainer = deployment.getSpec().getTemplate().getSpec().getContainers().get(0);
|
|
||||||
var envVars = kcContainer.getEnv();
|
|
||||||
if (envVars == null) {
|
|
||||||
envVars = new ArrayList<>();
|
|
||||||
kcContainer.setEnv(envVars);
|
|
||||||
}
|
|
||||||
this.envVars = envVars;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public <R> OptionMapper<T> mapOption(String optionName, Function<T, R> optionValueSupplier) {
|
public <R> OptionMapper<T> mapOption(String optionName, Function<T, R> optionValueSupplier) {
|
||||||
firstClassConfigOptions.add(optionName);
|
firstClassConfigOptions.put(optionName, new Mapper<>(optionValueSupplier));
|
||||||
|
|
||||||
if (categorySpec == null) {
|
|
||||||
Log.debugf("No category spec provided for %s", optionName);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
R value = optionValueSupplier.apply(categorySpec);
|
|
||||||
|
|
||||||
if (value == null || value.toString().trim().isEmpty()) {
|
|
||||||
Log.debugf("No value provided for %s", optionName);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
EnvVarBuilder envVarBuilder = new EnvVarBuilder()
|
|
||||||
.withName(getKeycloakOptionEnvVarName(optionName));
|
|
||||||
|
|
||||||
if (value instanceof SecretKeySelector) {
|
|
||||||
envVarBuilder.withValueFrom(new EnvVarSourceBuilder().withSecretKeyRef((SecretKeySelector) value).build());
|
|
||||||
} else {
|
|
||||||
envVarBuilder.withValue(String.valueOf(value));
|
|
||||||
}
|
|
||||||
|
|
||||||
var toAdd = envVarBuilder.build();
|
|
||||||
if (!envVars.stream().anyMatch(envVar -> envVar.getName().equals(toAdd.getName()))) {
|
|
||||||
envVars.add(toAdd);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public <R> OptionMapper<T> mapOption(String optionName) {
|
|
||||||
return mapOption(optionName, s -> null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public <R> OptionMapper<T> mapOption(String optionName, R optionValue) {
|
|
||||||
return mapOption(optionName, s -> optionValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected <R extends Collection<?>> OptionMapper<T> mapOptionFromCollection(String optionName, Function<T, R> optionValueSupplier) {
|
protected <R extends Collection<?>> OptionMapper<T> mapOptionFromCollection(String optionName, Function<T, R> optionValueSupplier) {
|
||||||
return mapOption(optionName, s -> {
|
return mapOption(optionName, s -> {
|
||||||
var value = optionValueSupplier.apply(s);
|
var value = optionValueSupplier.apply(s);
|
||||||
|
@ -276,4 +210,11 @@ public class KeycloakDistConfigurator {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public List<EnvVar> configureDistOptions(Keycloak keycloakCR) {
|
||||||
|
List<EnvVar> result = new ArrayList<>();
|
||||||
|
firstClassConfigOptions.entrySet().forEach(e -> e.getValue().map(e.getKey(), keycloakCR, result));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,6 +25,7 @@ import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDep
|
||||||
import io.javaoperatorsdk.operator.processing.dependent.workflow.Condition;
|
import io.javaoperatorsdk.operator.processing.dependent.workflow.Condition;
|
||||||
|
|
||||||
import org.keycloak.operator.Constants;
|
import org.keycloak.operator.Constants;
|
||||||
|
import org.keycloak.operator.Utils;
|
||||||
import org.keycloak.operator.crds.v2alpha1.deployment.Keycloak;
|
import org.keycloak.operator.crds.v2alpha1.deployment.Keycloak;
|
||||||
import org.keycloak.operator.crds.v2alpha1.deployment.spec.IngressSpec;
|
import org.keycloak.operator.crds.v2alpha1.deployment.spec.IngressSpec;
|
||||||
|
|
||||||
|
@ -73,8 +74,7 @@ public class KeycloakIngressDependentResource extends CRUDKubernetesDependentRes
|
||||||
.withNewMetadata()
|
.withNewMetadata()
|
||||||
.withName(getName(keycloak))
|
.withName(getName(keycloak))
|
||||||
.withNamespace(keycloak.getMetadata().getNamespace())
|
.withNamespace(keycloak.getMetadata().getNamespace())
|
||||||
.addToLabels(Constants.DEFAULT_LABELS)
|
.addToLabels(Utils.allInstanceLabels(keycloak))
|
||||||
.addToLabels(OperatorManagedResource.updateWithInstanceLabels(null, keycloak.getMetadata().getName()))
|
|
||||||
.addToAnnotations(annotations)
|
.addToAnnotations(annotations)
|
||||||
.endMetadata()
|
.endMetadata()
|
||||||
.withNewSpec()
|
.withNewSpec()
|
||||||
|
|
|
@ -34,6 +34,7 @@ import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDep
|
||||||
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResourceConfig;
|
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResourceConfig;
|
||||||
|
|
||||||
import org.keycloak.operator.Constants;
|
import org.keycloak.operator.Constants;
|
||||||
|
import org.keycloak.operator.Utils;
|
||||||
import org.keycloak.operator.crds.v2alpha1.realmimport.KeycloakRealmImport;
|
import org.keycloak.operator.crds.v2alpha1.realmimport.KeycloakRealmImport;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
@ -93,7 +94,7 @@ public class KeycloakRealmImportJobDependentResource extends KubernetesDependent
|
||||||
.withName(primary.getMetadata().getName())
|
.withName(primary.getMetadata().getName())
|
||||||
.withNamespace(primary.getMetadata().getNamespace())
|
.withNamespace(primary.getMetadata().getNamespace())
|
||||||
// this is labeling the instance as the realm import, not the keycloak
|
// this is labeling the instance as the realm import, not the keycloak
|
||||||
.withLabels(OperatorManagedResource.allInstanceLabels(primary))
|
.withLabels(Utils.allInstanceLabels(primary))
|
||||||
.endMetadata()
|
.endMetadata()
|
||||||
.withNewSpec()
|
.withNewSpec()
|
||||||
.withTemplate(keycloakPodTemplate)
|
.withTemplate(keycloakPodTemplate)
|
||||||
|
@ -117,7 +118,7 @@ public class KeycloakRealmImportJobDependentResource extends KubernetesDependent
|
||||||
|
|
||||||
var override = "--override=false";
|
var override = "--override=false";
|
||||||
|
|
||||||
var runBuild = !keycloakContainer.getArgs().contains(KeycloakDeployment.OPTIMIZED_ARG) ? "/opt/keycloak/bin/kc.sh --verbose build && " : "";
|
var runBuild = !keycloakContainer.getArgs().contains(KeycloakDeploymentDependentResource.OPTIMIZED_ARG) ? "/opt/keycloak/bin/kc.sh --verbose build && " : "";
|
||||||
|
|
||||||
var commandArgs = List.of("-c",
|
var commandArgs = List.of("-c",
|
||||||
runBuild + "/opt/keycloak/bin/kc.sh --verbose import --optimized --file='" + importMntPath + realmName + "-realm.json' " + override);
|
runBuild + "/opt/keycloak/bin/kc.sh --verbose import --optimized --file='" + importMntPath + realmName + "-realm.json' " + override);
|
||||||
|
|
|
@ -28,7 +28,7 @@ public class KeycloakRealmImportSecretDependentResource extends CRUDKubernetesDe
|
||||||
.withName(getSecretName(primary))
|
.withName(getSecretName(primary))
|
||||||
.withNamespace(primary.getMetadata().getNamespace())
|
.withNamespace(primary.getMetadata().getNamespace())
|
||||||
// this is labeling the instance as the realm import, not the keycloak
|
// this is labeling the instance as the realm import, not the keycloak
|
||||||
.addToLabels(OperatorManagedResource.allInstanceLabels(primary))
|
.addToLabels(Utils.allInstanceLabels(primary))
|
||||||
.endMetadata()
|
.endMetadata()
|
||||||
.addToData(fileName, Utils.asBase64(content))
|
.addToData(fileName, Utils.asBase64(content))
|
||||||
.build();
|
.build();
|
||||||
|
|
|
@ -29,6 +29,7 @@ import io.javaoperatorsdk.operator.processing.event.ResourceID;
|
||||||
import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource;
|
import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource;
|
||||||
|
|
||||||
import org.keycloak.operator.Constants;
|
import org.keycloak.operator.Constants;
|
||||||
|
import org.keycloak.operator.Utils;
|
||||||
import org.keycloak.operator.crds.v2alpha1.deployment.Keycloak;
|
import org.keycloak.operator.crds.v2alpha1.deployment.Keycloak;
|
||||||
import org.keycloak.operator.crds.v2alpha1.deployment.spec.HttpSpec;
|
import org.keycloak.operator.crds.v2alpha1.deployment.spec.HttpSpec;
|
||||||
|
|
||||||
|
@ -59,7 +60,7 @@ public class KeycloakServiceDependentResource extends CRUDKubernetesDependentRes
|
||||||
}
|
}
|
||||||
|
|
||||||
private ServiceSpec getServiceSpec(Keycloak keycloak) {
|
private ServiceSpec getServiceSpec(Keycloak keycloak) {
|
||||||
var builder = new ServiceSpecBuilder().withSelector(OperatorManagedResource.allInstanceLabels(keycloak));
|
var builder = new ServiceSpecBuilder().withSelector(Utils.allInstanceLabels(keycloak));
|
||||||
|
|
||||||
boolean tlsConfigured = isTlsConfigured(keycloak);
|
boolean tlsConfigured = isTlsConfigured(keycloak);
|
||||||
Optional<HttpSpec> httpSpec = Optional.ofNullable(keycloak.getSpec().getHttpSpec());
|
Optional<HttpSpec> httpSpec = Optional.ofNullable(keycloak.getSpec().getHttpSpec());
|
||||||
|
@ -81,7 +82,7 @@ public class KeycloakServiceDependentResource extends CRUDKubernetesDependentRes
|
||||||
.withNewMetadata()
|
.withNewMetadata()
|
||||||
.withName(getServiceName(primary))
|
.withName(getServiceName(primary))
|
||||||
.withNamespace(primary.getMetadata().getNamespace())
|
.withNamespace(primary.getMetadata().getNamespace())
|
||||||
.addToLabels(OperatorManagedResource.allInstanceLabels(primary))
|
.addToLabels(Utils.allInstanceLabels(primary))
|
||||||
.endMetadata()
|
.endMetadata()
|
||||||
.withSpec(getServiceSpec(primary))
|
.withSpec(getServiceSpec(primary))
|
||||||
.build();
|
.build();
|
||||||
|
|
|
@ -1,64 +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.HasMetadata;
|
|
||||||
import io.fabric8.kubernetes.api.model.ObjectMeta;
|
|
||||||
import io.fabric8.kubernetes.client.utils.Serialization;
|
|
||||||
import io.javaoperatorsdk.operator.processing.event.source.filter.OnUpdateFilter;
|
|
||||||
|
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* addresses the additional events noted on https://github.com/fabric8io/kubernetes-client/issues/5215
|
|
||||||
* <p>
|
|
||||||
* - usage of secret stringData has been removed,
|
|
||||||
* - but ingress usage of empty strings is still problematic
|
|
||||||
* it seems best to leave this in place for all resources to make sure we don't get in a reconciliation loop.
|
|
||||||
* <p>
|
|
||||||
* This should be removable after switching to dependent resources
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
public class MetadataAwareOnUpdateFilter<T extends HasMetadata> implements OnUpdateFilter<T> {
|
|
||||||
|
|
||||||
private static final ObjectMapper mapper = new ObjectMapper();
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean accept(HasMetadata newResource, HasMetadata oldResource) {
|
|
||||||
ObjectMeta newMetadata = newResource.getMetadata();
|
|
||||||
ObjectMeta oldMetadata = oldResource.getMetadata();
|
|
||||||
// quick check if anything meaningful has changed
|
|
||||||
if (!Objects.equals(newMetadata.getAnnotations(), oldMetadata.getAnnotations())
|
|
||||||
|| !Objects.equals(newMetadata.getLabels(), oldMetadata.getLabels())
|
|
||||||
|| !Objects.equals(newMetadata.getGeneration(), oldMetadata.getGeneration())) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
// check everything else besides the metadata
|
|
||||||
// since the hierarchy of model the does not implement hasCode/equals, we'll convert to a generic form
|
|
||||||
// that should be less expensive than full serialization
|
|
||||||
var newMap = (ObjectNode)mapper.valueToTree(newResource);
|
|
||||||
newMap.remove("metadata");
|
|
||||||
var oldMap = (ObjectNode)mapper.valueToTree(oldResource);
|
|
||||||
oldMap.remove("metadata");
|
|
||||||
return !oldMap.equals(newMap);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,134 +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.OwnerReference;
|
|
||||||
import io.fabric8.kubernetes.api.model.OwnerReferenceBuilder;
|
|
||||||
import io.fabric8.kubernetes.client.KubernetesClient;
|
|
||||||
import io.fabric8.kubernetes.client.KubernetesClientException;
|
|
||||||
import io.fabric8.kubernetes.client.dsl.base.PatchContext;
|
|
||||||
import io.fabric8.kubernetes.client.dsl.base.PatchType;
|
|
||||||
import io.quarkus.logging.Log;
|
|
||||||
|
|
||||||
import org.keycloak.operator.Constants;
|
|
||||||
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.LinkedHashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents a single K8s resource that is managed by this operator (e.g. Deployment, Service, Ingress, etc.)
|
|
||||||
*
|
|
||||||
* @author Vaclav Muzikar <vmuzikar@redhat.com>
|
|
||||||
*/
|
|
||||||
public abstract class OperatorManagedResource<T extends HasMetadata> {
|
|
||||||
private static final String KEYCLOAK_OPERATOR_FIELD_MANAGER = "keycloak-operator";
|
|
||||||
protected KubernetesClient client;
|
|
||||||
protected HasMetadata cr;
|
|
||||||
|
|
||||||
public OperatorManagedResource(KubernetesClient client, HasMetadata cr) {
|
|
||||||
this.client = client;
|
|
||||||
this.cr = cr;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected abstract Optional<T> getReconciledResource();
|
|
||||||
|
|
||||||
public Optional<T> createOrUpdateReconciled() {
|
|
||||||
return getReconciledResource().map(resource -> {
|
|
||||||
try {
|
|
||||||
setInstanceLabels(resource);
|
|
||||||
setOwnerReferences(resource);
|
|
||||||
|
|
||||||
Log.debugf("Creating or updating resource: %s", resource);
|
|
||||||
try {
|
|
||||||
resource = client.resource(resource).inNamespace(getNamespace()).forceConflicts().fieldManager(KEYCLOAK_OPERATOR_FIELD_MANAGER).serverSideApply();
|
|
||||||
} catch (KubernetesClientException e) {
|
|
||||||
if (e.getCode() != 422) {
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
Log.infof("Could not apply changes to resource %s %s/%s will try strategic merge instead",
|
|
||||||
resource.getKind(), resource.getMetadata().getNamespace(),
|
|
||||||
resource.getMetadata().getName(), e.getMessage());
|
|
||||||
try {
|
|
||||||
client.resource(resource).patch(PatchContext.of(PatchType.STRATEGIC_MERGE));
|
|
||||||
} catch (KubernetesClientException ex) {
|
|
||||||
if (ex.getCode() == 422) {
|
|
||||||
Log.warnf("Could not apply changes to resource %s %s/%s if you have modified the resource please revert it or delete the resource so that the operator may regain control",
|
|
||||||
resource.getKind(), resource.getMetadata().getNamespace(),
|
|
||||||
resource.getMetadata().getName());
|
|
||||||
}
|
|
||||||
throw ex;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Log.debugf("Successfully created or updated resource: %s %s/%s", resource.getKind(), resource.getMetadata().getNamespace(),
|
|
||||||
resource.getMetadata().getName());
|
|
||||||
return resource;
|
|
||||||
} catch (Exception e) {
|
|
||||||
Log.errorf("Failed to create or update resource %s %s/%s", resource.getKind(), resource.getMetadata().getNamespace(),
|
|
||||||
resource.getMetadata().getName());
|
|
||||||
throw KubernetesClientException.launderThrowable(e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void setInstanceLabels(HasMetadata resource) {
|
|
||||||
resource.getMetadata().setLabels(updateWithInstanceLabels(resource.getMetadata().getLabels(), cr.getMetadata().getName()));
|
|
||||||
}
|
|
||||||
|
|
||||||
protected Map<String, String> getInstanceLabels() {
|
|
||||||
return updateWithInstanceLabels(null, cr.getMetadata().getName());
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Map<String, String> updateWithInstanceLabels(Map<String, String> labels, String instanceName) {
|
|
||||||
labels = Optional.ofNullable(labels).orElse(new LinkedHashMap<>());
|
|
||||||
labels.putAll(Constants.DEFAULT_LABELS);
|
|
||||||
labels.put(Constants.INSTANCE_LABEL, instanceName);
|
|
||||||
return labels;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Map<String, String> allInstanceLabels(HasMetadata primary) {
|
|
||||||
var labels = new LinkedHashMap<>(Constants.DEFAULT_LABELS);
|
|
||||||
labels.put(Constants.INSTANCE_LABEL, primary.getMetadata().getName());
|
|
||||||
return labels;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void setOwnerReferences(HasMetadata resource) {
|
|
||||||
if (!cr.getMetadata().getNamespace().equals(resource.getMetadata().getNamespace())) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
OwnerReference owner = new OwnerReferenceBuilder()
|
|
||||||
.withApiVersion(cr.getApiVersion())
|
|
||||||
.withKind(cr.getKind())
|
|
||||||
.withName(cr.getMetadata().getName())
|
|
||||||
.withUid(cr.getMetadata().getUid())
|
|
||||||
.withBlockOwnerDeletion(true)
|
|
||||||
.withController(true)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
resource.getMetadata().setOwnerReferences(Collections.singletonList(owner));
|
|
||||||
}
|
|
||||||
|
|
||||||
protected String getNamespace() {
|
|
||||||
return cr.getMetadata().getNamespace();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected abstract String getName();
|
|
||||||
}
|
|
|
@ -31,12 +31,19 @@ import io.fabric8.kubernetes.client.ConfigBuilder;
|
||||||
import io.fabric8.kubernetes.client.KubernetesClient;
|
import io.fabric8.kubernetes.client.KubernetesClient;
|
||||||
import io.fabric8.kubernetes.client.KubernetesClientBuilder;
|
import io.fabric8.kubernetes.client.KubernetesClientBuilder;
|
||||||
import io.fabric8.kubernetes.client.KubernetesClientException;
|
import io.fabric8.kubernetes.client.KubernetesClientException;
|
||||||
import io.fabric8.kubernetes.client.NamespacedKubernetesClient;
|
|
||||||
import io.fabric8.kubernetes.client.dsl.Loggable;
|
import io.fabric8.kubernetes.client.dsl.Loggable;
|
||||||
import io.fabric8.kubernetes.client.dsl.Resource;
|
import io.fabric8.kubernetes.client.dsl.Resource;
|
||||||
import io.fabric8.kubernetes.client.utils.Serialization;
|
import io.fabric8.kubernetes.client.utils.Serialization;
|
||||||
import io.javaoperatorsdk.operator.Operator;
|
import io.javaoperatorsdk.operator.Operator;
|
||||||
|
import io.javaoperatorsdk.operator.api.config.BaseConfigurationService;
|
||||||
|
import io.javaoperatorsdk.operator.api.config.ControllerConfiguration;
|
||||||
|
import io.javaoperatorsdk.operator.api.config.Utils;
|
||||||
|
import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceConfigurationResolver;
|
||||||
|
import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceSpec;
|
||||||
import io.javaoperatorsdk.operator.api.reconciler.Reconciler;
|
import io.javaoperatorsdk.operator.api.reconciler.Reconciler;
|
||||||
|
import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent;
|
||||||
|
import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource;
|
||||||
|
import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResourceFactory;
|
||||||
import io.quarkiverse.operatorsdk.runtime.QuarkusConfigurationService;
|
import io.quarkiverse.operatorsdk.runtime.QuarkusConfigurationService;
|
||||||
import io.quarkus.logging.Log;
|
import io.quarkus.logging.Log;
|
||||||
import io.quarkus.test.junit.callback.QuarkusTestAfterEachCallback;
|
import io.quarkus.test.junit.callback.QuarkusTestAfterEachCallback;
|
||||||
|
@ -49,7 +56,7 @@ import org.junit.jupiter.api.BeforeAll;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.TestInfo;
|
import org.junit.jupiter.api.TestInfo;
|
||||||
import org.keycloak.operator.Constants;
|
import org.keycloak.operator.Constants;
|
||||||
import org.keycloak.operator.controllers.KeycloakDeployment;
|
import org.keycloak.operator.controllers.KeycloakDeploymentDependentResource;
|
||||||
import org.keycloak.operator.crds.v2alpha1.deployment.Keycloak;
|
import org.keycloak.operator.crds.v2alpha1.deployment.Keycloak;
|
||||||
import org.keycloak.operator.crds.v2alpha1.deployment.KeycloakSpecBuilder;
|
import org.keycloak.operator.crds.v2alpha1.deployment.KeycloakSpecBuilder;
|
||||||
import org.keycloak.operator.crds.v2alpha1.deployment.KeycloakSpecFluent.UnsupportedNested;
|
import org.keycloak.operator.crds.v2alpha1.deployment.KeycloakSpecFluent.UnsupportedNested;
|
||||||
|
@ -96,7 +103,6 @@ public class BaseOperatorTest implements QuarkusTestAfterEachCallback {
|
||||||
public enum OperatorDeployment {local,remote}
|
public enum OperatorDeployment {local,remote}
|
||||||
|
|
||||||
protected static OperatorDeployment operatorDeployment;
|
protected static OperatorDeployment operatorDeployment;
|
||||||
protected static Instance<Reconciler<? extends HasMetadata>> reconcilers;
|
|
||||||
protected static QuarkusConfigurationService configuration;
|
protected static QuarkusConfigurationService configuration;
|
||||||
protected static KubernetesClient k8sclient;
|
protected static KubernetesClient k8sclient;
|
||||||
protected static String namespace;
|
protected static String namespace;
|
||||||
|
@ -109,7 +115,6 @@ public class BaseOperatorTest implements QuarkusTestAfterEachCallback {
|
||||||
@BeforeAll
|
@BeforeAll
|
||||||
public static void before() throws FileNotFoundException {
|
public static void before() throws FileNotFoundException {
|
||||||
configuration = CDI.current().select(QuarkusConfigurationService.class).get();
|
configuration = CDI.current().select(QuarkusConfigurationService.class).get();
|
||||||
reconcilers = CDI.current().select(new TypeLiteral<>() {});
|
|
||||||
operatorDeployment = ConfigProvider.getConfig().getOptionalValue(OPERATOR_DEPLOYMENT_PROP, OperatorDeployment.class).orElse(OperatorDeployment.local);
|
operatorDeployment = ConfigProvider.getConfig().getOptionalValue(OPERATOR_DEPLOYMENT_PROP, OperatorDeployment.class).orElse(OperatorDeployment.local);
|
||||||
deploymentTarget = ConfigProvider.getConfig().getOptionalValue(QUARKUS_KUBERNETES_DEPLOYMENT_TARGET, String.class).orElse("kubernetes");
|
deploymentTarget = ConfigProvider.getConfig().getOptionalValue(QUARKUS_KUBERNETES_DEPLOYMENT_TARGET, String.class).orElse("kubernetes");
|
||||||
customImage = ConfigProvider.getConfig().getOptionalValue(OPERATOR_CUSTOM_IMAGE, String.class).orElse(null);
|
customImage = ConfigProvider.getConfig().getOptionalValue(OPERATOR_CUSTOM_IMAGE, String.class).orElse(null);
|
||||||
|
@ -179,6 +184,8 @@ public class BaseOperatorTest implements QuarkusTestAfterEachCallback {
|
||||||
private static void registerReconcilers() {
|
private static void registerReconcilers() {
|
||||||
Log.info("Registering reconcilers for operator : " + operator + " [" + operatorDeployment + "]");
|
Log.info("Registering reconcilers for operator : " + operator + " [" + operatorDeployment + "]");
|
||||||
|
|
||||||
|
Instance<Reconciler<? extends HasMetadata>> reconcilers = CDI.current().select(new TypeLiteral<>() {});
|
||||||
|
|
||||||
for (Reconciler<?> reconciler : reconcilers) {
|
for (Reconciler<?> reconciler : reconcilers) {
|
||||||
Log.info("Register and apply : " + reconciler.getClass().getName());
|
Log.info("Register and apply : " + reconciler.getClass().getName());
|
||||||
operator.register(reconciler, overrider -> overrider.settingNamespace(namespace));
|
operator.register(reconciler, overrider -> overrider.settingNamespace(namespace));
|
||||||
|
@ -186,14 +193,37 @@ public class BaseOperatorTest implements QuarkusTestAfterEachCallback {
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void createOperator() {
|
private static void createOperator() {
|
||||||
operator = new Operator(overrider -> overrider.withKubernetesClient(k8sclient));
|
// create the operator to use the current client / namespace and injected dependent resources
|
||||||
|
// to be replaced later with full cdi construction or test mechanics from quarkus operator sdk
|
||||||
|
operator = new Operator(new BaseConfigurationService() {
|
||||||
|
@Override
|
||||||
|
public KubernetesClient getKubernetesClient() {
|
||||||
|
return k8sclient;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DependentResourceFactory dependentResourceFactory() {
|
||||||
|
return new DependentResourceFactory<ControllerConfiguration<?>>() {
|
||||||
|
@Override
|
||||||
|
public DependentResource createFrom(DependentResourceSpec spec,
|
||||||
|
ControllerConfiguration<?> configuration) {
|
||||||
|
final var dependentResourceClass = spec.getDependentResourceClass();
|
||||||
|
// workaround for https://github.com/operator-framework/java-operator-sdk/issues/2010
|
||||||
|
// create a fresh instance of the dependentresource
|
||||||
|
CDI.current().destroy(CDI.current().select(dependentResourceClass).get());
|
||||||
|
DependentResource instance = (DependentResource) CDI.current().select(dependentResourceClass).get();
|
||||||
|
var context = Utils.contextFor(configuration, dependentResourceClass, Dependent.class);
|
||||||
|
DependentResourceConfigurationResolver.configure(instance, spec, configuration);
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void createNamespace() {
|
private static void createNamespace() {
|
||||||
Log.info("Creating Namespace " + namespace);
|
Log.info("Creating Namespace " + namespace);
|
||||||
k8sclient.resource(new NamespaceBuilder().withNewMetadata().addToLabels("app","keycloak-test").withName(namespace).endMetadata().build()).create();
|
k8sclient.resource(new NamespaceBuilder().withNewMetadata().addToLabels("app","keycloak-test").withName(namespace).endMetadata().build()).create();
|
||||||
// ensure that the client defaults to the namespace - eventually most of the test code usage of inNamespace can be removed
|
|
||||||
k8sclient = k8sclient.adapt(NamespacedKubernetesClient.class).inNamespace(namespace);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void calculateNamespace() {
|
private static void calculateNamespace() {
|
||||||
|
@ -314,7 +344,7 @@ public class BaseOperatorTest implements QuarkusTestAfterEachCallback {
|
||||||
.filter(kc -> !Optional.ofNullable(kc.getStatus()).map(KeycloakStatus::isReady).orElse(false))
|
.filter(kc -> !Optional.ofNullable(kc.getStatus()).map(KeycloakStatus::isReady).orElse(false))
|
||||||
.forEach(kc -> {
|
.forEach(kc -> {
|
||||||
Log.warnf("Keycloak failed to become ready \"%s\" %s", kc.getMetadata().getName(), Serialization.asYaml(kc.getStatus()));
|
Log.warnf("Keycloak failed to become ready \"%s\" %s", kc.getMetadata().getName(), Serialization.asYaml(kc.getStatus()));
|
||||||
var statefulSet = k8sclient.apps().statefulSets().withName(KeycloakDeployment.getName(kc)).get();
|
var statefulSet = k8sclient.apps().statefulSets().withName(KeycloakDeploymentDependentResource.getName(kc)).get();
|
||||||
if (statefulSet != null) {
|
if (statefulSet != null) {
|
||||||
Log.warnf("Keycloak \"%s\" StatefulSet status %s", kc.getMetadata().getName(), Serialization.asYaml(statefulSet.getStatus()));
|
Log.warnf("Keycloak \"%s\" StatefulSet status %s", kc.getMetadata().getName(), Serialization.asYaml(statefulSet.getStatus()));
|
||||||
k8sclient.pods().withLabels(statefulSet.getSpec().getSelector().getMatchLabels()).list()
|
k8sclient.pods().withLabels(statefulSet.getSpec().getSelector().getMatchLabels()).list()
|
||||||
|
|
|
@ -251,7 +251,7 @@ public class KeycloakDeploymentTest extends BaseOperatorTest {
|
||||||
|
|
||||||
// managed changes
|
// managed changes
|
||||||
deployment.getSpec().getTemplate().getSpec().getContainers().get(0).setEnv(List.of(flandersEnvVar));
|
deployment.getSpec().getTemplate().getSpec().getContainers().get(0).setEnv(List.of(flandersEnvVar));
|
||||||
String originalLabelValue = deployment.getMetadata().getLabels().put(Constants.MANAGED_BY_LABEL, "not-right");
|
String originalAnnotationValue = deployment.getMetadata().getAnnotations().put(Constants.KEYCLOAK_WATCHING_ANNOTATION, "not-right");
|
||||||
|
|
||||||
deployment.getMetadata().setResourceVersion(null);
|
deployment.getMetadata().setResourceVersion(null);
|
||||||
k8sclient.resource(deployment).update();
|
k8sclient.resource(deployment).update();
|
||||||
|
@ -266,7 +266,7 @@ public class KeycloakDeploymentTest extends BaseOperatorTest {
|
||||||
assertThat(d.getMetadata().getLabels().entrySet().containsAll(labels.entrySet())).isTrue();
|
assertThat(d.getMetadata().getLabels().entrySet().containsAll(labels.entrySet())).isTrue();
|
||||||
// managed changes should get reverted
|
// managed changes should get reverted
|
||||||
assertThat(d.getSpec()).isEqualTo(expectedSpec); // specs should be reconciled expected merged state
|
assertThat(d.getSpec()).isEqualTo(expectedSpec); // specs should be reconciled expected merged state
|
||||||
assertThat(d.getMetadata().getLabels().get(Constants.MANAGED_BY_LABEL)).isEqualTo(originalLabelValue);
|
assertThat(d.getMetadata().getAnnotations().get(Constants.KEYCLOAK_WATCHING_ANNOTATION)).isEqualTo(originalAnnotationValue);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,10 +17,7 @@
|
||||||
|
|
||||||
package org.keycloak.operator.testsuite.unit;
|
package org.keycloak.operator.testsuite.unit;
|
||||||
|
|
||||||
import io.fabric8.kubernetes.api.model.Container;
|
|
||||||
import io.fabric8.kubernetes.api.model.EnvVar;
|
import io.fabric8.kubernetes.api.model.EnvVar;
|
||||||
import io.fabric8.kubernetes.api.model.apps.StatefulSet;
|
|
||||||
import io.fabric8.kubernetes.api.model.apps.StatefulSetBuilder;
|
|
||||||
import io.quarkus.test.junit.QuarkusTest;
|
import io.quarkus.test.junit.QuarkusTest;
|
||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
@ -39,7 +36,6 @@ import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.function.Consumer;
|
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
@ -51,19 +47,21 @@ import static org.keycloak.operator.testsuite.utils.CRAssert.assertKeycloakStatu
|
||||||
@QuarkusTest
|
@QuarkusTest
|
||||||
public class KeycloakDistConfiguratorTest {
|
public class KeycloakDistConfiguratorTest {
|
||||||
|
|
||||||
|
final KeycloakDistConfigurator distConfig = new KeycloakDistConfigurator();
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void enabledFeatures() {
|
public void enabledFeatures() {
|
||||||
testFirstClassCitizen(Map.of("features", "docker,authorization"), KeycloakDistConfigurator::configureFeatures);
|
testFirstClassCitizen(Map.of("features", "docker,authorization"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void disabledFeatures() {
|
public void disabledFeatures() {
|
||||||
testFirstClassCitizen(Map.of("features-disabled", "admin,step-up-authentication"), KeycloakDistConfigurator::configureFeatures);
|
testFirstClassCitizen(Map.of("features-disabled", "admin,step-up-authentication"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void transactions() {
|
public void transactions() {
|
||||||
testFirstClassCitizen(Map.of("transaction-xa-enabled", "false"), KeycloakDistConfigurator::configureTransactions);
|
testFirstClassCitizen(Map.of("transaction-xa-enabled", "false"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -76,17 +74,13 @@ public class KeycloakDistConfiguratorTest {
|
||||||
"https-certificate-key-file", Constants.CERTIFICATES_FOLDER + "/tls.key"
|
"https-certificate-key-file", Constants.CERTIFICATES_FOLDER + "/tls.key"
|
||||||
);
|
);
|
||||||
|
|
||||||
testFirstClassCitizen(expectedValues, KeycloakDistConfigurator::configureHttp);
|
testFirstClassCitizen(expectedValues);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void featuresEmptyLists() {
|
public void featuresEmptyLists() {
|
||||||
final Keycloak keycloak = K8sUtils.getResourceFromFile("test-serialization-keycloak-cr-with-empty-list.yml", Keycloak.class);
|
final Keycloak keycloak = K8sUtils.getResourceFromFile("test-serialization-keycloak-cr-with-empty-list.yml", Keycloak.class);
|
||||||
final StatefulSet deployment = getBasicKcDeployment();
|
var envVars = distConfig.configureDistOptions(keycloak);
|
||||||
final KeycloakDistConfigurator distConfig = new KeycloakDistConfigurator(keycloak, deployment, null);
|
|
||||||
|
|
||||||
final List<EnvVar> envVars = deployment.getSpec().getTemplate().getSpec().getContainers().get(0).getEnv();
|
|
||||||
distConfig.configureFeatures();
|
|
||||||
assertEnvVarNotPresent(envVars, "KC_FEATURES");
|
assertEnvVarNotPresent(envVars, "KC_FEATURES");
|
||||||
assertEnvVarNotPresent(envVars, "KC_FEATURES_DISABLED");
|
assertEnvVarNotPresent(envVars, "KC_FEATURES_DISABLED");
|
||||||
}
|
}
|
||||||
|
@ -107,7 +101,7 @@ public class KeycloakDistConfiguratorTest {
|
||||||
));
|
));
|
||||||
expectedValues.put("db-url", "url");
|
expectedValues.put("db-url", "url");
|
||||||
|
|
||||||
testFirstClassCitizen(expectedValues, KeycloakDistConfigurator::configureDatabase);
|
testFirstClassCitizen(expectedValues);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -120,18 +114,14 @@ public class KeycloakDistConfiguratorTest {
|
||||||
"hostname-admin", "my-admin-hostname"
|
"hostname-admin", "my-admin-hostname"
|
||||||
);
|
);
|
||||||
|
|
||||||
testFirstClassCitizen(expectedValues, KeycloakDistConfigurator::configureHostname);
|
testFirstClassCitizen(expectedValues);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void missingHostname() {
|
public void missingHostname() {
|
||||||
final Keycloak keycloak = K8sUtils.getResourceFromFile("test-serialization-keycloak-cr-with-empty-list.yml", Keycloak.class);
|
final Keycloak keycloak = K8sUtils.getResourceFromFile("test-serialization-keycloak-cr-with-empty-list.yml", Keycloak.class);
|
||||||
final StatefulSet deployment = getBasicKcDeployment();
|
|
||||||
final KeycloakDistConfigurator distConfig = new KeycloakDistConfigurator(keycloak, deployment, null);
|
|
||||||
|
|
||||||
final List<EnvVar> envVars = deployment.getSpec().getTemplate().getSpec().getContainers().get(0).getEnv();
|
var envVars = distConfig.configureDistOptions(keycloak);
|
||||||
|
|
||||||
distConfig.configureHostname();
|
|
||||||
|
|
||||||
assertEnvVarNotPresent(envVars, "KC_HOSTNAME");
|
assertEnvVarNotPresent(envVars, "KC_HOSTNAME");
|
||||||
assertEnvVarNotPresent(envVars, "KC_HOSTNAME_ADMIN");
|
assertEnvVarNotPresent(envVars, "KC_HOSTNAME_ADMIN");
|
||||||
|
@ -142,17 +132,12 @@ public class KeycloakDistConfiguratorTest {
|
||||||
|
|
||||||
/* UTILS */
|
/* UTILS */
|
||||||
|
|
||||||
private void testFirstClassCitizen(Map<String, String> expectedValues, Consumer<KeycloakDistConfigurator> config) {
|
private void testFirstClassCitizen(Map<String, String> expectedValues) {
|
||||||
testFirstClassCitizen("/test-serialization-keycloak-cr.yml", expectedValues, config);
|
testFirstClassCitizen("/test-serialization-keycloak-cr.yml", expectedValues);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void testFirstClassCitizen(String crName, Map<String, String> expectedValues, Consumer<KeycloakDistConfigurator> config) {
|
private void testFirstClassCitizen(String crName, Map<String, String> expectedValues) {
|
||||||
final Keycloak keycloak = K8sUtils.getResourceFromFile(crName, Keycloak.class);
|
final Keycloak keycloak = K8sUtils.getResourceFromFile(crName, Keycloak.class);
|
||||||
final StatefulSet deployment = getBasicKcDeployment();
|
|
||||||
final KeycloakDistConfigurator distConfig = new KeycloakDistConfigurator(keycloak, deployment, null);
|
|
||||||
|
|
||||||
final Container container = deployment.getSpec().getTemplate().getSpec().getContainers().get(0);
|
|
||||||
assertThat(container).isNotNull();
|
|
||||||
|
|
||||||
final List<ValueOrSecret> serverConfig = expectedValues.keySet()
|
final List<ValueOrSecret> serverConfig = expectedValues.keySet()
|
||||||
.stream()
|
.stream()
|
||||||
|
@ -163,16 +148,10 @@ public class KeycloakDistConfiguratorTest {
|
||||||
|
|
||||||
final var expectedFields = expectedValues.keySet();
|
final var expectedFields = expectedValues.keySet();
|
||||||
|
|
||||||
assertWarningStatusFirstClassFields(distConfig, false, expectedFields);
|
assertWarningStatusFirstClassFields(keycloak, distConfig, true, expectedFields);
|
||||||
expectedValues.forEach((k, v) -> assertEnvVarNotPresent(container.getEnv(), getKeycloakOptionEnvVarName(k)));
|
|
||||||
|
|
||||||
// mimic what KeycloakDeployment does and set all additionalOptions as env first
|
var envVars = distConfig.configureDistOptions(keycloak);
|
||||||
expectedValues.forEach((k, v) -> container.getEnv().add(new EnvVar(getKeycloakOptionEnvVarName(k), v, null)));
|
expectedValues.forEach((k, v) -> assertContainerEnvVar(envVars, getKeycloakOptionEnvVarName(k), v));
|
||||||
|
|
||||||
config.accept(distConfig);
|
|
||||||
|
|
||||||
assertWarningStatusFirstClassFields(distConfig, true, expectedFields);
|
|
||||||
expectedValues.forEach((k, v) -> assertContainerEnvVar(container.getEnv(), getKeycloakOptionEnvVarName(k), v));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -210,10 +189,10 @@ public class KeycloakDistConfiguratorTest {
|
||||||
assertThat(containsEnvironmentVariable(envVars, varName)).isFalse();
|
assertThat(containsEnvironmentVariable(envVars, varName)).isFalse();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void assertWarningStatusFirstClassFields(KeycloakDistConfigurator distConfig, boolean expectWarning, Collection<String> firstClassFields) {
|
private void assertWarningStatusFirstClassFields(Keycloak keycloak, KeycloakDistConfigurator distConfig, boolean expectWarning, Collection<String> firstClassFields) {
|
||||||
final String message = "warning: You need to specify these fields as the first-class citizen of the CR: ";
|
final String message = "warning: You need to specify these fields as the first-class citizen of the CR: ";
|
||||||
final KeycloakStatusAggregator statusBuilder = new KeycloakStatusAggregator(1L);
|
final KeycloakStatusAggregator statusBuilder = new KeycloakStatusAggregator(1L);
|
||||||
distConfig.validateOptions(statusBuilder);
|
distConfig.validateOptions(keycloak, statusBuilder);
|
||||||
final KeycloakStatus status = statusBuilder.build();
|
final KeycloakStatus status = statusBuilder.build();
|
||||||
|
|
||||||
if (expectWarning) {
|
if (expectWarning) {
|
||||||
|
@ -240,21 +219,6 @@ public class KeycloakDistConfiguratorTest {
|
||||||
.map(KeycloakStatusCondition::getMessage);
|
.map(KeycloakStatusCondition::getMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
private StatefulSet getBasicKcDeployment() {
|
|
||||||
return new StatefulSetBuilder()
|
|
||||||
.withNewSpec()
|
|
||||||
.withNewTemplate()
|
|
||||||
.withNewSpec()
|
|
||||||
.addNewContainer()
|
|
||||||
.withName("keycloak")
|
|
||||||
.withArgs("start")
|
|
||||||
.endContainer()
|
|
||||||
.endSpec()
|
|
||||||
.endTemplate()
|
|
||||||
.endSpec()
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean containsEnvironmentVariable(List<EnvVar> envVars, String varName) {
|
private boolean containsEnvironmentVariable(List<EnvVar> envVars, String varName) {
|
||||||
if (CollectionUtil.isEmpty(envVars) || isBlank(varName)) return false;
|
if (CollectionUtil.isEmpty(envVars) || isBlank(varName)) return false;
|
||||||
return envVars.stream().anyMatch(f -> varName.equals(f.getName()));
|
return envVars.stream().anyMatch(f -> varName.equals(f.getName()));
|
||||||
|
|
|
@ -25,57 +25,48 @@ import io.fabric8.kubernetes.api.model.PodTemplateSpecBuilder;
|
||||||
import io.fabric8.kubernetes.api.model.ProbeBuilder;
|
import io.fabric8.kubernetes.api.model.ProbeBuilder;
|
||||||
import io.fabric8.kubernetes.api.model.apps.StatefulSet;
|
import io.fabric8.kubernetes.api.model.apps.StatefulSet;
|
||||||
import io.fabric8.kubernetes.api.model.apps.StatefulSetBuilder;
|
import io.fabric8.kubernetes.api.model.apps.StatefulSetBuilder;
|
||||||
|
import io.javaoperatorsdk.operator.api.reconciler.Context;
|
||||||
|
import io.quarkus.test.InjectMock;
|
||||||
import io.quarkus.test.junit.QuarkusTest;
|
import io.quarkus.test.junit.QuarkusTest;
|
||||||
|
|
||||||
import org.junit.jupiter.api.Assertions;
|
import org.junit.jupiter.api.Assertions;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.keycloak.operator.Config;
|
import org.keycloak.operator.Utils;
|
||||||
import org.keycloak.operator.controllers.KeycloakDeployment;
|
import org.keycloak.operator.controllers.KeycloakDeploymentDependentResource;
|
||||||
import org.keycloak.operator.controllers.OperatorManagedResource;
|
import org.keycloak.operator.controllers.WatchedSecretsController;
|
||||||
|
import org.keycloak.operator.crds.v2alpha1.deployment.Keycloak;
|
||||||
import org.keycloak.operator.crds.v2alpha1.deployment.KeycloakBuilder;
|
import org.keycloak.operator.crds.v2alpha1.deployment.KeycloakBuilder;
|
||||||
import org.keycloak.operator.crds.v2alpha1.deployment.KeycloakSpecBuilder;
|
import org.keycloak.operator.crds.v2alpha1.deployment.KeycloakSpecBuilder;
|
||||||
import org.keycloak.operator.crds.v2alpha1.deployment.ValueOrSecret;
|
import org.keycloak.operator.crds.v2alpha1.deployment.ValueOrSecret;
|
||||||
import org.keycloak.operator.crds.v2alpha1.deployment.spec.HostnameSpecBuilder;
|
import org.keycloak.operator.crds.v2alpha1.deployment.spec.HostnameSpecBuilder;
|
||||||
import org.keycloak.operator.crds.v2alpha1.deployment.spec.HttpSpecBuilder;
|
import org.keycloak.operator.crds.v2alpha1.deployment.spec.HttpSpecBuilder;
|
||||||
import org.keycloak.operator.crds.v2alpha1.deployment.spec.UnsupportedSpec;
|
import org.keycloak.operator.crds.v2alpha1.deployment.spec.UnsupportedSpec;
|
||||||
|
import org.mockito.Mockito;
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Optional;
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import jakarta.inject.Inject;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
@QuarkusTest
|
@QuarkusTest
|
||||||
public class PodTemplateTest {
|
public class PodTemplateTest {
|
||||||
private StatefulSet getDeployment(PodTemplateSpec podTemplate, StatefulSet existingDeployment, Consumer<KeycloakSpecBuilder> additionalSpec) {
|
|
||||||
var config = new Config() {
|
|
||||||
@Override
|
|
||||||
public Keycloak keycloak() {
|
|
||||||
return new Keycloak() {
|
|
||||||
@Override
|
|
||||||
public String image() {
|
|
||||||
return "dummy-image";
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@InjectMock
|
||||||
public String imagePullPolicy() {
|
WatchedSecretsController watchedSecrets;
|
||||||
return "Never";
|
|
||||||
}
|
@Inject
|
||||||
@Override
|
KeycloakDeploymentDependentResource deployment;
|
||||||
public Map<String, String> podLabels() {
|
|
||||||
return Collections.emptyMap();
|
private StatefulSet getDeployment(PodTemplateSpec podTemplate, StatefulSet existingDeployment, Consumer<KeycloakSpecBuilder> additionalSpec) {
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
var kc = new KeycloakBuilder().withNewMetadata().withName("instance").endMetadata().build();
|
var kc = new KeycloakBuilder().withNewMetadata().withName("instance").endMetadata().build();
|
||||||
existingDeployment = new StatefulSetBuilder(existingDeployment).editOrNewSpec().editOrNewSelector()
|
existingDeployment = new StatefulSetBuilder(existingDeployment).editOrNewSpec().editOrNewSelector()
|
||||||
.addToMatchLabels(OperatorManagedResource.updateWithInstanceLabels(null, kc.getMetadata().getName()))
|
.withMatchLabels(Utils.allInstanceLabels(kc))
|
||||||
.endSelector().endSpec().build();
|
.endSelector().endSpec().build();
|
||||||
|
|
||||||
var httpSpec = new HttpSpecBuilder().withTlsSecret("example-tls-secret").build();
|
var httpSpec = new HttpSpecBuilder().withTlsSecret("example-tls-secret").build();
|
||||||
|
@ -92,9 +83,10 @@ public class PodTemplateTest {
|
||||||
|
|
||||||
kc.setSpec(keycloakSpecBuilder.build());
|
kc.setSpec(keycloakSpecBuilder.build());
|
||||||
|
|
||||||
var deployment = new KeycloakDeployment(null, config, kc, existingDeployment, "dummy-admin");
|
Context<Keycloak> context = Mockito.mock(Context.class);
|
||||||
|
Mockito.when(context.getSecondaryResource(StatefulSet.class)).thenReturn(Optional.ofNullable(existingDeployment));
|
||||||
|
|
||||||
return deployment.getReconciledResource().get();
|
return deployment.desired(kc, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
private StatefulSet getDeployment(PodTemplateSpec podTemplate, StatefulSet existingDeployment) {
|
private StatefulSet getDeployment(PodTemplateSpec podTemplate, StatefulSet existingDeployment) {
|
||||||
|
|
Loading…
Reference in a new issue