KEYCLOAK-19308 Grouping for help commands and refactoring of Propertymapper usage to provida a fluid API

This commit is contained in:
Dominik Guhr 2021-10-29 09:37:37 +02:00 committed by Pedro Igor
parent 439e2e4288
commit 579c5462b2
36 changed files with 1115 additions and 718 deletions

View file

@ -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;

View file

@ -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

View file

@ -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<String> 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);

View file

@ -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<String> 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());
}
}
private static boolean hasAutoBuildOption(List<String> 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<String> cliArgs) {
CommandSpec spec = CommandSpec.forAnnotatedObject(new Main())
.name(Environment.getCommand());
boolean isStartCommand = cliArgs.size() == 1 && cliArgs.contains(Start.NAME);
// 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, Start.NAME, hasAutoBuildOption(cliArgs));
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);
}
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<String> iterator = argsList.iterator();
boolean expectValue = false;
List<String> ignoredArgs = asList("--verbose", "-v", "--help", "-h", AUTO_BUILD_OPTION);
List<String> 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<PropertyMapper> 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<String> 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("<feature>")
.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<PropertyMapper> propertyMappers) {
for(ConfigCategory category : ConfigCategory.values()) {
List<PropertyMapper> 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;
}
addOption(commandSpec, "--features", "Enables a group of features. Possible values are: "
+ String.join(",", Arrays.stream(Profile.Type.values()).map(
type -> type.name().toLowerCase()).toArray((IntFunction<CharSequence[]>) String[]::new)), null);
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());
}
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());
cSpec.addArgGroup(argGroupBuilder.build());
}
commandSpec.addOption(builder.build());
}
public static List<String> getCliArgs(CommandLine cmd) {

View file

@ -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 = "<config-file>",
scope = CommandLine.ScopeType.INHERIT)
public void setConfigFile(String path) {
System.setProperty(KeycloakConfigSourceProvider.KEYCLOAK_CONFIG_FILE_PROP, path);
}
}

View file

@ -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() {

View file

@ -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 <OPTIONS>%n%n"
+ " $ ${PARENT-COMMAND-FULL-NAME:-$PARENTCOMMAND} start --auto-build <OPTIONS>%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";

View file

@ -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 {
}

View file

@ -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 {

View file

@ -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 {

View file

@ -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 <OPTIONS>%n%n"
+ " Start the server in production mode:%n%n"
+ " $ ${COMMAND-NAME} <OPTIONS>%n%n"
+ " $ ${COMMAND-NAME} start <OPTIONS>%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} <command> --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<key>=<value>",
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 = "<path>")
public void setConfigFile(String path) {
System.setProperty(KeycloakConfigSourceProvider.KEYCLOAK_CONFIG_FILE_PROP, path);
}
}

View file

@ -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) {

View file

@ -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 <OPTIONS>%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;
}

View file

@ -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();

View file

@ -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})

View file

@ -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"
};
}
}

View file

@ -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);

View file

@ -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<String> 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<String> 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<String> 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;
}
}

View file

@ -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<String, String> 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;
}

View file

@ -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<String, String>) (Map) properties);
return transform((Map) properties);
} catch (IOException e) {
throw new IOError(e);
}

View file

@ -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<String> 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<String, ConfigSourceInterceptorContext, String> 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<String, ConfigSourceInterceptorContext, String> transformer, String description,
Iterable<String> expectedValues) {
return MAPPERS.computeIfAbsent(toProperty, s -> new PropertyMapper(fromProperty, s, defaultValue, transformer, description, expectedValues));
}
static PropertyMapper create(String fromProperty, String toProperty, BiFunction<String, ConfigSourceInterceptorContext, String> 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<String, ConfigSourceInterceptorContext, String> transformer, String description) {
return MAPPERS.computeIfAbsent(toProperty, s -> new PropertyMapper(fromProperty, s, null, transformer, mapFrom, description));
}
static PropertyMapper createBuildTimeProperty(String fromProperty, String toProperty, BiFunction<String, ConfigSourceInterceptorContext, String> 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<String, ConfigSourceInterceptorContext, String> transformer, String description,
Iterable<String> 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<String> 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<String, ConfigSourceInterceptorContext, String> transformer, String description,
Iterable<String> expectedValues) {
return MAPPERS.computeIfAbsent(toProperty, s -> new PropertyMapper(fromProperty, s, defaultValue, transformer, null, true, description, false, expectedValues));
}
static Map<String, PropertyMapper> 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<String, ConfigSourceInterceptorContext, String> mapper;
private final String mapFrom;
private final boolean buildTime;
private final String description;
private final boolean mask;
private final Iterable<String> expectedValues;
PropertyMapper(String from, String to, String defaultValue, BiFunction<String, ConfigSourceInterceptorContext, String> mapper, String description) {
this(from, to, defaultValue, mapper, null, description);
}
PropertyMapper(String from, String to, String defaultValue, BiFunction<String, ConfigSourceInterceptorContext, String> mapper,
String description, Iterable<String> expectedValues) {
this(from, to, defaultValue, mapper, null, false, description, false, expectedValues);
}
PropertyMapper(String from, String to, String defaultValue, BiFunction<String, ConfigSourceInterceptorContext, String> mapper, String mapFrom, String description) {
this(from, to, defaultValue, mapper, mapFrom, false, description, false);
}
PropertyMapper(String from, String to, String defaultValue, BiFunction<String, ConfigSourceInterceptorContext, String> 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<String, ConfigSourceInterceptorContext, String> mapper,
String mapFrom, boolean buildTime, String description, boolean mask, Iterable<String> 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<String> getExpectedValues() {
return expectedValues;
}
}

