Add a quarkus.properties for unsupported configuration options

Closes #9682
This commit is contained in:
Pedro Igor 2022-01-25 10:46:14 -03:00
parent de161d02b9
commit 781ceb24fd
24 changed files with 730 additions and 47 deletions

View file

@ -18,6 +18,8 @@
package org.keycloak.quarkus.deployment; package org.keycloak.quarkus.deployment;
import static org.keycloak.quarkus.runtime.configuration.Configuration.getPropertyNames; import static org.keycloak.quarkus.runtime.configuration.Configuration.getPropertyNames;
import static org.keycloak.quarkus.runtime.configuration.MicroProfileConfigProvider.NS_QUARKUS;
import static org.keycloak.quarkus.runtime.configuration.QuarkusPropertiesConfigSource.QUARKUS_PROPERTY_ENABLED;
import static org.keycloak.quarkus.runtime.storage.database.jpa.QuarkusJpaConnectionProviderFactory.QUERY_PROPERTY_PREFIX; import static org.keycloak.quarkus.runtime.storage.database.jpa.QuarkusJpaConnectionProviderFactory.QUERY_PROPERTY_PREFIX;
import static org.keycloak.connections.jpa.util.JpaUtils.loadSpecificNamedQueries; import static org.keycloak.connections.jpa.util.JpaUtils.loadSpecificNamedQueries;
import static org.keycloak.representations.provider.ScriptProviderDescriptor.AUTHENTICATORS; import static org.keycloak.representations.provider.ScriptProviderDescriptor.AUTHENTICATORS;
@ -82,6 +84,7 @@ import org.jboss.resteasy.plugins.server.servlet.ResteasyContextParameters;
import org.jboss.resteasy.spi.ResteasyDeployment; import org.jboss.resteasy.spi.ResteasyDeployment;
import org.keycloak.Config; import org.keycloak.Config;
import org.keycloak.quarkus.runtime.configuration.PersistedConfigSource; import org.keycloak.quarkus.runtime.configuration.PersistedConfigSource;
import org.keycloak.quarkus.runtime.configuration.QuarkusPropertiesConfigSource;
import org.keycloak.quarkus.runtime.configuration.mappers.PropertyMapper; import org.keycloak.quarkus.runtime.configuration.mappers.PropertyMapper;
import org.keycloak.quarkus.runtime.configuration.mappers.PropertyMappers; import org.keycloak.quarkus.runtime.configuration.mappers.PropertyMappers;
import org.keycloak.quarkus.runtime.integration.jaxrs.QuarkusKeycloakApplication; import org.keycloak.quarkus.runtime.integration.jaxrs.QuarkusKeycloakApplication;
@ -313,14 +316,21 @@ class KeycloakProcessor {
for (String name : getPropertyNames()) { for (String name : getPropertyNames()) {
PropertyMapper mapper = PropertyMappers.getMapper(name); PropertyMapper mapper = PropertyMappers.getMapper(name);
ConfigValue value = null;
if (mapper == null) { if (mapper == null) {
if (name.startsWith(NS_QUARKUS)) {
value = Configuration.getConfigValue(name);
if (!QuarkusPropertiesConfigSource.isSameSource(value)) {
continue; continue;
} }
}
} else if (mapper.isBuildTime()) {
value = Configuration.getConfigValue(mapper.getFrom());
}
ConfigValue value = Configuration.getConfigValue(mapper.getFrom()); if (value != null && value.getValue() != null) {
if (mapper.isBuildTime() && value != null && value.getValue() != null) {
properties.put(name, value.getValue()); properties.put(name, value.getValue());
} }
} }
@ -329,6 +339,8 @@ class KeycloakProcessor {
properties.put(String.format("kc.provider.file.%s.last-modified", jar.getName()), String.valueOf(jar.lastModified())); properties.put(String.format("kc.provider.file.%s.last-modified", jar.getName()), String.valueOf(jar.lastModified()));
} }
properties.put(QUARKUS_PROPERTY_ENABLED, String.valueOf(QuarkusPropertiesConfigSource.getConfigurationFile() != null));
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
properties.store(outputStream, " Auto-generated, DO NOT change this file"); properties.store(outputStream, " Auto-generated, DO NOT change this file");
resources.produce(new GeneratedResourceBuildItem(PersistedConfigSource.PERSISTED_PROPERTIES, outputStream.toByteArray())); resources.produce(new GeneratedResourceBuildItem(PersistedConfigSource.PERSISTED_PROPERTIES, outputStream.toByteArray()));

View file

