diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/Environment.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/Environment.java index 6368ab8965..7d33289c91 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/Environment.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/Environment.java @@ -17,7 +17,7 @@ package org.keycloak.quarkus.runtime; -import static org.keycloak.quarkus.runtime.configuration.PropertyMappers.getBuiltTimeProperty; +import static org.keycloak.quarkus.runtime.configuration.Configuration.getBuiltTimeProperty; import java.io.File; import java.io.FilenameFilter; diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/KeycloakMain.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/KeycloakMain.java index 7378252350..d135ad3d31 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/KeycloakMain.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/KeycloakMain.java @@ -51,9 +51,8 @@ public class KeycloakMain implements QuarkusApplication { System.setProperty(Environment.CLI_ARGS, Picocli.parseConfigArgs(cliArgs)); if (cliArgs.isEmpty()) { - // no arguments, just start the server without running picocli - start(cliArgs, new PrintWriter(System.err)); - return; + // default to show help message + cliArgs.add("-h"); } // parse arguments and execute any of the configured commands 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 854a537ffa..54bb70dbf3 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 @@ -21,6 +21,7 @@ import static org.keycloak.quarkus.runtime.configuration.Configuration.getBuiltT import java.util.List; import java.util.Map; +import java.util.Optional; import org.jboss.logging.Logger; import org.keycloak.common.Profile; @@ -86,14 +87,14 @@ public class KeycloakRecorder { feature = "kc.features"; } - String value = getBuiltTimeProperty(feature); + Optional value = getBuiltTimeProperty(feature); - if (value == null) { + if (value.isEmpty()) { value = getBuiltTimeProperty(feature.replaceAll("\\.features\\.", "\\.features-")); } - if (value != null) { - return value; + if (value.isPresent()) { + return value.get(); } return Configuration.getRawValue(feature); diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/Picocli.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/Picocli.java index dfc36e0212..1764c82d1a 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/Picocli.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/Picocli.java @@ -18,12 +18,13 @@ package org.keycloak.quarkus.runtime.cli; import static java.util.Arrays.asList; -import static org.keycloak.quarkus.runtime.cli.command.AbstractStartCommand.AUTO_BUILD_OPTION; +import static org.keycloak.quarkus.runtime.cli.command.AbstractStartCommand.AUTO_BUILD_OPTION_LONG; +import static org.keycloak.quarkus.runtime.cli.command.AbstractStartCommand.AUTO_BUILD_OPTION_SHORT; +import static org.keycloak.quarkus.runtime.configuration.Configuration.getBuiltTimeProperty; import static org.keycloak.quarkus.runtime.configuration.Configuration.getConfig; -import static org.keycloak.quarkus.runtime.configuration.PropertyMappers.getBuiltTimeProperty; -import static org.keycloak.quarkus.runtime.configuration.PropertyMappers.getRuntimeProperty; -import static org.keycloak.quarkus.runtime.configuration.PropertyMappers.isBuildTimeProperty; import static org.keycloak.quarkus.runtime.Environment.isDevMode; +import static org.keycloak.quarkus.runtime.configuration.Configuration.getRuntimeProperty; +import static org.keycloak.quarkus.runtime.configuration.mappers.PropertyMappers.isBuildTimeProperty; import static org.keycloak.utils.StringUtil.isNotBlank; import static picocli.CommandLine.Model.UsageMessageSpec.SECTION_KEY_COMMAND_LIST; @@ -32,16 +33,7 @@ import java.io.FileInputStream; import java.io.InputStream; import java.io.PrintWriter; import java.nio.file.FileSystemException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Properties; -import java.util.Set; -import java.util.function.IntFunction; +import java.util.*; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -51,10 +43,11 @@ import org.keycloak.quarkus.runtime.cli.command.Main; import org.keycloak.quarkus.runtime.cli.command.Start; import org.keycloak.quarkus.runtime.cli.command.StartDev; import org.keycloak.common.Profile; +import org.keycloak.quarkus.runtime.configuration.mappers.ConfigCategory; +import org.keycloak.quarkus.runtime.configuration.mappers.PropertyMappers; import org.keycloak.quarkus.runtime.configuration.KeycloakConfigSourceProvider; -import org.keycloak.quarkus.runtime.configuration.Messages; -import org.keycloak.quarkus.runtime.configuration.PropertyMapper; -import org.keycloak.quarkus.runtime.configuration.PropertyMappers; +import org.keycloak.quarkus.runtime.configuration.mappers.Messages; +import org.keycloak.quarkus.runtime.configuration.mappers.PropertyMapper; import org.keycloak.platform.Platform; import org.keycloak.quarkus.runtime.InitializationException; import org.keycloak.quarkus.runtime.integration.QuarkusPlatform; @@ -66,6 +59,7 @@ import picocli.CommandLine.Model.CommandSpec; import picocli.CommandLine.UnmatchedArgumentException; import picocli.CommandLine.ParseResult; import picocli.CommandLine.Model.OptionSpec; +import picocli.CommandLine.Model.ArgGroupSpec; public final class Picocli { @@ -117,15 +111,19 @@ public final class Picocli { } private static void runReAugmentationIfNeeded(List cliArgs, CommandLine cmd) { - if (cliArgs.contains(AUTO_BUILD_OPTION)) { + if (hasAutoBuildOption(cliArgs) && !(cliArgs.contains("--help") || cliArgs.contains("-h"))) { if (requiresReAugmentation(cmd)) { runReAugmentation(cliArgs, cmd); } - - if (Boolean.getBoolean("kc.config.rebuild-and-exit")) { - System.exit(cmd.getCommandSpec().exitCodeOnSuccess()); - } } + + if (Boolean.getBoolean("kc.config.rebuild-and-exit")) { + System.exit(cmd.getCommandSpec().exitCodeOnSuccess()); + } + } + + private static boolean hasAutoBuildOption(List cliArgs) { + return cliArgs.contains(AUTO_BUILD_OPTION_LONG) || cliArgs.contains(AUTO_BUILD_OPTION_SHORT); } private static boolean requiresReAugmentation(CommandLine cmd) { @@ -159,12 +157,13 @@ public final class Picocli { configArgsList.remove(0); } - configArgsList.remove("--auto-build"); + configArgsList.remove(AUTO_BUILD_OPTION_LONG); + configArgsList.remove(AUTO_BUILD_OPTION_SHORT); configArgsList.add(0, Build.NAME); cmd.execute(configArgsList.toArray(new String[0])); - cmd.getOut().printf("Next time you run the server, just run:%n%n\t%s%n%n", Environment.getCommand()); + cmd.getOut().printf("Next time you run the server, just run:%n%n\t%s %s%n%n", Environment.getCommand(), Start.NAME); } private static boolean hasProviderChanges() { @@ -257,25 +256,25 @@ public final class Picocli { private static CommandLine createCommandLine(List cliArgs) { CommandSpec spec = CommandSpec.forAnnotatedObject(new Main()) .name(Environment.getCommand()); + boolean isStartCommand = cliArgs.size() == 1 && cliArgs.contains(Start.NAME); - spec.usageMessage().width(100); + // avoid unnecessary processing when starting the server + if (!isStartCommand) { + spec.usageMessage().width(100); - boolean addBuildOptionsToStartCommand = cliArgs.contains(AUTO_BUILD_OPTION); - - addOption(spec, Start.NAME, addBuildOptionsToStartCommand); - addOption(spec, StartDev.NAME, true); - addOption(spec, Build.NAME, true); - - for (Profile.Feature feature : Profile.Feature.values()) { - addOption(spec.subcommands().get(Build.NAME).getCommandSpec(), "--features-" + feature.name().toLowerCase(), - "Enables the " + feature.name() + " feature. Set enabled to enable the feature or disabled otherwise.", null); + addOption(spec, Start.NAME, hasAutoBuildOption(cliArgs)); + addOption(spec, StartDev.NAME, true); + addOption(spec, Build.NAME, true); } - + CommandLine cmd = new CommandLine(spec); - cmd.getHelpSectionMap().put(SECTION_KEY_COMMAND_LIST, new SubCommandListRenderer()); cmd.setExecutionExceptionHandler(new ExecutionExceptionHandler()); + if (!isStartCommand) { + cmd.getHelpSectionMap().put(SECTION_KEY_COMMAND_LIST, new SubCommandListRenderer()); + } + return cmd; } @@ -283,7 +282,7 @@ public final class Picocli { StringBuilder options = new StringBuilder(); Iterator iterator = argsList.iterator(); boolean expectValue = false; - List ignoredArgs = asList("--verbose", "-v", "--help", "-h", AUTO_BUILD_OPTION); + List ignoredArgs = asList("--verbose", "-v", "--help", "-h", AUTO_BUILD_OPTION_LONG, AUTO_BUILD_OPTION_SHORT); while (iterator.hasNext()) { String key = iterator.next(); @@ -323,37 +322,78 @@ public final class Picocli { List mappers = new ArrayList<>(PropertyMappers.getRuntimeMappers()); if (includeBuildTime) { - mappers.addAll(PropertyMappers.getBuiltTimeMappers()); + mappers.addAll(PropertyMappers.getBuildTimeMappers()); + addFeatureOptions(commandSpec); } - for (PropertyMapper mapper : mappers) { - String name = ARG_PREFIX + PropertyMappers.toCLIFormat(mapper.getFrom()).substring(3); - String description = mapper.getDescription(); + addMappedOptionsToArgGroups(commandSpec, mappers); + } - if (description == null || commandSpec.optionsMap().containsKey(name) - || name.endsWith(ARG_PART_SEPARATOR)) { + private static void addFeatureOptions(CommandSpec commandSpec) { + ArgGroupSpec.Builder featureGroupBuilder = ArgGroupSpec.builder() + .heading(ConfigCategory.FEATURE.getHeading()) + .order(ConfigCategory.FEATURE.getOrder()) + .validate(false); + + Set featuresExpectedValues = Arrays.stream(Profile.Type.values()).map(type -> type.name().toLowerCase()).collect(Collectors.toSet()); + + featureGroupBuilder.addArg(OptionSpec.builder(new String[] {"-ft", "--features"}) + .description("Enables a group of features. Possible values are: " + String.join(",", featuresExpectedValues)) + .paramLabel("") + .completionCandidates(featuresExpectedValues) + .type(String.class) + .build()); + + for (Profile.Feature feature : Profile.Feature.values()) { + featureGroupBuilder.addArg(OptionSpec.builder("--features-" + feature.name().toLowerCase()) + .description("Enables the " + feature.name() + " feature.") + .paramLabel("[enabled|disabled]") + .type(String.class) + .completionCandidates(Arrays.asList("enabled", "disabled")) + .build()); + } + + commandSpec.addArgGroup(featureGroupBuilder.build()); + } + + private static void addMappedOptionsToArgGroups(CommandSpec cSpec, List propertyMappers) { + for(ConfigCategory category : ConfigCategory.values()) { + List mappersInCategory = propertyMappers.stream() + .filter(m -> category.equals(m.getCategory())) + .collect(Collectors.toList()); + + if(mappersInCategory.isEmpty()){ + //picocli raises an exception when an ArgGroup is empty, so ignore it when no mappings found for a category. continue; } - addOption(commandSpec, name, description, mapper); + ArgGroupSpec.Builder argGroupBuilder = ArgGroupSpec.builder() + .heading(category.getHeading()) + .order(category.getOrder()) + .validate(false); + + for(PropertyMapper mapper: mappersInCategory) { + String name = ARG_PREFIX + PropertyMappers.toCLIFormat(mapper.getFrom()).substring(3); + String description = mapper.getDescription(); + + if (description == null || cSpec.optionsMap().containsKey(name) || name.endsWith(ARG_PART_SEPARATOR)) { + //when key is already added or has no description, don't add. + continue; + } + + String defaultValue = mapper.getDefaultValue(); + + argGroupBuilder.addArg(OptionSpec.builder(name) + .defaultValue(defaultValue) + .description(description + (defaultValue == null ? "" : " Default: ${DEFAULT-VALUE}.")) + .paramLabel("<" + name.substring(2) + ">") + .completionCandidates(mapper.getExpectedValues()) + .type(String.class) + .build()); + } + + cSpec.addArgGroup(argGroupBuilder.build()); } - - 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)), null); - } - - private static void addOption(CommandSpec commandSpec, String name, String description, PropertyMapper mapper) { - OptionSpec.Builder builder = OptionSpec.builder(name) - .description(description) - .paramLabel(name.substring(2)) - .type(String.class); - - if (mapper != null) { - builder.completionCandidates(mapper.getExpectedValues()); - } - - commandSpec.addOption(builder.build()); } public static List getCliArgs(CommandLine cmd) { diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/AbstractCommand.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/AbstractCommand.java index 8e52440f53..14c7c0561c 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/AbstractCommand.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/AbstractCommand.java @@ -18,22 +18,35 @@ package org.keycloak.quarkus.runtime.cli.command; import org.keycloak.quarkus.runtime.Environment; +import org.keycloak.quarkus.runtime.configuration.KeycloakConfigSourceProvider; +import picocli.CommandLine; import picocli.CommandLine.Model.CommandSpec; import picocli.CommandLine.Spec; import picocli.CommandLine.Option; -import picocli.CommandLine.ScopeType; public abstract class AbstractCommand { @Spec protected CommandSpec spec; - @Option(names = "--profile", - arity = "1", - description = "Set the profile. Use 'dev' profile to enable development mode.", - scope = ScopeType.INHERIT) + @Option(names = { "-h", "--help" }, + description = "This help message.", + usageHelp = true) + boolean help; + + @Option(names = {"-pf", "--profile"}, + description = "Set the profile. Use 'dev' profile to enable development mode.") public void setProfile(String profile) { Environment.setProfile(profile); } + + @Option(names = { "-cf", "--config-file" }, + arity = "1", + description = "Set the path to a configuration file. By default, configuration properties are read from the \"keycloak.properties\" file in the \"conf\" directory.", + paramLabel = "", + scope = CommandLine.ScopeType.INHERIT) + public void setConfigFile(String path) { + System.setProperty(KeycloakConfigSourceProvider.KEYCLOAK_CONFIG_FILE_PROP, path); + } } diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/AbstractStartCommand.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/AbstractStartCommand.java index 99be78fcd7..716697a50c 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/AbstractStartCommand.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/AbstractStartCommand.java @@ -20,18 +20,11 @@ package org.keycloak.quarkus.runtime.cli.command; import org.keycloak.quarkus.runtime.KeycloakMain; import picocli.CommandLine; -import picocli.CommandLine.Option; public abstract class AbstractStartCommand extends AbstractCommand implements Runnable { - public static final String AUTO_BUILD_OPTION = "--auto-build"; - - @Option(names = AUTO_BUILD_OPTION, - description = "Automatically detects whether the server configuration changed and a new server image must be built" + - " prior to starting the server. This option provides an alternative to manually running the '" + Build.NAME + "'" + - " prior to starting the server. Use this configuration carefully in production as it might impact the startup time.", - order = 1) - Boolean autoConfig; + public static final String AUTO_BUILD_OPTION_LONG = "--auto-build"; + public static final String AUTO_BUILD_OPTION_SHORT = "-b"; @Override public void run() { diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/Build.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/Build.java index d432f64e97..ddc8fce098 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/Build.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/Build.java @@ -52,10 +52,10 @@ import picocli.CommandLine.Command; + " Enable metrics:%n%n" + " $ ${PARENT-COMMAND-FULL-NAME:-$PARENTCOMMAND} ${COMMAND-NAME} --metrics-enabled=true%n%n" + "You can also use the \"--auto-build\" option when starting the server to avoid running this command every time you change a configuration:%n%n" - + " $ ${PARENT-COMMAND-FULL-NAME:-$PARENTCOMMAND} --auto-build %n%n" + + " $ ${PARENT-COMMAND-FULL-NAME:-$PARENTCOMMAND} start --auto-build %n%n" + "By doing that you have an additional overhead when the server is starting.%n%n", - mixinStandardHelpOptions = true, - optionListHeading = "%nConfiguration Options%n%n") + abbreviateSynopsis = true, + optionListHeading = "%nOptions%n%n") public final class Build extends AbstractCommand implements Runnable { public static final String NAME = "build"; diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/Completion.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/Completion.java index 7ea8818d74..59c0af722e 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/Completion.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/Completion.java @@ -18,20 +18,20 @@ package org.keycloak.quarkus.runtime.cli.command; import picocli.AutoComplete; -import picocli.CommandLine; import picocli.CommandLine.Command; @Command(name = "completion", - version = "generate-completion " + CommandLine.VERSION, header = "Generate bash/zsh completion script for ${ROOT-COMMAND-NAME:-the root command of this command}.", - helpCommand = false, headerHeading = "%n", commandListHeading = "%nCommands:%n", - synopsisHeading = "%nUsage: ", optionListHeading = "Options:%n", + headerHeading = "%n", + commandListHeading = "%nCommands:%n", + synopsisHeading = "%nUsage: ", + optionListHeading = "Options:%n", description = { "Generate bash/zsh completion script for ${ROOT-COMMAND-NAME:-the root command of this command}.%n" + "Run the following command to give `${ROOT-COMMAND-NAME:-$PARENTCOMMAND}` TAB completion in the current shell:", "", " source <(${PARENT-COMMAND-FULL-NAME:-$PARENTCOMMAND} ${COMMAND-NAME})", ""}, - mixinStandardHelpOptions = false) + abbreviateSynopsis = true) public class Completion extends AutoComplete.GenerateCompletion { } diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/Export.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/Export.java index 67995c1f8e..3de005ad5f 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/Export.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/Export.java @@ -24,8 +24,8 @@ import picocli.CommandLine.Option; @Command(name = "export", description = "Export data from realms to a file or directory.", - mixinStandardHelpOptions = true, showDefaultValues = true, + abbreviateSynopsis = true, optionListHeading = "%nOptions%n", parameterListHeading = "Available Commands%n") public final class Export extends AbstractExportImportCommand implements Runnable { diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/Import.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/Import.java index 705649f638..71a5188e29 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/Import.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/Import.java @@ -26,8 +26,8 @@ import picocli.CommandLine.Option; @Command(name = "import", description = "Import data from a directory or a file.", - mixinStandardHelpOptions = true, showDefaultValues = true, + abbreviateSynopsis = true, optionListHeading = "%nOptions%n", parameterListHeading = "Available Commands%n") public final class Import extends AbstractExportImportCommand implements Runnable { diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/Main.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/Main.java index d7f6374e73..8e9f8e1507 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/Main.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/Main.java @@ -17,8 +17,6 @@ package org.keycloak.quarkus.runtime.cli.command; -import org.keycloak.quarkus.runtime.configuration.KeycloakConfigSourceProvider; - import picocli.CommandLine.Command; import picocli.CommandLine.Option; import picocli.CommandLine.ScopeType; @@ -37,21 +35,19 @@ import picocli.CommandLine.ScopeType; + " Building an optimized server runtime:%n%n" + " $ ${COMMAND-NAME} build %n%n" + " Start the server in production mode:%n%n" - + " $ ${COMMAND-NAME} %n%n" + + " $ ${COMMAND-NAME} start %n%n" + + " Enable auto-completion to bash/zsh:%n%n" + + " $ source <(${COMMAND-NAME} tools completion)%n%n" + " Please, take a look at the documentation for more details before deploying in production.%n", footer = { "", "Use \"${COMMAND-NAME} start --help\" for the available options when starting the server.", "Use \"${COMMAND-NAME} --help\" for more information about other commands.", - "", - "by 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})", - "OS: ${os.name} ${os.version} ${os.arch}" }, + optionListHeading = "Options%n%n", + commandListHeading = "%nCommands%n%n", + abbreviateSynopsis = true, + versionProvider = VersionProvider.class, subcommands = { Build.class, Start.class, @@ -65,7 +61,6 @@ public final class Main { @Option(names = "-D=", description = "Set a Java system property", - scope = ScopeType.INHERIT, order = 0) Boolean sysProps; @@ -84,12 +79,4 @@ public final class Main { required = false, scope = ScopeType.INHERIT) Boolean verbose; - - @Option(names = { "-cf", "--config-file" }, - arity = "1", - description = "Set the path to a configuration file. By default, configuration properties are read from the \"keycloak.properties\" file in the \"conf\" directory.", - paramLabel = "") - public void setConfigFile(String path) { - System.setProperty(KeycloakConfigSourceProvider.KEYCLOAK_CONFIG_FILE_PROP, path); - } } \ No newline at end of file diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/ShowConfig.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/ShowConfig.java index 7339d9aee1..18599dcca2 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/ShowConfig.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/ShowConfig.java @@ -18,11 +18,11 @@ package org.keycloak.quarkus.runtime.cli.command; import static java.lang.Boolean.parseBoolean; +import static org.keycloak.quarkus.runtime.configuration.Configuration.getBuiltTimeProperty; import static org.keycloak.quarkus.runtime.configuration.Configuration.getConfigValue; import static org.keycloak.quarkus.runtime.configuration.Configuration.getPropertyNames; -import static org.keycloak.quarkus.runtime.configuration.PropertyMappers.canonicalFormat; -import static org.keycloak.quarkus.runtime.configuration.PropertyMappers.formatValue; -import static org.keycloak.quarkus.runtime.configuration.PropertyMappers.getBuiltTimeProperty; +import static org.keycloak.quarkus.runtime.configuration.mappers.PropertyMappers.canonicalFormat; +import static org.keycloak.quarkus.runtime.configuration.mappers.PropertyMappers.formatValue; import java.util.HashSet; import java.util.Map; @@ -34,7 +34,6 @@ import java.util.stream.StreamSupport; import org.keycloak.quarkus.runtime.configuration.MicroProfileConfigProvider; import org.keycloak.quarkus.runtime.configuration.PersistedConfigSource; -import org.keycloak.quarkus.runtime.configuration.PropertyMappers; import org.keycloak.quarkus.runtime.Environment; import io.smallrye.config.ConfigValue; @@ -43,7 +42,7 @@ import picocli.CommandLine.Parameters; @Command(name = "show-config", description = "Print out the current configuration.", - mixinStandardHelpOptions = true, + abbreviateSynopsis = true, optionListHeading = "%nOptions%n", parameterListHeading = "Available Commands%n") public final class ShowConfig extends AbstractCommand implements Runnable { @@ -159,7 +158,7 @@ public final class ShowConfig extends AbstractCommand implements Runnable { } private void printProperty(String property) { - String canonicalFormat = PropertyMappers.canonicalFormat(property); + String canonicalFormat = canonicalFormat(property); ConfigValue configValue = getConfigValue(canonicalFormat); if (configValue.getValue() == null) { @@ -182,7 +181,14 @@ public final class ShowConfig extends AbstractCommand implements Runnable { if (property.startsWith("%")) { return "%"; } - return property.substring(0, property.indexOf('.')); + + int endIndex = property.indexOf('.'); + + if (endIndex == -1) { + return ""; + } + + return property.substring(0, endIndex); } private static boolean filterByGroup(String property) { diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/Start.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/Start.java index cb8b55b6e6..a4f2da65ac 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/Start.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/Start.java @@ -17,19 +17,27 @@ package org.keycloak.quarkus.runtime.cli.command; +import picocli.CommandLine; import picocli.CommandLine.Command; @Command(name = Start.NAME, - header = "Start the server.", + header = "Start the server.%n", description = { "%nUse this command to run the server in production." }, footerHeading = "%nYou may use the \"--auto-build\" option when starting the server to avoid running the \"build\" command everytime you need to change a static property:%n%n" + " $ ${PARENT-COMMAND-FULL-NAME:-$PARENTCOMMAND} ${COMMAND-NAME} --auto-build %n%n" + "By doing that you have an additional overhead when the server is starting. Run \"${PARENT-COMMAND-FULL-NAME:-$PARENTCOMMAND} build -h\" for more details.%n%n", - optionListHeading = "%nConfiguration Options%n%n", - mixinStandardHelpOptions = true) + optionListHeading = "%nOptions%n%n", + abbreviateSynopsis = true) public final class Start extends AbstractStartCommand implements Runnable { public static final String NAME = "start"; + + @CommandLine.Option(names = {AUTO_BUILD_OPTION_SHORT, AUTO_BUILD_OPTION_LONG }, + description = "Automatically detects whether the server configuration changed and a new server image must be built" + + " prior to starting the server. This option provides an alternative to manually running the '" + Build.NAME + "'" + + " prior to starting the server. Use this configuration carefully in production as it might impact the startup time.", + order = 1) + Boolean autoConfig; } diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/StartDev.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/StartDev.java index 31f9122978..21729117ec 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/StartDev.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/StartDev.java @@ -19,6 +19,7 @@ package org.keycloak.quarkus.runtime.cli.command; import org.keycloak.quarkus.runtime.Environment; +import picocli.CommandLine; import picocli.CommandLine.Command; @Command(name = StartDev.NAME, @@ -27,12 +28,15 @@ import picocli.CommandLine.Command; "%nUse this command if you want to run the server locally for development or testing purposes.", }, footerHeading = "%nDo NOT start the server using this command when deploying to production.%n%n", - optionListHeading = "%nConfiguration Options%n%n", - mixinStandardHelpOptions = true) + optionListHeading = "%nOptions%n%n", + abbreviateSynopsis = true) public final class StartDev extends AbstractStartCommand implements Runnable { public static final String NAME = "start-dev"; + @CommandLine.Option(names = AUTO_BUILD_OPTION_LONG, hidden = true) + Boolean autoConfig; + @Override protected void doBeforeRun() { Environment.forceDevProfile(); diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/Tools.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/Tools.java index 4c738fed02..d30d4f7cd9 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/Tools.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/Tools.java @@ -21,7 +21,7 @@ import picocli.CommandLine.Command; @Command(name = "tools", description = "Utilities for use and interaction with the server.", - mixinStandardHelpOptions = true, + abbreviateSynopsis = true, optionListHeading = "%nOptions%n", parameterListHeading = "Available Commands%n", subcommands = {Completion.class}) diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/VersionProvider.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/VersionProvider.java new file mode 100644 index 0000000000..6e213cbf8d --- /dev/null +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/VersionProvider.java @@ -0,0 +1,13 @@ +package org.keycloak.quarkus.runtime.cli.command; + +import picocli.CommandLine.IVersionProvider; + +public class VersionProvider implements IVersionProvider { + @Override + public String[] getVersion() { + return new String[]{"Keycloak ${sys:kc.version}", + "JVM: ${java.version} (${java.vendor} ${java.vm.name} ${java.vm.version})", + "OS: ${os.name} ${os.version} ${os.arch}%n" + }; +} +} diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/ConfigArgsConfigSource.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/ConfigArgsConfigSource.java index 241b4433e4..b8769fb937 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/ConfigArgsConfigSource.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/ConfigArgsConfigSource.java @@ -20,9 +20,8 @@ package org.keycloak.quarkus.runtime.configuration; import static org.keycloak.quarkus.runtime.cli.Picocli.ARG_KEY_VALUE_SPLIT; import static org.keycloak.quarkus.runtime.cli.Picocli.ARG_PREFIX; import static org.keycloak.quarkus.runtime.cli.Picocli.ARG_SPLIT; +import static org.keycloak.quarkus.runtime.configuration.Configuration.getMappedPropertyName; import static org.keycloak.quarkus.runtime.configuration.MicroProfileConfigProvider.NS_KEYCLOAK_PREFIX; -import static org.keycloak.quarkus.runtime.configuration.MicroProfileConfigProvider.NS_QUARKUS_PREFIX; -import static org.keycloak.quarkus.runtime.configuration.PropertyMappers.getMappedPropertyName; import java.util.Collections; import java.util.HashMap; @@ -48,8 +47,6 @@ public class ConfigArgsConfigSource extends PropertiesConfigSource { private static final Logger log = Logger.getLogger(ConfigArgsConfigSource.class); - private static final Pattern DOT_SPLIT = Pattern.compile("\\."); - ConfigArgsConfigSource() { // higher priority over default Quarkus config sources super(parseArgument(), "CliConfigSource", 500); diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/Configuration.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/Configuration.java index 62dac5bc55..f96de98c61 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/Configuration.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/Configuration.java @@ -17,13 +17,22 @@ package org.keycloak.quarkus.runtime.configuration; +import static org.keycloak.quarkus.runtime.Environment.getProfileOrDefault; +import static org.keycloak.quarkus.runtime.configuration.mappers.PropertyMappers.toCLIFormat; + +import java.util.Arrays; +import java.util.List; import java.util.Optional; import java.util.function.Function; import io.smallrye.config.ConfigValue; import io.smallrye.config.SmallRyeConfig; import io.smallrye.config.SmallRyeConfigProviderResolver; + +import org.eclipse.microprofile.config.spi.ConfigSource; import org.keycloak.quarkus.runtime.Environment; +import org.keycloak.quarkus.runtime.configuration.mappers.PropertyMapper; +import org.keycloak.quarkus.runtime.configuration.mappers.PropertyMappers; /** * The entry point for accessing the server configuration @@ -32,6 +41,10 @@ public final class Configuration { private static volatile SmallRyeConfig CONFIG; + private Configuration() { + + } + public static synchronized SmallRyeConfig getConfig() { if (CONFIG == null) { CONFIG = (SmallRyeConfig) SmallRyeConfigProviderResolver.instance().getConfig(); @@ -39,11 +52,11 @@ public final class Configuration { return CONFIG; } - public static String getBuiltTimeProperty(String name) { + public static Optional getBuiltTimeProperty(String name) { String value = KeycloakConfigSourceProvider.PERSISTED_CONFIG_SOURCE.getValue(name); if (value == null) { - value = KeycloakConfigSourceProvider.PERSISTED_CONFIG_SOURCE.getValue(PropertyMappers.getMappedPropertyName(name)); + value = KeycloakConfigSourceProvider.PERSISTED_CONFIG_SOURCE.getValue(getMappedPropertyName(name)); } if (value == null) { @@ -56,7 +69,7 @@ public final class Configuration { value = KeycloakConfigSourceProvider.PERSISTED_CONFIG_SOURCE.getValue("%" + profile + "." + name); } - return value; + return Optional.ofNullable(value); } public static String getRawValue(String propertyName) { @@ -83,4 +96,48 @@ public final class Configuration { } }); } + + public static String getMappedPropertyName(String key) { + for (PropertyMapper mapper : PropertyMappers.getMappers()) { + String mappedProperty = mapper.getFrom(); + List expectedFormats = Arrays.asList(mappedProperty, toCLIFormat(mappedProperty), mappedProperty.toUpperCase().replace('.', '_').replace('-', '_')); + + if (expectedFormats.contains(key)) { + // we also need to make sure the target property is available when defined such as when defining alias for provider config (no spi-prefix). + return mapper.getTo() == null ? mappedProperty : mapper.getTo(); + } + } + + return key; + } + + public static Optional getRuntimeProperty(String name) { + for (ConfigSource configSource : getConfig().getConfigSources()) { + if (PersistedConfigSource.NAME.equals(configSource.getName())) { + continue; + } + + String value = getValue(configSource, name); + + if (value == null) { + value = getValue(configSource, getMappedPropertyName(name)); + } + + if (value != null) { + return Optional.of(value); + } + } + + return Optional.empty(); + } + + private static String getValue(ConfigSource configSource, String name) { + String value = configSource.getValue(name); + + if (value == null) { + value = configSource.getValue("%".concat(getProfileOrDefault("prod").concat(".").concat(name))); + } + + return value; + } } diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/EnvConfigSource.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/EnvConfigSource.java index 58036cbe66..0536fdce93 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/EnvConfigSource.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/EnvConfigSource.java @@ -17,6 +17,8 @@ package org.keycloak.quarkus.runtime.configuration; +import static org.keycloak.quarkus.runtime.configuration.Configuration.getMappedPropertyName; + import java.util.Map; import java.util.Set; import java.util.TreeMap; @@ -30,7 +32,7 @@ public class EnvConfigSource implements ConfigSource { for (Map.Entry entry : System.getenv().entrySet()) { String key = entry.getKey(); if (key.startsWith(MicroProfileConfigProvider.NS_KEYCLOAK_PREFIX.toUpperCase().replace('.', '_'))) { - properties.put(PropertyMappers.getMappedPropertyName(key), entry.getValue()); + properties.put(getMappedPropertyName(key), entry.getValue()); } } } @@ -53,6 +55,7 @@ public class EnvConfigSource implements ConfigSource { return "KcEnvVarConfigSource"; } + @Override public int getOrdinal() { return 350; } diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/KeycloakPropertiesConfigSource.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/KeycloakPropertiesConfigSource.java index c686ffb196..0456513697 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/KeycloakPropertiesConfigSource.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/KeycloakPropertiesConfigSource.java @@ -36,9 +36,9 @@ import org.jboss.logging.Logger; import io.smallrye.config.PropertiesConfigSource; import static org.keycloak.common.util.StringPropertyReplacer.replaceProperties; +import static org.keycloak.quarkus.runtime.configuration.Configuration.getMappedPropertyName; import static org.keycloak.quarkus.runtime.configuration.MicroProfileConfigProvider.NS_KEYCLOAK; import static org.keycloak.quarkus.runtime.configuration.MicroProfileConfigProvider.NS_QUARKUS; -import static org.keycloak.quarkus.runtime.configuration.PropertyMappers.getMappedPropertyName; /** * A configuration source for {@code keycloak.properties}. @@ -61,7 +61,7 @@ public abstract class KeycloakPropertiesConfigSource extends PropertiesConfigSou try (Closeable ignored = is) { Properties properties = new Properties(); properties.load(is); - return transform((Map) (Map) properties); + return transform((Map) properties); } catch (IOException e) { throw new IOError(e); } 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 deleted file mode 100644 index cf5fdea8ef..0000000000 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/PropertyMapper.java +++ /dev/null @@ -1,240 +0,0 @@ -/* - * Copyright 2021 Red Hat, Inc. and/or its affiliates - * and other contributors as indicated by the @author tags. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.keycloak.quarkus.runtime.configuration; - -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; -import java.util.function.BiFunction; -import java.util.function.Supplier; - -import io.smallrye.config.ConfigSourceInterceptorContext; -import io.smallrye.config.ConfigValue; - -public class PropertyMapper { - - static PropertyMapper create(String fromProperty, String toProperty, String description) { - return MAPPERS.computeIfAbsent(toProperty, s -> new PropertyMapper(fromProperty, s, null, null, description)); - } - - static PropertyMapper createWithDefault(String fromProperty, String toProperty, String defaultValue, String description) { - return MAPPERS.computeIfAbsent(toProperty, s -> new PropertyMapper(fromProperty, s, defaultValue, null, description)); - } - - static PropertyMapper createWithDefault(String fromProperty, String toProperty, Supplier defaultValue, String description) { - return MAPPERS.computeIfAbsent(toProperty, s -> new PropertyMapper(fromProperty, s, defaultValue.get(), null, description)); - } - - static PropertyMapper createWithDefault(String fromProperty, String toProperty, String defaultValue, BiFunction transformer, String description) { - return MAPPERS.computeIfAbsent(toProperty, s -> new PropertyMapper(fromProperty, s, defaultValue, transformer, description)); - } - - static PropertyMapper createWithDefault(String fromProperty, String toProperty, String defaultValue, - BiFunction transformer, String description, - Iterable expectedValues) { - return MAPPERS.computeIfAbsent(toProperty, s -> new PropertyMapper(fromProperty, s, defaultValue, transformer, description, expectedValues)); - } - - static PropertyMapper create(String fromProperty, String toProperty, BiFunction transformer, String description) { - return MAPPERS.computeIfAbsent(toProperty, s -> new PropertyMapper(fromProperty, s, null, transformer, null, description)); - } - - static PropertyMapper create(String fromProperty, String toProperty, String description, boolean mask) { - return MAPPERS.computeIfAbsent(toProperty, s -> new PropertyMapper(fromProperty, s, null, null, null, false, description, mask)); - } - - static PropertyMapper create(String fromProperty, String mapFrom, String toProperty, BiFunction transformer, String description) { - return MAPPERS.computeIfAbsent(toProperty, s -> new PropertyMapper(fromProperty, s, null, transformer, mapFrom, description)); - } - - static PropertyMapper createBuildTimeProperty(String fromProperty, String toProperty, BiFunction transformer, String description) { - return MAPPERS.computeIfAbsent(toProperty, s -> new PropertyMapper(fromProperty, s, null, transformer, null, true, description, false)); - } - - static PropertyMapper createBuildTimeProperty(String fromProperty, String toProperty, - BiFunction transformer, String description, - Iterable expectedValues) { - return MAPPERS.computeIfAbsent(toProperty, s -> new PropertyMapper(fromProperty, s, null, transformer, null, true, description, false, expectedValues)); - } - - static PropertyMapper createBuildTimeProperty(String fromProperty, String toProperty, String description) { - return MAPPERS.computeIfAbsent(toProperty, s -> new PropertyMapper(fromProperty, s, null, null, null, true, description, false)); - } - - static PropertyMapper createBuildTimeProperty(String fromProperty, String toProperty, String description, Iterable expectedValues) { - 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) { - @Override - public ConfigValue getOrDefault(String name, ConfigSourceInterceptorContext context, ConfigValue current) { - return current; - } - }; - - private static String defaultTransformer(String value, ConfigSourceInterceptorContext context) { - return value; - } - - private final String to; - private final String from; - private final String defaultValue; - private final BiFunction mapper; - private final String mapFrom; - private final boolean buildTime; - private final String description; - private final boolean mask; - private final Iterable expectedValues; - - PropertyMapper(String from, String to, String defaultValue, BiFunction mapper, String description) { - this(from, to, defaultValue, mapper, null, description); - } - - PropertyMapper(String from, String to, String defaultValue, BiFunction mapper, - String description, Iterable expectedValues) { - this(from, to, defaultValue, mapper, null, false, description, false, expectedValues); - } - - PropertyMapper(String from, String to, String defaultValue, BiFunction mapper, String mapFrom, String description) { - this(from, to, defaultValue, mapper, mapFrom, false, description, false); - } - - PropertyMapper(String from, String to, String defaultValue, BiFunction mapper, String mapFrom, boolean buildTime, String description, boolean mask) { - this(from, to, defaultValue, mapper, mapFrom, buildTime, description, mask, Collections.emptyList()); - } - - PropertyMapper(String from, String to, String defaultValue, BiFunction mapper, - String mapFrom, boolean buildTime, String description, boolean mask, Iterable expectedValues) { - this.from = MicroProfileConfigProvider.NS_KEYCLOAK_PREFIX + from; - this.to = to; - this.defaultValue = defaultValue; - this.mapper = mapper == null ? PropertyMapper::defaultTransformer : mapper; - this.mapFrom = mapFrom; - this.buildTime = buildTime; - this.description = description; - this.mask = mask; - this.expectedValues = expectedValues == null ? Collections.emptyList() : expectedValues; - } - - ConfigValue getOrDefault(ConfigSourceInterceptorContext context, ConfigValue current) { - return getOrDefault(null, context, current); - } - - ConfigValue getOrDefault(String name, ConfigSourceInterceptorContext context, ConfigValue current) { - String from = this.from; - - if (to != null && to.endsWith(".")) { - // in case mapping is based on prefixes instead of full property names - from = name.replace(to.substring(0, to.lastIndexOf('.')), from.substring(0, from.lastIndexOf('.'))); - } - - // try to obtain the value for the property we want to map - ConfigValue config = context.proceed(from); - - if (config == null) { - if (mapFrom != null) { - // if the property we want to map depends on another one, we use the value from the other property to call the mapper - String parentKey = MicroProfileConfigProvider.NS_KEYCLOAK + "." + mapFrom; - ConfigValue parentValue = context.proceed(parentKey); - - if (parentValue != null) { - ConfigValue value = transformValue(parentValue.getValue(), context); - - if (value != null) { - return value; - } - } - } - - // if not defined, return the current value from the property as a default if the property is not explicitly set - if (defaultValue == null - || (current != null && !current.getConfigSourceName().equalsIgnoreCase("default values"))) { - return current; - } - - if (mapper != null) { - return transformValue(defaultValue, context); - } - - return ConfigValue.builder().withName(to).withValue(defaultValue).build(); - } - - if (mapFrom != null) { - return config; - } - - ConfigValue value = transformValue(config.getValue(), context); - - // we always fallback to the current value from the property we are mapping - if (value == null) { - return current; - } - - return value; - } - - public String getFrom() { - return from; - } - - public String getDescription() { - return description; - } - - private ConfigValue transformValue(String value, ConfigSourceInterceptorContext context) { - if (value == null) { - return null; - } - - if (mapper == null) { - return ConfigValue.builder().withName(to).withValue(value).build(); - } - - String mappedValue = mapper.apply(value, context); - - if (mappedValue != null) { - return ConfigValue.builder().withName(to).withValue(mappedValue).build(); - } - - return null; - } - - boolean isBuildTime() { - return buildTime; - } - - - boolean isMask() { - return mask; - } - - String getTo() { - return to; - } - - public Iterable getExpectedValues() { - return expectedValues; - } -} 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 deleted file mode 100644 index f715675c32..0000000000 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/PropertyMappers.java +++ /dev/null @@ -1,341 +0,0 @@ -/* - * Copyright 2021 Red Hat, Inc. and/or its affiliates - * and other contributors as indicated by the @author tags. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.keycloak.quarkus.runtime.configuration; - -import static org.keycloak.quarkus.runtime.Environment.getProfileOrDefault; -import static org.keycloak.quarkus.runtime.configuration.Configuration.getConfig; -import static org.keycloak.quarkus.runtime.configuration.Messages.invalidDatabaseVendor; -import static org.keycloak.quarkus.runtime.configuration.PropertyMapper.MAPPERS; -import static org.keycloak.quarkus.runtime.configuration.PropertyMapper.create; -import static org.keycloak.quarkus.runtime.configuration.PropertyMapper.createWithDefault; -import static org.keycloak.quarkus.runtime.configuration.PropertyMapper.createBuildTimeProperty; -import static org.keycloak.quarkus.runtime.integration.QuarkusPlatform.addInitializationException; - -import java.io.File; -import java.nio.file.Paths; -import java.util.Arrays; -import java.util.Collection; -import java.util.List; -import java.util.Optional; -import java.util.function.Predicate; -import java.util.function.Supplier; -import java.util.stream.Collectors; - -import io.smallrye.config.ConfigSourceInterceptorContext; -import io.smallrye.config.ConfigValue; - -import org.eclipse.microprofile.config.spi.ConfigSource; -import org.keycloak.quarkus.runtime.storage.database.Database; -import org.keycloak.quarkus.runtime.Environment; - -/** - * Configures the {@link PropertyMapper} instances for all Keycloak configuration properties that should be mapped to their - * corresponding properties in Quarkus. - */ -public final class PropertyMappers { - - static { - configureDatabasePropertyMappers(); - configureHttpPropertyMappers(); - configureProxyMappers(); - configureClustering(); - configureHostnameProviderMappers(); - configureMetrics(); - configureVault(); - } - - private static void configureHttpPropertyMappers() { - createWithDefault("http.enabled", "quarkus.http.insecure-requests", "disabled", (value, context) -> { - Boolean enabled = Boolean.valueOf(value); - ConfigValue proxy = context.proceed(MicroProfileConfigProvider.NS_KEYCLOAK_PREFIX + "proxy"); - - if (Environment.isDevMode() || Environment.isImportExportMode() - || (proxy != null && "edge".equalsIgnoreCase(proxy.getValue()))) { - enabled = true; - } - - if (!enabled) { - ConfigValue proceed = context.proceed("kc.https.certificate.file"); - - if (proceed == null || proceed.getValue() == null) { - proceed = getMapper("quarkus.http.ssl.certificate.key-store-file").getOrDefault(context, null); - } - - if (proceed == null || proceed.getValue() == null) { - addInitializationException(Messages.httpsConfigurationNotSet()); - } - } - - return enabled ? "enabled" : "disabled"; - }, "Enables the HTTP listener.", Arrays.asList(Boolean.TRUE.toString(), Boolean.FALSE.toString())); - createWithDefault("http.host", "quarkus.http.host", "0.0.0.0", "The HTTP host."); - createWithDefault("http.port", "quarkus.http.port", String.valueOf(8080), "The HTTP port."); - createWithDefault("https.port", "quarkus.http.ssl-port", String.valueOf(8443), "The HTTPS port."); - createWithDefault("https.client-auth", "quarkus.http.ssl.client-auth", "none", "Configures the server to require/request client authentication. none, request, required."); - create("https.cipher-suites", "quarkus.http.ssl.cipher-suites", "The cipher suites to use. If none is given, a reasonable default is selected."); - create("https.protocols", "quarkus.http.ssl.protocols", "The list of protocols to explicitly enable."); - create("https.certificate.file", "quarkus.http.ssl.certificate.file", "The file path to a server certificate or certificate chain in PEM format."); - create("https.certificate.key-file", "quarkus.http.ssl.certificate.key-file", "The file path to a private key in PEM format."); - createWithDefault("https.certificate.key-store-file", "quarkus.http.ssl.certificate.key-store-file", - new Supplier() { - @Override - public String get() { - String homeDir = Environment.getHomeDir(); - - if (homeDir != null) { - File file = Paths.get(homeDir, "conf", "server.keystore").toFile(); - - if (file.exists()) { - return file.getAbsolutePath(); - } - } - - return null; - } - }, "An optional key store which holds the certificate information instead of specifying separate files."); - create("https.certificate.key-store-password", "quarkus.http.ssl.certificate.key-store-password", "A parameter to specify the password of the key store file. If not given, the default (\"password\") is used.", true); - create("https.certificate.key-store-file-type", "quarkus.http.ssl.certificate.key-store-file-type", "An optional parameter to specify type of the key store file. If not given, the type is automatically detected based on the file name."); - create("https.certificate.trust-store-file", "quarkus.http.ssl.certificate.trust-store-file", "An optional trust store which holds the certificate information of the certificates to trust."); - create("https.certificate.trust-store-password", "quarkus.http.ssl.certificate.trust-store-password", "A parameter to specify the password of the trust store file.", true); - create("https.certificate.trust-store-file-type", "quarkus.http.ssl.certificate.trust-store-file-type", "An optional parameter to specify type of the trust store file. If not given, the type is automatically detected based on the file name."); - } - - private static void configureProxyMappers() { - createWithDefault("proxy", "quarkus.http.proxy.proxy-address-forwarding", "none", (mode, context) -> { - switch (mode) { - case "none": - return "false"; - case "edge": - case "reencrypt": - case "passthrough": - return "true"; - } - addInitializationException(Messages.invalidProxyMode(mode)); - return "false"; - }, "The proxy mode if the server is behind a reverse proxy. Possible values are: none, edge, reencrypt, and passthrough.", - Arrays.asList("none", "edge", "reencrypt", "passthrough")); - } - - private static void configureDatabasePropertyMappers() { - createBuildTimeProperty("db", "quarkus.hibernate-orm.dialect", (db, context) -> Database.getDialect(db).orElse(null), null); - create("db", "quarkus.datasource.jdbc.driver", (db, context) -> Database.getDriver(db).orElse(null), null); - createBuildTimeProperty("db", "quarkus.datasource.db-kind", (db, context) -> { - if (Database.isSupported(db)) { - return Database.getDatabaseKind(db).orElse(db); - } - addInitializationException(invalidDatabaseVendor(db, "h2-file", "h2-mem", "mariadb", "mysql", "postgres", "postgres-95", "postgres-10")); - return "h2"; - }, "The database vendor. Possible values are: h2-mem, h2-file, mariadb, mysql, postgres, postgres-95, postgres-10.", - Arrays.asList("h2-file", "h2-mem", "mariadb", "mysql", "postgres", "postgres-95", "postgres-10")); - create("db", "quarkus.datasource.jdbc.transactions", (db, context) -> "xa", null); - create("db.url", "db", "quarkus.datasource.jdbc.url", (value, context) -> Database.getDefaultUrl(value).orElse(value), "The database JDBC URL. If not provided a default URL is set based on the selected database vendor. For instance, if using 'postgres', the JDBC URL would be 'jdbc:postgresql://localhost/keycloak'. The host, database and properties can be overridden by setting the following system properties, respectively: -Dkc.db.url.host, -Dkc.db.url.database, -Dkc.db.url.properties."); - create("db.username", "quarkus.datasource.username", "The database username."); - create("db.password", "quarkus.datasource.password", "The database password.", true); - create("db.schema", "quarkus.datasource.schema", "The database schema."); - create("db.pool.initial-size", "quarkus.datasource.jdbc.initial-size", "The initial size of the connection pool."); - create("db.pool.min-size", "quarkus.datasource.jdbc.min-size", "The minimal size of the connection pool."); - createWithDefault("db.pool.max-size", "quarkus.datasource.jdbc.max-size", String.valueOf(100), "The maximum size of the connection pool."); - } - - private static void configureClustering() { - 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")); - create("cluster-stack", "kc.spi.connections-infinispan.quarkus.stack", "Define the default stack to use for cluster communication and node discovery. Possible values are: tcp, udp, kubernetes, ec2, azure, google."); - } - - private static void configureHostnameProviderMappers() { - create("hostname-frontend-url", "kc.spi.hostname.default.frontend-url", "The URL that should be used to serve frontend requests that are usually sent through the a public domain."); - create("hostname-admin-url", "kc.spi.hostname.default.admin-url", "The URL that should be used to expose the admin endpoints and console."); - create("hostname-force-backend-url-to-frontend-url ", "kc.spi.hostname.default.force-backend-url-to-frontend-url", "Forces backend requests to go through the URL defined as the frontend-url. Defaults to false. Possible values are true or false."); - } - - private static void configureMetrics() { - createBuildTimeProperty("metrics.enabled", "quarkus.datasource.metrics.enabled", "If the server should expose metrics and healthcheck. If enabled, metrics are available at the '/metrics' endpoint and healthcheck at the '/health' endpoint.", - Arrays.asList(Boolean.TRUE.toString(), Boolean.FALSE.toString())); - } - - private static void configureVault() { - createBuildTimeProperty("vault.file.path", "kc.spi.vault.files-plaintext.dir", "If set, secrets can be obtained by reading the content of files within the given path."); - createBuildTimeProperty("vault.hashicorp.", "quarkus.vault.", "If set, secrets can be obtained from Hashicorp Vault."); - createBuildTimeProperty("vault.hashicorp.paths", "kc.spi.vault.hashicorp.paths", "A set of one or more paths that should be used when looking up secrets."); - } - - static ConfigValue getValue(ConfigSourceInterceptorContext context, String name) { - PropertyMapper mapper = MAPPERS.getOrDefault(name, PropertyMapper.IDENTITY); - ConfigValue configValue = mapper - .getOrDefault(name, context, context.proceed(name)); - - if (configValue == null) { - Optional prefixedMapper = getPrefixedMapper(name); - - if (prefixedMapper.isPresent()) { - return MAPPERS.get(prefixedMapper.get()).getOrDefault(name, context, configValue); - } - } else { - configValue.withName(mapper.getTo()); - } - - return configValue; - } - - private static Optional getPrefixedMapper(String name) { - Optional prefixedMapper = MAPPERS.keySet().stream().filter(new Predicate() { - @Override - public boolean test(String key) { - if (!key.endsWith(".")) { - return false; - } - - String prefix = key.substring(0, key.lastIndexOf('.') - 1); - - return name.startsWith(prefix); - } - }).findAny(); - - return prefixedMapper; - } - - public static boolean isBuildTimeProperty(String name) { - - if (isFeaturesBuildTimeProperty(name) || isSpiBuildTimeProperty(name)) { - return true; - } - - return name.startsWith(MicroProfileConfigProvider.NS_KEYCLOAK_PREFIX) - && PropertyMapper.MAPPERS.values().stream() - .filter(PropertyMapper::isBuildTime) - .anyMatch(mapper -> mapper.getFrom().equals(name) || mapper.getTo().equals(name)) - && !"kc.version".equals(name) - && !Environment.CLI_ARGS.equals(name) - && !"kc.home.dir".equals(name) - && !"kc.config.file".equals(name) - && !Environment.PROFILE.equals(name) - && !"kc.show.config".equals(name) - && !"kc.show.config.runtime".equals(name) - && !PropertyMappers.toCLIFormat("kc.config.file").equals(name); - } - - private static boolean isSpiBuildTimeProperty(String name) { - return name.startsWith("kc.spi") && (name.endsWith("provider") || name.endsWith("enabled")); - } - - private static boolean isFeaturesBuildTimeProperty(String name) { - return name.startsWith("kc.features"); - } - - public static String toCLIFormat(String name) { - if (name.indexOf('.') == -1) { - return name; - } - return MicroProfileConfigProvider.NS_KEYCLOAK_PREFIX - .concat(name.substring(3, name.lastIndexOf('.') + 1) - .replaceAll("\\.", "-") + name.substring(name.lastIndexOf('.') + 1)); - } - - public static List getRuntimeMappers() { - return PropertyMapper.MAPPERS.values().stream() - .filter(entry -> !entry.isBuildTime()).collect(Collectors.toList()); - } - - public static List getBuiltTimeMappers() { - return PropertyMapper.MAPPERS.values().stream() - .filter(entry -> entry.isBuildTime()).collect(Collectors.toList()); - } - - public static Collection getMappers() { - return MAPPERS.values(); - } - - public static String canonicalFormat(String name) { - return name.replaceAll("-", "\\."); - } - - public static String formatValue(String property, String value) { - PropertyMapper mapper = PropertyMappers.getMapper(property); - - if (mapper != null && mapper.isMask()) { - return "*******"; - } - - return value; - } - - public static PropertyMapper getMapper(String property) { - return MAPPERS.values().stream().filter(new Predicate() { - @Override - public boolean test(PropertyMapper propertyMapper) { - return property.equals(propertyMapper.getFrom()) || property.equals(propertyMapper.getTo()); - } - }).findFirst().orElse(null); - } - - public static String getMappedPropertyName(String key) { - for (PropertyMapper mapper : PropertyMappers.getMappers()) { - String mappedProperty = mapper.getFrom(); - List expectedFormats = Arrays.asList(mappedProperty, toCLIFormat(mappedProperty), mappedProperty.toUpperCase().replace('.', '_').replace('-', '_')); - - if (expectedFormats.contains(key)) { - // we also need to make sure the target property is available when defined such as when defining alias for provider config (no spi-prefix). - return mapper.getTo() == null ? mappedProperty : mapper.getTo(); - } - } - - return key; - } - - public static Optional getBuiltTimeProperty(String name) { - String value = Configuration.getBuiltTimeProperty(name); - - if (value == null) { - return Optional.empty(); - } - - return Optional.of(value); - } - - public static Optional getRuntimeProperty(String name) { - for (ConfigSource configSource : getConfig().getConfigSources()) { - if (PersistedConfigSource.NAME.equals(configSource.getName())) { - continue; - } - - String value = getValue(configSource, name); - - if (value == null) { - value = getValue(configSource, PropertyMappers.getMappedPropertyName(name)); - } - - if (value != null) { - return Optional.of(value); - } - } - - return Optional.empty(); - } - - private static String getValue(ConfigSource configSource, String name) { - String value = configSource.getValue(name); - - if (value == null) { - value = configSource.getValue("%".concat(getProfileOrDefault("prod").concat(".").concat(name))); - } - - return value; - } -} diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/PropertyMappingInterceptor.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/PropertyMappingInterceptor.java index 8cd89b5ca2..e4483c6532 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/PropertyMappingInterceptor.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/PropertyMappingInterceptor.java @@ -20,6 +20,8 @@ import io.smallrye.config.ConfigSourceInterceptor; import io.smallrye.config.ConfigSourceInterceptorContext; import io.smallrye.config.ConfigValue; import org.keycloak.common.util.StringPropertyReplacer; +import org.keycloak.quarkus.runtime.configuration.mappers.PropertyMapper; +import org.keycloak.quarkus.runtime.configuration.mappers.PropertyMappers; /** *

This interceptor is responsible for mapping Keycloak properties to their corresponding properties in Quarkus. diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/SysPropConfigSource.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/SysPropConfigSource.java index c3472a7052..d30d0e4db4 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/SysPropConfigSource.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/SysPropConfigSource.java @@ -58,6 +58,7 @@ public class SysPropConfigSource implements ConfigSource { return "KcSysPropConfigSource"; } + @Override public int getOrdinal() { return 400; } diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/ClusteringPropertyMappers.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/ClusteringPropertyMappers.java new file mode 100644 index 0000000000..766da8f4c2 --- /dev/null +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/ClusteringPropertyMappers.java @@ -0,0 +1,36 @@ +package org.keycloak.quarkus.runtime.configuration.mappers; + +import java.util.Arrays; + +final class ClusteringPropertyMappers { + + private ClusteringPropertyMappers() { + } + + public static PropertyMapper[] getClusteringPropertyMappers() { + return new PropertyMapper[] { + builder().from("cluster") + .to("kc.spi.connections-infinispan.quarkus.config-file") + .defaultValue("default") + .transformer((value, context) -> "cluster-" + value + ".xml") + .description("Specifies clustering configuration. The specified value points to the infinispan " + + "configuration file prefixed with the 'cluster-` inside the distribution configuration directory. " + + "Value 'local' effectively disables clustering and use infinispan caches in the local mode. " + + "Value 'default' enables clustering for infinispan caches.") + .isBuildTimeProperty(true) + .expectedValues(Arrays.asList("local", "default")) + .build(), + builder().from("cluster-stack") + .to("kc.spi.connections-infinispan.quarkus.stack") + .description("Define the default stack to use for cluster communication and node discovery.") + .defaultValue("udp") + .isBuildTimeProperty(true) + .expectedValues(Arrays.asList("tcp", "udp", "kubernetes", "ec2", "azure", "google")) + .build() + }; + } + + private static PropertyMapper.Builder builder() { + return PropertyMapper.builder(ConfigCategory.CLUSTERING); + } +} diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/ConfigCategory.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/ConfigCategory.java new file mode 100644 index 0000000000..710c116a7a --- /dev/null +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/ConfigCategory.java @@ -0,0 +1,32 @@ +package org.keycloak.quarkus.runtime.configuration.mappers; + +public enum ConfigCategory { + // ordered by name asc + CLUSTERING("%nCluster:%n%n", 10), + DATABASE("%nDatabase:%n%n", 20), + FEATURE("%nFeature:%n%n", 30), + HOSTNAME("%nHostname:%n%n", 40), + HTTP("%nHTTP/TLS:%n%n", 50), + METRICS("%nMetrics:%n%n", 60), + PROXY("%nProxy:%n%n", 70), + VAULT("%nVault:%n%n", 80), + GENERAL("%nGeneral:%n%n", 999); + + private final String heading; + + //Categories with a lower number are shown before groups with a higher number + private final int order; + + ConfigCategory(String heading, int order) { + this.heading = heading; this.order = order; + } + + public String getHeading() { + return this.heading; + } + + public int getOrder() { + return this.order; + } + +} diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/DatabasePropertyMappers.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/DatabasePropertyMappers.java new file mode 100644 index 0000000000..1e96ceb107 --- /dev/null +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/DatabasePropertyMappers.java @@ -0,0 +1,91 @@ +package org.keycloak.quarkus.runtime.configuration.mappers; + +import io.smallrye.config.ConfigSourceInterceptorContext; +import org.keycloak.quarkus.runtime.storage.database.Database; + +import java.util.Arrays; +import java.util.function.BiFunction; + +import static org.keycloak.quarkus.runtime.configuration.mappers.Messages.invalidDatabaseVendor; +import static org.keycloak.quarkus.runtime.integration.QuarkusPlatform.addInitializationException; + +final class DatabasePropertyMappers { + + private static final String[] supportedDatabaseVendors = {"h2-file", "h2-mem", "mariadb", "mysql", "postgres", "postgres-95", "postgres-10"}; + + private DatabasePropertyMappers(){} + + public static PropertyMapper[] getDatabasePropertyMappers() { + return new PropertyMapper[] { + builder().from("db") + .to("quarkus.hibernate-orm.dialect") + .isBuildTimeProperty(true) + .transformer((db, context) -> Database.getDialect(db).orElse(null)) + .build(), + builder().from("db") + .to("quarkus.datasource.jdbc.driver") + .transformer((db, context) -> Database.getDriver(db).orElse(null)) + .build(), + builder().from("db"). + to("quarkus.datasource.db-kind") + .isBuildTimeProperty(true) + .transformer(getSupportedDbValue()) + .description("The database vendor. Possible values are: " + String.join(",", supportedDatabaseVendors)) + .expectedValues(Arrays.asList(supportedDatabaseVendors)) + .build(), + builder().from("db") + .to("quarkus.datasource.jdbc.transactions") + .transformer((db, context) -> "xa") + .build(), + builder().from("db.url") + .to("quarkus.datasource.jdbc.url") + .mapFrom("db") + .transformer((value, context) -> Database.getDefaultUrl(value).orElse(value)) + .description("The database JDBC URL. If not provided, a default URL is set based on the selected database vendor. " + + "For instance, if using 'postgres', the JDBC URL would be 'jdbc:postgresql://localhost/keycloak'. " + + "The host, database and properties can be overridden by setting the following system properties," + + " respectively: -Dkc.db.url.host, -Dkc.db.url.database, -Dkc.db.url.properties.") + .build(), + builder().from("db.username") + .to("quarkus.datasource.username") + .description("The username of the database user.") + .build(), + builder().from("db.password") + .to("quarkus.datasource.password") + .description("The password of the database user.") + .isMasked(true) + .build(), + builder().from("db.schema") + .to("quarkus.datasource.schema") + .description("The database schema to be used.") + .build(), + builder().from("db.pool.initial-size") + .to("quarkus.datasource.jdbc.initial-size") + .description("The initial size of the connection pool.") + .build(), + builder().from("db.pool.min-size") + .to("quarkus.datasource.jdbc.min-size") + .description("The minimal size of the connection pool.") + .build(), + builder().from("db.pool.max-size") + .to("quarkus.datasource.jdbc.max-size") + .defaultValue(String.valueOf(100)) + .description("The maximum size of the connection pool.") + .build() + }; + } + + private static BiFunction getSupportedDbValue() { + return (db, context) -> { + if (Database.isSupported(db)) { + return Database.getDatabaseKind(db).orElse(db); + } + addInitializationException(invalidDatabaseVendor(db, "h2-file", "h2-mem", "mariadb", "mysql", "postgres", "postgres-95", "postgres-10")); + return "h2"; + }; + } + + private static PropertyMapper.Builder builder() { + return PropertyMapper.builder(ConfigCategory.DATABASE); + } +} diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/HostnamePropertyMappers.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/HostnamePropertyMappers.java new file mode 100644 index 0000000000..1c3a8b7ede --- /dev/null +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/HostnamePropertyMappers.java @@ -0,0 +1,31 @@ +package org.keycloak.quarkus.runtime.configuration.mappers; + + +import java.util.Arrays; + +final class HostnamePropertyMappers { + + private HostnamePropertyMappers(){} + + public static PropertyMapper[] getHostnamePropertyMappers() { + return new PropertyMapper[] { + builder().from("hostname-frontend-url") + .to("kc.spi.hostname.default.frontend-url") + .description("The URL that should be used to serve frontend requests that are usually sent through a public domain.") + .build(), + builder().from("hostname-admin-url") + .to("kc.spi.hostname.default.admin-url") + .description("The URL that should be used to expose the admin endpoints and console.") + .build(), + builder().from("hostname-force-backend-url-to-frontend-url") + .to("kc.spi.hostname.default.force-backend-url-to-frontend-url") + .description("Forces backend requests to go through the URL defined as the frontend-url. Defaults to false. Possible values are true or false.") + .expectedValues(Arrays.asList(Boolean.TRUE.toString(), Boolean.FALSE.toString())) + .build() + }; + } + + private static PropertyMapper.Builder builder() { + return PropertyMapper.builder(ConfigCategory.HOSTNAME); + } +} diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/HttpPropertyMappers.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/HttpPropertyMappers.java new file mode 100644 index 0000000000..fed733f719 --- /dev/null +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/HttpPropertyMappers.java @@ -0,0 +1,141 @@ +package org.keycloak.quarkus.runtime.configuration.mappers; + +import io.smallrye.config.ConfigSourceInterceptorContext; +import io.smallrye.config.ConfigValue; +import org.keycloak.quarkus.runtime.Environment; +import org.keycloak.quarkus.runtime.configuration.MicroProfileConfigProvider; + +import java.io.File; +import java.nio.file.Paths; +import java.util.Arrays; + +import static org.keycloak.quarkus.runtime.configuration.mappers.PropertyMappers.getMapper; +import static org.keycloak.quarkus.runtime.integration.QuarkusPlatform.addInitializationException; + +final class HttpPropertyMappers { + + private HttpPropertyMappers(){} + + public static PropertyMapper[] getHttpPropertyMappers() { + return new PropertyMapper[] { + builder().from("http.enabled") + .to("quarkus.http.insecure-requests") + .defaultValue(Boolean.FALSE.toString()) + .transformer(HttpPropertyMappers::getHttpEnabledTransformer) + .description("Enables the HTTP listener.") + .expectedValues(Arrays.asList(Boolean.TRUE.toString(), Boolean.FALSE.toString())) + .build(), + builder().from("http.host") + .to("quarkus.http.host") + .defaultValue("0.0.0.0") + .description("The used HTTP Host.") + .build(), + builder().from("http.port") + .to("quarkus.http.port") + .defaultValue(String.valueOf(8080)) + .description("The used HTTP port.") + .build(), + builder().from("https.port") + .to("quarkus.http.ssl-port") + .defaultValue(String.valueOf(8443)) + .description("The used HTTPS port.") + .build(), + builder().from("https.client-auth") + .to("quarkus.http.ssl.client-auth") + .defaultValue("none") + .description("Configures the server to require/request client authentication. Possible Values: none, request, required.") + .expectedValues(Arrays.asList("none", "request", "required")) + .build(), + builder().from("https.cipher-suites") + .to("quarkus.http.ssl.cipher-suites") + .description("The cipher suites to use. If none is given, a reasonable default is selected.") + .build(), + builder().from("https.protocols") + .to("quarkus.http.ssl.protocols") + .description("The list of protocols to explicitly enable.") + .build(), + builder().from("https.certificate.file") + .to("quarkus.http.ssl.certificate.file") + .description("The file path to a server certificate or certificate chain in PEM format.") + .build(), + builder().from("https.certificate.key-file") + .to("quarkus.http.ssl.certificate.key-file") + .description("The file path to a private key in PEM format.") + .build(), + builder().from("https.certificate.key-store-file") + .to("quarkus.http.ssl.certificate.key-store-file") + .defaultValue(getDefaultKeystorePathValue()) + .description("The key store which holds the certificate information instead of specifying separate files.") + .build(), + builder().from("https.certificate.key-store-password") + .to("quarkus.http.ssl.certificate.key-store-password") + .description("The password of the key store file. If not given, the default (\"password\") is used.") + .isMasked(true) + .build(), + builder().from("https.certificate.key-store-file-type") + .to("quarkus.http.ssl.certificate.key-store-file-type") + .description("The type of the key store file. " + + "If not given, the type is automatically detected based on the file name.") + .build(), + builder().from("https.certificate.trust-store-file") + .to("quarkus.http.ssl.certificate.trust-store-file") + .description("The trust store which holds the certificate information of the certificates to trust.") + .build(), + builder().from("https.certificate.trust-store-password") + .to("quarkus.http.ssl.certificate.trust-store-password") + .description("The password of the trust store file.") + .isMasked(true) + .build(), + builder().from("https.certificate.trust-store-file-type") + .to("quarkus.http.ssl.certificate.trust-store-file-type") + .defaultValue(getDefaultKeystorePathValue()) + .description("The type of the trust store file. " + + "If not given, the type is automatically detected based on the file name.") + .build() + + }; + } + + private static String getHttpEnabledTransformer(String value, ConfigSourceInterceptorContext context) { + boolean enabled = Boolean.parseBoolean(value); + ConfigValue proxy = context.proceed(MicroProfileConfigProvider.NS_KEYCLOAK_PREFIX + "proxy"); + + if (Environment.isDevMode() || Environment.isImportExportMode() + || (proxy != null && "edge".equalsIgnoreCase(proxy.getValue()))) { + enabled = true; + } + + if (!enabled) { + ConfigValue proceed = context.proceed("kc.https.certificate.file"); + + if (proceed == null || proceed.getValue() == null) { + proceed = getMapper("quarkus.http.ssl.certificate.key-store-file").getOrDefault(context, null); + } + + if (proceed == null || proceed.getValue() == null) { + addInitializationException(Messages.httpsConfigurationNotSet()); + } + } + + return enabled ? "enabled" : "disabled"; + } + + private static String getDefaultKeystorePathValue() { + String homeDir = Environment.getHomeDir(); + + if (homeDir != null) { + File file = Paths.get(homeDir, "conf", "server.keystore").toFile(); + + if (file.exists()) { + return file.getAbsolutePath(); + } + } + + return null; + } + + private static PropertyMapper.Builder builder() { + return PropertyMapper.builder(ConfigCategory.HTTP); + } +} + diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/Messages.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/Messages.java similarity index 94% rename from quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/Messages.java rename to quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/Messages.java index cae9c82c19..51b517a0f4 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/Messages.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/Messages.java @@ -15,12 +15,16 @@ * limitations under the License. */ -package org.keycloak.quarkus.runtime.configuration; +package org.keycloak.quarkus.runtime.configuration.mappers; import org.keycloak.quarkus.runtime.Environment; public final class Messages { + private Messages() { + + } + static IllegalArgumentException invalidDatabaseVendor(String db, String... availableOptions) { return new IllegalArgumentException("Invalid database vendor [" + db + "]. Possible values are: " + String.join(", ", availableOptions) + "."); } diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/MetricsPropertyMappers.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/MetricsPropertyMappers.java new file mode 100644 index 0000000000..66513533ad --- /dev/null +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/MetricsPropertyMappers.java @@ -0,0 +1,25 @@ +package org.keycloak.quarkus.runtime.configuration.mappers; + +import java.util.Arrays; + + +final class MetricsPropertyMappers { + + private MetricsPropertyMappers(){} + + public static PropertyMapper[] getMetricsPropertyMappers() { + return new PropertyMapper[] { + builder().from("metrics.enabled") + .to("quarkus.datasource.metrics.enabled") + .isBuildTimeProperty(true) + .defaultValue(Boolean.FALSE.toString()) + .description("If the server should expose metrics and healthcheck. If enabled, metrics are available at the '/metrics' endpoint and healthcheck at the '/health' endpoint.") + .expectedValues(Arrays.asList(Boolean.TRUE.toString(), Boolean.FALSE.toString())) + .build() + }; + } + + private static PropertyMapper.Builder builder() { + return PropertyMapper.builder(ConfigCategory.METRICS); + } +} diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/PropertyMapper.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/PropertyMapper.java new file mode 100644 index 0000000000..2e565f1b19 --- /dev/null +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/PropertyMapper.java @@ -0,0 +1,254 @@ +/* + * 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.configuration.mappers; + +import java.util.Collections; +import java.util.function.BiFunction; + +import io.smallrye.config.ConfigSourceInterceptorContext; +import io.smallrye.config.ConfigValue; +import org.keycloak.quarkus.runtime.configuration.MicroProfileConfigProvider; + +public class PropertyMapper { + + static PropertyMapper IDENTITY = new PropertyMapper(null, null, null, null, null,false,null,false,Collections.emptyList(),null) { + @Override + public ConfigValue getOrDefault(String name, ConfigSourceInterceptorContext context, ConfigValue current) { + return current; + } + }; + + private final String to; + private final String from; + private final String defaultValue; + private final BiFunction mapper; + private final String mapFrom; + private final boolean buildTime; + private final String description; + private final boolean mask; + private final Iterable expectedValues; + private final ConfigCategory category; + + PropertyMapper(String from, String to, String defaultValue, BiFunction mapper, + String mapFrom, boolean buildTime, String description, boolean mask, Iterable expectedValues, ConfigCategory category) { + this.from = MicroProfileConfigProvider.NS_KEYCLOAK_PREFIX + from; + this.to = to; + this.defaultValue = defaultValue; + this.mapper = mapper == null ? PropertyMapper::defaultTransformer : mapper; + this.mapFrom = mapFrom; + this.buildTime = buildTime; + this.description = description; + this.mask = mask; + this.expectedValues = expectedValues == null ? Collections.emptyList() : expectedValues; + this.category = category != null ? category : ConfigCategory.GENERAL; + } + + public static PropertyMapper.Builder builder(String fromProp, String toProp) { + return new PropertyMapper.Builder(fromProp, toProp); + } + + public static PropertyMapper.Builder builder(ConfigCategory category) { + return new PropertyMapper.Builder(category); + } + + private static String defaultTransformer(String value, ConfigSourceInterceptorContext context) { + return value; + } + + ConfigValue getOrDefault(ConfigSourceInterceptorContext context, ConfigValue current) { + return getOrDefault(null, context, current); + } + + ConfigValue getOrDefault(String name, ConfigSourceInterceptorContext context, ConfigValue current) { + String from = this.from; + + if (to != null && to.endsWith(".")) { + // in case mapping is based on prefixes instead of full property names + from = name.replace(to.substring(0, to.lastIndexOf('.')), from.substring(0, from.lastIndexOf('.'))); + } + + // try to obtain the value for the property we want to map + ConfigValue config = context.proceed(from); + + if (config == null) { + if (mapFrom != null) { + // if the property we want to map depends on another one, we use the value from the other property to call the mapper + String parentKey = MicroProfileConfigProvider.NS_KEYCLOAK + "." + mapFrom; + ConfigValue parentValue = context.proceed(parentKey); + + if (parentValue != null) { + ConfigValue value = transformValue(parentValue.getValue(), context); + + if (value != null) { + return value; + } + } + } + + // if not defined, return the current value from the property as a default if the property is not explicitly set + if (defaultValue == null + || (current != null && !current.getConfigSourceName().equalsIgnoreCase("default values"))) { + return current; + } + + if (mapper != null) { + return transformValue(defaultValue, context); + } + + return ConfigValue.builder().withName(to).withValue(defaultValue).build(); + } + + if (mapFrom != null) { + return config; + } + + ConfigValue value = transformValue(config.getValue(), context); + + // we always fallback to the current value from the property we are mapping + if (value == null) { + return current; + } + + return value; + } + + public String getFrom() { + return from; + } + + public String getDescription() { + return description; + } + + public Iterable getExpectedValues() { + return expectedValues; + } + + public String getDefaultValue() {return defaultValue; } + + public ConfigCategory getCategory() { + return category; + } + + private ConfigValue transformValue(String value, ConfigSourceInterceptorContext context) { + if (value == null) { + return null; + } + + if (mapper == null) { + return ConfigValue.builder().withName(to).withValue(value).build(); + } + + String mappedValue = mapper.apply(value, context); + + if (mappedValue != null) { + return ConfigValue.builder().withName(to).withValue(mappedValue).build(); + } + + return null; + } + + boolean isBuildTime() { + return buildTime; + } + + boolean isMask() { + return mask; + } + + public String getTo() { + return to; + } + + public static class Builder { + + private String from; + private String to; + private String defaultValue; + private BiFunction mapper; + private String description; + private String mapFrom = null; + private Iterable expectedValues = Collections.emptyList(); + private boolean isBuildTimeProperty = false; + private boolean isMasked = false; + private ConfigCategory category = ConfigCategory.GENERAL; + + public Builder(ConfigCategory category) { + this.category = category; + } + + public Builder(String fromProp, String toProp) { + this.from = fromProp; + this.to = toProp; + } + + public Builder from(String from) { + this.from = from; + return this; + } + + public Builder to(String to) { + this.to = to; + return this; + } + + + public Builder defaultValue(String defaultValue) { + this.defaultValue = defaultValue; + return this; + } + + public Builder transformer(BiFunction mapper) { + this.mapper = mapper; + return this; + } + + public Builder description(String description) { + this.description = description; + return this; + } + + public Builder mapFrom(String mapFrom) { + this.mapFrom = mapFrom; + return this; + } + + public Builder expectedValues(Iterable expectedValues) { + this.expectedValues = expectedValues; + return this; + } + + public Builder isBuildTimeProperty(boolean isBuildTime) { + this.isBuildTimeProperty = isBuildTime; + return this; + } + + public Builder isMasked(boolean isMasked) { + this.isMasked = isMasked; + return this; + } + + public Builder category(ConfigCategory category) { + this.category = category; + return this; + } + + public PropertyMapper build() { + return new PropertyMapper(from, to, defaultValue, mapper, mapFrom, isBuildTimeProperty, description, isMasked, expectedValues, category); + } + } +} diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/PropertyMappers.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/PropertyMappers.java new file mode 100644 index 0000000000..2c3d94ca73 --- /dev/null +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/PropertyMappers.java @@ -0,0 +1,159 @@ +package org.keycloak.quarkus.runtime.configuration.mappers; + +import io.smallrye.config.ConfigSourceInterceptorContext; +import io.smallrye.config.ConfigValue; +import org.keycloak.quarkus.runtime.Environment; +import org.keycloak.quarkus.runtime.configuration.MicroProfileConfigProvider; + +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +public final class PropertyMappers { + + static final Map MAPPERS = new HashMap<>(); + + private PropertyMappers(){} + + static { + addMappers(ClusteringPropertyMappers.getClusteringPropertyMappers()); + addMappers(DatabasePropertyMappers.getDatabasePropertyMappers()); + addMappers(HostnamePropertyMappers.getHostnamePropertyMappers()); + addMappers(HttpPropertyMappers.getHttpPropertyMappers()); + addMappers(MetricsPropertyMappers.getMetricsPropertyMappers()); + addMappers(ProxyPropertyMappers.getProxyPropertyMappers()); + addMappers(VaultPropertyMappers.getVaultPropertyMappers()); + } + + private static void addMappers(PropertyMapper[] mappers) { + for (PropertyMapper mapper : mappers) { + MAPPERS.put(mapper.getTo(), mapper); + } + } + + public static ConfigValue getValue(ConfigSourceInterceptorContext context, String name) { + PropertyMapper mapper = MAPPERS.getOrDefault(name, PropertyMapper.IDENTITY); + ConfigValue configValue = mapper + .getOrDefault(name, context, context.proceed(name)); + + if (configValue == null) { + Optional prefixedMapper = getPrefixedMapper(name); + + if (prefixedMapper.isPresent()) { + return MAPPERS.get(prefixedMapper.get()).getOrDefault(name, context, configValue); + } + } else { + configValue.withName(mapper.getTo()); + } + + return configValue; + } + + public static boolean isBuildTimeProperty(String name) { + if (isFeaturesBuildTimeProperty(name) || isSpiBuildTimeProperty(name)) { + return true; + } + + if (!name.startsWith(MicroProfileConfigProvider.NS_KEYCLOAK_PREFIX)) { + return false; + } + + boolean isBuildTimeProperty = MAPPERS.entrySet().stream() + .anyMatch(entry -> entry.getValue().getFrom().equals(name) && entry.getValue().isBuildTime()); + + if (!isBuildTimeProperty) { + Optional prefixedMapper = PropertyMappers.getPrefixedMapper(name); + + if (prefixedMapper.isPresent()) { + isBuildTimeProperty = MAPPERS.get(prefixedMapper.get()).isBuildTime(); + } + } + + return isBuildTimeProperty + && !"kc.version".equals(name) + && !Environment.CLI_ARGS.equals(name) + && !"kc.home.dir".equals(name) + && !"kc.config.file".equals(name) + && !Environment.PROFILE.equals(name) + && !"kc.show.config".equals(name) + && !"kc.show.config.runtime".equals(name) + && !toCLIFormat("kc.config.file").equals(name); + } + + private static boolean isSpiBuildTimeProperty(String name) { + return name.startsWith("kc.spi") && (name.endsWith("provider") || name.endsWith("enabled")); + } + + private static boolean isFeaturesBuildTimeProperty(String name) { + return name.startsWith("kc.features"); + } + + public static String toCLIFormat(String name) { + if (name.indexOf('.') == -1) { + return name; + } + return MicroProfileConfigProvider.NS_KEYCLOAK_PREFIX + .concat(name.substring(3, name.lastIndexOf('.') + 1) + .replaceAll("\\.", "-") + name.substring(name.lastIndexOf('.') + 1)); + } + + public static List getRuntimeMappers() { + return MAPPERS.values().stream() + .filter(entry -> !entry.isBuildTime()).collect(Collectors.toList()); + } + + public static List getBuildTimeMappers() { + return MAPPERS.values().stream() + .filter(PropertyMapper::isBuildTime).collect(Collectors.toList()); + } + + public static String canonicalFormat(String name) { + return name.replaceAll("-", "\\."); + } + + public static String formatValue(String property, String value) { + PropertyMapper mapper = getMapper(property); + + if (mapper != null && mapper.isMask()) { + return "*******"; + } + + return value; + } + + public static PropertyMapper getMapper(String property) { + return MAPPERS.values().stream().filter(new Predicate() { + @Override + public boolean test(PropertyMapper propertyMapper) { + return property.equals(propertyMapper.getFrom()) || property.equals(propertyMapper.getTo()); + } + }).findFirst().orElse(null); + } + + public static Collection getMappers() { + return MAPPERS.values(); + } + + private static Optional getPrefixedMapper(String name) { + return MAPPERS.entrySet().stream().filter( + new Predicate>() { + @Override + public boolean test(Map.Entry entry) { + String key = entry.getKey(); + + if (!key.endsWith(".")) { + return false; + } + + // checks both to and from mapping + return name.startsWith(key) || name.startsWith(entry.getValue().getFrom()); + } + }) + .map(Map.Entry::getKey) + .findAny(); + } +} diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/ProxyPropertyMappers.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/ProxyPropertyMappers.java new file mode 100644 index 0000000000..e364fa93e2 --- /dev/null +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/ProxyPropertyMappers.java @@ -0,0 +1,49 @@ +package org.keycloak.quarkus.runtime.configuration.mappers; + +import io.smallrye.config.ConfigSourceInterceptorContext; + +import java.util.Arrays; +import java.util.function.BiFunction; + +import static org.keycloak.quarkus.runtime.integration.QuarkusPlatform.addInitializationException; + +final class ProxyPropertyMappers { + + private static final String[] possibleProxyValues = {"none", "edge", "reencrypt", "passthrough"}; + + private ProxyPropertyMappers(){} + + public static PropertyMapper[] getProxyPropertyMappers() { + return new PropertyMapper[] { + builder().from("proxy") + .to("quarkus.http.proxy.proxy-address-forwarding") + .defaultValue("none") + .transformer(getValidProxyModeValue()) + .expectedValues(Arrays.asList(possibleProxyValues)) + .description("The proxy address forwarding mode if the server is behind a reverse proxy. " + + "Possible values are: " + String.join(",",possibleProxyValues)) + .category(ConfigCategory.PROXY) + .build() + }; + } + + private static BiFunction getValidProxyModeValue() { + return (mode, context) -> { + switch (mode) { + case "none": + return "false"; + case "edge": + case "reencrypt": + case "passthrough": + return "true"; + default: + addInitializationException(Messages.invalidProxyMode(mode)); + return "false"; + } + }; + } + + private static PropertyMapper.Builder builder() { + return PropertyMapper.builder(ConfigCategory.PROXY); + } +} diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/VaultPropertyMappers.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/VaultPropertyMappers.java new file mode 100644 index 0000000000..37636063c4 --- /dev/null +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/VaultPropertyMappers.java @@ -0,0 +1,31 @@ +package org.keycloak.quarkus.runtime.configuration.mappers; + +final class VaultPropertyMappers { + + private VaultPropertyMappers() { + } + + public static PropertyMapper[] getVaultPropertyMappers() { + return new PropertyMapper[] { + builder() + .from("vault.file.path") + .to("kc.spi.vault.files-plaintext.dir") + .description("If set, secrets can be obtained by reading the content of files within the given path.") + .build(), + builder() + .from("vault.hashicorp.") + .to("quarkus.vault.") + .description("If set, secrets can be obtained from Hashicorp Vault.") + .build(), + builder() + .from("vault.hashicorp.paths") + .to("kc.spi.vault.hashicorp.paths") + .description("A set of one or more paths that should be used when looking up secrets.") + .build() + }; + } + + private static PropertyMapper.Builder builder() { + return PropertyMapper.builder(ConfigCategory.VAULT).isBuildTimeProperty(true); + } +} diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/containers/KeycloakQuarkusServerDeployableContainer.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/containers/KeycloakQuarkusServerDeployableContainer.java index 64317ad008..4c3df6beaf 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/containers/KeycloakQuarkusServerDeployableContainer.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/containers/KeycloakQuarkusServerDeployableContainer.java @@ -171,6 +171,7 @@ public class KeycloakQuarkusServerDeployableContainer implements DeployableConta List commands = new ArrayList<>(); commands.add("./kc.sh"); + commands.add("start"); commands.add("-Dquarkus.http.root-path=/auth"); commands.add("--auto-build"); commands.add("--http-enabled=true");