fix: provide better error messages for list options (#25918)
closes: #25235 Signed-off-by: Steve Hawkins <shawkins@redhat.com>
This commit is contained in:
parent
b1e3caebba
commit
fe39d1b5eb
18 changed files with 184 additions and 214 deletions
|
@ -19,7 +19,7 @@ public class DatabaseOptions {
|
|||
.category(OptionCategory.DATABASE)
|
||||
.description("The database vendor.")
|
||||
.defaultValue("dev-file")
|
||||
.expectedValues(Database::getDatabaseAliases)
|
||||
.expectedValues(Database.getDatabaseAliases())
|
||||
.buildTime(true)
|
||||
.build();
|
||||
|
||||
|
|
|
@ -10,18 +10,18 @@ import java.util.stream.Collectors;
|
|||
|
||||
public class FeatureOptions {
|
||||
|
||||
public static final Option<List> FEATURES = new OptionBuilder("features", List.class, Profile.Feature.class)
|
||||
public static final Option<List<String>> FEATURES = OptionBuilder.listOptionBuilder("features", String.class)
|
||||
.category(OptionCategory.FEATURE)
|
||||
.description("Enables a set of one or more features.")
|
||||
.defaultValue(Optional.empty())
|
||||
.expectedValues(() -> getFeatureValues(true))
|
||||
.expectedValues(getFeatureValues(true))
|
||||
.buildTime(true)
|
||||
.build();
|
||||
|
||||
public static final Option<List> FEATURES_DISABLED = new OptionBuilder("features-disabled", List.class, Profile.Feature.class)
|
||||
public static final Option<List<String>> FEATURES_DISABLED = OptionBuilder.listOptionBuilder("features-disabled", String.class)
|
||||
.category(OptionCategory.FEATURE)
|
||||
.description("Disables a set of one or more features.")
|
||||
.expectedValues(() -> getFeatureValues(false))
|
||||
.expectedValues(getFeatureValues(false))
|
||||
.buildTime(true)
|
||||
.build();
|
||||
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
package org.keycloak.config;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import org.keycloak.common.crypto.FipsMode;
|
||||
|
||||
public class HttpOptions {
|
||||
|
@ -54,10 +57,10 @@ public class HttpOptions {
|
|||
.description("The cipher suites to use. If none is given, a reasonable default is selected.")
|
||||
.build();
|
||||
|
||||
public static final Option<String> HTTPS_PROTOCOLS = new OptionBuilder<>("https-protocols", String.class)
|
||||
public static final Option<List<String>> HTTPS_PROTOCOLS = OptionBuilder.listOptionBuilder("https-protocols", String.class)
|
||||
.category(OptionCategory.HTTP)
|
||||
.description("The list of protocols to explicitly enable.")
|
||||
.defaultValue("TLSv1.3,TLSv1.2")
|
||||
.defaultValue(Arrays.asList("TLSv1.3,TLSv1.2"))
|
||||
.build();
|
||||
|
||||
public static final Option<File> HTTPS_CERTIFICATE_FILE = new OptionBuilder<>("https-certificate-file", File.class)
|
||||
|
|
|
@ -30,11 +30,11 @@ public class LoggingOptions {
|
|||
.toList();
|
||||
}
|
||||
|
||||
public static final Option<List> LOG = new OptionBuilder("log", List.class, Handler.class)
|
||||
public static final Option<List<Handler>> LOG = OptionBuilder.listOptionBuilder("log", Handler.class)
|
||||
.category(OptionCategory.LOGGING)
|
||||
.description("Enable one or more log handlers in a comma-separated list.")
|
||||
.expectedValues(() -> getAvailableHandlerNames())
|
||||
.defaultValue(DEFAULT_LOG_HANDLER)
|
||||
.expectedValues(getAvailableHandlerNames())
|
||||
.defaultValue(Arrays.asList(DEFAULT_LOG_HANDLER))
|
||||
.build();
|
||||
|
||||
public enum Level {
|
||||
|
@ -53,9 +53,9 @@ public class LoggingOptions {
|
|||
}
|
||||
}
|
||||
|
||||
public static final Option<String> LOG_LEVEL = new OptionBuilder<>("log-level", String.class)
|
||||
public static final Option<List<String>> LOG_LEVEL = OptionBuilder.listOptionBuilder("log-level", String.class)
|
||||
.category(OptionCategory.LOGGING)
|
||||
.defaultValue(DEFAULT_LOG_LEVEL.toString())
|
||||
.defaultValue(Arrays.asList(DEFAULT_LOG_LEVEL.toString()))
|
||||
.description("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 specify a category.")
|
||||
.build();
|
||||
|
||||
|
|
|
@ -1,19 +0,0 @@
|
|||
package org.keycloak.config;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
public class MultiOption<T> extends Option<T> {
|
||||
|
||||
private final Class auxiliaryType;
|
||||
|
||||
public MultiOption(Class type, Class auxiliaryType, String key, OptionCategory category, boolean hidden, boolean buildTime, String description, Optional defaultValue, Supplier<List<String>> expectedValues, DeprecatedMetadata deprecatedMetadata) {
|
||||
super(type, key, category, hidden, buildTime, description, defaultValue, expectedValues, deprecatedMetadata);
|
||||
this.auxiliaryType = auxiliaryType;
|
||||
}
|
||||
|
||||
public Class<?> getAuxiliaryType() {
|
||||
return auxiliaryType;
|
||||
}
|
||||
}
|
|
@ -2,7 +2,7 @@ package org.keycloak.config;
|
|||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class Option<T> {
|
||||
|
||||
|
@ -13,10 +13,10 @@ public class Option<T> {
|
|||
private final boolean buildTime;
|
||||
private final String description;
|
||||
private final Optional<T> defaultValue;
|
||||
private final Supplier<List<String>> expectedValues;
|
||||
private final List<String> expectedValues;
|
||||
private final DeprecatedMetadata deprecatedMetadata;
|
||||
|
||||
public Option(Class<T> type, String key, OptionCategory category, boolean hidden, boolean buildTime, String description, Optional<T> defaultValue, Supplier<List<String>> expectedValues, DeprecatedMetadata deprecatedMetadata) {
|
||||
public Option(Class<T> type, String key, OptionCategory category, boolean hidden, boolean buildTime, String description, Optional<T> defaultValue, List<String> expectedValues, DeprecatedMetadata deprecatedMetadata) {
|
||||
this.type = type;
|
||||
this.key = key;
|
||||
this.category = category;
|
||||
|
@ -53,7 +53,7 @@ public class Option<T> {
|
|||
}
|
||||
|
||||
public List<String> getExpectedValues() {
|
||||
return expectedValues.get();
|
||||
return expectedValues;
|
||||
}
|
||||
|
||||
public Optional<DeprecatedMetadata> getDeprecatedMetadata() {
|
||||
|
@ -90,4 +90,14 @@ public class Option<T> {
|
|||
|
||||
return description;
|
||||
}
|
||||
|
||||
public static String getDefaultValueString(Object value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
if (value instanceof List) {
|
||||
return ((List<?>) value).stream().map(String::valueOf).collect(Collectors.joining(","));
|
||||
}
|
||||
return String.valueOf(value);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,56 +1,54 @@
|
|||
package org.keycloak.config;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class OptionBuilder<T> {
|
||||
|
||||
private static final Supplier<List<String>> EMPTY_VALUES_SUPPLIER = List::of;
|
||||
private static final Supplier<List<String>> BOOLEAN_TYPE_VALUES = new Supplier<List<String>>() {
|
||||
List<String> values = List.of(Boolean.TRUE.toString(), Boolean.FALSE.toString());
|
||||
|
||||
@Override
|
||||
public List<String> get() {
|
||||
return values;
|
||||
}
|
||||
};
|
||||
private static final List<String> BOOLEAN_TYPE_VALUES = List.of(Boolean.TRUE.toString(), Boolean.FALSE.toString());
|
||||
|
||||
private final Class<T> type;
|
||||
private final Class<T> auxiliaryType;
|
||||
private final String key;
|
||||
private OptionCategory category;
|
||||
private boolean hidden;
|
||||
private boolean build;
|
||||
private String description;
|
||||
private Optional<T> defaultValue;
|
||||
private Supplier<List<String>> expectedValues;
|
||||
private List<String> expectedValues = List.of();
|
||||
private DeprecatedMetadata deprecatedMetadata;
|
||||
|
||||
public static <A> OptionBuilder<List<A>> listOptionBuilder(String key, Class<A> type) {
|
||||
return new OptionBuilder(key, List.class, type);
|
||||
}
|
||||
|
||||
public OptionBuilder(String key, Class<T> type) {
|
||||
this(key, type, null);
|
||||
}
|
||||
|
||||
public OptionBuilder(String key, Class<T> type, Class<T> auxiliaryType) {
|
||||
private OptionBuilder(String key, Class<T> type, Class<?> auxiliaryType) {
|
||||
this.type = type;
|
||||
this.auxiliaryType = auxiliaryType;
|
||||
if (type.isArray() || ((Collection.class.isAssignableFrom(type) || Map.class.isAssignableFrom(type)) && type != java.util.List.class)) {
|
||||
throw new IllegalArgumentException("Non-List multi-valued options are not yet supported");
|
||||
}
|
||||
this.key = key;
|
||||
category = OptionCategory.GENERAL;
|
||||
hidden = false;
|
||||
build = false;
|
||||
description = null;
|
||||
defaultValue = Boolean.class.equals(type) ? Optional.of((T) Boolean.FALSE) : Optional.empty();
|
||||
expectedValues = EMPTY_VALUES_SUPPLIER;
|
||||
if (Boolean.class.equals(type)) {
|
||||
Class<?> expected = type;
|
||||
if (auxiliaryType != null) {
|
||||
expected = auxiliaryType;
|
||||
}
|
||||
defaultValue = Boolean.class.equals(expected) ? Optional.of((T) Boolean.FALSE) : Optional.empty();
|
||||
if (Boolean.class.equals(expected)) {
|
||||
expectedValues(BOOLEAN_TYPE_VALUES);
|
||||
}
|
||||
if (Enum.class.isAssignableFrom(type)) {
|
||||
expectedValues((Class<? extends Enum>) type);
|
||||
}
|
||||
if (auxiliaryType != null && Enum.class.isAssignableFrom(auxiliaryType)) {
|
||||
expectedValues((Class<? extends Enum>) auxiliaryType);
|
||||
if (Enum.class.isAssignableFrom(expected)) {
|
||||
expectedValues((Class<? extends Enum>) expected);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -84,32 +82,18 @@ public class OptionBuilder<T> {
|
|||
return this;
|
||||
}
|
||||
|
||||
public OptionBuilder<T> expectedValues(Supplier<List<String>> expected) {
|
||||
public OptionBuilder<T> expectedValues(List<String> expected) {
|
||||
this.expectedValues = expected;
|
||||
return this;
|
||||
}
|
||||
|
||||
public OptionBuilder<T> expectedValues(Class<? extends Enum> expected) {
|
||||
this.expectedValues = new Supplier<>() {
|
||||
List<String> values = List.of(expected.getEnumConstants()).stream().map(Object::toString).collect(Collectors.toList());
|
||||
|
||||
@Override
|
||||
public List<String> get() {
|
||||
return values;
|
||||
}
|
||||
};
|
||||
this.expectedValues = List.of(expected.getEnumConstants()).stream().map(Object::toString).collect(Collectors.toList());
|
||||
return this;
|
||||
}
|
||||
|
||||
public OptionBuilder<T> expectedValues(T ... expected) {
|
||||
this.expectedValues = new Supplier<>() {
|
||||
List<String> values = List.of(expected).stream().map(v -> v.toString()).collect(Collectors.toList());
|
||||
|
||||
@Override
|
||||
public List<String> get() {
|
||||
return values;
|
||||
}
|
||||
};
|
||||
this.expectedValues = List.of(expected).stream().map(v -> v.toString()).collect(Collectors.toList());
|
||||
return this;
|
||||
}
|
||||
|
||||
|
@ -135,11 +119,7 @@ public class OptionBuilder<T> {
|
|||
|
||||
|
||||
public Option<T> build() {
|
||||
if (auxiliaryType != null) {
|
||||
return new MultiOption<T>(type, auxiliaryType, key, category, hidden, build, description, defaultValue, expectedValues, deprecatedMetadata);
|
||||
} else {
|
||||
return new Option<T>(type, key, category, hidden, build, description, defaultValue, expectedValues, deprecatedMetadata);
|
||||
}
|
||||
return new Option<T>(type, key, category, hidden, build, description, defaultValue, expectedValues, deprecatedMetadata);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ public class SecurityOptions {
|
|||
|
||||
public static final Option<FipsMode> FIPS_MODE = new OptionBuilder<>("fips-mode", FipsMode.class)
|
||||
.category(OptionCategory.SECURITY)
|
||||
.expectedValues(SecurityOptions::getFipsModeValues)
|
||||
.expectedValues(getFipsModeValues())
|
||||
.buildTime(true)
|
||||
.description("Sets the FIPS mode. If '" + FipsMode.NON_STRICT + "' is set, FIPS is enabled but on non-approved mode. For full FIPS compliance, set '" + FipsMode.STRICT + "' to run on approved mode. "
|
||||
+ "This option defaults to '" + FipsMode.DISABLED + "' when '" + Profile.Feature.FIPS.getKey() + "' feature is disabled, which is by default. "
|
||||
|
|
|
@ -2,9 +2,11 @@ package org.keycloak.config;
|
|||
|
||||
import org.keycloak.common.enums.HostnameVerificationPolicy;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class TruststoreOptions {
|
||||
|
||||
public static final Option<String> TRUSTSTORE_PATHS = new OptionBuilder<>("truststore-paths", String.class)
|
||||
public static final Option<List<String>> TRUSTSTORE_PATHS = OptionBuilder.listOptionBuilder("truststore-paths", String.class)
|
||||
.category(OptionCategory.TRUSTSTORE)
|
||||
.description("List of pkcs12 (p12 or pfx file extensions), PEM files, or directories containing those files that will be used as a system truststore.")
|
||||
.build();
|
||||
|
|
|
@ -56,7 +56,7 @@ import java.util.stream.Collectors;
|
|||
import org.eclipse.microprofile.config.spi.ConfigSource;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.config.DeprecatedMetadata;
|
||||
import org.keycloak.config.MultiOption;
|
||||
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;
|
||||
|
@ -399,7 +399,7 @@ public final class Picocli {
|
|||
if (runtimeValue == null && isNotBlank(persistedValue)) {
|
||||
PropertyMapper<?> mapper = PropertyMappers.getMapper(propertyName);
|
||||
|
||||
if (mapper != null && persistedValue.equals(mapper.getDefaultValue().map(Object::toString).orElse(null))) {
|
||||
if (mapper != null && persistedValue.equals(Option.getDefaultValueString(mapper.getDefaultValue().orElse(null)))) {
|
||||
// same as default
|
||||
continue;
|
||||
}
|
||||
|
@ -608,14 +608,11 @@ public final class Picocli {
|
|||
.hidden(mapper.isHidden());
|
||||
|
||||
if (mapper.getDefaultValue().isPresent()) {
|
||||
optBuilder.defaultValue(mapper.getDefaultValue().get().toString());
|
||||
optBuilder.defaultValue(Option.getDefaultValueString(mapper.getDefaultValue().get()));
|
||||
}
|
||||
|
||||
if (mapper.getType() != null) {
|
||||
optBuilder.type(mapper.getType());
|
||||
if (mapper.getOption() instanceof MultiOption) {
|
||||
optBuilder.auxiliaryTypes(((MultiOption<?>) mapper.getOption()).getAuxiliaryType());
|
||||
}
|
||||
} else {
|
||||
optBuilder.type(String.class);
|
||||
}
|
||||
|
@ -639,7 +636,7 @@ public final class Picocli {
|
|||
}
|
||||
|
||||
mapper.getDefaultValue()
|
||||
.map(d -> d.toString().replaceAll("%", "%%")) // escape formats
|
||||
.map(d -> Option.getDefaultValueString(d).replaceAll("%", "%%")) // escape formats
|
||||
.map(d -> " Default: " + d + ".")
|
||||
.ifPresent(transformedDesc::append);
|
||||
|
||||
|
|
|
@ -19,11 +19,8 @@ package org.keycloak.quarkus.runtime.cli;
|
|||
|
||||
import static org.keycloak.quarkus.runtime.cli.Picocli.ARG_PREFIX;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Stack;
|
||||
|
||||
import org.keycloak.utils.StringUtil;
|
||||
|
||||
import picocli.CommandLine;
|
||||
import picocli.CommandLine.Model.ArgSpec;
|
||||
import picocli.CommandLine.Model.CommandSpec;
|
||||
|
@ -52,21 +49,27 @@ public final class PropertyMapperParameterConsumer implements CommandLine.IParam
|
|||
CommandLine commandLine = commandSpec.commandLine();
|
||||
|
||||
if (args.isEmpty() || !isOptionValue(args.peek())) {
|
||||
throw new ParameterException(
|
||||
commandLine, "Missing required value for option '" + name + "' (" + argSpec.paramLabel() + ")." + getExpectedValuesMessage(argSpec.completionCandidates(), option.completionCandidates()));
|
||||
throw new ParameterException(commandLine,
|
||||
"Missing required value. " + getExpectedMessage(argSpec, option, name));
|
||||
}
|
||||
|
||||
// consumes the value, actual value validation will be performed later
|
||||
args.pop();
|
||||
|
||||
if (!args.isEmpty() && isOptionValue(args.peek())) {
|
||||
throw new ParameterException(
|
||||
commandLine, "Option '" + name + "' expects a single value (" + argSpec.paramLabel() + ")" + getExpectedValuesMessage(argSpec.completionCandidates(), option.completionCandidates()));
|
||||
throw new ParameterException(commandLine, getExpectedMessage(argSpec, option, name));
|
||||
}
|
||||
}
|
||||
|
||||
public static String getErrorMessage(String name, String value, Iterable<String> specCandidates, Iterable<String> optionCandidates) {
|
||||
return "Invalid value for option '" + name + "': " + value + "." + getExpectedValuesMessage(specCandidates, optionCandidates);
|
||||
private String getExpectedMessage(ArgSpec argSpec, OptionSpec option, String name) {
|
||||
return String.format("Option '%s' (%s) expects %s.%s", name, argSpec.paramLabel(),
|
||||
option.typeInfo().isMultiValue() ? "one or more comma separated values without whitespace": "a single value",
|
||||
getExpectedValuesMessage(argSpec.completionCandidates(), option.completionCandidates()));
|
||||
}
|
||||
|
||||
public static String getErrorMessage(String name, String value, Iterable<String> expected) {
|
||||
return String.format("Invalid value for option '%s': %s.%s", name, value,
|
||||
getExpectedValuesMessage(expected, expected));
|
||||
}
|
||||
|
||||
private boolean isOptionValue(String arg) {
|
||||
|
@ -77,22 +80,4 @@ public final class PropertyMapperParameterConsumer implements CommandLine.IParam
|
|||
return optionCandidates.iterator().hasNext() ? " Expected values are: " + String.join(", ", specCandidates) : "";
|
||||
}
|
||||
|
||||
public static boolean isExpectedValue(Collection<String> expectedValues, String value) {
|
||||
if (expectedValues.isEmpty()) {
|
||||
// accept any
|
||||
return true;
|
||||
}
|
||||
|
||||
if (StringUtil.isBlank(value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (String v : value.split(",")) {
|
||||
if (!expectedValues.contains(v)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,7 +9,6 @@ import java.util.Set;
|
|||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.keycloak.quarkus.runtime.configuration.mappers.PropertyMapper.fromOption;
|
||||
|
||||
|
@ -24,7 +23,8 @@ public final class FeaturePropertyMappers {
|
|||
return new PropertyMapper[] {
|
||||
fromOption(FeatureOptions.FEATURES)
|
||||
.paramLabel("feature")
|
||||
.validator((mapper, value) -> validateEnabledFeatures(value.getValue()))
|
||||
.validator((mapper, value) -> mapper.validateExpectedValues(value,
|
||||
(c, v) -> validateEnabledFeature(v)))
|
||||
.build(),
|
||||
fromOption(FeatureOptions.FEATURES_DISABLED)
|
||||
.paramLabel("feature")
|
||||
|
@ -32,36 +32,34 @@ public final class FeaturePropertyMappers {
|
|||
};
|
||||
}
|
||||
|
||||
public static void validateEnabledFeatures(String s) {
|
||||
Stream.of(s.split(",")).forEach(feature -> {
|
||||
if (!Profile.getFeatureVersions(feature).isEmpty()) {
|
||||
return;
|
||||
public static void validateEnabledFeature(String feature) {
|
||||
if (!Profile.getFeatureVersions(feature).isEmpty()) {
|
||||
return;
|
||||
}
|
||||
if (feature.equals(Profile.Feature.Type.PREVIEW.name().toLowerCase())) {
|
||||
return;
|
||||
}
|
||||
Matcher matcher = VERSIONED_PATTERN.matcher(feature);
|
||||
if (!matcher.matches()) {
|
||||
if (feature.contains(":")) {
|
||||
throw new PropertyException(String.format(
|
||||
"%s has an invalid format for enabling a feature, expected format is feature:v{version}, e.g. docker:v1",
|
||||
feature));
|
||||
}
|
||||
if (feature.equals(Profile.Feature.Type.PREVIEW.name().toLowerCase())) {
|
||||
return;
|
||||
}
|
||||
Matcher matcher = VERSIONED_PATTERN.matcher(feature);
|
||||
if (!matcher.matches()) {
|
||||
if (feature.contains(":")) {
|
||||
throw new PropertyException(String.format(
|
||||
"%s has an invalid format for enabling a feature, expected format is feature:v{version}, e.g. docker:v1",
|
||||
feature));
|
||||
}
|
||||
throw new PropertyException(String.format("%s is an unrecognized feature, it should be one of %s", feature,
|
||||
FeatureOptions.getFeatureValues(false)));
|
||||
}
|
||||
String unversionedFeature = matcher.group(1);
|
||||
Set<Feature> featureVersions = Profile.getFeatureVersions(unversionedFeature);
|
||||
if (featureVersions.isEmpty()) {
|
||||
throw new PropertyException(String.format("%s has an unrecognized feature, it should be one of %s",
|
||||
feature, FeatureOptions.getFeatureValues(false)));
|
||||
}
|
||||
int version = Integer.parseInt(matcher.group(2));
|
||||
if (featureVersions.stream().noneMatch(f -> f.getVersion() == version)) {
|
||||
throw new PropertyException(
|
||||
String.format("%s has an unrecognized feature version, it should be one of %s", feature,
|
||||
featureVersions.stream().map(Feature::getVersion).map(String::valueOf).collect(Collectors.toList())));
|
||||
}
|
||||
});
|
||||
throw new PropertyException(String.format("%s is an unrecognized feature, it should be one of %s", feature,
|
||||
FeatureOptions.getFeatureValues(false)));
|
||||
}
|
||||
String unversionedFeature = matcher.group(1);
|
||||
Set<Feature> featureVersions = Profile.getFeatureVersions(unversionedFeature);
|
||||
if (featureVersions.isEmpty()) {
|
||||
throw new PropertyException(String.format("%s has an unrecognized feature, it should be one of %s",
|
||||
feature, FeatureOptions.getFeatureValues(false)));
|
||||
}
|
||||
int version = Integer.parseInt(matcher.group(2));
|
||||
if (!featureVersions.stream().anyMatch(f -> f.getVersion() == version)) {
|
||||
throw new PropertyException(
|
||||
String.format("%s has an unrecognized feature version, it should be one of %s", feature,
|
||||
featureVersions.stream().map(Feature::getVersion).map(String::valueOf).collect(Collectors.toList())));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,7 +24,6 @@ import static org.keycloak.quarkus.runtime.configuration.Configuration.toCliForm
|
|||
import static org.keycloak.quarkus.runtime.configuration.Configuration.toEnvVarFormat;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.BiFunction;
|
||||
|
@ -32,7 +31,6 @@ import java.util.function.BiFunction;
|
|||
import io.smallrye.config.ConfigSourceInterceptorContext;
|
||||
import io.smallrye.config.ConfigValue;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.config.DeprecatedMetadata;
|
||||
import org.keycloak.config.Option;
|
||||
import org.keycloak.config.OptionBuilder;
|
||||
|
@ -69,8 +67,6 @@ public class PropertyMapper<T> {
|
|||
private String cliFormat;
|
||||
private BiConsumer<PropertyMapper<T>, ConfigValue> validator;
|
||||
|
||||
private static final Logger logger = Logger.getLogger(PropertyMapper.class);
|
||||
|
||||
PropertyMapper(Option<T> option, String to, BiFunction<Optional<String>, ConfigSourceInterceptorContext, Optional<String>> mapper,
|
||||
String mapFrom, String paramLabel, boolean mask, BiConsumer<PropertyMapper<T>, ConfigValue> validator) {
|
||||
this.option = option;
|
||||
|
@ -119,14 +115,14 @@ public class PropertyMapper<T> {
|
|||
PropertyMapper<?> parentMapper = PropertyMappers.getMapper(parentKey);
|
||||
|
||||
if (parentMapper != null && parentMapper.getDefaultValue().isPresent()) {
|
||||
parentValue = ConfigValue.builder().withValue(parentMapper.getDefaultValue().get().toString()).build();
|
||||
parentValue = ConfigValue.builder().withValue(Option.getDefaultValueString(parentMapper.getDefaultValue().get())).build();
|
||||
}
|
||||
}
|
||||
|
||||
return transformValue(name, ofNullable(parentValue == null ? null : parentValue.getValue()), context, null);
|
||||
}
|
||||
|
||||
ConfigValue defaultValue = transformValue(name, this.option.getDefaultValue().map(Objects::toString), context, null);
|
||||
ConfigValue defaultValue = transformValue(name, this.option.getDefaultValue().map(Option::getDefaultValueString), context, null);
|
||||
|
||||
if (defaultValue != null) {
|
||||
return defaultValue;
|
||||
|
@ -242,7 +238,7 @@ public class PropertyMapper<T> {
|
|||
private String mapFrom = null;
|
||||
private boolean isMasked = false;
|
||||
private String paramLabel;
|
||||
private BiConsumer<PropertyMapper<T>, ConfigValue> validator = (mapper, value) -> mapper.validateExpectedValues(value);
|
||||
private BiConsumer<PropertyMapper<T>, ConfigValue> validator = (mapper, value) -> mapper.validateExpectedValues(value, (c, v) -> mapper.validateSingleValue(c, v));
|
||||
|
||||
public Builder(Option<T> option) {
|
||||
this.option = option;
|
||||
|
@ -296,15 +292,39 @@ public class PropertyMapper<T> {
|
|||
}
|
||||
}
|
||||
|
||||
public void validateExpectedValues(ConfigValue value) {
|
||||
if (PropertyMapperParameterConsumer.isExpectedValue(getExpectedValues(), value.getValue())) {
|
||||
return;
|
||||
public void validateExpectedValues(ConfigValue configValue, BiConsumer<ConfigValue, String> singleValidator) {
|
||||
String value = configValue.getValue();
|
||||
|
||||
boolean multiValued = getOption().getType() == java.util.List.class;
|
||||
|
||||
String[] values = multiValued ? value.split(",") : new String[] { value };
|
||||
for (String v : values) {
|
||||
boolean cli = isCliOption(configValue);
|
||||
if (multiValued && !v.trim().equals(v)) {
|
||||
throw new PropertyException("Invalid value for multivalued option '" + (cli ? this.getCliFormat() : getFrom())
|
||||
+ "': list value '" + v + "' should not have leading nor trailing whitespace"
|
||||
+ getConfigSourceMessage(configValue, cli));
|
||||
}
|
||||
singleValidator.accept(configValue, v);
|
||||
}
|
||||
boolean cli = Optional.ofNullable(value.getConfigSourceName()).filter(name -> name.contains(ConfigArgsConfigSource.NAME)).isPresent();
|
||||
throw new PropertyException(
|
||||
PropertyMapperParameterConsumer.getErrorMessage(cli ? this.getCliFormat() : getFrom(),
|
||||
value.getValue(), getExpectedValues(), getExpectedValues())
|
||||
+ (cli ? "" : ". From ConfigSource " + value.getConfigSourceName()));
|
||||
}
|
||||
|
||||
private boolean isCliOption(ConfigValue configValue) {
|
||||
return Optional.ofNullable(configValue.getConfigSourceName()).filter(name -> name.contains(ConfigArgsConfigSource.NAME)).isPresent();
|
||||
}
|
||||
|
||||
void validateSingleValue(ConfigValue configValue, String v) {
|
||||
List<String> expectedValues = getExpectedValues();
|
||||
if (!expectedValues.isEmpty() && !expectedValues.contains(v)) {
|
||||
boolean cli = isCliOption(configValue);
|
||||
throw new PropertyException(
|
||||
PropertyMapperParameterConsumer.getErrorMessage(cli ? this.getCliFormat() : getFrom(), v,
|
||||
expectedValues) + getConfigSourceMessage(configValue, cli));
|
||||
}
|
||||
}
|
||||
|
||||
String getConfigSourceMessage(ConfigValue configValue, boolean cli) {
|
||||
return cli ? "" : ". From ConfigSource " + configValue.getConfigSourceName();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -28,28 +28,28 @@ public class FeaturePropertyMappersTest {
|
|||
|
||||
@Test
|
||||
public void testInvalidFeatureFormat() {
|
||||
assertThrows(PropertyException.class, () -> FeaturePropertyMappers.validateEnabledFeatures("invalid:"));
|
||||
assertThrows(PropertyException.class, () -> FeaturePropertyMappers.validateEnabledFeature("invalid:"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInvalidFeature() {
|
||||
assertThrows(PropertyException.class, () -> FeaturePropertyMappers.validateEnabledFeatures("invalid"));
|
||||
assertThrows(PropertyException.class, () -> FeaturePropertyMappers.validateEnabledFeature("invalid"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInvalidVersionedFeature() {
|
||||
assertThrows(PropertyException.class, () -> FeaturePropertyMappers.validateEnabledFeatures("invalid:v1"));
|
||||
assertThrows(PropertyException.class, () -> FeaturePropertyMappers.validateEnabledFeature("invalid:v1"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInvalidFeatureVersion() {
|
||||
assertThrows(PropertyException.class, () -> FeaturePropertyMappers.validateEnabledFeatures(Feature.DOCKER.getUnversionedKey() + ":v0"));
|
||||
assertThrows(PropertyException.class, () -> FeaturePropertyMappers.validateEnabledFeature(Feature.DOCKER.getUnversionedKey() + ":v0"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValidFeatures() {
|
||||
FeaturePropertyMappers.validateEnabledFeatures(
|
||||
Feature.DOCKER.getUnversionedKey() + "," + "preview" + "," + Feature.ACCOUNT2.getVersionedKey());
|
||||
FeaturePropertyMappers.validateEnabledFeature("preview");
|
||||
FeaturePropertyMappers.validateEnabledFeature(Feature.ACCOUNT2.getVersionedKey());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -36,14 +36,28 @@ public class OptionValidationTest {
|
|||
@Launch({"build", "--db"})
|
||||
public void failMissingOptionValue(LaunchResult result) {
|
||||
CLIResult cliResult = (CLIResult) result;
|
||||
assertThat(cliResult.getErrorOutput(), containsString("Missing required value for option '--db' (vendor). Expected values are: dev-file, dev-mem, mariadb, mssql, mysql, oracle, postgres"));
|
||||
assertThat(cliResult.getErrorOutput(), containsString("Missing required value. Option '--db' (vendor) expects a single value. Expected values are: dev-file, dev-mem, mariadb, mssql, mysql, oracle, postgres"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Launch({"build", "--db", "foo", "bar"})
|
||||
public void failMultipleOptionValue(LaunchResult result) {
|
||||
CLIResult cliResult = (CLIResult) result;
|
||||
assertThat(cliResult.getErrorOutput(), containsString("Option '--db' expects a single value (vendor) Expected values are: dev-file, dev-mem, mariadb, mssql, mysql, oracle, postgres"));
|
||||
assertThat(cliResult.getErrorOutput(), containsString("Option '--db' (vendor) expects a single value. Expected values are: dev-file, dev-mem, mariadb, mssql, mysql, oracle, postgres"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Launch({"build", "--features", "account2", "account3"})
|
||||
public void failMultipleMultiOptionValue(LaunchResult result) {
|
||||
CLIResult cliResult = (CLIResult) result;
|
||||
assertThat(cliResult.getErrorOutput(), containsString("Option '--features' (feature) expects one or more comma separated values without whitespace. Expected values are: "));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Launch({"build", "--features", "xyz,account2"})
|
||||
public void failInvalidMultiOptionValue(LaunchResult result) {
|
||||
CLIResult cliResult = (CLIResult) result;
|
||||
assertThat(cliResult.getErrorOutput(), containsString("xyz is an unrecognized feature, it should be one of"));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
@ -145,7 +145,7 @@ public class LoggingDistTest {
|
|||
void failUnknownHandlersInConfFile(KeycloakDistribution dist) {
|
||||
dist.copyOrReplaceFileFromClasspath("/logging/keycloak.conf", Paths.get("conf", "keycloak.conf"));
|
||||
CLIResult cliResult = dist.run("start-dev");
|
||||
cliResult.assertError("Invalid value for option 'kc.log': foo,console. Expected values are: console, file, gelf.");
|
||||
cliResult.assertError("Invalid value for option 'kc.log': foo. Expected values are: console, file, gelf.");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -159,7 +159,7 @@ public class LoggingDistTest {
|
|||
@Launch({ "start-dev","--log=foo,bar" })
|
||||
void failUnknownHandlersInCliCommand(LaunchResult result) {
|
||||
CLIResult cliResult = (CLIResult) result;
|
||||
cliResult.assertError("Invalid value for option '--log': foo,bar");
|
||||
cliResult.assertError("Invalid value for option '--log': foo");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
@ -28,7 +28,6 @@ import java.util.concurrent.TimeoutException;
|
|||
import org.eclipse.microprofile.config.spi.ConfigProviderResolver;
|
||||
import org.keycloak.common.Version;
|
||||
import org.keycloak.common.crypto.FipsMode;
|
||||
import org.keycloak.config.DatabaseOptions;
|
||||
import org.keycloak.config.HttpOptions;
|
||||
import org.keycloak.config.LoggingOptions;
|
||||
import org.keycloak.config.Option;
|
||||
|
@ -117,7 +116,7 @@ public class Keycloak {
|
|||
addOptionIfNotSet(args, HttpOptions.HTTP_PORT);
|
||||
addOptionIfNotSet(args, HttpOptions.HTTPS_PORT);
|
||||
|
||||
boolean isFipsEnabled = ofNullable(getOptionValue(args, SecurityOptions.FIPS_MODE)).orElse(FipsMode.DISABLED).isFipsEnabled();
|
||||
boolean isFipsEnabled = ofNullable(getOptionValue(args, SecurityOptions.FIPS_MODE)).map(FipsMode::valueOf).orElse(FipsMode.DISABLED).isFipsEnabled();
|
||||
|
||||
if (isFipsEnabled) {
|
||||
String logLevel = getOptionValue(args, LoggingOptions.LOG_LEVEL);
|
||||
|
@ -135,41 +134,22 @@ public class Keycloak {
|
|||
}
|
||||
|
||||
private <T> void addOptionIfNotSet(List<String> args, Option<T> option, T defaultValue) {
|
||||
T value = getOptionValue(args, option);
|
||||
String value = getOptionValue(args, option);
|
||||
|
||||
if (value == null) {
|
||||
defaultValue = ofNullable(defaultValue).orElseGet(option.getDefaultValue()::get);
|
||||
args.add(Configuration.toCliFormat(option.getKey()) + "=" + defaultValue);
|
||||
args.add(Configuration.toCliFormat(option.getKey()) + "=" + Option.getDefaultValueString(defaultValue));
|
||||
}
|
||||
}
|
||||
|
||||
private <T> T getOptionValue(List<String> args, Option<T> option) {
|
||||
private String getOptionValue(List<String> args, Option<?> option) {
|
||||
for (String arg : args) {
|
||||
if (arg.contains(option.getKey())) {
|
||||
if (arg.endsWith(option.getKey())) {
|
||||
throw new IllegalArgumentException("Option '" + arg + "' value must be set using '=' as a separator");
|
||||
}
|
||||
|
||||
String value = arg.substring(Picocli.ARG_PREFIX.length() + option.getKey().length() + 1);
|
||||
Class<T> type = option.getType();
|
||||
|
||||
if (type.equals(String.class)) {
|
||||
return (T) value;
|
||||
}
|
||||
|
||||
if (type.isEnum()) {
|
||||
return (T) Enum.valueOf((Class<Enum>) type, value);
|
||||
}
|
||||
|
||||
if (Integer.class.isAssignableFrom(type)) {
|
||||
return (T) Integer.valueOf(value);
|
||||
}
|
||||
|
||||
if (Boolean.class.isAssignableFrom(type)) {
|
||||
return (T) Boolean.valueOf(value);
|
||||
}
|
||||
|
||||
throw new RuntimeException("Unsupported option type '" + type + "'");
|
||||
return arg.substring(Picocli.ARG_PREFIX.length() + option.getKey().length() + 1);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -22,7 +22,10 @@ import io.quarkus.runtime.configuration.QuarkusConfigFactory;
|
|||
import io.quarkus.test.junit.QuarkusMainTestExtension;
|
||||
import io.quarkus.test.junit.main.Launch;
|
||||
import io.quarkus.test.junit.main.LaunchResult;
|
||||
|
||||
import org.junit.jupiter.api.extension.ExtensionContext;
|
||||
import org.junit.jupiter.api.extension.ExtensionContext.Namespace;
|
||||
import org.junit.jupiter.api.extension.ExtensionContext.Store;
|
||||
import org.junit.jupiter.api.extension.ParameterContext;
|
||||
import org.junit.jupiter.api.extension.ParameterResolutionException;
|
||||
import org.junit.jupiter.api.extension.ReflectiveInvocationContext;
|
||||
|
@ -35,20 +38,20 @@ import org.keycloak.quarkus.runtime.cli.command.StartDev;
|
|||
import org.keycloak.quarkus.runtime.configuration.KeycloakPropertiesConfigSource;
|
||||
import org.keycloak.quarkus.runtime.configuration.test.TestConfigArgsConfigSource;
|
||||
import org.keycloak.quarkus.runtime.integration.QuarkusPlatform;
|
||||
import org.testcontainers.containers.GenericContainer;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.Properties;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static java.lang.System.setProperty;
|
||||
import static org.keycloak.it.junit5.extension.DistributionTest.ReInstall.BEFORE_ALL;
|
||||
import static org.keycloak.it.junit5.extension.DistributionType.RAW;
|
||||
import static org.keycloak.quarkus.runtime.Environment.forceTestLaunchMode;
|
||||
|
@ -57,9 +60,9 @@ import static org.keycloak.quarkus.runtime.cli.command.Main.CONFIG_FILE_SHORT_NA
|
|||
|
||||
public class CLITestExtension extends QuarkusMainTestExtension {
|
||||
|
||||
private static final String SYS_PROPS = "sys-props";
|
||||
private static final String KEY_VALUE_SEPARATOR = "[= ]";
|
||||
private KeycloakDistribution dist;
|
||||
private final Set<String> testSysProps = new HashSet<>();
|
||||
private DatabaseContainer databaseContainer;
|
||||
private InfinispanContainer infinispanContainer;
|
||||
private CLIResult result;
|
||||
|
@ -68,6 +71,7 @@ public class CLITestExtension extends QuarkusMainTestExtension {
|
|||
public void beforeEach(ExtensionContext context) throws Exception {
|
||||
DistributionTest distConfig = getDistributionConfig(context);
|
||||
Launch launch = context.getRequiredTestMethod().getAnnotation(Launch.class);
|
||||
getStore(context).put(SYS_PROPS, new HashMap<>(System.getProperties()));
|
||||
|
||||
if (launch != null) {
|
||||
for (String arg : launch.value()) {
|
||||
|
@ -117,6 +121,10 @@ public class CLITestExtension extends QuarkusMainTestExtension {
|
|||
}
|
||||
}
|
||||
|
||||
private Store getStore(ExtensionContext context) {
|
||||
return context.getStore(Namespace.create(context.getRequiredTestClass(), context.getRequiredTestMethod()));
|
||||
}
|
||||
|
||||
private static LegacyStore getLegacyStoreConfig(ExtensionContext context) {
|
||||
return context.getTestClass().get().getDeclaredAnnotation(LegacyStore.class);
|
||||
}
|
||||
|
@ -180,18 +188,15 @@ public class CLITestExtension extends QuarkusMainTestExtension {
|
|||
}
|
||||
|
||||
super.afterEach(context);
|
||||
reset(distConfig);
|
||||
reset(distConfig, context);
|
||||
}
|
||||
|
||||
private void reset(DistributionTest distConfig) {
|
||||
private void reset(DistributionTest distConfig, ExtensionContext context) {
|
||||
QuarkusConfigFactory.setConfig(null);
|
||||
//remove the config file property if set, and also the profile, to not have side effects in other tests.
|
||||
System.getProperties().remove(Environment.PROFILE);
|
||||
System.getProperties().remove("quarkus.profile");
|
||||
HashMap props = getStore(context).remove(SYS_PROPS, HashMap.class);
|
||||
System.getProperties().clear();
|
||||
System.getProperties().putAll(props);
|
||||
TestConfigArgsConfigSource.setCliArgs(new String[0]);
|
||||
for (String property : testSysProps) {
|
||||
System.getProperties().remove(property);
|
||||
}
|
||||
if (databaseContainer != null && databaseContainer.isRunning()) {
|
||||
databaseContainer.stop();
|
||||
databaseContainer = null;
|
||||
|
@ -367,11 +372,6 @@ public class CLITestExtension extends QuarkusMainTestExtension {
|
|||
}
|
||||
}
|
||||
|
||||
private void setProperty(String name, String value) {
|
||||
System.setProperty(name, value);
|
||||
testSysProps.add(name);
|
||||
}
|
||||
|
||||
private List<String> getCliArgs(ExtensionContext context) {
|
||||
Launch annotation = context.getRequiredTestMethod().getAnnotation(Launch.class);
|
||||
|
||||
|
|
Loading…
Reference in a new issue