[KEYCLOAK-19847] - Optimizations and refactoring for better/stable startup time

This commit is contained in:
Pedro Igor 2021-12-02 08:39:28 -03:00
parent f64441a54a
commit 9a4ab82d08
39 changed files with 746 additions and 513 deletions

View file

@ -414,7 +414,7 @@ jobs:
- name: Run Quarkus Tests in Docker
run: |
mvn clean install -nsu -B -f quarkus/tests/pom.xml -Dkc.quarkus.tests.dist=docker | misc/log/trimmer.sh
mvn clean install -nsu -B -f quarkus/tests/pom.xml -Dkc.quarkus.tests.dist=docker -Dtest=StartDevCommandTest | misc/log/trimmer.sh
TEST_RESULT=${PIPESTATUS[0]}
exit $TEST_RESULT

View file

@ -0,0 +1,88 @@
/*
* 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.deployment;
import static org.keycloak.quarkus.runtime.configuration.Configuration.getConfigValue;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.stream.Collectors;
import javax.enterprise.context.ApplicationScoped;
import org.infinispan.commons.util.FileLookupFactory;
import org.keycloak.quarkus.runtime.Environment;
import org.keycloak.quarkus.runtime.KeycloakRecorder;
import org.keycloak.quarkus.runtime.storage.infinispan.CacheInitializer;
import io.quarkus.arc.deployment.SyntheticBeanBuildItem;
import io.quarkus.deployment.annotations.BuildProducer;
import io.quarkus.deployment.annotations.BuildStep;
import io.quarkus.deployment.annotations.ExecutionTime;
import io.quarkus.deployment.annotations.Record;
public class CLusteringBuildSteps {
@Record(ExecutionTime.RUNTIME_INIT)
@BuildStep
void configureInfinispan(KeycloakRecorder recorder, BuildProducer<SyntheticBeanBuildItem> syntheticBeanBuildItems) {
String pathPrefix;
String homeDir = Environment.getHomeDir();
if (homeDir == null) {
pathPrefix = "";
} else {
pathPrefix = homeDir + "/conf/";
}
String configFile = getConfigValue("kc.spi.connections-infinispan.quarkus.config-file").getValue();
if (configFile != null) {
Path configPath = Paths.get(pathPrefix + configFile);
String path;
if (configPath.toFile().exists()) {
path = configPath.toFile().getAbsolutePath();
} else {
path = configPath.getFileName().toString();
}
InputStream url = FileLookupFactory.newInstance().lookupFile(path, KeycloakProcessor.class.getClassLoader());
if (url == null) {
throw new IllegalArgumentException("Could not load cluster configuration file at [" + configPath + "]");
}
try (BufferedReader reader = new BufferedReader(new InputStreamReader(url))) {
String config = reader.lines().collect(Collectors.joining("\n"));
syntheticBeanBuildItems.produce(SyntheticBeanBuildItem.configure(CacheInitializer.class)
.scope(ApplicationScoped.class)
.unremovable()
.setRuntimeInit()
.runtimeValue(recorder.createCacheInitializer(config)).done());
} catch (Exception cause) {
throw new RuntimeException("Failed to read clustering configuration from [" + url + "]", cause);
}
} else {
throw new IllegalArgumentException("Option 'configFile' needs to be specified");
}
}
}

View file

@ -17,7 +17,7 @@
package org.keycloak.quarkus.deployment;
import static org.keycloak.quarkus.runtime.configuration.Configuration.getConfigValue;
import static org.keycloak.quarkus.runtime.configuration.ConfigArgsConfigSource.CLI_ARGS;
import static org.keycloak.quarkus.runtime.configuration.Configuration.getPropertyNames;
import static org.keycloak.quarkus.runtime.storage.database.jpa.QuarkusJpaConnectionProviderFactory.QUERY_PROPERTY_PREFIX;
import static org.keycloak.connections.jpa.util.JpaUtils.loadSpecificNamedQueries;
@ -25,22 +25,16 @@ import static org.keycloak.quarkus.runtime.configuration.MicroProfileConfigProvi
import static org.keycloak.representations.provider.ScriptProviderDescriptor.AUTHENTICATORS;
import static org.keycloak.representations.provider.ScriptProviderDescriptor.MAPPERS;
import static org.keycloak.representations.provider.ScriptProviderDescriptor.POLICIES;
import static org.keycloak.quarkus.runtime.Environment.CLI_ARGS;
import static org.keycloak.quarkus.runtime.Environment.getProviderFiles;
import javax.enterprise.context.ApplicationScoped;
import javax.persistence.Entity;
import javax.persistence.spi.PersistenceUnitTransactionType;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
@ -56,12 +50,11 @@ import java.util.function.Consumer;
import java.util.function.Function;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.stream.Collectors;
import io.quarkus.agroal.spi.JdbcDataSourceBuildItem;
import io.quarkus.arc.deployment.SyntheticBeanBuildItem;
import io.quarkus.deployment.IsDevelopment;
import io.quarkus.deployment.builditem.CombinedIndexBuildItem;
import io.quarkus.deployment.builditem.GeneratedResourceBuildItem;
import io.quarkus.deployment.builditem.HotDeploymentWatchedFileBuildItem;
import io.quarkus.deployment.builditem.IndexDependencyBuildItem;
import io.quarkus.deployment.builditem.LaunchModeBuildItem;
@ -77,7 +70,6 @@ import io.vertx.core.Handler;
import io.vertx.ext.web.RoutingContext;
import org.hibernate.cfg.AvailableSettings;
import org.hibernate.jpa.boot.internal.ParsedPersistenceXmlDescriptor;
import org.infinispan.commons.util.FileLookupFactory;
import org.jboss.jandex.AnnotationInstance;
import org.jboss.jandex.AnnotationTarget;
import org.jboss.jandex.DotName;
@ -86,6 +78,7 @@ import org.jboss.logging.Logger;
import org.jboss.resteasy.plugins.server.servlet.ResteasyContextParameters;
import org.jboss.resteasy.spi.ResteasyDeployment;
import org.keycloak.Config;
import org.keycloak.quarkus.runtime.configuration.PersistedConfigSource;
import org.keycloak.quarkus.runtime.integration.jaxrs.QuarkusKeycloakApplication;
import org.keycloak.authentication.AuthenticatorSpi;
import org.keycloak.authentication.authenticators.browser.DeployedScriptAuthenticatorFactory;
@ -118,7 +111,6 @@ import io.quarkus.deployment.annotations.Record;
import io.quarkus.deployment.builditem.FeatureBuildItem;
import io.quarkus.vertx.http.deployment.FilterBuildItem;
import org.keycloak.quarkus.runtime.storage.infinispan.CacheInitializer;
import org.keycloak.representations.provider.ScriptProviderDescriptor;
import org.keycloak.representations.provider.ScriptProviderMetadata;
import org.keycloak.quarkus.runtime.integration.web.NotFoundHandler;
@ -275,12 +267,9 @@ class KeycloakProcessor {
/**
* <p>Make the build time configuration available at runtime so that the server can run without having to specify some of
* the properties again.
*
* @param recorder the recorder
*/
@Record(ExecutionTime.STATIC_INIT)
@BuildStep(onlyIf = isReAugmentation.class)
void setBuildTimeProperties(KeycloakRecorder recorder) {
void persistBuildTimeProperties(BuildProducer<GeneratedResourceBuildItem> resources) {
Properties properties = new Properties();
for (String name : getPropertyNames()) {
@ -299,62 +288,11 @@ class KeycloakProcessor {
properties.put(String.format("kc.provider.file.%s.last-modified", jar.getName()), String.valueOf(jar.lastModified()));
}
File file = KeycloakConfigSourceProvider.getPersistedConfigFile().toFile();
if (file.exists()) {
file.delete();
}
try (FileOutputStream fos = new FileOutputStream(file)) {
properties.store(fos, " Auto-generated, DO NOT change this file");
} catch (Exception e) {
throw new RuntimeException("Failed to generate persisted.properties file", e);
}
}
@Record(ExecutionTime.RUNTIME_INIT)
@BuildStep
void configureInfinispan(KeycloakRecorder recorder, BuildProducer<SyntheticBeanBuildItem> syntheticBeanBuildItems) {
String pathPrefix;
String homeDir = Environment.getHomeDir();
if (homeDir == null) {
pathPrefix = "";
} else {
pathPrefix = homeDir + "/conf/";
}
String configFile = getConfigValue("kc.spi.connections-infinispan.quarkus.config-file").getValue();
if (configFile != null) {
Path configPath = Paths.get(pathPrefix + configFile);
String path;
if (configPath.toFile().exists()) {
path = configPath.toFile().getAbsolutePath();
} else {
path = configPath.getFileName().toString();
}
InputStream url = FileLookupFactory.newInstance().lookupFile(path, KeycloakProcessor.class.getClassLoader());
if (url == null) {
throw new IllegalArgumentException("Could not load cluster configuration file at [" + configPath + "]");
}
try (BufferedReader reader = new BufferedReader(new InputStreamReader(url))) {
String config = reader.lines().collect(Collectors.joining("\n"));
syntheticBeanBuildItems.produce(SyntheticBeanBuildItem.configure(CacheInitializer.class)
.scope(ApplicationScoped.class)
.unremovable()
.setRuntimeInit()
.runtimeValue(recorder.createCacheInitializer(config)).done());
} catch (Exception cause) {
throw new RuntimeException("Failed to read clustering configuration from [" + url + "]", cause);
}
} else {
throw new IllegalArgumentException("Option 'configFile' needs to be specified");
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
properties.store(outputStream, " Auto-generated, DO NOT change this file");
resources.produce(new GeneratedResourceBuildItem(PersistedConfigSource.PERSISTED_PROPERTIES, outputStream.toByteArray()));
} catch (Exception cause) {
throw new RuntimeException("Failed to persist configuration", cause);
}
}

View file

@ -17,13 +17,16 @@
package org.keycloak.quarkus.runtime;
import static org.keycloak.quarkus.runtime.configuration.Configuration.getBuiltTimeProperty;
import static org.keycloak.quarkus.runtime.configuration.Configuration.getBuildTimeProperty;
import java.io.File;
import java.io.FilenameFilter;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.Arrays;
import java.util.Collections;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Collectors;
@ -34,13 +37,11 @@ import org.apache.commons.lang3.SystemUtils;
public final class Environment {
public static final String IMPORT_EXPORT_MODE = "import_export";
public static final String CLI_ARGS = "kc.config.args";
public static final String PROFILE ="kc.profile";
public static final String ENV_PROFILE ="KC_PROFILE";
public static final String DATA_PATH = "/data";
public static final String DEFAULT_THEMES_PATH = "/themes";
public static final String DEV_PROFILE_VALUE = "dev";
public static final String USER_INVOKED_CLI_COMMAND = "picocli.invoked.command";
public static final String LAUNCH_MODE = "kc.launch.mode";
private Environment() {}
@ -95,33 +96,6 @@ public final class Environment {
return "kc.sh";
}
/**
* Sets the originally invoked cli args. Useful to verify the originally invoked command
* when calling another cli command internally (e.g. start-dev calls build internally)
*/
public static void setUserInvokedCliArgs(List<String> cliArgs) {
System.setProperty(USER_INVOKED_CLI_COMMAND, String.join(",", cliArgs));
}
/**
* Reads the previously set system property for the originally command.
* Use the System variable, when you trigger other command executions internally, but need a reference to the
* actually invoked command.
*
* @return the invoked command from the CLI, or empty List if not set.
*/
public static List<String> getUserInvokedCliArgs() {
if(System.getProperty(USER_INVOKED_CLI_COMMAND) == null) {
return Collections.emptyList();
}
return List.of(System.getProperty(USER_INVOKED_CLI_COMMAND).split(","));
}
public static String getConfigArgs() {
return System.getProperty(CLI_ARGS, "");
}
public static String getProfile() {
String profile = System.getProperty(PROFILE);
@ -156,7 +130,7 @@ public final class Environment {
return true;
}
return DEV_PROFILE_VALUE.equals(getBuiltTimeProperty(PROFILE).orElse(null));
return DEV_PROFILE_VALUE.equals(getBuildTimeProperty(PROFILE).orElse(null));
}
public static boolean isDevProfile(){

View file

@ -21,7 +21,9 @@ import static org.keycloak.quarkus.runtime.Environment.isDevProfile;
import static org.keycloak.quarkus.runtime.Environment.getProfileOrDefault;
import static org.keycloak.quarkus.runtime.Environment.isTestLaunchMode;
import static org.keycloak.quarkus.runtime.cli.Picocli.parseAndRun;
import static org.keycloak.quarkus.runtime.cli.command.Start.isDevProfileNotAllowed;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@ -35,10 +37,10 @@ import org.jboss.logging.Logger;
import org.keycloak.quarkus.runtime.cli.ExecutionExceptionHandler;
import org.keycloak.quarkus.runtime.cli.Picocli;
import org.keycloak.common.Version;
import org.keycloak.quarkus.runtime.cli.command.Start;
import io.quarkus.runtime.QuarkusApplication;
import io.quarkus.runtime.annotations.QuarkusMain;
import picocli.CommandLine;
/**
* <p>The main entry point, responsible for initialize and run the CLI as well as start the server.
@ -51,25 +53,36 @@ public class KeycloakMain implements QuarkusApplication {
public static void main(String[] args) {
System.setProperty("kc.version", Version.VERSION_KEYCLOAK);
List<String> cliArgs = new ArrayList<>(Arrays.asList(args));
System.setProperty(Environment.CLI_ARGS, Picocli.parseConfigArgs(cliArgs));
List<String> cliArgs = Picocli.parseArgs(args);
if (cliArgs.isEmpty()) {
cliArgs = new ArrayList<>(cliArgs);
// default to show help message
cliArgs.add("-h");
} else if (cliArgs.contains(Start.NAME) && cliArgs.size() == 1) {
// fast path for starting the server without bootstrapping CLI
ExecutionExceptionHandler errorHandler = new ExecutionExceptionHandler();
PrintWriter errStream = new PrintWriter(System.err, true);
if (isDevProfileNotAllowed(Arrays.asList(args))) {
errorHandler.error(errStream, Messages.devProfileNotAllowedError(Start.NAME), null);
return;
}
start(errorHandler, errStream);
return;
}
// parse arguments and execute any of the configured commands
parseAndRun(cliArgs);
}
public static void start(CommandLine cmd) {
public static void start(ExecutionExceptionHandler errorHandler, PrintWriter errStream) {
try {
Quarkus.run(KeycloakMain.class, (exitCode, cause) -> {
if (cause != null) {
ExecutionExceptionHandler exceptionHandler = (ExecutionExceptionHandler) cmd.getExecutionExceptionHandler();
exceptionHandler.error(cmd.getErr(),
errorHandler.error(errStream,
String.format("Failed to start server using profile (%s)", getProfileOrDefault("prod")),
cause.getCause());
}
@ -81,9 +94,7 @@ public class KeycloakMain implements QuarkusApplication {
}
});
} catch (Throwable cause) {
ExecutionExceptionHandler exceptionHandler = (ExecutionExceptionHandler) cmd.getExecutionExceptionHandler();
exceptionHandler.error(cmd.getErr(),
errorHandler.error(errStream,
String.format("Unexpected error when starting the server using profile (%s)", getProfileOrDefault("prod")),
cause.getCause());
}

View file

@ -17,7 +17,7 @@
package org.keycloak.quarkus.runtime;
import static org.keycloak.quarkus.runtime.configuration.Configuration.getBuiltTimeProperty;
import static org.keycloak.quarkus.runtime.configuration.Configuration.getBuildTimeProperty;
import java.util.List;
import java.util.Map;
@ -85,10 +85,10 @@ public class KeycloakRecorder {
feature = "kc.features";
}
Optional<String> value = getBuiltTimeProperty(feature);
Optional<String> value = getBuildTimeProperty(feature);
if (value.isEmpty()) {
value = getBuiltTimeProperty(feature.replaceAll("\\.features\\.", "\\.features-"));
value = getBuildTimeProperty(feature.replaceAll("\\.features\\.", "\\.features-"));
}
if (value.isPresent()) {

View file

@ -45,4 +45,8 @@ public final class Messages {
public static void cliExecutionError(CommandLine cmd, String message, Throwable cause) {
throw new CommandLine.ExecutionException(cmd, message, cause);
}
public static String devProfileNotAllowedError(String cmd) {
return String.format("You can not '%s' the server using the '%s' configuration profile. Please re-build the server first, using 'kc.sh build' for the default production profile, or using 'kc.sh build --profile=<profile>' with a profile more suitable for production.%n", cmd, Environment.DEV_PROFILE_VALUE);
}
}

View file

@ -0,0 +1,29 @@
/*
* 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.cli;
import picocli.CommandLine.IFactory;
public class DefaultFactory implements IFactory {
@Override
public <K> K create(Class<K> cls) throws Exception {
// picocli tries different approaches for creating instances, this is what we need
return cls.getDeclaredConstructor().newInstance();
}
}

View file

@ -38,7 +38,7 @@ public final class ExecutionExceptionHandler implements CommandLine.IExecutionEx
private Logger logger;
private boolean verbose;
ExecutionExceptionHandler() {}
public ExecutionExceptionHandler() {}
@Override
public int handleExecutionException(Exception cause, CommandLine cmd, ParseResult parseResult) {

View file

@ -21,7 +21,9 @@ import static io.smallrye.config.common.utils.StringUtil.replaceNonAlphanumericB
import static java.util.Arrays.asList;
import static org.keycloak.quarkus.runtime.cli.command.AbstractStartCommand.AUTO_BUILD_OPTION_LONG;
import static org.keycloak.quarkus.runtime.cli.command.AbstractStartCommand.AUTO_BUILD_OPTION_SHORT;
import static org.keycloak.quarkus.runtime.configuration.Configuration.getBuiltTimeProperty;
import static org.keycloak.quarkus.runtime.configuration.ConfigArgsConfigSource.hasOptionValue;
import static org.keycloak.quarkus.runtime.configuration.ConfigArgsConfigSource.parseConfigArgs;
import static org.keycloak.quarkus.runtime.configuration.Configuration.getBuildTimeProperty;
import static org.keycloak.quarkus.runtime.configuration.Configuration.getConfig;
import static org.keycloak.quarkus.runtime.Environment.isDevMode;
import static org.keycloak.quarkus.runtime.configuration.Configuration.getRuntimeProperty;
@ -31,19 +33,16 @@ import static org.keycloak.utils.StringUtil.isNotBlank;
import static picocli.CommandLine.Model.UsageMessageSpec.SECTION_KEY_COMMAND_LIST;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Properties;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.Predicate;
import java.util.function.UnaryOperator;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import org.keycloak.quarkus.runtime.cli.command.Build;
@ -51,10 +50,10 @@ import org.keycloak.quarkus.runtime.cli.command.Main;
import org.keycloak.quarkus.runtime.cli.command.Start;
import org.keycloak.quarkus.runtime.cli.command.StartDev;
import org.keycloak.common.Profile;
import org.keycloak.quarkus.runtime.configuration.MicroProfileConfigProvider;
import org.keycloak.quarkus.runtime.configuration.ConfigArgsConfigSource;
import org.keycloak.quarkus.runtime.configuration.PersistedConfigSource;
import org.keycloak.quarkus.runtime.configuration.mappers.ConfigCategory;
import org.keycloak.quarkus.runtime.configuration.mappers.PropertyMappers;
import org.keycloak.quarkus.runtime.configuration.KeycloakConfigSourceProvider;
import org.keycloak.quarkus.runtime.configuration.mappers.PropertyMapper;
import org.keycloak.quarkus.runtime.Environment;
@ -66,13 +65,10 @@ import picocli.CommandLine.Model.ArgGroupSpec;
public final class Picocli {
private static final String ARG_SEPARATOR = ";;";
public static final String ARG_PREFIX = "--";
private static final String ARG_KEY_VALUE_SEPARATOR = "=";
public static final String ARG_SHORT_PREFIX = "-";
public static final String ARG_PART_SEPARATOR = "-";
public static final char ARG_KEY_VALUE_SEPARATOR = '=';
public static final Pattern ARG_SPLIT = Pattern.compile(";;");
public static final Pattern ARG_KEY_VALUE_SPLIT = Pattern.compile("=");
public static final String NO_PARAM_LABEL = "none";
private Picocli() {
@ -95,8 +91,6 @@ public final class Picocli {
// force the server image to be set with the dev profile
Environment.forceDevProfile();
}
Environment.setUserInvokedCliArgs(cliArgs);
}
if (requiresReAugmentation(cmd)) {
runReAugmentation(cliArgs, cmd);
@ -112,20 +106,30 @@ public final class Picocli {
return cliArgs.contains("--help") || cliArgs.contains("-h") || cliArgs.contains("--help-all");
}
private static boolean hasAutoBuildOption(List<String> cliArgs) {
public static boolean hasAutoBuildOption(List<String> cliArgs) {
return cliArgs.contains(AUTO_BUILD_OPTION_LONG) || cliArgs.contains(AUTO_BUILD_OPTION_SHORT);
}
private static boolean requiresReAugmentation(CommandLine cmd) {
public static boolean requiresReAugmentation(CommandLine cmd) {
if (hasConfigChanges()) {
cmd.getOut().println("Changes detected in configuration. Updating the server image.");
Predicate<String> profileOptionMatcher = Main.PROFILE_LONG_NAME::equals;
profileOptionMatcher = profileOptionMatcher.or(Main.PROFILE_SHORT_NAME::equals);
if (hasOptionValue(profileOptionMatcher, "dev") && !ConfigArgsConfigSource.getAllCliArgs().contains(StartDev.NAME)) {
return false;
}
if(!isDevMode()) {
List<String> cliInput = getSanitizedCliInput();
cmd.getOut().printf("For an optional runtime and bypass this step, please run the '%s' command prior to starting the server:%n%n\t%s %s %s%n",
Build.NAME,
Environment.getCommand(),
Build.NAME,
String.join(" ", cliInput) + "\n");
if (cmd != null) {
cmd.getOut().println("Changes detected in configuration. Updating the server image.");
List<String> cliInput = getSanitizedCliInput();
cmd.getOut()
.printf("For an optional runtime and bypass this step, please run the '%s' command prior to starting the server:%n%n\t%s %s %s%n%n",
Build.NAME,
Environment.getCommand(),
Build.NAME,
String.join(" ", cliInput) + "\n");
}
}
return true;
}
@ -140,29 +144,15 @@ public final class Picocli {
* instead of the actual passwords value.
*/
private static List<String> getSanitizedCliInput() {
if(Environment.getConfigArgs().isEmpty()) {
return Collections.emptyList();
}
List<String> rawCliArgs = asList(ARG_SPLIT.split(Environment.getConfigArgs()));
List<String> properties = new ArrayList<>();
if (!rawCliArgs.isEmpty()) {
for(String rawCliArg : rawCliArgs) {
String rawKey = rawCliArg.split("=")[0];
PropertyMapper mapper = PropertyMappers.getMapper(rawKey);
String value = rawCliArg.split("=")[1];
if (mapper != null) {
value = formatValue(
mapper.getFrom(),
value);
}
properties.add(rawKey + "=" + value);
parseConfigArgs(new BiConsumer<String, String>() {
@Override
public void accept(String key, String value) {
properties.add(key + "=" + formatValue(key, value));
}
}
});
return properties;
}
@ -190,23 +180,14 @@ public final class Picocli {
}
private static boolean hasProviderChanges() {
File propertiesFile = KeycloakConfigSourceProvider.getPersistedConfigFile().toFile();
Map<String, String> persistedProps = PersistedConfigSource.getInstance().getProperties();
Map<String, File> deployedProviders = Environment.getProviderFiles();
if (!propertiesFile.exists()) {
if (persistedProps.isEmpty()) {
return !deployedProviders.isEmpty();
}
Properties properties = new Properties();
try (InputStream is = new FileInputStream(propertiesFile)) {
properties.load(is);
} catch (Exception e) {
throw new RuntimeException("Failed to load persisted properties", e);
}
Set<String> providerKeys = properties.stringPropertyNames().stream().filter(Picocli::isProviderKey).collect(
Collectors.toSet());
Set<String> providerKeys = persistedProps.keySet().stream().filter(Picocli::isProviderKey).collect(Collectors.toSet());
if (deployedProviders.size() != providerKeys.size()) {
return true;
@ -220,7 +201,7 @@ public final class Picocli {
}
File file = deployedProviders.get(fileName);
String lastModified = properties.getProperty(key);
String lastModified = persistedProps.get(key);
if (!lastModified.equals(String.valueOf(file.lastModified()))) {
return true;
@ -232,7 +213,7 @@ public final class Picocli {
private static boolean hasConfigChanges() {
Optional<String> currentProfile = Optional.ofNullable(Environment.getProfile());
Optional<String> persistedProfile = getBuiltTimeProperty("kc.profile");
Optional<String> persistedProfile = getBuildTimeProperty("kc.profile");
if (!persistedProfile.orElse("").equals(currentProfile.orElse(""))) {
return true;
@ -255,7 +236,7 @@ public final class Picocli {
propertyName = propertyName.substring(propertyName.indexOf('.') + 1);
}
String persistedValue = getBuiltTimeProperty(propertyName).orElse("");
String persistedValue = getBuildTimeProperty(propertyName).orElse("");
String runtimeValue = getRuntimeProperty(propertyName).orElse(null);
if (runtimeValue == null && isNotBlank(persistedValue)) {
@ -277,8 +258,7 @@ public final class Picocli {
}
public static CommandLine createCommandLine(List<String> cliArgs) {
CommandSpec spec = CommandSpec.forAnnotatedObject(new Main())
.name(Environment.getCommand());
CommandSpec spec = CommandSpec.forAnnotatedObject(new Main(), new DefaultFactory()).name(Environment.getCommand());
for (CommandLine subCommand : spec.subcommands().values()) {
CommandSpec subCommandSpec = subCommand.getCommandSpec();
@ -290,66 +270,20 @@ public final class Picocli {
.build());
}
boolean isStartCommand = cliArgs.size() == 1 && cliArgs.contains(Start.NAME);
// avoid unnecessary processing when starting the server
if (!isStartCommand) {
addOption(spec, Start.NAME, hasAutoBuildOption(cliArgs));
addOption(spec, StartDev.NAME, true);
addOption(spec, Build.NAME, true);
}
addOption(spec, Start.NAME, hasAutoBuildOption(cliArgs));
addOption(spec, StartDev.NAME, true);
addOption(spec, Build.NAME, true);
CommandLine cmd = new CommandLine(spec);
cmd.setExecutionExceptionHandler(new ExecutionExceptionHandler());
if (!isStartCommand) {
cmd.setHelpFactory(new HelpFactory());
cmd.getHelpSectionMap().put(SECTION_KEY_COMMAND_LIST, new SubCommandListRenderer());
}
cmd.setHelpFactory(new HelpFactory());
cmd.getHelpSectionMap().put(SECTION_KEY_COMMAND_LIST, new SubCommandListRenderer());
return cmd;
}
public static String parseConfigArgs(List<String> argsList) {
StringBuilder options = new StringBuilder();
Iterator<String> iterator = argsList.iterator();
boolean expectValue = false;
List<String> ignoredArgs = asList("--verbose", "-v", "--help", "-h", AUTO_BUILD_OPTION_LONG, AUTO_BUILD_OPTION_SHORT);
while (iterator.hasNext()) {
String key = iterator.next();
// TODO: ignore properties for providers for now, need to fetch them from the providers, otherwise CLI will complain about invalid options
// change this once we are able to obtain properties from providers
if (key.startsWith("--spi")) {
iterator.remove();
}
if (ignoredArgs.contains(key)) {
continue;
}
if (key.startsWith(ARG_PREFIX)) {
if (options.length() > 0) {
options.append(ARG_SEPARATOR);
}
options.append(key);
if (key.indexOf(ARG_KEY_VALUE_SEPARATOR) == -1) {
// values can be set using spaces (e.g.: --option <value>)
expectValue = true;
}
} else if (expectValue) {
options.append(ARG_KEY_VALUE_SEPARATOR).append(key);
expectValue = false;
}
}
return options.toString();
}
private static void addOption(CommandSpec spec, String command, boolean includeBuildTime) {
CommandSpec commandSpec = spec.subcommands().get(command).getCommandSpec();
List<PropertyMapper> mappers = new ArrayList<>(PropertyMappers.getRuntimeMappers());
@ -443,4 +377,36 @@ public final class Picocli {
public static String normalizeKey(String key) {
return replaceNonAlphanumericByUnderscores(key).replace('_', '.');
}
public static List<String> parseArgs(String[] rawArgs) {
if (rawArgs.length == 0) {
return List.of();
}
// makes sure cli args are available to the config source
ConfigArgsConfigSource.setCliArgs(rawArgs);
List<String> args = new ArrayList<>(List.of(rawArgs));
Iterator<String> iterator = args.iterator();
while (iterator.hasNext()) {
String arg = iterator.next();
if (arg.startsWith("--spi")) {
// TODO: ignore properties for providers for now, need to fetch them from the providers, otherwise CLI will complain about invalid options
// change this once we are able to obtain properties from providers
iterator.remove();
if (!arg.contains(ARG_KEY_VALUE_SEPARATOR)) {
String next = iterator.next();
if (!next.startsWith("--")) {
// ignore the value if the arg is using space as separator
iterator.remove();
}
}
}
}
return args;
}
}

View file

@ -19,8 +19,6 @@ package org.keycloak.quarkus.runtime.cli.command;
import static org.keycloak.quarkus.runtime.Messages.cliExecutionError;
import org.keycloak.quarkus.runtime.Environment;
import picocli.CommandLine;
import picocli.CommandLine.Model.CommandSpec;
import picocli.CommandLine.Spec;
@ -30,10 +28,6 @@ public abstract class AbstractCommand {
@Spec
protected CommandSpec spec;
protected void devProfileNotAllowedError(String cmd) {
executionError(spec.commandLine(), String.format("You can not '%s' the server using the '%s' configuration profile. Please re-build the server first, using 'kc.sh build' for the default production profile, or using 'kc.sh build --profile=<profile>' with a profile more suitable for production.%n", cmd, Environment.DEV_PROFILE_VALUE));
}
protected void executionError(CommandLine cmd, String message) {
executionError(cmd, message, null);
}

View file

@ -18,6 +18,9 @@
package org.keycloak.quarkus.runtime.cli.command;
import org.keycloak.quarkus.runtime.KeycloakMain;
import org.keycloak.quarkus.runtime.cli.ExecutionExceptionHandler;
import picocli.CommandLine;
public abstract class AbstractStartCommand extends AbstractCommand implements Runnable {
@ -27,7 +30,8 @@ public abstract class AbstractStartCommand extends AbstractCommand implements Ru
@Override
public void run() {
doBeforeRun();
KeycloakMain.start(spec.commandLine());
CommandLine cmd = spec.commandLine();
KeycloakMain.start((ExecutionExceptionHandler) cmd.getExecutionExceptionHandler(), cmd.getErr());
}
protected void doBeforeRun() {

View file

@ -20,8 +20,10 @@ package org.keycloak.quarkus.runtime.cli.command;
import static org.keycloak.quarkus.runtime.Environment.getHomePath;
import static org.keycloak.quarkus.runtime.Environment.isDevMode;
import static org.keycloak.quarkus.runtime.cli.Picocli.println;
import static org.keycloak.quarkus.runtime.configuration.ConfigArgsConfigSource.getAllCliArgs;
import org.keycloak.quarkus.runtime.Environment;
import org.keycloak.quarkus.runtime.Messages;
import io.quarkus.bootstrap.runner.QuarkusEntryPoint;
import io.quarkus.bootstrap.runner.RunnerClassLoader;
@ -30,10 +32,6 @@ import io.quarkus.runtime.configuration.ProfileManager;
import picocli.CommandLine.Command;
import picocli.CommandLine.Mixin;
import java.io.IOException;
import java.nio.file.Files;
import java.util.List;
@Command(name = Build.NAME,
header = "Creates a new and optimized server image.",
description = {
@ -94,9 +92,8 @@ public final class Build extends AbstractCommand implements Runnable {
}
private void exitWithErrorIfDevProfileIsSetAndNotStartDev() {
List<String> userInvokedCliArgs = Environment.getUserInvokedCliArgs();
if(Environment.isDevProfile() && !userInvokedCliArgs.contains(StartDev.NAME)) {
devProfileNotAllowedError(Build.NAME);
if (Environment.isDevProfile() && !getAllCliArgs().contains(StartDev.NAME)) {
executionError(spec.commandLine(), Messages.devProfileNotAllowedError(NAME));
}
}
@ -117,11 +114,7 @@ public final class Build extends AbstractCommand implements Runnable {
private void cleanTempResources() {
if (!ProfileManager.getLaunchMode().isDevOrTest()) {
// only needed for dev/testing purposes
try {
Files.delete(getHomePath().resolve("quarkus-artifact.properties"));
} catch (IOException cause) {
throw new RuntimeException("Failed to delete temporary resources", cause);
}
getHomePath().resolve("quarkus-artifact.properties").toFile().delete();
}
}
}

View file

@ -21,7 +21,7 @@ import org.keycloak.quarkus.runtime.cli.Help;
import picocli.CommandLine;
final class HelpAllMixin {
public final class HelpAllMixin {
@CommandLine.Spec
private CommandLine.Model.CommandSpec spec;

View file

@ -68,6 +68,9 @@ import picocli.CommandLine.Option;
})
public final class Main {
public static final String PROFILE_SHORT_NAME = "-pf";
public static final String PROFILE_LONG_NAME = "--profile";
@CommandLine.Spec
CommandLine.Model.CommandSpec spec;
@ -94,7 +97,7 @@ public final class Main {
exceptionHandler.setVerbose(verbose);
}
@Option(names = {"-pf", "--profile"},
@Option(names = { PROFILE_SHORT_NAME, PROFILE_LONG_NAME },
description = "Set the profile. Use 'dev' profile to enable development mode.")
public void setProfile(String profile) {
Environment.setProfile(profile);

View file

@ -17,8 +17,7 @@
package org.keycloak.quarkus.runtime.cli.command;
import static java.lang.Boolean.parseBoolean;
import static org.keycloak.quarkus.runtime.configuration.Configuration.getBuiltTimeProperty;
import static org.keycloak.quarkus.runtime.configuration.Configuration.getBuildTimeProperty;
import static org.keycloak.quarkus.runtime.configuration.Configuration.getConfigValue;
import static org.keycloak.quarkus.runtime.configuration.Configuration.getPropertyNames;
import static org.keycloak.quarkus.runtime.configuration.mappers.PropertyMappers.canonicalFormat;
@ -119,7 +118,7 @@ public final class ShowConfig extends AbstractCommand implements Runnable {
String profile = Environment.getProfile();
if (profile == null) {
return getBuiltTimeProperty("quarkus.profile").orElse(null);
return getBuildTimeProperty("quarkus.profile").orElse(null);
}
return profile;

View file

@ -17,12 +17,13 @@
package org.keycloak.quarkus.runtime.cli.command;
import static org.keycloak.quarkus.runtime.Environment.isDevProfile;
import static org.keycloak.quarkus.runtime.Environment.setProfile;
import static org.keycloak.quarkus.runtime.cli.Picocli.NO_PARAM_LABEL;
import static org.keycloak.quarkus.runtime.configuration.Configuration.getBuiltTimeProperty;
import static org.keycloak.quarkus.runtime.configuration.Configuration.getRawPersistedProperty;
import org.keycloak.quarkus.runtime.Environment;
import org.keycloak.quarkus.runtime.Messages;
import picocli.CommandLine;
import picocli.CommandLine.Command;
@ -51,23 +52,25 @@ public final class Start extends AbstractStartCommand implements Runnable {
@Override
protected void doBeforeRun() {
checkIfProfileIsNotDev();
devProfileNotAllowedError();
}
/**
* Checks if the profile provided by either the current argument, the system environment or the persisted properties is dev.
* Fails with an error when dev profile is used for the start command, or continues with the found profile if its not the dev profile.
*/
private void checkIfProfileIsNotDev() {
List<String> currentCliArgs = spec.commandLine().getParseResult().expandedArgs();
private void devProfileNotAllowedError() {
if (isDevProfileNotAllowed(spec.commandLine().getParseResult().expandedArgs())) {
executionError(spec.commandLine(), Messages.devProfileNotAllowedError(NAME));
}
}
public static boolean isDevProfileNotAllowed(List<String> currentCliArgs) {
Optional<String> currentProfile = Optional.ofNullable(Environment.getProfile());
Optional<String> persistedProfile = getBuiltTimeProperty("kc.profile");
Optional<String> persistedProfile = getRawPersistedProperty("kc.profile");
setProfile(currentProfile.orElse(persistedProfile.orElse("prod")));
if (isDevProfile() && (!currentCliArgs.contains(AUTO_BUILD_OPTION_LONG) || !currentCliArgs.contains(AUTO_BUILD_OPTION_SHORT))) {
devProfileNotAllowedError(Start.NAME);
if (Environment.isDevProfile() && (!currentCliArgs.contains(AUTO_BUILD_OPTION_LONG) || !currentCliArgs.contains(AUTO_BUILD_OPTION_SHORT))) {
return true;
}
return false;
}
}

View file

@ -17,21 +17,27 @@
package org.keycloak.quarkus.runtime.configuration;
import static java.util.Arrays.asList;
import static org.keycloak.quarkus.runtime.Environment.isTestLaunchMode;
import static org.keycloak.quarkus.runtime.cli.Picocli.ARG_KEY_VALUE_SPLIT;
import static org.keycloak.quarkus.runtime.cli.Picocli.ARG_PREFIX;
import static org.keycloak.quarkus.runtime.cli.Picocli.ARG_SPLIT;
import static org.keycloak.quarkus.runtime.cli.Picocli.ARG_SHORT_PREFIX;
import static org.keycloak.quarkus.runtime.cli.command.AbstractStartCommand.AUTO_BUILD_OPTION_LONG;
import static org.keycloak.quarkus.runtime.cli.command.AbstractStartCommand.AUTO_BUILD_OPTION_SHORT;
import static org.keycloak.quarkus.runtime.configuration.Configuration.getMappedPropertyName;
import static org.keycloak.quarkus.runtime.configuration.MicroProfileConfigProvider.NS_KEYCLOAK_PREFIX;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.BiConsumer;
import java.util.function.Predicate;
import java.util.regex.Pattern;
import org.jboss.logging.Logger;
import io.smallrye.config.PropertiesConfigSource;
import org.keycloak.quarkus.runtime.Environment;
import org.keycloak.quarkus.runtime.cli.Picocli;
import org.keycloak.quarkus.runtime.configuration.mappers.PropertyMapper;
import org.keycloak.quarkus.runtime.configuration.mappers.PropertyMappers;
@ -49,14 +55,48 @@ public class ConfigArgsConfigSource extends PropertiesConfigSource {
private static final Logger log = Logger.getLogger(ConfigArgsConfigSource.class);
public static final String CLI_ARGS = "kc.config.args";
private static final String ARG_SEPARATOR = ";;";
private static final Pattern ARG_SPLIT = Pattern.compile(";;");
private static final Pattern ARG_KEY_VALUE_SPLIT = Pattern.compile("=");
private static final ConfigArgsConfigSource INSTANCE = new ConfigArgsConfigSource();
private static List<String> IGNORED_ARGS;
private final boolean alwaysParseArgs;
public static ConfigArgsConfigSource getInstance() {
return INSTANCE;
}
ConfigArgsConfigSource() {
// higher priority over default Quarkus config sources
super(parseArgument(), "CliConfigSource", 500);
alwaysParseArgs = isTestLaunchMode();
}
public static void setCliArgs(String[] args) {
System.setProperty(CLI_ARGS, String.join(ARG_SEPARATOR, args));
}
/**
* Reads the previously set system property for the originally command.
* Use the System variable, when you trigger other command executions internally, but need a reference to the
* actually invoked command.
*
* @return the invoked command from the CLI, or empty List if not set.
*/
public static List<String> getAllCliArgs() {
if(System.getProperty(CLI_ARGS) == null) {
return Collections.emptyList();
}
return List.of(System.getProperty(CLI_ARGS).split(ARG_SEPARATOR));
}
private static String getRawConfigArgs() {
return System.getProperty(CLI_ARGS, "");
}
@Override
public String getValue(String propertyName) {
Map<String, String> properties = getProperties();
@ -75,31 +115,93 @@ public class ConfigArgsConfigSource extends PropertiesConfigSource {
}
private static Map<String, String> parseArgument() {
String args = Environment.getConfigArgs();
// init here because the class might be loaded by CL without init
IGNORED_ARGS = asList("--verbose", "-v", "--help", "-h", AUTO_BUILD_OPTION_LONG, AUTO_BUILD_OPTION_SHORT);
String rawArgs = getRawConfigArgs();
if (args == null || "".equals(args.trim())) {
if (rawArgs == null || "".equals(rawArgs.trim())) {
log.trace("No command-line arguments provided");
return Collections.emptyMap();
}
Map<String, String> properties = new HashMap<>();
for (String arg : ARG_SPLIT.split(args)) {
if (!arg.startsWith(ARG_PREFIX)) {
throw new IllegalArgumentException("Invalid argument format [" + arg + "], arguments must start with '--'");
parseConfigArgs(new BiConsumer<String, String>() {
@Override
public void accept(String key, String value) {
key = NS_KEYCLOAK_PREFIX + key.substring(2);
log.tracef("Adding property [%s=%s] from command-line", key, value);
properties.put(key, value);
String mappedPropertyName = getMappedPropertyName(key);
properties.put(mappedPropertyName, value);
PropertyMapper mapper = PropertyMappers.getMapper(mappedPropertyName);
if (mapper != null) {
properties.put(mapper.getFrom(), value);
}
// to make lookup easier, we normalize the key
properties.put(Picocli.normalizeKey(key), value);
}
});
return properties;
}
public static boolean hasOptionValue(Predicate<String> keyMatcher, String expectedValue) {
AtomicBoolean result = new AtomicBoolean();
parseConfigArgs(new BiConsumer<String, String>() {
@Override
public void accept(String key, String value) {
if (keyMatcher.test(key) && expectedValue.equals(value)) {
result.set(true);
}
}
});
return result.get();
}
public static void parseConfigArgs(BiConsumer<String, String> cliArgConsumer) {
String rawArgs = getRawConfigArgs();
if (rawArgs == null || "".equals(rawArgs.trim())) {
log.trace("No command-line arguments provided");
return;
}
String[] args = ARG_SPLIT.split(rawArgs);
for (int i = 0; i < args.length; i++) {
String arg = args[i];
if (IGNORED_ARGS.contains(arg)) {
continue;
}
if (!arg.startsWith(ARG_SHORT_PREFIX)) {
continue;
}
String[] keyValue = ARG_KEY_VALUE_SPLIT.split(arg);
String key = keyValue[0];
if ("".equals(key.trim())) {
throw new IllegalArgumentException("Invalid argument key");
}
String value;
if (keyValue.length == 1) {
continue;
if (args.length <= i + 1) {
continue;
}
value = args[i + 1];
} else if (keyValue.length == 2) {
// the argument has a simple value. Eg.: key=pair
value = keyValue[1];
@ -107,26 +209,8 @@ public class ConfigArgsConfigSource extends PropertiesConfigSource {
// to support cases like --db-url=jdbc:mariadb://localhost/kc?a=1
value = arg.substring(key.length() + 1);
}
key = NS_KEYCLOAK_PREFIX + key.substring(2);
log.tracef("Adding property [%s=%s] from command-line", key, value);
properties.put(key, value);
String mappedPropertyName = getMappedPropertyName(key);
properties.put(mappedPropertyName, value);
PropertyMapper mapper = PropertyMappers.getMapper(mappedPropertyName);
if (mapper != null) {
properties.put(mapper.getFrom(), value);
}
// to make lookup easier, we normalize the key
properties.put(Picocli.normalizeKey(key), value);
cliArgConsumer.accept(key, value);
}
return properties;
}
}

View file

@ -52,24 +52,28 @@ public final class Configuration {
return CONFIG;
}
public static Optional<String> getBuiltTimeProperty(String name) {
String value = KeycloakConfigSourceProvider.PERSISTED_CONFIG_SOURCE.getValue(name);
public static Optional<String> getBuildTimeProperty(String name) {
Optional<String> value = getRawPersistedProperty(name);
if (value == null) {
value = KeycloakConfigSourceProvider.PERSISTED_CONFIG_SOURCE.getValue(getMappedPropertyName(name));
if (value.isEmpty()) {
value = getRawPersistedProperty(getMappedPropertyName(name));
}
if (value == null) {
if (value.isEmpty()) {
String profile = Environment.getProfile();
if (profile == null) {
profile = getConfig().getRawValue(Environment.PROFILE);
}
value = KeycloakConfigSourceProvider.PERSISTED_CONFIG_SOURCE.getValue("%" + profile + "." + name);
value = getRawPersistedProperty("%" + profile + "." + name);
}
return Optional.ofNullable(value);
return value;
}
public static Optional<String> getRawPersistedProperty(String name) {
return Optional.ofNullable(PersistedConfigSource.getInstance().getValue(name));
}
public static String getRawValue(String propertyName) {

View file

@ -18,22 +18,16 @@
package org.keycloak.quarkus.runtime.configuration;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import org.eclipse.microprofile.config.spi.ConfigSource;
import org.eclipse.microprofile.config.spi.ConfigSourceProvider;
import org.jboss.logging.Logger;
import org.keycloak.quarkus.runtime.Environment;
public class KeycloakConfigSourceProvider implements ConfigSourceProvider {
private static final Logger log = Logger.getLogger(KeycloakConfigSourceProvider.class);
private static final List<ConfigSource> CONFIG_SOURCES = new ArrayList<>();
public static PersistedConfigSource PERSISTED_CONFIG_SOURCE;
// we initialize in a static block to avoid discovering the config sources multiple times when starting the application
static {
@ -50,8 +44,7 @@ public class KeycloakConfigSourceProvider implements ConfigSourceProvider {
CONFIG_SOURCES.add(new ConfigArgsConfigSource());
CONFIG_SOURCES.add(new SysPropConfigSource());
CONFIG_SOURCES.add(new KcEnvConfigSource());
PERSISTED_CONFIG_SOURCE = new PersistedConfigSource(getPersistedConfigFile());
CONFIG_SOURCES.add(PERSISTED_CONFIG_SOURCE);
CONFIG_SOURCES.add(PersistedConfigSource.getInstance());
CONFIG_SOURCES.addAll(new KeycloakPropertiesConfigSource.InFileSystem().getConfigSources(Thread.currentThread().getContextClassLoader()));
@ -68,20 +61,6 @@ public class KeycloakConfigSourceProvider implements ConfigSourceProvider {
initializeSources();
}
public static Path getPersistedConfigFile() {
String homeDir = Environment.getHomeDir();
if (homeDir == null) {
return Paths.get(System.getProperty("java.io.tmpdir"), PersistedConfigSource.KEYCLOAK_PROPERTIES);
}
Path generatedPath = Paths.get(homeDir, "data", "generated");
generatedPath.toFile().mkdirs();
return generatedPath.resolve(PersistedConfigSource.KEYCLOAK_PROPERTIES);
}
@Override
public Iterable<ConfigSource> getConfigSources(ClassLoader forClassLoader) {
return CONFIG_SOURCES;

View file

@ -17,27 +17,33 @@
package org.keycloak.quarkus.runtime.configuration;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.io.InputStream;
import java.net.URL;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import io.smallrye.config.PropertiesConfigSource;
import io.smallrye.config.common.utils.ConfigSourceUtil;
import org.keycloak.quarkus.runtime.Environment;
/**
* A {@link org.eclipse.microprofile.config.spi.ConfigSource} based on the configuration properties persisted into the server
* image.
*/
public class PersistedConfigSource extends PropertiesConfigSource {
public final class PersistedConfigSource extends PropertiesConfigSource {
public static final String NAME = "PersistedConfigSource";
static final String KEYCLOAK_PROPERTIES = "persisted.properties";
public static final String PERSISTED_PROPERTIES = "/META-INF/keycloak-persisted.properties";
private static final PersistedConfigSource INSTANCE = new PersistedConfigSource();
public PersistedConfigSource(Path file) {
super(readProperties(file), "", 300);
private PersistedConfigSource() {
super(readProperties(), "", 300);
}
public static PersistedConfigSource getInstance() {
return INSTANCE;
}
@Override
@ -45,11 +51,6 @@ public class PersistedConfigSource extends PropertiesConfigSource {
return NAME;
}
@Override
public Map<String, String> getProperties() {
return Collections.emptyMap();
}
@Override
public String getValue(String propertyName) {
String value = super.getValue(propertyName);
@ -61,19 +62,46 @@ public class PersistedConfigSource extends PropertiesConfigSource {
return null;
}
private static Map<String, String> readProperties(Path path) {
private static Map<String, String> readProperties() {
if (!Environment.isRebuild()) {
File file = path.toFile();
InputStream fileStream = loadPersistedConfig();
if (file.exists()) {
try {
return ConfigSourceUtil.urlToMap(file.toURL());
} catch (IOException e) {
throw new RuntimeException("Failed to load persisted properties from [" + file.getAbsolutePath() + ".", e);
if (fileStream == null) {
return Collections.emptyMap();
}
try (fileStream) {
Properties properties = new Properties();
properties.load(fileStream);
Map<String, String> props = new HashMap<>();
for (Map.Entry<Object, Object> entry : properties.entrySet()) {
props.put(entry.getKey().toString(), entry.getValue().toString());
}
return props;
} catch (IOException e) {
throw new RuntimeException("Failed to load persisted properties.", e);
}
}
return Collections.emptyMap();
}
private static InputStream loadPersistedConfig() {
URL resource = Thread.currentThread().getContextClassLoader().getResource(PERSISTED_PROPERTIES);
if (resource == null) {
return null;
}
try {
return resource.openStream();
} catch (Exception cause) {
throw new RuntimeException("Failed to resolve persisted propertied file", cause);
}
}
}

View file

@ -133,7 +133,7 @@ final class HttpPropertyMappers {
ConfigValue proceed = context.proceed("kc.https.certificate.file");
if (proceed == null || proceed.getValue() == null) {
proceed = getMapper("quarkus.http.ssl.certificate.key-store-file").getOrDefault(context, null);
proceed = getMapper("quarkus.http.ssl.certificate.key-store-file").getConfigValue(context);
}
if (proceed == null || proceed.getValue() == null) {

View file

@ -30,8 +30,8 @@ public class PropertyMapper {
static PropertyMapper IDENTITY = new PropertyMapper(null, null, null, null, null,
false,null, null, false,Collections.emptyList(),null, true) {
@Override
public ConfigValue getOrDefault(String name, ConfigSourceInterceptorContext context, ConfigValue current) {
return current;
public ConfigValue getConfigValue(String name, ConfigSourceInterceptorContext context) {
return context.proceed(name);
}
};
@ -80,11 +80,11 @@ public class PropertyMapper {
return value;
}
ConfigValue getOrDefault(ConfigSourceInterceptorContext context, ConfigValue current) {
return getOrDefault(null, context, current);
ConfigValue getConfigValue(ConfigSourceInterceptorContext context) {
return getConfigValue(to, context);
}
ConfigValue getOrDefault(String name, ConfigSourceInterceptorContext context, ConfigValue current) {
ConfigValue getConfigValue(String name, ConfigSourceInterceptorContext context) {
String from = this.from;
if (to != null && to.endsWith(".")) {
@ -92,7 +92,7 @@ public class PropertyMapper {
from = name.replace(to.substring(0, to.lastIndexOf('.')), from.substring(0, from.lastIndexOf('.')));
}
// try to obtain the value for the property we want to map
// try to obtain the value for the property we want to map first
ConfigValue config = context.proceed(from);
if (config == null) {
@ -107,28 +107,18 @@ public class PropertyMapper {
if (value != null) {
return value;
}
return parentValue;
}
}
// if not defined, return the current value from the property as a default if the property is not explicitly set
if (defaultValue == null
|| (current != null && !current.getConfigSourceName().equalsIgnoreCase("default values"))) {
if (defaultValue == null && mapper != null) {
String value = current == null ? null : current.getValue();
return ConfigValue.builder().withName(to).withValue(mapper.apply(value, context)).build();
}
return current;
}
ConfigValue current = context.proceed(name);
if (mapper != null) {
if (current == null) {
return transformValue(defaultValue, context);
}
return ConfigValue.builder().withName(to).withValue(defaultValue).build();
}
if (mapFrom != null) {
return config;
return current;
}
if (config.getName().equals(name)) {
@ -139,7 +129,7 @@ public class PropertyMapper {
// we always fallback to the current value from the property we are mapping
if (value == null) {
return current;
return context.proceed(name);
}
return value;

View file

@ -3,6 +3,7 @@ package org.keycloak.quarkus.runtime.configuration.mappers;
import io.smallrye.config.ConfigSourceInterceptorContext;
import io.smallrye.config.ConfigValue;
import org.keycloak.quarkus.runtime.Environment;
import org.keycloak.quarkus.runtime.configuration.ConfigArgsConfigSource;
import org.keycloak.quarkus.runtime.configuration.MicroProfileConfigProvider;
import java.util.Collection;
@ -32,14 +33,13 @@ public final class PropertyMappers {
public static ConfigValue getValue(ConfigSourceInterceptorContext context, String name) {
PropertyMapper mapper = MAPPERS.getOrDefault(name, PropertyMapper.IDENTITY);
ConfigValue configValue = mapper
.getOrDefault(name, context, context.proceed(name));
ConfigValue configValue = mapper.getConfigValue(name, context);
if (configValue == null) {
Optional<String> prefixedMapper = getPrefixedMapper(name);
if (prefixedMapper.isPresent()) {
return MAPPERS.get(prefixedMapper.get()).getOrDefault(name, context, configValue);
return MAPPERS.get(prefixedMapper.get()).getConfigValue(name, context);
}
} else {
configValue.withName(mapper.getTo());
@ -76,7 +76,7 @@ public final class PropertyMappers {
return isBuildTimeProperty
&& !"kc.version".equals(name)
&& !Environment.CLI_ARGS.equals(name)
&& !ConfigArgsConfigSource.CLI_ARGS.equals(name)
&& !"kc.home.dir".equals(name)
&& !"kc.config.file".equals(name)
&& !Environment.PROFILE.equals(name)

View file

@ -22,21 +22,11 @@ import io.quarkus.runtime.StartupEvent;
import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.event.Observes;
import org.keycloak.Config;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.KeycloakTransactionManager;
import org.keycloak.platform.Platform;
import org.keycloak.services.ServicesLogger;
import org.keycloak.services.managers.ApplianceBootstrap;
import org.keycloak.services.resources.KeycloakApplication;
@ApplicationScoped
public class QuarkusLifecycleObserver {
private static final String KEYCLOAK_ADMIN_ENV_VAR = "KEYCLOAK_ADMIN";
private static final String KEYCLOAK_ADMIN_PASSWORD_ENV_VAR = "KEYCLOAK_ADMIN_PASSWORD";
void onStartupEvent(@Observes StartupEvent event) {
QuarkusPlatform platform = (QuarkusPlatform) Platform.getPlatform();
platform.started();
@ -45,7 +35,6 @@ public class QuarkusLifecycleObserver {
if (startupHook != null) {
startupHook.run();
createAdminUser();
}
}
@ -57,35 +46,4 @@ public class QuarkusLifecycleObserver {
shutdownHook.run();
}
private void createAdminUser() {
String adminUserName = System.getenv(KEYCLOAK_ADMIN_ENV_VAR);
String adminPassword = System.getenv(KEYCLOAK_ADMIN_PASSWORD_ENV_VAR);
if ((adminUserName == null || adminUserName.trim().length() == 0)
|| (adminPassword == null || adminPassword.trim().length() == 0)) {
return;
}
KeycloakSessionFactory sessionFactory = KeycloakApplication.getSessionFactory();
KeycloakSession session = sessionFactory.create();
KeycloakTransactionManager transaction = session.getTransactionManager();
try {
transaction.begin();
new ApplianceBootstrap(session).createMasterRealmUser(adminUserName, adminPassword);
ServicesLogger.LOGGER.addUserSuccess(adminUserName, Config.getAdminRealm());
transaction.commit();
} catch (IllegalStateException e) {
session.getTransactionManager().rollback();
ServicesLogger.LOGGER.addUserFailedUserExists(adminUserName, Config.getAdminRealm());
} catch (Throwable t) {
session.getTransactionManager().rollback();
ServicesLogger.LOGGER.addUserFailed(t, adminUserName, Config.getAdminRealm());
} finally {
session.close();
}
}
}

View file

@ -65,18 +65,6 @@ public class QuarkusPlatform implements PlatformProvider {
}
}
/**
* Similar behavior as per {@code #exitOnError} but convenient to throw a {@link InitializationException} with a single
* {@code cause}
*
* @param cause the cause
* @throws InitializationException the initialization exception with the given {@code cause}.
*/
public static void exitOnError(Throwable cause) throws InitializationException{
addInitializationException(cause);
exitOnError();
}
Runnable startupHook;
Runnable shutdownHook;
@ -155,7 +143,7 @@ public class QuarkusPlatform implements PlatformProvider {
} else {
String dataDir = Environment.getDataDir();
tmpDir = new File(dataDir, "tmp");
tmpDir.mkdir();
tmpDir.mkdirs();
}
if (tmpDir.isDirectory()) {

View file

@ -25,9 +25,14 @@ import javax.inject.Inject;
import javax.persistence.EntityManagerFactory;
import javax.ws.rs.ApplicationPath;
import org.keycloak.Config;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.KeycloakTransactionManager;
import org.keycloak.models.utils.PostMigrationEvent;
import org.keycloak.quarkus.runtime.integration.QuarkusKeycloakSessionFactory;
import org.keycloak.quarkus.runtime.integration.QuarkusPlatform;
import org.keycloak.services.ServicesLogger;
import org.keycloak.services.managers.ApplianceBootstrap;
import org.keycloak.services.resources.KeycloakApplication;
import org.keycloak.quarkus.runtime.services.resources.QuarkusWelcomeResource;
import org.keycloak.services.resources.WelcomeResource;
@ -35,6 +40,9 @@ import org.keycloak.services.resources.WelcomeResource;
@ApplicationPath("/")
public class QuarkusKeycloakApplication extends KeycloakApplication {
private static final String KEYCLOAK_ADMIN_ENV_VAR = "KEYCLOAK_ADMIN";
private static final String KEYCLOAK_ADMIN_PASSWORD_ENV_VAR = "KEYCLOAK_ADMIN_PASSWORD";
private static boolean filterSingletons(Object o) {
return !WelcomeResource.class.isInstance(o);
}
@ -44,13 +52,10 @@ public class QuarkusKeycloakApplication extends KeycloakApplication {
@Override
protected void startup() {
try {
forceEntityManagerInitialization();
initializeKeycloakSessionFactory();
setupScheduledTasks(sessionFactory);
} catch (Throwable cause) {
QuarkusPlatform.exitOnError(cause);
}
forceEntityManagerInitialization();
initializeKeycloakSessionFactory();
setupScheduledTasks(sessionFactory);
createAdminUser();
}
@Override
@ -60,7 +65,6 @@ public class QuarkusKeycloakApplication extends KeycloakApplication {
.collect(Collectors.toSet());
singletons.add(new QuarkusWelcomeResource());
singletons.add(new QuarkusWelcomeResource());
return singletons;
}
@ -77,4 +81,35 @@ public class QuarkusKeycloakApplication extends KeycloakApplication {
// when first creating an entity manager
entityManagerFactory.get().createEntityManager().close();
}
private void createAdminUser() {
String adminUserName = System.getenv(KEYCLOAK_ADMIN_ENV_VAR);
String adminPassword = System.getenv(KEYCLOAK_ADMIN_PASSWORD_ENV_VAR);
if ((adminUserName == null || adminUserName.trim().length() == 0)
|| (adminPassword == null || adminPassword.trim().length() == 0)) {
return;
}
KeycloakSessionFactory sessionFactory = KeycloakApplication.getSessionFactory();
KeycloakSession session = sessionFactory.create();
KeycloakTransactionManager transaction = session.getTransactionManager();
try {
transaction.begin();
new ApplianceBootstrap(session).createMasterRealmUser(adminUserName, adminPassword);
ServicesLogger.LOGGER.addUserSuccess(adminUserName, Config.getAdminRealm());
transaction.commit();
} catch (IllegalStateException e) {
session.getTransactionManager().rollback();
ServicesLogger.LOGGER.addUserFailedUserExists(adminUserName, Config.getAdminRealm());
} catch (Throwable t) {
session.getTransactionManager().rollback();
ServicesLogger.LOGGER.addUserFailed(t, adminUserName, Config.getAdminRealm());
} finally {
session.close();
}
}
}

View file

@ -28,42 +28,42 @@
<key media-type="application/x-java-object"/>
<value media-type="application/x-java-object"/>
</encoding>
<memory storage="HEAP" max-count="10000"/>
<memory max-count="10000"/>
</local-cache>
<local-cache name="users">
<encoding>
<key media-type="application/x-java-object"/>
<value media-type="application/x-java-object"/>
</encoding>
<memory storage="HEAP" max-count="10000"/>
<memory max-count="10000"/>
</local-cache>
<distributed-cache name="sessions" owners="1">
<expiration lifespan="900000000000000000"/>
<distributed-cache name="sessions" owners="2">
<expiration lifespan="-1"/>
</distributed-cache>
<distributed-cache name="authenticationSessions" owners="1">
<expiration lifespan="900000000000000000"/>
<distributed-cache name="authenticationSessions" owners="2">
<expiration lifespan="-1"/>
</distributed-cache>
<distributed-cache name="offlineSessions" owners="1">
<expiration lifespan="900000000000000000"/>
<distributed-cache name="offlineSessions" owners="2">
<expiration lifespan="-1"/>
</distributed-cache>
<distributed-cache name="clientSessions" owners="1">
<expiration lifespan="900000000000000000"/>
<distributed-cache name="clientSessions" owners="2">
<expiration lifespan="-1"/>
</distributed-cache>
<distributed-cache name="offlineClientSessions" owners="1">
<expiration lifespan="900000000000000000"/>
<distributed-cache name="offlineClientSessions" owners="2">
<expiration lifespan="-1"/>
</distributed-cache>
<distributed-cache name="loginFailures" owners="1">
<expiration lifespan="900000000000000000"/>
<distributed-cache name="loginFailures" owners="2">
<expiration lifespan="-1"/>
</distributed-cache>
<local-cache name="authorization">
<encoding>
<key media-type="application/x-java-object"/>
<value media-type="application/x-java-object"/>
</encoding>
<memory storage="HEAP" max-count="10000"/>
<memory max-count="10000"/>
</local-cache>
<replicated-cache name="work">
<expiration lifespan="900000000000000000"/>
<expiration lifespan="-1"/>
</replicated-cache>
<local-cache name="keys">
<encoding>
@ -71,15 +71,15 @@
<value media-type="application/x-java-object"/>
</encoding>
<expiration max-idle="3600000"/>
<memory storage="HEAP" max-count="1000"/>
<memory max-count="1000"/>
</local-cache>
<distributed-cache name="actionTokens" owners="2">
<encoding>
<key media-type="application/x-java-object"/>
<value media-type="application/x-java-object"/>
</encoding>
<expiration max-idle="-1" lifespan="900000000000000000" interval="300000"/>
<memory storage="HEAP" max-count="-1"/>
<expiration max-idle="-1" lifespan="-1" interval="300000"/>
<memory max-count="-1"/>
</distributed-cache>
</cache-container>
</infinispan>

View file

@ -30,28 +30,42 @@
<key media-type="application/x-java-object"/>
<value media-type="application/x-java-object"/>
</encoding>
<memory storage="HEAP" max-count="10000"/>
<memory max-count="10000"/>
</local-cache>
<local-cache name="users">
<encoding>
<key media-type="application/x-java-object"/>
<value media-type="application/x-java-object"/>
</encoding>
<memory storage="HEAP" max-count="10000"/>
<memory max-count="10000"/>
</local-cache>
<local-cache name="sessions">
<expiration lifespan="-1"/>
</local-cache>
<local-cache name="authenticationSessions">
<expiration lifespan="-1"/>
</local-cache>
<local-cache name="offlineSessions">
<expiration lifespan="-1"/>
</local-cache>
<local-cache name="clientSessions">
<expiration lifespan="-1"/>
</local-cache>
<local-cache name="offlineClientSessions">
<expiration lifespan="-1"/>
</local-cache>
<local-cache name="loginFailures">
<expiration lifespan="-1"/>
</local-cache>
<local-cache name="sessions"/>
<local-cache name="authenticationSessions"/>
<local-cache name="offlineSessions"/>
<local-cache name="clientSessions"/>
<local-cache name="offlineClientSessions"/>
<local-cache name="loginFailures"/>
<local-cache name="work"/>
<local-cache name="authorization">
<encoding>
<key media-type="application/x-java-object"/>
<value media-type="application/x-java-object"/>
</encoding>
<memory storage="HEAP" max-count="10000"/>
<memory max-count="10000"/>
</local-cache>
<local-cache name="work">
<expiration lifespan="-1"/>
</local-cache>
<local-cache name="keys">
<encoding>
@ -59,15 +73,15 @@
<value media-type="application/x-java-object"/>
</encoding>
<expiration max-idle="3600000"/>
<memory storage="HEAP" max-count="10000"/>
<memory max-count="1000"/>
</local-cache>
<local-cache name="actionTokens">
<encoding>
<key media-type="application/x-java-object"/>
<value media-type="application/x-java-object"/>
</encoding>
<expiration max-idle="-1" interval="300000"/>
<memory storage="HEAP" max-count="-1"/>
<expiration max-idle="-1" lifespan="-1" interval="300000"/>
<memory max-count="-1"/>
</local-cache>
</cache-container>
</infinispan>

View file

@ -19,7 +19,7 @@ package org.keycloak.provider.quarkus;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.keycloak.quarkus.runtime.Environment.CLI_ARGS;
import static org.keycloak.quarkus.runtime.configuration.ConfigArgsConfigSource.CLI_ARGS;
import java.io.File;
import java.lang.reflect.Field;

View file

@ -18,6 +18,7 @@
package org.keycloak.it.junit5.extension;
import static org.keycloak.it.junit5.extension.DistributionTest.ReInstall.BEFORE_ALL;
import static org.keycloak.it.junit5.extension.DistributionType.RAW;
import static org.keycloak.quarkus.runtime.Environment.forceTestLaunchMode;
import java.util.Arrays;
@ -27,8 +28,6 @@ import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.ParameterContext;
import org.junit.jupiter.api.extension.ParameterResolutionException;
import org.keycloak.it.utils.KeycloakDistribution;
import org.keycloak.it.utils.DockerKeycloakDistribution;
import org.keycloak.it.utils.RawKeycloakDistribution;
import org.keycloak.quarkus.runtime.Environment;
import org.keycloak.quarkus.runtime.cli.command.Start;
import org.keycloak.quarkus.runtime.cli.command.StartDev;
@ -83,26 +82,7 @@ public class CLITestExtension extends QuarkusMainTestExtension {
}
private KeycloakDistribution createDistribution(DistributionTest config) {
KeycloakDistribution distribution = null;
switch (System.getProperty("kc.quarkus.tests.dist", "raw")) {
case "docker":
distribution = new DockerKeycloakDistribution(
config.debug(),
config.keepAlive(),
!DistributionTest.ReInstall.NEVER.equals(config.reInstall())
);
break;
case "raw":
default:
distribution = new RawKeycloakDistribution(
config.debug(),
config.keepAlive(),
!DistributionTest.ReInstall.NEVER.equals(config.reInstall())
);
}
return distribution;
return DistributionType.getCurrent().orElse(RAW).newInstance(config);
}
@Override

View file

@ -0,0 +1,69 @@
/*
* 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.util.Optional;
import java.util.function.Function;
import org.keycloak.it.utils.DockerKeycloakDistribution;
import org.keycloak.it.utils.KeycloakDistribution;
import org.keycloak.it.utils.RawKeycloakDistribution;
import org.keycloak.utils.StringUtil;
public enum DistributionType {
RAW(DistributionType::createRawDistribution),
DOCKER(DistributionType::createDockerDistribution);
private static KeycloakDistribution createDockerDistribution(DistributionTest config) {
return new DockerKeycloakDistribution(
config.debug(),
config.keepAlive(),
!DistributionTest.ReInstall.NEVER.equals(config.reInstall()));
}
private static KeycloakDistribution createRawDistribution(DistributionTest config) {
return new RawKeycloakDistribution(
config.debug(),
config.keepAlive(),
!DistributionTest.ReInstall.NEVER.equals(config.reInstall()));
}
private final Function<DistributionTest, KeycloakDistribution> factory;
DistributionType(Function<DistributionTest, KeycloakDistribution> factory) {
this.factory = factory;
}
public static Optional<DistributionType> getCurrent() {
String distributionType = System.getProperty("kc.quarkus.tests.dist");
if (StringUtil.isBlank(distributionType)) {
return Optional.empty();
}
try {
return Optional.of(valueOf(distributionType.toUpperCase()));
} catch (IllegalStateException cause) {
throw new RuntimeException("Invalid distribution type: " + distributionType);
}
}
public KeycloakDistribution newInstance(DistributionTest config) {
return factory.apply(config);
}
}

View file

@ -0,0 +1,39 @@
/*
* 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 org.junit.jupiter.api.condition.EnabledIfSystemProperty;
import org.junit.jupiter.api.extension.ExtendWith;
/**
* {@link RawDistOnly} is used to signal that the annotated tests class is only enabled when running tests using the {@link DistributionType#RAW}.
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@EnabledIfSystemProperty(named = "kc.quarkus.tests.dist", matches = "^$|raw")
public @interface RawDistOnly {
/**
* The reason why the test is disabled.
*/
String reason();
}

View file

@ -29,6 +29,13 @@ import io.quarkus.test.junit.main.LaunchResult;
@CLITest
public class HelpCommandTest {
@Test
@Launch({})
void testDefaultToHelp(LaunchResult result) {
CLIResult cliResult = (CLIResult) result;
cliResult.assertHelp("kc.sh");
}
@Test
@Launch({ "--help" })
void testHelpCommand(LaunchResult result) {

View file

@ -0,0 +1,54 @@
/*
* 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.assertTrue;
import org.junit.jupiter.api.MethodOrderer;
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.junit.jupiter.api.condition.DisabledIf;
import org.junit.jupiter.api.condition.EnabledIfSystemProperty;
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(OrderAnnotation.class)
public class BuildAndStartDistTest {
@Test
@Launch({ "build", "--http-enabled=true", "--hostname-strict=false", "--cluster=local" })
@Order(1)
void firstYouBuild(LaunchResult result) {
}
@Test
@Launch({ "start" })
@Order(2)
void thenYouStart(LaunchResult result) {
CLIResult cliResult = (CLIResult) result;
cliResult.assertStarted();
}
}

View file

@ -17,6 +17,7 @@
package org.keycloak.it.cli.dist;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.junit.jupiter.api.Test;
@ -50,5 +51,6 @@ class BuildCommandDistTest {
() -> "The Error Output:\n" + result.getErrorOutput() + "doesn't contains the expected string.");
assertTrue(result.getErrorOutput().contains("For more details run the same command passing the '--verbose' option. Also you can use '--help' to see the details about the usage of the particular command."),
() -> "The Error Output:\n" + result.getErrorOutput() + "doesn't contains the expected string.");
assertEquals(4, result.getErrorStream().size());
}
}

View file

@ -17,9 +17,24 @@
package org.keycloak.it.cli.dist;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.junit.jupiter.api.Test;
import org.keycloak.it.cli.StartCommandTest;
import org.keycloak.it.junit5.extension.DistributionTest;
import io.quarkus.test.junit.main.Launch;
import io.quarkus.test.junit.main.LaunchResult;
@DistributionTest
public class StartCommandDistTest extends StartCommandTest {
@Test
@Launch({ "-pf=dev", "start", "--auto-build", "--http-enabled=true", "--hostname-strict=false" })
void failIfAutoBuildUsingDevProfile(LaunchResult result) {
assertTrue(result.getErrorOutput().contains("ERROR: You can not 'start' the server using the 'dev' configuration profile. Please re-build the server first, using 'kc.sh build' for the default production profile, or using 'kc.sh build --profile=<profile>' with a profile more suitable for production."),
() -> "The Output:\n" + result.getErrorOutput() + "doesn't contains the expected string.");
assertEquals(4, result.getErrorStream().size());
}
}

View file

@ -28,6 +28,7 @@ spi.truststore.file.file=${kc.home.dir}/conf/keycloak.truststore
spi.truststore.file.password=secret
# Declarative User Profile
spi.user-profile.provider=declarative-user-profile
spi.user-profile.declarative-user-profile.read-only-attributes=deniedFoo,deniedBar*,deniedSome/thing,deniedsome*thing
spi.user-profile.declarative-user-profile.admin-read-only-attributes=deniedSomeAdmin

View file

@ -46,9 +46,6 @@ import org.keycloak.testsuite.arquillian.SuiteContext;
public class KeycloakQuarkusServerDeployableContainer implements DeployableContainer<KeycloakQuarkusConfiguration> {
private static final Logger log = Logger.getLogger(KeycloakQuarkusServerDeployableContainer.class);
public static final String CONF_PERSISTED_PROPERTIES_FILENAME = "data/generated/persisted.properties";
public static final String SPI_PROVIDER_PROPERTY_PREFIX = "kc.spi-";
public static final String SPI_PROVIDER_PROPERTY_SUFFIX = "-provider";
private KeycloakQuarkusConfiguration configuration;
private Process container;
@ -129,23 +126,6 @@ public class KeycloakQuarkusServerDeployableContainer implements DeployableConta
}
/**
* Get the currently configured default provider for the passed spi.
*
* @param spi the spi to get the current default provider for (dash-cased)
* @return the current configured provider or null if not configured
*/
public String getCurrentlyConfiguredSpiProviderFor(String spi) {
try {
File persistedPropertiesFile = configuration.getProvidersPath().resolve(CONF_PERSISTED_PROPERTIES_FILENAME).toFile();
Properties props = new Properties();
props.load(new FileInputStream(persistedPropertiesFile));
return props.get(SPI_PROVIDER_PROPERTY_PREFIX + spi + SPI_PROVIDER_PROPERTY_SUFFIX).toString().trim();
} catch (IOException e) {
return null;
}
}
private Process startContainer() throws IOException {
ProcessBuilder pb = new ProcessBuilder(getProcessCommands());
File wrkDir = configuration.getProvidersPath().resolve("bin").toFile();