[KEYCLOAK-19306] - Automatic re-augmentation

This commit is contained in:
Pedro Igor 2021-09-21 19:53:52 -03:00
parent 15b3af7b06
commit 36706c7bd1
12 changed files with 328 additions and 240 deletions

View file

@ -29,7 +29,7 @@ DEBUG_MODE="${DEBUG:-false}"
DEBUG_PORT="${DEBUG_PORT:-8787}"
CONFIG_ARGS=${CONFIG_ARGS:-""}
IS_CONFIGURE="false"
IS_DEV_MODE="false"
while [ "$#" -gt 0 ]
do
@ -47,6 +47,9 @@ do
;;
*)
if [[ $1 = --* || ! $1 =~ ^-.* ]]; then
if [ "$1" = "start-dev" ]; then
IS_DEV_MODE=true
fi
CONFIG_ARGS="$CONFIG_ARGS $1"
else
SERVER_OPTS="$SERVER_OPTS $1"
@ -77,4 +80,10 @@ fi
CLASSPATH_OPTS="$DIRNAME/../lib/quarkus-run.jar"
exec java $JAVA_OPTS $SERVER_OPTS -cp $CLASSPATH_OPTS io.quarkus.bootstrap.runner.QuarkusEntryPoint ${CONFIG_ARGS#?}
JAVA_RUN_OPTS="$JAVA_OPTS $SERVER_OPTS -cp $CLASSPATH_OPTS io.quarkus.bootstrap.runner.QuarkusEntryPoint ${CONFIG_ARGS#?}"
if [ "$IS_DEV_MODE" = "true" ]; then
java -Dkc.dev.rebuild=true $JAVA_RUN_OPTS
fi
exec java $JAVA_RUN_OPTS

View file

@ -1,61 +0,0 @@
/*
* Copyright 2020 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.deployment;
import java.io.File;
import java.io.FilenameFilter;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import org.jboss.logging.Logger;
import org.keycloak.util.Environment;
public class BuildClassLoader extends URLClassLoader {
private static final Logger logger = Logger.getLogger(BuildClassLoader.class);
public BuildClassLoader() {
super(new URL[] {}, Thread.currentThread().getContextClassLoader());
String homeDir = Environment.getHomeDir();
if (homeDir == null) {
return;
}
File providersDir = new File(homeDir + File.separator + "providers");
if (providersDir.isDirectory()) {
for (File file : providersDir.listFiles(new JarFilter())) {
try {
addURL(file.toURI().toURL());
logger.debug("Loading providers from " + file.getAbsolutePath());
} catch (MalformedURLException e) {
throw new RuntimeException("Failed to add provider JAR at " + file.getAbsolutePath());
}
}
}
}
class JarFilter implements FilenameFilter {
@Override
public boolean accept(File dir, String name) {
return name.toLowerCase().endsWith(".jar");
}
}
}

View file

@ -19,12 +19,14 @@ package org.keycloak.quarkus.deployment;
import static java.util.Collections.emptyList;
import static org.keycloak.configuration.Configuration.getPropertyNames;
import static org.keycloak.configuration.Configuration.getRawValue;
import static org.keycloak.connections.jpa.QuarkusJpaConnectionProviderFactory.QUERY_PROPERTY_PREFIX;
import static org.keycloak.connections.jpa.util.JpaUtils.loadSpecificNamedQueries;
import static org.keycloak.configuration.MicroProfileConfigProvider.NS_KEYCLOAK;
import static org.keycloak.representations.provider.ScriptProviderDescriptor.AUTHENTICATORS;
import static org.keycloak.representations.provider.ScriptProviderDescriptor.MAPPERS;
import static org.keycloak.representations.provider.ScriptProviderDescriptor.POLICIES;
import static org.keycloak.util.Environment.CLI_ARGS;
import static org.keycloak.util.Environment.getProviderFiles;
import javax.persistence.spi.PersistenceUnitTransactionType;
import java.io.File;
@ -78,10 +80,8 @@ import org.keycloak.configuration.Configuration;
import org.keycloak.configuration.KeycloakConfigSourceProvider;
import org.keycloak.configuration.MicroProfileConfigProvider;
import org.keycloak.connections.jpa.DefaultJpaConnectionProviderFactory;
import org.keycloak.connections.jpa.QuarkusJpaConnectionProviderFactory;
import org.keycloak.connections.jpa.updater.liquibase.LiquibaseJpaUpdaterProviderFactory;
import org.keycloak.connections.jpa.updater.liquibase.conn.DefaultLiquibaseConnectionProvider;
import org.keycloak.connections.jpa.util.JpaUtils;
import org.keycloak.protocol.ProtocolMapperSpi;
import org.keycloak.protocol.oidc.mappers.DeployedScriptOIDCProtocolMapper;
import org.keycloak.provider.EnvironmentDependentProviderFactory;
@ -106,7 +106,6 @@ import org.keycloak.representations.provider.ScriptProviderMetadata;
import org.keycloak.services.NotFoundHandler;
import org.keycloak.services.ServicesLogger;
import org.keycloak.services.health.KeycloakMetricsHandler;
import org.keycloak.services.resources.KeycloakApplication;
import org.keycloak.transaction.JBossJtaTransactionManagerLookup;
import org.keycloak.util.Environment;
import org.keycloak.util.JsonSerialization;
@ -226,7 +225,7 @@ class KeycloakProcessor {
* @param recorder the recorder
*/
@Record(ExecutionTime.STATIC_INIT)
@BuildStep
@BuildStep(onlyIf = isReAugmentation.class)
void setBuildTimeProperties(KeycloakRecorder recorder) {
Properties properties = new Properties();
@ -242,6 +241,10 @@ class KeycloakProcessor {
}
}
for (File jar : getProviderFiles()) {
properties.put(String.format("kc.provider.file.%s.last-modified", jar.getName()), String.valueOf(jar.lastModified()));
}
File file = KeycloakConfigSourceProvider.getPersistedConfigFile().toFile();
if (file.exists()) {
@ -253,15 +256,11 @@ class KeycloakProcessor {
} catch (Exception e) {
throw new RuntimeException("Failed to generate persisted.properties file", e);
}
recorder.validateAndSetBuildTimeProperties(Environment.isRebuild(), getRawValue("kc.config.args"));
recorder.showConfig();
}
private boolean isNotPersistentProperty(String name) {
// these properties are ignored from the build time properties as they are runtime-specific
return !name.startsWith("kc") || "kc.home.dir".equals(name) || "kc.config.args".equals(name);
return !name.startsWith(NS_KEYCLOAK) || "kc.home.dir".equals(name) || CLI_ARGS.equals(name);
}
/**
@ -343,14 +342,14 @@ class KeycloakProcessor {
private Map<Spi, Map<Class<? extends Provider>, Map<String, ProviderFactory>>> loadFactories(
Map<String, ProviderFactory> preConfiguredProviders) {
Config.init(new MicroProfileConfigProvider());
BuildClassLoader providerClassLoader = new BuildClassLoader();
ProviderManager pm = new ProviderManager(KeycloakDeploymentInfo.create().services(), providerClassLoader);
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
ProviderManager pm = new ProviderManager(KeycloakDeploymentInfo.create().services(), classLoader);
Map<Spi, Map<Class<? extends Provider>, Map<String, ProviderFactory>>> factories = new HashMap<>();
for (Spi spi : pm.loadSpis()) {
Map<Class<? extends Provider>, Map<String, ProviderFactory>> providers = new HashMap<>();
List<ProviderFactory> loadedFactories = new ArrayList<>(pm.load(spi));
Map<String, ProviderFactory> deployedScriptProviders = loadDeployedScriptProviders(providerClassLoader, spi);
Map<String, ProviderFactory> deployedScriptProviders = loadDeployedScriptProviders(classLoader, spi);
loadedFactories.addAll(deployedScriptProviders.values());
preConfiguredProviders.putAll(deployedScriptProviders);
@ -384,12 +383,12 @@ class KeycloakProcessor {
return factories;
}
private Map<String, ProviderFactory> loadDeployedScriptProviders(BuildClassLoader providerClassLoader, Spi spi) {
private Map<String, ProviderFactory> loadDeployedScriptProviders(ClassLoader classLoader, Spi spi) {
Map<String, ProviderFactory> providers = new HashMap<>();
if (supportsDeployeableScripts(spi)) {
try {
Enumeration<URL> urls = providerClassLoader.getResources(KEYCLOAK_SCRIPTS_JSON_PATH);
Enumeration<URL> urls = classLoader.getResources(KEYCLOAK_SCRIPTS_JSON_PATH);
while (urls.hasMoreElements()) {
URL url = urls.nextElement();

View file

@ -0,0 +1,30 @@
/*
* 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.deployment;
import java.util.function.BooleanSupplier;
import org.keycloak.util.Environment;
public class isReAugmentation implements BooleanSupplier {
@Override
public boolean getAsBoolean() {
return Environment.isRebuild();
}
}

View file

@ -17,24 +17,35 @@
package org.keycloak.cli;
import static org.keycloak.cli.MainCommand.CONFIG_COMMAND;
import static org.keycloak.cli.MainCommand.START_COMMAND;
import static org.keycloak.cli.MainCommand.isStartDevCommand;
import static org.keycloak.cli.Picocli.createCommandLine;
import static org.keycloak.cli.Picocli.error;
import static org.keycloak.cli.Picocli.getCliArgs;
import static org.keycloak.cli.Picocli.parseConfigArgs;
import static org.keycloak.configuration.Configuration.getConfig;
import static org.keycloak.configuration.PropertyMappers.isBuildTimeProperty;
import static org.keycloak.util.Environment.getProfileOrDefault;
import static org.keycloak.util.Environment.isDevMode;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Properties;
import io.quarkus.runtime.Quarkus;
import org.keycloak.common.Version;
import io.quarkus.runtime.annotations.QuarkusMain;
import org.keycloak.configuration.KeycloakConfigSourceProvider;
import org.keycloak.util.Environment;
import picocli.CommandLine;
/**
@ -46,12 +57,14 @@ import picocli.CommandLine;
@QuarkusMain(name = "keycloak")
public class KeycloakMain {
public static void main(String cliArgs[]) {
public static void main(String[] args) {
System.setProperty("kc.version", Version.VERSION_KEYCLOAK);
List<String> cliArgs = new ArrayList<>(Arrays.asList(args));
System.setProperty(Environment.CLI_ARGS, parseConfigArgs(cliArgs));
if (cliArgs.length == 0) {
if (cliArgs.isEmpty()) {
// no arguments, just start the server
start(Collections.emptyList(), new PrintWriter(System.err));
start(cliArgs, new PrintWriter(System.err));
if (!isDevMode()) {
System.exit(CommandLine.ExitCode.OK);
}
@ -84,19 +97,26 @@ public class KeycloakMain {
Quarkus.waitForExit();
}
private static void parseAndRun(String[] args) {
List<String> cliArgs = new LinkedList<>(Arrays.asList(args));
private static void parseAndRun(List<String> cliArgs) {
CommandLine cmd = createCommandLine();
// set the arguments as a system property so that arguments can be mapped to their respective configuration options
System.setProperty("kc.config.args", parseConfigArgs(cliArgs));
try {
CommandLine.ParseResult result = cmd.parseArgs(cliArgs.toArray(new String[cliArgs.size()]));
CommandLine.ParseResult result = cmd.parseArgs(cliArgs.toArray(new String[0]));
if (result.hasSubcommand()) {
if (isStartDevCommand(result.subcommand().commandSpec())) {
String profile = Environment.getProfile();
if (profile == null) {
// force the server image to be set with the dev profile
Environment.forceDevProfile();
}
runReAugmentationIfNeeded(cliArgs, cmd);
}
} else if ((!result.isUsageHelpRequested() && !result.isVersionHelpRequested())) {
// if no command was set, the start command becomes the default
if (!result.hasSubcommand() && (!result.isUsageHelpRequested() && !result.isVersionHelpRequested())) {
cliArgs.add(0, "start");
cliArgs.add(0, START_COMMAND);
}
} catch (CommandLine.UnmatchedArgumentException e) {
// if no command was set but options were provided, the start command becomes the default
@ -111,10 +131,114 @@ public class KeycloakMain {
System.exit(cmd.getCommandSpec().exitCodeOnExecutionException());
}
int exitCode = cmd.execute(cliArgs.toArray(new String[cliArgs.size()]));
int exitCode = cmd.execute(cliArgs.toArray(new String[0]));
if (!isDevMode()) {
System.exit(exitCode);
}
}
private static void runReAugmentationIfNeeded(List<String> cliArgs, CommandLine cmd) {
if (Boolean.getBoolean("kc.dev.rebuild")) {
if (requiresReAugmentation(cliArgs)) {
runReAugmentation(cliArgs, cmd);
}
System.exit(cmd.getCommandSpec().exitCodeOnSuccess());
}
}
private static boolean requiresReAugmentation(List<String> cliArgs) {
if (hasConfigChanges()) {
System.out.printf("Changes detected in configuration. Updating the server image.\n");
List<String> suggestedArgs = cliArgs.subList(1, cliArgs.size());
suggestedArgs.removeAll(Arrays.asList("--verbose", "--help"));
System.out.printf("For an optional runtime and bypass this step, please run the 'config' command prior to starting the server:\n\n\t%s config %s\n",
Environment.getCommand(),
String.join(" ", suggestedArgs) + "\n");
return true;
}
return hasProviderChanges();
}
private static void runReAugmentation(List<String> cliArgs, CommandLine cmd) {
List<String> configArgsList = new ArrayList<>(cliArgs);
if (!configArgsList.get(0).startsWith("--")) {
configArgsList.remove(0);
}
configArgsList.add(0, CONFIG_COMMAND);
cmd.execute(configArgsList.toArray(new String[0]));
System.out.printf("Next time you run the server, just run:\n\n\t%s\n\n", Environment.getCommand());
}
public static boolean hasProviderChanges() {
File propertiesFile = KeycloakConfigSourceProvider.getPersistedConfigFile().toFile();
File[] providerFiles = Environment.getProviderFiles();
if (!propertiesFile.exists()) {
return providerFiles.length > 0;
}
Properties properties = new Properties();
try (InputStream is = new FileInputStream(propertiesFile)) {
properties.load(is);
} catch (Exception e) {
throw new RuntimeException("Failed to load persisted properties", e);
}
for (String key : properties.stringPropertyNames()) {
if (key.startsWith("kc.provider.file")) {
if (providerFiles.length == 0) {
return true;
}
String fileName = key.substring("kc.provider.file".length() + 1, key.lastIndexOf('.'));
String lastModified = properties.getProperty(key);
for (File file : providerFiles) {
if (file.getName().equals(fileName) && !lastModified.equals(String.valueOf(file.lastModified()))) {
return true;
}
}
return false;
}
}
return providerFiles.length > 0;
}
public static boolean hasConfigChanges() {
for (String propertyName : getConfig().getPropertyNames()) {
// only check keycloak build-time properties
if (!isBuildTimeProperty(propertyName)) {
continue;
}
// try to resolve any property set using profiles
if (propertyName.startsWith("%")) {
propertyName = propertyName.substring(propertyName.indexOf('.') + 1);
}
String currentValue = Environment.getBuiltTimeProperty(propertyName).orElse(null);
String newValue = getConfig().getConfigValue(propertyName).getValue();
if (newValue != null && !newValue.equalsIgnoreCase(currentValue)) {
// changes to a single property are enough to indicate changes to configuration
return true;
}
}
return false;
}
}

View file

@ -38,7 +38,7 @@ import picocli.CommandLine.Spec;
@Command(name = "keycloak",
usageHelpWidth = 150,
header = "Keycloak - Open Source Identity and Access Management\n\nFind more information at: https://www.keycloak.org/%n",
header = "Keycloak - Open Source Identity and Access Management%n%nFind more information at: https://www.keycloak.org/%n",
description = "Use this command-line tool to manage your Keycloak cluster%n", footerHeading = "%nUse \"${COMMAND-NAME} <command> --help\" for more information about a command.%nUse \"${COMMAND-NAME} options\" for a list of all command-line options.",
footer = "%nby Red Hat",
optionListHeading = "Configuration Options%n%n",
@ -50,6 +50,14 @@ import picocli.CommandLine.Spec;
})
public class MainCommand {
static final String START_DEV_COMMAND = "start-dev";
static final String START_COMMAND = "start";
static final String CONFIG_COMMAND = "config";
public static boolean isStartDevCommand(CommandSpec commandSpec) {
return START_DEV_COMMAND.equals(commandSpec.name());
}
@Spec
CommandSpec spec;
@ -70,7 +78,7 @@ public class MainCommand {
System.setProperty(KeycloakConfigSourceProvider.KEYCLOAK_CONFIG_FILE_PROP, path);
}
@Command(name = "config",
@Command(name = CONFIG_COMMAND,
description = "%nCreates a new server image based on the options passed to this command. Once created, configuration will be read from the server image and the server can be started without passing the same options again. Some configuration options require this command to be executed in order to actually change a configuration. For instance, the database vendor.%n",
mixinStandardHelpOptions = true,
usageHelpAutoWidth = true,
@ -104,14 +112,18 @@ public class MainCommand {
}
}
@Command(name = "start-dev",
@Command(name = START_DEV_COMMAND,
description = "%nStart the server in development mode.%n",
mixinStandardHelpOptions = true,
optionListHeading = "%nOptions%n",
parameterListHeading = "Available Commands%n")
public void startDev(@Option(names = "--verbose", description = "Print out more details when running this command.", required = false) Boolean verbose) {
setProfile("dev");
KeycloakMain.start(spec.commandLine());
Environment.forceDevProfile();
CommandLine cmd = spec.commandLine();
cmd.getOut().printf("Running the server in dev mode. DO NOT run the '%s' command in production.\n", START_DEV_COMMAND);
KeycloakMain.start(cmd);
}
@Command(name = "export",
@ -151,7 +163,7 @@ public class MainCommand {
runImportExport(ACTION_IMPORT, toDir, toFile, realm, verbose);
}
@Command(name = "start",
@Command(name = START_COMMAND,
description = "%nStart the server.%n",
mixinStandardHelpOptions = true,
usageHelpAutoWidth = true,
@ -180,7 +192,7 @@ public class MainCommand {
@CommandLine.Parameters(paramLabel = "filter", defaultValue = "none", description = "Show all configuration options. Use 'all' to show all options.") String filter,
@Option(names = "--verbose", description = "Print out more details when running this command.", required = false) Boolean verbose) {
System.setProperty("kc.show.config", filter);
KeycloakMain.start(spec.commandLine());
ShowConfigCommand.run();
}
private void runImportExport(String action, String toDir, String toFile, String realm, Boolean verbose) {

View file

@ -17,7 +17,12 @@
package org.keycloak.cli;
import static org.keycloak.cli.MainCommand.CONFIG_COMMAND;
import static org.keycloak.cli.MainCommand.START_COMMAND;
import static org.keycloak.cli.MainCommand.START_DEV_COMMAND;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
@ -42,16 +47,12 @@ final class Picocli {
CommandLine.Model.CommandSpec spec = CommandLine.Model.CommandSpec.forAnnotatedObject(new MainCommand())
.name(Environment.getCommand());
addOption(spec, "start", PropertyMappers.getRuntimeMappers());
addOption(spec, "start-dev", PropertyMappers.getRuntimeMappers());
addOption(spec, "config", PropertyMappers.getRuntimeMappers());
addOption(spec, "config", PropertyMappers.getBuiltTimeMappers());
addOption(spec.subcommands().get("config").getCommandSpec(), "--features", "Enables a group of features. Possible values are: "
+ String.join(",", Arrays.asList(Profile.Type.values()).stream().map(
type -> type.name().toLowerCase()).toArray((IntFunction<CharSequence[]>) String[]::new)));
addOption(spec, START_COMMAND, false);
addOption(spec, START_DEV_COMMAND, true);
addOption(spec, CONFIG_COMMAND, true);
for (Profile.Feature feature : Profile.Feature.values()) {
addOption(spec.subcommands().get("config").getCommandSpec(), "--features-" + feature.name().toLowerCase(),
addOption(spec.subcommands().get(CONFIG_COMMAND).getCommandSpec(), "--features-" + feature.name().toLowerCase(),
"Enables the " + feature.name() + " feature. Set enabled to enable the feature or disabled otherwise.");
}
@ -78,6 +79,7 @@ final class Picocli {
String key = iterator.next();
// TODO: ignore properties for providers for now, need to fetch them from the providers, otherwise CLI will complain about invalid options
// change this once we are able to obtain properties from providers
if (key.startsWith("--spi")) {
iterator.remove();
}
@ -93,8 +95,13 @@ final class Picocli {
return options.toString();
}
private static void addOption(CommandLine.Model.CommandSpec spec, String command, List<PropertyMapper> mappers) {
private static void addOption(CommandLine.Model.CommandSpec spec, String command, boolean includeBuildTime) {
CommandLine.Model.CommandSpec commandSpec = spec.subcommands().get(command).getCommandSpec();
List<PropertyMapper> mappers = new ArrayList<>(PropertyMappers.getRuntimeMappers());
if (includeBuildTime) {
mappers.addAll(PropertyMappers.getBuiltTimeMappers());
}
for (PropertyMapper mapper : mappers) {
String name = "--" + PropertyMappers.toCLIFormat(mapper.getFrom()).substring(3);
@ -106,6 +113,10 @@ final class Picocli {
addOption(commandSpec, name, description);
}
addOption(commandSpec, "--features", "Enables a group of features. Possible values are: "
+ String.join(",", Arrays.stream(Profile.Type.values()).map(
type -> type.name().toLowerCase()).toArray((IntFunction<CharSequence[]>) String[]::new)));
}
private static void addOption(CommandLine.Model.CommandSpec commandSpec, String name, String description) {
@ -129,7 +140,7 @@ final class Picocli {
logError(errorWriter, "ERROR: " + message);
if (throwable != null) {
boolean verbose = cliArgs.stream().anyMatch((arg) -> "--verbose".equals(arg));
boolean verbose = cliArgs.stream().anyMatch("--verbose"::equals);
if (throwable instanceof InitializationException) {
InitializationException initializationException = (InitializationException) throwable;

View file

@ -122,12 +122,12 @@ public final class ShowConfigCommand {
return PersistedConfigSource.NAME.equals(configValue.getConfigSourceName());
}
})
.filter(property -> filterByGroup(property))
.filter(ShowConfigCommand::filterByGroup)
.collect(Collectors.groupingBy(ShowConfigCommand::groupProperties, Collectors.toSet()))
.forEach(new BiConsumer<String, Set<String>>() {
@Override
public void accept(String group, Set<String> propertyNames) {
properties.computeIfAbsent(group, (name) -> new HashSet<>()).addAll(propertyNames);
properties.computeIfAbsent(group, name -> new HashSet<>()).addAll(propertyNames);
}
});

View file

@ -163,8 +163,20 @@ public final class PropertyMappers {
}
public static boolean isBuildTimeProperty(String name) {
return PropertyMapper.MAPPERS.entrySet().stream()
.anyMatch(entry -> entry.getValue().getFrom().equals(name) && entry.getValue().isBuildTime());
if ("kc.features".equals(name)) {
return true;
}
return name.startsWith(MicroProfileConfigProvider.NS_KEYCLOAK_PREFIX)
&& PropertyMapper.MAPPERS.entrySet().stream()
.anyMatch(entry -> entry.getValue().getFrom().equals(name) && entry.getValue().isBuildTime())
&& !"kc.version".equals(name)
&& !Environment.CLI_ARGS.equals(name)
&& !"kc.home.dir".equals(name)
&& !"kc.config.file".equals(name)
&& !"kc.profile".equals(name)
&& !"kc.show.config".equals(name)
&& !"kc.show.config.runtime".equals(name)
&& !PropertyMappers.toCLIFormat("kc.config.file").equals(name);
}
public static String toCLIFormat(String name) {

View file

@ -84,106 +84,6 @@ public class KeycloakRecorder {
QuarkusKeycloakSessionFactory.setInstance(new QuarkusKeycloakSessionFactory(factories, defaultProviders, preConfiguredProviders, reaugmented));
}
/**
* <p>Validate the build time properties with any property passed during runtime in order to advertise any difference with the
* server image state.
*
* <p>This method also keep the build time properties available at runtime.
*
*
* @param buildTimeProperties the build time properties set when running the last re-augmentation
* @param rebuild indicates whether or not the server was re-augmented
* @param configArgs the configuration args if provided when the server was re-augmented
*/
public void validateAndSetBuildTimeProperties(Boolean rebuild, String configArgs) {
String configHelpText = configArgs;
for (String propertyName : getConfig().getPropertyNames()) {
// we should only validate if there is a server image and if the property is a runtime property
if (!shouldValidate(propertyName, rebuild)) {
continue;
}
// try to resolve any property set using profiles
if (propertyName.startsWith("%")) {
propertyName = propertyName.substring(propertyName.indexOf('.') + 1);
}
String buildValue = Environment.getBuiltTimeProperty(propertyName).orElse(null);
ConfigValue value = getConfig().getConfigValue(propertyName);
if (value.getValue() != null && !value.getValue().equalsIgnoreCase(buildValue)) {
if (configHelpText != null) {
String cliNameFormat = PropertyMappers.toCLIFormat(propertyName);
if (buildValue != null) {
String currentProp = "--" + cliNameFormat.substring(3) + "=" + buildValue;
String newProp = "--" + cliNameFormat.substring(3) + "=" + value.getValue();
if (configHelpText.contains(currentProp)) {
LOGGER.warnf("The new value [%s] of the property [%s] in [%s] differs from the value [%s] set into the server image. The new value will override the value set into the server image.",
value.getValue(), propertyName, value.getConfigSourceName(), buildValue);
configHelpText = configHelpText.replaceAll(currentProp, newProp);
} else if (!configHelpText
.contains("--" + cliNameFormat.substring(3))) {
LOGGER.warnf("The new value [%s] of the property [%s] in [%s] differs from the value [%s] set into the server image. The new value will override the value set into the server image.",
value.getValue(), propertyName, value.getConfigSourceName(), buildValue);
configHelpText += " " + newProp;
}
} else {
String finalPropertyName = propertyName;
if (!StreamSupport.stream(getConfig().getPropertyNames().spliterator(), false)
.filter(new Predicate<String>() {
@Override
public boolean test(String propertyName) {
ConfigValue configValue = getConfigValue(propertyName);
if (configValue == null) {
return false;
}
return PersistedConfigSource.NAME.equals(configValue.getSourceName());
}
})
.anyMatch(new Predicate<String>() {
@Override
public boolean test(String propertyName) {
return PropertyMappers.canonicalFormat(finalPropertyName)
.equalsIgnoreCase(PropertyMappers.canonicalFormat(propertyName));
}
})) {
String prop = "--" + cliNameFormat.substring(3) + "=" + value.getValue();
if (!configHelpText.contains(prop)) {
LOGGER.warnf("New property [%s] set with value [%s] in [%s]. This property is not persisted into the server image.",
propertyName, value.getValue(), value.getConfigSourceName(), buildValue);
configHelpText += " " + prop;
}
}
}
}
}
}
if (configArgs != null && !configArgs.equals(configHelpText)) {
LOGGER.warnf("Please, run the 'config' command if you want to persist the new configuration into the server image:\n\n\t%s config %s\n", Environment.getCommand(), String.join(" ", configHelpText.split(",")));
}
}
private boolean shouldValidate(String name, boolean rebuild) {
return rebuild && name.contains(MicroProfileConfigProvider.NS_KEYCLOAK_PREFIX)
&& (!PropertyMappers.isBuildTimeProperty(name)
&& !"kc.version".equals(name)
&& !"kc.config.args".equals(name)
&& !"kc.home.dir".equals(name)
&& !"kc.config.file".equals(name)
&& !"kc.profile".equals(name)
&& !"kc.show.config".equals(name)
&& !"kc.show.config.runtime".equals(name)
&& !PropertyMappers.toCLIFormat("kc.config.file").equals(name));
}
/**
* This method should be executed during static init so that the configuration is printed (if demanded) based on the properties
* set from the previous reaugmentation

View file

@ -17,6 +17,10 @@
package org.keycloak.util;
import java.io.File;
import java.io.FilenameFilter;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Optional;
import io.quarkus.runtime.LaunchMode;
@ -27,15 +31,36 @@ import org.keycloak.configuration.Configuration;
public final class Environment {
public static final String IMPORT_EXPORT_MODE = "import_export";
public static final String CLI_ARGS = "kc.config.args";
public static Boolean isRebuild() {
return Boolean.valueOf(System.getProperty("quarkus.launch.rebuild"));
return Boolean.getBoolean("quarkus.launch.rebuild");
}
public static String getHomeDir() {
return System.getProperty("kc.home.dir");
}
public static Path getHomePath() {
String homeDir = getHomeDir();
if (homeDir != null) {
return Paths.get(homeDir);
}
return null;
}
public static Path getProvidersPath() {
Path homePath = Environment.getHomePath();
if (homePath != null) {
return homePath.resolve("providers");
}
return null;
}
public static String getCommand() {
String homeDir = getHomeDir();
@ -50,7 +75,7 @@ public final class Environment {
}
public static String getConfigArgs() {
return System.getProperty("kc.config.args");
return System.getProperty(CLI_ARGS);
}
public static String getProfile() {
@ -99,4 +124,30 @@ public final class Environment {
public static boolean isWindows() {
return SystemUtils.IS_OS_WINDOWS;
}
public static void forceDevProfile() {
System.setProperty("kc.profile", "dev");
System.setProperty("quarkus.profile", "dev");
}
public static File[] getProviderFiles() {
Path providersPath = Environment.getProvidersPath();
if (providersPath == null) {
return new File[] {};
}
File providersDir = providersPath.toFile();
if (!providersDir.exists() || !providersDir.isDirectory()) {
throw new RuntimeException("The 'providers' directory does not exist or is not a valid directory.");
}
return providersDir.listFiles(new FilenameFilter() {
@Override
public boolean accept(File dir, String name) {
return name.endsWith(".jar");
}
});
}
}

View file

@ -18,6 +18,7 @@
package org.keycloak.provider.quarkus;
import static org.junit.Assert.assertEquals;
import static org.keycloak.util.Environment.CLI_ARGS;
import java.io.File;
import java.lang.reflect.Field;
@ -123,7 +124,7 @@ public class ConfigurationTest {
@Test
public void testCLIPriorityOverSysProp() {
System.setProperty("kc.spi.hostname.default.frontend-url", "http://propvar.com");
System.setProperty("kc.config.args", "--spi-hostname-default-frontend-url=http://cli.com");
System.setProperty(CLI_ARGS, "--spi-hostname-default-frontend-url=http://cli.com");
assertEquals("http://cli.com", initConfig("hostname", "default").get("frontendUrl"));
}
@ -152,29 +153,29 @@ public class ConfigurationTest {
@Test
public void testCommandLineArguments() {
System.setProperty("kc.config.args", "--spi-hostname-default-frontend-url=http://fromargs.com,--no-ssl");
System.setProperty(CLI_ARGS, "--spi-hostname-default-frontend-url=http://fromargs.com,--no-ssl");
assertEquals("http://fromargs.com", initConfig("hostname", "default").get("frontendUrl"));
}
@Test
public void testSpiConfigurationUsingCommandLineArguments() {
System.setProperty("kc.config.args", "--spi-hostname-default-frontend-url=http://spifull.com");
System.setProperty(CLI_ARGS, "--spi-hostname-default-frontend-url=http://spifull.com");
assertEquals("http://spifull.com", initConfig("hostname", "default").get("frontendUrl"));
// test multi-word SPI names using camel cases
System.setProperty("kc.config.args", "--spi-action-token-handler-verify-email-some-property=test");
System.setProperty(CLI_ARGS, "--spi-action-token-handler-verify-email-some-property=test");
assertEquals("test", initConfig("action-token-handler", "verify-email").get("some-property"));
System.setProperty("kc.config.args", "--spi-action-token-handler-verify-email-some-property=test");
System.setProperty(CLI_ARGS, "--spi-action-token-handler-verify-email-some-property=test");
assertEquals("test", initConfig("actionTokenHandler", "verifyEmail").get("someProperty"));
// test multi-word SPI names using slashes
System.setProperty("kc.config.args", "--spi-client-registration-openid-connect-static-jwk-url=http://c.jwk.url");
System.setProperty(CLI_ARGS, "--spi-client-registration-openid-connect-static-jwk-url=http://c.jwk.url");
assertEquals("http://c.jwk.url", initConfig("client-registration", "openid-connect").get("static-jwk-url"));
}
@Test
public void testPropertyMapping() {
System.setProperty("kc.config.args", "--db=mariadb,--db-url=jdbc:mariadb://localhost/keycloak");
System.setProperty(CLI_ARGS, "--db=mariadb,--db-url=jdbc:mariadb://localhost/keycloak");
SmallRyeConfig config = createConfig();
assertEquals(MariaDBDialect.class.getName(), config.getConfigValue("quarkus.hibernate-orm.dialect").getValue());
assertEquals("jdbc:mariadb://localhost/keycloak", config.getConfigValue("quarkus.datasource.jdbc.url").getValue());
@ -182,7 +183,7 @@ public class ConfigurationTest {
@Test
public void testDatabaseUrlProperties() {
System.setProperty("kc.config.args", "--db=mariadb,--db-url=jdbc:mariadb:aurora://foo/bar?a=1&b=2");
System.setProperty(CLI_ARGS, "--db=mariadb,--db-url=jdbc:mariadb:aurora://foo/bar?a=1&b=2");
SmallRyeConfig config = createConfig();
assertEquals(MariaDBDialect.class.getName(), config.getConfigValue("quarkus.hibernate-orm.dialect").getValue());
assertEquals("jdbc:mariadb:aurora://foo/bar?a=1&b=2", config.getConfigValue("quarkus.datasource.jdbc.url").getValue());
@ -190,12 +191,12 @@ public class ConfigurationTest {
@Test
public void testDatabaseDefaults() {
System.setProperty("kc.config.args", "--db=h2-file");
System.setProperty(CLI_ARGS, "--db=h2-file");
SmallRyeConfig config = createConfig();
assertEquals(QuarkusH2Dialect.class.getName(), config.getConfigValue("quarkus.hibernate-orm.dialect").getValue());
assertEquals("jdbc:h2:file:~/data/keycloakdb;;AUTO_SERVER=TRUE", config.getConfigValue("quarkus.datasource.jdbc.url").getValue());
System.setProperty("kc.config.args", "--db=h2-mem");
System.setProperty(CLI_ARGS, "--db=h2-mem");
config = createConfig();
assertEquals(QuarkusH2Dialect.class.getName(), config.getConfigValue("quarkus.hibernate-orm.dialect").getValue());
assertEquals("jdbc:h2:mem:keycloakdb", config.getConfigValue("quarkus.datasource.jdbc.url").getValue());
@ -204,7 +205,7 @@ public class ConfigurationTest {
@Test
public void testDatabaseKindProperties() {
System.setProperty("kc.config.args", "--db=postgres-10,--db-url=jdbc:postgresql://localhost/keycloak");
System.setProperty(CLI_ARGS, "--db=postgres-10,--db-url=jdbc:postgresql://localhost/keycloak");
SmallRyeConfig config = createConfig();
assertEquals("io.quarkus.hibernate.orm.runtime.dialect.QuarkusPostgreSQL10Dialect",
config.getConfigValue("quarkus.hibernate-orm.dialect").getValue());
@ -216,13 +217,13 @@ public class ConfigurationTest {
public void testDatabaseProperties() {
System.setProperty("kc.db.url.properties", ";;test=test;test1=test1");
System.setProperty("kc.db.url.path", "test-dir");
System.setProperty("kc.config.args", "--db=h2-file");
System.setProperty(CLI_ARGS, "--db=h2-file");
SmallRyeConfig config = createConfig();
assertEquals(QuarkusH2Dialect.class.getName(), config.getConfigValue("quarkus.hibernate-orm.dialect").getValue());
assertEquals("jdbc:h2:file:test-dir" + File.separator + "data" + File.separator + "keycloakdb;;test=test;test1=test1", config.getConfigValue("quarkus.datasource.jdbc.url").getValue());
System.setProperty("kc.db.url.properties", "?test=test&test1=test1");
System.setProperty("kc.config.args", "--db=mariadb");
System.setProperty(CLI_ARGS, "--db=mariadb");
config = createConfig();
assertEquals("jdbc:mariadb://localhost/keycloak?test=test&test1=test1", config.getConfigValue("quarkus.datasource.jdbc.url").getValue());
}
@ -256,13 +257,13 @@ public class ConfigurationTest {
// If explicitly set, then it is always used regardless of the profile
System.clearProperty("kc.profile");
System.setProperty("kc.config.args", "--cluster=foo");
System.setProperty(CLI_ARGS, "--cluster=foo");
Assert.assertEquals("cluster-foo.xml", initConfig("connectionsInfinispan", "quarkus").get("configFile"));
System.setProperty("kc.profile", "dev");
Assert.assertEquals("cluster-foo.xml", initConfig("connectionsInfinispan", "quarkus").get("configFile"));
System.setProperty("kc.config.args", "--cluster-stack=foo");
System.setProperty(CLI_ARGS, "--cluster-stack=foo");
Assert.assertEquals("foo", initConfig("connectionsInfinispan", "quarkus").get("stack"));
}