From 9dfcaf016291c17894547fd1bbbcd684621ef605 Mon Sep 17 00:00:00 2001 From: Pedro Igor Date: Thu, 28 Oct 2021 12:20:51 -0300 Subject: [PATCH] [KEYCLOAK-19687] - Moving cluster config parsing to build time --- .../quarkus/deployment/KeycloakProcessor.java | 60 ++++++++++++++- .../quarkus/runtime/KeycloakRecorder.java | 6 ++ .../runtime/configuration/PropertyMapper.java | 6 ++ .../configuration/PropertyMappers.java | 2 +- .../storage/infinispan/CacheInitializer.java | 64 +++++++++++++++ .../QuarkusCacheManagerProvider.java | 77 +------------------ 6 files changed, 137 insertions(+), 78 deletions(-) create mode 100644 quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/infinispan/CacheInitializer.java diff --git a/quarkus/deployment/src/main/java/org/keycloak/quarkus/deployment/KeycloakProcessor.java b/quarkus/deployment/src/main/java/org/keycloak/quarkus/deployment/KeycloakProcessor.java index a1a87f5909..5ac03a3407 100644 --- a/quarkus/deployment/src/main/java/org/keycloak/quarkus/deployment/KeycloakProcessor.java +++ b/quarkus/deployment/src/main/java/org/keycloak/quarkus/deployment/KeycloakProcessor.java @@ -17,6 +17,7 @@ package org.keycloak.quarkus.deployment; +import static org.keycloak.quarkus.runtime.configuration.Configuration.getConfigValue; import static org.keycloak.quarkus.runtime.configuration.Configuration.getPropertyNames; import static org.keycloak.quarkus.runtime.storage.database.jpa.QuarkusJpaConnectionProviderFactory.QUERY_PROPERTY_PREFIX; import static org.keycloak.connections.jpa.util.JpaUtils.loadSpecificNamedQueries; @@ -27,14 +28,19 @@ import static org.keycloak.representations.provider.ScriptProviderDescriptor.POL import static org.keycloak.quarkus.runtime.Environment.CLI_ARGS; import static org.keycloak.quarkus.runtime.Environment.getProviderFiles; +import javax.enterprise.context.ApplicationScoped; import javax.persistence.Entity; import javax.persistence.spi.PersistenceUnitTransactionType; +import java.io.BufferedReader; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; +import java.io.InputStreamReader; import java.net.URL; import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -50,8 +56,10 @@ import java.util.function.Consumer; import java.util.function.Function; import java.util.jar.JarEntry; import java.util.jar.JarFile; +import java.util.stream.Collectors; import io.quarkus.agroal.spi.JdbcDataSourceBuildItem; +import io.quarkus.arc.deployment.SyntheticBeanBuildItem; import io.quarkus.deployment.IsDevelopment; import io.quarkus.deployment.builditem.CombinedIndexBuildItem; import io.quarkus.deployment.builditem.HotDeploymentWatchedFileBuildItem; @@ -69,6 +77,7 @@ import io.vertx.core.Handler; import io.vertx.ext.web.RoutingContext; import org.hibernate.cfg.AvailableSettings; import org.hibernate.jpa.boot.internal.ParsedPersistenceXmlDescriptor; +import org.infinispan.commons.util.FileLookupFactory; import org.jboss.jandex.AnnotationInstance; import org.jboss.jandex.AnnotationTarget; import org.jboss.jandex.DotName; @@ -108,6 +117,8 @@ import io.quarkus.deployment.annotations.ExecutionTime; import io.quarkus.deployment.annotations.Record; import io.quarkus.deployment.builditem.FeatureBuildItem; import io.quarkus.vertx.http.deployment.FilterBuildItem; + +import org.keycloak.quarkus.runtime.storage.infinispan.CacheInitializer; import org.keycloak.representations.provider.ScriptProviderDescriptor; import org.keycloak.representations.provider.ScriptProviderMetadata; import org.keycloak.quarkus.runtime.integration.web.NotFoundHandler; @@ -257,9 +268,6 @@ class KeycloakProcessor { *

Make the build time configuration available at runtime so that the server can run without having to specify some of * the properties again. * - *

This build step also adds a static call to {@link org.keycloak.quarkus.runtime.cli.ShowConfigCommand#run} via the recorder - * so that the configuration can be shown when requested. - * * @param recorder the recorder */ @Record(ExecutionTime.STATIC_INIT) @@ -296,6 +304,52 @@ class KeycloakProcessor { } } + @Record(ExecutionTime.RUNTIME_INIT) + @BuildStep + void configureInfinispan(KeycloakRecorder recorder, BuildProducer syntheticBeanBuildItems) { + String pathPrefix; + String homeDir = Environment.getHomeDir(); + + if (homeDir == null) { + pathPrefix = ""; + } else { + pathPrefix = homeDir + "/conf/"; + } + + String configFile = getConfigValue("kc.spi.connections-infinispan.quarkus.config-file").getValue(); + + if (configFile != null) { + Path configPath = Paths.get(pathPrefix + configFile); + String path; + + if (configPath.toFile().exists()) { + path = configPath.toFile().getAbsolutePath(); + } else { + path = configPath.getFileName().toString(); + } + + InputStream url = FileLookupFactory.newInstance().lookupFile(path, KeycloakProcessor.class.getClassLoader()); + + if (url == null) { + throw new IllegalArgumentException("Could not load cluster configuration file at [" + configPath + "]"); + } + + try (BufferedReader reader = new BufferedReader(new InputStreamReader(url))) { + String config = reader.lines().collect(Collectors.joining("\n")); + + syntheticBeanBuildItems.produce(SyntheticBeanBuildItem.configure(CacheInitializer.class) + .scope(ApplicationScoped.class) + .unremovable() + .setRuntimeInit() + .runtimeValue(recorder.createCacheInitializer(config)).done()); + } catch (Exception cause) { + throw new RuntimeException("Failed to read clustering configuration from [" + url + "]", cause); + } + } else { + throw new IllegalArgumentException("Option 'configFile' needs to be specified"); + } + } + private boolean isNotPersistentProperty(String name) { // these properties are ignored from the build time properties as they are runtime-specific return !name.startsWith(NS_KEYCLOAK) || "kc.home.dir".equals(name) || CLI_ARGS.equals(name); diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/KeycloakRecorder.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/KeycloakRecorder.java index 64999eebc9..854a537ffa 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/KeycloakRecorder.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/KeycloakRecorder.java @@ -31,7 +31,9 @@ import org.keycloak.quarkus.runtime.storage.database.liquibase.KeycloakLogger; import org.keycloak.provider.Provider; import org.keycloak.provider.ProviderFactory; import org.keycloak.provider.Spi; +import org.keycloak.quarkus.runtime.storage.infinispan.CacheInitializer; +import io.quarkus.runtime.RuntimeValue; import io.quarkus.runtime.annotations.Recorder; import liquibase.logging.LogFactory; import liquibase.servicelocator.ServiceLocator; @@ -98,4 +100,8 @@ public class KeycloakRecorder { } }); } + + public RuntimeValue createCacheInitializer(String config) { + return new RuntimeValue<>(new CacheInitializer(config)); + } } diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/PropertyMapper.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/PropertyMapper.java index 3ccdf6751f..ed7c2b096a 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/PropertyMapper.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/PropertyMapper.java @@ -79,6 +79,12 @@ public class PropertyMapper { return MAPPERS.computeIfAbsent(toProperty, s -> new PropertyMapper(fromProperty, s, null, null, null, true, description, false, expectedValues)); } + static PropertyMapper createBuildTimeProperty(String fromProperty, String toProperty, String defaultValue, + BiFunction transformer, String description, + Iterable expectedValues) { + return MAPPERS.computeIfAbsent(toProperty, s -> new PropertyMapper(fromProperty, s, defaultValue, transformer, null, true, description, false, expectedValues)); + } + static Map MAPPERS = new HashMap<>(); static PropertyMapper IDENTITY = new PropertyMapper(null, null, null, null, null) { diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/PropertyMappers.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/PropertyMappers.java index 4a10e1dcf9..ac1127342e 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/PropertyMappers.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/PropertyMappers.java @@ -146,7 +146,7 @@ public final class PropertyMappers { } private static void configureClustering() { - createWithDefault("cluster", "kc.spi.connections-infinispan.quarkus.config-file", "default", (value, context) -> "cluster-" + value + ".xml", "Specifies clustering configuration. The specified value points to the infinispan configuration file prefixed with the 'cluster-` " + createBuildTimeProperty("cluster", "kc.spi.connections-infinispan.quarkus.config-file", "default", (value, context) -> "cluster-" + value + ".xml", "Specifies clustering configuration. The specified value points to the infinispan configuration file prefixed with the 'cluster-` " + "inside the distribution configuration directory. Supported values out of the box are 'local' and 'default'. Value 'local' points to the file cluster-local.xml and " + "effectively disables clustering and use infinispan caches in the local mode. Value 'default' points to the file cluster-default.xml, which has clustering enabled for infinispan caches.", Arrays.asList("local", "default")); diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/infinispan/CacheInitializer.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/infinispan/CacheInitializer.java new file mode 100644 index 0000000000..6866f2c7e7 --- /dev/null +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/infinispan/CacheInitializer.java @@ -0,0 +1,64 @@ +/* + * 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.quarkus.runtime.storage.infinispan; + +import org.infinispan.configuration.parsing.ConfigurationBuilderHolder; +import org.infinispan.configuration.parsing.ParserRegistry; +import org.infinispan.jboss.marshalling.core.JBossUserMarshaller; +import org.infinispan.manager.DefaultCacheManager; +import org.jboss.logging.Logger; +import org.keycloak.Config; + +public class CacheInitializer { + + private static final Logger log = Logger.getLogger(CacheInitializer.class); + + private final String config; + + public CacheInitializer(String config) { + this.config = config; + } + + public DefaultCacheManager getCacheManager(Config.Scope config) { + try { + ConfigurationBuilderHolder builder = new ParserRegistry().parse(this.config); + + if (builder.getNamedConfigurationBuilders().get("sessions").clustering().cacheMode().isClustered()) { + configureTransportStack(config, builder); + } + + // For Infinispan 10, we go with the JBoss marshalling. + // TODO: This should be replaced later with the marshalling recommended by infinispan. Probably protostream. + // See https://infinispan.org/docs/stable/titles/developing/developing.html#marshalling for the details + builder.getGlobalConfigurationBuilder().serialization().marshaller(new JBossUserMarshaller()); + + return new DefaultCacheManager(builder, false); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private void configureTransportStack(Config.Scope config, ConfigurationBuilderHolder builder) { + String transportStack = config.get("stack"); + + if (transportStack != null) { + builder.getGlobalConfigurationBuilder().transport().defaultTransport() + .addProperty("configurationFile", "default-configs/default-jgroups-" + transportStack + ".xml"); + } + } +} diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/infinispan/QuarkusCacheManagerProvider.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/infinispan/QuarkusCacheManagerProvider.java index 5b66c2d268..95b549bf81 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/infinispan/QuarkusCacheManagerProvider.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/infinispan/QuarkusCacheManagerProvider.java @@ -17,89 +17,18 @@ package org.keycloak.quarkus.runtime.storage.infinispan; -import java.net.URL; -import java.nio.file.Path; -import java.nio.file.Paths; - -import org.infinispan.commons.util.FileLookupFactory; -import org.infinispan.configuration.parsing.ConfigurationBuilderHolder; -import org.infinispan.configuration.parsing.ParserRegistry; -import org.infinispan.jboss.marshalling.core.JBossUserMarshaller; -import org.infinispan.manager.DefaultCacheManager; -import org.jboss.logging.Logger; import org.keycloak.cluster.ManagedCacheManagerProvider; import org.keycloak.Config; -import org.keycloak.quarkus.runtime.Environment; + +import io.quarkus.arc.Arc; /** * @author Pedro Igor */ public final class QuarkusCacheManagerProvider implements ManagedCacheManagerProvider { - private static final Logger log = Logger.getLogger(QuarkusCacheManagerProvider.class); - @Override public C getCacheManager(Config.Scope config) { - try { - ConfigurationBuilderHolder builder = new ParserRegistry().parse(loadConfiguration(config)); - - if (builder.getNamedConfigurationBuilders().get("sessions").clustering().cacheMode().isClustered()) { - configureTransportStack(config, builder); - } - - // For Infinispan 10, we go with the JBoss marshalling. - // TODO: This should be replaced later with the marshalling recommended by infinispan. Probably protostream. - // See https://infinispan.org/docs/stable/titles/developing/developing.html#marshalling for the details - builder.getGlobalConfigurationBuilder().serialization().marshaller(new JBossUserMarshaller()); - - return (C) new DefaultCacheManager(builder, false); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - private URL loadConfiguration(Config.Scope config) { - String pathPrefix; - String homeDir = Environment.getHomeDir(); - - if (homeDir == null) { - log.warn("Keycloak home directory not set"); - pathPrefix = ""; - } else { - pathPrefix = homeDir + "/conf/"; - } - - // Always try to use "configFile" if explicitly specified - String configFile = config.get("configFile"); - if (configFile != null) { - Path configPath = Paths.get(pathPrefix + configFile); - String path; - - if (configPath.toFile().exists()) { - path = configPath.toFile().getAbsolutePath(); - } else { - path = configPath.getFileName().toString(); - } - - log.infof("Loading cluster configuration from %s", configPath); - URL url = FileLookupFactory.newInstance().lookupFileLocation(path, Thread.currentThread().getContextClassLoader()); - - if (url == null) { - throw new IllegalArgumentException("Could not load cluster configuration file at [" + configPath + "]"); - } - - return url; - } else { - throw new IllegalArgumentException("Option 'configFile' needs to be specified"); - } - } - - private void configureTransportStack(Config.Scope config, ConfigurationBuilderHolder builder) { - String transportStack = config.get("stack"); - - if (transportStack != null) { - builder.getGlobalConfigurationBuilder().transport().defaultTransport() - .addProperty("configurationFile", "default-configs/default-jgroups-" + transportStack + ".xml"); - } + return (C) Arc.container().instance(CacheInitializer.class).get().getCacheManager(config); } }