Upgrade to Quarkus 3.18.2 (#37300)

* Upgrade to Quarkus 3.18.2

Closes #37056

Signed-off-by: Václav Muzikář <vmuzikar@redhat.com>

* Update docs/documentation/upgrading/topics/changes/changes-26_2_0.adoc

Co-authored-by: Alexander Schwartz <alexander.schwartz@gmx.net>
Signed-off-by: Václav Muzikář <vaclav@muzikari.cz>

---------

Signed-off-by: Václav Muzikář <vmuzikar@redhat.com>
Signed-off-by: Václav Muzikář <vaclav@muzikari.cz>
Co-authored-by: Alexander Schwartz <alexander.schwartz@gmx.net>
This commit is contained in:
Václav Muzikář 2025-02-17 16:30:05 +01:00 committed by GitHub
parent 2b74e6148e
commit 764ca50fc4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
31 changed files with 273 additions and 255 deletions

View File

@ -42,4 +42,5 @@ https://saml.xml.org*
# To be removed once KC 26.1 has been released
https://www.keycloak.org/server/logging#_configuring_levels_as_individual_options
https://www.keycloak.org/observability/*
https://www.keycloak.org/high-availability/concepts-memory-and-cpu-sizing#_measuring_the_activity_of_a_running_keycloak_instance
https://www.keycloak.org/high-availability/concepts-memory-and-cpu-sizing#_measuring_the_activity_of_a_running_keycloak_instance
http://example.com:8080

View File

@ -0,0 +1,15 @@
== Breaking changes
Breaking changes are identified as requiring changes from existing users to their configurations.
=== Changes to port behaviour with the `X-Forwarded-Host` header
The `X-Forwarded-Host` header can optionally also contain the port. In previous versions when the port was omitted from the header,
{project_name} fell back to the actual request port. For example if {project_name} was listening on port 8080 and the request contained
`X-Forwarded-Host: example.com` header, the resolved URL was `+http://example.com:8080+`.
This is now changed and omitting the port results in removing it from the resolved URL. The resolved URL from the previous example
would now be `http://example.com`.
To mitigate that, either make your reverse proxy include the port in the `X-Forwarded-Host` header or configure it to set
the `X-Forwarded-Port` header with the desired port.

View File

@ -1,6 +1,10 @@
[[migration-changes]]
== Migration Changes
=== Migrating to 26.2.0
include::changes-26_2_0.adoc[leveloffset=2]
=== Migrating to 26.1.3
include::changes-26_1_3.adoc[leveloffset=2]

View File

@ -17,44 +17,72 @@
package org.keycloak.operator;
import java.util.Optional;
import io.fabric8.kubernetes.api.model.apps.StatefulSet;
import io.javaoperatorsdk.operator.api.reconciler.Context;
import org.keycloak.operator.crds.v2alpha1.deployment.Keycloak;
import org.keycloak.operator.controllers.KeycloakDistConfigurator;
import org.keycloak.operator.controllers.WatchedResources;
import org.keycloak.operator.upgrade.UpgradeType;
import java.util.Optional;
public final class ContextUtils {
// context keys
private static final String OLD_DEPLOYMENT_KEY = "current_stateful_set";
private static final String NEW_DEPLOYMENT_KEY = "desired_new_stateful_set";
private static final String UPGRADE_TYPE_KEY = "upgrade_type";
public static final String OLD_DEPLOYMENT_KEY = "current_stateful_set";
public static final String NEW_DEPLOYMENT_KEY = "desired_new_stateful_set";
public static final String UPGRADE_TYPE_KEY = "upgrade_type";
public static final String OPERATOR_CONFIG_KEY = "operator_config";
public static final String WATCHED_RESOURCES_KEY = "watched_resources";
public static final String DIST_CONFIGURATOR_KEY = "dist_configurator";
private ContextUtils() {}
public static void storeCurrentStatefulSet(Context<Keycloak> context, StatefulSet statefulSet) {
context.managedDependentResourceContext().put(OLD_DEPLOYMENT_KEY, statefulSet);
public static void storeCurrentStatefulSet(Context<?> context, StatefulSet statefulSet) {
context.managedWorkflowAndDependentResourceContext().put(OLD_DEPLOYMENT_KEY, statefulSet);
}
public static StatefulSet getCurrentStatefulSet(Context<Keycloak> context) {
return context.managedDependentResourceContext().getMandatory(OLD_DEPLOYMENT_KEY, StatefulSet.class);
public static Optional<StatefulSet> getCurrentStatefulSet(Context<?> context) {
return context.managedWorkflowAndDependentResourceContext().get(OLD_DEPLOYMENT_KEY, StatefulSet.class);
}
public static void storeDesiredStatefulSet(Context<Keycloak> context, StatefulSet statefulSet) {
context.managedDependentResourceContext().put(NEW_DEPLOYMENT_KEY, statefulSet);
public static void storeDesiredStatefulSet(Context<?> context, StatefulSet statefulSet) {
context.managedWorkflowAndDependentResourceContext().put(NEW_DEPLOYMENT_KEY, statefulSet);
}
public static StatefulSet getDesiredStatefulSet(Context<Keycloak> context) {
return context.managedDependentResourceContext().getMandatory(NEW_DEPLOYMENT_KEY, StatefulSet.class);
public static StatefulSet getDesiredStatefulSet(Context<?> context) {
return context.managedWorkflowAndDependentResourceContext().getMandatory(NEW_DEPLOYMENT_KEY, StatefulSet.class);
}
public static void storeUpgradeType(Context<Keycloak> context, UpgradeType upgradeType) {
context.managedDependentResourceContext().put(UPGRADE_TYPE_KEY, upgradeType);
public static void storeUpgradeType(Context<?> context, UpgradeType upgradeType) {
context.managedWorkflowAndDependentResourceContext().put(UPGRADE_TYPE_KEY, upgradeType);
}
public static Optional<UpgradeType> getUpgradeType(Context<Keycloak> context) {
return context.managedDependentResourceContext().get(UPGRADE_TYPE_KEY, UpgradeType.class);
public static Optional<UpgradeType> getUpgradeType(Context<?> context) {
return context.managedWorkflowAndDependentResourceContext().get(UPGRADE_TYPE_KEY, UpgradeType.class);
}
public static void storeOperatorConfig(Context<?> context, Config operatorConfig) {
context.managedWorkflowAndDependentResourceContext().put(OPERATOR_CONFIG_KEY, operatorConfig);
}
public static Config getOperatorConfig(Context<?> context) {
return context.managedWorkflowAndDependentResourceContext().getMandatory(OPERATOR_CONFIG_KEY, Config.class);
}
public static void storeWatchedResources(Context<?> context, WatchedResources watchedResources) {
context.managedWorkflowAndDependentResourceContext().put(WATCHED_RESOURCES_KEY, watchedResources);
}
public static WatchedResources getWatchedResources(Context<?> context) {
return context.managedWorkflowAndDependentResourceContext().getMandatory(WATCHED_RESOURCES_KEY, WatchedResources.class);
}
public static void storeDistConfigurator(Context<?> context, KeycloakDistConfigurator distConfigurator) {
context.managedWorkflowAndDependentResourceContext().put(DIST_CONFIGURATOR_KEY, distConfigurator);
}
public static KeycloakDistConfigurator getDistConfigurator(Context<?> context) {
return context.managedWorkflowAndDependentResourceContext().getMandatory(DIST_CONFIGURATOR_KEY, KeycloakDistConfigurator.class);
}
}

View File

@ -80,13 +80,6 @@ public final class Utils {
return labels;
}
public static <T extends HasMetadata> Optional<T> getByName(Class<T> clazz, Function<Keycloak, String> nameFunction, Keycloak primary, Context<Keycloak> context) {
InformerEventSource<T, Keycloak> ies = (InformerEventSource<T, Keycloak>) context
.eventSourceRetriever().getResourceEventSourceFor(clazz);
return ies.get(new ResourceID(nameFunction.apply(primary), primary.getMetadata().getNamespace()));
}
/**
* Set resources requests/limits for Keycloak container
* </p>

View File

@ -3,8 +3,8 @@ package org.keycloak.operator.controllers;
import io.fabric8.kubernetes.api.model.Secret;
import io.fabric8.kubernetes.api.model.SecretBuilder;
import io.fabric8.kubernetes.client.utils.KubernetesResourceUtil;
import io.javaoperatorsdk.operator.api.config.informer.Informer;
import io.javaoperatorsdk.operator.api.reconciler.Context;
import io.javaoperatorsdk.operator.api.reconciler.ResourceDiscriminator;
import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource;
import io.javaoperatorsdk.operator.api.reconciler.dependent.GarbageCollected;
import io.javaoperatorsdk.operator.processing.dependent.Creator;
@ -20,22 +20,16 @@ import org.keycloak.operator.crds.v2alpha1.deployment.spec.BootstrapAdminSpec;
import java.util.Optional;
import java.util.UUID;
@KubernetesDependent(labelSelector = Constants.DEFAULT_LABELS_AS_STRING, resourceDiscriminator = KeycloakAdminSecretDependentResource.NameResourceDiscriminator.class)
@KubernetesDependent(
informer = @Informer(labelSelector = Constants.DEFAULT_LABELS_AS_STRING)
)
public class KeycloakAdminSecretDependentResource extends KubernetesDependentResource<Secret, Keycloak> implements Creator<Secret, Keycloak>, GarbageCollected<Keycloak> {
public static class EnabledCondition implements Condition<Secret, Keycloak> {
@Override
public boolean isMet(DependentResource<Secret, Keycloak> dependentResource, Keycloak primary,
Context<Keycloak> context) {
return Optional.ofNullable(primary.getSpec().getBootstrapAdminSpec()).map(BootstrapAdminSpec::getUser)
.map(BootstrapAdminSpec.User::getSecret).filter(s -> !s.equals(KeycloakAdminSecretDependentResource.getName(primary))).isEmpty();
}
}
public static class NameResourceDiscriminator implements ResourceDiscriminator<Secret, Keycloak> {
@Override
public Optional<Secret> distinguish(Class<Secret> resource, Keycloak primary, Context<Keycloak> context) {
return Utils.getByName(Secret.class, KeycloakAdminSecretDependentResource::getName, primary, context);
return !hasCustomAdminSecret(primary);
}
}
@ -62,4 +56,9 @@ public class KeycloakAdminSecretDependentResource extends KubernetesDependentRes
return KubernetesResourceUtil.sanitizeName(keycloak.getMetadata().getName() + "-initial-admin");
}
public static boolean hasCustomAdminSecret(Keycloak keycloak) {
return Optional.ofNullable(keycloak.getSpec().getBootstrapAdminSpec()).map(BootstrapAdminSpec::getUser)
.map(BootstrapAdminSpec.User::getSecret).filter(s -> !s.equals(KeycloakAdminSecretDependentResource.getName(keycloak))).isPresent();
}
}

View File

@ -21,55 +21,50 @@ import io.fabric8.kubernetes.api.model.ContainerStateWaiting;
import io.fabric8.kubernetes.api.model.ContainerStatus;
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.apps.StatefulSet;
import io.fabric8.kubernetes.client.readiness.Readiness;
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.reconciler.Context;
import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration;
import io.javaoperatorsdk.operator.api.reconciler.ErrorStatusHandler;
import io.javaoperatorsdk.operator.api.reconciler.ErrorStatusUpdateControl;
import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext;
import io.javaoperatorsdk.operator.api.reconciler.EventSourceInitializer;
import io.javaoperatorsdk.operator.api.reconciler.EventSourceUtils;
import io.javaoperatorsdk.operator.api.reconciler.Reconciler;
import io.javaoperatorsdk.operator.api.reconciler.UpdateControl;
import io.javaoperatorsdk.operator.api.reconciler.Workflow;
import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent;
import io.javaoperatorsdk.operator.processing.event.source.EventSource;
import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource;
import io.javaoperatorsdk.operator.processing.event.source.informer.Mappers;
import io.quarkus.logging.Log;
import jakarta.inject.Inject;
import org.keycloak.common.util.CollectionUtil;
import org.keycloak.operator.Config;
import org.keycloak.operator.Constants;
import org.keycloak.operator.ContextUtils;
import org.keycloak.operator.Utils;
import org.keycloak.operator.crds.v2alpha1.deployment.Keycloak;
import org.keycloak.operator.crds.v2alpha1.deployment.KeycloakBuilder;
import org.keycloak.operator.crds.v2alpha1.deployment.KeycloakStatus;
import org.keycloak.operator.crds.v2alpha1.deployment.KeycloakStatusAggregator;
import org.keycloak.operator.crds.v2alpha1.deployment.spec.HostnameSpec;
import org.keycloak.operator.crds.v2alpha1.deployment.spec.HostnameSpecBuilder;
import org.keycloak.operator.upgrade.UpgradeLogicFactory;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import jakarta.inject.Inject;
import org.keycloak.operator.upgrade.UpgradeLogicFactory;
@ControllerConfiguration(
@Workflow(
explicitInvocation = true,
dependents = {
@Dependent(type = KeycloakDeploymentDependentResource.class, reconcilePrecondition = KeycloakDeploymentDependentResource.ReconcilePrecondition.class),
@Dependent(type = KeycloakAdminSecretDependentResource.class, reconcilePrecondition = KeycloakAdminSecretDependentResource.EnabledCondition.class),
@Dependent(type = KeycloakIngressDependentResource.class, reconcilePrecondition = KeycloakIngressDependentResource.EnabledCondition.class),
@Dependent(type = KeycloakServiceDependentResource.class, useEventSourceWithName = "serviceSource"),
@Dependent(type = KeycloakDiscoveryServiceDependentResource.class, useEventSourceWithName = "serviceSource"),
@Dependent(type = KeycloakServiceDependentResource.class),
@Dependent(type = KeycloakDiscoveryServiceDependentResource.class),
@Dependent(type = KeycloakNetworkPolicyDependentResource.class, reconcilePrecondition = KeycloakNetworkPolicyDependentResource.EnabledCondition.class)
})
public class KeycloakController implements Reconciler<Keycloak>, EventSourceInitializer<Keycloak>, ErrorStatusHandler<Keycloak> {
public class KeycloakController implements Reconciler<Keycloak> {
public static final String OPENSHIFT_DEFAULT = "openshift-default";
@ -85,32 +80,12 @@ public class KeycloakController implements Reconciler<Keycloak>, EventSourceInit
@Inject
UpgradeLogicFactory upgradeLogicFactory;
volatile KeycloakDeploymentDependentResource deploymentDependentResource;
volatile KeycloakUpdateJobDependentResource updateJobDependentResource;
@Inject
KeycloakUpdateJobDependentResource updateJobDependentResource;
@Override
public Map<String, EventSource> prepareEventSources(EventSourceContext<Keycloak> context) {
var namespaces = context.getControllerConfiguration().getNamespaces();
InformerConfiguration<Service> servicesIC = InformerConfiguration
.from(Service.class)
.withLabelSelector(Constants.DEFAULT_LABELS_AS_STRING)
.withNamespaces(namespaces)
.withSecondaryToPrimaryMapper(Mappers.fromOwnerReference())
.build();
EventSource servicesEvent = new InformerEventSource<>(servicesIC, context);
Map<String, EventSource> sources = new HashMap<>();
sources.put("serviceSource", servicesEvent);
this.deploymentDependentResource = new KeycloakDeploymentDependentResource(config, watchedResources, distConfigurator);
sources.putAll(EventSourceInitializer.nameEventSourcesFromDependentResource(context, this.deploymentDependentResource));
updateJobDependentResource = new KeycloakUpdateJobDependentResource(config);
sources.putAll(EventSourceInitializer.nameEventSourcesFromDependentResource(context, updateJobDependentResource));
return sources;
public List<EventSource<?, Keycloak>> prepareEventSources(EventSourceContext<Keycloak> context) {
return EventSourceUtils.dependentEventSources(context, updateJobDependentResource);
}
@Override
@ -141,10 +116,25 @@ public class KeycloakController implements Reconciler<Keycloak>, EventSourceInit
}
if (modifiedSpec) {
return UpdateControl.updateResource(kc);
// just patch spec using SSA, nothing more
Keycloak patchedKc = new KeycloakBuilder()
.withNewMetadata()
.withName(kc.getMetadata().getName())
.withNamespace(kc.getMetadata().getNamespace())
.endMetadata()
.withSpec(kc.getSpec())
.build();
return UpdateControl.patchResource(patchedKc);
}
var upgradeLogicControl = upgradeLogicFactory.create(kc, context, deploymentDependentResource, updateJobDependentResource)
var existingDeployment = context.getSecondaryResource(StatefulSet.class).orElse(null);
ContextUtils.storeOperatorConfig(context, config);
ContextUtils.storeWatchedResources(context, watchedResources);
ContextUtils.storeDistConfigurator(context, distConfigurator);
ContextUtils.storeCurrentStatefulSet(context, existingDeployment);
ContextUtils.storeDesiredStatefulSet(context, new KeycloakDeploymentDependentResource().desired(kc, context));
var upgradeLogicControl = upgradeLogicFactory.create(kc, context)
.decideUpgrade();
if (upgradeLogicControl.isPresent()) {
Log.debug("--- Reconciliation interrupted due to upgrade logic");
@ -152,11 +142,11 @@ public class KeycloakController implements Reconciler<Keycloak>, EventSourceInit
}
// after the spec has possibly been updated, reconcile the StatefulSet
this.deploymentDependentResource.reconcile(kc, context);
context.managedWorkflowAndDependentResourceContext().reconcileManagedWorkflow();
var statusAggregator = new KeycloakStatusAggregator(kc.getStatus(), kc.getMetadata().getGeneration());
updateStatus(kc, context.getSecondaryResource(StatefulSet.class).orElse(null), statusAggregator, context);
updateStatus(kc, existingDeployment, statusAggregator, context);
var status = statusAggregator.build();
Log.debug("--- Reconciliation finished successfully");
@ -167,7 +157,7 @@ public class KeycloakController implements Reconciler<Keycloak>, EventSourceInit
}
else {
kc.setStatus(status);
updateControl = UpdateControl.updateStatus(kc);
updateControl = UpdateControl.patchStatus(kc);
}
var statefulSet = context.getSecondaryResource(StatefulSet.class);
@ -190,7 +180,7 @@ public class KeycloakController implements Reconciler<Keycloak>, EventSourceInit
kc.setStatus(status);
return ErrorStatusUpdateControl.updateStatus(kc);
return ErrorStatusUpdateControl.patchStatus(kc);
}
public static Optional<String> generateOpenshiftHostname(Keycloak keycloak, Context<Keycloak> context) {

View File

@ -31,11 +31,13 @@ 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.client.KubernetesClient;
import io.javaoperatorsdk.operator.api.config.informer.Informer;
import io.javaoperatorsdk.operator.api.reconciler.Context;
import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource;
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource;
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResourceConfigBuilder;
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent;
import io.javaoperatorsdk.operator.processing.dependent.workflow.Condition;
import io.quarkus.logging.Log;
import org.keycloak.operator.Config;
import org.keycloak.operator.Constants;
import org.keycloak.operator.ContextUtils;
@ -72,6 +74,9 @@ import static org.keycloak.operator.controllers.KeycloakDistConfigurator.getKeyc
import static org.keycloak.operator.crds.v2alpha1.CRDUtils.isTlsConfigured;
import static org.keycloak.operator.crds.v2alpha1.deployment.spec.TracingSpec.convertTracingAttributesToString;
@KubernetesDependent(
informer = @Informer(labelSelector = Constants.DEFAULT_LABELS_AS_STRING)
)
public class KeycloakDeploymentDependentResource extends CRUDKubernetesDependentResource<StatefulSet, Keycloak> {
public static final String POD_IP = "POD_IP";
@ -95,23 +100,23 @@ public class KeycloakDeploymentDependentResource extends CRUDKubernetesDependent
public static final String OPTIMIZED_ARG = "--optimized";
Config operatorConfig;
WatchedResources watchedResources;
KeycloakDistConfigurator distConfigurator;
private boolean useServiceCaCrt;
public KeycloakDeploymentDependentResource(Config operatorConfig, WatchedResources watchedResources, KeycloakDistConfigurator distConfigurator) {
// Do not create the deployment before the initial admin secret is created to prevent the deployment from restarting.
// Not using native dependsOn as the initial admin secret may not be created by the operator and might be provided by the user,
// in which case we want to create the deployment immediately.
public static class ReconcilePrecondition implements Condition<StatefulSet, Keycloak> {
@Override
public boolean isMet(DependentResource<StatefulSet, Keycloak> dependentResource, Keycloak primary, Context<Keycloak> context) {
return KeycloakAdminSecretDependentResource.hasCustomAdminSecret(primary)
|| context.getSecondaryResourcesAsStream(Secret.class)
.anyMatch(s -> s.getMetadata().getName().equals(KeycloakAdminSecretDependentResource.getName(primary)));
}
}
public KeycloakDeploymentDependentResource() {
super(StatefulSet.class);
this.operatorConfig = operatorConfig;
this.watchedResources = watchedResources;
this.distConfigurator = distConfigurator;
useServiceCaCrt = Files.exists(Path.of(SERVICE_CA_CRT));
this.configureWith(new KubernetesDependentResourceConfigBuilder<StatefulSet>()
.withLabelSelector(Constants.DEFAULT_LABELS_AS_STRING)
.build());
}
public void setUseServiceCaCrt(boolean useServiceCaCrt) {
@ -120,17 +125,20 @@ public class KeycloakDeploymentDependentResource extends CRUDKubernetesDependent
@Override
public StatefulSet desired(Keycloak primary, Context<Keycloak> context) {
StatefulSet baseDeployment = createBaseDeployment(primary, context);
Config operatorConfig = ContextUtils.getOperatorConfig(context);
WatchedResources watchedResources = ContextUtils.getWatchedResources(context);
StatefulSet baseDeployment = createBaseDeployment(primary, context, operatorConfig);
TreeSet<String> allSecrets = new TreeSet<>();
if (isTlsConfigured(primary)) {
configureTLS(primary, baseDeployment, allSecrets);
}
Container kcContainer = baseDeployment.getSpec().getTemplate().getSpec().getContainers().get(0);
addTruststores(primary, baseDeployment, kcContainer, allSecrets);
addEnvVars(baseDeployment, primary, allSecrets);
addEnvVars(baseDeployment, primary, allSecrets, context);
addResources(primary.getSpec().getResourceRequirements(), operatorConfig, kcContainer);
Optional.ofNullable(primary.getSpec().getCacheSpec())
.ifPresent(c -> configureCache(baseDeployment, kcContainer, c, context.getClient()));
.ifPresent(c -> configureCache(baseDeployment, kcContainer, c, context.getClient(), watchedResources));
if (!allSecrets.isEmpty()) {
watchedResources.annotateDeployment(new ArrayList<>(allSecrets), Secret.class, baseDeployment, context.getClient());
@ -142,7 +150,7 @@ public class KeycloakDeploymentDependentResource extends CRUDKubernetesDependent
return baseDeployment;
}
var existingDeployment = ContextUtils.getCurrentStatefulSet(context);
var existingDeployment = ContextUtils.getCurrentStatefulSet(context).orElseThrow();
// version 22 changed the match labels, account for older versions
if (!existingDeployment.isMarkedForDeletion() && !hasExpectedMatchLabels(existingDeployment, primary)) {
@ -156,7 +164,7 @@ public class KeycloakDeploymentDependentResource extends CRUDKubernetesDependent
};
}
private void configureCache(StatefulSet deployment, Container kcContainer, CacheSpec spec, KubernetesClient client) {
private void configureCache(StatefulSet deployment, Container kcContainer, CacheSpec spec, KubernetesClient client, WatchedResources watchedResources) {
Optional.ofNullable(spec.getConfigMapFile()).ifPresent(configFile -> {
if (configFile.getName() == null || configFile.getKey() == null) {
throw new IllegalStateException("Cache file ConfigMap requires both a name and a key");
@ -236,7 +244,7 @@ public class KeycloakDeploymentDependentResource extends CRUDKubernetesDependent
return Optional.ofNullable(keycloakCR.getSpec()).map(KeycloakSpec::getUnsupported).map(UnsupportedSpec::getPodTemplate);
}
private StatefulSet createBaseDeployment(Keycloak keycloakCR, Context<Keycloak> context) {
private StatefulSet createBaseDeployment(Keycloak keycloakCR, Context<Keycloak> context, Config operatorConfig) {
Map<String, String> labels = Utils.allInstanceLabels(keycloakCR);
labels.put("app.kubernetes.io/component", "server");
Map<String, String> schedulingLabels = new LinkedHashMap<>(labels);
@ -404,7 +412,8 @@ public class KeycloakDeploymentDependentResource extends CRUDKubernetesDependent
return JGROUPS_DNS_QUERY_PARAM + KeycloakDiscoveryServiceDependentResource.getName(keycloakCR) +"." + keycloakCR.getMetadata().getNamespace();
}
private void addEnvVars(StatefulSet baseDeployment, Keycloak keycloakCR, TreeSet<String> allSecrets) {
private void addEnvVars(StatefulSet baseDeployment, Keycloak keycloakCR, TreeSet<String> allSecrets, Context<Keycloak> context) {
var distConfigurator = ContextUtils.getDistConfigurator(context);
var firstClasssEnvVars = distConfigurator.configureDistOptions(keycloakCR);
var additionalEnvVars = getDefaultAndAdditionalEnvVars(keycloakCR);

View File

@ -20,8 +20,8 @@ import io.fabric8.kubernetes.api.model.Service;
import io.fabric8.kubernetes.api.model.ServiceBuilder;
import io.fabric8.kubernetes.api.model.ServiceSpec;
import io.fabric8.kubernetes.api.model.ServiceSpecBuilder;
import io.javaoperatorsdk.operator.api.config.informer.Informer;
import io.javaoperatorsdk.operator.api.reconciler.Context;
import io.javaoperatorsdk.operator.api.reconciler.ResourceDiscriminator;
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource;
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent;
@ -29,18 +29,11 @@ import org.keycloak.operator.Constants;
import org.keycloak.operator.Utils;
import org.keycloak.operator.crds.v2alpha1.deployment.Keycloak;
import java.util.Optional;
@KubernetesDependent(labelSelector = Constants.DEFAULT_LABELS_AS_STRING, resourceDiscriminator = KeycloakDiscoveryServiceDependentResource.NameResourceDiscriminator.class)
@KubernetesDependent(
informer = @Informer(labelSelector = Constants.DEFAULT_LABELS_AS_STRING)
)
public class KeycloakDiscoveryServiceDependentResource extends CRUDKubernetesDependentResource<Service, Keycloak> {
public static class NameResourceDiscriminator implements ResourceDiscriminator<Service, Keycloak> {
@Override
public Optional<Service> distinguish(Class<Service> resource, Keycloak primary, Context<Keycloak> context) {
return Utils.getByName(Service.class, KeycloakDiscoveryServiceDependentResource::getName, primary, context);
}
}
public KeycloakDiscoveryServiceDependentResource() {
super(Service.class);
}

View File

@ -19,6 +19,7 @@ package org.keycloak.operator.controllers;
import io.fabric8.kubernetes.api.model.networking.v1.Ingress;
import io.fabric8.kubernetes.api.model.networking.v1.IngressBuilder;
import io.fabric8.kubernetes.client.KubernetesClientException;
import io.javaoperatorsdk.operator.api.config.informer.Informer;
import io.javaoperatorsdk.operator.api.reconciler.Context;
import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource;
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource;
@ -39,7 +40,9 @@ import java.util.Optional;
import static org.keycloak.operator.crds.v2alpha1.CRDUtils.isTlsConfigured;
@KubernetesDependent(labelSelector = Constants.DEFAULT_LABELS_AS_STRING)
@KubernetesDependent(
informer = @Informer(labelSelector = Constants.DEFAULT_LABELS_AS_STRING)
)
public class KeycloakIngressDependentResource extends CRUDKubernetesDependentResource<Ingress, Keycloak> {
private static final Logger LOG = Logger.getLogger(KeycloakIngressDependentResource.class.getName());

View File

@ -26,6 +26,7 @@ import io.fabric8.kubernetes.api.model.networking.v1.NetworkPolicy;
import io.fabric8.kubernetes.api.model.networking.v1.NetworkPolicyBuilder;
import io.fabric8.kubernetes.api.model.networking.v1.NetworkPolicyFluent;
import io.fabric8.kubernetes.api.model.networking.v1.NetworkPolicyPeer;
import io.javaoperatorsdk.operator.api.config.informer.Informer;
import io.javaoperatorsdk.operator.api.reconciler.Context;
import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource;
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource;
@ -46,7 +47,9 @@ import static org.keycloak.operator.Constants.KEYCLOAK_JGROUPS_FD_PORT;
import static org.keycloak.operator.Constants.KEYCLOAK_JGROUPS_PROTOCOL;
import static org.keycloak.operator.Constants.KEYCLOAK_SERVICE_PROTOCOL;
@KubernetesDependent(labelSelector = Constants.DEFAULT_LABELS_AS_STRING)
@KubernetesDependent(
informer = @Informer(labelSelector = Constants.DEFAULT_LABELS_AS_STRING)
)
public class KeycloakNetworkPolicyDependentResource extends CRUDKubernetesDependentResource<NetworkPolicy, Keycloak> {
private static final Logger LOG = Logger.getLogger(KeycloakNetworkPolicyDependentResource.class.getName());

View File

@ -21,49 +21,34 @@ import io.fabric8.kubernetes.api.model.apps.StatefulSetStatus;
import io.fabric8.kubernetes.api.model.batch.v1.Job;
import io.fabric8.kubernetes.client.KubernetesClient;
import io.javaoperatorsdk.operator.api.reconciler.Context;
import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration;
import io.javaoperatorsdk.operator.api.reconciler.ErrorStatusHandler;
import io.javaoperatorsdk.operator.api.reconciler.ErrorStatusUpdateControl;
import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext;
import io.javaoperatorsdk.operator.api.reconciler.EventSourceInitializer;
import io.javaoperatorsdk.operator.api.reconciler.Reconciler;
import io.javaoperatorsdk.operator.api.reconciler.UpdateControl;
import io.javaoperatorsdk.operator.api.reconciler.Workflow;
import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent;
import io.javaoperatorsdk.operator.processing.event.source.EventSource;
import io.quarkus.logging.Log;
import jakarta.inject.Inject;
import org.keycloak.operator.Config;
import org.keycloak.operator.ContextUtils;
import org.keycloak.operator.crds.v2alpha1.realmimport.KeycloakRealmImport;
import org.keycloak.operator.crds.v2alpha1.realmimport.KeycloakRealmImportStatus;
import org.keycloak.operator.crds.v2alpha1.realmimport.KeycloakRealmImportStatusBuilder;
import org.keycloak.operator.crds.v2alpha1.realmimport.KeycloakRealmImportStatusCondition;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import jakarta.inject.Inject;
@ControllerConfiguration(
@Workflow(
explicitInvocation = true,
dependents = {
@Dependent(type = KeycloakRealmImportSecretDependentResource.class)
@Dependent(type = KeycloakRealmImportJobDependentResource.class, dependsOn = KeycloakRealmImportSecretDependentResource.DEPENDENT_NAME),
@Dependent(type = KeycloakRealmImportSecretDependentResource.class, name = KeycloakRealmImportSecretDependentResource.DEPENDENT_NAME)
})
public class KeycloakRealmImportController implements Reconciler<KeycloakRealmImport>, ErrorStatusHandler<KeycloakRealmImport>, EventSourceInitializer<KeycloakRealmImport> {
public class KeycloakRealmImportController implements Reconciler<KeycloakRealmImport> {
@Inject
Config config;
@Inject
KubernetesClient client;
volatile KeycloakRealmImportJobDependentResource jobDependentResource;
@Override
public Map<String, EventSource> prepareEventSources(EventSourceContext<KeycloakRealmImport> context) {
this.jobDependentResource = new KeycloakRealmImportJobDependentResource(config);
return EventSourceInitializer.nameEventSourcesFromDependentResource(context, jobDependentResource);
}
@Override
public UpdateControl<KeycloakRealmImport> reconcile(KeycloakRealmImport realm, Context<KeycloakRealmImport> context) {
String realmName = realm.getMetadata().getName();
@ -78,13 +63,14 @@ public class KeycloakRealmImportController implements Reconciler<KeycloakRealmIm
.withName(realm.getSpec().getKeycloakCRName()).get();
if (existingDeployment != null) {
context.managedDependentResourceContext().put(StatefulSet.class, existingDeployment);
ContextUtils.storeOperatorConfig(context, config);
ContextUtils.storeCurrentStatefulSet(context, existingDeployment);
if (getReadyReplicas(existingDeployment) > 0) {
jobDependentResource.reconcile(realm, context);
context.managedWorkflowAndDependentResourceContext().reconcileManagedWorkflow();
}
}
updateStatus(statusBuilder, realm, existingJob, existingDeployment);
updateStatus(statusBuilder, realm, existingJob, existingDeployment, context.getClient());
var status = statusBuilder.build();
@ -95,7 +81,7 @@ public class KeycloakRealmImportController implements Reconciler<KeycloakRealmIm
updateControl = UpdateControl.noUpdate();
} else {
realm.setStatus(status);
updateControl = UpdateControl.updateStatus(realm);
updateControl = UpdateControl.patchStatus(realm);
}
if (status
@ -116,10 +102,10 @@ public class KeycloakRealmImportController implements Reconciler<KeycloakRealmIm
.build();
realm.setStatus(status);
return ErrorStatusUpdateControl.updateStatus(realm);
return ErrorStatusUpdateControl.patchStatus(realm);
}
public void updateStatus(KeycloakRealmImportStatusBuilder status, KeycloakRealmImport realmCR, Job existingJob, StatefulSet existingDeployment) {
public void updateStatus(KeycloakRealmImportStatusBuilder status, KeycloakRealmImport realmCR, Job existingJob, StatefulSet existingDeployment, KubernetesClient client) {
if (existingDeployment == null) {
status.addErrorMessage("No existing Deployment found, waiting for it to be created");
return;
@ -144,7 +130,7 @@ public class KeycloakRealmImportController implements Reconciler<KeycloakRealmIm
} else if (oldStatus.getSucceeded() != null && oldStatus.getSucceeded() > 0) {
if (!lastReportedStatus.isDone()) {
Log.info("Job finished performing a rolling restart of the deployment");
rollingRestart(realmCR); // could be based upon a hash annotation on the deployment instead
rollingRestart(realmCR, client); // could be based upon a hash annotation on the deployment instead
}
status.addDone();
} else if (oldStatus.getFailed() != null && oldStatus.getFailed() > 0) {
@ -161,7 +147,7 @@ public class KeycloakRealmImportController implements Reconciler<KeycloakRealmIm
return Optional.ofNullable(existingDeployment.getStatus()).map(StatefulSetStatus::getReadyReplicas).orElse(0);
}
private void rollingRestart(KeycloakRealmImport realmCR) {
private void rollingRestart(KeycloakRealmImport realmCR, KubernetesClient client) {
client.apps().statefulSets()
.inNamespace(realmCR.getMetadata().getNamespace())
.withName(realmCR.getSpec().getKeycloakCRName())

View File

@ -30,11 +30,10 @@ import io.fabric8.kubernetes.client.utils.KubernetesResourceUtil;
import io.javaoperatorsdk.operator.api.reconciler.Context;
import io.javaoperatorsdk.operator.api.reconciler.dependent.GarbageCollected;
import io.javaoperatorsdk.operator.processing.dependent.Creator;
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent;
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource;
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResourceConfigBuilder;
import org.keycloak.operator.Config;
import org.keycloak.operator.Constants;
import org.keycloak.operator.ContextUtils;
import org.keycloak.operator.Utils;
import org.keycloak.operator.crds.v2alpha1.realmimport.KeycloakRealmImport;
import org.keycloak.operator.crds.v2alpha1.realmimport.Placeholder;
@ -46,21 +45,17 @@ import java.util.Set;
import static org.keycloak.operator.Utils.addResources;
import static org.keycloak.operator.controllers.KeycloakDistConfigurator.getKeycloakOptionEnvVarName;
@KubernetesDependent
public class KeycloakRealmImportJobDependentResource extends KubernetesDependentResource<Job, KeycloakRealmImport> implements Creator<Job, KeycloakRealmImport>, GarbageCollected<KeycloakRealmImport> {
private final Config config;
KeycloakRealmImportJobDependentResource(Config config) {
KeycloakRealmImportJobDependentResource() {
super(Job.class);
this.config = config;
this.configureWith(new KubernetesDependentResourceConfigBuilder<Job>()
.withLabelSelector(Constants.DEFAULT_LABELS_AS_STRING)
.build());
}
@Override
protected Job desired(KeycloakRealmImport primary, Context<KeycloakRealmImport> context) {
StatefulSet existingDeployment = context.managedDependentResourceContext().get(StatefulSet.class, StatefulSet.class).orElseThrow();
Config config = ContextUtils.getOperatorConfig(context);
StatefulSet existingDeployment = ContextUtils.getCurrentStatefulSet(context).orElseThrow();
Map<String, Placeholder> placeholders = primary.getSpec().getPlaceholders();
boolean replacePlaceholders = (placeholders != null && !placeholders.isEmpty());
@ -71,7 +66,7 @@ public class KeycloakRealmImportJobDependentResource extends KubernetesDependent
String secretName = KeycloakRealmImportSecretDependentResource.getSecretName(primary);
String volumeName = KubernetesResourceUtil.sanitizeName(secretName + "-volume");
buildKeycloakJobContainer(keycloakPodTemplate.getSpec().getContainers().get(0), primary, volumeName, replacePlaceholders);
buildKeycloakJobContainer(keycloakPodTemplate.getSpec().getContainers().get(0), primary, volumeName, config);
keycloakPodTemplate.getSpec().getVolumes().add(buildSecretVolume(volumeName, secretName));
var labels = keycloakPodTemplate.getMetadata().getLabels();
@ -138,7 +133,7 @@ public class KeycloakRealmImportJobDependentResource extends KubernetesDependent
.build();
}
private void buildKeycloakJobContainer(Container keycloakContainer, KeycloakRealmImport keycloakRealmImport, String volumeName, boolean replacePlaceholders) {
private void buildKeycloakJobContainer(Container keycloakContainer, KeycloakRealmImport keycloakRealmImport, String volumeName, Config config) {
var importMntPath = "/mnt/realm-import/";
var command = List.of("/bin/bash");

View File

@ -3,6 +3,7 @@ package org.keycloak.operator.controllers;
import io.fabric8.kubernetes.api.model.Secret;
import io.fabric8.kubernetes.api.model.SecretBuilder;
import io.fabric8.kubernetes.client.utils.KubernetesResourceUtil;
import io.javaoperatorsdk.operator.api.config.informer.Informer;
import io.javaoperatorsdk.operator.api.reconciler.Context;
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource;
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent;
@ -11,9 +12,13 @@ import org.keycloak.operator.Constants;
import org.keycloak.operator.Utils;
import org.keycloak.operator.crds.v2alpha1.realmimport.KeycloakRealmImport;
@KubernetesDependent(labelSelector = Constants.DEFAULT_LABELS_AS_STRING)
@KubernetesDependent(
informer = @Informer(labelSelector = Constants.DEFAULT_LABELS_AS_STRING)
)
public class KeycloakRealmImportSecretDependentResource extends CRUDKubernetesDependentResource<Secret, KeycloakRealmImport> {
public static final String DEPENDENT_NAME = "realm-import-secret";
public KeycloakRealmImportSecretDependentResource() {
super(Secret.class);
}

View File

@ -23,8 +23,8 @@ import io.fabric8.kubernetes.api.model.Service;
import io.fabric8.kubernetes.api.model.ServiceBuilder;
import io.fabric8.kubernetes.api.model.ServiceSpec;
import io.fabric8.kubernetes.api.model.ServiceSpecBuilder;
import io.javaoperatorsdk.operator.api.config.informer.Informer;
import io.javaoperatorsdk.operator.api.reconciler.Context;
import io.javaoperatorsdk.operator.api.reconciler.ResourceDiscriminator;
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource;
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent;
import org.keycloak.operator.Constants;
@ -35,16 +35,11 @@ import org.keycloak.operator.crds.v2alpha1.deployment.spec.HttpSpec;
import static org.keycloak.operator.crds.v2alpha1.CRDUtils.isTlsConfigured;
@KubernetesDependent(labelSelector = Constants.DEFAULT_LABELS_AS_STRING, resourceDiscriminator = KeycloakServiceDependentResource.NameResourceDiscriminator.class)
@KubernetesDependent(
informer = @Informer(labelSelector = Constants.DEFAULT_LABELS_AS_STRING)
)
public class KeycloakServiceDependentResource extends CRUDKubernetesDependentResource<Service, Keycloak> {
public static class NameResourceDiscriminator implements ResourceDiscriminator<Service, Keycloak> {
@Override
public Optional<Service> distinguish(Class<Service> resource, Keycloak primary, Context<Keycloak> context) {
return Utils.getByName(Service.class, KeycloakServiceDependentResource::getServiceName, primary, context);
}
}
public KeycloakServiceDependentResource() {
super(Service.class);
}

View File

@ -36,16 +36,18 @@ import io.fabric8.kubernetes.api.model.Volume;
import io.fabric8.kubernetes.api.model.batch.v1.Job;
import io.fabric8.kubernetes.api.model.batch.v1.JobBuilder;
import io.fabric8.kubernetes.api.model.batch.v1.JobSpecFluent;
import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration;
import io.javaoperatorsdk.operator.api.reconciler.Context;
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource;
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResourceConfigBuilder;
import org.keycloak.operator.Config;
import jakarta.enterprise.context.ApplicationScoped;
import org.keycloak.operator.Constants;
import org.keycloak.operator.ContextUtils;
import org.keycloak.operator.Utils;
import org.keycloak.operator.crds.v2alpha1.CRDUtils;
import org.keycloak.operator.crds.v2alpha1.deployment.Keycloak;
@ApplicationScoped
public class KeycloakUpdateJobDependentResource extends CRUDKubernetesDependentResource<Job, Keycloak> {
// shared volume configuration
@ -71,13 +73,12 @@ public class KeycloakUpdateJobDependentResource extends CRUDKubernetesDependentR
// container args to replace
private static final Set<String> START_ARGS = Set.of("start", "start-dev");
private final Config operatorConfig;
public KeycloakUpdateJobDependentResource(Config config) {
public KeycloakUpdateJobDependentResource() {
super(Job.class);
operatorConfig = config;
this.configureWith(new KubernetesDependentResourceConfigBuilder<Job>()
.withLabelSelector(Constants.DEFAULT_LABELS_AS_STRING)
.withKubernetesDependentInformerConfig(InformerConfiguration.builder(resourceType())
.withLabelSelector(Constants.DEFAULT_LABELS_AS_STRING)
.build())
.build());
}
@ -144,12 +145,12 @@ public class KeycloakUpdateJobDependentResource extends CRUDKubernetesDependentR
// For test KeycloakDeploymentTest#testDeploymentDurability
// it uses a pause image, which never ends.
// After this seconds, the job is terminated allowing the test to complete.
builder.withActiveDeadlineSeconds(operatorConfig.keycloak().updatePodDeadlineSeconds());
builder.withActiveDeadlineSeconds(ContextUtils.getOperatorConfig(context).keycloak().updatePodDeadlineSeconds());
return builder.build();
}
private static void addInitContainer(PodSpecBuilder builder, Context<Keycloak> context, Collection<String> availableVolumes, Collection<String> requiredVolumes) {
var existing = CRDUtils.firstContainerOf(ContextUtils.getCurrentStatefulSet(context)).orElseThrow();
var existing = CRDUtils.firstContainerOf(ContextUtils.getCurrentStatefulSet(context).orElseThrow()).orElseThrow();
var containerBuilder = builder.addNewInitContainerLike(existing);
configureContainer(containerBuilder, INIT_CONTAINER_NAME, INIT_CONTAINER_ARGS, availableVolumes, requiredVolumes);
containerBuilder.endInitContainer();
@ -196,7 +197,7 @@ public class KeycloakUpdateJobDependentResource extends CRUDKubernetesDependentR
private Map<String, Volume> getAllVolumes(Context<Keycloak> context) {
Map<String, Volume> allVolumes = new HashMap<>();
Consumer<Volume> volumeConsumer = volume -> allVolumes.put(volume.getName(), volume);
CRDUtils.volumesFromStatefulSet(ContextUtils.getCurrentStatefulSet(context)).forEach(volumeConsumer);
CRDUtils.volumesFromStatefulSet(ContextUtils.getCurrentStatefulSet(context).orElseThrow()).forEach(volumeConsumer);
CRDUtils.volumesFromStatefulSet(ContextUtils.getDesiredStatefulSet(context)).forEach(volumeConsumer);
return allVolumes;
}

View File

@ -24,14 +24,13 @@ import com.fasterxml.jackson.annotation.JsonIgnore;
import io.fabric8.kubernetes.model.annotation.LabelSelector;
import io.fabric8.kubernetes.model.annotation.StatusReplicas;
import io.javaoperatorsdk.operator.api.ObservedGenerationAware;
import io.sundr.builder.annotations.Buildable;
/**
* @author Vaclav Muzikar <vmuzikar@redhat.com>
*/
@Buildable(editableEnabled = false, builderPackage = "io.fabric8.kubernetes.api.builder", lazyCollectionInitEnabled = false)
public class KeycloakStatus implements ObservedGenerationAware {
public class KeycloakStatus {
@LabelSelector
private String selector;
@ -77,12 +76,10 @@ public class KeycloakStatus implements ObservedGenerationAware {
return findCondition(KeycloakStatusCondition.READY).map(KeycloakStatusCondition::getStatus).orElse(false);
}
@Override
public Long getObservedGeneration() {
return observedGeneration;
}
@Override
public void setObservedGeneration(Long generation) {
this.observedGeneration = generation;
}

View File

@ -64,7 +64,6 @@ public class KeycloakStatusAggregator {
existingConditions = Map.of();
}
// we're not setting this on the statusBuilder as we're letting the sdk manage that
observedGeneration = generation;
readyCondition.setType(KeycloakStatusCondition.READY);
@ -144,7 +143,10 @@ public class KeycloakStatusAggregator {
updateConditionFromExisting(hasErrorsCondition, existingConditions, now);
updateConditionFromExisting(rollingUpdate, existingConditions, now);
return statusBuilder.withConditions(List.of(readyCondition, hasErrorsCondition, rollingUpdate)).build();
return statusBuilder
.withObservedGeneration(observedGeneration)
.withConditions(List.of(readyCondition, hasErrorsCondition, rollingUpdate))
.build();
}
static void updateConditionFromExisting(KeycloakStatusCondition condition, Map<String, KeycloakStatusCondition> existingConditions, String now) {

View File

@ -19,7 +19,7 @@ package org.keycloak.operator.upgrade;
import io.javaoperatorsdk.operator.api.reconciler.Context;
import jakarta.enterprise.context.ApplicationScoped;
import org.keycloak.operator.controllers.KeycloakDeploymentDependentResource;
import jakarta.inject.Inject;
import org.keycloak.operator.controllers.KeycloakUpdateJobDependentResource;
import org.keycloak.operator.crds.v2alpha1.deployment.Keycloak;
import org.keycloak.operator.crds.v2alpha1.deployment.spec.UpdateSpec;
@ -32,13 +32,15 @@ import org.keycloak.operator.upgrade.impl.RecreateOnImageChangeUpgradeLogic;
*/
@ApplicationScoped
public class UpgradeLogicFactory {
@Inject
KeycloakUpdateJobDependentResource updateJobDependentResource;
public UpgradeLogic create(Keycloak keycloak, Context<Keycloak> context, KeycloakDeploymentDependentResource dependentResource, KeycloakUpdateJobDependentResource updateJobDependentResource) {
public UpgradeLogic create(Keycloak keycloak, Context<Keycloak> context) {
var strategy = UpdateSpec.getUpdateStrategy(keycloak);
return switch (strategy) {
case RECREATE_ON_IMAGE_CHANGE -> new RecreateOnImageChangeUpgradeLogic(context, keycloak, dependentResource);
case FORCE_RECREATE -> new ForceRecreateUpgradeLogic(context, keycloak, dependentResource);
case AUTO -> new AutoUpgradeLogic(context, keycloak, dependentResource, updateJobDependentResource);
case RECREATE_ON_IMAGE_CHANGE -> new RecreateOnImageChangeUpgradeLogic(context, keycloak);
case FORCE_RECREATE -> new ForceRecreateUpgradeLogic(context, keycloak);
case AUTO -> new AutoUpgradeLogic(context, keycloak, updateJobDependentResource);
};
}

View File

@ -32,7 +32,6 @@ import io.fabric8.kubernetes.client.KubernetesClient;
import io.javaoperatorsdk.operator.api.reconciler.Context;
import io.javaoperatorsdk.operator.api.reconciler.UpdateControl;
import io.quarkus.logging.Log;
import org.keycloak.operator.controllers.KeycloakDeploymentDependentResource;
import org.keycloak.operator.controllers.KeycloakUpdateJobDependentResource;
import org.keycloak.operator.crds.v2alpha1.CRDUtils;
import org.keycloak.operator.crds.v2alpha1.deployment.Keycloak;
@ -41,8 +40,8 @@ public class AutoUpgradeLogic extends BaseUpgradeLogic {
private final KeycloakUpdateJobDependentResource updateJobResource;
public AutoUpgradeLogic(Context<Keycloak> context, Keycloak keycloak, KeycloakDeploymentDependentResource statefulSetResource, KeycloakUpdateJobDependentResource updateJobResource) {
super(context, keycloak, statefulSetResource);
public AutoUpgradeLogic(Context<Keycloak> context, Keycloak keycloak, KeycloakUpdateJobDependentResource updateJobResource) {
super(context, keycloak);
this.updateJobResource = updateJobResource;
}

View File

@ -46,23 +46,21 @@ abstract class BaseUpgradeLogic implements UpgradeLogic {
protected final Context<Keycloak> context;
protected final Keycloak keycloak;
protected final KeycloakDeploymentDependentResource statefulSetResource;
BaseUpgradeLogic(Context<Keycloak> context, Keycloak keycloak, KeycloakDeploymentDependentResource statefulSetResource) {
BaseUpgradeLogic(Context<Keycloak> context, Keycloak keycloak) {
this.context = context;
this.keycloak = keycloak;
this.statefulSetResource = statefulSetResource;
}
@Override
public final Optional<UpdateControl<Keycloak>> decideUpgrade() {
var existing = context.getSecondaryResource(StatefulSet.class);
var existing = ContextUtils.getCurrentStatefulSet(context);
if (existing.isEmpty()) {
// new deployment, no upgrade needed
Log.debug("New deployment - skipping upgrade logic");
return Optional.empty();
}
var desiredStatefulSet = statefulSetResource.desired(keycloak, context);
var desiredStatefulSet = ContextUtils.getDesiredStatefulSet(context);
var desiredContainer = CRDUtils.firstContainerOf(desiredStatefulSet).orElseThrow(BaseUpgradeLogic::containerNotFound);
var actualContainer = CRDUtils.firstContainerOf(existing.get()).orElseThrow(BaseUpgradeLogic::containerNotFound);
@ -72,9 +70,6 @@ abstract class BaseUpgradeLogic implements UpgradeLogic {
return Optional.empty();
}
// store in context the current and desired stateful set for easy access.
ContextUtils.storeCurrentStatefulSet(context, existing.get());
ContextUtils.storeDesiredStatefulSet(context, desiredStatefulSet);
return onUpgrade();
}

View File

@ -21,7 +21,6 @@ import java.util.Optional;
import io.javaoperatorsdk.operator.api.reconciler.Context;
import io.javaoperatorsdk.operator.api.reconciler.UpdateControl;
import org.keycloak.operator.controllers.KeycloakDeploymentDependentResource;
import org.keycloak.operator.crds.v2alpha1.deployment.Keycloak;
import org.keycloak.operator.upgrade.UpgradeLogic;
import org.keycloak.operator.upgrade.UpgradeType;
@ -32,8 +31,8 @@ import org.keycloak.operator.upgrade.UpgradeType;
*/
public class ForceRecreateUpgradeLogic extends BaseUpgradeLogic {
public ForceRecreateUpgradeLogic(Context<Keycloak> context, Keycloak keycloak, KeycloakDeploymentDependentResource statefulSetResource) {
super(context, keycloak, statefulSetResource);
public ForceRecreateUpgradeLogic(Context<Keycloak> context, Keycloak keycloak) {
super(context, keycloak);
}
@Override

View File

@ -25,7 +25,6 @@ import io.fabric8.kubernetes.api.model.apps.StatefulSet;
import io.javaoperatorsdk.operator.api.reconciler.Context;
import io.javaoperatorsdk.operator.api.reconciler.UpdateControl;
import org.keycloak.operator.ContextUtils;
import org.keycloak.operator.controllers.KeycloakDeploymentDependentResource;
import org.keycloak.operator.crds.v2alpha1.CRDUtils;
import org.keycloak.operator.crds.v2alpha1.deployment.Keycloak;
import org.keycloak.operator.upgrade.UpgradeType;
@ -38,13 +37,13 @@ import org.keycloak.operator.upgrade.UpgradeType;
@SuppressWarnings("ALL")
public class RecreateOnImageChangeUpgradeLogic extends BaseUpgradeLogic {
public RecreateOnImageChangeUpgradeLogic(Context<Keycloak> context, Keycloak keycloak, KeycloakDeploymentDependentResource dependentResource) {
super(context, keycloak, dependentResource);
public RecreateOnImageChangeUpgradeLogic(Context<Keycloak> context, Keycloak keycloak) {
super(context, keycloak);
}
@Override
Optional<UpdateControl<Keycloak>> onUpgrade() {
var currentImage = extractImage(ContextUtils.getCurrentStatefulSet(context));
var currentImage = extractImage(ContextUtils.getCurrentStatefulSet(context).orElseThrow());
var desiredImage = extractImage(ContextUtils.getDesiredStatefulSet(context));
if (Objects.equals(currentImage, desiredImage)) {

View File

@ -62,7 +62,10 @@ import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.TestInfo;
import org.keycloak.operator.Constants;
import org.keycloak.operator.controllers.KeycloakController;
import org.keycloak.operator.controllers.KeycloakDeploymentDependentResource;
import org.keycloak.operator.controllers.KeycloakRealmImportController;
import org.keycloak.operator.controllers.KeycloakUpdateJobDependentResource;
import org.keycloak.operator.crds.v2alpha1.deployment.Keycloak;
import org.keycloak.operator.crds.v2alpha1.deployment.KeycloakSpecBuilder;
import org.keycloak.operator.crds.v2alpha1.deployment.KeycloakStatus;
@ -213,24 +216,6 @@ public class BaseOperatorTest implements QuarkusTestAfterEachCallback {
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;
}
};
}
});
}
@ -449,6 +434,11 @@ public class BaseOperatorTest implements QuarkusTestAfterEachCallback {
Log.info("Stopping Operator");
operator.stop();
// Avoid issues with Event Informers between tests
Log.info("Removing Controllers and application scoped DRs from CDI");
Stream.of(KeycloakController.class, KeycloakRealmImportController.class, KeycloakUpdateJobDependentResource.class)
.forEach(c -> CDI.current().destroy(CDI.current().select(c).get()));
Log.info("Creating new K8s Client");
// create a new client bc operator has closed the old one
createK8sClient();

View File

@ -53,9 +53,9 @@ class KeycloakControllerTest {
// both the instances and hostname should be updated
UpdateControl<Keycloak> update = controller.reconcile(kc, mockContext);
assertTrue(update.isUpdateResource());
assertEquals(1, update.getResource().getSpec().getInstances());
assertEquals("example-kc-ingress-ns.openshift.com", update.getResource().getSpec().getHostnameSpec().getHostname());
assertTrue(update.isPatchResource());
assertEquals(1, update.getResource().orElseThrow().getSpec().getInstances());
assertEquals("example-kc-ingress-ns.openshift.com", update.getResource().orElseThrow().getSpec().getHostnameSpec().getHostname());
// just the instances should be updated if not openshift-default
kc = K8sUtils.getDefaultKeycloakDeployment();
@ -63,9 +63,9 @@ class KeycloakControllerTest {
kc.getSpec().setInstances(null);
kc.getSpec().getHostnameSpec().setHostname(null);
update = controller.reconcile(kc, mockContext);
assertTrue(update.isUpdateResource());
assertEquals(1, update.getResource().getSpec().getInstances());
assertNull(update.getResource().getSpec().getHostnameSpec().getHostname());
assertTrue(update.isPatchResource());
assertEquals(1, update.getResource().orElseThrow().getSpec().getInstances());
assertNull(update.getResource().orElseThrow().getSpec().getHostnameSpec().getHostname());
}
}

View File

@ -32,12 +32,13 @@ import io.fabric8.kubernetes.api.model.Volume;
import io.fabric8.kubernetes.api.model.VolumeMount;
import io.fabric8.kubernetes.api.model.apps.StatefulSet;
import io.fabric8.kubernetes.api.model.apps.StatefulSetBuilder;
import io.fabric8.kubernetes.client.KubernetesClient;
import io.fabric8.kubernetes.client.utils.Serialization;
import io.javaoperatorsdk.operator.api.reconciler.Context;
import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.DefaultManagedDependentResourceContext;
import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.ManagedWorkflowAndDependentResourceContext;
import io.quarkus.test.InjectMock;
import io.quarkus.test.junit.QuarkusTest;
import jakarta.inject.Inject;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@ -57,17 +58,18 @@ import org.keycloak.operator.crds.v2alpha1.deployment.spec.UnsupportedSpec;
import org.mockito.Mockito;
import java.util.List;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Collectors;
import jakarta.inject.Inject;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.keycloak.operator.ContextUtils.DIST_CONFIGURATOR_KEY;
import static org.keycloak.operator.ContextUtils.OLD_DEPLOYMENT_KEY;
import static org.keycloak.operator.ContextUtils.OPERATOR_CONFIG_KEY;
import static org.keycloak.operator.ContextUtils.WATCHED_RESOURCES_KEY;
@QuarkusTest
public class PodTemplateTest {
@ -85,7 +87,7 @@ public class PodTemplateTest {
@BeforeEach
protected void setup() {
this.deployment = new KeycloakDeploymentDependentResource(operatorConfig, watchedResources, distConfigurator);
this.deployment = new KeycloakDeploymentDependentResource();
}
private StatefulSet getDeployment(PodTemplateSpec podTemplate, StatefulSet existingDeployment, Consumer<KeycloakSpecBuilder> additionalSpec) {
@ -108,12 +110,15 @@ public class PodTemplateTest {
kc.setSpec(keycloakSpecBuilder.build());
var managedDependentResourceContext = new DefaultManagedDependentResourceContext();
//noinspection unchecked
Context<Keycloak> context = Mockito.mock(Context.class);
Mockito.when(context.managedDependentResourceContext()).thenReturn(managedDependentResourceContext);
Mockito.when(context.getSecondaryResource(StatefulSet.class)).thenReturn(Optional.ofNullable(existingDeployment));
ManagedWorkflowAndDependentResourceContext managedWorkflowAndDependentResourceContext = Mockito.mock(ManagedWorkflowAndDependentResourceContext.class);
Mockito.when(context.managedWorkflowAndDependentResourceContext()).thenReturn(managedWorkflowAndDependentResourceContext);
Mockito.when(managedWorkflowAndDependentResourceContext.getMandatory(OLD_DEPLOYMENT_KEY, StatefulSet.class)).thenReturn(existingDeployment);
Mockito.when(managedWorkflowAndDependentResourceContext.getMandatory(OPERATOR_CONFIG_KEY, Config.class)).thenReturn(operatorConfig);
Mockito.when(managedWorkflowAndDependentResourceContext.getMandatory(WATCHED_RESOURCES_KEY, WatchedResources.class)).thenReturn(watchedResources);
Mockito.when(managedWorkflowAndDependentResourceContext.getMandatory(DIST_CONFIGURATOR_KEY, KeycloakDistConfigurator.class)).thenReturn(distConfigurator);
Mockito.when(context.getClient()).thenReturn(Mockito.mock(KubernetesClient.class));
return deployment.desired(kc, context);
}

View File

@ -120,6 +120,7 @@ public final class CRAssert {
.await()
.pollInterval(1, TimeUnit.SECONDS)
.timeout(Duration.ofMinutes(5))
.ignoreExceptions()
.untilAsserted(() -> client.pods()
.inNamespace(namespaceOf(keycloak))
.withLabels(Utils.allInstanceLabels(keycloak))

View File

@ -52,8 +52,8 @@
<jboss.snapshots.repo.id>jboss-snapshots-repository</jboss.snapshots.repo.id>
<jboss.snapshots.repo.url>https://s01.oss.sonatype.org/content/repositories/snapshots/</jboss.snapshots.repo.url>
<quarkus.version>3.17.8</quarkus.version>
<quarkus.build.version>3.17.8</quarkus.build.version>
<quarkus.version>3.18.2</quarkus.version>
<quarkus.build.version>3.18.2</quarkus.build.version>
<project.build-time>${timestamp}</project.build-time>
@ -165,7 +165,7 @@
<postgresql.container>mirror.gcr.io/postgres:${postgresql.version}</postgresql.container>
<aurora-postgresql.version>16.1</aurora-postgresql.version>
<aws-jdbc-wrapper.version>2.3.1</aws-jdbc-wrapper.version>
<postgresql-jdbc.version>42.7.4</postgresql-jdbc.version>
<postgresql-jdbc.version>42.7.5</postgresql-jdbc.version>
<mariadb.version>11.4</mariadb.version>
<mariadb.container>mirror.gcr.io/mariadb:${mariadb.version}</mariadb.container>
<mariadb-jdbc.version>3.4.1</mariadb-jdbc.version>

View File

@ -117,9 +117,9 @@ public class ProxyHostnameV2DistTest {
}
private void assertXForwardedHeaders() {
given().header("X-Forwarded-Host", "test").when().get("http://mykeycloak.org:8080").then().header(HttpHeaders.LOCATION, containsString("http://test:8080/admin"));
given().header("X-Forwarded-Host", "test").when().get("http://localhost:8080").then().header(HttpHeaders.LOCATION, containsString("http://test:8080/admin"));
given().header("X-Forwarded-Host", "test").when().get("https://localhost:8443").then().header(HttpHeaders.LOCATION, containsString("https://test:8443/admin"));
given().header("X-Forwarded-Host", "test:123").when().get("http://mykeycloak.org:8080").then().header(HttpHeaders.LOCATION, containsString("http://test:123/admin"));
given().header("X-Forwarded-Host", "test:123").when().get("http://localhost:8080").then().header(HttpHeaders.LOCATION, containsString("http://test:123/admin"));
given().header("X-Forwarded-Host", "test:123").when().get("https://localhost:8443").then().header(HttpHeaders.LOCATION, containsString("https://test:123/admin"));
given().header("X-Forwarded-Proto", "https").when().get("http://localhost:8080").then().header(HttpHeaders.LOCATION, containsString("https://localhost/admin"));
given().header("X-Forwarded-Proto", "https").header("X-Forwarded-Port", "8443").when().get("http://localhost:8080").then().header(HttpHeaders.LOCATION, containsString("https://localhost:8443/admin"));
}
@ -140,4 +140,4 @@ public class ProxyHostnameV2DistTest {
Assert.assertEquals(expectedBaseUrl + "realms/master/protocol/openid-connect/auth", getServerMetadata(requestBaseUrl)
.getAuthorizationEndpoint());
}
}
}

View File

@ -1,6 +1,7 @@
package org.keycloak.testframework;
import io.quarkus.runtime.logging.LoggingSetupRecorder;
import io.smallrye.config.SmallRyeConfigProviderResolver;
import org.eclipse.microprofile.config.spi.ConfigProviderResolver;
import org.jboss.logging.Logger;
import org.jboss.logmanager.LogManager;
@ -26,7 +27,14 @@ public class LogHandler {
}
private static void initializeQuarkusLogging() {
ConfigProviderResolver.instance().registerConfig(Config.getConfig(), Thread.currentThread().getContextClassLoader());
// We do not care about Config that was created by Quarkus' TestConfigProviderResolver.
// Alternatively, a Customizer could be used so we could keep the Config created by Quarkus but this is not Quarkus tests,
// relying on Quarkus' Config is not necessary and might be fragile.
SmallRyeConfigProviderResolver configProviderResolver = (SmallRyeConfigProviderResolver) ConfigProviderResolver.instance();
ClassLoader cl = Thread.currentThread().getContextClassLoader();
configProviderResolver.releaseConfig(cl);
ConfigProviderResolver.instance().registerConfig(Config.getConfig(), cl);
LoggingSetupRecorder.handleFailedStart();
}

View File

@ -111,7 +111,8 @@ public class UserPropertyFileStorage implements UserLookupProvider, UserStorageP
public int getUsersCount(RealmModel realm, Map<String, String> params) {
addCall(COUNT_SEARCH_METHOD);
return (int) searchForUser(realm, params.get(UserModel.SEARCH), null, null, username -> username.contains(params.get(UserModel.SEARCH))).count();
String search = params.get(UserModel.SEARCH);
return (int) searchForUser(realm, search, null, null, username -> search == null || username.contains(search)).count();
}
@Override