View file

@ -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<String>() {
@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<String> 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<String> getPrefixedMapper(String name) {
Optional<String> prefixedMapper = MAPPERS.keySet().stream().filter(new Predicate<String>() {
@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<PropertyMapper> getRuntimeMappers() {
return PropertyMapper.MAPPERS.values().stream()
.filter(entry -> !entry.isBuildTime()).collect(Collectors.toList());
}
public static List<PropertyMapper> getBuiltTimeMappers() {
return PropertyMapper.MAPPERS.values().stream()
.filter(entry -> entry.isBuildTime()).collect(Collectors.toList());
}
public static Collection<PropertyMapper> 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<PropertyMapper>() {
@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<String> 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<String> getBuiltTimeProperty(String name) {
String value = Configuration.getBuiltTimeProperty(name);
if (value == null) {
return Optional.empty();
}
return Optional.of(value);
}
public static Optional<String> 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;
}
}

View file

@ -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;
/**
* <p>This interceptor is responsible for mapping Keycloak properties to their corresponding properties in Quarkus.

View file

@ -58,6 +58,7 @@ public class SysPropConfigSource implements ConfigSource {
return "KcSysPropConfigSource";
}
@Override
public int getOrdinal() {
return 400;
}

View file

@ -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);
}
}

View file

@ -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;
}
}

View file

@ -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<String, ConfigSourceInterceptorContext, String> 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);
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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) + ".");
}

View file

@ -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);
}
}

View file

@ -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<String, ConfigSourceInterceptorContext, String> mapper;
private final String mapFrom;
private final boolean buildTime;
private final String description;
private final boolean mask;
private final Iterable<String> expectedValues;
private final ConfigCategory category;
PropertyMapper(String from, String to, String defaultValue, BiFunction<String, ConfigSourceInterceptorContext, String> mapper,
String mapFrom, boolean buildTime, String description, boolean mask, Iterable<String> 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<String> 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<String, ConfigSourceInterceptorContext, String> mapper;
private String description;
private String mapFrom = null;
private Iterable<String> 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<String, ConfigSourceInterceptorContext, String> 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<String> 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);
}
}
}

View file

@ -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<String, PropertyMapper> 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<String> 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<String> 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<PropertyMapper> getRuntimeMappers() {
return MAPPERS.values().stream()
.filter(entry -> !entry.isBuildTime()).collect(Collectors.toList());
}
public static List<PropertyMapper> 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<PropertyMapper>() {
@Override
public boolean test(PropertyMapper propertyMapper) {
return property.equals(propertyMapper.getFrom()) || property.equals(propertyMapper.getTo());
}
}).findFirst().orElse(null);
}
public static Collection<PropertyMapper> getMappers() {
return MAPPERS.values();
}
private static Optional<String> getPrefixedMapper(String name) {
return MAPPERS.entrySet().stream().filter(
new Predicate<Map.Entry<String, PropertyMapper>>() {
@Override
public boolean test(Map.Entry<String, PropertyMapper> 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();
}
}

View file

@ -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<String, ConfigSourceInterceptorContext, String> 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);
}
}

View file

@ -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);
}
}

View file

@ -171,6 +171,7 @@ public class KeycloakQuarkusServerDeployableContainer implements DeployableConta
List<String> 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");