Conditionally enable and disable CLI options (#25333)

* Conditionally enable and disable CLI options

Closes #13113

Signed-off-by: Martin Bartoš <mabartos@redhat.com>

* Support for duplicates in config

Signed-off-by: Martin Bartoš <mabartos@redhat.com>

* Fix rendering config options in docs

Fixes #26515

Signed-off-by: Martin Bartoš <mabartos@redhat.com>

* Reorder OptionsDistTest

Signed-off-by: Martin Bartoš <mabartos@redhat.com>

---------

Signed-off-by: Martin Bartoš <mabartos@redhat.com>
This commit is contained in:
Martin Bartoš 2024-03-07 21:36:43 +01:00 committed by GitHub
parent 40385061f7
commit e4aa1b5f95
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
45 changed files with 1250 additions and 489 deletions

View file

@ -215,7 +215,7 @@ public class Profile {
private static final Logger logger = Logger.getLogger(Profile.class); private static final Logger logger = Logger.getLogger(Profile.class);
private static Profile CURRENT; private static volatile Profile CURRENT;
private final ProfileName profileName; private final ProfileName profileName;

View file

@ -1,6 +1,6 @@
<#import "/templates/options.adoc" as opts> <#import "/templates/options.adoc" as opts>
<#macro guide title summary priority=999 includedOptions="" preview="" tileVisible="true" previewDiscussionLink=""> <#macro guide title summary priority=999 deniedCategories="" includedOptions="" preview="" tileVisible="true" previewDiscussionLink="">
:guide-id: ${id} :guide-id: ${id}
:guide-title: ${title} :guide-title: ${title}
:guide-summary: ${summary} :guide-summary: ${summary}
@ -28,6 +28,6 @@ endif::[]
<#if includedOptions?has_content> <#if includedOptions?has_content>
== Relevant options == Relevant options
<@opts.list options=ctx.options.getOptions(includedOptions) anchor=false></@opts.list> <@opts.list options=ctx.options.getOptions(includedOptions, deniedCategories) anchor=false></@opts.list>
</#if> </#if>
</#macro> </#macro>

View file

@ -23,6 +23,10 @@
*Env:* `${option.keyEnv}` *Env:* `${option.keyEnv}`
-- --
<#if option.enabledWhen?has_content>
${option.enabledWhen!}
</#if>
<#if option.deprecated?has_content> <#if option.deprecated?has_content>
<#-- Either mark the whole option as deprecated, or just selected values --> <#-- Either mark the whole option as deprecated, or just selected values -->
<#if !option.deprecated.deprecatedValues?has_content> <#if !option.deprecated.deprecatedValues?has_content>

View file

@ -15,40 +15,54 @@ import org.keycloak.provider.Spi;
import org.keycloak.quarkus.runtime.Providers; import org.keycloak.quarkus.runtime.Providers;
import org.keycloak.quarkus.runtime.configuration.Configuration; import org.keycloak.quarkus.runtime.configuration.Configuration;
import org.keycloak.quarkus.runtime.configuration.mappers.PropertyMappers; import org.keycloak.quarkus.runtime.configuration.mappers.PropertyMappers;
import org.keycloak.utils.StringUtil;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
import java.util.EnumMap;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.TreeSet;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.StreamSupport; import java.util.stream.StreamSupport;
public class Options { public class Options {
private final Map<String, Option> options; private final Map<OptionCategory, Set<Option>> options;
private final Map<String, Map<String, List<Option>>> providerOptions = new LinkedHashMap<>(); private final Map<String, Map<String, List<Option>>> providerOptions = new LinkedHashMap<>();
@SuppressWarnings("unchecked")
public Options() { public Options() {
options = PropertyMappers.getMappers().stream() this.options = new EnumMap<>(OptionCategory.class);
PropertyMappers.getMappers().stream()
.filter(m -> !m.isHidden()) .filter(m -> !m.isHidden())
.filter(propertyMapper -> Objects.nonNull(propertyMapper.getDescription())) .filter(propertyMapper -> Objects.nonNull(propertyMapper.getDescription()))
.map(m -> new Option(m.getFrom(), m.getCategory(), m.isBuildTime(), null, m.getDescription(), (String) m.getDefaultValue().map(Object::toString).orElse(null), m.getExpectedValues(), (DeprecatedMetadata) m.getDeprecatedMetadata().orElse(null))) .map(m -> new Option(m.getFrom(),
.sorted(Comparator.comparing(Option::getKey)) m.getCategory(),
.collect(Collectors.toMap(Option::getKey, o -> o, (o1, o2) -> o1, LinkedHashMap::new)); // Need to ignore duplicate keys?? m.isBuildTime(),
null,
m.getDescription(),
m.getDefaultValue().map(Object::toString).orElse(null),
m.getExpectedValues(),
m.getEnabledWhen().orElse(""),
m.getDeprecatedMetadata().orElse(null)))
.forEach(o -> options.computeIfAbsent(o.category, k -> new TreeSet<>(Comparator.comparing(Option::getKey))).add(o));
ProviderManager providerManager = Providers.getProviderManager(Thread.currentThread().getContextClassLoader()); ProviderManager providerManager = Providers.getProviderManager(Thread.currentThread().getContextClassLoader());
options.forEach((s, option) -> { options.values()
option.description = option.description.replaceAll("'([^ ]*)'", "`$1`"); .stream()
}); .flatMap(Collection::stream)
.forEach(option -> option.description = option.description.replaceAll("'([^ ]*)'", "`$1`"));
for (Spi loadSpi : providerManager.loadSpis().stream().sorted(Comparator.comparing(Spi::getName)).collect(Collectors.toList())) { for (Spi loadSpi : providerManager.loadSpis().stream().sorted(Comparator.comparing(Spi::getName)).toList()) {
for (ProviderFactory providerFactory : providerManager.load(loadSpi).stream().sorted(Comparator.comparing(ProviderFactory::getId)).collect(Collectors.toList())) { for (ProviderFactory<?> providerFactory : providerManager.load(loadSpi).stream().sorted(Comparator.comparing(ProviderFactory::getId)).toList()) {
List<ProviderConfigProperty> configMetadata = providerFactory.getConfigMetadata(); List<ProviderConfigProperty> configMetadata = providerFactory.getConfigMetadata();
if (configMetadata == null) { if (configMetadata == null) {
@ -62,6 +76,7 @@ public class Options {
m.getHelpText(), m.getHelpText(),
m.getDefaultValue() == null ? null : m.getDefaultValue().toString(), m.getDefaultValue() == null ? null : m.getDefaultValue().toString(),
m.getOptions() == null ? Collections.emptyList() : m.getOptions(), m.getOptions() == null ? Collections.emptyList() : m.getOptions(),
"",
null)) null))
.sorted(Comparator.comparing(Option::getKey)).collect(Collectors.toList()); .sorted(Comparator.comparing(Option::getKey)).collect(Collectors.toList());
@ -88,39 +103,84 @@ public class Options {
.collect(Collectors.toList()); .collect(Collectors.toList());
} }
public Collection<Option> getValues() {
return options.values();
}
public Collection<Option> getValues(OptionCategory category) { public Collection<Option> getValues(OptionCategory category) {
return options.values().stream().filter(o -> o.category.equals(category)).collect(Collectors.toList()); return options.getOrDefault(category, Collections.emptySet());
} }
public Option getOption(String key) { public Option getOption(String key) {
return options.get(key); Set<Option> foundOptions = options.values().stream().flatMap(Collection::stream).filter(f -> f.getKey().equals(key)).collect(Collectors.toSet());
if (foundOptions.size() > 1) {
final var categories = foundOptions.stream().map(f -> f.category).map(OptionCategory::getHeading).collect(Collectors.joining(","));
throw new IllegalArgumentException(String.format("Ambiguous options '%s' with categories: %s\n", key, categories));
}
return foundOptions.iterator().next();
} }
public List<Option> getOptions(String includeOptions) { /**
* Get denied categories for guide options
* <p>
* Used in cases when multiple options can be found under the same name
* By providing 'deniedCategories' parameter, we will not search for the option in these categories
* <p>
* f.e. when we specify {@code includedOptions="hostname"}, we should provide also {@code deniedCategories="hostname_v2"}
* In that case, we will use the option from the old hostname provider
*
* @return denied categories, otherwise an empty set
*/
public Set<OptionCategory> getDeniedCategories(String deniedCategories) {
return Optional.ofNullable(deniedCategories)
.filter(StringUtil::isNotBlank)
.map(f -> f.split(" "))
.map(Arrays::asList)
.map(f -> f.stream()
.map(g -> {
try {
return OptionCategory.valueOf(g.toUpperCase());
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException("You have specified wrong category name in the 'deniedCategories' property", e);
}
})
.collect(Collectors.toSet()))
.orElseGet(Collections::emptySet);
}
public List<Option> getOptions(String includeOptions, String deniedCategories) {
final String r = includeOptions.replaceAll("\\.", "\\\\.").replaceAll("\\*", ".*").replace(' ', '|'); final String r = includeOptions.replaceAll("\\.", "\\\\.").replaceAll("\\*", ".*").replace(' ', '|');
return this.options.values().stream().filter(o -> o.getKey().matches(r)).collect(Collectors.toList()); final Set<OptionCategory> denied = getDeniedCategories(deniedCategories);
return options.values()
.stream()
.flatMap(Collection::stream)
.filter(f -> !denied.contains(f.category))
.filter(f -> f.getKey().matches(r))
.toList();
} }
public Map<String, Map<String, List<Option>>> getProviderOptions() { public Map<String, Map<String, List<Option>>> getProviderOptions() {
return providerOptions; return providerOptions;
} }
public class Option { public static class Option {
private String key; private final String key;
private OptionCategory category; private final OptionCategory category;
private boolean build; private final boolean build;
private String type; private final String type;
private String description; private String description;
private String defaultValue; private final String defaultValue;
private List<String> expectedValues; private List<String> expectedValues;
private DeprecatedMetadata deprecated; private final String enabledWhen;
private final DeprecatedMetadata deprecated;
public Option(String key, OptionCategory category, boolean build, String type, String description, String defaultValue, Iterable<String> expectedValues, DeprecatedMetadata deprecatedMetadata) { public Option(String key,
OptionCategory category,
boolean build,
String type,
String description,
String defaultValue,
Iterable<String> expectedValues,
String enabledWhen,
DeprecatedMetadata deprecatedMetadata) {
this.key = key; this.key = key;
this.category = category; this.category = category;
this.build = build; this.build = build;
@ -128,6 +188,7 @@ public class Options {
this.description = description; this.description = description;
this.defaultValue = defaultValue; this.defaultValue = defaultValue;
this.expectedValues = StreamSupport.stream(expectedValues.spliterator(), false).collect(Collectors.toList()); this.expectedValues = StreamSupport.stream(expectedValues.spliterator(), false).collect(Collectors.toList());
this.enabledWhen = enabledWhen;
this.deprecated = deprecatedMetadata; this.deprecated = deprecatedMetadata;
} }
@ -178,6 +239,11 @@ public class Options {
return expectedValues; return expectedValues;
} }
public String getEnabledWhen() {
if (StringUtil.isBlank(enabledWhen)) return null;
return enabledWhen;
}
public DeprecatedMetadata getDeprecated() { public DeprecatedMetadata getDeprecated() {
return deprecated; return deprecated;
} }

View file

@ -44,14 +44,10 @@ public enum OptionCategory {
} }
private String getHeadingBySupportLevel(String heading) { private String getHeadingBySupportLevel(String heading) {
if (this.supportLevel.equals(ConfigSupportLevel.EXPERIMENTAL)){ return switch (supportLevel) {
heading = heading + " (Experimental)"; case EXPERIMENTAL -> heading + " (Experimental)";
} case PREVIEW -> heading + " (Preview)";
default -> heading;
if (this.supportLevel.equals(ConfigSupportLevel.PREVIEW)){ };
heading = heading + " (Preview)";
}
return heading;
} }
} }

View file

@ -68,6 +68,8 @@ import org.keycloak.common.Profile;
import org.keycloak.common.crypto.FipsMode; import org.keycloak.common.crypto.FipsMode;
import org.keycloak.common.util.StreamUtil; import org.keycloak.common.util.StreamUtil;
import org.keycloak.config.DatabaseOptions; import org.keycloak.config.DatabaseOptions;
import org.keycloak.config.HealthOptions;
import org.keycloak.config.MetricsOptions;
import org.keycloak.config.SecurityOptions; import org.keycloak.config.SecurityOptions;
import org.keycloak.connections.jpa.DefaultJpaConnectionProviderFactory; import org.keycloak.connections.jpa.DefaultJpaConnectionProviderFactory;
import org.keycloak.connections.jpa.JpaConnectionProvider; import org.keycloak.connections.jpa.JpaConnectionProvider;
@ -915,11 +917,11 @@ class KeycloakProcessor {
} }
private boolean isMetricsEnabled() { private boolean isMetricsEnabled() {
return Configuration.getOptionalBooleanValue(NS_KEYCLOAK_PREFIX.concat("metrics-enabled")).orElse(false); return Configuration.isTrue(MetricsOptions.METRICS_ENABLED);
} }
private boolean isHealthEnabled() { private boolean isHealthEnabled() {
return Configuration.getOptionalBooleanValue(NS_KEYCLOAK_PREFIX.concat("health-enabled")).orElse(false); return Configuration.isTrue(HealthOptions.HEALTH_ENABLED);
} }
static JdbcDataSourceBuildItem getDefaultDataSource(List<JdbcDataSourceBuildItem> jdbcDataSources) { static JdbcDataSourceBuildItem getDefaultDataSource(List<JdbcDataSourceBuildItem> jdbcDataSources) {

View file

@ -38,6 +38,7 @@ import org.apache.commons.lang3.SystemUtils;
import org.keycloak.common.Profile; import org.keycloak.common.Profile;
import org.keycloak.common.profile.PropertiesFileProfileConfigResolver; import org.keycloak.common.profile.PropertiesFileProfileConfigResolver;
import org.keycloak.common.profile.PropertiesProfileConfigResolver; import org.keycloak.common.profile.PropertiesProfileConfigResolver;
import org.keycloak.quarkus.runtime.cli.command.AbstractCommand;
import org.keycloak.quarkus.runtime.configuration.PersistedConfigSource; import org.keycloak.quarkus.runtime.configuration.PersistedConfigSource;
public final class Environment { public final class Environment {
@ -50,6 +51,9 @@ public final class Environment {
public static final String DEV_PROFILE_VALUE = "dev"; public static final String DEV_PROFILE_VALUE = "dev";
public static final String PROD_PROFILE_VALUE = "prod"; public static final String PROD_PROFILE_VALUE = "prod";
public static final String LAUNCH_MODE = "kc.launch.mode"; public static final String LAUNCH_MODE = "kc.launch.mode";
private static volatile AbstractCommand parsedCommand;
private Environment() {} private Environment() {}
public static Boolean isRebuild() { public static Boolean isRebuild() {
@ -255,4 +259,19 @@ public final class Environment {
return profile; return profile;
} }
/**
* Get parsed AbstractCommand we obtained from the CLI
*/
public static Optional<AbstractCommand> getParsedCommand() {
return Optional.ofNullable(parsedCommand);
}
public static boolean isParsedCommand(String commandName) {
return getParsedCommand().filter(f -> f.getName().equals(commandName)).isPresent();
}
public static void setParsedCommand(AbstractCommand command) {
Environment.parsedCommand = command;
}
} }

View file

@ -31,6 +31,8 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.context.ApplicationScoped;
import org.keycloak.common.profile.ProfileException;
import org.keycloak.quarkus.runtime.configuration.mappers.PropertyMappers;
import picocli.CommandLine.ExitCode; import picocli.CommandLine.ExitCode;
import io.quarkus.runtime.ApplicationLifecycleManager; import io.quarkus.runtime.ApplicationLifecycleManager;
@ -68,10 +70,7 @@ public class KeycloakMain implements QuarkusApplication {
try { try {
cliArgs = Picocli.parseArgs(args); cliArgs = Picocli.parseArgs(args);
} catch (PropertyException e) { } catch (PropertyException e) {
ExecutionExceptionHandler errorHandler = new ExecutionExceptionHandler(); handleUsageError(e.getMessage());
PrintWriter errStream = new PrintWriter(System.err, true);
errorHandler.error(errStream, e.getMessage(), null);
System.exit(ExitCode.USAGE);
return; return;
} }
@ -79,25 +78,24 @@ public class KeycloakMain implements QuarkusApplication {
cliArgs = new ArrayList<>(cliArgs); cliArgs = new ArrayList<>(cliArgs);
// default to show help message // default to show help message
cliArgs.add("-h"); cliArgs.add("-h");
} else if (isFastStart(cliArgs)) { } else if (isFastStart(cliArgs)) { // fast path for starting the server without bootstrapping CLI
// fast path for starting the server without bootstrapping CLI
ExecutionExceptionHandler errorHandler = new ExecutionExceptionHandler();
PrintWriter errStream = new PrintWriter(System.err, true);
if (isDevProfileNotAllowed()) { if (isDevProfileNotAllowed()) {
errorHandler.error(errStream, Messages.devProfileNotAllowedError(Start.NAME), null); handleUsageError(Messages.devProfileNotAllowedError(Start.NAME));
System.exit(ExitCode.USAGE);
return; return;
} }
try { try {
PropertyMappers.sanitizeDisabledMappers();
Picocli.validateConfig(cliArgs, new Start()); Picocli.validateConfig(cliArgs, new Start());
} catch (PropertyException e) { } catch (PropertyException | ProfileException e) {
errorHandler.error(errStream, e.getMessage(), null); handleUsageError(e.getMessage(), e.getCause());
System.exit(ExitCode.USAGE);
return; return;
} }
ExecutionExceptionHandler errorHandler = new ExecutionExceptionHandler();
PrintWriter errStream = new PrintWriter(System.err, true);
start(errorHandler, errStream, args); start(errorHandler, errStream, args);
return; return;
@ -107,6 +105,17 @@ public class KeycloakMain implements QuarkusApplication {
parseAndRun(cliArgs); parseAndRun(cliArgs);
} }
private static void handleUsageError(String message) {
handleUsageError(message, null);
}
private static void handleUsageError(String message, Throwable cause) {
ExecutionExceptionHandler errorHandler = new ExecutionExceptionHandler();
PrintWriter errStream = new PrintWriter(System.err, true);
errorHandler.error(errStream, message, cause);
System.exit(ExitCode.USAGE);
}
private static boolean isFastStart(List<String> cliArgs) { private static boolean isFastStart(List<String> cliArgs) {
// 'start --optimized' should start the server without parsing CLI // 'start --optimized' should start the server without parsing CLI
return cliArgs.size() == 2 && cliArgs.get(0).equals(Start.NAME) && cliArgs.stream().anyMatch(OPTIMIZED_BUILD_OPTION_LONG::equals); return cliArgs.size() == 2 && cliArgs.get(0).equals(Start.NAME) && cliArgs.stream().anyMatch(OPTIMIZED_BUILD_OPTION_LONG::equals);

View file

@ -161,8 +161,21 @@ public final class Help extends CommandLine.Help {
PropertyMapper<?> mapper = getMapper(option.longestName()); PropertyMapper<?> mapper = getMapper(option.longestName());
if (mapper == null) { if (mapper == null) {
final var disabledMapper = PropertyMappers.getDisabledMapper(option.longestName());
final var isDisabledMapper = disabledMapper.isPresent();
// Show disabled mappers, which do not have a description when they're enabled
final var isEnabledWhenEmpty = isDisabledMapper && disabledMapper.get().getEnabledWhen().isEmpty();
if (isEnabledWhenEmpty) {
return true;
}
if (allOptions && isDisabledMapper) {
return true;
}
// only filter mapped options, defaults to the hidden marker // only filter mapped options, defaults to the hidden marker
return !option.hidden(); return !option.hidden() && !isDisabledMapper;
} }
boolean isUnsupportedOption = !PropertyMappers.isSupported(mapper); boolean isUnsupportedOption = !PropertyMappers.isSupported(mapper);

View file

@ -17,8 +17,10 @@
package org.keycloak.quarkus.runtime.cli; package org.keycloak.quarkus.runtime.cli;
import static java.lang.String.format;
import static java.util.Optional.ofNullable; import static java.util.Optional.ofNullable;
import static java.util.stream.StreamSupport.stream; import static java.util.stream.StreamSupport.stream;
import static org.keycloak.quarkus.runtime.Environment.isRebuild;
import static org.keycloak.quarkus.runtime.Environment.isRebuildCheck; import static org.keycloak.quarkus.runtime.Environment.isRebuildCheck;
import static org.keycloak.quarkus.runtime.Environment.isRebuilt; import static org.keycloak.quarkus.runtime.Environment.isRebuilt;
import static org.keycloak.quarkus.runtime.cli.command.AbstractStartCommand.OPTIMIZED_BUILD_OPTION_LONG; import static org.keycloak.quarkus.runtime.cli.command.AbstractStartCommand.OPTIMIZED_BUILD_OPTION_LONG;
@ -40,7 +42,6 @@ import java.io.File;
import java.io.PrintWriter; import java.io.PrintWriter;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections;
import java.util.EnumMap; import java.util.EnumMap;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
@ -56,11 +57,13 @@ import java.util.stream.Collectors;
import org.eclipse.microprofile.config.spi.ConfigSource; import org.eclipse.microprofile.config.spi.ConfigSource;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.common.profile.ProfileException;
import org.keycloak.config.DeprecatedMetadata; import org.keycloak.config.DeprecatedMetadata;
import org.keycloak.config.Option; import org.keycloak.config.Option;
import org.keycloak.config.OptionCategory; import org.keycloak.config.OptionCategory;
import org.keycloak.quarkus.runtime.cli.command.AbstractCommand; import org.keycloak.quarkus.runtime.cli.command.AbstractCommand;
import org.keycloak.quarkus.runtime.cli.command.Build; import org.keycloak.quarkus.runtime.cli.command.Build;
import org.keycloak.quarkus.runtime.cli.command.HelpAllMixin;
import org.keycloak.quarkus.runtime.cli.command.ImportRealmMixin; import org.keycloak.quarkus.runtime.cli.command.ImportRealmMixin;
import org.keycloak.quarkus.runtime.cli.command.Main; import org.keycloak.quarkus.runtime.cli.command.Main;
import org.keycloak.quarkus.runtime.cli.command.ShowConfig; import org.keycloak.quarkus.runtime.cli.command.ShowConfig;
@ -68,6 +71,8 @@ import org.keycloak.quarkus.runtime.cli.command.StartDev;
import org.keycloak.quarkus.runtime.cli.command.Tools; import org.keycloak.quarkus.runtime.cli.command.Tools;
import org.keycloak.quarkus.runtime.configuration.ConfigArgsConfigSource; import org.keycloak.quarkus.runtime.configuration.ConfigArgsConfigSource;
import org.keycloak.quarkus.runtime.configuration.Configuration; import org.keycloak.quarkus.runtime.configuration.Configuration;
import org.keycloak.quarkus.runtime.configuration.DisabledMappersInterceptor;
import org.keycloak.quarkus.runtime.configuration.KcUnmatchedArgumentException;
import org.keycloak.quarkus.runtime.configuration.PersistedConfigSource; import org.keycloak.quarkus.runtime.configuration.PersistedConfigSource;
import org.keycloak.quarkus.runtime.configuration.PropertyMappingInterceptor; import org.keycloak.quarkus.runtime.configuration.PropertyMappingInterceptor;
import org.keycloak.quarkus.runtime.configuration.QuarkusPropertiesConfigSource; import org.keycloak.quarkus.runtime.configuration.QuarkusPropertiesConfigSource;
@ -94,6 +99,7 @@ public final class Picocli {
private static class IncludeOptions { private static class IncludeOptions {
boolean includeRuntime; boolean includeRuntime;
boolean includeBuildTime; boolean includeBuildTime;
boolean includeDisabled;
} }
private Picocli() { private Picocli() {
@ -101,31 +107,45 @@ public final class Picocli {
public static void parseAndRun(List<String> cliArgs) { public static void parseAndRun(List<String> cliArgs) {
CommandLine cmd = createCommandLine(cliArgs); CommandLine cmd = createCommandLine(cliArgs);
String[] argArray = cliArgs.toArray(new String[0]); String[] argArray = cliArgs.toArray(new String[0]);
if (Environment.isRebuildCheck()) {
int exitCode = 0;
try {
// process the cli args first to init the config file and perform validation
cmd.parseArgs(argArray);
exitCode = runReAugmentationIfNeeded(cliArgs, cmd);
} catch (ParameterException ex) {
try {
exitCode = cmd.getParameterExceptionHandler().handleParseException(ex, argArray);
} catch (Exception e) {
ExecutionExceptionHandler errorHandler = new ExecutionExceptionHandler();
errorHandler.error(cmd.getErr(), e.getMessage(), null);
exitCode = ex.getCommandLine().getCommandSpec().exitCodeOnInvalidInput();
}
}
exitOnFailure(exitCode, cmd);
return;
}
int exitCode = cmd.execute(argArray); try {
cmd.parseArgs(argArray); // process the cli args first to init the config file and perform validation
int exitCode;
if (isRebuildCheck()) {
exitCode = runReAugmentationIfNeeded(cliArgs, cmd);
} else {
PropertyMappers.sanitizeDisabledMappers();
exitCode = cmd.execute(argArray);
}
exitOnFailure(exitCode, cmd);
} catch (ParameterException parEx) {
catchParameterException(parEx, cmd, argArray);
} catch (ProfileException | PropertyException proEx) {
catchProfileException(proEx.getMessage(), proEx.getCause(), cmd);
}
}
private static void catchParameterException(ParameterException parEx, CommandLine cmd, String[] args) {
int exitCode;
try {
exitCode = cmd.getParameterExceptionHandler().handleParseException(parEx, args);
} catch (Exception e) {
ExecutionExceptionHandler errorHandler = new ExecutionExceptionHandler();
errorHandler.error(cmd.getErr(), e.getMessage(), null);
exitCode = parEx.getCommandLine().getCommandSpec().exitCodeOnInvalidInput();
}
exitOnFailure(exitCode, cmd); exitOnFailure(exitCode, cmd);
} }
private static void catchProfileException(String message, Throwable cause, CommandLine cmd) {
ExecutionExceptionHandler errorHandler = new ExecutionExceptionHandler();
errorHandler.error(cmd.getErr(), message, cause);
exitOnFailure(CommandLine.ExitCode.USAGE, cmd);
}
private static void exitOnFailure(int exitCode, CommandLine cmd) { private static void exitOnFailure(int exitCode, CommandLine cmd) {
if (exitCode != cmd.getCommandSpec().exitCodeOnSuccess() && !Environment.isTestLaunchMode() || isRebuildCheck()) { if (exitCode != cmd.getCommandSpec().exitCodeOnSuccess() && !Environment.isTestLaunchMode() || isRebuildCheck()) {
// hard exit wanted, as build failed and no subsequent command should be executed. no quarkus involved. // hard exit wanted, as build failed and no subsequent command should be executed. no quarkus involved.
@ -157,6 +177,7 @@ public final class Picocli {
} }
} }
if (requiresReAugmentation(currentCommandSpec)) { if (requiresReAugmentation(currentCommandSpec)) {
PropertyMappers.sanitizeDisabledMappers();
exitCode = runReAugmentation(cliArgs, cmd); exitCode = runReAugmentation(cliArgs, cmd);
} }
@ -279,13 +300,27 @@ public final class Picocli {
return; return;
} }
final boolean disabledMappersInterceptorEnabled = DisabledMappersInterceptor.isEnabled(); // return to the state before the disable
try { try {
PropertyMappingInterceptor.disable(); // we don't want the mapped / transformed properties, we want what the user effectively supplied PropertyMappingInterceptor.disable(); // we don't want the mapped / transformed properties, we want what the user effectively supplied
List<String> ignoredBuildTime = new ArrayList<>(); DisabledMappersInterceptor.disable(); // we want all properties, even disabled ones
List<String> ignoredRunTime = new ArrayList<>();
Set<String> deprecatedInUse = new HashSet<>(); final List<String> ignoredBuildTime = new ArrayList<>();
final List<String> ignoredRunTime = new ArrayList<>();
final Set<String> disabledBuildTime = new HashSet<>();
final Set<String> disabledRunTime = new HashSet<>();
final Set<String> deprecatedInUse = new HashSet<>();
final Set<PropertyMapper<?>> disabledMappers = new HashSet<>();
if (options.includeBuildTime) {
disabledMappers.addAll(PropertyMappers.getDisabledBuildTimeMappers().values());
}
if (options.includeRuntime) {
disabledMappers.addAll(PropertyMappers.getDisabledRuntimeMappers().values());
}
for (OptionCategory category : abstractCommand.getOptionCategories()) { for (OptionCategory category : abstractCommand.getOptionCategories()) {
List<PropertyMapper<?>> mappers = new ArrayList<>(); List<PropertyMapper<?>> mappers = new ArrayList<>(disabledMappers);
Optional.ofNullable(PropertyMappers.getRuntimeMappers().get(category)).ifPresent(mappers::addAll); Optional.ofNullable(PropertyMappers.getRuntimeMappers().get(category)).ifPresent(mappers::addAll);
Optional.ofNullable(PropertyMappers.getBuildTimeMappers().get(category)).ifPresent(mappers::addAll); Optional.ofNullable(PropertyMappers.getBuildTimeMappers().get(category)).ifPresent(mappers::addAll);
for (PropertyMapper<?> mapper : mappers) { for (PropertyMapper<?> mapper : mappers) {
@ -297,6 +332,20 @@ public final class Picocli {
continue; continue;
} }
if (disabledMappers.contains(mapper)) {
if (!PropertyMappers.isDisabledMapper(mapper.getFrom())) {
continue; // we found enabled mapper with the same name
}
final boolean deniedPrintException = mapper.isRunTime() && isRebuild();
if (PropertyMapper.isCliOption(configValue) && !deniedPrintException) {
throw new KcUnmatchedArgumentException(abstractCommand.getCommandLine(), List.of(mapper.getCliFormat()));
} else {
handleDisabled(mapper.isRunTime() ? disabledRunTime : disabledBuildTime, mapper);
}
continue;
}
if (mapper.isBuildTime() && !options.includeBuildTime) { if (mapper.isBuildTime() && !options.includeBuildTime) {
ignoredBuildTime.add(mapper.getFrom()); ignoredBuildTime.add(mapper.getFrom());
continue; continue;
@ -322,10 +371,17 @@ public final class Picocli {
outputIgnoredProperties(ignoredRunTime, false, logger); outputIgnoredProperties(ignoredRunTime, false, logger);
} }
if (!disabledBuildTime.isEmpty()) {
outputDisabledProperties(disabledBuildTime, true, logger);
} else if (!disabledRunTime.isEmpty()) {
outputDisabledProperties(disabledRunTime, false, logger);
}
if (!deprecatedInUse.isEmpty()) { if (!deprecatedInUse.isEmpty()) {
logger.warn("The following used options or option values are DEPRECATED and will be removed in a future release:\n" + String.join("\n", deprecatedInUse) + "\nConsult the Release Notes for details."); logger.warn("The following used options or option values are DEPRECATED and will be removed in a future release:\n" + String.join("\n", deprecatedInUse) + "\nConsult the Release Notes for details.");
} }
} finally { } finally {
DisabledMappersInterceptor.enable(disabledMappersInterceptorEnabled);
PropertyMappingInterceptor.enable(); PropertyMappingInterceptor.enable();
} }
} }
@ -372,10 +428,36 @@ public final class Picocli {
deprecatedInUse.add(sb.toString()); deprecatedInUse.add(sb.toString());
} }
private static void handleDisabled(Set<String> disabledInUse, PropertyMapper<?> mapper) {
String optionName = mapper.getFrom();
if (optionName.startsWith(NS_KEYCLOAK_PREFIX)) {
optionName = optionName.substring(NS_KEYCLOAK_PREFIX.length());
}
final StringBuilder sb = new StringBuilder("\t- ");
sb.append(optionName);
if (mapper.getEnabledWhen().isPresent()) {
final String enabledWhen = mapper.getEnabledWhen().get();
sb.append(": ");
sb.append(enabledWhen);
if (!enabledWhen.endsWith(".")) {
sb.append(".");
}
}
disabledInUse.add(sb.toString());
}
private static void outputIgnoredProperties(List<String> properties, boolean build, Logger logger) { private static void outputIgnoredProperties(List<String> properties, boolean build, Logger logger) {
logger.warn(String.format("The following %s time non-cli options were found, but will be ignored during %s time: %s\n", logger.warn(format("The following %s time non-cli options were found, but will be ignored during %s time: %s\n",
build ? "build" : "run", build ? "run" : "build", build ? "build" : "run", build ? "run" : "build",
properties.stream().collect(Collectors.joining(", ")))); String.join(", ", properties)));
}
private static void outputDisabledProperties(Set<String> properties, boolean build, Logger logger) {
logger.warn(format("The following used %s time options are UNAVAILABLE and will be ignored during %s time:\n %s",
build ? "build" : "run", build ? "run" : "build",
String.join("\n", properties)));
} }
private static boolean hasConfigChanges(CommandLine cmdCommand) { private static boolean hasConfigChanges(CommandLine cmdCommand) {
@ -536,6 +618,7 @@ public final class Picocli {
} }
result.includeRuntime = abstractCommand.includeRuntime(); result.includeRuntime = abstractCommand.includeRuntime();
result.includeBuildTime = abstractCommand.includeBuildTime(); result.includeBuildTime = abstractCommand.includeBuildTime();
result.includeDisabled = cliArgs.contains(HelpAllMixin.HELP_ALL_OPTION);
if (!result.includeBuildTime && !result.includeRuntime) { if (!result.includeBuildTime && !result.includeRuntime) {
return result; return result;
@ -548,14 +631,17 @@ public final class Picocli {
} }
private static void addCommandOptions(List<String> cliArgs, CommandLine command) { private static void addCommandOptions(List<String> cliArgs, CommandLine command) {
if (command != null && command.getCommand() instanceof AbstractCommand) { if (command != null && command.getCommand() instanceof AbstractCommand ac) {
IncludeOptions options = getIncludeOptions(cliArgs, command.getCommand(), command.getCommandName()); IncludeOptions options = getIncludeOptions(cliArgs, command.getCommand(), command.getCommandName());
// set current parsed command
Environment.setParsedCommand(ac);
if (!options.includeBuildTime && !options.includeRuntime) { if (!options.includeBuildTime && !options.includeRuntime) {
return; return;
} }
addOptionsToCli(command, options.includeBuildTime, options.includeRuntime); addOptionsToCli(command, options);
} }
} }
@ -571,30 +657,48 @@ public final class Picocli {
return null; return null;
} }
private static void addOptionsToCli(CommandLine commandLine, boolean includeBuildTime, boolean includeRuntime) { private static void addOptionsToCli(CommandLine commandLine, IncludeOptions includeOptions) {
Map<OptionCategory, List<PropertyMapper<?>>> mappers = new EnumMap<>(OptionCategory.class); final Map<OptionCategory, List<PropertyMapper<?>>> mappers = new EnumMap<>(OptionCategory.class);
if (includeRuntime) { if (includeOptions.includeRuntime) {
mappers.putAll(PropertyMappers.getRuntimeMappers()); mappers.putAll(PropertyMappers.getRuntimeMappers());
if (includeOptions.includeDisabled) {
appendDisabledMappers(mappers, PropertyMappers.getDisabledRuntimeMappers());
}
} }
if (includeBuildTime) { if (includeOptions.includeBuildTime) {
for (Map.Entry<OptionCategory, List<PropertyMapper<?>>> entry : PropertyMappers.getBuildTimeMappers() combinePropertyMappers(mappers, PropertyMappers.getBuildTimeMappers());
.entrySet()) {
List<PropertyMapper<?>> result = new ArrayList<>(mappers.getOrDefault(entry.getKey(), Collections.emptyList()));
result.addAll(entry.getValue()); if (includeOptions.includeDisabled) {
appendDisabledMappers(mappers, PropertyMappers.getDisabledBuildTimeMappers());
mappers.put(entry.getKey(), result);
} }
} }
addMappedOptionsToArgGroups(commandLine, mappers); addMappedOptionsToArgGroups(commandLine, mappers);
} }
private static void appendDisabledMappers(Map<OptionCategory, List<PropertyMapper<?>>> origMappers,
Map<String, PropertyMapper<?>> additionalMappers) {
for (var pm : additionalMappers.values()) {
final List<PropertyMapper<?>> result = origMappers.getOrDefault(pm.getCategory(), new ArrayList<>());
result.add(pm);
origMappers.put(pm.getCategory(), result);
}
}
private static <T extends Map<OptionCategory, List<PropertyMapper<?>>>> void combinePropertyMappers(T origMappers, T additionalMappers) {
for (var entry : additionalMappers.entrySet()) {
final List<PropertyMapper<?>> result = origMappers.getOrDefault(entry.getKey(), new ArrayList<>());
result.addAll(entry.getValue());
origMappers.put(entry.getKey(), result);
}
}
private static void addMappedOptionsToArgGroups(CommandLine commandLine, Map<OptionCategory, List<PropertyMapper<?>>> propertyMappers) { private static void addMappedOptionsToArgGroups(CommandLine commandLine, Map<OptionCategory, List<PropertyMapper<?>>> propertyMappers) {
CommandSpec cSpec = commandLine.getCommandSpec(); CommandSpec cSpec = commandLine.getCommandSpec();
for(OptionCategory category : ((AbstractCommand) commandLine.getCommand()).getOptionCategories()) { for (OptionCategory category : ((AbstractCommand) commandLine.getCommand()).getOptionCategories()) {
List<PropertyMapper<?>> mappersInCategory = propertyMappers.get(category); List<PropertyMapper<?>> mappersInCategory = propertyMappers.get(category);
if (mappersInCategory == null) { if (mappersInCategory == null) {
@ -607,11 +711,13 @@ public final class Picocli {
.order(category.getOrder()) .order(category.getOrder())
.validate(false); .validate(false);
for (PropertyMapper<?> mapper: mappersInCategory) { final Set<String> alreadyPresentArgs = new HashSet<>();
for (PropertyMapper<?> mapper : mappersInCategory) {
String name = mapper.getCliFormat(); String name = mapper.getCliFormat();
String description = mapper.getDescription(); String description = mapper.getDescription();
if (description == null || cSpec.optionsMap().containsKey(name) || name.endsWith(OPTION_PART_SEPARATOR)) { if (description == null || cSpec.optionsMap().containsKey(name) || name.endsWith(OPTION_PART_SEPARATOR) || alreadyPresentArgs.contains(name)) {
//when key is already added or has no description, don't add. //when key is already added or has no description, don't add.
continue; continue;
} }
@ -638,6 +744,8 @@ public final class Picocli {
optBuilder.type(String.class); optBuilder.type(String.class);
} }
alreadyPresentArgs.add(name);
argGroupBuilder.addArg(optBuilder.build()); argGroupBuilder.addArg(optBuilder.build());
} }
@ -667,6 +775,8 @@ public final class Picocli {
.map(d -> " Default: " + d + ".") .map(d -> " Default: " + d + ".")
.ifPresent(transformedDesc::append); .ifPresent(transformedDesc::append);
mapper.getEnabledWhen().map(e -> format(" %s.", e)).ifPresent(transformedDesc::append);
// only fully deprecated options, not just deprecated values // only fully deprecated options, not just deprecated values
mapper.getDeprecatedMetadata() mapper.getDeprecatedMetadata()
.filter(deprecatedMetadata -> deprecatedMetadata.getDeprecatedValues().isEmpty()) .filter(deprecatedMetadata -> deprecatedMetadata.getDeprecatedValues().isEmpty())
@ -722,7 +832,7 @@ public final class Picocli {
if (!arg.contains(ARG_KEY_VALUE_SEPARATOR)) { if (!arg.contains(ARG_KEY_VALUE_SEPARATOR)) {
if (!iterator.hasNext()) { if (!iterator.hasNext()) {
if (arg.startsWith("--spi")) { if (arg.startsWith("--spi")) {
throw new PropertyException(String.format("spi argument %s requires a value.", arg)); throw new PropertyException(format("spi argument %s requires a value.", arg));
} }
return args; return args;
} }

View file

@ -14,6 +14,10 @@ import picocli.CommandLine.Model.CommandSpec;
import picocli.CommandLine.ParameterException; import picocli.CommandLine.ParameterException;
import picocli.CommandLine.UnmatchedArgumentException; import picocli.CommandLine.UnmatchedArgumentException;
import java.util.Optional;
import java.util.function.BooleanSupplier;
import static java.lang.String.format;
import static org.keycloak.quarkus.runtime.cli.command.AbstractStartCommand.OPTIMIZED_BUILD_OPTION_LONG; import static org.keycloak.quarkus.runtime.cli.command.AbstractStartCommand.OPTIMIZED_BUILD_OPTION_LONG;
public class ShortErrorMessageHandler implements IParameterExceptionHandler { public class ShortErrorMessageHandler implements IParameterExceptionHandler {
@ -23,17 +27,30 @@ public class ShortErrorMessageHandler implements IParameterExceptionHandler {
CommandLine cmd = ex.getCommandLine(); CommandLine cmd = ex.getCommandLine();
PrintWriter writer = cmd.getErr(); PrintWriter writer = cmd.getErr();
String errorMessage = ex.getMessage(); String errorMessage = ex.getMessage();
String additionalSuggestion = null;
if (ex instanceof UnmatchedArgumentException) { if (ex instanceof UnmatchedArgumentException) {
UnmatchedArgumentException uae = (UnmatchedArgumentException) ex; UnmatchedArgumentException uae = (UnmatchedArgumentException) ex;
String[] unmatched = getUnmatchedPartsByOptionSeparator(uae,"="); String[] unmatched = getUnmatchedPartsByOptionSeparator(uae, "=");
String cliKey = unmatched[0]; String cliKey = unmatched[0];
PropertyMapper<?> mapper = PropertyMappers.getMapper(cliKey); PropertyMapper<?> mapper = PropertyMappers.getMapper(cliKey);
if (mapper == null || !(cmd.getCommand() instanceof AbstractCommand)) { final boolean isDisabledOption = mapper == null && PropertyMappers.isDisabledMapper(cliKey);
final BooleanSupplier isUnknownOption = () -> mapper == null || !(cmd.getCommand() instanceof AbstractCommand);
if (isDisabledOption) {
var enabledWhen = PropertyMappers.getDisabledMapper(cliKey)
.map(PropertyMapper::getEnabledWhen)
.filter(Optional::isPresent)
.map(desc -> format(". %s", desc.get()))
.orElse("");
errorMessage = format("Disabled option: '%s'%s", cliKey, enabledWhen);
additionalSuggestion = "Specify '--help-all' to obtain information on all options and their availability.";
} else if (isUnknownOption.getAsBoolean()) {
if (cliKey.split("\\s").length > 1) { if (cliKey.split("\\s").length > 1) {
errorMessage = "Option: '" + cliKey + "' is not expected to contain whitespace, please remove any unnecessary quoting/escaping"; errorMessage = "Option: '" + cliKey + "' is not expected to contain whitespace, please remove any unnecessary quoting/escaping";
} else { } else {
@ -42,12 +59,13 @@ public class ShortErrorMessageHandler implements IParameterExceptionHandler {
} else { } else {
AbstractCommand command = cmd.getCommand(); AbstractCommand command = cmd.getCommand();
if (!command.getOptionCategories().contains(mapper.getCategory())) { if (!command.getOptionCategories().contains(mapper.getCategory())) {
errorMessage = "Option: '" + cliKey + "' not valid for command " + cmd.getCommandName(); errorMessage = format("Option: '%s' not valid for command %s", cliKey, cmd.getCommandName());
} else { } else {
if (Stream.of(args).anyMatch(OPTIMIZED_BUILD_OPTION_LONG::equals) && mapper.isBuildTime() && Start.NAME.equals(cmd.getCommandName())) { if (Stream.of(args).anyMatch(OPTIMIZED_BUILD_OPTION_LONG::equals) && mapper.isBuildTime() && Start.NAME.equals(cmd.getCommandName())) {
errorMessage = "Build time option: '" + cliKey + "' not usable with pre-built image and --optimized"; errorMessage = format("Build time option: '%s' not usable with pre-built image and --optimized", cliKey);
} else { } else {
errorMessage = (mapper.isRunTime()?"Run time":"Build time") + " option: '" + cliKey + "' not usable with " + cmd.getCommandName(); final var optionType = mapper.isRunTime() ? "Run time" : "Build time";
errorMessage = format("%s option: '%s' not usable with %s", optionType, cliKey, cmd.getCommandName());
} }
} }
} }
@ -59,6 +77,10 @@ public class ShortErrorMessageHandler implements IParameterExceptionHandler {
CommandSpec spec = cmd.getCommandSpec(); CommandSpec spec = cmd.getCommandSpec();
writer.printf("Try '%s --help' for more information on the available options.%n", spec.qualifiedName()); writer.printf("Try '%s --help' for more information on the available options.%n", spec.qualifiedName());
if (additionalSuggestion != null) {
writer.println(additionalSuggestion);
}
return getInvalidInputExitCode(ex, cmd); return getInvalidInputExitCode(ex, cmd);
} }

View file

@ -69,4 +69,8 @@ public abstract class AbstractCommand {
} }
public abstract String getName(); public abstract String getName();
public CommandLine getCommandLine() {
return spec.commandLine();
}
} }

View file

@ -19,23 +19,20 @@ package org.keycloak.quarkus.runtime.cli.command;
import static org.keycloak.config.ClassLoaderOptions.QUARKUS_REMOVED_ARTIFACTS_PROPERTY; import static org.keycloak.config.ClassLoaderOptions.QUARKUS_REMOVED_ARTIFACTS_PROPERTY;
import static org.keycloak.quarkus.runtime.Environment.getHomePath; import static org.keycloak.quarkus.runtime.Environment.getHomePath;
import static org.keycloak.quarkus.runtime.Environment.isDevMode; import static org.keycloak.quarkus.runtime.Environment.isDevProfile;
import static org.keycloak.quarkus.runtime.cli.Picocli.println; import static org.keycloak.quarkus.runtime.cli.Picocli.println;
import static org.keycloak.quarkus.runtime.configuration.ConfigArgsConfigSource.getAllCliArgs; import static org.keycloak.quarkus.runtime.configuration.ConfigArgsConfigSource.getAllCliArgs;
import org.keycloak.config.ClassLoaderOptions;
import org.keycloak.config.OptionCategory; import org.keycloak.config.OptionCategory;
import org.keycloak.quarkus.runtime.Environment; import org.keycloak.quarkus.runtime.Environment;
import org.keycloak.quarkus.runtime.Messages; import org.keycloak.quarkus.runtime.Messages;
import org.keycloak.quarkus.runtime.configuration.Configuration; import org.keycloak.quarkus.runtime.configuration.Configuration;
import org.keycloak.quarkus.runtime.configuration.MicroProfileConfigProvider;
import io.quarkus.bootstrap.runner.QuarkusEntryPoint; import io.quarkus.bootstrap.runner.QuarkusEntryPoint;
import io.quarkus.bootstrap.runner.RunnerClassLoader; import io.quarkus.bootstrap.runner.RunnerClassLoader;
import io.quarkus.runtime.configuration.ProfileManager; import io.quarkus.runtime.configuration.ProfileManager;
import io.smallrye.config.ConfigValue; import io.smallrye.config.ConfigValue;
import org.keycloak.utils.StringUtil;
import picocli.CommandLine; import picocli.CommandLine;
import picocli.CommandLine.Command; import picocli.CommandLine.Command;
@ -84,7 +81,7 @@ public final class Build extends AbstractCommand implements Runnable {
beforeReaugmentationOnWindows(); beforeReaugmentationOnWindows();
QuarkusEntryPoint.main(); QuarkusEntryPoint.main();
if (!isDevMode()) { if (!isDevProfile()) {
println(spec.commandLine(), "Server configuration updated and persisted. Run the following command to review the configuration:\n"); println(spec.commandLine(), "Server configuration updated and persisted. Run the following command to review the configuration:\n");
println(spec.commandLine(), "\t" + Environment.getCommand() + " show-config\n"); println(spec.commandLine(), "\t" + Environment.getCommand() + " show-config\n");
} }

View file

@ -20,6 +20,7 @@ package org.keycloak.quarkus.runtime.cli.command;
import static org.keycloak.exportimport.ExportImportConfig.ACTION_EXPORT; import static org.keycloak.exportimport.ExportImportConfig.ACTION_EXPORT;
import org.keycloak.config.OptionCategory; import org.keycloak.config.OptionCategory;
import org.keycloak.quarkus.runtime.configuration.mappers.ExportPropertyMappers;
import picocli.CommandLine.Command; import picocli.CommandLine.Command;
import java.util.List; import java.util.List;
@ -42,6 +43,12 @@ public final class Export extends AbstractExportImportCommand implements Runnabl
optionCategory != OptionCategory.IMPORT).collect(Collectors.toList()); optionCategory != OptionCategory.IMPORT).collect(Collectors.toList());
} }
@Override
public void validateConfig() {
ExportPropertyMappers.validateConfig();
super.validateConfig();
}
@Override @Override
public String getName() { public String getName() {
return NAME; return NAME;

View file

@ -23,10 +23,12 @@ import picocli.CommandLine;
public final class HelpAllMixin { public final class HelpAllMixin {
public static final String HELP_ALL_OPTION = "--help-all";
@CommandLine.Spec @CommandLine.Spec
private CommandLine.Model.CommandSpec spec; private CommandLine.Model.CommandSpec spec;
@CommandLine.Option(names = { "--help-all" }, usageHelp = true, description = "This same help message but with additional options.") @CommandLine.Option(names = {HELP_ALL_OPTION}, usageHelp = true, description = "This same help message but with additional options.")
public void setHelpAll(boolean allOptions) { public void setHelpAll(boolean allOptions) {
Help help = (Help) spec.commandLine().getHelp(); Help help = (Help) spec.commandLine().getHelp();
help.setAllOptions(true); help.setAllOptions(true);

View file

@ -20,6 +20,7 @@ package org.keycloak.quarkus.runtime.cli.command;
import static org.keycloak.exportimport.ExportImportConfig.ACTION_IMPORT; import static org.keycloak.exportimport.ExportImportConfig.ACTION_IMPORT;
import org.keycloak.config.OptionCategory; import org.keycloak.config.OptionCategory;
import org.keycloak.quarkus.runtime.configuration.mappers.ImportPropertyMappers;
import picocli.CommandLine.Command; import picocli.CommandLine.Command;
import java.util.List; import java.util.List;
@ -42,6 +43,12 @@ public final class Import extends AbstractExportImportCommand implements Runnabl
optionCategory != OptionCategory.EXPORT).collect(Collectors.toList()); optionCategory != OptionCategory.EXPORT).collect(Collectors.toList());
} }
@Override
public void validateConfig() {
ImportPropertyMappers.validateConfig();
super.validateConfig();
}
@Override @Override
public String getName() { public String getName() {
return NAME; return NAME;

View file

@ -28,6 +28,7 @@ import io.smallrye.config.SmallRyeConfig;
import org.eclipse.microprofile.config.spi.ConfigProviderResolver; import org.eclipse.microprofile.config.spi.ConfigProviderResolver;
import org.eclipse.microprofile.config.spi.ConfigSource; import org.eclipse.microprofile.config.spi.ConfigSource;
import org.keycloak.config.Option;
import org.keycloak.quarkus.runtime.Environment; import org.keycloak.quarkus.runtime.Environment;
import org.keycloak.quarkus.runtime.configuration.mappers.PropertyMapper; import org.keycloak.quarkus.runtime.configuration.mappers.PropertyMapper;
import org.keycloak.quarkus.runtime.configuration.mappers.PropertyMappers; import org.keycloak.quarkus.runtime.configuration.mappers.PropertyMappers;
@ -47,6 +48,22 @@ public final class Configuration {
} }
public static boolean isTrue(Option<Boolean> option) {
return getOptionalBooleanValue(NS_KEYCLOAK_PREFIX + option.getKey()).orElse(false);
}
public static boolean contains(Option<?> option, String value) {
return getOptionalValue(NS_KEYCLOAK_PREFIX + option.getKey())
.filter(f -> f.contains(value))
.isPresent();
}
public static boolean equals(Option<?> option, String value) {
return getOptionalValue(NS_KEYCLOAK_PREFIX + option.getKey())
.filter(f -> f.equals(value))
.isPresent();
}
public static synchronized SmallRyeConfig getConfig() { public static synchronized SmallRyeConfig getConfig() {
return (SmallRyeConfig) ConfigProviderResolver.instance().getConfig(); return (SmallRyeConfig) ConfigProviderResolver.instance().getConfig();
} }
@ -222,13 +239,6 @@ public final class Configuration {
} }
public static ConfigValue getCurrentBuiltTimeProperty(String name) { public static ConfigValue getCurrentBuiltTimeProperty(String name) {
PersistedConfigSource persistedConfigSource = PersistedConfigSource.getInstance(); return PersistedConfigSource.getInstance().runWithDisabled(() -> getConfigValue(name));
try {
persistedConfigSource.enable(false);
return getConfigValue(name);
} finally {
persistedConfigSource.enable(true);
}
} }
} }

View file

@ -0,0 +1,91 @@
/*
* Copyright 2024 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 io.smallrye.config.ConfigSourceInterceptor;
import io.smallrye.config.ConfigSourceInterceptorContext;
import io.smallrye.config.ConfigValue;
import io.smallrye.config.Priorities;
import jakarta.annotation.Priority;
import org.apache.commons.collections4.iterators.FilterIterator;
import org.keycloak.quarkus.runtime.configuration.mappers.PropertyMappers;
import java.util.Iterator;
import static org.keycloak.quarkus.runtime.configuration.MicroProfileConfigProvider.NS_KEYCLOAK_PREFIX;
/**
* <p>This interceptor is responsible for ignoring disabled Keycloak properties
*
* <p>This interceptor should execute before the {@link PropertyMappingInterceptor} so that disabled properties
* are not mapped to the Quarkus properties.
* <p>
* The reason for the used priority is to always execute the interceptor before default Application Config Source interceptors
* and before the {@link PropertyMappingInterceptor}
*/
@Priority(Priorities.APPLICATION - 20)
public class DisabledMappersInterceptor implements ConfigSourceInterceptor {
private static final ThreadLocal<Boolean> ENABLED = ThreadLocal.withInitial(() -> false);
public static void enable() {
enable(true);
}
public static void disable() {
enable(false);
}
public static void enable(boolean enable) {
ENABLED.set(enable);
}
private <T> boolean isDisabledMapper(String property) {
return property.startsWith(NS_KEYCLOAK_PREFIX) && PropertyMappers.isDisabledMapper(property);
}
Iterator<String> filterDisabledMappers(Iterator<String> iter) {
return new FilterIterator<>(iter, item -> !isDisabledMapper(item));
}
@Override
public Iterator<String> iterateNames(ConfigSourceInterceptorContext context) {
return filterDisabledMappers(context.iterateNames());
}
@Override
public ConfigValue getValue(ConfigSourceInterceptorContext context, String name) {
if (isEnabled() && isDisabledMapper(name)) {
return null;
}
return context.proceed(name);
}
public static boolean isEnabled() {
return Boolean.TRUE.equals(ENABLED.get());
}
public static void runWithDisabled(Runnable execution) {
try {
disable();
execution.run();
} finally {
enable();
}
}
}

View file

@ -0,0 +1,39 @@
/*
* Copyright 2024 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 org.keycloak.quarkus.runtime.configuration.mappers.PropertyMappers;
import picocli.CommandLine;
import java.util.List;
/**
* Custom CommandLine.UnmatchedArgumentException with amended suggestions
*/
public class KcUnmatchedArgumentException extends CommandLine.UnmatchedArgumentException {
public KcUnmatchedArgumentException(CommandLine commandLine, List<String> args) {
super(commandLine, args);
}
@Override
public List<String> getSuggestions() {
// filter out disabled mappers
return super.getSuggestions().stream().filter(f -> !PropertyMappers.isDisabledMapper(f)).toList();
}
}

View file

@ -51,7 +51,7 @@ public final class PersistedConfigSource extends PropertiesConfigSource {
* to ignore this config source. Otherwise, default values are not resolved at runtime because the property will be * to ignore this config source. Otherwise, default values are not resolved at runtime because the property will be
* resolved from this config source, if persisted. * resolved from this config source, if persisted.
*/ */
private static final ThreadLocal<Boolean> ENABLED = new ThreadLocal<>(); private static final ThreadLocal<Boolean> ENABLED = ThreadLocal.withInitial(() -> true);
private PersistedConfigSource() { private PersistedConfigSource() {
super(readProperties(), "", 200); super(readProperties(), "", 200);
@ -146,16 +146,24 @@ public final class PersistedConfigSource extends PropertiesConfigSource {
return null; return null;
} }
public void enable(boolean enabled) { public void enable() {
if (enabled) { ENABLED.set(true);
ENABLED.remove(); }
} else {
ENABLED.set(enabled); public void disable() {
} ENABLED.set(false);
} }
private boolean isEnabled() { private boolean isEnabled() {
Boolean result = ENABLED.get(); return Boolean.TRUE.equals(ENABLED.get());
return result == null ? true : result; }
public <T> T runWithDisabled(Supplier<T> execution) {
try {
disable();
return execution.get();
} finally {
enable();
}
} }
} }

View file

@ -20,6 +20,8 @@ import io.smallrye.config.ConfigSourceInterceptor;
import io.smallrye.config.ConfigSourceInterceptorContext; import io.smallrye.config.ConfigSourceInterceptorContext;
import io.smallrye.config.ConfigValue; import io.smallrye.config.ConfigValue;
import io.smallrye.config.Priorities;
import jakarta.annotation.Priority;
import org.apache.commons.collections4.iterators.FilterIterator; import org.apache.commons.collections4.iterators.FilterIterator;
import org.keycloak.common.util.StringPropertyReplacer; import org.keycloak.common.util.StringPropertyReplacer;
import org.keycloak.quarkus.runtime.Environment; import org.keycloak.quarkus.runtime.Environment;
@ -42,8 +44,11 @@ import static org.keycloak.quarkus.runtime.Environment.isRebuild;
* from Keycloak (e.g.: database) is mapped to multiple properties in Quarkus. * from Keycloak (e.g.: database) is mapped to multiple properties in Quarkus.
* *
* <p>This interceptor must execute after the {@link io.smallrye.config.ExpressionConfigSourceInterceptor} so that expressions * <p>This interceptor must execute after the {@link io.smallrye.config.ExpressionConfigSourceInterceptor} so that expressions
* are properly resolved before executing this interceptor. Hence, leaving the default priority. * are properly resolved before executing this interceptor.
* <p>
* The reason for the used priority is to always execute the interceptor before default Application Config Source interceptors
*/ */
@Priority(Priorities.APPLICATION - 10)
public class PropertyMappingInterceptor implements ConfigSourceInterceptor { public class PropertyMappingInterceptor implements ConfigSourceInterceptor {
private static ThreadLocal<Boolean> disable = new ThreadLocal<>(); private static ThreadLocal<Boolean> disable = new ThreadLocal<>();

View file

@ -20,22 +20,30 @@ package org.keycloak.quarkus.runtime.configuration.mappers;
import io.smallrye.config.ConfigSourceInterceptorContext; import io.smallrye.config.ConfigSourceInterceptorContext;
import io.smallrye.config.ConfigValue; import io.smallrye.config.ConfigValue;
import org.keycloak.config.ExportOptions; import org.keycloak.config.ExportOptions;
import picocli.CommandLine; import org.keycloak.config.Option;
import org.keycloak.config.OptionBuilder;
import org.keycloak.config.OptionCategory;
import org.keycloak.quarkus.runtime.cli.PropertyException;
import org.keycloak.quarkus.runtime.configuration.Configuration;
import java.util.Optional; import java.util.Optional;
import static org.keycloak.exportimport.ExportImportConfig.PROVIDER; import static org.keycloak.exportimport.ExportImportConfig.PROVIDER;
import static org.keycloak.quarkus.runtime.configuration.Configuration.getOptionalValue;
import static org.keycloak.quarkus.runtime.configuration.mappers.PropertyMapper.fromOption; import static org.keycloak.quarkus.runtime.configuration.mappers.PropertyMapper.fromOption;
final class ExportPropertyMappers { public final class ExportPropertyMappers {
private static final String EXPORTER_PROPERTY = "kc.spi-export-exporter";
private static final String SINGLE_FILE = "singleFile";
private static final String DIR = "dir";
private ExportPropertyMappers() { private ExportPropertyMappers() {
} }
public static PropertyMapper<?>[] getMappers() { public static PropertyMapper<?>[] getMappers() {
return new PropertyMapper[] { return new PropertyMapper[]{
fromOption(ExportOptions.FILE) fromOption(EXPORTER_PLACEHOLDER)
.to("kc.spi-export-exporter") .to(EXPORTER_PROPERTY)
.transformer(ExportPropertyMappers::transformExporter) .transformer(ExportPropertyMappers::transformExporter)
.paramLabel("file") .paramLabel("file")
.build(), .build(),
@ -49,43 +57,69 @@ final class ExportPropertyMappers {
.build(), .build(),
fromOption(ExportOptions.REALM) fromOption(ExportOptions.REALM)
.to("kc.spi-export-single-file-realm-name") .to("kc.spi-export-single-file-realm-name")
.isEnabled(ExportPropertyMappers::isSingleFileProvider)
.paramLabel("realm") .paramLabel("realm")
.build(), .build(),
fromOption(ExportOptions.REALM) fromOption(ExportOptions.REALM)
.to("kc.spi-export-dir-realm-name") .to("kc.spi-export-dir-realm-name")
.isEnabled(ExportPropertyMappers::isDirProvider)
.paramLabel("realm") .paramLabel("realm")
.build(), .build(),
fromOption(ExportOptions.USERS) fromOption(ExportOptions.USERS)
.to("kc.spi-export-dir-users-export-strategy") .to("kc.spi-export-dir-users-export-strategy")
.isEnabled(ExportPropertyMappers::isDirProvider)
.paramLabel("strategy") .paramLabel("strategy")
.build(), .build(),
fromOption(ExportOptions.USERS_PER_FILE) fromOption(ExportOptions.USERS_PER_FILE)
.to("kc.spi-export-dir-users-per-file") .to("kc.spi-export-dir-users-per-file")
.isEnabled(ExportPropertyMappers::isDirProvider)
.paramLabel("number") .paramLabel("number")
.build() .build()
}; };
} }
public static void validateConfig() {
if (getOptionalValue(EXPORTER_PROPERTY).isEmpty() && System.getProperty(PROVIDER) == null) {
throw new PropertyException("Must specify either --dir or --file options.");
}
}
private static final Option<String> EXPORTER_PLACEHOLDER = new OptionBuilder<>("exporter", String.class)
.category(OptionCategory.EXPORT)
.description("Placeholder for determining export mode")
.buildTime(false)
.hidden()
.build();
private static boolean isSingleFileProvider() {
return isProvider(SINGLE_FILE);
}
private static boolean isDirProvider() {
return isProvider(DIR);
}
private static boolean isProvider(String provider) {
return Configuration.getOptionalValue(EXPORTER_PROPERTY)
.filter(provider::equals)
.isPresent();
}
private static Optional<String> transformExporter(Optional<String> option, ConfigSourceInterceptorContext context) { private static Optional<String> transformExporter(Optional<String> option, ConfigSourceInterceptorContext context) {
ConfigValue exporter = context.proceed("kc.spi-export-exporter"); ConfigValue exporter = context.proceed(EXPORTER_PROPERTY);
if (exporter != null) { if (exporter != null) {
return Optional.of(exporter.getValue()); return Optional.of(exporter.getValue());
} }
if (option.isPresent()) {
return Optional.of("singleFile"); var file = Configuration.getOptionalValue("kc.spi-export-single-file-file").map(f -> SINGLE_FILE);
} var dir = Configuration.getOptionalValue("kc.spi-export-dir-dir")
ConfigValue dirConfigValue = context.proceed("kc.spi-export-dir-dir"); .or(() -> Configuration.getOptionalValue("kc.dir"))
if (dirConfigValue != null && dirConfigValue.getValue() != null) { .map(f -> DIR);
return Optional.of("dir");
} // Only one option can be specified
ConfigValue dirValue = context.proceed("kc.dir"); boolean xor = file.isPresent() ^ dir.isPresent();
if (dirValue != null && dirValue.getValue() != null) {
return Optional.of("dir"); return xor ? file.or(() -> dir) : Optional.empty();
}
if (System.getProperty(PROVIDER) == null) {
throw new CommandLine.PicocliException("Must specify either --dir or --file options.");
}
return Optional.empty();
} }
} }

View file

@ -20,23 +20,30 @@ package org.keycloak.quarkus.runtime.configuration.mappers;
import io.smallrye.config.ConfigSourceInterceptorContext; import io.smallrye.config.ConfigSourceInterceptorContext;
import io.smallrye.config.ConfigValue; import io.smallrye.config.ConfigValue;
import org.keycloak.config.ImportOptions; import org.keycloak.config.ImportOptions;
import org.keycloak.config.Option;
import org.keycloak.config.OptionBuilder;
import org.keycloak.config.OptionCategory;
import org.keycloak.exportimport.Strategy; import org.keycloak.exportimport.Strategy;
import picocli.CommandLine; import org.keycloak.quarkus.runtime.cli.PropertyException;
import java.util.Optional; import java.util.Optional;
import static org.keycloak.exportimport.ExportImportConfig.PROVIDER; import static org.keycloak.exportimport.ExportImportConfig.PROVIDER;
import static org.keycloak.quarkus.runtime.configuration.Configuration.getOptionalValue;
import static org.keycloak.quarkus.runtime.configuration.mappers.PropertyMapper.fromOption; import static org.keycloak.quarkus.runtime.configuration.mappers.PropertyMapper.fromOption;
final class ImportPropertyMappers { public final class ImportPropertyMappers {
private static final String IMPORTER_PROPERTY = "kc.spi-import-importer";
private static final String SINGLE_FILE = "singleFile";
private static final String DIR = "dir";
private ImportPropertyMappers() { private ImportPropertyMappers() {
} }
public static PropertyMapper<?>[] getMappers() { public static PropertyMapper<?>[] getMappers() {
return new PropertyMapper[] { return new PropertyMapper[]{
fromOption(ImportOptions.FILE) fromOption(IMPORTER_PLACEHOLDER)
.to("kc.spi-import-importer") .to(IMPORTER_PROPERTY)
.transformer(ImportPropertyMappers::transformImporter) .transformer(ImportPropertyMappers::transformImporter)
.paramLabel("file") .paramLabel("file")
.build(), .build(),
@ -51,14 +58,43 @@ final class ImportPropertyMappers {
fromOption(ImportOptions.OVERRIDE) fromOption(ImportOptions.OVERRIDE)
.to("kc.spi-import-single-file-strategy") .to("kc.spi-import-single-file-strategy")
.transformer(ImportPropertyMappers::transformOverride) .transformer(ImportPropertyMappers::transformOverride)
.isEnabled(ImportPropertyMappers::isSingleFileProvider)
.build(), .build(),
fromOption(ImportOptions.OVERRIDE) fromOption(ImportOptions.OVERRIDE)
.to("kc.spi-import-dir-strategy") .to("kc.spi-import-dir-strategy")
.transformer(ImportPropertyMappers::transformOverride) .transformer(ImportPropertyMappers::transformOverride)
.isEnabled(ImportPropertyMappers::isDirProvider)
.build(), .build(),
}; };
} }
public static void validateConfig() {
if (getOptionalValue(IMPORTER_PROPERTY).isEmpty() && System.getProperty(PROVIDER) == null) {
throw new PropertyException("Must specify either --dir or --file options.");
}
}
private static final Option<String> IMPORTER_PLACEHOLDER = new OptionBuilder<>("importer", String.class)
.category(OptionCategory.IMPORT)
.description("Placeholder for determining import mode")
.buildTime(false)
.hidden()
.build();
private static boolean isSingleFileProvider() {
return isProvider(SINGLE_FILE);
}
private static boolean isDirProvider() {
return isProvider(DIR);
}
private static boolean isProvider(String provider) {
return getOptionalValue(IMPORTER_PROPERTY)
.filter(provider::equals)
.isPresent();
}
private static Optional<String> transformOverride(Optional<String> option, ConfigSourceInterceptorContext context) { private static Optional<String> transformOverride(Optional<String> option, ConfigSourceInterceptorContext context) {
if (option.isPresent() && Boolean.parseBoolean(option.get())) { if (option.isPresent() && Boolean.parseBoolean(option.get())) {
return Optional.of(Strategy.OVERWRITE_EXISTING.name()); return Optional.of(Strategy.OVERWRITE_EXISTING.name());
@ -68,25 +104,20 @@ final class ImportPropertyMappers {
} }
private static Optional<String> transformImporter(Optional<String> option, ConfigSourceInterceptorContext context) { private static Optional<String> transformImporter(Optional<String> option, ConfigSourceInterceptorContext context) {
ConfigValue importer = context.proceed("kc.spi-import-importer"); ConfigValue importer = context.proceed(IMPORTER_PROPERTY);
if (importer != null) { if (importer != null) {
return Optional.of(importer.getValue()); return Optional.of(importer.getValue());
} }
if (option.isPresent()) {
return Optional.of("singleFile"); var file = getOptionalValue("kc.spi-import-single-file-file").map(f -> SINGLE_FILE);
} var dir = getOptionalValue("kc.spi-import-dir-dir")
ConfigValue dirConfigValue = context.proceed("kc.spi-import-dir-dir"); .or(() -> getOptionalValue("kc.dir"))
if (dirConfigValue != null && dirConfigValue.getValue() != null) { .map(f -> DIR);
return Optional.of("dir");
} // Only one option can be specified
ConfigValue dirValue = context.proceed("kc.dir"); boolean xor = file.isPresent() ^ dir.isPresent();
if (dirConfigValue != null && dirValue.getValue() != null) {
return Optional.of("dir"); return xor ? file.or(() -> dir) : Optional.empty();
}
if (System.getProperty(PROVIDER) == null) {
throw new CommandLine.PicocliException("Must specify either --dir or --file options.");
}
return Optional.empty();
} }
} }

View file

@ -2,6 +2,7 @@ package org.keycloak.quarkus.runtime.configuration.mappers;
import static java.util.Optional.of; import static java.util.Optional.of;
import static org.keycloak.config.LoggingOptions.GELF_ACTIVATED; import static org.keycloak.config.LoggingOptions.GELF_ACTIVATED;
import static org.keycloak.quarkus.runtime.configuration.Configuration.isTrue;
import static org.keycloak.quarkus.runtime.configuration.mappers.PropertyMapper.fromOption; import static org.keycloak.quarkus.runtime.configuration.mappers.PropertyMapper.fromOption;
import static org.keycloak.quarkus.runtime.integration.QuarkusPlatform.addInitializationException; import static org.keycloak.quarkus.runtime.integration.QuarkusPlatform.addInitializationException;
@ -18,10 +19,16 @@ import org.keycloak.config.LoggingOptions;
import org.keycloak.quarkus.runtime.Messages; import org.keycloak.quarkus.runtime.Messages;
import io.smallrye.config.ConfigSourceInterceptorContext; import io.smallrye.config.ConfigSourceInterceptorContext;
import org.keycloak.quarkus.runtime.configuration.Configuration;
public final class LoggingPropertyMappers { public final class LoggingPropertyMappers {
private LoggingPropertyMappers(){} private static final String CONSOLE_ENABLED_MSG = "Console log handler is activated";
private static final String FILE_ENABLED_MSG = "File log handler is activated";
private static final String GELF_ENABLED_MSG = "GELF is activated";
private LoggingPropertyMappers() {
}
public static PropertyMapper<?>[] getMappers() { public static PropertyMapper<?>[] getMappers() {
PropertyMapper<?>[] defaultMappers = new PropertyMapper[]{ PropertyMapper<?>[] defaultMappers = new PropertyMapper[]{
@ -29,15 +36,18 @@ public final class LoggingPropertyMappers {
.paramLabel("<handler>") .paramLabel("<handler>")
.build(), .build(),
fromOption(LoggingOptions.LOG_CONSOLE_OUTPUT) fromOption(LoggingOptions.LOG_CONSOLE_OUTPUT)
.isEnabled(LoggingPropertyMappers::isConsoleEnabled, CONSOLE_ENABLED_MSG)
.to("quarkus.log.console.json") .to("quarkus.log.console.json")
.paramLabel("output") .paramLabel("output")
.transformer(LoggingPropertyMappers::resolveLogOutput) .transformer(LoggingPropertyMappers::resolveLogOutput)
.build(), .build(),
fromOption(LoggingOptions.LOG_CONSOLE_FORMAT) fromOption(LoggingOptions.LOG_CONSOLE_FORMAT)
.isEnabled(LoggingPropertyMappers::isConsoleEnabled, CONSOLE_ENABLED_MSG)
.to("quarkus.log.console.format") .to("quarkus.log.console.format")
.paramLabel("format") .paramLabel("format")
.build(), .build(),
fromOption(LoggingOptions.LOG_CONSOLE_COLOR) fromOption(LoggingOptions.LOG_CONSOLE_COLOR)
.isEnabled(LoggingPropertyMappers::isConsoleEnabled, CONSOLE_ENABLED_MSG)
.to("quarkus.log.console.color") .to("quarkus.log.console.color")
.build(), .build(),
fromOption(LoggingOptions.LOG_CONSOLE_ENABLED) fromOption(LoggingOptions.LOG_CONSOLE_ENABLED)
@ -51,15 +61,18 @@ public final class LoggingPropertyMappers {
.transformer(LoggingPropertyMappers.resolveLogHandler("file")) .transformer(LoggingPropertyMappers.resolveLogHandler("file"))
.build(), .build(),
fromOption(LoggingOptions.LOG_FILE) fromOption(LoggingOptions.LOG_FILE)
.isEnabled(LoggingPropertyMappers::isFileEnabled, FILE_ENABLED_MSG)
.to("quarkus.log.file.path") .to("quarkus.log.file.path")
.paramLabel("file") .paramLabel("file")
.transformer(LoggingPropertyMappers::resolveFileLogLocation) .transformer(LoggingPropertyMappers::resolveFileLogLocation)
.build(), .build(),
fromOption(LoggingOptions.LOG_FILE_FORMAT) fromOption(LoggingOptions.LOG_FILE_FORMAT)
.isEnabled(LoggingPropertyMappers::isFileEnabled, FILE_ENABLED_MSG)
.to("quarkus.log.file.format") .to("quarkus.log.file.format")
.paramLabel("<format>") .paramLabel("<format>")
.build(), .build(),
fromOption(LoggingOptions.LOG_FILE_OUTPUT) fromOption(LoggingOptions.LOG_FILE_OUTPUT)
.isEnabled(LoggingPropertyMappers::isFileEnabled, FILE_ENABLED_MSG)
.to("quarkus.log.file.json") .to("quarkus.log.file.json")
.paramLabel("output") .paramLabel("output")
.transformer(LoggingPropertyMappers::resolveLogOutput) .transformer(LoggingPropertyMappers::resolveLogOutput)
@ -82,52 +95,74 @@ public final class LoggingPropertyMappers {
.transformer(LoggingPropertyMappers.resolveLogHandler("gelf")) .transformer(LoggingPropertyMappers.resolveLogHandler("gelf"))
.build(), .build(),
fromOption(LoggingOptions.LOG_GELF_LEVEL) fromOption(LoggingOptions.LOG_GELF_LEVEL)
.isEnabled(LoggingPropertyMappers::isGelfEnabled, GELF_ENABLED_MSG)
.to("quarkus.log.handler.gelf.level") .to("quarkus.log.handler.gelf.level")
.paramLabel("level") .paramLabel("level")
.build(), .build(),
fromOption(LoggingOptions.LOG_GELF_HOST) fromOption(LoggingOptions.LOG_GELF_HOST)
.isEnabled(LoggingPropertyMappers::isGelfEnabled, GELF_ENABLED_MSG)
.to("quarkus.log.handler.gelf.host") .to("quarkus.log.handler.gelf.host")
.paramLabel("hostname") .paramLabel("hostname")
.build(), .build(),
fromOption(LoggingOptions.LOG_GELF_PORT) fromOption(LoggingOptions.LOG_GELF_PORT)
.isEnabled(LoggingPropertyMappers::isGelfEnabled, GELF_ENABLED_MSG)
.to("quarkus.log.handler.gelf.port") .to("quarkus.log.handler.gelf.port")
.paramLabel("port") .paramLabel("port")
.build(), .build(),
fromOption(LoggingOptions.LOG_GELF_VERSION) fromOption(LoggingOptions.LOG_GELF_VERSION)
.isEnabled(LoggingPropertyMappers::isGelfEnabled, GELF_ENABLED_MSG)
.to("quarkus.log.handler.gelf.version") .to("quarkus.log.handler.gelf.version")
.paramLabel("version") .paramLabel("version")
.build(), .build(),
fromOption(LoggingOptions.LOG_GELF_INCLUDE_STACK_TRACE) fromOption(LoggingOptions.LOG_GELF_INCLUDE_STACK_TRACE)
.isEnabled(LoggingPropertyMappers::isGelfEnabled, GELF_ENABLED_MSG)
.to("quarkus.log.handler.gelf.extract-stack-trace") .to("quarkus.log.handler.gelf.extract-stack-trace")
.build(), .build(),
fromOption(LoggingOptions.LOG_GELF_TIMESTAMP_FORMAT) fromOption(LoggingOptions.LOG_GELF_TIMESTAMP_FORMAT)
.isEnabled(LoggingPropertyMappers::isGelfEnabled, GELF_ENABLED_MSG)
.to("quarkus.log.handler.gelf.timestamp-pattern") .to("quarkus.log.handler.gelf.timestamp-pattern")
.paramLabel("pattern") .paramLabel("pattern")
.build(), .build(),
fromOption(LoggingOptions.LOG_GELF_FACILITY) fromOption(LoggingOptions.LOG_GELF_FACILITY)
.isEnabled(LoggingPropertyMappers::isGelfEnabled, GELF_ENABLED_MSG)
.to("quarkus.log.handler.gelf.facility") .to("quarkus.log.handler.gelf.facility")
.paramLabel("name") .paramLabel("name")
.build(), .build(),
fromOption(LoggingOptions.LOG_GELF_MAX_MSG_SIZE) fromOption(LoggingOptions.LOG_GELF_MAX_MSG_SIZE)
.isEnabled(LoggingPropertyMappers::isGelfEnabled, GELF_ENABLED_MSG)
.to("quarkus.log.handler.gelf.maximum-message-size") .to("quarkus.log.handler.gelf.maximum-message-size")
.paramLabel("size") .paramLabel("size")
.build(), .build(),
fromOption(LoggingOptions.LOG_GELF_INCLUDE_LOG_MSG_PARAMS) fromOption(LoggingOptions.LOG_GELF_INCLUDE_LOG_MSG_PARAMS)
.isEnabled(LoggingPropertyMappers::isGelfEnabled, GELF_ENABLED_MSG)
.to("quarkus.log.handler.gelf.include-log-message-parameters") .to("quarkus.log.handler.gelf.include-log-message-parameters")
.build(), .build(),
fromOption(LoggingOptions.LOG_GELF_INCLUDE_LOCATION) fromOption(LoggingOptions.LOG_GELF_INCLUDE_LOCATION)
.isEnabled(LoggingPropertyMappers::isGelfEnabled, GELF_ENABLED_MSG)
.to("quarkus.log.handler.gelf.include-location") .to("quarkus.log.handler.gelf.include-location")
.build() .build()
}; };
} }
public static boolean isGelfEnabled() {
return isTrue(LoggingOptions.LOG_GELF_ENABLED);
}
public static boolean isConsoleEnabled() {
return isTrue(LoggingOptions.LOG_CONSOLE_ENABLED);
}
public static boolean isFileEnabled() {
return isTrue(LoggingOptions.LOG_FILE_ENABLED);
}
private static BiFunction<Optional<String>, ConfigSourceInterceptorContext, Optional<String>> resolveLogHandler(String handler) { private static BiFunction<Optional<String>, ConfigSourceInterceptorContext, Optional<String>> resolveLogHandler(String handler) {
return (parentValue, context) -> { return (parentValue, context) -> {
//we want to fall back to console to not have nothing shown up when wrong values are set. //we want to fall back to console to not have nothing shown up when wrong values are set.
String consoleDependantErrorResult = handler.equals(LoggingOptions.DEFAULT_LOG_HANDLER.name()) ? Boolean.TRUE.toString() : Boolean.FALSE.toString(); String consoleDependantErrorResult = handler.equals(LoggingOptions.DEFAULT_LOG_HANDLER.name()) ? Boolean.TRUE.toString() : Boolean.FALSE.toString();
String handlers = parentValue.get(); String handlers = parentValue.get();
if(handlers.isBlank()) { if (handlers.isBlank()) {
addInitializationException(Messages.emptyValueForKey("log")); addInitializationException(Messages.emptyValueForKey("log"));
return of(consoleDependantErrorResult); return of(consoleDependantErrorResult);
} }

View file

@ -27,6 +27,7 @@ import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.function.BiConsumer; import java.util.function.BiConsumer;
import java.util.function.BiFunction; import java.util.function.BiFunction;
import java.util.function.BooleanSupplier;
import io.smallrye.config.ConfigSourceInterceptorContext; import io.smallrye.config.ConfigSourceInterceptorContext;
import io.smallrye.config.ConfigValue; import io.smallrye.config.ConfigValue;
@ -40,12 +41,15 @@ import org.keycloak.quarkus.runtime.cli.PropertyMapperParameterConsumer;
import org.keycloak.quarkus.runtime.configuration.ConfigArgsConfigSource; import org.keycloak.quarkus.runtime.configuration.ConfigArgsConfigSource;
import org.keycloak.quarkus.runtime.Environment; import org.keycloak.quarkus.runtime.Environment;
import org.keycloak.quarkus.runtime.configuration.MicroProfileConfigProvider; import org.keycloak.quarkus.runtime.configuration.MicroProfileConfigProvider;
import org.keycloak.utils.StringUtil;
public class PropertyMapper<T> { public class PropertyMapper<T> {
static PropertyMapper<?> IDENTITY = new PropertyMapper<>( static PropertyMapper<?> IDENTITY = new PropertyMapper<>(
new OptionBuilder<>(null, String.class).build(), new OptionBuilder<>(null, String.class).build(),
null, null,
() -> false,
"",
null, null,
null, null,
null, null,
@ -59,18 +63,23 @@ public class PropertyMapper<T> {
private final Option<T> option; private final Option<T> option;
private final String to; private final String to;
private BooleanSupplier enabled;
private String enabledWhen;
private final BiFunction<Optional<String>, ConfigSourceInterceptorContext, Optional<String>> mapper; private final BiFunction<Optional<String>, ConfigSourceInterceptorContext, Optional<String>> mapper;
private final String mapFrom; private final String mapFrom;
private final boolean mask; private final boolean mask;
private final String paramLabel; private final String paramLabel;
private final String envVarFormat; private final String envVarFormat;
private String cliFormat; private final String cliFormat;
private BiConsumer<PropertyMapper<T>, ConfigValue> validator; private final BiConsumer<PropertyMapper<T>, ConfigValue> validator;
PropertyMapper(Option<T> option, String to, BiFunction<Optional<String>, ConfigSourceInterceptorContext, Optional<String>> mapper, PropertyMapper(Option<T> option, String to, BooleanSupplier enabled, String enabledWhen,
BiFunction<Optional<String>, ConfigSourceInterceptorContext, Optional<String>> mapper,
String mapFrom, String paramLabel, boolean mask, BiConsumer<PropertyMapper<T>, ConfigValue> validator) { String mapFrom, String paramLabel, boolean mask, BiConsumer<PropertyMapper<T>, ConfigValue> validator) {
this.option = option; this.option = option;
this.to = to == null ? getFrom() : to; this.to = to == null ? getFrom() : to;
this.enabled = enabled;
this.enabledWhen = enabledWhen;
this.mapper = mapper == null ? PropertyMapper::defaultTransformer : mapper; this.mapper = mapper == null ? PropertyMapper::defaultTransformer : mapper;
this.mapFrom = mapFrom; this.mapFrom = mapFrom;
this.paramLabel = paramLabel; this.paramLabel = paramLabel;
@ -148,15 +157,39 @@ public class PropertyMapper<T> {
return transformedValue; return transformedValue;
} }
public Option<T> getOption() { return this.option; } public Option<T> getOption() {
return this.option;
}
public Class<T> getType() { return this.option.getType(); } public void setEnabled(BooleanSupplier enabled) {
this.enabled = enabled;
}
public boolean isEnabled() {
return enabled.getAsBoolean();
}
public Optional<String> getEnabledWhen() {
return Optional.of(enabledWhen)
.filter(StringUtil::isNotBlank)
.map(e -> "Available only when " + e);
}
public void setEnabledWhen(String enabledWhen) {
this.enabledWhen = enabledWhen;
}
public Class<T> getType() {
return this.option.getType();
}
public String getFrom() { public String getFrom() {
return MicroProfileConfigProvider.NS_KEYCLOAK_PREFIX + this.option.getKey(); return MicroProfileConfigProvider.NS_KEYCLOAK_PREFIX + this.option.getKey();
} }
public String getDescription() { return this.option.getDescription(); } public String getDescription() {
return this.option.getDescription();
}
public List<String> getExpectedValues() { public List<String> getExpectedValues() {
return this.option.getExpectedValues(); return this.option.getExpectedValues();
@ -209,7 +242,11 @@ public class PropertyMapper<T> {
if (mapper == null || (mapFrom == null && name.equals(getFrom()))) { if (mapper == null || (mapFrom == null && name.equals(getFrom()))) {
// no mapper set or requesting a property that does not depend on other property, just return the value from the config source // no mapper set or requesting a property that does not depend on other property, just return the value from the config source
return ConfigValue.builder().withName(name).withValue(value.orElse(null)).withConfigSourceName(configSourceName).build(); return ConfigValue.builder()
.withName(name)
.withValue(value.orElse(null))
.withConfigSourceName(configSourceName)
.build();
} }
Optional<String> mappedValue = mapper.apply(value, context); Optional<String> mappedValue = mapper.apply(value, context);
@ -218,8 +255,12 @@ public class PropertyMapper<T> {
return null; return null;
} }
return ConfigValue.builder().withName(name).withValue(mappedValue.get()).withRawValue(value.orElse(null)) return ConfigValue.builder()
.withConfigSourceName(configSourceName).build(); .withName(name)
.withValue(mappedValue.get())
.withRawValue(value.orElse(null))
.withConfigSourceName(configSourceName)
.build();
} }
private ConfigValue convertValue(ConfigValue configValue) { private ConfigValue convertValue(ConfigValue configValue) {
@ -237,8 +278,10 @@ public class PropertyMapper<T> {
private BiFunction<Optional<String>, ConfigSourceInterceptorContext, Optional<String>> mapper; private BiFunction<Optional<String>, ConfigSourceInterceptorContext, Optional<String>> mapper;
private String mapFrom = null; private String mapFrom = null;
private boolean isMasked = false; private boolean isMasked = false;
private BooleanSupplier isEnabled = () -> true;
private String enabledWhen = "";
private String paramLabel; private String paramLabel;
private BiConsumer<PropertyMapper<T>, ConfigValue> validator = (mapper, value) -> mapper.validateExpectedValues(value, (c, v) -> mapper.validateSingleValue(c, v)); private BiConsumer<PropertyMapper<T>, ConfigValue> validator = (mapper, value) -> mapper.validateExpectedValues(value, mapper::validateSingleValue);
public Builder(Option<T> option) { public Builder(Option<T> option) {
this.option = option; this.option = option;
@ -269,6 +312,17 @@ public class PropertyMapper<T> {
return this; return this;
} }
public Builder<T> isEnabled(BooleanSupplier isEnabled, String enabledWhen) {
this.isEnabled = isEnabled;
this.enabledWhen=enabledWhen;
return this;
}
public Builder<T> isEnabled(BooleanSupplier isEnabled) {
this.isEnabled = isEnabled;
return this;
}
public Builder<T> validator(BiConsumer<PropertyMapper<T>, ConfigValue> validator) { public Builder<T> validator(BiConsumer<PropertyMapper<T>, ConfigValue> validator) {
this.validator = validator; this.validator = validator;
return this; return this;
@ -278,7 +332,7 @@ public class PropertyMapper<T> {
if (paramLabel == null && Boolean.class.equals(option.getType())) { if (paramLabel == null && Boolean.class.equals(option.getType())) {
paramLabel = Boolean.TRUE + "|" + Boolean.FALSE; paramLabel = Boolean.TRUE + "|" + Boolean.FALSE;
} }
return new PropertyMapper<T>(option, to, mapper, mapFrom, paramLabel, isMasked, validator); return new PropertyMapper<T>(option, to, isEnabled, enabledWhen, mapper, mapFrom, paramLabel, isMasked, validator);
} }
} }
@ -309,7 +363,7 @@ public class PropertyMapper<T> {
} }
} }
private boolean isCliOption(ConfigValue configValue) { public static boolean isCliOption(ConfigValue configValue) {
return Optional.ofNullable(configValue.getConfigSourceName()).filter(name -> name.contains(ConfigArgsConfigSource.NAME)).isPresent(); return Optional.ofNullable(configValue.getConfigSourceName()).filter(name -> name.contains(ConfigArgsConfigSource.NAME)).isPresent();
} }

View file

@ -3,23 +3,45 @@ package org.keycloak.quarkus.runtime.configuration.mappers;
import io.smallrye.config.ConfigSourceInterceptorContext; import io.smallrye.config.ConfigSourceInterceptorContext;
import io.smallrye.config.ConfigValue; import io.smallrye.config.ConfigValue;
import jakarta.ws.rs.core.MultivaluedHashMap;
import org.jboss.logging.Logger;
import org.keycloak.common.util.CollectionUtil;
import org.keycloak.config.ConfigSupportLevel; import org.keycloak.config.ConfigSupportLevel;
import org.keycloak.config.OptionCategory; import org.keycloak.config.OptionCategory;
import org.keycloak.quarkus.runtime.Environment; import org.keycloak.quarkus.runtime.Environment;
import org.keycloak.quarkus.runtime.cli.PropertyException;
import org.keycloak.quarkus.runtime.cli.command.AbstractCommand;
import org.keycloak.quarkus.runtime.cli.command.Build;
import org.keycloak.quarkus.runtime.cli.command.ShowConfig;
import org.keycloak.quarkus.runtime.configuration.ConfigArgsConfigSource; import org.keycloak.quarkus.runtime.configuration.ConfigArgsConfigSource;
import org.keycloak.quarkus.runtime.configuration.DisabledMappersInterceptor;
import org.keycloak.quarkus.runtime.configuration.PersistedConfigSource;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.Collections;
import java.util.EnumMap; import java.util.EnumMap;
import java.util.EnumSet;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.function.Function; import java.util.Optional;
import java.util.function.BiConsumer;
import java.util.Set;
import java.util.function.BooleanSupplier;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import static org.keycloak.quarkus.runtime.Environment.isParsedCommand;
import static org.keycloak.quarkus.runtime.Environment.isRebuild;
import static org.keycloak.quarkus.runtime.Environment.isRebuildCheck;
public final class PropertyMappers { public final class PropertyMappers {
public static String VALUE_MASK = "*******"; public static String VALUE_MASK = "*******";
private static final MappersConfig MAPPERS = new MappersConfig(); private static final MappersConfig MAPPERS = new MappersConfig();
private static final Logger log = Logger.getLogger(PropertyMappers.class);
private PropertyMappers(){} private PropertyMappers(){}
@ -44,8 +66,7 @@ public final class PropertyMappers {
} }
public static ConfigValue getValue(ConfigSourceInterceptorContext context, String name) { public static ConfigValue getValue(ConfigSourceInterceptorContext context, String name) {
PropertyMapper<?> mapper = MAPPERS.getOrDefault(name, PropertyMapper.IDENTITY); return getMapperOrDefault(name, PropertyMapper.IDENTITY).getConfigValue(name, context);
return mapper.getConfigValue(name, context);
} }
public static boolean isBuildTimeProperty(String name) { public static boolean isBuildTimeProperty(String name) {
@ -53,7 +74,7 @@ public final class PropertyMappers {
return true; return true;
} }
PropertyMapper<?> mapper = MAPPERS.get(name); final PropertyMapper<?> mapper = getMapperOrDefault(name, null);
boolean isBuildTimeProperty = mapper == null ? false : mapper.isBuildTime(); boolean isBuildTimeProperty = mapper == null ? false : mapper.isBuildTime();
return isBuildTimeProperty return isBuildTimeProperty
@ -83,6 +104,27 @@ public final class PropertyMappers {
return MAPPERS.getBuildTimeMappers(); return MAPPERS.getBuildTimeMappers();
} }
public static Map<String, PropertyMapper<?>> getDisabledMappers() {
final var disabledMappers = new HashMap<>(getDisabledBuildTimeMappers());
disabledMappers.putAll(getDisabledRuntimeMappers());
return disabledMappers;
}
public static Map<String, PropertyMapper<?>> getDisabledRuntimeMappers() {
return MAPPERS.getDisabledRuntimeMappers();
}
public static Map<String, PropertyMapper<?>> getDisabledBuildTimeMappers() {
return MAPPERS.getDisabledBuildTimeMappers();
}
/**
* Removes all disabled mappers from the runtime/buildtime mappers
*/
public static void sanitizeDisabledMappers() {
MAPPERS.sanitizeDisabledMappers();
}
public static String formatValue(String property, String value) { public static String formatValue(String property, String value) {
property = removeProfilePrefixIfNeeded(property); property = removeProfilePrefixIfNeeded(property);
PropertyMapper<?> mapper = getMapper(property); PropertyMapper<?> mapper = getMapper(property);
@ -102,32 +144,95 @@ public final class PropertyMappers {
return property; return property;
} }
public static PropertyMapper<?> getMapper(String property) { private static PropertyMapper<?> getMapperOrDefault(String property, PropertyMapper<?> defaultMapper) {
if (property.startsWith("%")) { final var mappers = MAPPERS.getOrDefault(property, Collections.emptyList());
return MAPPERS.get(property.substring(property.indexOf('.') + 1));
} return switch (mappers.size()) {
return MAPPERS.get(property); case 0 -> defaultMapper;
case 1 -> mappers.get(0);
default -> {
var allowedMappers = filterDeniedCategories(mappers);
yield switch (allowedMappers.size()) {
case 0 -> defaultMapper;
case 1 -> allowedMappers.iterator().next();
default -> {
log.debugf("Duplicated mappers for key '%s'. Used the first found.", property);
yield allowedMappers.iterator().next();
}
};
}
};
} }
public static Collection<PropertyMapper<?>> getMappers() { public static PropertyMapper<?> getMapper(String property) {
return MAPPERS.values(); return getMapperOrDefault(polishProperty(property), null);
}
public static List<PropertyMapper<?>> getMappers(String property) {
return MAPPERS.get(polishProperty(property));
}
public static Set<PropertyMapper<?>> getMappers() {
return MAPPERS.values().stream().flatMap(Collection::stream).collect(Collectors.toSet());
} }
public static boolean isSupported(PropertyMapper<?> mapper) { public static boolean isSupported(PropertyMapper<?> mapper) {
return mapper.getCategory().getSupportLevel().equals(ConfigSupportLevel.SUPPORTED); return mapper.getCategory().getSupportLevel().equals(ConfigSupportLevel.SUPPORTED);
} }
private static class MappersConfig extends HashMap<String, PropertyMapper<?>> { public static Optional<PropertyMapper<?>> getDisabledMapper(String property) {
if (property == null) return Optional.empty();
private Map<OptionCategory, List<PropertyMapper<?>>> buildTimeMappers = new EnumMap<>(OptionCategory.class); PropertyMapper<?> mapper = getDisabledBuildTimeMappers().get(property);
private Map<OptionCategory, List<PropertyMapper<?>>> runtimeTimeMappers = new EnumMap<>(OptionCategory.class); if (mapper == null) {
mapper = getDisabledRuntimeMappers().get(property);
}
return Optional.ofNullable(mapper);
}
public static boolean isDisabledMapper(String property) {
final Predicate<String> isDisabledMapper = (p) -> getDisabledMapper(p).isPresent() && getMapper(p) == null;
if (property.startsWith("%")) {
return isDisabledMapper.test(property.substring(property.indexOf('.') + 1));
}
return isDisabledMapper.test(property);
}
private static String polishProperty(String property) {
return property.startsWith("%") ? property.substring(property.indexOf('.') + 1) : property;
}
private static Set<PropertyMapper<?>> filterDeniedCategories(List<PropertyMapper<?>> mappers) {
final var allowedCategories = Environment.getParsedCommand()
.map(AbstractCommand::getOptionCategories)
.map(EnumSet::copyOf)
.orElseGet(() -> EnumSet.allOf(OptionCategory.class));
return mappers.stream().filter(f -> allowedCategories.contains(f.getCategory())).collect(Collectors.toSet());
}
private static class MappersConfig extends MultivaluedHashMap<String, PropertyMapper<?>> {
private final Map<OptionCategory, List<PropertyMapper<?>>> buildTimeMappers = new EnumMap<>(OptionCategory.class);
private final Map<OptionCategory, List<PropertyMapper<?>>> runtimeTimeMappers = new EnumMap<>(OptionCategory.class);
private final Map<String, PropertyMapper<?>> disabledBuildTimeMappers = new HashMap<>();
private final Map<String, PropertyMapper<?>> disabledRuntimeMappers = new HashMap<>();
public void addAll(PropertyMapper<?>[] mappers, BooleanSupplier isEnabled, String enabledWhen) {
Arrays.stream(mappers).forEach(mapper -> {
mapper.setEnabled(isEnabled);
mapper.setEnabledWhen(enabledWhen);
});
addAll(mappers);
}
public void addAll(PropertyMapper<?>[] mappers) { public void addAll(PropertyMapper<?>[] mappers) {
for (PropertyMapper<?> mapper : mappers) { for (PropertyMapper<?> mapper : mappers) {
super.put(mapper.getTo(), mapper); addMapper(mapper);
super.put(mapper.getFrom(), mapper);
super.put(mapper.getCliFormat(), mapper);
super.put(mapper.getEnvVarFormat(), mapper);
if (mapper.isBuildTime()) { if (mapper.isBuildTime()) {
addMapperByStage(mapper, buildTimeMappers); addMapperByStage(mapper, buildTimeMappers);
@ -137,22 +242,68 @@ public final class PropertyMappers {
} }
} }
private void addMapperByStage(PropertyMapper<?> mapper, Map<OptionCategory, List<PropertyMapper<?>>> mappers) { private static void addMapperByStage(PropertyMapper<?> mapper, Map<OptionCategory, List<PropertyMapper<?>>> mappers) {
mappers.computeIfAbsent(mapper.getCategory(), mappers.computeIfAbsent(mapper.getCategory(), c -> new ArrayList<>()).add(mapper);
new Function<OptionCategory, List<PropertyMapper<?>>>() {
@Override
public List<PropertyMapper<?>> apply(OptionCategory c) {
return new ArrayList<>();
}
}).add(mapper);
} }
@Override public void addMapper(PropertyMapper<?> mapper) {
public PropertyMapper<?> put(String key, PropertyMapper<?> value) { handleMapper(mapper, this::add);
if (containsKey(key)) { }
throw new IllegalArgumentException("Duplicated mapper for key [" + key + "]");
public void removeMapper(PropertyMapper<?> mapper) {
handleMapper(mapper, this::remove);
}
private void remove(String key, PropertyMapper<?> mapper) {
List<PropertyMapper<?>> list = get(key);
if (CollectionUtil.isNotEmpty(list)) {
list.remove(mapper);
}
}
public void sanitizeDisabledMappers() {
if (Environment.getParsedCommand().isEmpty()) return; // do not sanitize when no command is present
DisabledMappersInterceptor.runWithDisabled(() -> { // We need to have the whole configuration available
// Initialize profile in order to check state of features. Disable Persisted CS for re-augmentation
if (isRebuildCheck()) {
PersistedConfigSource.getInstance().runWithDisabled(Environment::getCurrentOrCreateFeatureProfile);
} else {
Environment.getCurrentOrCreateFeatureProfile();
}
sanitizeMappers(buildTimeMappers, disabledBuildTimeMappers);
sanitizeMappers(runtimeTimeMappers, disabledRuntimeMappers);
assertDuplicatedMappers();
});
}
private void assertDuplicatedMappers() {
final var duplicatedMappers = entrySet().stream()
.filter(e -> CollectionUtil.isNotEmpty(e.getValue()))
.filter(e -> e.getValue().size() > 1)
.toList();
final var isBuildPhase = isRebuild() || isRebuildCheck() || isParsedCommand(Build.NAME);
final var allowedForCommand = isParsedCommand(ShowConfig.NAME);
if (!duplicatedMappers.isEmpty()) {
duplicatedMappers.forEach(f -> {
final var filteredMappers = filterDeniedCategories(f.getValue());
if (filteredMappers.size() > 1) {
final var areBuildTimeMappers = filteredMappers.stream().anyMatch(PropertyMapper::isBuildTime);
// thrown in runtime, or in build time, when some mapper is marked as buildTime + not allowed to have duplicates for specific command
final var shouldBeThrown = !allowedForCommand && (!isBuildPhase || areBuildTimeMappers);
if (shouldBeThrown) {
throw new PropertyException(String.format("Duplicated mapper for key '%s'.", f.getKey()));
}
}
});
} }
return super.put(key, value);
} }
public Map<OptionCategory, List<PropertyMapper<?>>> getRuntimeMappers() { public Map<OptionCategory, List<PropertyMapper<?>>> getRuntimeMappers() {
@ -162,6 +313,35 @@ public final class PropertyMappers {
public Map<OptionCategory, List<PropertyMapper<?>>> getBuildTimeMappers() { public Map<OptionCategory, List<PropertyMapper<?>>> getBuildTimeMappers() {
return buildTimeMappers; return buildTimeMappers;
} }
}
public Map<String, PropertyMapper<?>> getDisabledBuildTimeMappers() {
return disabledBuildTimeMappers;
}
public Map<String, PropertyMapper<?>> getDisabledRuntimeMappers() {
return disabledRuntimeMappers;
}
private static void sanitizeMappers(Map<OptionCategory, List<PropertyMapper<?>>> mappers,
Map<String, PropertyMapper<?>> disabledMappers) {
mappers.forEach((category, propertyMappers) ->
propertyMappers.removeIf(pm -> {
final boolean shouldRemove = !pm.isEnabled();
if (shouldRemove) {
MAPPERS.removeMapper(pm);
handleMapper(pm, disabledMappers::put);
}
return shouldRemove;
}));
}
private static void handleMapper(PropertyMapper<?> mapper, BiConsumer<String, PropertyMapper<?>> operation) {
operation.accept(mapper.getFrom(), mapper);
if (!mapper.getFrom().equals(mapper.getTo())) {
operation.accept(mapper.getTo(), mapper);
}
operation.accept(mapper.getCliFormat(), mapper);
operation.accept(mapper.getEnvVarFormat(), mapper);
}
}
} }

View file

@ -15,4 +15,5 @@
# limitations under the License. # limitations under the License.
# #
org.keycloak.quarkus.runtime.configuration.PropertyMappingInterceptor org.keycloak.quarkus.runtime.configuration.PropertyMappingInterceptor
org.keycloak.quarkus.runtime.configuration.DisabledMappersInterceptor

View file

@ -2,4 +2,8 @@ spi-hostname-default-frontend-url = ${keycloak.frontendUrl:http://filepropdefaul
%user-profile.spi-hostname-default-frontend-url = http://filepropprofile.unittest %user-profile.spi-hostname-default-frontend-url = http://filepropprofile.unittest
log-level=${SOME_LOG_LEVEL:warn} log-level=${SOME_LOG_LEVEL:warn}
config-keystore=src/test/resources/keystore config-keystore=src/test/resources/keystore
config-keystore-password=secret config-keystore-password=secret
quarkus.log.file.path=random/path
log-gelf-level=WARN

View file

@ -87,7 +87,7 @@ public class OptionValidationTest {
} }
@Test @Test
@Launch({"start", "--db-username=foobar","--db-pasword=mytestpw", "--foobar=barfoo"}) @Launch({"start", "--db-username=foobar", "--db-pasword=mytestpw", "--foobar=barfoo"})
public void failWithFirstOptionOnMultipleUnknownOptions(LaunchResult result) { public void failWithFirstOptionOnMultipleUnknownOptions(LaunchResult result) {
CLIResult cliResult = (CLIResult) result; CLIResult cliResult = (CLIResult) result;
assertEquals("Unknown option: '--db-pasword'\n" + assertEquals("Unknown option: '--db-pasword'\n" +
@ -96,7 +96,7 @@ public class OptionValidationTest {
} }
@Test @Test
@Launch({ "start", "--db postgres" }) @Launch({"start", "--db postgres"})
void failSingleParamWithSpace(LaunchResult result) { void failSingleParamWithSpace(LaunchResult result) {
CLIResult cliResult = (CLIResult) result; CLIResult cliResult = (CLIResult) result;
cliResult.assertError("Option: '--db postgres' is not expected to contain whitespace, please remove any unnecessary quoting/escaping"); cliResult.assertError("Option: '--db postgres' is not expected to contain whitespace, please remove any unnecessary quoting/escaping");

View file

@ -38,6 +38,6 @@ public class ExportDistTest {
cliResult.assertNoMessage("Listening on: http"); cliResult.assertNoMessage("Listening on: http");
cliResult = dist.run("export", "--realm=master"); cliResult = dist.run("export", "--realm=master");
cliResult.assertMessage("Must specify either --dir or --file options."); cliResult.assertError("Must specify either --dir or --file options.");
} }
} }

View file

@ -43,5 +43,8 @@ public class ImportDistTest {
cliResult.assertMessage("Import finished successfully"); cliResult.assertMessage("Import finished successfully");
cliResult.assertNoMessage("Changes detected in configuration"); cliResult.assertNoMessage("Changes detected in configuration");
cliResult.assertNoMessage("Listening on: http"); cliResult.assertNoMessage("Listening on: http");
cliResult = dist.run("import");
cliResult.assertError("Must specify either --dir or --file options.");
} }
} }

View file

@ -96,6 +96,9 @@ public class LoggingDistTest {
@Launch({ "start-dev", "--log-console-output=json" }) @Launch({ "start-dev", "--log-console-output=json" })
void testJsonFormatApplied(LaunchResult result) throws JsonProcessingException { void testJsonFormatApplied(LaunchResult result) throws JsonProcessingException {
CLIResult cliResult = (CLIResult) result; CLIResult cliResult = (CLIResult) result;
cliResult.assertMessage("The following used run time options are UNAVAILABLE and will be ignored during build time:");
cliResult.assertMessage("- log-console-output: Available only when Console log handler is activated.");
cliResult.assertJsonLogDefaultsApplied(); cliResult.assertJsonLogDefaultsApplied();
cliResult.assertStartedDevMode(); cliResult.assertStartedDevMode();
} }

View file

@ -20,43 +20,117 @@ package org.keycloak.it.cli.dist;
import io.quarkus.test.junit.main.Launch; import io.quarkus.test.junit.main.Launch;
import io.quarkus.test.junit.main.LaunchResult; import io.quarkus.test.junit.main.LaunchResult;
import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import org.keycloak.it.junit5.extension.CLIResult; import org.keycloak.it.junit5.extension.CLIResult;
import org.keycloak.it.junit5.extension.DistributionTest; import org.keycloak.it.junit5.extension.DistributionTest;
import org.keycloak.it.junit5.extension.RawDistOnly; import org.keycloak.it.junit5.extension.RawDistOnly;
import org.keycloak.it.junit5.extension.WithEnvVars;
import org.keycloak.it.utils.KeycloakDistribution; import org.keycloak.it.utils.KeycloakDistribution;
import java.nio.file.Paths; import java.nio.file.Paths;
import static org.junit.Assert.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.keycloak.quarkus.runtime.cli.command.Main.CONFIG_FILE_LONG_NAME; import static org.keycloak.quarkus.runtime.cli.command.Main.CONFIG_FILE_LONG_NAME;
@DistributionTest @DistributionTest
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class OptionsDistTest { public class OptionsDistTest {
@Test @Test
@Order(1)
@Launch({"build", "--db=invalid"}) @Launch({"build", "--db=invalid"})
public void failInvalidOptionValue(LaunchResult result) { public void failInvalidOptionValue(LaunchResult result) {
Assertions.assertTrue(result.getErrorOutput().contains("Invalid value for option '--db': invalid. Expected values are: dev-file, dev-mem, mariadb, mssql, mysql, oracle, postgres")); Assertions.assertTrue(result.getErrorOutput().contains("Invalid value for option '--db': invalid. Expected values are: dev-file, dev-mem, mariadb, mssql, mysql, oracle, postgres"));
} }
@Test @Test
@Launch({"start-dev", "--test=invalid"}) @Order(2)
public void testServerDoesNotStartIfValidationFailDuringReAugStartDev(LaunchResult result) {
assertEquals(1, result.getErrorStream().stream().filter(s -> s.contains("Unknown option: '--test'")).count());
}
@Test
@Launch({"start", "--test=invalid"}) @Launch({"start", "--test=invalid"})
public void testServerDoesNotStartIfValidationFailDuringReAugStart(LaunchResult result) { public void testServerDoesNotStartIfValidationFailDuringReAugStart(LaunchResult result) {
assertEquals(1, result.getErrorStream().stream().filter(s -> s.contains("Unknown option: '--test'")).count()); assertEquals(1, result.getErrorStream().stream().filter(s -> s.contains("Unknown option: '--test'")).count());
} }
@Test @Test
@Order(3)
@Launch({"start", "--log=console", "--log-file-output=json", "--http-enabled=true", "--hostname-strict=false"})
public void testServerDoesNotStartIfDisabledFileLogOption(LaunchResult result) {
assertEquals(1, result.getErrorStream().stream().filter(s -> s.contains("Disabled option: '--log-file-output'. Available only when File log handler is activated")).count());
assertEquals(1, result.getErrorStream().stream().filter(s -> s.contains("Possible solutions: --log, --log-console-output, --log-console-format, --log-console-color")).count());
}
@Test
@Order(4)
@Launch({"start", "--log=file", "--log-file-output=json", "--http-enabled=true", "--hostname-strict=false"})
public void testServerStartIfEnabledFileLogOption(LaunchResult result) {
assertEquals(0, result.getErrorStream().stream().filter(s -> s.contains("Disabled option: '--log-file-output'. Available only when File log handler is activated")).count());
}
@Test
@Order(5)
@WithEnvVars({"KC_LOG", "console", "KC_LOG_CONSOLE_COLOR", "true", "KC_LOG_FILE", "something-env", "KC_LOG_GELF_VERSION", "1.1", "KC_HTTP_ENABLED", "true", "KC_HOSTNAME_STRICT", "false"})
@Launch({"start"})
public void testSettingEnvVars(LaunchResult result) {
CLIResult cliResult = (CLIResult) result;
cliResult.assertMessage("The following used run time options are UNAVAILABLE and will be ignored during build time:");
cliResult.assertMessage("- log-file: Available only when File log handler is activated.");
cliResult.assertMessage("- log-gelf-version: Available only when GELF is activated.");
cliResult.assertMessage("quarkus.log.console.color");
cliResult.assertMessage("config property is deprecated and should not be used anymore");
}
@Test
@Order(6)
@RawDistOnly(reason = "Raw is enough and we avoid issues with including custom conf file in the container") @RawDistOnly(reason = "Raw is enough and we avoid issues with including custom conf file in the container")
public void testExpressionsInConfigFile(KeycloakDistribution distribution) { public void testExpressionsInConfigFile(KeycloakDistribution distribution) {
distribution.setEnvVar("MY_LOG_LEVEL", "debug"); distribution.setEnvVar("MY_LOG_LEVEL", "warn");
CLIResult result = distribution.run(CONFIG_FILE_LONG_NAME + "=" + Paths.get("src/test/resources/OptionsDistTest/keycloak.conf").toAbsolutePath().normalize(), "start-dev"); CLIResult result = distribution.run(CONFIG_FILE_LONG_NAME + "=" + Paths.get("src/test/resources/OptionsDistTest/keycloak.conf").toAbsolutePath().normalize(), "start", "--http-enabled=true", "--hostname-strict=false", "--optimized");
result.assertMessage("DEBUG [org.keycloak"); result.assertNoMessage("INFO [io.quarkus]");
result.assertNoMessage("Listening on:");
// specified in the OptionsDistTest/keycloak.conf
result.assertMessage("The following used run time options are UNAVAILABLE and will be ignored during build time:");
result.assertMessage("- log-gelf-level: Available only when GELF is activated.");
result.assertMessage("- log-gelf-version: Available only when GELF is activated.");
}
@Test
@Order(7)
@Launch({"start", "--log=console", "--log-gelf-include-stack-trace=true"})
public void testDisabledGelfOption(LaunchResult result) {
CLIResult cliResult = (CLIResult) result;
cliResult.assertError("Disabled option: '--log-gelf-include-stack-trace'. Available only when GELF is activated");
cliResult.assertError("Possible solutions: --log, --log-console-output, --log-console-format, --log-console-color, --log-level");
cliResult.assertError("Try '" + KeycloakDistribution.SCRIPT_CMD + " start --help' for more information on the available options.");
cliResult.assertError("Specify '--help-all' to obtain information on all options and their availability.");
}
// Start-dev should be executed as last tests - build is done for development mode
@Test
@Order(8)
@Launch({"start-dev", "--test=invalid"})
public void testServerDoesNotStartIfValidationFailDuringReAugStartDev(LaunchResult result) {
assertEquals(1, result.getErrorStream().stream().filter(s -> s.contains("Unknown option: '--test'")).count());
}
@Test
@Order(9)
@Launch({"start-dev", "--log=console", "--log-file-output=json"})
public void testServerDoesNotStartDevIfDisabledFileLogOption(LaunchResult result) {
assertEquals(1, result.getErrorStream().stream().filter(s -> s.contains("Disabled option: '--log-file-output'. Available only when File log handler is activated")).count());
assertEquals(1, result.getErrorStream().stream().filter(s -> s.contains("Possible solutions: --log, --log-console-output, --log-console-format, --log-console-color")).count());
}
@Test
@Order(10)
@Launch({"start-dev", "--log=file", "--log-file-output=json", "--log-console-color=true"})
public void testServerStartDevIfEnabledFileLogOption(LaunchResult result) {
assertEquals(0, result.getErrorStream().stream().filter(s -> s.contains("Disabled option: '--log-file-output'. Available only when File log handler is activated")).count());
assertEquals(1, result.getErrorStream().stream().filter(s -> s.contains("Disabled option: '--log-console-color'. Available only when Console log handler is activated")).count());
assertEquals(1, result.getErrorStream().stream().filter(s -> s.contains("Possible solutions: --log, --log-file, --log-file-format, --log-file-output, --log-level")).count());
} }
} }

View file

@ -1 +1,8 @@
log-level=${MY_LOG_LEVEL:warn} log-level=${MY_LOG_LEVEL:warn}
log=console,file
log-file-output=json
# Ignored disabled
log-gelf-level=WARN
log-gelf-version=1.1

View file

@ -74,47 +74,16 @@ Logging:
--log <handler> Enable one or more log handlers in a comma-separated list. Possible values --log <handler> Enable one or more log handlers in a comma-separated list. Possible values
are: console, file, gelf (deprecated). Default: console. are: console, file, gelf (deprecated). Default: console.
--log-console-color <true|false> --log-console-color <true|false>
Enable or disable colors when logging to console. Default: false. Enable or disable colors when logging to console. Default: false. Available
only when Console log handler is activated.
--log-console-format <format> --log-console-format <format>
The format of unstructured console log entries. If the format has spaces in The format of unstructured console log entries. If the format has spaces in
it, escape the value using "<format>". Default: %d{yyyy-MM-dd HH:mm:ss,SSS} % it, escape the value using "<format>". Default: %d{yyyy-MM-dd HH:mm:ss,SSS} %
-5p [%c] (%t) %s%e%n. -5p [%c] (%t) %s%e%n. Available only when Console log handler is activated.
--log-console-output <output> --log-console-output <output>
Set the log output to JSON or default (plain) unstructured logging. Possible Set the log output to JSON or default (plain) unstructured logging. Possible
values are: default, json. Default: default. values are: default, json. Default: default. Available only when Console log
--log-file <file> Set the log file path and filename. Default: data/log/keycloak.log. handler is activated.
--log-file-format <format>
Set a format specific to file log entries. Default: %d{yyyy-MM-dd HH:mm:ss,
SSS} %-5p [%c] (%t) %s%e%n.
--log-file-output <output>
Set the log output to JSON or default (plain) unstructured logging. Possible
values are: default, json. Default: default.
--log-gelf-facility <name>
DEPRECATED. The facility (name of the process) that sends the message.
Default: keycloak.
--log-gelf-host <hostname>
DEPRECATED. Hostname of the Logstash or Graylog Host. By default UDP is used,
prefix the host with 'tcp:' to switch to TCP. Example: 'tcp:localhost'
Default: localhost.
--log-gelf-include-location <true|false>
DEPRECATED. Include source code location. Default: true.
--log-gelf-include-message-parameters <true|false>
DEPRECATED. Include message parameters from the log event. Default: true.
--log-gelf-include-stack-trace <true|false>
DEPRECATED. If set to true, occuring stack traces are included in the
'StackTrace' field in the GELF output. Default: true.
--log-gelf-level <level>
DEPRECATED. The log level specifying which message levels will be logged by
the GELF logger. Message levels lower than this value will be discarded.
Default: INFO.
--log-gelf-max-message-size <size>
DEPRECATED. Maximum message size (in bytes). If the message size is exceeded,
GELF will submit the message in multiple chunks. Default: 8192.
--log-gelf-port <port>
DEPRECATED. The port the Logstash or Graylog Host is called on. Default: 12201.
--log-gelf-timestamp-format <pattern>
DEPRECATED. Set the format for the GELF timestamp field. Uses Java
SimpleDateFormat pattern. Default: yyyy-MM-dd HH:mm:ss,SSS.
--log-level <category:level> --log-level <category:level>
The log level of the root category or a comma-separated list of individual The log level of the root category or a comma-separated list of individual
categories and their levels. For the root category, you don't need to categories and their levels. For the root category, you don't need to

View file

@ -74,47 +74,61 @@ Logging:
--log <handler> Enable one or more log handlers in a comma-separated list. Possible values --log <handler> Enable one or more log handlers in a comma-separated list. Possible values
are: console, file, gelf (deprecated). Default: console. are: console, file, gelf (deprecated). Default: console.
--log-console-color <true|false> --log-console-color <true|false>
Enable or disable colors when logging to console. Default: false. Enable or disable colors when logging to console. Default: false. Available
only when Console log handler is activated.
--log-console-format <format> --log-console-format <format>
The format of unstructured console log entries. If the format has spaces in The format of unstructured console log entries. If the format has spaces in
it, escape the value using "<format>". Default: %d{yyyy-MM-dd HH:mm:ss,SSS} % it, escape the value using "<format>". Default: %d{yyyy-MM-dd HH:mm:ss,SSS} %
-5p [%c] (%t) %s%e%n. -5p [%c] (%t) %s%e%n. Available only when Console log handler is activated.
--log-console-output <output> --log-console-output <output>
Set the log output to JSON or default (plain) unstructured logging. Possible Set the log output to JSON or default (plain) unstructured logging. Possible
values are: default, json. Default: default. values are: default, json. Default: default. Available only when Console log
--log-file <file> Set the log file path and filename. Default: data/log/keycloak.log. handler is activated.
--log-file <file> Set the log file path and filename. Default: data/log/keycloak.log. Available
only when File log handler is activated.
--log-file-format <format> --log-file-format <format>
Set a format specific to file log entries. Default: %d{yyyy-MM-dd HH:mm:ss, Set a format specific to file log entries. Default: %d{yyyy-MM-dd HH:mm:ss,
SSS} %-5p [%c] (%t) %s%e%n. SSS} %-5p [%c] (%t) %s%e%n. Available only when File log handler is
activated.
--log-file-output <output> --log-file-output <output>
Set the log output to JSON or default (plain) unstructured logging. Possible Set the log output to JSON or default (plain) unstructured logging. Possible
values are: default, json. Default: default. values are: default, json. Default: default. Available only when File log
handler is activated.
--log-gelf-facility <name> --log-gelf-facility <name>
DEPRECATED. The facility (name of the process) that sends the message. DEPRECATED. The facility (name of the process) that sends the message.
Default: keycloak. Default: keycloak. Available only when GELF is activated.
--log-gelf-host <hostname> --log-gelf-host <hostname>
DEPRECATED. Hostname of the Logstash or Graylog Host. By default UDP is used, DEPRECATED. Hostname of the Logstash or Graylog Host. By default UDP is used,
prefix the host with 'tcp:' to switch to TCP. Example: 'tcp:localhost' prefix the host with 'tcp:' to switch to TCP. Example: 'tcp:localhost'
Default: localhost. Default: localhost. Available only when GELF is activated.
--log-gelf-include-location <true|false> --log-gelf-include-location <true|false>
DEPRECATED. Include source code location. Default: true. DEPRECATED. Include source code location. Default: true. Available only when
GELF is activated.
--log-gelf-include-message-parameters <true|false> --log-gelf-include-message-parameters <true|false>
DEPRECATED. Include message parameters from the log event. Default: true. DEPRECATED. Include message parameters from the log event. Default: true.
Available only when GELF is activated.
--log-gelf-include-stack-trace <true|false> --log-gelf-include-stack-trace <true|false>
DEPRECATED. If set to true, occuring stack traces are included in the DEPRECATED. If set to true, occuring stack traces are included in the
'StackTrace' field in the GELF output. Default: true. 'StackTrace' field in the GELF output. Default: true. Available only when
GELF is activated.
--log-gelf-level <level> --log-gelf-level <level>
DEPRECATED. The log level specifying which message levels will be logged by DEPRECATED. The log level specifying which message levels will be logged by
the GELF logger. Message levels lower than this value will be discarded. the GELF logger. Message levels lower than this value will be discarded.
Default: INFO. Default: INFO. Available only when GELF is activated.
--log-gelf-max-message-size <size> --log-gelf-max-message-size <size>
DEPRECATED. Maximum message size (in bytes). If the message size is exceeded, DEPRECATED. Maximum message size (in bytes). If the message size is exceeded,
GELF will submit the message in multiple chunks. Default: 8192. GELF will submit the message in multiple chunks. Default: 8192. Available
only when GELF is activated.
--log-gelf-port <port> --log-gelf-port <port>
DEPRECATED. The port the Logstash or Graylog Host is called on. Default: 12201. DEPRECATED. The port the Logstash or Graylog Host is called on. Default:
12201. Available only when GELF is activated.
--log-gelf-timestamp-format <pattern> --log-gelf-timestamp-format <pattern>
DEPRECATED. Set the format for the GELF timestamp field. Uses Java DEPRECATED. Set the format for the GELF timestamp field. Uses Java
SimpleDateFormat pattern. Default: yyyy-MM-dd HH:mm:ss,SSS. SimpleDateFormat pattern. Default: yyyy-MM-dd HH:mm:ss,SSS. Available only
when GELF is activated.
--log-gelf-version <version>
The GELF version to be used. Possible values are: 1.0, 1.1. Default: 1.1.
Available only when GELF is activated.
--log-level <category:level> --log-level <category:level>
The log level of the root category or a comma-separated list of individual The log level of the root category or a comma-separated list of individual
categories and their levels. For the root category, you don't need to categories and their levels. For the root category, you don't need to

View file

@ -74,47 +74,16 @@ Logging:
--log <handler> Enable one or more log handlers in a comma-separated list. Possible values --log <handler> Enable one or more log handlers in a comma-separated list. Possible values
are: console, file, gelf (deprecated). Default: console. are: console, file, gelf (deprecated). Default: console.
--log-console-color <true|false> --log-console-color <true|false>
Enable or disable colors when logging to console. Default: false. Enable or disable colors when logging to console. Default: false. Available
only when Console log handler is activated.
--log-console-format <format> --log-console-format <format>
The format of unstructured console log entries. If the format has spaces in The format of unstructured console log entries. If the format has spaces in
it, escape the value using "<format>". Default: %d{yyyy-MM-dd HH:mm:ss,SSS} % it, escape the value using "<format>". Default: %d{yyyy-MM-dd HH:mm:ss,SSS} %
-5p [%c] (%t) %s%e%n. -5p [%c] (%t) %s%e%n. Available only when Console log handler is activated.
--log-console-output <output> --log-console-output <output>
Set the log output to JSON or default (plain) unstructured logging. Possible Set the log output to JSON or default (plain) unstructured logging. Possible
values are: default, json. Default: default. values are: default, json. Default: default. Available only when Console log
--log-file <file> Set the log file path and filename. Default: data/log/keycloak.log. handler is activated.
--log-file-format <format>
Set a format specific to file log entries. Default: %d{yyyy-MM-dd HH:mm:ss,
SSS} %-5p [%c] (%t) %s%e%n.
--log-file-output <output>
Set the log output to JSON or default (plain) unstructured logging. Possible
values are: default, json. Default: default.
--log-gelf-facility <name>
DEPRECATED. The facility (name of the process) that sends the message.
Default: keycloak.
--log-gelf-host <hostname>
DEPRECATED. Hostname of the Logstash or Graylog Host. By default UDP is used,
prefix the host with 'tcp:' to switch to TCP. Example: 'tcp:localhost'
Default: localhost.
--log-gelf-include-location <true|false>
DEPRECATED. Include source code location. Default: true.
--log-gelf-include-message-parameters <true|false>
DEPRECATED. Include message parameters from the log event. Default: true.
--log-gelf-include-stack-trace <true|false>
DEPRECATED. If set to true, occuring stack traces are included in the
'StackTrace' field in the GELF output. Default: true.
--log-gelf-level <level>
DEPRECATED. The log level specifying which message levels will be logged by
the GELF logger. Message levels lower than this value will be discarded.
Default: INFO.
--log-gelf-max-message-size <size>
DEPRECATED. Maximum message size (in bytes). If the message size is exceeded,
GELF will submit the message in multiple chunks. Default: 8192.
--log-gelf-port <port>
DEPRECATED. The port the Logstash or Graylog Host is called on. Default: 12201.
--log-gelf-timestamp-format <pattern>
DEPRECATED. Set the format for the GELF timestamp field. Uses Java
SimpleDateFormat pattern. Default: yyyy-MM-dd HH:mm:ss,SSS.
--log-level <category:level> --log-level <category:level>
The log level of the root category or a comma-separated list of individual The log level of the root category or a comma-separated list of individual
categories and their levels. For the root category, you don't need to categories and their levels. For the root category, you don't need to

View file

@ -74,47 +74,61 @@ Logging:
--log <handler> Enable one or more log handlers in a comma-separated list. Possible values --log <handler> Enable one or more log handlers in a comma-separated list. Possible values
are: console, file, gelf (deprecated). Default: console. are: console, file, gelf (deprecated). Default: console.
--log-console-color <true|false> --log-console-color <true|false>
Enable or disable colors when logging to console. Default: false. Enable or disable colors when logging to console. Default: false. Available
only when Console log handler is activated.
--log-console-format <format> --log-console-format <format>
The format of unstructured console log entries. If the format has spaces in The format of unstructured console log entries. If the format has spaces in
it, escape the value using "<format>". Default: %d{yyyy-MM-dd HH:mm:ss,SSS} % it, escape the value using "<format>". Default: %d{yyyy-MM-dd HH:mm:ss,SSS} %
-5p [%c] (%t) %s%e%n. -5p [%c] (%t) %s%e%n. Available only when Console log handler is activated.
--log-console-output <output> --log-console-output <output>
Set the log output to JSON or default (plain) unstructured logging. Possible Set the log output to JSON or default (plain) unstructured logging. Possible
values are: default, json. Default: default. values are: default, json. Default: default. Available only when Console log
--log-file <file> Set the log file path and filename. Default: data/log/keycloak.log. handler is activated.
--log-file <file> Set the log file path and filename. Default: data/log/keycloak.log. Available
only when File log handler is activated.
--log-file-format <format> --log-file-format <format>
Set a format specific to file log entries. Default: %d{yyyy-MM-dd HH:mm:ss, Set a format specific to file log entries. Default: %d{yyyy-MM-dd HH:mm:ss,
SSS} %-5p [%c] (%t) %s%e%n. SSS} %-5p [%c] (%t) %s%e%n. Available only when File log handler is
activated.
--log-file-output <output> --log-file-output <output>
Set the log output to JSON or default (plain) unstructured logging. Possible Set the log output to JSON or default (plain) unstructured logging. Possible
values are: default, json. Default: default. values are: default, json. Default: default. Available only when File log
handler is activated.
--log-gelf-facility <name> --log-gelf-facility <name>
DEPRECATED. The facility (name of the process) that sends the message. DEPRECATED. The facility (name of the process) that sends the message.
Default: keycloak. Default: keycloak. Available only when GELF is activated.
--log-gelf-host <hostname> --log-gelf-host <hostname>
DEPRECATED. Hostname of the Logstash or Graylog Host. By default UDP is used, DEPRECATED. Hostname of the Logstash or Graylog Host. By default UDP is used,
prefix the host with 'tcp:' to switch to TCP. Example: 'tcp:localhost' prefix the host with 'tcp:' to switch to TCP. Example: 'tcp:localhost'
Default: localhost. Default: localhost. Available only when GELF is activated.
--log-gelf-include-location <true|false> --log-gelf-include-location <true|false>
DEPRECATED. Include source code location. Default: true. DEPRECATED. Include source code location. Default: true. Available only when
GELF is activated.
--log-gelf-include-message-parameters <true|false> --log-gelf-include-message-parameters <true|false>
DEPRECATED. Include message parameters from the log event. Default: true. DEPRECATED. Include message parameters from the log event. Default: true.
Available only when GELF is activated.
--log-gelf-include-stack-trace <true|false> --log-gelf-include-stack-trace <true|false>
DEPRECATED. If set to true, occuring stack traces are included in the DEPRECATED. If set to true, occuring stack traces are included in the
'StackTrace' field in the GELF output. Default: true. 'StackTrace' field in the GELF output. Default: true. Available only when
GELF is activated.
--log-gelf-level <level> --log-gelf-level <level>
DEPRECATED. The log level specifying which message levels will be logged by DEPRECATED. The log level specifying which message levels will be logged by
the GELF logger. Message levels lower than this value will be discarded. the GELF logger. Message levels lower than this value will be discarded.
Default: INFO. Default: INFO. Available only when GELF is activated.
--log-gelf-max-message-size <size> --log-gelf-max-message-size <size>
DEPRECATED. Maximum message size (in bytes). If the message size is exceeded, DEPRECATED. Maximum message size (in bytes). If the message size is exceeded,
GELF will submit the message in multiple chunks. Default: 8192. GELF will submit the message in multiple chunks. Default: 8192. Available
only when GELF is activated.
--log-gelf-port <port> --log-gelf-port <port>
DEPRECATED. The port the Logstash or Graylog Host is called on. Default: 12201. DEPRECATED. The port the Logstash or Graylog Host is called on. Default:
12201. Available only when GELF is activated.
--log-gelf-timestamp-format <pattern> --log-gelf-timestamp-format <pattern>
DEPRECATED. Set the format for the GELF timestamp field. Uses Java DEPRECATED. Set the format for the GELF timestamp field. Uses Java
SimpleDateFormat pattern. Default: yyyy-MM-dd HH:mm:ss,SSS. SimpleDateFormat pattern. Default: yyyy-MM-dd HH:mm:ss,SSS. Available only
when GELF is activated.
--log-gelf-version <version>
The GELF version to be used. Possible values are: 1.0, 1.1. Default: 1.1.
Available only when GELF is activated.
--log-level <category:level> --log-level <category:level>
The log level of the root category or a comma-separated list of individual The log level of the root category or a comma-separated list of individual
categories and their levels. For the root category, you don't need to categories and their levels. For the root category, you don't need to

View file

@ -238,47 +238,16 @@ Logging:
--log <handler> Enable one or more log handlers in a comma-separated list. Possible values --log <handler> Enable one or more log handlers in a comma-separated list. Possible values
are: console, file, gelf (deprecated). Default: console. are: console, file, gelf (deprecated). Default: console.
--log-console-color <true|false> --log-console-color <true|false>
Enable or disable colors when logging to console. Default: false. Enable or disable colors when logging to console. Default: false. Available
only when Console log handler is activated.
--log-console-format <format> --log-console-format <format>
The format of unstructured console log entries. If the format has spaces in The format of unstructured console log entries. If the format has spaces in
it, escape the value using "<format>". Default: %d{yyyy-MM-dd HH:mm:ss,SSS} % it, escape the value using "<format>". Default: %d{yyyy-MM-dd HH:mm:ss,SSS} %
-5p [%c] (%t) %s%e%n. -5p [%c] (%t) %s%e%n. Available only when Console log handler is activated.
--log-console-output <output> --log-console-output <output>
Set the log output to JSON or default (plain) unstructured logging. Possible Set the log output to JSON or default (plain) unstructured logging. Possible
values are: default, json. Default: default. values are: default, json. Default: default. Available only when Console log
--log-file <file> Set the log file path and filename. Default: data/log/keycloak.log. handler is activated.
--log-file-format <format>
Set a format specific to file log entries. Default: %d{yyyy-MM-dd HH:mm:ss,
SSS} %-5p [%c] (%t) %s%e%n.
--log-file-output <output>
Set the log output to JSON or default (plain) unstructured logging. Possible
values are: default, json. Default: default.
--log-gelf-facility <name>
DEPRECATED. The facility (name of the process) that sends the message.
Default: keycloak.
--log-gelf-host <hostname>
DEPRECATED. Hostname of the Logstash or Graylog Host. By default UDP is used,
prefix the host with 'tcp:' to switch to TCP. Example: 'tcp:localhost'
Default: localhost.
--log-gelf-include-location <true|false>
DEPRECATED. Include source code location. Default: true.
--log-gelf-include-message-parameters <true|false>
DEPRECATED. Include message parameters from the log event. Default: true.
--log-gelf-include-stack-trace <true|false>
DEPRECATED. If set to true, occuring stack traces are included in the
'StackTrace' field in the GELF output. Default: true.
--log-gelf-level <level>
DEPRECATED. The log level specifying which message levels will be logged by
the GELF logger. Message levels lower than this value will be discarded.
Default: INFO.
--log-gelf-max-message-size <size>
DEPRECATED. Maximum message size (in bytes). If the message size is exceeded,
GELF will submit the message in multiple chunks. Default: 8192.
--log-gelf-port <port>
DEPRECATED. The port the Logstash or Graylog Host is called on. Default: 12201.
--log-gelf-timestamp-format <pattern>
DEPRECATED. Set the format for the GELF timestamp field. Uses Java
SimpleDateFormat pattern. Default: yyyy-MM-dd HH:mm:ss,SSS.
--log-level <category:level> --log-level <category:level>
The log level of the root category or a comma-separated list of individual The log level of the root category or a comma-separated list of individual
categories and their levels. For the root category, you don't need to categories and their levels. For the root category, you don't need to
@ -305,4 +274,4 @@ Security:
Do NOT start the server using this command when deploying to production. Do NOT start the server using this command when deploying to production.
Use 'kc.sh start-dev --help-all' to list all available options, including build Use 'kc.sh start-dev --help-all' to list all available options, including build
options. options.

View file

@ -238,47 +238,61 @@ Logging:
--log <handler> Enable one or more log handlers in a comma-separated list. Possible values --log <handler> Enable one or more log handlers in a comma-separated list. Possible values
are: console, file, gelf (deprecated). Default: console. are: console, file, gelf (deprecated). Default: console.
--log-console-color <true|false> --log-console-color <true|false>
Enable or disable colors when logging to console. Default: false. Enable or disable colors when logging to console. Default: false. Available
only when Console log handler is activated.
--log-console-format <format> --log-console-format <format>
The format of unstructured console log entries. If the format has spaces in The format of unstructured console log entries. If the format has spaces in
it, escape the value using "<format>". Default: %d{yyyy-MM-dd HH:mm:ss,SSS} % it, escape the value using "<format>". Default: %d{yyyy-MM-dd HH:mm:ss,SSS} %
-5p [%c] (%t) %s%e%n. -5p [%c] (%t) %s%e%n. Available only when Console log handler is activated.
--log-console-output <output> --log-console-output <output>
Set the log output to JSON or default (plain) unstructured logging. Possible Set the log output to JSON or default (plain) unstructured logging. Possible
values are: default, json. Default: default. values are: default, json. Default: default. Available only when Console log
--log-file <file> Set the log file path and filename. Default: data/log/keycloak.log. handler is activated.
--log-file <file> Set the log file path and filename. Default: data/log/keycloak.log. Available
only when File log handler is activated.
--log-file-format <format> --log-file-format <format>
Set a format specific to file log entries. Default: %d{yyyy-MM-dd HH:mm:ss, Set a format specific to file log entries. Default: %d{yyyy-MM-dd HH:mm:ss,
SSS} %-5p [%c] (%t) %s%e%n. SSS} %-5p [%c] (%t) %s%e%n. Available only when File log handler is
activated.
--log-file-output <output> --log-file-output <output>
Set the log output to JSON or default (plain) unstructured logging. Possible Set the log output to JSON or default (plain) unstructured logging. Possible
values are: default, json. Default: default. values are: default, json. Default: default. Available only when File log
handler is activated.
--log-gelf-facility <name> --log-gelf-facility <name>
DEPRECATED. The facility (name of the process) that sends the message. DEPRECATED. The facility (name of the process) that sends the message.
Default: keycloak. Default: keycloak. Available only when GELF is activated.
--log-gelf-host <hostname> --log-gelf-host <hostname>
DEPRECATED. Hostname of the Logstash or Graylog Host. By default UDP is used, DEPRECATED. Hostname of the Logstash or Graylog Host. By default UDP is used,
prefix the host with 'tcp:' to switch to TCP. Example: 'tcp:localhost' prefix the host with 'tcp:' to switch to TCP. Example: 'tcp:localhost'
Default: localhost. Default: localhost. Available only when GELF is activated.
--log-gelf-include-location <true|false> --log-gelf-include-location <true|false>
DEPRECATED. Include source code location. Default: true. DEPRECATED. Include source code location. Default: true. Available only when
GELF is activated.
--log-gelf-include-message-parameters <true|false> --log-gelf-include-message-parameters <true|false>
DEPRECATED. Include message parameters from the log event. Default: true. DEPRECATED. Include message parameters from the log event. Default: true.
Available only when GELF is activated.
--log-gelf-include-stack-trace <true|false> --log-gelf-include-stack-trace <true|false>
DEPRECATED. If set to true, occuring stack traces are included in the DEPRECATED. If set to true, occuring stack traces are included in the
'StackTrace' field in the GELF output. Default: true. 'StackTrace' field in the GELF output. Default: true. Available only when
GELF is activated.
--log-gelf-level <level> --log-gelf-level <level>
DEPRECATED. The log level specifying which message levels will be logged by DEPRECATED. The log level specifying which message levels will be logged by
the GELF logger. Message levels lower than this value will be discarded. the GELF logger. Message levels lower than this value will be discarded.
Default: INFO. Default: INFO. Available only when GELF is activated.
--log-gelf-max-message-size <size> --log-gelf-max-message-size <size>
DEPRECATED. Maximum message size (in bytes). If the message size is exceeded, DEPRECATED. Maximum message size (in bytes). If the message size is exceeded,
GELF will submit the message in multiple chunks. Default: 8192. GELF will submit the message in multiple chunks. Default: 8192. Available
only when GELF is activated.
--log-gelf-port <port> --log-gelf-port <port>
DEPRECATED. The port the Logstash or Graylog Host is called on. Default: 12201. DEPRECATED. The port the Logstash or Graylog Host is called on. Default:
12201. Available only when GELF is activated.
--log-gelf-timestamp-format <pattern> --log-gelf-timestamp-format <pattern>
DEPRECATED. Set the format for the GELF timestamp field. Uses Java DEPRECATED. Set the format for the GELF timestamp field. Uses Java
SimpleDateFormat pattern. Default: yyyy-MM-dd HH:mm:ss,SSS. SimpleDateFormat pattern. Default: yyyy-MM-dd HH:mm:ss,SSS. Available only
when GELF is activated.
--log-gelf-version <version>
The GELF version to be used. Possible values are: 1.0, 1.1. Default: 1.1.
Available only when GELF is activated.
--log-level <category:level> --log-level <category:level>
The log level of the root category or a comma-separated list of individual The log level of the root category or a comma-separated list of individual
categories and their levels. For the root category, you don't need to categories and their levels. For the root category, you don't need to
@ -305,4 +319,4 @@ Security:
Do NOT start the server using this command when deploying to production. Do NOT start the server using this command when deploying to production.
Use 'kc.sh start-dev --help-all' to list all available options, including build Use 'kc.sh start-dev --help-all' to list all available options, including build
options. options.

View file

@ -239,47 +239,16 @@ Logging:
--log <handler> Enable one or more log handlers in a comma-separated list. Possible values --log <handler> Enable one or more log handlers in a comma-separated list. Possible values
are: console, file, gelf (deprecated). Default: console. are: console, file, gelf (deprecated). Default: console.
--log-console-color <true|false> --log-console-color <true|false>
Enable or disable colors when logging to console. Default: false. Enable or disable colors when logging to console. Default: false. Available
only when Console log handler is activated.
--log-console-format <format> --log-console-format <format>
The format of unstructured console log entries. If the format has spaces in The format of unstructured console log entries. If the format has spaces in
it, escape the value using "<format>". Default: %d{yyyy-MM-dd HH:mm:ss,SSS} % it, escape the value using "<format>". Default: %d{yyyy-MM-dd HH:mm:ss,SSS} %
-5p [%c] (%t) %s%e%n. -5p [%c] (%t) %s%e%n. Available only when Console log handler is activated.
--log-console-output <output> --log-console-output <output>
Set the log output to JSON or default (plain) unstructured logging. Possible Set the log output to JSON or default (plain) unstructured logging. Possible
values are: default, json. Default: default. values are: default, json. Default: default. Available only when Console log
--log-file <file> Set the log file path and filename. Default: data/log/keycloak.log. handler is activated.
--log-file-format <format>
Set a format specific to file log entries. Default: %d{yyyy-MM-dd HH:mm:ss,
SSS} %-5p [%c] (%t) %s%e%n.
--log-file-output <output>
Set the log output to JSON or default (plain) unstructured logging. Possible
values are: default, json. Default: default.
--log-gelf-facility <name>
DEPRECATED. The facility (name of the process) that sends the message.
Default: keycloak.
--log-gelf-host <hostname>
DEPRECATED. Hostname of the Logstash or Graylog Host. By default UDP is used,
prefix the host with 'tcp:' to switch to TCP. Example: 'tcp:localhost'
Default: localhost.
--log-gelf-include-location <true|false>
DEPRECATED. Include source code location. Default: true.
--log-gelf-include-message-parameters <true|false>
DEPRECATED. Include message parameters from the log event. Default: true.
--log-gelf-include-stack-trace <true|false>
DEPRECATED. If set to true, occuring stack traces are included in the
'StackTrace' field in the GELF output. Default: true.
--log-gelf-level <level>
DEPRECATED. The log level specifying which message levels will be logged by
the GELF logger. Message levels lower than this value will be discarded.
Default: INFO.
--log-gelf-max-message-size <size>
DEPRECATED. Maximum message size (in bytes). If the message size is exceeded,
GELF will submit the message in multiple chunks. Default: 8192.
--log-gelf-port <port>
DEPRECATED. The port the Logstash or Graylog Host is called on. Default: 12201.
--log-gelf-timestamp-format <pattern>
DEPRECATED. Set the format for the GELF timestamp field. Uses Java
SimpleDateFormat pattern. Default: yyyy-MM-dd HH:mm:ss,SSS.
--log-level <category:level> --log-level <category:level>
The log level of the root category or a comma-separated list of individual The log level of the root category or a comma-separated list of individual
categories and their levels. For the root category, you don't need to categories and their levels. For the root category, you don't need to
@ -310,4 +279,4 @@ By default, this command tries to update the server configuration by running a
$ kc.sh start '--optimized' $ kc.sh start '--optimized'
By doing that, the server should start faster based on any previous By doing that, the server should start faster based on any previous
configuration you have set when manually running the 'build' command. configuration you have set when manually running the 'build' command.

View file

@ -239,47 +239,61 @@ Logging:
--log <handler> Enable one or more log handlers in a comma-separated list. Possible values --log <handler> Enable one or more log handlers in a comma-separated list. Possible values
are: console, file, gelf (deprecated). Default: console. are: console, file, gelf (deprecated). Default: console.
--log-console-color <true|false> --log-console-color <true|false>
Enable or disable colors when logging to console. Default: false. Enable or disable colors when logging to console. Default: false. Available
only when Console log handler is activated.
--log-console-format <format> --log-console-format <format>
The format of unstructured console log entries. If the format has spaces in The format of unstructured console log entries. If the format has spaces in
it, escape the value using "<format>". Default: %d{yyyy-MM-dd HH:mm:ss,SSS} % it, escape the value using "<format>". Default: %d{yyyy-MM-dd HH:mm:ss,SSS} %
-5p [%c] (%t) %s%e%n. -5p [%c] (%t) %s%e%n. Available only when Console log handler is activated.
--log-console-output <output> --log-console-output <output>
Set the log output to JSON or default (plain) unstructured logging. Possible Set the log output to JSON or default (plain) unstructured logging. Possible
values are: default, json. Default: default. values are: default, json. Default: default. Available only when Console log
--log-file <file> Set the log file path and filename. Default: data/log/keycloak.log. handler is activated.
--log-file <file> Set the log file path and filename. Default: data/log/keycloak.log. Available
only when File log handler is activated.
--log-file-format <format> --log-file-format <format>
Set a format specific to file log entries. Default: %d{yyyy-MM-dd HH:mm:ss, Set a format specific to file log entries. Default: %d{yyyy-MM-dd HH:mm:ss,
SSS} %-5p [%c] (%t) %s%e%n. SSS} %-5p [%c] (%t) %s%e%n. Available only when File log handler is
activated.
--log-file-output <output> --log-file-output <output>
Set the log output to JSON or default (plain) unstructured logging. Possible Set the log output to JSON or default (plain) unstructured logging. Possible
values are: default, json. Default: default. values are: default, json. Default: default. Available only when File log
handler is activated.
--log-gelf-facility <name> --log-gelf-facility <name>
DEPRECATED. The facility (name of the process) that sends the message. DEPRECATED. The facility (name of the process) that sends the message.
Default: keycloak. Default: keycloak. Available only when GELF is activated.
--log-gelf-host <hostname> --log-gelf-host <hostname>
DEPRECATED. Hostname of the Logstash or Graylog Host. By default UDP is used, DEPRECATED. Hostname of the Logstash or Graylog Host. By default UDP is used,
prefix the host with 'tcp:' to switch to TCP. Example: 'tcp:localhost' prefix the host with 'tcp:' to switch to TCP. Example: 'tcp:localhost'
Default: localhost. Default: localhost. Available only when GELF is activated.
--log-gelf-include-location <true|false> --log-gelf-include-location <true|false>
DEPRECATED. Include source code location. Default: true. DEPRECATED. Include source code location. Default: true. Available only when
GELF is activated.
--log-gelf-include-message-parameters <true|false> --log-gelf-include-message-parameters <true|false>
DEPRECATED. Include message parameters from the log event. Default: true. DEPRECATED. Include message parameters from the log event. Default: true.
Available only when GELF is activated.
--log-gelf-include-stack-trace <true|false> --log-gelf-include-stack-trace <true|false>
DEPRECATED. If set to true, occuring stack traces are included in the DEPRECATED. If set to true, occuring stack traces are included in the
'StackTrace' field in the GELF output. Default: true. 'StackTrace' field in the GELF output. Default: true. Available only when
GELF is activated.
--log-gelf-level <level> --log-gelf-level <level>
DEPRECATED. The log level specifying which message levels will be logged by DEPRECATED. The log level specifying which message levels will be logged by
the GELF logger. Message levels lower than this value will be discarded. the GELF logger. Message levels lower than this value will be discarded.
Default: INFO. Default: INFO. Available only when GELF is activated.
--log-gelf-max-message-size <size> --log-gelf-max-message-size <size>
DEPRECATED. Maximum message size (in bytes). If the message size is exceeded, DEPRECATED. Maximum message size (in bytes). If the message size is exceeded,
GELF will submit the message in multiple chunks. Default: 8192. GELF will submit the message in multiple chunks. Default: 8192. Available
only when GELF is activated.
--log-gelf-port <port> --log-gelf-port <port>
DEPRECATED. The port the Logstash or Graylog Host is called on. Default: 12201. DEPRECATED. The port the Logstash or Graylog Host is called on. Default:
12201. Available only when GELF is activated.
--log-gelf-timestamp-format <pattern> --log-gelf-timestamp-format <pattern>
DEPRECATED. Set the format for the GELF timestamp field. Uses Java DEPRECATED. Set the format for the GELF timestamp field. Uses Java
SimpleDateFormat pattern. Default: yyyy-MM-dd HH:mm:ss,SSS. SimpleDateFormat pattern. Default: yyyy-MM-dd HH:mm:ss,SSS. Available only
when GELF is activated.
--log-gelf-version <version>
The GELF version to be used. Possible values are: 1.0, 1.1. Default: 1.1.
Available only when GELF is activated.
--log-level <category:level> --log-level <category:level>
The log level of the root category or a comma-separated list of individual The log level of the root category or a comma-separated list of individual
categories and their levels. For the root category, you don't need to categories and their levels. For the root category, you don't need to
@ -310,4 +324,4 @@ By default, this command tries to update the server configuration by running a
$ kc.sh start '--optimized' $ kc.sh start '--optimized'
By doing that, the server should start faster based on any previous By doing that, the server should start faster based on any previous
configuration you have set when manually running the 'build' command. configuration you have set when manually running the 'build' command.

View file

@ -177,47 +177,16 @@ Logging:
--log <handler> Enable one or more log handlers in a comma-separated list. Possible values --log <handler> Enable one or more log handlers in a comma-separated list. Possible values
are: console, file, gelf (deprecated). Default: console. are: console, file, gelf (deprecated). Default: console.
--log-console-color <true|false> --log-console-color <true|false>
Enable or disable colors when logging to console. Default: false. Enable or disable colors when logging to console. Default: false. Available
only when Console log handler is activated.
--log-console-format <format> --log-console-format <format>
The format of unstructured console log entries. If the format has spaces in The format of unstructured console log entries. If the format has spaces in
it, escape the value using "<format>". Default: %d{yyyy-MM-dd HH:mm:ss,SSS} % it, escape the value using "<format>". Default: %d{yyyy-MM-dd HH:mm:ss,SSS} %
-5p [%c] (%t) %s%e%n. -5p [%c] (%t) %s%e%n. Available only when Console log handler is activated.
--log-console-output <output> --log-console-output <output>
Set the log output to JSON or default (plain) unstructured logging. Possible Set the log output to JSON or default (plain) unstructured logging. Possible
values are: default, json. Default: default. values are: default, json. Default: default. Available only when Console log
--log-file <file> Set the log file path and filename. Default: data/log/keycloak.log. handler is activated.
--log-file-format <format>
Set a format specific to file log entries. Default: %d{yyyy-MM-dd HH:mm:ss,
SSS} %-5p [%c] (%t) %s%e%n.
--log-file-output <output>
Set the log output to JSON or default (plain) unstructured logging. Possible
values are: default, json. Default: default.
--log-gelf-facility <name>
DEPRECATED. The facility (name of the process) that sends the message.
Default: keycloak.
--log-gelf-host <hostname>
DEPRECATED. Hostname of the Logstash or Graylog Host. By default UDP is used,
prefix the host with 'tcp:' to switch to TCP. Example: 'tcp:localhost'
Default: localhost.
--log-gelf-include-location <true|false>
DEPRECATED. Include source code location. Default: true.
--log-gelf-include-message-parameters <true|false>
DEPRECATED. Include message parameters from the log event. Default: true.
--log-gelf-include-stack-trace <true|false>
DEPRECATED. If set to true, occuring stack traces are included in the
'StackTrace' field in the GELF output. Default: true.
--log-gelf-level <level>
DEPRECATED. The log level specifying which message levels will be logged by
the GELF logger. Message levels lower than this value will be discarded.
Default: INFO.
--log-gelf-max-message-size <size>
DEPRECATED. Maximum message size (in bytes). If the message size is exceeded,
GELF will submit the message in multiple chunks. Default: 8192.
--log-gelf-port <port>
DEPRECATED. The port the Logstash or Graylog Host is called on. Default: 12201.
--log-gelf-timestamp-format <pattern>
DEPRECATED. Set the format for the GELF timestamp field. Uses Java
SimpleDateFormat pattern. Default: yyyy-MM-dd HH:mm:ss,SSS.
--log-level <category:level> --log-level <category:level>
The log level of the root category or a comma-separated list of individual The log level of the root category or a comma-separated list of individual
categories and their levels. For the root category, you don't need to categories and their levels. For the root category, you don't need to
@ -239,4 +208,4 @@ By default, this command tries to update the server configuration by running a
$ kc.sh start '--optimized' $ kc.sh start '--optimized'
By doing that, the server should start faster based on any previous By doing that, the server should start faster based on any previous
configuration you have set when manually running the 'build' command. configuration you have set when manually running the 'build' command.

View file

@ -177,47 +177,61 @@ Logging:
--log <handler> Enable one or more log handlers in a comma-separated list. Possible values --log <handler> Enable one or more log handlers in a comma-separated list. Possible values
are: console, file, gelf (deprecated). Default: console. are: console, file, gelf (deprecated). Default: console.
--log-console-color <true|false> --log-console-color <true|false>
Enable or disable colors when logging to console. Default: false. Enable or disable colors when logging to console. Default: false. Available
only when Console log handler is activated.
--log-console-format <format> --log-console-format <format>
The format of unstructured console log entries. If the format has spaces in The format of unstructured console log entries. If the format has spaces in
it, escape the value using "<format>". Default: %d{yyyy-MM-dd HH:mm:ss,SSS} % it, escape the value using "<format>". Default: %d{yyyy-MM-dd HH:mm:ss,SSS} %
-5p [%c] (%t) %s%e%n. -5p [%c] (%t) %s%e%n. Available only when Console log handler is activated.
--log-console-output <output> --log-console-output <output>
Set the log output to JSON or default (plain) unstructured logging. Possible Set the log output to JSON or default (plain) unstructured logging. Possible
values are: default, json. Default: default. values are: default, json. Default: default. Available only when Console log
--log-file <file> Set the log file path and filename. Default: data/log/keycloak.log. handler is activated.
--log-file <file> Set the log file path and filename. Default: data/log/keycloak.log. Available
only when File log handler is activated.
--log-file-format <format> --log-file-format <format>
Set a format specific to file log entries. Default: %d{yyyy-MM-dd HH:mm:ss, Set a format specific to file log entries. Default: %d{yyyy-MM-dd HH:mm:ss,
SSS} %-5p [%c] (%t) %s%e%n. SSS} %-5p [%c] (%t) %s%e%n. Available only when File log handler is
activated.
--log-file-output <output> --log-file-output <output>
Set the log output to JSON or default (plain) unstructured logging. Possible Set the log output to JSON or default (plain) unstructured logging. Possible
values are: default, json. Default: default. values are: default, json. Default: default. Available only when File log
handler is activated.
--log-gelf-facility <name> --log-gelf-facility <name>
DEPRECATED. The facility (name of the process) that sends the message. DEPRECATED. The facility (name of the process) that sends the message.
Default: keycloak. Default: keycloak. Available only when GELF is activated.
--log-gelf-host <hostname> --log-gelf-host <hostname>
DEPRECATED. Hostname of the Logstash or Graylog Host. By default UDP is used, DEPRECATED. Hostname of the Logstash or Graylog Host. By default UDP is used,
prefix the host with 'tcp:' to switch to TCP. Example: 'tcp:localhost' prefix the host with 'tcp:' to switch to TCP. Example: 'tcp:localhost'
Default: localhost. Default: localhost. Available only when GELF is activated.
--log-gelf-include-location <true|false> --log-gelf-include-location <true|false>
DEPRECATED. Include source code location. Default: true. DEPRECATED. Include source code location. Default: true. Available only when
GELF is activated.
--log-gelf-include-message-parameters <true|false> --log-gelf-include-message-parameters <true|false>
DEPRECATED. Include message parameters from the log event. Default: true. DEPRECATED. Include message parameters from the log event. Default: true.
Available only when GELF is activated.
--log-gelf-include-stack-trace <true|false> --log-gelf-include-stack-trace <true|false>
DEPRECATED. If set to true, occuring stack traces are included in the DEPRECATED. If set to true, occuring stack traces are included in the
'StackTrace' field in the GELF output. Default: true. 'StackTrace' field in the GELF output. Default: true. Available only when
GELF is activated.
--log-gelf-level <level> --log-gelf-level <level>
DEPRECATED. The log level specifying which message levels will be logged by DEPRECATED. The log level specifying which message levels will be logged by
the GELF logger. Message levels lower than this value will be discarded. the GELF logger. Message levels lower than this value will be discarded.
Default: INFO. Default: INFO. Available only when GELF is activated.
--log-gelf-max-message-size <size> --log-gelf-max-message-size <size>
DEPRECATED. Maximum message size (in bytes). If the message size is exceeded, DEPRECATED. Maximum message size (in bytes). If the message size is exceeded,
GELF will submit the message in multiple chunks. Default: 8192. GELF will submit the message in multiple chunks. Default: 8192. Available
only when GELF is activated.
--log-gelf-port <port> --log-gelf-port <port>
DEPRECATED. The port the Logstash or Graylog Host is called on. Default: 12201. DEPRECATED. The port the Logstash or Graylog Host is called on. Default:
12201. Available only when GELF is activated.
--log-gelf-timestamp-format <pattern> --log-gelf-timestamp-format <pattern>
DEPRECATED. Set the format for the GELF timestamp field. Uses Java DEPRECATED. Set the format for the GELF timestamp field. Uses Java
SimpleDateFormat pattern. Default: yyyy-MM-dd HH:mm:ss,SSS. SimpleDateFormat pattern. Default: yyyy-MM-dd HH:mm:ss,SSS. Available only
when GELF is activated.
--log-gelf-version <version>
The GELF version to be used. Possible values are: 1.0, 1.1. Default: 1.1.
Available only when GELF is activated.
--log-level <category:level> --log-level <category:level>
The log level of the root category or a comma-separated list of individual The log level of the root category or a comma-separated list of individual
categories and their levels. For the root category, you don't need to categories and their levels. For the root category, you don't need to
@ -239,4 +253,4 @@ By default, this command tries to update the server configuration by running a
$ kc.sh start '--optimized' $ kc.sh start '--optimized'
By doing that, the server should start faster based on any previous By doing that, the server should start faster based on any previous
configuration you have set when manually running the 'build' command. configuration you have set when manually running the 'build' command.

View file

@ -20,14 +20,23 @@ import java.util.Collection;
public class StringUtil { public class StringUtil {
/**
* Returns true if string is null or blank
*/
public static boolean isBlank(String str) { public static boolean isBlank(String str) {
return !(isNotBlank(str)); return !(isNotBlank(str));
} }
public static boolean isNotBlank(String str) { /**
return str != null && !"".equals(str.trim()); * Returns true if string is not null and not blank
*/
public static boolean isNotBlank(String str) {
return str != null && !str.isBlank();
} }
/**
* Returns true if string is null or empty
*/
public static boolean isNullOrEmpty(String str) { public static boolean isNullOrEmpty(String str) {
return str == null || str.isEmpty(); return str == null || str.isEmpty();
} }