@ -31,6 +31,7 @@ import java.util.Optional;
import java.util.function.Function; import java.util.function.Function;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import io.quarkus.bootstrap.runner.RunnerClassLoader;
import io.quarkus.runtime.LaunchMode; import io.quarkus.runtime.LaunchMode;
import io.quarkus.runtime.configuration.ProfileManager; import io.quarkus.runtime.configuration.ProfileManager;
import org.apache.commons.lang3.SystemUtils; import org.apache.commons.lang3.SystemUtils;
@ -48,7 +49,11 @@ public final class Environment {
private Environment() {} private Environment() {}
public static Boolean isRebuild() { public static Boolean isRebuild() {
return Boolean.getBoolean("quarkus.launch.rebuild"); return !isRuntimeMode();
}
public static Boolean isRuntimeMode() {
return Thread.currentThread().getContextClassLoader() instanceof RunnerClassLoader;
} }
public static String getHomeDir() { public static String getHomeDir() {
@ -187,4 +192,8 @@ public final class Environment {
public static boolean isDistribution() { public static boolean isDistribution() {
return getHomeDir() != null; return getHomeDir() != null;
} }
public static boolean isRebuildCheck() {
return Boolean.getBoolean("kc.config.rebuild-and-exit");
}
} }

View file

@ -78,7 +78,7 @@ public final class Picocli {
public static void parseAndRun(List<String> cliArgs) { public static void parseAndRun(List<String> cliArgs) {
CommandLine cmd = createCommandLine(cliArgs); CommandLine cmd = createCommandLine(cliArgs);
if (Boolean.getBoolean("kc.config.rebuild-and-exit")) { if (Environment.isRebuildCheck()) {
runReAugmentationIfNeeded(cliArgs, cmd); runReAugmentationIfNeeded(cliArgs, cmd);
Quarkus.asyncExit(cmd.getCommandSpec().exitCodeOnSuccess()); Quarkus.asyncExit(cmd.getCommandSpec().exitCodeOnSuccess());
return; return;

View file

@ -43,6 +43,9 @@ public class KeycloakConfigSourceProvider implements ConfigSourceProvider {
CONFIG_SOURCES.add(new ConfigArgsConfigSource()); CONFIG_SOURCES.add(new ConfigArgsConfigSource());
CONFIG_SOURCES.add(new KcEnvConfigSource()); CONFIG_SOURCES.add(new KcEnvConfigSource());
CONFIG_SOURCES.addAll(new QuarkusPropertiesConfigSource().getConfigSources(Thread.currentThread().getContextClassLoader()));
CONFIG_SOURCES.add(PersistedConfigSource.getInstance()); CONFIG_SOURCES.add(PersistedConfigSource.getInstance());
CONFIG_SOURCES.addAll(new KeycloakPropertiesConfigSource.InFileSystem().getConfigSources(Thread.currentThread().getContextClassLoader())); CONFIG_SOURCES.addAll(new KeycloakPropertiesConfigSource.InFileSystem().getConfigSources(Thread.currentThread().getContextClassLoader()));

View file

@ -28,12 +28,13 @@ import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import org.eclipse.microprofile.config.spi.ConfigSource; import org.eclipse.microprofile.config.spi.ConfigSource;
import org.eclipse.microprofile.config.spi.ConfigSourceProvider; import org.eclipse.microprofile.config.spi.ConfigSourceProvider;
import org.keycloak.quarkus.runtime.Environment; import org.keycloak.quarkus.runtime.Environment;
import org.keycloak.quarkus.runtime.configuration.mappers.PropertyMapper;
import org.keycloak.quarkus.runtime.configuration.mappers.PropertyMappers;
import io.smallrye.config.AbstractLocationConfigSourceLoader; import io.smallrye.config.AbstractLocationConfigSourceLoader;
import io.smallrye.config.PropertiesConfigSource; import io.smallrye.config.PropertiesConfigSource;
@ -62,7 +63,7 @@ public class KeycloakPropertiesConfigSource extends AbstractLocationConfigSource
@Override @Override
protected ConfigSource loadConfigSource(URL url, int ordinal) throws IOException { protected ConfigSource loadConfigSource(URL url, int ordinal) throws IOException {
return new PropertiesConfigSource(transform(ConfigSourceUtil.urlToMap(url)), KEYCLOAK_CONF_FILE, ordinal); return new PropertiesConfigSource(transform(ConfigSourceUtil.urlToMap(url)), url.toString(), ordinal);
} }
public static class InClassPath extends KeycloakPropertiesConfigSource implements ConfigSourceProvider { public static class InClassPath extends KeycloakPropertiesConfigSource implements ConfigSourceProvider {
@ -143,11 +144,22 @@ public class KeycloakPropertiesConfigSource extends AbstractLocationConfigSource
Map<String, String> result = new HashMap<>(properties.size()); Map<String, String> result = new HashMap<>(properties.size());
properties.keySet().forEach(k -> { properties.keySet().forEach(k -> {
String key = transformKey(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 = replaceProperties(properties.get(k)); String value = replaceProperties(properties.get(k));
result.put(key, value); result.put(key, value);
if (mapper != null && key.charAt(0) != '%') {
result.put(getMappedPropertyName(key), value); result.put(getMappedPropertyName(key), value);
}
}
}); });
return result; return result;
} }

View file

@ -16,10 +16,14 @@
*/ */
package org.keycloak.quarkus.runtime.configuration; package org.keycloak.quarkus.runtime.configuration;
import static org.keycloak.quarkus.runtime.configuration.MicroProfileConfigProvider.NS_QUARKUS;
import io.quarkus.runtime.configuration.AbstractRawDefaultConfigSource;
import io.smallrye.config.ConfigSourceInterceptor; import io.smallrye.config.ConfigSourceInterceptor;
import io.smallrye.config.ConfigSourceInterceptorContext; import io.smallrye.config.ConfigSourceInterceptorContext;
import io.smallrye.config.ConfigValue; import io.smallrye.config.ConfigValue;
import org.keycloak.common.util.StringPropertyReplacer; import org.keycloak.common.util.StringPropertyReplacer;
import org.keycloak.quarkus.runtime.Environment;
import org.keycloak.quarkus.runtime.configuration.mappers.PropertyMapper; import org.keycloak.quarkus.runtime.configuration.mappers.PropertyMapper;
import org.keycloak.quarkus.runtime.configuration.mappers.PropertyMappers; import org.keycloak.quarkus.runtime.configuration.mappers.PropertyMappers;
@ -35,6 +39,8 @@ import org.keycloak.quarkus.runtime.configuration.mappers.PropertyMappers;
*/ */
public class PropertyMappingInterceptor implements ConfigSourceInterceptor { public class PropertyMappingInterceptor implements ConfigSourceInterceptor {
private final boolean isQuarkusPropertiesEnabled = QuarkusPropertiesConfigSource.isQuarkusPropertiesEnabled();
@Override @Override
public ConfigValue getValue(ConfigSourceInterceptorContext context, String name) { public ConfigValue getValue(ConfigSourceInterceptorContext context, String name) {
ConfigValue value = PropertyMappers.getValue(context, name); ConfigValue value = PropertyMappers.getValue(context, name);
@ -43,6 +49,11 @@ public class PropertyMappingInterceptor implements ConfigSourceInterceptor {
return null; return null;
} }
if (isPersistedOnlyProperty(value)) {
// quarkus properties values always resolved from persisted config source
return value.withValue(PersistedConfigSource.getInstance().getValue(name));
}
if (value.getValue().indexOf("${") == -1) { if (value.getValue().indexOf("${") == -1) {
return value; return value;
} }
@ -59,4 +70,19 @@ public class PropertyMappingInterceptor implements ConfigSourceInterceptor {
return prop.getValue(); return prop.getValue();
})); }));
} }
private boolean isPersistedOnlyProperty(ConfigValue value) {
if (isQuarkusPropertiesEnabled && value.getName().startsWith(NS_QUARKUS)) {
String configSourceName = value.getConfigSourceName();
return Environment.isRuntimeMode()
&& configSourceName != null
&& !configSourceName.equals(PersistedConfigSource.NAME)
&& !configSourceName.equals(AbstractRawDefaultConfigSource.NAME)
&& !configSourceName.contains("Runtime Defaults")
&& !configSourceName.contains("application.properties");
}
return false;
}
} }

View file

@ -0,0 +1,140 @@
/*
* Copyright 2021 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.quarkus.runtime.configuration;
import static java.lang.Boolean.parseBoolean;
import static org.keycloak.quarkus.runtime.configuration.Configuration.getRawPersistedProperty;
import static org.keycloak.quarkus.runtime.configuration.MicroProfileConfigProvider.NS_QUARKUS;
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.net.URL;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.eclipse.microprofile.config.spi.ConfigSource;
import org.eclipse.microprofile.config.spi.ConfigSourceProvider;
import org.keycloak.quarkus.runtime.Environment;
import io.smallrye.config.AbstractLocationConfigSourceLoader;
import io.smallrye.config.ConfigValue;
import io.smallrye.config.PropertiesConfigSource;
import io.smallrye.config.common.utils.ConfigSourceUtil;
/**
* A configuration source for {@code quarkus.properties}.
*/
public final class QuarkusPropertiesConfigSource extends AbstractLocationConfigSourceLoader implements ConfigSourceProvider {
private static final String NAME = "QuarkusProperties";
private static final String FILE_NAME = "quarkus.properties";
public static final String QUARKUS_PROPERTY_ENABLED = "kc.quarkus-properties-enabled";
public static boolean isSameSource(ConfigValue value) {
if (value == null) {
return false;
}
return NAME.equals(value.getConfigSourceName());
}
public static boolean isQuarkusPropertiesEnabled() {
return parseBoolean(getRawPersistedProperty(QUARKUS_PROPERTY_ENABLED).orElse(Boolean.FALSE.toString()));
}
public static Path getConfigurationFile() {
String homeDir = Environment.getHomeDir();
if (homeDir != null) {
File file = Paths.get(homeDir, "conf", FILE_NAME).toFile();
if (file.exists()) {
return file.toPath();
}
}
return null;
}
@Override
protected String[] getFileExtensions() {
return new String[] { "properties" };
}
@Override
protected ConfigSource loadConfigSource(URL url, int ordinal) throws IOException {
return new PropertiesConfigSource(ConfigSourceUtil.urlToMap(url), FILE_NAME, ordinal) {
@Override
public String getName() {
return NAME;
}
@Override
public String getValue(String propertyName) {
if (propertyName.startsWith(NS_QUARKUS)) {
String value = super.getValue(propertyName);
if (value == null) {
return PersistedConfigSource.getInstance().getValue(propertyName);
}
return value;
}
return null;
}
};
}
@Override
public List<ConfigSource> getConfigSources(final ClassLoader classLoader) {
List<ConfigSource> configSources = new ArrayList<>();
configSources.addAll(loadConfigSources("META-INF/services/" + FILE_NAME, 450, classLoader));
if (Environment.isRebuild() || Environment.isRebuildCheck()) {
Path configFile = getConfigurationFile();
if (configFile != null) {
configSources.addAll(loadConfigSources(configFile.toUri().toString(), 500, classLoader));
}
}
return configSources;
}
@Override
protected List<ConfigSource> tryClassPath(URI uri, int ordinal, ClassLoader classLoader) {
try {
return super.tryClassPath(uri, ordinal, classLoader);
} catch (RuntimeException e) {
Throwable cause = e.getCause();
if (cause instanceof NoSuchFileException) {
// configuration step happens before classpath is updated, and it might happen that
// provider JARs are still in classpath index but removed from the providers dir
return Collections.emptyList();
}
throw e;
}
}
}

View file

@ -49,7 +49,7 @@ public final class PropertyMappers {
} }
public static boolean isBuildTimeProperty(String name) { public static boolean isBuildTimeProperty(String name) {
if (isFeaturesBuildTimeProperty(name) || isSpiBuildTimeProperty(name)) { if (isFeaturesBuildTimeProperty(name) || isSpiBuildTimeProperty(name) || name.startsWith(MicroProfileConfigProvider.NS_QUARKUS)) {
return true; return true;
} }
@ -123,6 +123,9 @@ public final class PropertyMappers {
} }
public static PropertyMapper getMapper(String property) { public static PropertyMapper getMapper(String property) {
if (property.startsWith("%")) {
return MAPPERS.get(property.substring(property.indexOf('.') + 1));
}
return MAPPERS.get(property); return MAPPERS.get(property);
} }

View file

@ -23,8 +23,3 @@ metrics-enabled=false
%import_export.hostname-strict=false %import_export.hostname-strict=false
%import_export.hostname-strict-https=false %import_export.hostname-strict-https=false
%import_export.cluster=local %import_export.cluster=local
# Logging configuration. INFO is the default level for most of the categories
#quarkus.log.level = DEBUG
quarkus.log.category."org.jboss.resteasy.resteasy_jaxrs.i18n".level=WARN
quarkus.log.category."org.infinispan.transaction.lookup.JBossStandaloneJTAManagerLookup".level=WARN

View file

@ -0,0 +1,23 @@
#
# 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.
#
# Default options that rely on Quarkus specific options and lacking proper support in Keycloak
# Logging configuration. INFO is the default level for most of the categories
quarkus.log.level = INFO
quarkus.log.category."org.jboss.resteasy.resteasy_jaxrs.i18n".level=WARN
quarkus.log.category."org.infinispan.transaction.lookup.JBossStandaloneJTAManagerLookup".level=WARN

View file

@ -391,6 +391,15 @@ public class ConfigurationTest {
assertEquals("my_secret=", config.getConfigValue("kc.db-password").getValue()); assertEquals("my_secret=", config.getConfigValue("kc.db-password").getValue());
} }
@Test
public void testResolvePropertyFromDefaultProfile() {
Environment.setProfile("import_export");
assertEquals("false", createConfig().getConfigValue("kc.hostname-strict").getValue());
Environment.setProfile("prod");
assertEquals("true", createConfig().getConfigValue("kc.hostname-strict").getValue());
}
private Config.Scope initConfig(String... scope) { private Config.Scope initConfig(String... scope) {
Config.init(new MicroProfileConfigProvider(createConfig())); Config.init(new MicroProfileConfigProvider(createConfig()));
return Config.scope(scope); return Config.scope(scope);

View file

@ -1,15 +1,2 @@
spi-hostname-default-frontend-url = ${keycloak.frontendUrl:http://filepropdefault.unittest} spi-hostname-default-frontend-url = ${keycloak.frontendUrl:http://filepropdefault.unittest}
%user-profile.spi-hostname-default-frontend-url = http://filepropprofile.unittest %user-profile.spi-hostname-default-frontend-url = http://filepropprofile.unittest
# Default Non-Production Grade Datasource
quarkus.datasource.db-kind=h2
quarkus.hibernate-orm.dialect=org.hibernate.dialect.H2Dialect
quarkus.datasource.jdbc.driver=org.h2.jdbcx.JdbcDataSource
quarkus.datasource.jdbc.url = jdbc:h2:file:${kc.home.dir:~}/data/keycloakdb;;AUTO_SERVER=TRUE
quarkus.datasource.username = sa
quarkus.datasource.password = keycloak
quarkus.datasource.jdbc.transactions=xa
# For test nested properties
quarkus.datasource.foo = jdbc:h2:file:${kc.home.dir:${kc.db.url.path:~}}/data/keycloakdb
quarkus.datasource.bar = foo-${kc.prop3:${kc.prop4:${kc.prop5:def}-suffix}}

View file

@ -0,0 +1,10 @@
# Default options that rely on Quarkus specific options and lacking proper support in Keycloak
# Logging configuration. INFO is the default level for most of the categories
quarkus.log.level = INFO
quarkus.log.category."org.jboss.resteasy.resteasy_jaxrs.i18n".level=WARN
quarkus.log.category."org.infinispan.transaction.lookup.JBossStandaloneJTAManagerLookup".level=WARN
# For test nested properties
quarkus.datasource.foo = jdbc:h2:file:${kc.home.dir:${kc.db.url.path:~}}/data/keycloakdb
quarkus.datasource.bar = foo-${kc.prop3:${kc.prop4:${kc.prop5:def}-suffix}}

View file

@ -0,0 +1,36 @@
/*
* 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.it.junit5.extension;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.function.Consumer;
import org.keycloak.it.utils.KeycloakDistribution;
/**
* {@link BeforeStartDistribution} is used to perform additional steps prior to starting the distribution.
*/
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
public @interface BeforeStartDistribution {
Class<? extends Consumer<KeycloakDistribution>> value();
}

View file

@ -81,4 +81,20 @@ public interface CLIResult extends LaunchResult {
default void assertBuild() { default void assertBuild() {
assertMessage("Server configuration updated and persisted"); assertMessage("Server configuration updated and persisted");
} }
default void assertNoBuild() {
assertFalse(getOutput().contains("Server configuration updated and persisted"));
}
default boolean isClustered() {
return getOutput().contains("Starting JGroups channel `ISPN`");
}
default void assertLocalCache() {
assertFalse(isClustered());
}
default void assertClusteredCache() {
assertTrue(isClustered());
}
} }

View file

@ -83,6 +83,10 @@ public class CLITestExtension extends QuarkusMainTestExtension {
if (dist == null) { if (dist == null) {
dist = createDistribution(distConfig); dist = createDistribution(distConfig);
} }
onBeforeStartDistribution(context.getRequiredTestClass().getAnnotation(BeforeStartDistribution.class));
onBeforeStartDistribution(context.getRequiredTestMethod().getAnnotation(BeforeStartDistribution.class));
dist.start(Arrays.asList(launch.value())); dist.start(Arrays.asList(launch.value()));
} }
} else { } else {
@ -94,6 +98,16 @@ public class CLITestExtension extends QuarkusMainTestExtension {
} }
} }
private void onBeforeStartDistribution(BeforeStartDistribution annotation) {
if (annotation != null) {
try {
annotation.value().getDeclaredConstructor().newInstance().accept(dist);
} catch (Exception cause) {
throw new RuntimeException("Error when invoking " + annotation.value() + " instance before starting distribution", cause);
}
}
}
@Override @Override
public void afterEach(ExtensionContext context) throws Exception { public void afterEach(ExtensionContext context) throws Exception {
DistributionTest distConfig = getDistributionConfig(context); DistributionTest distConfig = getDistributionConfig(context);

View file

@ -0,0 +1,25 @@
/*
* 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.it.junit5.extension;
import org.keycloak.it.utils.KeycloakDistribution;
public interface DistributionLifecycleManager<D extends KeycloakDistribution> {
void beforeStart(D distribution);
}

View file

@ -38,4 +38,16 @@ public interface KeycloakDistribution {
return commands.toArray(new String[0]); return commands.toArray(new String[0]);
} }
default void setQuarkusProperty(String key, String value) {
throw new RuntimeException("Not implemented");
}
default void setProperty(String key, String value) {
throw new RuntimeException("Not implemented");
}
default void deleteQuarkusProperties() {
throw new RuntimeException("Not implemented");
}
} }

View file

@ -19,6 +19,8 @@ package org.keycloak.it.utils;
import java.io.BufferedReader; import java.io.BufferedReader;
import java.io.File; import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import java.net.HttpURLConnection; import java.net.HttpURLConnection;
@ -32,6 +34,7 @@ import java.security.SecureRandom;
import java.security.cert.X509Certificate; import java.security.cert.X509Certificate;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Properties;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@ -60,6 +63,7 @@ public final class RawKeycloakDistribution implements KeycloakDistribution {
private boolean debug; private boolean debug;
private boolean reCreate; private boolean reCreate;
private ExecutorService outputExecutor; private ExecutorService outputExecutor;
private boolean inited = false;
public RawKeycloakDistribution(boolean debug, boolean manualStop, boolean reCreate) { public RawKeycloakDistribution(boolean debug, boolean manualStop, boolean reCreate) {
this.debug = debug; this.debug = debug;
@ -254,7 +258,7 @@ public final class RawKeycloakDistribution implements KeycloakDistribution {
String distDirName = distFile.getName().replace("keycloak-server-x-dist", "keycloak.x"); String distDirName = distFile.getName().replace("keycloak-server-x-dist", "keycloak.x");
Path distPath = distRootPath.resolve(distDirName.substring(0, distDirName.lastIndexOf('.'))); Path distPath = distRootPath.resolve(distDirName.substring(0, distDirName.lastIndexOf('.')));
if (reCreate || !distPath.toFile().exists()) { if (!inited || (reCreate || !distPath.toFile().exists())) {
distPath.toFile().delete(); distPath.toFile().delete();
ZipUtils.unzip(distFile.toPath(), distRootPath); ZipUtils.unzip(distFile.toPath(), distRootPath);
} }
@ -264,6 +268,8 @@ public final class RawKeycloakDistribution implements KeycloakDistribution {
throw new RuntimeException("Cannot set kc.sh executable"); throw new RuntimeException("Cannot set kc.sh executable");
} }
inited = true;
return distPath; return distPath;
} catch (Exception cause) { } catch (Exception cause) {
throw new RuntimeException("Failed to prepare distribution", cause); throw new RuntimeException("Failed to prepare distribution", cause);
@ -311,4 +317,51 @@ public final class RawKeycloakDistribution implements KeycloakDistribution {
keycloak = builder.start(); keycloak = builder.start();
} }
@Override
public void setProperty(String key, String value) {
setProperty(key, value, distPath.resolve("conf").resolve("keycloak.conf").toFile());
}
@Override
public void setQuarkusProperty(String key, String value) {
setProperty(key, value, getQuarkusPropertiesFile());
}
@Override
public void deleteQuarkusProperties() {
File file = getQuarkusPropertiesFile();
if (file.exists()) {
file.delete();
}
}
private void setProperty(String key, String value, File confFile) {
Properties properties = new Properties();
if (confFile.exists()) {
try (
FileInputStream in = new FileInputStream(confFile);
) {
properties.load(in);
} catch (Exception e) {
throw new RuntimeException("Failed to update " + confFile, e);
}
}
try (
FileOutputStream out = new FileOutputStream(confFile)
) {
properties.put(key, value);
properties.store(out, "");
} catch (Exception e) {
throw new RuntimeException("Failed to update " + confFile, e);
}
}
private File getQuarkusPropertiesFile() {
return distPath.resolve("conf").resolve("quarkus.properties").toFile();
}
} }

View file

@ -17,18 +17,16 @@
package org.keycloak.it.cli.dist; package org.keycloak.it.cli.dist;
import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.function.Consumer;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.MethodOrderer.OrderAnnotation; import org.junit.jupiter.api.MethodOrderer.OrderAnnotation;
import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder; import org.junit.jupiter.api.TestMethodOrder;
import org.junit.jupiter.api.condition.DisabledIf; import org.keycloak.it.junit5.extension.BeforeStartDistribution;
import org.junit.jupiter.api.condition.EnabledIfSystemProperty;
import org.keycloak.it.junit5.extension.CLIResult; import org.keycloak.it.junit5.extension.CLIResult;
import org.keycloak.it.junit5.extension.DistributionTest; import org.keycloak.it.junit5.extension.DistributionTest;
import org.keycloak.it.junit5.extension.RawDistOnly; import org.keycloak.it.junit5.extension.RawDistOnly;
import org.keycloak.it.utils.KeycloakDistribution;
import io.quarkus.test.junit.main.Launch; import io.quarkus.test.junit.main.Launch;
import io.quarkus.test.junit.main.LaunchResult; import io.quarkus.test.junit.main.LaunchResult;
@ -41,14 +39,45 @@ public class BuildAndStartDistTest {
@Test @Test
@Launch({ "build", "--cache=local" }) @Launch({ "build", "--cache=local" })
@Order(1) @Order(1)
void firstYouBuild(LaunchResult result) { void testBuildWithCliArgs(LaunchResult result) {
CLIResult cliResult = (CLIResult) result;
cliResult.assertBuild();
} }
@Test @Test
@Launch({ "start", "--http-enabled=true", "--hostname-strict=false" }) @Launch({ "start", "--http-enabled=true", "--hostname-strict=false" })
@Order(2) @Order(2)
void thenYouStart(LaunchResult result) { void testStartUsingCliArgs(LaunchResult result) {
CLIResult cliResult = (CLIResult) result; CLIResult cliResult = (CLIResult) result;
cliResult.assertStarted(); cliResult.assertStarted();
cliResult.assertLocalCache();
}
@Test
@BeforeStartDistribution(SetDefaultOptions.class)
@Launch({ "build" })
@Order(3)
void testBuildUsingConfFile(LaunchResult result) {
CLIResult cliResult = (CLIResult) result;
cliResult.assertBuild();
}
@Test
@Launch({ "start" })
@Order(4)
void testStartUsingConfFile(LaunchResult result) {
CLIResult cliResult = (CLIResult) result;
cliResult.assertStarted();
cliResult.assertLocalCache();
}
public static class SetDefaultOptions implements Consumer<KeycloakDistribution> {
@Override
public void accept(KeycloakDistribution distribution) {
distribution.setProperty("http-enabled", "true");
distribution.setProperty("hostname-strict", "false");
distribution.setProperty("cache", "local");
}
} }
} }

View file

@ -35,7 +35,8 @@ public class ClusterConfigDistTest {
@Test @Test
@Launch({ "start-dev", "--cache=ispn" }) @Launch({ "start-dev", "--cache=ispn" })
void changeClusterSetting(LaunchResult result) { void changeClusterSetting(LaunchResult result) {
assertTrue(isClustered(result)); CLIResult cliResult = (CLIResult) result;
cliResult.assertClusteredCache();
} }
@Test @Test
@ -61,7 +62,7 @@ public class ClusterConfigDistTest {
void testExplicitCacheConfigFile(LaunchResult result) { void testExplicitCacheConfigFile(LaunchResult result) {
CLIResult cliResult = (CLIResult) result; CLIResult cliResult = (CLIResult) result;
cliResult.assertStartedDevMode(); cliResult.assertStartedDevMode();
assertTrue(isClustered(cliResult)); cliResult.assertClusteredCache();
} }
@Test @Test
@ -69,7 +70,7 @@ public class ClusterConfigDistTest {
void testStartDefaultsToClustering(LaunchResult result) { void testStartDefaultsToClustering(LaunchResult result) {
CLIResult cliResult = (CLIResult) result; CLIResult cliResult = (CLIResult) result;
cliResult.assertStarted(); cliResult.assertStarted();
assertTrue(isClustered(result)); cliResult.assertClusteredCache();
} }
@Test @Test
@ -77,10 +78,6 @@ public class ClusterConfigDistTest {
void testStartDevDefaultsToLocalCaches(LaunchResult result) { void testStartDevDefaultsToLocalCaches(LaunchResult result) {
CLIResult cliResult = (CLIResult) result; CLIResult cliResult = (CLIResult) result;
cliResult.assertStartedDevMode(); cliResult.assertStartedDevMode();
assertFalse(isClustered(result)); cliResult.assertLocalCache();
}
private boolean isClustered(LaunchResult result) {
return result.getOutput().contains("Starting JGroups channel `ISPN`");
} }
} }

View file

@ -0,0 +1,89 @@
/*
* 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.it.cli.dist;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.function.Consumer;
import org.junit.jupiter.api.MethodOrderer.OrderAnnotation;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import org.keycloak.it.junit5.extension.BeforeStartDistribution;
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.utils.KeycloakDistribution;
import io.quarkus.test.junit.main.Launch;
import io.quarkus.test.junit.main.LaunchResult;
@DistributionTest(reInstall = DistributionTest.ReInstall.NEVER)
@BeforeStartDistribution(QuarkusPropertiesAutoBuildDistTest.SetDebugLogLevel.class)
@RawDistOnly(reason = "Containers are immutable")
@TestMethodOrder(OrderAnnotation.class)
public class QuarkusPropertiesAutoBuildDistTest {
@Test
@Launch({ "start", "--auto-build", "--http-enabled=true", "--hostname-strict=false", "--cache=local" })
@Order(1)
void testReAugOnFirstRun(LaunchResult result) {
CLIResult cliResult = (CLIResult) result;
cliResult.assertBuild();
cliResult.assertMessage("DEBUG [");
cliResult.assertStarted();
}
@Test
@Launch({ "start", "--auto-build", "--http-enabled=true", "--hostname-strict=false", "--cache=local" })
@Order(2)
void testSecondStartDoNotTriggerReAug(LaunchResult result) {
CLIResult cliResult = (CLIResult) result;
cliResult.assertNoBuild();
cliResult.assertMessage("DEBUG [");
cliResult.assertStarted();
}
@Test
@BeforeStartDistribution(SetInfoLogLevel.class)
@Launch({ "start", "--auto-build", "--http-enabled=true", "--hostname-strict=false", "--cache=local" })
@Order(3)
void testReAugAfterChangingProperty(LaunchResult result) {
CLIResult cliResult = (CLIResult) result;
cliResult.assertBuild();
assertFalse(cliResult.getOutput().contains("DEBUG ["));
}
public static class SetDebugLogLevel implements Consumer<KeycloakDistribution> {
@Override
public void accept(KeycloakDistribution distribution) {
distribution.setQuarkusProperty("quarkus.log.level", "DEBUG");
}
}
public static class SetInfoLogLevel implements Consumer<KeycloakDistribution> {
@Override
public void accept(KeycloakDistribution distribution) {
distribution.setQuarkusProperty("quarkus.log.level", "INFO");
}
}
}

View file

@ -0,0 +1,104 @@
/*
* 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.it.cli.dist;
import static org.junit.jupiter.api.Assertions.assertFalse;
import java.util.function.Consumer;
import org.junit.jupiter.api.MethodOrderer.OrderAnnotation;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import org.keycloak.it.junit5.extension.BeforeStartDistribution;
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.utils.KeycloakDistribution;
import io.quarkus.test.junit.main.Launch;
import io.quarkus.test.junit.main.LaunchResult;
@DistributionTest(reInstall = DistributionTest.ReInstall.NEVER)
@BeforeStartDistribution(QuarkusPropertiesDistTest.SetDebugLogLevel.class)
@RawDistOnly(reason = "Containers are immutable")
@TestMethodOrder(OrderAnnotation.class)
public class QuarkusPropertiesDistTest {
@Test
@Launch({ "build", "--cache=local" })
@Order(1)
void testBuildWithPropertyFromQuarkusProperties(LaunchResult result) {
CLIResult cliResult = (CLIResult) result;
cliResult.assertMessage("DEBUG [");
cliResult.assertBuild();
}
@Test
@Launch({ "start", "--http-enabled=true", "--hostname-strict=false" })
@Order(2)
void testPropertyEnabledAtRuntime(LaunchResult result) {
CLIResult cliResult = (CLIResult) result;
cliResult.assertMessage("DEBUG [");
cliResult.assertStarted();
}
@Test
@Launch({ "-Dquarkus.log.level=INFO", "start", "--http-enabled=true", "--hostname-strict=false" })
@Order(3)
void testIgnoreQuarkusSystemPropertiesAtStart(LaunchResult result) {
CLIResult cliResult = (CLIResult) result;
cliResult.assertMessage("DEBUG [");
cliResult.assertStarted();
}
@Test
@Launch({ "-Dquarkus.log.level=INFO", "build" })
@Order(4)
void testIgnoreQuarkusSystemPropertyAtBuild(LaunchResult result) {
CLIResult cliResult = (CLIResult) result;
cliResult.assertMessage("DEBUG [");
cliResult.assertBuild();
}
@Test
@BeforeStartDistribution(SetDebugLogLevelInKeycloakConf.class)
@Launch({ "build" })
@Order(5)
void testIgnoreQuarkusPropertyFromKeycloakConf(LaunchResult result) {
CLIResult cliResult = (CLIResult) result;
assertFalse(cliResult.getOutput().contains("DEBUG ["));
cliResult.assertBuild();
}
public static class SetDebugLogLevel implements Consumer<KeycloakDistribution> {
@Override
public void accept(KeycloakDistribution distribution) {
distribution.setQuarkusProperty("quarkus.log.level", "DEBUG");
}
}
public static class SetDebugLogLevelInKeycloakConf implements Consumer<KeycloakDistribution> {
@Override
public void accept(KeycloakDistribution distribution) {
distribution.deleteQuarkusProperties();
distribution.setProperty("quarkus.log.level", "DEBUG");
}
}
}

View file

@ -0,0 +1,79 @@
/*
* 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.it.cli.dist;
import static org.junit.jupiter.api.Assertions.assertFalse;
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 io.quarkus.test.junit.main.Launch;
import io.quarkus.test.junit.main.LaunchResult;
@DistributionTest(reInstall = DistributionTest.ReInstall.NEVER)
@RawDistOnly(reason = "Containers are immutable")
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class StartAutoBuildDistTest {
@Test
@Launch({ "start", "--auto-build", "--http-enabled=true", "--hostname-strict=false", "--cache=local" })
@Order(1)
void testStartAutoBuild(LaunchResult result) {
CLIResult cliResult = (CLIResult) result;
cliResult.assertMessage("Changes detected in configuration. Updating the server image.");
cliResult.assertMessage("Updating the configuration and installing your custom providers, if any. Please wait.");
cliResult.assertMessage("Server configuration updated and persisted. Run the following command to review the configuration:");
cliResult.assertMessage("kc.sh show-config");
cliResult.assertMessage("Next time you run the server, just run:");
cliResult.assertMessage("kc.sh start --http-enabled=true --hostname-strict=false");
assertFalse(cliResult.getOutput().contains("--cache"));
cliResult.assertStarted();
}
@Test
@Launch({ "start", "--auto-build", "--http-enabled=true", "--hostname-strict=false", "--cache=local" })
@Order(2)
void testShouldNotReAugIfConfigIsSame(LaunchResult result) {
CLIResult cliResult = (CLIResult) result;
cliResult.assertNoBuild();
cliResult.assertStarted();
}
@Test
@Launch({ "start", "--auto-build", "--db=h2-mem", "--http-enabled=true", "--hostname-strict=false", "--cache=local" })
@Order(3)
void testShouldReAugIfConfigChanged(LaunchResult result) {
CLIResult cliResult = (CLIResult) result;
cliResult.assertBuild();
cliResult.assertStarted();
}
@Test
@Launch({ "start", "--auto-build", "--db=h2-mem", "--http-enabled=true", "--hostname-strict=false", "--cache=local" })
@Order(4)
void testShouldNotReAugIfSameDatabase(LaunchResult result) {
CLIResult cliResult = (CLIResult) result;
cliResult.assertNoBuild();
cliResult.assertStarted();
}
}