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 Profile CURRENT;
private static volatile Profile CURRENT;
private final ProfileName profileName;

View file

@ -1,6 +1,6 @@
<#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-title: ${title}
:guide-summary: ${summary}
@ -28,6 +28,6 @@ endif::[]
<#if includedOptions?has_content>
== 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>
</#macro>

View file

@ -23,6 +23,10 @@
*Env:* `${option.keyEnv}`
--
<#if option.enabledWhen?has_content>
${option.enabledWhen!}
</#if>
<#if option.deprecated?has_content>
<#-- Either mark the whole option as deprecated, or just selected values -->
<#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.configuration.Configuration;
import org.keycloak.quarkus.runtime.configuration.mappers.PropertyMappers;
import org.keycloak.utils.StringUtil;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.EnumMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
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.StreamSupport;
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<>();
@SuppressWarnings("unchecked")
public Options() {
options = PropertyMappers.getMappers().stream()
this.options = new EnumMap<>(OptionCategory.class);
PropertyMappers.getMappers().stream()
.filter(m -> !m.isHidden())
.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)))
.sorted(Comparator.comparing(Option::getKey))
.collect(Collectors.toMap(Option::getKey, o -> o, (o1, o2) -> o1, LinkedHashMap::new)); // Need to ignore duplicate keys??
.map(m -> new Option(m.getFrom(),
m.getCategory(),
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());
options.forEach((s, option) -> {
option.description = option.description.replaceAll("'([^ ]*)'", "`$1`");
});
options.values()
.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 (ProviderFactory providerFactory : providerManager.load(loadSpi).stream().sorted(Comparator.comparing(ProviderFactory::getId)).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)).toList()) {
List<ProviderConfigProperty> configMetadata = providerFactory.getConfigMetadata();
if (configMetadata == null) {
@ -62,6 +76,7 @@ public class Options {
m.getHelpText(),
m.getDefaultValue() == null ? null : m.getDefaultValue().toString(),
m.getOptions() == null ? Collections.emptyList() : m.getOptions(),
"",
null))
.sorted(Comparator.comparing(Option::getKey)).collect(Collectors.toList());
@ -88,39 +103,84 @@ public class Options {
.collect(Collectors.toList());
}
public Collection<Option> getValues() {
return options.values();
}
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) {
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(' ', '|');
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() {
return providerOptions;
}
public class Option {
public static class Option {
private String key;
private OptionCategory category;
private boolean build;
private String type;
private final String key;
private final OptionCategory category;
private final boolean build;
private final String type;
private String description;
private String defaultValue;
private final String defaultValue;
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.category = category;
this.build = build;
@ -128,6 +188,7 @@ public class Options {
this.description = description;
this.defaultValue = defaultValue;
this.expectedValues = StreamSupport.stream(expectedValues.spliterator(), false).collect(Collectors.toList());
this.enabledWhen = enabledWhen;
this.deprecated = deprecatedMetadata;
}
@ -178,6 +239,11 @@ public class Options {
return expectedValues;
}
public String getEnabledWhen() {
if (StringUtil.isBlank(enabledWhen)) return null;
return enabledWhen;
}
public DeprecatedMetadata getDeprecated() {
return deprecated;
}

View file

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

View file

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

View file

@ -38,6 +38,7 @@ import org.apache.commons.lang3.SystemUtils;
import org.keycloak.common.Profile;
import org.keycloak.common.profile.PropertiesFileProfileConfigResolver;
import org.keycloak.common.profile.PropertiesProfileConfigResolver;
import org.keycloak.quarkus.runtime.cli.command.AbstractCommand;
import org.keycloak.quarkus.runtime.configuration.PersistedConfigSource;
public final class Environment {
@ -50,6 +51,9 @@ public final class Environment {
public static final String DEV_PROFILE_VALUE = "dev";
public static final String PROD_PROFILE_VALUE = "prod";
public static final String LAUNCH_MODE = "kc.launch.mode";
private static volatile AbstractCommand parsedCommand;
private Environment() {}
public static Boolean isRebuild() {
@ -255,4 +259,19 @@ public final class Environment {
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 jakarta.enterprise.context.ApplicationScoped;
import org.keycloak.common.profile.ProfileException;
import org.keycloak.quarkus.runtime.configuration.mappers.PropertyMappers;
import picocli.CommandLine.ExitCode;
import io.quarkus.runtime.ApplicationLifecycleManager;
@ -68,10 +70,7 @@ public class KeycloakMain implements QuarkusApplication {
try {
cliArgs = Picocli.parseArgs(args);
} catch (PropertyException e) {
ExecutionExceptionHandler errorHandler = new ExecutionExceptionHandler();
PrintWriter errStream = new PrintWriter(System.err, true);
errorHandler.error(errStream, e.getMessage(), null);
System.exit(ExitCode.USAGE);
handleUsageError(e.getMessage());
return;
}
@ -79,25 +78,24 @@ public class KeycloakMain implements QuarkusApplication {
cliArgs = new ArrayList<>(cliArgs);
// default to show help message
cliArgs.add("-h");
} else if (isFastStart(cliArgs)) {
// fast path for starting the server without bootstrapping CLI
ExecutionExceptionHandler errorHandler = new ExecutionExceptionHandler();
PrintWriter errStream = new PrintWriter(System.err, true);
} else if (isFastStart(cliArgs)) { // fast path for starting the server without bootstrapping CLI
if (isDevProfileNotAllowed()) {
errorHandler.error(errStream, Messages.devProfileNotAllowedError(Start.NAME), null);
System.exit(ExitCode.USAGE);
handleUsageError(Messages.devProfileNotAllowedError(Start.NAME));
return;
}
try {
PropertyMappers.sanitizeDisabledMappers();
Picocli.validateConfig(cliArgs, new Start());
} catch (PropertyException e) {
errorHandler.error(errStream, e.getMessage(), null);
System.exit(ExitCode.USAGE);
} catch (PropertyException | ProfileException e) {
handleUsageError(e.getMessage(), e.getCause());
return;
}
ExecutionExceptionHandler errorHandler = new ExecutionExceptionHandler();
PrintWriter errStream = new PrintWriter(System.err, true);
start(errorHandler, errStream, args);
return;
@ -107,6 +105,17 @@ public class KeycloakMain implements QuarkusApplication {
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) {
// '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);

View file

@ -161,8 +161,21 @@ public final class Help extends CommandLine.Help {
PropertyMapper<?> mapper = getMapper(option.longestName());
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
return !option.hidden();
return !option.hidden() && !isDisabledMapper;
}
boolean isUnsupportedOption = !PropertyMappers.isSupported(mapper);

View file

@ -17,8 +17,10 @@
package org.keycloak.quarkus.runtime.cli;
import static java.lang.String.format;
import static java.util.Optional.ofNullable;
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.isRebuilt;
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.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.HashSet;
@ -56,11 +57,13 @@ import java.util.stream.Collectors;
import org.eclipse.microprofile.config.spi.ConfigSource;
import org.jboss.logging.Logger;
import org.keycloak.common.profile.ProfileException;
import org.keycloak.config.DeprecatedMetadata;
import org.keycloak.config.Option;
import org.keycloak.config.OptionCategory;
import org.keycloak.quarkus.runtime.cli.command.AbstractCommand;
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.Main;
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.configuration.ConfigArgsConfigSource;
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.PropertyMappingInterceptor;
import org.keycloak.quarkus.runtime.configuration.QuarkusPropertiesConfigSource;
@ -94,6 +99,7 @@ public final class Picocli {
private static class IncludeOptions {
boolean includeRuntime;
boolean includeBuildTime;
boolean includeDisabled;
}
private Picocli() {
@ -101,31 +107,45 @@ public final class Picocli {
public static void parseAndRun(List<String> cliArgs) {
CommandLine cmd = createCommandLine(cliArgs);
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);
}
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) {
if (exitCode != cmd.getCommandSpec().exitCodeOnSuccess() && !Environment.isTestLaunchMode() || isRebuildCheck()) {
// 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)) {
PropertyMappers.sanitizeDisabledMappers();
exitCode = runReAugmentation(cliArgs, cmd);
}
@ -279,13 +300,27 @@ public final class Picocli {
return;
}
final boolean disabledMappersInterceptorEnabled = DisabledMappersInterceptor.isEnabled(); // return to the state before the disable
try {
PropertyMappingInterceptor.disable(); // we don't want the mapped / transformed properties, we want what the user effectively supplied
List<String> ignoredBuildTime = new ArrayList<>();
List<String> ignoredRunTime = new ArrayList<>();
Set<String> deprecatedInUse = new HashSet<>();
DisabledMappersInterceptor.disable(); // we want all properties, even disabled ones
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()) {
List<PropertyMapper<?>> mappers = new ArrayList<>();
List<PropertyMapper<?>> mappers = new ArrayList<>(disabledMappers);
Optional.ofNullable(PropertyMappers.getRuntimeMappers().get(category)).ifPresent(mappers::addAll);
Optional.ofNullable(PropertyMappers.getBuildTimeMappers().get(category)).ifPresent(mappers::addAll);
for (PropertyMapper<?> mapper : mappers) {
@ -297,6 +332,20 @@ public final class Picocli {
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) {
ignoredBuildTime.add(mapper.getFrom());
continue;
@ -322,10 +371,17 @@ public final class Picocli {
outputIgnoredProperties(ignoredRunTime, false, logger);
}
if (!disabledBuildTime.isEmpty()) {
outputDisabledProperties(disabledBuildTime, true, logger);
} else if (!disabledRunTime.isEmpty()) {
outputDisabledProperties(disabledRunTime, false, logger);
}
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.");
}
} finally {
DisabledMappersInterceptor.enable(disabledMappersInterceptorEnabled);
PropertyMappingInterceptor.enable();
}
}
@ -372,10 +428,36 @@ public final class Picocli {
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) {
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",
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) {
@ -536,6 +618,7 @@ public final class Picocli {
}
result.includeRuntime = abstractCommand.includeRuntime();
result.includeBuildTime = abstractCommand.includeBuildTime();
result.includeDisabled = cliArgs.contains(HelpAllMixin.HELP_ALL_OPTION);
if (!result.includeBuildTime && !result.includeRuntime) {
return result;
@ -548,14 +631,17 @@ public final class Picocli {
}
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());
// set current parsed command
Environment.setParsedCommand(ac);
if (!options.includeBuildTime && !options.includeRuntime) {
return;
}
addOptionsToCli(command, options.includeBuildTime, options.includeRuntime);
addOptionsToCli(command, options);
}
}
@ -571,30 +657,48 @@ public final class Picocli {
return null;
}
private static void addOptionsToCli(CommandLine commandLine, boolean includeBuildTime, boolean includeRuntime) {
Map<OptionCategory, List<PropertyMapper<?>>> mappers = new EnumMap<>(OptionCategory.class);
private static void addOptionsToCli(CommandLine commandLine, IncludeOptions includeOptions) {
final Map<OptionCategory, List<PropertyMapper<?>>> mappers = new EnumMap<>(OptionCategory.class);
if (includeRuntime) {
if (includeOptions.includeRuntime) {
mappers.putAll(PropertyMappers.getRuntimeMappers());
if (includeOptions.includeDisabled) {
appendDisabledMappers(mappers, PropertyMappers.getDisabledRuntimeMappers());
}
}
if (includeBuildTime) {
for (Map.Entry<OptionCategory, List<PropertyMapper<?>>> entry : PropertyMappers.getBuildTimeMappers()
.entrySet()) {
List<PropertyMapper<?>> result = new ArrayList<>(mappers.getOrDefault(entry.getKey(), Collections.emptyList()));
if (includeOptions.includeBuildTime) {
combinePropertyMappers(mappers, PropertyMappers.getBuildTimeMappers());
result.addAll(entry.getValue());
mappers.put(entry.getKey(), result);
if (includeOptions.includeDisabled) {
appendDisabledMappers(mappers, PropertyMappers.getDisabledBuildTimeMappers());
}
}
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) {
CommandSpec cSpec = commandLine.getCommandSpec();
for(OptionCategory category : ((AbstractCommand) commandLine.getCommand()).getOptionCategories()) {
for (OptionCategory category : ((AbstractCommand) commandLine.getCommand()).getOptionCategories()) {
List<PropertyMapper<?>> mappersInCategory = propertyMappers.get(category);
if (mappersInCategory == null) {
@ -607,11 +711,13 @@ public final class Picocli {
.order(category.getOrder())
.validate(false);
for (PropertyMapper<?> mapper: mappersInCategory) {
final Set<String> alreadyPresentArgs = new HashSet<>();
for (PropertyMapper<?> mapper : mappersInCategory) {
String name = mapper.getCliFormat();
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.
continue;
}
@ -638,6 +744,8 @@ public final class Picocli {
optBuilder.type(String.class);
}
alreadyPresentArgs.add(name);
argGroupBuilder.addArg(optBuilder.build());
}
@ -667,6 +775,8 @@ public final class Picocli {
.map(d -> " Default: " + d + ".")
.ifPresent(transformedDesc::append);
mapper.getEnabledWhen().map(e -> format(" %s.", e)).ifPresent(transformedDesc::append);
// only fully deprecated options, not just deprecated values
mapper.getDeprecatedMetadata()
.filter(deprecatedMetadata -> deprecatedMetadata.getDeprecatedValues().isEmpty())
@ -722,7 +832,7 @@ public final class Picocli {
if (!arg.contains(ARG_KEY_VALUE_SEPARATOR)) {
if (!iterator.hasNext()) {
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;
}

View file

@ -14,6 +14,10 @@ import picocli.CommandLine.Model.CommandSpec;
import picocli.CommandLine.ParameterException;
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;
public class ShortErrorMessageHandler implements IParameterExceptionHandler {
@ -23,17 +27,30 @@ public class ShortErrorMessageHandler implements IParameterExceptionHandler {
CommandLine cmd = ex.getCommandLine();
PrintWriter writer = cmd.getErr();
String errorMessage = ex.getMessage();
String additionalSuggestion = null;
if (ex instanceof UnmatchedArgumentException) {
UnmatchedArgumentException uae = (UnmatchedArgumentException) ex;
String[] unmatched = getUnmatchedPartsByOptionSeparator(uae,"=");
String[] unmatched = getUnmatchedPartsByOptionSeparator(uae, "=");
String cliKey = unmatched[0];
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) {
errorMessage = "Option: '" + cliKey + "' is not expected to contain whitespace, please remove any unnecessary quoting/escaping";
} else {
@ -42,12 +59,13 @@ public class ShortErrorMessageHandler implements IParameterExceptionHandler {
} else {
AbstractCommand command = cmd.getCommand();
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 {
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 {
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();
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);
}

View file

@ -69,4 +69,8 @@ public abstract class AbstractCommand {
}
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.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.configuration.ConfigArgsConfigSource.getAllCliArgs;
import org.keycloak.config.ClassLoaderOptions;
import org.keycloak.config.OptionCategory;
import org.keycloak.quarkus.runtime.Environment;
import org.keycloak.quarkus.runtime.Messages;
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.RunnerClassLoader;
import io.quarkus.runtime.configuration.ProfileManager;
import io.smallrye.config.ConfigValue;
import org.keycloak.utils.StringUtil;
import picocli.CommandLine;
import picocli.CommandLine.Command;
@ -84,7 +81,7 @@ public final class Build extends AbstractCommand implements Runnable {
beforeReaugmentationOnWindows();
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(), "\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 org.keycloak.config.OptionCategory;
import org.keycloak.quarkus.runtime.configuration.mappers.ExportPropertyMappers;
import picocli.CommandLine.Command;
import java.util.List;
@ -42,6 +43,12 @@ public final class Export extends AbstractExportImportCommand implements Runnabl
optionCategory != OptionCategory.IMPORT).collect(Collectors.toList());
}
@Override
public void validateConfig() {
ExportPropertyMappers.validateConfig();
super.validateConfig();
}
@Override
public String getName() {
return NAME;

View file

@ -23,10 +23,12 @@ import picocli.CommandLine;
public final class HelpAllMixin {
public static final String HELP_ALL_OPTION = "--help-all";
@CommandLine.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) {
Help help = (Help) spec.commandLine().getHelp();
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 org.keycloak.config.OptionCategory;
import org.keycloak.quarkus.runtime.configuration.mappers.ImportPropertyMappers;
import picocli.CommandLine.Command;
import java.util.List;
@ -42,6 +43,12 @@ public final class Import extends AbstractExportImportCommand implements Runnabl
optionCategory != OptionCategory.EXPORT).collect(Collectors.toList());
}
@Override
public void validateConfig() {
ImportPropertyMappers.validateConfig();
super.validateConfig();
}
@Override
public String getName() {
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.ConfigSource;
import org.keycloak.config.Option;
import org.keycloak.quarkus.runtime.Environment;
import org.keycloak.quarkus.runtime.configuration.mappers.PropertyMapper;
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() {
return (SmallRyeConfig) ConfigProviderResolver.instance().getConfig();
}
@ -222,13 +239,6 @@ public final class Configuration {
}
public static ConfigValue getCurrentBuiltTimeProperty(String name) {
PersistedConfigSource persistedConfigSource = PersistedConfigSource.getInstance();
try {
persistedConfigSource.enable(false);
return getConfigValue(name);
} finally {
persistedConfigSource.enable(true);
}
return PersistedConfigSource.getInstance().runWithDisabled(() -> getConfigValue(name));
}
}

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
* 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() {
super(readProperties(), "", 200);
@ -146,16 +146,24 @@ public final class PersistedConfigSource extends PropertiesConfigSource {
return null;
}
public void enable(boolean enabled) {
if (enabled) {
ENABLED.remove();
} else {
ENABLED.set(enabled);
}
public void enable() {
ENABLED.set(true);
}
public void disable() {
ENABLED.set(false);
}
private boolean isEnabled() {
Boolean result = ENABLED.get();
return result == null ? true : result;
return Boolean.TRUE.equals(ENABLED.get());
}
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.ConfigValue;
import io.smallrye.config.Priorities;
import jakarta.annotation.Priority;
import org.apache.commons.collections4.iterators.FilterIterator;
import org.keycloak.common.util.StringPropertyReplacer;
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.
*
* <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 {
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.ConfigValue;
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 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;
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() {
}
public static PropertyMapper<?>[] getMappers() {
return new PropertyMapper[] {
fromOption(ExportOptions.FILE)
.to("kc.spi-export-exporter")
return new PropertyMapper[]{
fromOption(EXPORTER_PLACEHOLDER)
.to(EXPORTER_PROPERTY)
.transformer(ExportPropertyMappers::transformExporter)
.paramLabel("file")
.build(),
@ -49,43 +57,69 @@ final class ExportPropertyMappers {
.build(),
fromOption(ExportOptions.REALM)
.to("kc.spi-export-single-file-realm-name")
.isEnabled(ExportPropertyMappers::isSingleFileProvider)
.paramLabel("realm")
.build(),
fromOption(ExportOptions.REALM)
.to("kc.spi-export-dir-realm-name")
.isEnabled(ExportPropertyMappers::isDirProvider)
.paramLabel("realm")
.build(),
fromOption(ExportOptions.USERS)
.to("kc.spi-export-dir-users-export-strategy")
.isEnabled(ExportPropertyMappers::isDirProvider)
.paramLabel("strategy")
.build(),
fromOption(ExportOptions.USERS_PER_FILE)
.to("kc.spi-export-dir-users-per-file")
.isEnabled(ExportPropertyMappers::isDirProvider)
.paramLabel("number")
.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) {
ConfigValue exporter = context.proceed("kc.spi-export-exporter");
ConfigValue exporter = context.proceed(EXPORTER_PROPERTY);
if (exporter != null) {
return Optional.of(exporter.getValue());
}
if (option.isPresent()) {
return Optional.of("singleFile");
}
ConfigValue dirConfigValue = context.proceed("kc.spi-export-dir-dir");
if (dirConfigValue != null && dirConfigValue.getValue() != null) {
return Optional.of("dir");
}
ConfigValue dirValue = context.proceed("kc.dir");
if (dirValue != null && dirValue.getValue() != null) {
return Optional.of("dir");
}
if (System.getProperty(PROVIDER) == null) {
throw new CommandLine.PicocliException("Must specify either --dir or --file options.");
}
return Optional.empty();
var file = Configuration.getOptionalValue("kc.spi-export-single-file-file").map(f -> SINGLE_FILE);
var dir = Configuration.getOptionalValue("kc.spi-export-dir-dir")
.or(() -> Configuration.getOptionalValue("kc.dir"))
.map(f -> DIR);
// Only one option can be specified
boolean xor = file.isPresent() ^ dir.isPresent();
return xor ? file.or(() -> dir) : Optional.empty();
}
}

View file

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

View file

@ -2,6 +2,7 @@ package org.keycloak.quarkus.runtime.configuration.mappers;
import static java.util.Optional.of;
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.integration.QuarkusPlatform.addInitializationException;
@ -18,10 +19,16 @@ import org.keycloak.config.LoggingOptions;
import org.keycloak.quarkus.runtime.Messages;
import io.smallrye.config.ConfigSourceInterceptorContext;
import org.keycloak.quarkus.runtime.configuration.Configuration;
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() {
PropertyMapper<?>[] defaultMappers = new PropertyMapper[]{
@ -29,15 +36,18 @@ public final class LoggingPropertyMappers {
.paramLabel("<handler>")
.build(),
fromOption(LoggingOptions.LOG_CONSOLE_OUTPUT)
.isEnabled(LoggingPropertyMappers::isConsoleEnabled, CONSOLE_ENABLED_MSG)
.to("quarkus.log.console.json")
.paramLabel("output")
.transformer(LoggingPropertyMappers::resolveLogOutput)
.build(),
fromOption(LoggingOptions.LOG_CONSOLE_FORMAT)
.isEnabled(LoggingPropertyMappers::isConsoleEnabled, CONSOLE_ENABLED_MSG)
.to("quarkus.log.console.format")
.paramLabel("format")
.build(),
fromOption(LoggingOptions.LOG_CONSOLE_COLOR)
.isEnabled(LoggingPropertyMappers::isConsoleEnabled, CONSOLE_ENABLED_MSG)
.to("quarkus.log.console.color")
.build(),
fromOption(LoggingOptions.LOG_CONSOLE_ENABLED)
@ -51,15 +61,18 @@ public final class LoggingPropertyMappers {
.transformer(LoggingPropertyMappers.resolveLogHandler("file"))
.build(),
fromOption(LoggingOptions.LOG_FILE)
.isEnabled(LoggingPropertyMappers::isFileEnabled, FILE_ENABLED_MSG)
.to("quarkus.log.file.path")
.paramLabel("file")
.transformer(LoggingPropertyMappers::resolveFileLogLocation)
.build(),
fromOption(LoggingOptions.LOG_FILE_FORMAT)
.isEnabled(LoggingPropertyMappers::isFileEnabled, FILE_ENABLED_MSG)
.to("quarkus.log.file.format")
.paramLabel("<format>")
.build(),
fromOption(LoggingOptions.LOG_FILE_OUTPUT)
.isEnabled(LoggingPropertyMappers::isFileEnabled, FILE_ENABLED_MSG)
.to("quarkus.log.file.json")
.paramLabel("output")
.transformer(LoggingPropertyMappers::resolveLogOutput)
@ -82,52 +95,74 @@ public final class LoggingPropertyMappers {
.transformer(LoggingPropertyMappers.resolveLogHandler("gelf"))
.build(),
fromOption(LoggingOptions.LOG_GELF_LEVEL)
.isEnabled(LoggingPropertyMappers::isGelfEnabled, GELF_ENABLED_MSG)
.to("quarkus.log.handler.gelf.level")
.paramLabel("level")
.build(),
fromOption(LoggingOptions.LOG_GELF_HOST)
.isEnabled(LoggingPropertyMappers::isGelfEnabled, GELF_ENABLED_MSG)
.to("quarkus.log.handler.gelf.host")
.paramLabel("hostname")
.build(),
fromOption(LoggingOptions.LOG_GELF_PORT)
.isEnabled(LoggingPropertyMappers::isGelfEnabled, GELF_ENABLED_MSG)
.to("quarkus.log.handler.gelf.port")
.paramLabel("port")
.build(),
fromOption(LoggingOptions.LOG_GELF_VERSION)
.isEnabled(LoggingPropertyMappers::isGelfEnabled, GELF_ENABLED_MSG)
.to("quarkus.log.handler.gelf.version")
.paramLabel("version")
.build(),
fromOption(LoggingOptions.LOG_GELF_INCLUDE_STACK_TRACE)
.isEnabled(LoggingPropertyMappers::isGelfEnabled, GELF_ENABLED_MSG)
.to("quarkus.log.handler.gelf.extract-stack-trace")
.build(),
fromOption(LoggingOptions.LOG_GELF_TIMESTAMP_FORMAT)
.isEnabled(LoggingPropertyMappers::isGelfEnabled, GELF_ENABLED_MSG)
.to("quarkus.log.handler.gelf.timestamp-pattern")
.paramLabel("pattern")
.build(),
fromOption(LoggingOptions.LOG_GELF_FACILITY)
.isEnabled(LoggingPropertyMappers::isGelfEnabled, GELF_ENABLED_MSG)
.to("quarkus.log.handler.gelf.facility")
.paramLabel("name")
.build(),
fromOption(LoggingOptions.LOG_GELF_MAX_MSG_SIZE)
.isEnabled(LoggingPropertyMappers::isGelfEnabled, GELF_ENABLED_MSG)
.to("quarkus.log.handler.gelf.maximum-message-size")
.paramLabel("size")
.build(),
fromOption(LoggingOptions.LOG_GELF_INCLUDE_LOG_MSG_PARAMS)
.isEnabled(LoggingPropertyMappers::isGelfEnabled, GELF_ENABLED_MSG)
.to("quarkus.log.handler.gelf.include-log-message-parameters")
.build(),
fromOption(LoggingOptions.LOG_GELF_INCLUDE_LOCATION)
.isEnabled(LoggingPropertyMappers::isGelfEnabled, GELF_ENABLED_MSG)
.to("quarkus.log.handler.gelf.include-location")
.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) {
return (parentValue, context) -> {
//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 handlers = parentValue.get();
if(handlers.isBlank()) {
if (handlers.isBlank()) {
addInitializationException(Messages.emptyValueForKey("log"));
return of(consoleDependantErrorResult);
}

View file

@ -27,6 +27,7 @@ import java.util.List;
import java.util.Optional;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.BooleanSupplier;
import io.smallrye.config.ConfigSourceInterceptorContext;
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.Environment;
import org.keycloak.quarkus.runtime.configuration.MicroProfileConfigProvider;
import org.keycloak.utils.StringUtil;
public class PropertyMapper<T> {
static PropertyMapper<?> IDENTITY = new PropertyMapper<>(
new OptionBuilder<>(null, String.class).build(),
null,
() -> false,
"",
null,
null,
null,
@ -59,18 +63,23 @@ public class PropertyMapper<T> {
private final Option<T> option;
private final String to;
private BooleanSupplier enabled;
private String enabledWhen;
private final BiFunction<Optional<String>, ConfigSourceInterceptorContext, Optional<String>> mapper;
private final String mapFrom;
private final boolean mask;
private final String paramLabel;
private final String envVarFormat;
private String cliFormat;
private BiConsumer<PropertyMapper<T>, ConfigValue> validator;
private final String cliFormat;
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) {
this.option = option;
this.to = to == null ? getFrom() : to;
this.enabled = enabled;
this.enabledWhen = enabledWhen;
this.mapper = mapper == null ? PropertyMapper::defaultTransformer : mapper;
this.mapFrom = mapFrom;
this.paramLabel = paramLabel;
@ -148,15 +157,39 @@ public class PropertyMapper<T> {
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() {
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() {
return this.option.getExpectedValues();
@ -209,7 +242,11 @@ public class PropertyMapper<T> {
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
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);
@ -218,8 +255,12 @@ public class PropertyMapper<T> {
return null;
}
return ConfigValue.builder().withName(name).withValue(mappedValue.get()).withRawValue(value.orElse(null))
.withConfigSourceName(configSourceName).build();
return ConfigValue.builder()
.withName(name)
.withValue(mappedValue.get())
.withRawValue(value.orElse(null))
.withConfigSourceName(configSourceName)
.build();
}
private ConfigValue convertValue(ConfigValue configValue) {
@ -237,8 +278,10 @@ public class PropertyMapper<T> {
private BiFunction<Optional<String>, ConfigSourceInterceptorContext, Optional<String>> mapper;
private String mapFrom = null;
private boolean isMasked = false;
private BooleanSupplier isEnabled = () -> true;
private String enabledWhen = "";
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) {
this.option = option;
@ -269,6 +312,17 @@ public class PropertyMapper<T> {
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) {
this.validator = validator;
return this;
@ -278,7 +332,7 @@ public class PropertyMapper<T> {
if (paramLabel == null && Boolean.class.equals(option.getType())) {
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();
}

View file

@ -3,23 +3,45 @@ package org.keycloak.quarkus.runtime.configuration.mappers;
import io.smallrye.config.ConfigSourceInterceptorContext;
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.OptionCategory;
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.DisabledMappersInterceptor;
import org.keycloak.quarkus.runtime.configuration.PersistedConfigSource;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumMap;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
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 static String VALUE_MASK = "*******";
private static final MappersConfig MAPPERS = new MappersConfig();
private static final Logger log = Logger.getLogger(PropertyMappers.class);
private PropertyMappers(){}
@ -44,8 +66,7 @@ public final class PropertyMappers {
}
public static ConfigValue getValue(ConfigSourceInterceptorContext context, String name) {
PropertyMapper<?> mapper = MAPPERS.getOrDefault(name, PropertyMapper.IDENTITY);
return mapper.getConfigValue(name, context);
return getMapperOrDefault(name, PropertyMapper.IDENTITY).getConfigValue(name, context);
}
public static boolean isBuildTimeProperty(String name) {
@ -53,7 +74,7 @@ public final class PropertyMappers {
return true;
}
PropertyMapper<?> mapper = MAPPERS.get(name);
final PropertyMapper<?> mapper = getMapperOrDefault(name, null);
boolean isBuildTimeProperty = mapper == null ? false : mapper.isBuildTime();
return isBuildTimeProperty
@ -83,6 +104,27 @@ public final class PropertyMappers {
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) {
property = removeProfilePrefixIfNeeded(property);
PropertyMapper<?> mapper = getMapper(property);
@ -102,32 +144,95 @@ public final class PropertyMappers {
return property;
}
public static PropertyMapper<?> getMapper(String property) {
if (property.startsWith("%")) {
return MAPPERS.get(property.substring(property.indexOf('.') + 1));
}
return MAPPERS.get(property);
private static PropertyMapper<?> getMapperOrDefault(String property, PropertyMapper<?> defaultMapper) {
final var mappers = MAPPERS.getOrDefault(property, Collections.emptyList());
return switch (mappers.size()) {
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() {
return MAPPERS.values();
public static PropertyMapper<?> getMapper(String property) {
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) {
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);
private Map<OptionCategory, List<PropertyMapper<?>>> runtimeTimeMappers = new EnumMap<>(OptionCategory.class);
PropertyMapper<?> mapper = getDisabledBuildTimeMappers().get(property);
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) {
for (PropertyMapper<?> mapper : mappers) {
super.put(mapper.getTo(), mapper);
super.put(mapper.getFrom(), mapper);
super.put(mapper.getCliFormat(), mapper);
super.put(mapper.getEnvVarFormat(), mapper);
addMapper(mapper);
if (mapper.isBuildTime()) {
addMapperByStage(mapper, buildTimeMappers);
@ -137,22 +242,68 @@ public final class PropertyMappers {
}
}
private void addMapperByStage(PropertyMapper<?> mapper, Map<OptionCategory, List<PropertyMapper<?>>> mappers) {
mappers.computeIfAbsent(mapper.getCategory(),
new Function<OptionCategory, List<PropertyMapper<?>>>() {
@Override
public List<PropertyMapper<?>> apply(OptionCategory c) {
return new ArrayList<>();
}
}).add(mapper);
private static void addMapperByStage(PropertyMapper<?> mapper, Map<OptionCategory, List<PropertyMapper<?>>> mappers) {
mappers.computeIfAbsent(mapper.getCategory(), c -> new ArrayList<>()).add(mapper);
}
@Override
public PropertyMapper<?> put(String key, PropertyMapper<?> value) {
if (containsKey(key)) {
throw new IllegalArgumentException("Duplicated mapper for key [" + key + "]");
public void addMapper(PropertyMapper<?> mapper) {
handleMapper(mapper, this::add);
}
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() {
@ -162,6 +313,35 @@ public final class PropertyMappers {
public Map<OptionCategory, List<PropertyMapper<?>>> getBuildTimeMappers() {
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.
#
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
log-level=${SOME_LOG_LEVEL:warn}
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
@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) {
CLIResult cliResult = (CLIResult) result;
assertEquals("Unknown option: '--db-pasword'\n" +
@ -96,7 +96,7 @@ public class OptionValidationTest {
}
@Test
@Launch({ "start", "--db postgres" })
@Launch({"start", "--db postgres"})
void failSingleParamWithSpace(LaunchResult result) {
CLIResult cliResult = (CLIResult) result;
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 = 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.assertNoMessage("Changes detected in configuration");
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" })
void testJsonFormatApplied(LaunchResult result) throws JsonProcessingException {
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.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.LaunchResult;
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.TestMethodOrder;
import org.keycloak.it.junit5.extension.CLIResult;
import org.keycloak.it.junit5.extension.DistributionTest;
import org.keycloak.it.junit5.extension.RawDistOnly;
import org.keycloak.it.junit5.extension.WithEnvVars;
import org.keycloak.it.utils.KeycloakDistribution;
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;
@DistributionTest
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class OptionsDistTest {
@Test
@Order(1)
@Launch({"build", "--db=invalid"})
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"));
}
@Test
@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(2)
@Launch({"start", "--test=invalid"})
public void testServerDoesNotStartIfValidationFailDuringReAugStart(LaunchResult result) {
assertEquals(1, result.getErrorStream().stream().filter(s -> s.contains("Unknown option: '--test'")).count());
}
@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")
public void testExpressionsInConfigFile(KeycloakDistribution distribution) {
distribution.setEnvVar("MY_LOG_LEVEL", "debug");
CLIResult result = distribution.run(CONFIG_FILE_LONG_NAME + "=" + Paths.get("src/test/resources/OptionsDistTest/keycloak.conf").toAbsolutePath().normalize(), "start-dev");
result.assertMessage("DEBUG [org.keycloak");
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", "--http-enabled=true", "--hostname-strict=false", "--optimized");
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
are: console, file, gelf (deprecated). Default: console.
--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>
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} %
-5p [%c] (%t) %s%e%n.
-5p [%c] (%t) %s%e%n. Available only when Console log handler is activated.
--log-console-output <output>
Set the log output to JSON or default (plain) unstructured logging. Possible
values are: default, json. Default: default.
--log-file <file> Set the log file path and filename. Default: data/log/keycloak.log.
--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.
values are: default, json. Default: default. Available only when Console log
handler is activated.
--log-level <category:level>
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

View file

@ -74,47 +74,61 @@ Logging:
--log <handler> Enable one or more log handlers in a comma-separated list. Possible values
are: console, file, gelf (deprecated). Default: console.
--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>
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} %
-5p [%c] (%t) %s%e%n.
-5p [%c] (%t) %s%e%n. Available only when Console log handler is activated.
--log-console-output <output>
Set the log output to JSON or default (plain) unstructured logging. Possible
values are: default, json. Default: default.
--log-file <file> Set the log file path and filename. Default: data/log/keycloak.log.
values are: default, json. Default: default. Available only when Console 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>
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>
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>
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>
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.
Default: localhost. Available only when GELF is activated.
--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>
DEPRECATED. Include message parameters from the log event. Default: true.
Available only when GELF is activated.
--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.
'StackTrace' field in the GELF output. Default: true. Available only when
GELF is activated.
--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.
Default: INFO. Available only when GELF is activated.
--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.
GELF will submit the message in multiple chunks. Default: 8192. Available
only when GELF is activated.
--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>
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>
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

View file

@ -74,47 +74,16 @@ Logging:
--log <handler> Enable one or more log handlers in a comma-separated list. Possible values
are: console, file, gelf (deprecated). Default: console.
--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>
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} %
-5p [%c] (%t) %s%e%n.
-5p [%c] (%t) %s%e%n. Available only when Console log handler is activated.
--log-console-output <output>
Set the log output to JSON or default (plain) unstructured logging. Possible
values are: default, json. Default: default.
--log-file <file> Set the log file path and filename. Default: data/log/keycloak.log.
--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.
values are: default, json. Default: default. Available only when Console log
handler is activated.
--log-level <category:level>
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

View file

@ -74,47 +74,61 @@ Logging:
--log <handler> Enable one or more log handlers in a comma-separated list. Possible values
are: console, file, gelf (deprecated). Default: console.
--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>
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} %
-5p [%c] (%t) %s%e%n.
-5p [%c] (%t) %s%e%n. Available only when Console log handler is activated.
--log-console-output <output>
Set the log output to JSON or default (plain) unstructured logging. Possible
values are: default, json. Default: default.
--log-file <file> Set the log file path and filename. Default: data/log/keycloak.log.
values are: default, json. Default: default. Available only when Console 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>
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>
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>
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>
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.
Default: localhost. Available only when GELF is activated.
--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>
DEPRECATED. Include message parameters from the log event. Default: true.
Available only when GELF is activated.
--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.
'StackTrace' field in the GELF output. Default: true. Available only when
GELF is activated.
--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.
Default: INFO. Available only when GELF is activated.
--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.
GELF will submit the message in multiple chunks. Default: 8192. Available
only when GELF is activated.
--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>
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>
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

View file

@ -238,47 +238,16 @@ Logging:
--log <handler> Enable one or more log handlers in a comma-separated list. Possible values
are: console, file, gelf (deprecated). Default: console.
--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>
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} %
-5p [%c] (%t) %s%e%n.
-5p [%c] (%t) %s%e%n. Available only when Console log handler is activated.
--log-console-output <output>
Set the log output to JSON or default (plain) unstructured logging. Possible
values are: default, json. Default: default.
--log-file <file> Set the log file path and filename. Default: data/log/keycloak.log.
--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.
values are: default, json. Default: default. Available only when Console log
handler is activated.
--log-level <category:level>
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
@ -305,4 +274,4 @@ Security:
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
options.
options.

View file

@ -238,47 +238,61 @@ Logging:
--log <handler> Enable one or more log handlers in a comma-separated list. Possible values
are: console, file, gelf (deprecated). Default: console.
--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>
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} %
-5p [%c] (%t) %s%e%n.
-5p [%c] (%t) %s%e%n. Available only when Console log handler is activated.
--log-console-output <output>
Set the log output to JSON or default (plain) unstructured logging. Possible
values are: default, json. Default: default.
--log-file <file> Set the log file path and filename. Default: data/log/keycloak.log.
values are: default, json. Default: default. Available only when Console 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>
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>
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>
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>
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.
Default: localhost. Available only when GELF is activated.
--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>
DEPRECATED. Include message parameters from the log event. Default: true.
Available only when GELF is activated.
--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.
'StackTrace' field in the GELF output. Default: true. Available only when
GELF is activated.
--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.
Default: INFO. Available only when GELF is activated.
--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.
GELF will submit the message in multiple chunks. Default: 8192. Available
only when GELF is activated.
--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>
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>
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
@ -305,4 +319,4 @@ Security:
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
options.
options.

View file

@ -239,47 +239,16 @@ Logging:
--log <handler> Enable one or more log handlers in a comma-separated list. Possible values
are: console, file, gelf (deprecated). Default: console.
--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>
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} %
-5p [%c] (%t) %s%e%n.
-5p [%c] (%t) %s%e%n. Available only when Console log handler is activated.
--log-console-output <output>
Set the log output to JSON or default (plain) unstructured logging. Possible
values are: default, json. Default: default.
--log-file <file> Set the log file path and filename. Default: data/log/keycloak.log.
--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.
values are: default, json. Default: default. Available only when Console log
handler is activated.
--log-level <category:level>
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
@ -310,4 +279,4 @@ By default, this command tries to update the server configuration by running a
$ kc.sh start '--optimized'
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
are: console, file, gelf (deprecated). Default: console.
--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>
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} %
-5p [%c] (%t) %s%e%n.
-5p [%c] (%t) %s%e%n. Available only when Console log handler is activated.
--log-console-output <output>
Set the log output to JSON or default (plain) unstructured logging. Possible
values are: default, json. Default: default.
--log-file <file> Set the log file path and filename. Default: data/log/keycloak.log.
values are: default, json. Default: default. Available only when Console 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>
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>
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>
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>
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.
Default: localhost. Available only when GELF is activated.
--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>
DEPRECATED. Include message parameters from the log event. Default: true.
Available only when GELF is activated.
--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.
'StackTrace' field in the GELF output. Default: true. Available only when
GELF is activated.
--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.
Default: INFO. Available only when GELF is activated.
--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.
GELF will submit the message in multiple chunks. Default: 8192. Available
only when GELF is activated.
--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>
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>
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
@ -310,4 +324,4 @@ By default, this command tries to update the server configuration by running a
$ kc.sh start '--optimized'
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
are: console, file, gelf (deprecated). Default: console.
--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>
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} %
-5p [%c] (%t) %s%e%n.
-5p [%c] (%t) %s%e%n. Available only when Console log handler is activated.
--log-console-output <output>
Set the log output to JSON or default (plain) unstructured logging. Possible
values are: default, json. Default: default.
--log-file <file> Set the log file path and filename. Default: data/log/keycloak.log.
--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.
values are: default, json. Default: default. Available only when Console log
handler is activated.
--log-level <category:level>
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
@ -239,4 +208,4 @@ By default, this command tries to update the server configuration by running a
$ kc.sh start '--optimized'
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
are: console, file, gelf (deprecated). Default: console.
--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>
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} %
-5p [%c] (%t) %s%e%n.
-5p [%c] (%t) %s%e%n. Available only when Console log handler is activated.
--log-console-output <output>
Set the log output to JSON or default (plain) unstructured logging. Possible
values are: default, json. Default: default.
--log-file <file> Set the log file path and filename. Default: data/log/keycloak.log.
values are: default, json. Default: default. Available only when Console 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>
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>
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>
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>
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.
Default: localhost. Available only when GELF is activated.
--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>
DEPRECATED. Include message parameters from the log event. Default: true.
Available only when GELF is activated.
--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.
'StackTrace' field in the GELF output. Default: true. Available only when
GELF is activated.
--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.
Default: INFO. Available only when GELF is activated.
--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.
GELF will submit the message in multiple chunks. Default: 8192. Available
only when GELF is activated.
--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>
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>
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
@ -239,4 +253,4 @@ By default, this command tries to update the server configuration by running a
$ kc.sh start '--optimized'
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 {
/**
* Returns true if string is null or blank
*/
public static boolean isBlank(String 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) {
return str == null || str.isEmpty();
}