refinement of propertymapperinterceptor names (#37504)

* fix: generalizing the reporting of names by property mapping

closes: #37503 #37781 #37780

Signed-off-by: Steve Hawkins <shawkins@redhat.com>

* Update quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/NestedPropertyMappingInterceptor.java

Co-authored-by: Václav Muzikář <vaclav@muzikari.cz>
Signed-off-by: Steven Hawkins <shawkins@redhat.com>

* adding more explanation of going from a parent to wildcard values

Signed-off-by: Steve Hawkins <shawkins@redhat.com>

* refining the nested logic and comments

Signed-off-by: Steve Hawkins <shawkins@redhat.com>

* preventing nested expressions from always resolving the mapped value

Signed-off-by: Steve Hawkins <shawkins@redhat.com>

---------

Signed-off-by: Steve Hawkins <shawkins@redhat.com>
Signed-off-by: Steven Hawkins <shawkins@redhat.com>
Co-authored-by: Václav Muzikář <vaclav@muzikari.cz>
This commit is contained in:
Steven Hawkins
2025-03-17 07:48:51 -04:00
committed by GitHub
parent b8e85432a8
commit 2dd783c9c6
20 changed files with 414 additions and 300 deletions

View File

@@ -2,7 +2,6 @@ package org.keycloak.config;
import java.util.List;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

View File

@@ -39,7 +39,6 @@ import java.util.EnumMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Properties;
import java.util.Set;
@@ -382,57 +381,46 @@ public class Picocli {
}
if (options.includeRuntime) {
disabledMappers.addAll(PropertyMappers.getDisabledRuntimeMappers().values());
} else {
checkRuntimeSpiOptions(options, ignoredRunTime);
}
for (OptionCategory category : abstractCommand.getOptionCategories()) {
List<PropertyMapper<?>> mappers = new ArrayList<>(disabledMappers);
var categories = new HashSet<>(abstractCommand.getOptionCategories());
// first validate the advertised property names
// - this allows for efficient resolution of wildcard values and checking spi options
Configuration.getConfig().getPropertyNames().forEach(name -> {
if (!options.includeRuntime) {
checkRuntimeSpiOptions(name, ignoredRunTime);
}
PropertyMapper<?> mapper = PropertyMappers.getMapper(name);
if (mapper == null) {
return; // TODO: need to look for disabled Wildcard mappers
}
if (!categories.contains(mapper.getCategory())) {
return; // not of interest to this command
// TODO: due to picking values up from the env and auto-builds, this probably isn't correct
// - the same issue exists with the second pass
}
String from = mapper.getFrom();
if (!mapper.hasWildcard()) {
return; // non-wildcard options will be validated in the next pass
}
from = mapper.forKey(name).getFrom();
validateProperty(abstractCommand, options, ignoredRunTime, disabledBuildTime, disabledRunTime,
deprecatedInUse, missingOption, disabledMappers, mapper, from);
});
// second pass validate any property mapper not seen in the first pass
// - this will catch required values, anything missing from the property names, or disabled
List<PropertyMapper<?>> mappers = new ArrayList<>(disabledMappers);
for (OptionCategory category : categories) {
Optional.ofNullable(PropertyMappers.getRuntimeMappers().get(category)).ifPresent(mappers::addAll);
Optional.ofNullable(PropertyMappers.getBuildTimeMappers().get(category)).ifPresent(mappers::addAll);
for (PropertyMapper<?> mapper : mappers) {
mapper.getKcConfigValues().forEach(configValue -> {
String configValueStr = configValue.getValue();
}
// don't consider missing or anything below standard env properties
if (configValueStr != null && !isUserModifiable(configValue)) {
return;
}
if (disabledMappers.contains(mapper)) {
if (!PropertyMappers.isDisabledMapper(mapper.getFrom())) {
return; // we found enabled mapper with the same name
}
// only check build-time for a rebuild, we'll check the runtime later
if (configValueStr != null && (!mapper.isRunTime() || !isRebuild())) {
if (PropertyMapper.isCliOption(configValue)) {
throw new KcUnmatchedArgumentException(abstractCommand.getCommandLine().orElseThrow(), List.of(mapper.getCliFormat()));
} else {
handleDisabled(mapper.isRunTime() ? disabledRunTime : disabledBuildTime, mapper);
}
}
return;
}
if (mapper.isRunTime() && !options.includeRuntime) {
if (configValueStr != null) {
ignoredRunTime.add(mapper.getFrom());
}
return;
}
if (configValueStr == null) {
if (mapper.isRequired()) {
handleRequired(missingOption, mapper);
}
return;
}
mapper.validate(configValue);
mapper.getDeprecatedMetadata().ifPresent(metadata -> handleDeprecated(deprecatedInUse, mapper, configValueStr, metadata));
});
for (PropertyMapper<?> mapper : mappers) {
if (!mapper.hasWildcard()) {
validateProperty(abstractCommand, options, ignoredRunTime, disabledBuildTime, disabledRunTime,
deprecatedInUse, missingOption, disabledMappers, mapper, mapper.getFrom());
}
}
@@ -462,27 +450,72 @@ public class Picocli {
}
}
private void validateProperty(AbstractCommand abstractCommand, IncludeOptions options,
final List<String> ignoredRunTime, final Set<String> disabledBuildTime, final Set<String> disabledRunTime,
final Set<String> deprecatedInUse, final Set<String> missingOption,
final Set<PropertyMapper<?>> disabledMappers, PropertyMapper<?> mapper, String from) {
ConfigValue configValue = Configuration.getConfigValue(from);
String configValueStr = configValue.getValue();
// don't consider missing or anything below standard env properties
if (configValueStr != null && !isUserModifiable(configValue)) {
return;
}
if (disabledMappers.contains(mapper)) {
if (!PropertyMappers.isDisabledMapper(from)) {
return; // we found enabled mapper with the same name
}
// only check build-time for a rebuild, we'll check the runtime later
if (configValueStr != null && (!mapper.isRunTime() || !isRebuild())) {
if (PropertyMapper.isCliOption(configValue)) {
throw new KcUnmatchedArgumentException(abstractCommand.getCommandLine().orElseThrow(), List.of(mapper.getCliFormat()));
} else {
handleDisabled(mapper.isRunTime() ? disabledRunTime : disabledBuildTime, mapper);
}
}
return;
}
if (mapper.isRunTime() && !options.includeRuntime) {
if (configValueStr != null) {
ignoredRunTime.add(mapper.getFrom());
}
return;
}
if (configValueStr == null) {
if (mapper.isRequired()) {
handleRequired(missingOption, mapper);
}
return;
}
mapper.validate(configValue);
mapper.getDeprecatedMetadata().ifPresent(metadata -> handleDeprecated(deprecatedInUse, mapper, configValueStr, metadata));
}
private static boolean isUserModifiable(ConfigValue configValue) {
// This could check as low as SysPropConfigSource DEFAULT_ORDINAL, which is 400
// for now we won't validate these as it's not expected for the user to specify options via system properties
return configValue.getConfigSourceOrdinal() >= KeycloakPropertiesConfigSource.PROPERTIES_FILE_ORDINAL;
}
private static void checkRuntimeSpiOptions(IncludeOptions options, final List<String> ignoredRunTime) {
for (String key : Configuration.getConfig().getPropertyNames()) {
if (!key.startsWith(PropertyMappers.KC_SPI_PREFIX)) {
continue;
}
boolean buildTimeOption = PropertyMappers.isSpiBuildTimeProperty(key);
private static void checkRuntimeSpiOptions(String key, final List<String> ignoredRunTime) {
if (!key.startsWith(PropertyMappers.KC_SPI_PREFIX)) {
return;
}
boolean buildTimeOption = PropertyMappers.isSpiBuildTimeProperty(key);
if (!buildTimeOption) {
ConfigValue configValue = Configuration.getConfigValue(key);
String configValueStr = configValue.getValue();
if (!buildTimeOption) {
ConfigValue configValue = Configuration.getConfigValue(key);
String configValueStr = configValue.getValue();
// don't consider missing or anything below standard env properties
if (configValueStr != null && isUserModifiable(configValue)) {
ignoredRunTime.add(key);
}
// don't consider missing or anything below standard env properties
if (configValueStr != null && isUserModifiable(configValue)) {
ignoredRunTime.add(key);
}
}
}

View File

@@ -125,20 +125,6 @@ public class ConfigArgsConfigSource extends PropertiesConfigSource {
key = NS_KEYCLOAK_PREFIX + key.substring(2);
properties.put(key, value);
PropertyMapper<?> mapper = PropertyMappers.getMapper(key);
if (mapper != null) {
mapper = mapper.forKey(key);
String to = mapper.getTo();
if (to != null) {
properties.put(mapper.getTo(), value);
}
properties.put(mapper.getFrom(), value);
}
}
}, ignored -> {});

View File

@@ -28,8 +28,6 @@ import io.smallrye.config.ConfigValue;
import io.smallrye.config.SmallRyeConfig;
import org.keycloak.config.Option;
import org.keycloak.quarkus.runtime.configuration.mappers.PropertyMapper;
import org.keycloak.quarkus.runtime.configuration.mappers.PropertyMappers;
import org.keycloak.utils.StringUtil;
import static org.keycloak.quarkus.runtime.configuration.MicroProfileConfigProvider.NS_KEYCLOAK_PREFIX;
@@ -153,17 +151,6 @@ public final class Configuration {
return getConfig().getOptionalValue(NS_KEYCLOAK_PREFIX.concat(propertyName), Integer.class);
}
public static String getMappedPropertyName(String key) {
PropertyMapper<?> mapper = PropertyMappers.getMapper(key);
if (mapper == null) {
return key;
}
// we also need to make sure the target property is available when defined such as when defining alias for provider config (no spi-prefix).
return mapper.getTo() == null ? mapper.getFrom() : mapper.getTo();
}
public static String toEnvVarFormat(String key) {
return replaceNonAlphanumericByUnderscores(key).toUpperCase();
}

View File

@@ -130,11 +130,6 @@ public class IgnoredArtifacts {
}
});
// since the default may not be a known property name, look for it explicitly
Configuration.getOptionalValue("quarkus.datasource.db-kind")
.flatMap(Database::getVendor)
.ifPresent(vendorsOfAllDatasources::add);
final Set<String> jdbcArtifacts = vendorsOfAllDatasources.stream()
.map(vendor -> switch (vendor) {
case H2 -> JDBC_H2;

View File

@@ -24,6 +24,7 @@ import java.util.HashMap;
import java.util.Map;
import io.smallrye.config.PropertiesConfigSource;
import org.keycloak.quarkus.runtime.configuration.mappers.PropertyMapper;
import org.keycloak.quarkus.runtime.configuration.mappers.PropertyMappers;
@@ -50,13 +51,7 @@ public class KcEnvConfigSource extends PropertiesConfigSource {
PropertyMapper<?> mapper = PropertyMappers.getMapper(key);
if (mapper != null) {
mapper = mapper.forEnvKey(key);
String to = mapper.getTo();
if (to != null) {
properties.put(to, value);
}
mapper = mapper.forKey(key);
properties.put(mapper.getFrom(), value);
}

View File

@@ -17,6 +17,9 @@
package org.keycloak.quarkus.runtime.configuration;
import static org.keycloak.quarkus.runtime.configuration.MicroProfileConfigProvider.NS_KEYCLOAK;
import static org.keycloak.quarkus.runtime.configuration.MicroProfileConfigProvider.NS_KEYCLOAK_PREFIX;
import java.io.File;
import java.io.IOException;
import java.net.URI;
@@ -33,23 +36,16 @@ import java.util.regex.Pattern;
import org.eclipse.microprofile.config.spi.ConfigSource;
import org.eclipse.microprofile.config.spi.ConfigSourceProvider;
import org.keycloak.quarkus.runtime.Environment;
import org.keycloak.quarkus.runtime.configuration.mappers.PropertyMapper;
import org.keycloak.quarkus.runtime.configuration.mappers.PropertyMappers;
import io.smallrye.config.AbstractLocationConfigSourceLoader;
import io.smallrye.config.PropertiesConfigSource;
import io.smallrye.config.common.utils.ConfigSourceUtil;
import static org.keycloak.quarkus.runtime.configuration.Configuration.getMappedPropertyName;
import static org.keycloak.quarkus.runtime.configuration.MicroProfileConfigProvider.NS_KEYCLOAK;
import static org.keycloak.quarkus.runtime.configuration.MicroProfileConfigProvider.NS_KEYCLOAK_PREFIX;
import static org.keycloak.quarkus.runtime.configuration.MicroProfileConfigProvider.NS_QUARKUS;
/**
* A configuration source for {@code keycloak.conf}.
*/
public class KeycloakPropertiesConfigSource extends AbstractLocationConfigSourceLoader {
public static final int PROPERTIES_FILE_ORDINAL = 475;
private static final Pattern DOT_SPLIT = Pattern.compile("\\.");
@@ -122,8 +118,9 @@ public class KeycloakPropertiesConfigSource extends AbstractLocationConfigSource
public Path getConfigurationFile() {
String filePath = System.getProperty(KEYCLOAK_CONFIG_FILE_PROP);
if (filePath == null)
if (filePath == null) {
filePath = System.getenv(KEYCLOAK_CONFIG_FILE_ENV);
}
if (filePath == null) {
String homeDir = Environment.getHomeDir();
@@ -147,22 +144,10 @@ public class KeycloakPropertiesConfigSource extends AbstractLocationConfigSource
private static Map<String, String> transform(Map<String, String> properties) {
Map<String, String> result = new HashMap<>(properties.size());
properties.keySet().forEach(k -> {
String key = transformKey(k);
PropertyMapper<?> mapper = PropertyMappers.getMapper(key);
//TODO: remove explicit checks for spi and feature options once we have proper support in our config mappers
if (mapper != null
|| key.contains(NS_KEYCLOAK_PREFIX + "spi")
|| key.contains(NS_KEYCLOAK_PREFIX + "feature")) {
String value = properties.get(k);
result.put(key, value);
if (mapper != null && key.charAt(0) != '%') {
result.put(getMappedPropertyName(key), value);
}
}
properties.entrySet().forEach(entry -> {
String key = transformKey(entry.getKey());
result.put(key, entry.getValue());
});
return result;
@@ -176,7 +161,7 @@ public class KeycloakPropertiesConfigSource extends AbstractLocationConfigSource
* @return the same key but prefixed with the namespace
*/
private static String transformKey(String key) {
String namespace;
String namespace = NS_KEYCLOAK;
String[] keyParts = DOT_SPLIT.split(key);
String extension = keyParts[0];
String profile = "";
@@ -188,12 +173,6 @@ public class KeycloakPropertiesConfigSource extends AbstractLocationConfigSource
transformed = key.substring(key.indexOf('.') + 1);
}
if (extension.equalsIgnoreCase(NS_QUARKUS)) {
return key;
} else {
namespace = NS_KEYCLOAK;
}
return profile + namespace + "." + transformed;
}

View File

@@ -0,0 +1,80 @@
/*
* Copyright 2021 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.quarkus.runtime.configuration;
import java.util.LinkedHashSet;
import java.util.Optional;
import java.util.function.Function;
import org.keycloak.quarkus.runtime.configuration.mappers.PropertyMappers;
import io.smallrye.config.ConfigSourceInterceptor;
import io.smallrye.config.ConfigSourceInterceptorContext;
import io.smallrye.config.ConfigValue;
import io.smallrye.config.Priorities;
import jakarta.annotation.Priority;
/**
* Some resolution of values that come from PropertyMappers
* happens at the ExpressionConfigSourceInterceptor, which is after
* property mapping. This interceptor appears just after the expression
* interceptor and will restart the context for anything not actively recursing.
* This is needed in case the expression contains something that requires property mapping.
*/
@Priority(Priorities.LIBRARY + 299)
public class NestedPropertyMappingInterceptor implements ConfigSourceInterceptor {
static final ThreadLocal<LinkedHashSet<String>> recursions = new ThreadLocal<>();
@Override
public ConfigValue getValue(ConfigSourceInterceptorContext context, String name) {
// don't look up the mapped value for direct env references
if (Character.isUpperCase(name.charAt(0))) {
return context.proceed(name);
}
return resolve(context::restart, context::proceed, name, false);
}
private static <T> T resolve(Function<String, T> resolver, Function<String, T> nonRecursiveResolver, String name, boolean startNew) {
LinkedHashSet<String> recursing = recursions.get();
if (recursing == null && startNew) {
recursing = new LinkedHashSet<String>();
recursions.set(recursing);
}
if (recursing != null && recursing.add(name)) {
try {
return resolver.apply(name);
} finally {
recursing.remove(name);
if (recursing.isEmpty()) {
recursions.set(null);
}
}
}
return nonRecursiveResolver.apply(name);
}
public static Optional<String> getResolvingRoot() {
return Optional.ofNullable(recursions.get()).filter(s -> !s.isEmpty()).map(s -> s.iterator().next());
}
public static ConfigValue getValueFromPropertyMappers(ConfigSourceInterceptorContext context, String name) {
Function<String, ConfigValue> resolver = (n) -> PropertyMappers.getValue(context, n);
return resolve(resolver, resolver, name, true);
}
}

View File

@@ -16,24 +16,25 @@
*/
package org.keycloak.quarkus.runtime.configuration;
import io.smallrye.config.ConfigSourceInterceptor;
import io.smallrye.config.ConfigSourceInterceptorContext;
import io.smallrye.config.ConfigValue;
import static org.keycloak.quarkus.runtime.Environment.isRebuild;
import java.util.Iterator;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
import io.smallrye.config.Priorities;
import jakarta.annotation.Priority;
import org.apache.commons.collections4.IteratorUtils;
import org.apache.commons.collections4.iterators.FilterIterator;
import org.keycloak.config.OptionCategory;
import org.keycloak.quarkus.runtime.Environment;
import org.keycloak.quarkus.runtime.configuration.mappers.PropertyMapper;
import org.keycloak.quarkus.runtime.configuration.mappers.PropertyMappers;
import org.keycloak.quarkus.runtime.configuration.mappers.WildcardPropertyMapper;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import static org.keycloak.quarkus.runtime.Environment.isRebuild;
import io.smallrye.config.ConfigSourceInterceptor;
import io.smallrye.config.ConfigSourceInterceptorContext;
import io.smallrye.config.ConfigValue;
import io.smallrye.config.Priorities;
import jakarta.annotation.Priority;
/**
* <p>This interceptor is responsible for mapping Keycloak properties to their corresponding properties in Quarkus.
@@ -54,7 +55,6 @@ import static org.keycloak.quarkus.runtime.Environment.isRebuild;
public class PropertyMappingInterceptor implements ConfigSourceInterceptor {
private static final ThreadLocal<Boolean> disable = new ThreadLocal<>();
private static final ThreadLocal<Boolean> disableAdditionalNames = new ThreadLocal<>();
public static void disable() {
disable.set(true);
@@ -64,40 +64,74 @@ public class PropertyMappingInterceptor implements ConfigSourceInterceptor {
disable.remove();
}
static Iterator<String> filterRuntime(Iterator<String> iter) {
if (!isRebuild() && !Environment.isRebuildCheck()) {
return iter;
}
return new FilterIterator<>(iter, item -> !isRuntime(item));
}
static boolean isRuntime(String name) {
PropertyMapper<?> mapper = PropertyMappers.getMapper(name);
return mapper != null && mapper.isRunTime();
}
/**
* Provides a curated iteration of names based upon the mapping logic.
* Quarkus logic, such as config mapping, is dependent upon seeing the quarkus
* form of the key. We want to expose that here, rather than in the config sources
* because we lack a simple way to do name mapping for some sources, such as the
* keystore config source.
* <p>
* We currently expose:
* <li>anything based upon a property mapper that has a map to a quarkus property - including
* our kc. properties that have defaults.
* <li>wildcard key names for wildcard keys that map from a keycloak property (e.g. kc.log-level)
*
* We selectively exclude:
* <li>Config keystore properties at build time
*/
@Override
public Iterator<String> iterateNames(ConfigSourceInterceptorContext context) {
// We need to iterate through names to get wildcard option names.
// Additionally, wildcardValuesTransformer might also trigger iterateNames.
// Hence we need to disable this to prevent infinite recursion.
// But we don't want to disable the whole interceptor, as wildcardValuesTransformer
// might still need mappers to work.
List<String> mappedWildcardNames = List.of();
if (!Boolean.TRUE.equals(disableAdditionalNames.get())) {
disableAdditionalNames.set(true);
try {
mappedWildcardNames = PropertyMappers.getWildcardMappers().stream()
.map(WildcardPropertyMapper::getToWithWildcards)
.flatMap(Set::stream)
.toList();
} finally {
disableAdditionalNames.remove();
}
}
Iterable<String> iterable = () -> context.iterateNames();
// this could be optimized by filtering the wildcard names in the stream above
return filterRuntime(IteratorUtils.chainedIterator(mappedWildcardNames.iterator(), context.iterateNames()));
final Set<PropertyMapper<?>> allMappers = PropertyMappers.getMappers();
//TODO: this is still not a complete list - things like quarkus.log.console.enabled
// come from kc.log - but via a map from, not to.
// so we'd need additional logic like the getWildcardMappedFrom case for that
boolean filterRuntime = isRebuild() || Environment.isRebuildCheck();
var baseStream = StreamSupport.stream(iterable.spliterator(), false).flatMap(name -> {
PropertyMapper<?> mapper = PropertyMappers.getMapper(name);
if (mapper == null) {
return Stream.of(name);
}
if (filterRuntime && mapper.getCategory() == OptionCategory.CONFIG) {
return Stream.of(); // advertising the keystore type causes the keystore to be used early
}
allMappers.remove(mapper);
if (!mapper.hasWildcard()) {
// this is not a wildcard value, but may map to wildcards
// the current example is something like log-level=wildcardCat1:level,wildcardCat2:level
var wildCard = PropertyMappers.getWildcardMappedFrom(mapper.getOption());
if (wildCard != null) {
ConfigValue value = context.proceed(name);
if (value != null && value.getValue() != null) {
return Stream.concat(Stream.of(name), wildCard.getToFromWildcardTransformer(value.getValue()));
}
}
}
mapper = mapper.forKey(name);
// there is a corner case here: -1 for the reload period has no 'to' value.
// if that becomes an issue we could use more metadata to perform a full mapping
return toDistinctStream(name, mapper.getTo());
});
// include anything remaining that has a default value
var defaultStream = allMappers.stream()
.filter(m -> !m.getDefaultValue().isEmpty() && !m.hasWildcard()
&& m.getCategory() != OptionCategory.CONFIG) // advertising the keystore type causes the keystore to be used early
.flatMap(m -> toDistinctStream(m.getTo()));
return IteratorUtils.chainedIterator(baseStream.iterator(), defaultStream.iterator());
}
private static Stream<String> toDistinctStream(String... values) {
return Stream.of(values).filter(Objects::nonNull).distinct();
}
@Override
@@ -105,6 +139,8 @@ public class PropertyMappingInterceptor implements ConfigSourceInterceptor {
if (Boolean.TRUE.equals(disable.get())) {
return context.proceed(name);
}
return PropertyMappers.getValue(context, name);
// Call through NestedPropertyMappingInterceptor to track what we are currently getting the value for
return NestedPropertyMappingInterceptor.getValueFromPropertyMappers(context, name);
}
}

View File

@@ -1,15 +1,15 @@
package org.keycloak.quarkus.runtime.configuration.mappers;
import io.smallrye.config.ConfigSourceInterceptorContext;
import io.smallrye.config.ConfigValue;
import org.keycloak.config.ConfigKeystoreOptions;
import org.keycloak.quarkus.runtime.configuration.MicroProfileConfigProvider;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.keycloak.quarkus.runtime.configuration.mappers.PropertyMapper.fromOption;
final class ConfigKeystorePropertyMappers {
public final class ConfigKeystorePropertyMappers {
private static final String SMALLRYE_KEYSTORE_PATH = "smallrye.config.source.keystore.kc-default.path";
private static final String SMALLRYE_KEYSTORE_PASSWORD = "smallrye.config.source.keystore.kc-default.password";
@@ -38,18 +38,16 @@ final class ConfigKeystorePropertyMappers {
}
private static String validatePath(String option, ConfigSourceInterceptorContext context) {
ConfigValue path = context.proceed(SMALLRYE_KEYSTORE_PATH);
boolean isPasswordDefined = context.proceed(SMALLRYE_KEYSTORE_PASSWORD) != null;
if (path == null) {
throw new IllegalArgumentException("config-keystore must be specified");
if (option == null) {
return null;
}
boolean isPasswordDefined = context.proceed(MicroProfileConfigProvider.NS_KEYCLOAK_PREFIX + ConfigKeystoreOptions.CONFIG_KEYSTORE_PASSWORD.getKey()) != null;
if (!isPasswordDefined) {
throw new IllegalArgumentException("config-keystore-password must be specified");
}
final Path realPath = Path.of(path.getValue()).toAbsolutePath().normalize();
final Path realPath = Path.of(option).toAbsolutePath().normalize();
if (!Files.exists(realPath)) {
throw new IllegalArgumentException("config-keystore path does not exist: " + realPath);
}
@@ -58,12 +56,10 @@ final class ConfigKeystorePropertyMappers {
}
private static String validatePassword(String option, ConfigSourceInterceptorContext context) {
boolean isPasswordDefined = context.proceed(SMALLRYE_KEYSTORE_PASSWORD).getValue() != null;
boolean isPathDefined = context.proceed(SMALLRYE_KEYSTORE_PATH) != null;
if (!isPasswordDefined) {
throw new IllegalArgumentException("config-keystore-password must be specified");
if (option == null) {
return null;
}
boolean isPathDefined = context.proceed(MicroProfileConfigProvider.NS_KEYCLOAK_PREFIX + ConfigKeystoreOptions.CONFIG_KEYSTORE.getKey()) != null;
if (!isPathDefined) {
throw new IllegalArgumentException("config-keystore must be specified");

View File

@@ -127,7 +127,7 @@ public final class LoggingPropertyMappers {
.to("quarkus.log.category.\"<categories>\".level")
.validator(LoggingPropertyMappers::validateCategoryLogLevel)
.wildcardKeysTransformer(LoggingPropertyMappers::getConfiguredLogCategories)
.transformer((v,c) -> toLevel(v).getName())
.transformer((v,c) -> v == null ? null : toLevel(v).getName())
.wildcardMapFrom(LoggingOptions.LOG_LEVEL, LoggingPropertyMappers::resolveCategoryLogLevelFromParentLogLevelOption) // a fallback to log-level
.paramLabel("level")
.build(),
@@ -267,8 +267,8 @@ public final class LoggingPropertyMappers {
return DEFAULT_ROOT_LOG_LEVEL; // defaults are not resolved in the mapper if transformer is present, so doing it explicitly here
}
private static Set<String> getConfiguredLogCategories(Set<String> categories) {
for (CategoryLevel categoryLevel : parseRootLogLevel(null)) {
private static Set<String> getConfiguredLogCategories(String value, Set<String> categories) {
for (CategoryLevel categoryLevel : parseRootLogLevel(value)) {
if (categoryLevel.category != null) {
categories.add(categoryLevel.category);
}

View File

@@ -18,7 +18,6 @@ package org.keycloak.quarkus.runtime.configuration.mappers;
import static java.util.Optional.ofNullable;
import static org.keycloak.config.Option.WILDCARD_PLACEHOLDER_PATTERN;
import static org.keycloak.quarkus.runtime.Environment.isRebuild;
import static org.keycloak.quarkus.runtime.configuration.Configuration.OPTION_PART_SEPARATOR;
import static org.keycloak.quarkus.runtime.configuration.Configuration.OPTION_PART_SEPARATOR_CHAR;
import static org.keycloak.quarkus.runtime.configuration.Configuration.toCliFormat;
@@ -34,7 +33,6 @@ import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.BooleanSupplier;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Stream;
import io.smallrye.config.ConfigSourceInterceptorContext;
@@ -45,14 +43,11 @@ import io.smallrye.config.Expressions;
import org.keycloak.config.DeprecatedMetadata;
import org.keycloak.config.Option;
import org.keycloak.config.OptionCategory;
import org.keycloak.quarkus.runtime.Environment;
import org.keycloak.quarkus.runtime.cli.PropertyException;
import org.keycloak.quarkus.runtime.cli.ShortErrorMessageHandler;
import org.keycloak.quarkus.runtime.configuration.ConfigArgsConfigSource;
import org.keycloak.quarkus.runtime.configuration.Configuration;
import org.keycloak.quarkus.runtime.configuration.KcEnvConfigSource;
import org.keycloak.quarkus.runtime.configuration.KeycloakConfigSourceProvider;
import org.keycloak.quarkus.runtime.configuration.MicroProfileConfigProvider;
import org.keycloak.utils.StringUtil;
public class PropertyMapper<T> {
@@ -103,10 +98,6 @@ public class PropertyMapper<T> {
this.parentMapper = parentMapper;
}
ConfigValue getConfigValue(ConfigSourceInterceptorContext context) {
return getConfigValue(to, context);
}
ConfigValue getConfigValue(String name, ConfigSourceInterceptorContext context) {
String from = getFrom();
@@ -123,7 +114,7 @@ public class PropertyMapper<T> {
// if the property we want to map depends on another one, we use the value from the other property to call the mapper
// not getting the value directly from SmallRye Config to avoid the risk of infinite recursion when Config is initializing
String mapFromWithPrefix = NS_KEYCLOAK_PREFIX + mapFrom;
config = PropertyMappers.getMapper(mapFromWithPrefix).getConfigValue(mapFromWithPrefix, context);
config = context.restart(mapFromWithPrefix);
parentValue = true;
}
@@ -293,7 +284,7 @@ public class PropertyMapper<T> {
String map(String name, String value, ConfigSourceInterceptorContext context);
}
private final class ContextWrapper implements ConfigSourceInterceptorContext {
private static final class ContextWrapper implements ConfigSourceInterceptorContext {
private final ConfigSourceInterceptorContext context;
private final ConfigValue value;
@@ -336,7 +327,7 @@ public class PropertyMapper<T> {
private String description;
private BooleanSupplier isRequired = () -> false;
private String requiredWhen = "";
private Function<Set<String>, Set<String>> wildcardKeysTransformer;
private BiFunction<String, Set<String>, Set<String>> wildcardKeysTransformer;
private ValueMapper wildcardMapFrom;
public Builder(Option<T> option) {
@@ -462,7 +453,7 @@ public class PropertyMapper<T> {
return this;
}
public Builder<T> wildcardKeysTransformer(Function<Set<String>, Set<String>> wildcardValuesTransformer) {
public Builder<T> wildcardKeysTransformer(BiFunction<String, Set<String>, Set<String>> wildcardValuesTransformer) {
this.wildcardKeysTransformer = wildcardValuesTransformer;
return this;
}
@@ -567,25 +558,6 @@ public class PropertyMapper<T> {
KeycloakConfigSourceProvider.getConfigSourceDisplayName(configValue.getConfigSourceName()));
}
/**
* Get all Keycloak config values for the mapper. A multivalued config option is a config option that
* has a wildcard in its name, e.g. log-level-<category>.
*
* @return a list of config values where the key is the resolved wildcard (e.g. category) and the value is the config value
*/
public List<ConfigValue> getKcConfigValues() {
return List.of(Configuration.getConfigValue(getFrom()));
}
/**
* Returns a new PropertyMapper tailored for the given env var key.
* This is currently useful in {@link WildcardPropertyMapper} where "to" and "from" fields need to include a specific
* wildcard key.
*/
public PropertyMapper<?> forEnvKey(String key) {
return this;
}
/**
* Returns a new PropertyMapper tailored for the given key.
* This is currently useful in {@link WildcardPropertyMapper} where "to" and "from" fields need to include a specific
@@ -595,4 +567,8 @@ public class PropertyMapper<T> {
return this;
}
String getMapFrom() {
return mapFrom;
}
}

View File

@@ -7,6 +7,7 @@ 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.Option;
import org.keycloak.config.OptionCategory;
import org.keycloak.quarkus.runtime.Environment;
import org.keycloak.quarkus.runtime.cli.PropertyException;
@@ -15,6 +16,7 @@ import org.keycloak.quarkus.runtime.cli.command.Build;
import org.keycloak.quarkus.runtime.cli.command.ShowConfig;
import org.keycloak.quarkus.runtime.configuration.DisabledMappersInterceptor;
import org.keycloak.quarkus.runtime.configuration.MicroProfileConfigProvider;
import org.keycloak.quarkus.runtime.configuration.NestedPropertyMappingInterceptor;
import org.keycloak.quarkus.runtime.configuration.PersistedConfigSource;
import java.util.ArrayList;
@@ -24,6 +26,7 @@ import java.util.EnumMap;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@@ -79,12 +82,13 @@ public final class PropertyMappers {
name = removeProfilePrefixIfNeeded(name);
PropertyMapper<?> mapper = getMapper(name);
// During re-aug do not resolve the server runtime properties and avoid they included by quarkus in the default value config source.
// During re-aug do not resolve server runtime properties and avoid they included by quarkus in the default value config source.
//
// The special handling of log properties is because some logging runtime properties are requested during build time
// and we need to resolve them. That should be fine as they are generally not considered security sensitive.
// See https://github.com/quarkusio/quarkus/pull/42157
if ((isRebuild() || Environment.isRebuildCheck()) && isKeycloakRuntime(name, mapper) && !name.startsWith("quarkus.log.")) {
if ((isRebuild() || Environment.isRebuildCheck()) && isKeycloakRuntime(name, mapper)
&& !NestedPropertyMappingInterceptor.getResolvingRoot().orElse(name).startsWith("quarkus.log.")) {
return ConfigValue.builder().withName(name).build();
}
@@ -182,14 +186,21 @@ public final class PropertyMappers {
return getMapper(property, null);
}
/**
* @return a mutable copy of all known mappers
*/
public static Set<PropertyMapper<?>> getMappers() {
return MAPPERS.values().stream().flatMap(Collection::stream).collect(Collectors.toSet());
return MAPPERS.values().stream().flatMap(Collection::stream).collect(Collectors.toCollection(LinkedHashSet::new));
}
public static Set<WildcardPropertyMapper<?>> getWildcardMappers() {
return MAPPERS.getWildcardMappers();
}
public static WildcardPropertyMapper<?> getWildcardMappedFrom(Option<?> from) {
return MAPPERS.wildcardMapFrom.get(from.getKey());
}
public static boolean isSupported(PropertyMapper<?> mapper) {
ConfigSupportLevel supportLevel = mapper.getCategory().getSupportLevel();
return supportLevel.equals(ConfigSupportLevel.SUPPORTED) || supportLevel.equals(ConfigSupportLevel.DEPRECATED);
@@ -232,7 +243,9 @@ public final class PropertyMappers {
private final Map<String, PropertyMapper<?>> disabledBuildTimeMappers = new HashMap<>();
private final Map<String, PropertyMapper<?>> disabledRuntimeMappers = new HashMap<>();
private final Set<WildcardPropertyMapper<?>> wildcardMappers = new HashSet<>();
private final Map<String, WildcardPropertyMapper<?>> wildcardMapFrom = new HashMap<>();
public void addAll(PropertyMapper<?>[] mappers) {
for (PropertyMapper<?> mapper : mappers) {
@@ -252,13 +265,22 @@ public final class PropertyMappers {
public void addMapper(PropertyMapper<?> mapper) {
if (mapper.hasWildcard()) {
if (mapper.getMapFrom() != null) {
wildcardMapFrom.put(mapper.getMapFrom(), (WildcardPropertyMapper<?>) mapper);
}
wildcardMappers.add((WildcardPropertyMapper<?>)mapper);
} else {
handleMapper(mapper, this::add);
}
handleMapper(mapper, this::add);
}
public void removeMapper(PropertyMapper<?> mapper) {
wildcardMappers.remove(mapper);
if (mapper.hasWildcard()) {
wildcardMappers.remove(mapper);
if (mapper.getFrom() != null) {
wildcardMapFrom.remove(mapper.getMapFrom());
}
}
handleMapper(mapper, this::remove);
}
@@ -272,17 +294,23 @@ public final class PropertyMappers {
@Override
@SuppressWarnings({"rawtypes", "unchecked"})
public List<PropertyMapper<?>> get(Object key) {
// First check if the requested option matches any wildcard mappers
// First check the base mappings
String strKey = (String) key;
List ret = wildcardMappers.stream()
List ret = super.get(key);
if (ret != null) {
return ret;
}
// TODO: we may want to introduce a prefix tree here as we add more wildcardMappers
ret = wildcardMappers.stream()
.filter(m -> m.matchesWildcardOptionName(strKey))
.toList();
if (!ret.isEmpty()) {
return ret;
}
// If no wildcard mappers match, check for exact matches
return super.get(key);
return null;
}
@Override
@@ -295,7 +323,9 @@ public final class PropertyMappers {
}
public void sanitizeDisabledMappers() {
if (Environment.getParsedCommand().isEmpty()) return; // do not sanitize when no command is present
if (Environment.getParsedCommand().isEmpty()) {
return; // do not sanitize when no command is present
}
DisabledMappersInterceptor.runWithDisabled(() -> { // We need to have the whole configuration available
@@ -377,4 +407,5 @@ public final class PropertyMappers {
operation.accept(mapper.getEnvVarFormat(), mapper);
}
}
}

View File

@@ -3,20 +3,18 @@ package org.keycloak.quarkus.runtime.configuration.mappers;
import static org.keycloak.config.Option.WILDCARD_PLACEHOLDER_PATTERN;
import static org.keycloak.quarkus.runtime.cli.Picocli.ARG_PREFIX;
import java.util.List;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.BooleanSupplier;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
import java.util.stream.Stream;
import org.keycloak.config.Option;
import org.keycloak.quarkus.runtime.configuration.Configuration;
import org.keycloak.quarkus.runtime.cli.Picocli;
import org.keycloak.quarkus.runtime.configuration.MicroProfileConfigProvider;
import io.smallrye.config.ConfigSourceInterceptorContext;
@@ -29,14 +27,14 @@ public class WildcardPropertyMapper<T> extends PropertyMapper<T> {
private final Pattern envVarNameWildcardPattern;
private Matcher toWildcardMatcher;
private Pattern toWildcardPattern;
private final Function<Set<String>, Set<String>> wildcardKeysTransformer;
private final BiFunction<String, Set<String>, Set<String>> wildcardKeysTransformer;
private final ValueMapper wildcardMapFrom;
public WildcardPropertyMapper(Option<T> option, String to, BooleanSupplier enabled, String enabledWhen,
BiFunction<String, ConfigSourceInterceptorContext, String> mapper,
String mapFrom, BiFunction<String, ConfigSourceInterceptorContext, String> parentMapper,
String paramLabel, boolean mask, BiConsumer<PropertyMapper<T>, ConfigValue> validator,
String description, BooleanSupplier required, String requiredWhen, Matcher fromWildcardMatcher, Function<Set<String>, Set<String>> wildcardKeysTransformer, ValueMapper wildcardMapFrom) {
String description, BooleanSupplier required, String requiredWhen, Matcher fromWildcardMatcher, BiFunction<String, Set<String>, Set<String>> wildcardKeysTransformer, ValueMapper wildcardMapFrom) {
super(option, to, enabled, enabledWhen, mapper, mapFrom, parentMapper, paramLabel, mask, validator, description, required, requiredWhen, null);
this.wildcardMapFrom = wildcardMapFrom;
this.fromWildcardMatcher = fromWildcardMatcher;
@@ -72,25 +70,11 @@ public class WildcardPropertyMapper<T> extends PropertyMapper<T> {
return MicroProfileConfigProvider.NS_KEYCLOAK_PREFIX + fromWildcardMatcher.replaceFirst(wildcardKey);
}
@Override
public List<ConfigValue> getKcConfigValues() {
return this.getWildcardKeys().stream().map(v -> Configuration.getConfigValue(getFrom(v))).toList();
}
public Set<String> getWildcardKeys() {
// this is not optimal
// TODO find an efficient way to get all values that match the wildcard
Set<String> values = StreamSupport.stream(Configuration.getPropertyNames().spliterator(), false)
.map(n -> getMappedKey(n, false))
.filter(Optional::isPresent)
.map(Optional::get)
.collect(Collectors.toSet());
if (wildcardKeysTransformer != null) {
return wildcardKeysTransformer.apply(values);
public Stream<String> getToFromWildcardTransformer(String value) {
if (wildcardKeysTransformer == null) {
return Stream.empty();
}
return values;
return wildcardKeysTransformer.apply(value, new HashSet<String>()).stream().map(toWildcardMatcher::replaceFirst);
}
/**
@@ -99,50 +83,39 @@ public class WildcardPropertyMapper<T> extends PropertyMapper<T> {
* E.g. for the option "log-level-<category>" and the option name "log-level-io.quarkus",
* the wildcard value would be "io.quarkus".
*/
private Optional<String> getMappedKey(String originalKey, boolean tryTo) {
Matcher matcher = fromWildcardPattern.matcher(originalKey);
if (matcher.matches()) {
return Optional.of(matcher.group(1));
private Optional<String> getMappedKey(String originalKey) {
Matcher matcher = null;
if (originalKey.startsWith(Picocli.ARG_PREFIX) || originalKey.startsWith(MicroProfileConfigProvider.NS_KEYCLOAK_PREFIX)) {
matcher = fromWildcardPattern.matcher(originalKey);
if (matcher.matches()) {
return Optional.of(matcher).map(m -> m.group(1));
}
}
if (tryTo && toWildcardPattern != null) {
if (toWildcardPattern != null) {
matcher = toWildcardPattern.matcher(originalKey);
if (matcher.matches()) {
return Optional.of(matcher.group(1));
return Optional.of(matcher).map(m -> m.group(1));
}
}
if (originalKey.startsWith("KC_")) {
matcher = envVarNameWildcardPattern.matcher(originalKey);
if (matcher.matches()) {
// we opiniotatedly convert env var names to CLI format with dots
return Optional.of(matcher).map(m -> m.group(1).toLowerCase().replace("_", "."));
}
}
return Optional.empty();
}
public Set<String> getToWithWildcards() {
if (toWildcardMatcher == null) {
return Set.of();
}
return getWildcardKeys().stream()
.map(v -> toWildcardMatcher.replaceFirst(v))
.collect(Collectors.toSet());
}
/**
* Checks if the given option name matches the wildcard pattern of this option.
* E.g. check if "log-level-io.quarkus" matches the wildcard pattern "log-level-<category>".
*/
public boolean matchesWildcardOptionName(String name) {
return fromWildcardPattern.matcher(name).matches() || envVarNameWildcardPattern.matcher(name).matches()
|| (toWildcardPattern != null && toWildcardPattern.matcher(name).matches());
}
@Override
public PropertyMapper<?> forEnvKey(String key) {
Matcher matcher = envVarNameWildcardPattern.matcher(key);
if (!matcher.matches()) {
throw new IllegalStateException("Env var '" + key + "' does not match the expected pattern '" + envVarNameWildcardPattern + "'");
}
String value = matcher.group(1);
final String wildcardValue = value.toLowerCase().replace("_", "."); // we opiniotatedly convert env var names to CLI format with dots
return forWildcardValue(wildcardValue);
return getMappedKey(name).isPresent();
}
private PropertyMapper<?> forWildcardValue(final String wildcardValue) {
@@ -153,8 +126,7 @@ public class WildcardPropertyMapper<T> extends PropertyMapper<T> {
@Override
public PropertyMapper<?> forKey(String key) {
final String wildcardValue = getMappedKey(key, true).orElseThrow();
return forWildcardValue(wildcardValue);
return getMappedKey(key).map(this::forWildcardValue).orElseThrow();
}
}

View File

@@ -16,4 +16,5 @@
#
org.keycloak.quarkus.runtime.configuration.PropertyMappingInterceptor
org.keycloak.quarkus.runtime.configuration.NestedPropertyMappingInterceptor
org.keycloak.quarkus.runtime.configuration.DisabledMappersInterceptor

View File

@@ -55,7 +55,11 @@ public abstract class AbstractConfigurationTest {
try {
field = env.getClass().getDeclaredField("m");
field.setAccessible(true);
((Map<String, String>) field.get(env)).put(name, value);
if (value == null) {
((Map<String, String>) field.get(env)).remove(name);
} else {
((Map<String, String>) field.get(env)).put(name, value);
}
} catch (Exception cause) {
throw new RuntimeException("Failed to update environment variables", cause);
} finally {

View File

@@ -18,6 +18,7 @@
package org.keycloak.quarkus.runtime.configuration.test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.keycloak.quarkus.runtime.Environment.isWindows;
@@ -149,6 +150,28 @@ public class ConfigurationTest extends AbstractConfigurationTest {
assertEquals("http://c.jwk.url", initConfig("client-registration", "openid-connect").get("static-jwk-url"));
}
@Test
public void testExpressionEnvValue() {
putEnvVar("KC_HOSTNAME_STRICT", "false");
putEnvVar("MY_EXPRESSION", "${KC_HOSTNAME_STRICT}");
ConfigArgsConfigSource.setCliArgs("");
var config = createConfig();
// with the env variable set, we should get the same value either way
assertEquals("false", config.getConfigValue("KC_HOSTNAME_STRICT").getValue());
assertEquals("false", config.getConfigValue("MY_EXPRESSION").getValue());
// without the env variable set, the expression should use the missing env variable
putEnvVar("KC_HOSTNAME_STRICT", null);
ConfigArgsConfigSource.setCliArgs("");
config = createConfig();
// check that we get the mapped default value
assertEquals("true", config.getConfigValue("kc.hostname-strict").getValue());
// check that we don't get the mapped value
assertNull(config.getConfigValue("MY_EXPRESSION").getValue());
// could change after https://github.com/keycloak/keycloak/issues/38072
assertEquals("true", config.getConfigValue("KC_HOSTNAME_STRICT").getValue());
}
@Test
public void testResolveTransformedValue() {
ConfigArgsConfigSource.setCliArgs("");
@@ -478,11 +501,7 @@ public class ConfigurationTest extends AbstractConfigurationTest {
@Test
public void testKeystoreConfigSourcePropertyMapping() {
SmallRyeConfig config = new SmallRyeConfigBuilder()
.addDefaultInterceptors()
.addDiscoveredSources()
.build();
SmallRyeConfig config = createConfig();
assertEquals(config.getConfigValue("smallrye.config.source.keystore.kc-default.password").getValue(),config.getConfigValue("kc.config-keystore-password").getValue());
// Properties are loaded from the file - secret can be obtained only if the mapping works correctly
ConfigValue secret = config.getConfigValue("my.secret");
@@ -540,4 +559,28 @@ public class ConfigurationTest extends AbstractConfigurationTest {
assertEquals(Integer.toString(maxCount), config.getConfigValue(prop).getValue());
}
}
@Test
public void testDirectWildcardTo() {
// the mapping to for a wildcard property shouldn't be to anything
ConfigArgsConfigSource.setCliArgs("");
SmallRyeConfig config = createConfig();
assertNull(config.getConfigValue("quarkus.log.category.\"<categories>\".level").getValue());
}
@Test
public void testKeycloakConfQuarkusPropertyNotUsed() {
ConfigArgsConfigSource.setCliArgs("");
SmallRyeConfig config = createConfig();
assertNull(config.getConfigValue("quarkus.management.ssl.cipher-suites").getValue());
assertNotNull(config.getConfigValue("kc.quarkus.management.ssl.cipher-suites").getValue());
}
@Test
public void testQuarkusLogPropDependentUponKeycloak() {
Environment.setRebuildCheck(); // will be reset by the system properties logic
ConfigArgsConfigSource.setCliArgs("--log-level=debug");
SmallRyeConfig config = createConfig();
assertEquals("DEBUG", config.getConfigValue("quarkus.log.category.\"something\".level").getValue());
}
}

View File

@@ -6,4 +6,5 @@ KC_LOG_LEVEL_IO_QUARKUS=trace
config-keystore=src/test/resources/keystore
config-keystore-password=secret
quarkus.log.file.path=random/path
#quarkus option should not be used
quarkus.management.ssl.cipher-suites=foo

View File

@@ -29,7 +29,6 @@ import org.keycloak.it.utils.KeycloakDistribution;
import org.keycloak.it.utils.RawKeycloakDistribution;
import io.quarkus.test.junit.main.Launch;
import io.quarkus.test.junit.main.LaunchResult;
@DistributionTest(keepAlive = true, defaultOptions = { "--db=dev-file", "--features=fips", "--http-enabled=true", "--hostname-strict=false", "--log-level=org.keycloak.common.crypto.CryptoIntegration:trace" })
@RawDistOnly(reason = "Containers are immutable")

View File

@@ -22,7 +22,7 @@ import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import org.junit.jupiter.api.condition.DisabledOnOs;
import org.junit.jupiter.api.condition.EnabledOnOs;
import org.junit.jupiter.api.condition.OS;
import org.keycloak.it.junit5.extension.CLIResult;
import org.keycloak.it.junit5.extension.DistributionTest;
@@ -62,6 +62,8 @@ public class StartDevCommandDistTest {
cliResult.assertMessageWasShownExactlyNumberOfTimes("Listening for transport dt_socket at address:", 2);
cliResult.assertStartedDevMode();
cliResult.assertMessage("passkeys");
// ensure consistency with build-time properties
cliResult.assertNoMessage("Build time property cannot");
}
@DryRun
@@ -80,7 +82,6 @@ public class StartDevCommandDistTest {
}
@Test
@DisabledOnOs(value = { OS.LINUX, OS.MAC }, disabledReason = "A drive letter in URI can cause a problem.")
void testConfigKeystoreAbsolutePath(KeycloakDistribution dist) {
CLIResult cliResult = dist.run("start-dev", "--config-keystore=" + Paths.get("src/test/resources/keystore").toAbsolutePath().normalize(),
"--config-keystore-password=secret");