From 36706c7bd1c6a7439096a81563640955028be750 Mon Sep 17 00:00:00 2001 From: Pedro Igor Date: Tue, 21 Sep 2021 19:53:52 -0300 Subject: [PATCH] [KEYCLOAK-19306] - Automatic re-augmentation --- .../server-x-dist/src/main/content/bin/kc.sh | 13 +- .../quarkus/deployment/BuildClassLoader.java | 61 ------- .../quarkus/deployment/KeycloakProcessor.java | 29 ++-- .../quarkus/deployment/isReAugmentation.java | 30 ++++ .../java/org/keycloak/cli/KeycloakMain.java | 154 ++++++++++++++++-- .../java/org/keycloak/cli/MainCommand.java | 44 +++-- .../main/java/org/keycloak/cli/Picocli.java | 31 ++-- .../org/keycloak/cli/ShowConfigCommand.java | 4 +- .../configuration/PropertyMappers.java | 16 +- .../keycloak/quarkus/KeycloakRecorder.java | 100 ------------ .../java/org/keycloak/util/Environment.java | 55 ++++++- .../provider/quarkus/ConfigurationTest.java | 31 ++-- 12 files changed, 328 insertions(+), 240 deletions(-) delete mode 100644 quarkus/deployment/src/main/java/org/keycloak/quarkus/deployment/BuildClassLoader.java create mode 100644 quarkus/deployment/src/main/java/org/keycloak/quarkus/deployment/isReAugmentation.java diff --git a/distribution/server-x-dist/src/main/content/bin/kc.sh b/distribution/server-x-dist/src/main/content/bin/kc.sh index 7fd84add71..856bdf00ea 100644 --- a/distribution/server-x-dist/src/main/content/bin/kc.sh +++ b/distribution/server-x-dist/src/main/content/bin/kc.sh @@ -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#?} \ No newline at end of file +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 \ No newline at end of file diff --git a/quarkus/deployment/src/main/java/org/keycloak/quarkus/deployment/BuildClassLoader.java b/quarkus/deployment/src/main/java/org/keycloak/quarkus/deployment/BuildClassLoader.java deleted file mode 100644 index 7fe80133d0..0000000000 --- a/quarkus/deployment/src/main/java/org/keycloak/quarkus/deployment/BuildClassLoader.java +++ /dev/null @@ -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"); - } - - } -} 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 9deabdd26a..0a87bdfe74 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 @@ -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, Map>> loadFactories( Map 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, Map>> factories = new HashMap<>(); for (Spi spi : pm.loadSpis()) { Map, Map> providers = new HashMap<>(); List loadedFactories = new ArrayList<>(pm.load(spi)); - Map deployedScriptProviders = loadDeployedScriptProviders(providerClassLoader, spi); + Map deployedScriptProviders = loadDeployedScriptProviders(classLoader, spi); loadedFactories.addAll(deployedScriptProviders.values()); preConfiguredProviders.putAll(deployedScriptProviders); @@ -384,12 +383,12 @@ class KeycloakProcessor { return factories; } - private Map loadDeployedScriptProviders(BuildClassLoader providerClassLoader, Spi spi) { + private Map loadDeployedScriptProviders(ClassLoader classLoader, Spi spi) { Map providers = new HashMap<>(); if (supportsDeployeableScripts(spi)) { try { - Enumeration urls = providerClassLoader.getResources(KEYCLOAK_SCRIPTS_JSON_PATH); + Enumeration urls = classLoader.getResources(KEYCLOAK_SCRIPTS_JSON_PATH); while (urls.hasMoreElements()) { URL url = urls.nextElement(); diff --git a/quarkus/deployment/src/main/java/org/keycloak/quarkus/deployment/isReAugmentation.java b/quarkus/deployment/src/main/java/org/keycloak/quarkus/deployment/isReAugmentation.java new file mode 100644 index 0000000000..d0c416bdcb --- /dev/null +++ b/quarkus/deployment/src/main/java/org/keycloak/quarkus/deployment/isReAugmentation.java @@ -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(); + } + +} diff --git a/quarkus/runtime/src/main/java/org/keycloak/cli/KeycloakMain.java b/quarkus/runtime/src/main/java/org/keycloak/cli/KeycloakMain.java index 983e15971b..83ec9f6da9 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/cli/KeycloakMain.java +++ b/quarkus/runtime/src/main/java/org/keycloak/cli/KeycloakMain.java @@ -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 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 cliArgs = new LinkedList<>(Arrays.asList(args)); + private static void parseAndRun(List 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 no command was set, the start command becomes the default - if (!result.hasSubcommand() && (!result.isUsageHelpRequested() && !result.isVersionHelpRequested())) { - cliArgs.add(0, "start"); + 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 + 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 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 cliArgs) { + if (hasConfigChanges()) { + System.out.printf("Changes detected in configuration. Updating the server image.\n"); + + List 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 cliArgs, CommandLine cmd) { + List 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; + } + } diff --git a/quarkus/runtime/src/main/java/org/keycloak/cli/MainCommand.java b/quarkus/runtime/src/main/java/org/keycloak/cli/MainCommand.java index fff586df5c..402076d236 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/cli/MainCommand.java +++ b/quarkus/runtime/src/main/java/org/keycloak/cli/MainCommand.java @@ -38,11 +38,11 @@ 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", - description = "Use this command-line tool to manage your Keycloak cluster%n", footerHeading = "%nUse \"${COMMAND-NAME} --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", - commandListHeading = "%nCommands%n%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} --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", + commandListHeading = "%nCommands%n%n", version = { "Keycloak ${sys:kc.version}", "JVM: ${java.version} (${java.vendor} ${java.vm.name} ${java.vm.version})", @@ -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,8 +78,8 @@ public class MainCommand { System.setProperty(KeycloakConfigSourceProvider.KEYCLOAK_CONFIG_FILE_PROP, path); } - @Command(name = "config", - 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", + @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, optionListHeading = "%nOptions%n", @@ -104,18 +112,22 @@ public class MainCommand { } } - @Command(name = "start-dev", - description = "%nStart the server in development mode.%n", + @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", - description = "%nExport data from realms to a file or directory.%n", + description = "%nExport data from realms to a file or directory.%n", mixinStandardHelpOptions = true, showDefaultValues = true, optionListHeading = "%nOptions%n", @@ -136,7 +148,7 @@ public class MainCommand { } @Command(name = "import", - description = "%nImport data from a directory or a file.%n", + description = "%nImport data from a directory or a file.%n", mixinStandardHelpOptions = true, showDefaultValues = true, optionListHeading = "%nOptions%n", @@ -151,8 +163,8 @@ public class MainCommand { runImportExport(ACTION_IMPORT, toDir, toFile, realm, verbose); } - @Command(name = "start", - description = "%nStart the server.%n", + @Command(name = START_COMMAND, + description = "%nStart the server.%n", mixinStandardHelpOptions = true, usageHelpAutoWidth = true, optionListHeading = "%nOptions%n", @@ -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) { diff --git a/quarkus/runtime/src/main/java/org/keycloak/cli/Picocli.java b/quarkus/runtime/src/main/java/org/keycloak/cli/Picocli.java index 5435364cd8..a194a131a1 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/cli/Picocli.java +++ b/quarkus/runtime/src/main/java/org/keycloak/cli/Picocli.java @@ -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) 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 mappers) { + private static void addOption(CommandLine.Model.CommandSpec spec, String command, boolean includeBuildTime) { CommandLine.Model.CommandSpec commandSpec = spec.subcommands().get(command).getCommandSpec(); + List 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) 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; diff --git a/quarkus/runtime/src/main/java/org/keycloak/cli/ShowConfigCommand.java b/quarkus/runtime/src/main/java/org/keycloak/cli/ShowConfigCommand.java index 75c819e779..ba031144ad 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/cli/ShowConfigCommand.java +++ b/quarkus/runtime/src/main/java/org/keycloak/cli/ShowConfigCommand.java @@ -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>() { @Override public void accept(String group, Set propertyNames) { - properties.computeIfAbsent(group, (name) -> new HashSet<>()).addAll(propertyNames); + properties.computeIfAbsent(group, name -> new HashSet<>()).addAll(propertyNames); } }); diff --git a/quarkus/runtime/src/main/java/org/keycloak/configuration/PropertyMappers.java b/quarkus/runtime/src/main/java/org/keycloak/configuration/PropertyMappers.java index 64cd7942af..6a15175b0c 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/configuration/PropertyMappers.java +++ b/quarkus/runtime/src/main/java/org/keycloak/configuration/PropertyMappers.java @@ -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) { diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/KeycloakRecorder.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/KeycloakRecorder.java index 244eb93610..bbd8d55a61 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/KeycloakRecorder.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/KeycloakRecorder.java @@ -84,106 +84,6 @@ public class KeycloakRecorder { QuarkusKeycloakSessionFactory.setInstance(new QuarkusKeycloakSessionFactory(factories, defaultProviders, preConfiguredProviders, reaugmented)); } - /** - *

Validate the build time properties with any property passed during runtime in order to advertise any difference with the - * server image state. - * - *

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() { - @Override - public boolean test(String propertyName) { - ConfigValue configValue = getConfigValue(propertyName); - - if (configValue == null) { - return false; - } - - return PersistedConfigSource.NAME.equals(configValue.getSourceName()); - } - }) - .anyMatch(new Predicate() { - @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 diff --git a/quarkus/runtime/src/main/java/org/keycloak/util/Environment.java b/quarkus/runtime/src/main/java/org/keycloak/util/Environment.java index af9ad216e0..b1d25b856b 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/util/Environment.java +++ b/quarkus/runtime/src/main/java/org/keycloak/util/Environment.java @@ -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"); + } + }); + } } diff --git a/quarkus/runtime/src/test/java/org/keycloak/provider/quarkus/ConfigurationTest.java b/quarkus/runtime/src/test/java/org/keycloak/provider/quarkus/ConfigurationTest.java index 1a1dc5e38b..8cdb4497ae 100644 --- a/quarkus/runtime/src/test/java/org/keycloak/provider/quarkus/ConfigurationTest.java +++ b/quarkus/runtime/src/test/java/org/keycloak/provider/quarkus/ConfigurationTest.java @@ -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")); }