improvement: validates the expected values of non-cli properties (#23797)
also adds better messages for unknown options closes #13608
This commit is contained in:
parent
bafc6da6b2
commit
f4d1dd9b7f
20 changed files with 293 additions and 39 deletions
|
@ -31,6 +31,7 @@ import java.util.ArrayList;
|
|||
import java.util.List;
|
||||
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import picocli.CommandLine.ExitCode;
|
||||
|
||||
import io.quarkus.runtime.ApplicationLifecycleManager;
|
||||
import io.quarkus.runtime.Quarkus;
|
||||
|
@ -40,6 +41,7 @@ import org.keycloak.Config;
|
|||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.quarkus.runtime.cli.ExecutionExceptionHandler;
|
||||
import org.keycloak.quarkus.runtime.cli.NonCliPropertyException;
|
||||
import org.keycloak.quarkus.runtime.cli.Picocli;
|
||||
import org.keycloak.common.Version;
|
||||
import org.keycloak.quarkus.runtime.cli.command.Start;
|
||||
|
@ -75,6 +77,15 @@ public class KeycloakMain implements QuarkusApplication {
|
|||
|
||||
if (isDevProfileNotAllowed()) {
|
||||
errorHandler.error(errStream, Messages.devProfileNotAllowedError(Start.NAME), null);
|
||||
System.exit(ExitCode.USAGE);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
Picocli.validateNonCliConfig(cliArgs, new Start(), new PrintWriter(System.out, true));
|
||||
} catch (NonCliPropertyException e) {
|
||||
errorHandler.error(errStream, e.getMessage(), null);
|
||||
System.exit(ExitCode.USAGE);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -42,6 +42,11 @@ public final class ExecutionExceptionHandler implements CommandLine.IExecutionEx
|
|||
|
||||
@Override
|
||||
public int handleExecutionException(Exception cause, CommandLine cmd, ParseResult parseResult) {
|
||||
if (cause instanceof NonCliPropertyException) {
|
||||
PrintWriter writer = cmd.getErr();
|
||||
writer.println(cmd.getColorScheme().errorText(cause.getMessage()));
|
||||
return ShortErrorMessageHandler.getInvalidInputExitCode(cause, cmd);
|
||||
}
|
||||
error(cmd.getErr(), "Failed to run '" + parseResult.subcommands().stream()
|
||||
.map(ParseResult::commandSpec)
|
||||
.map(CommandLine.Model.CommandSpec::name)
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* 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;
|
||||
|
||||
public class NonCliPropertyException extends RuntimeException {
|
||||
|
||||
public NonCliPropertyException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
}
|
|
@ -69,7 +69,9 @@ import org.keycloak.quarkus.runtime.configuration.mappers.PropertyMapper;
|
|||
import org.keycloak.quarkus.runtime.Environment;
|
||||
|
||||
import io.smallrye.config.ConfigValue;
|
||||
|
||||
import picocli.CommandLine;
|
||||
import picocli.CommandLine.ParameterException;
|
||||
import picocli.CommandLine.Help.Ansi;
|
||||
import picocli.CommandLine.Model.CommandSpec;
|
||||
import picocli.CommandLine.Model.OptionSpec;
|
||||
|
@ -82,19 +84,38 @@ public final class Picocli {
|
|||
public static final String NO_PARAM_LABEL = "none";
|
||||
private static final String ARG_KEY_VALUE_SEPARATOR = "=";
|
||||
|
||||
private static class IncludeOptions {
|
||||
boolean includeRuntime;
|
||||
boolean includeBuildTime;
|
||||
}
|
||||
|
||||
private Picocli() {
|
||||
}
|
||||
|
||||
public static void parseAndRun(List<String> cliArgs) {
|
||||
CommandLine cmd = createCommandLine(cliArgs);
|
||||
|
||||
String[] argArray = cliArgs.toArray(new String[0]);
|
||||
if (Environment.isRebuildCheck()) {
|
||||
int exitCode = runReAugmentationIfNeeded(cliArgs, cmd);
|
||||
int exitCode = 0;
|
||||
try {
|
||||
// process the cli args first to init the config file and perform validation
|
||||
cmd.parseArgs(argArray);
|
||||
exitCode = runReAugmentationIfNeeded(cliArgs, cmd);
|
||||
} catch (ParameterException ex) {
|
||||
try {
|
||||
exitCode = cmd.getParameterExceptionHandler().handleParseException(ex, argArray);
|
||||
} catch (Exception e) {
|
||||
ExecutionExceptionHandler errorHandler = new ExecutionExceptionHandler();
|
||||
errorHandler.error(cmd.getErr(), e.getMessage(), null);
|
||||
exitCode = ex.getCommandLine().getCommandSpec().exitCodeOnInvalidInput();
|
||||
}
|
||||
}
|
||||
exitOnFailure(exitCode, cmd);
|
||||
return;
|
||||
}
|
||||
|
||||
int exitCode = cmd.execute(cliArgs.toArray(new String[0]));
|
||||
int exitCode = cmd.execute(argArray);
|
||||
exitOnFailure(exitCode, cmd);
|
||||
}
|
||||
|
||||
|
@ -228,6 +249,74 @@ public final class Picocli {
|
|||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* validate the expected values of non-cli properties
|
||||
*
|
||||
* @param cliArgs
|
||||
* @param abstractCommand
|
||||
*/
|
||||
public static void validateNonCliConfig(List<String> cliArgs, AbstractCommand abstractCommand, PrintWriter out) {
|
||||
IncludeOptions options = getIncludeOptions(cliArgs, abstractCommand, abstractCommand.getName());
|
||||
|
||||
if (!options.includeBuildTime && !options.includeRuntime) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<String> ignoredBuildTime = new ArrayList<>();
|
||||
List<String> ignoredRunTime = new ArrayList<>();
|
||||
for (OptionCategory category : abstractCommand.getOptionCategories()) {
|
||||
List<PropertyMapper> mappers = new ArrayList<>();
|
||||
Optional.ofNullable(PropertyMappers.getRuntimeMappers().get(category)).ifPresent(mappers::addAll);
|
||||
Optional.ofNullable(PropertyMappers.getBuildTimeMappers().get(category)).ifPresent(mappers::addAll);
|
||||
for (PropertyMapper mapper : mappers) {
|
||||
// bypass the PropertyMappingInterceptor - the transformations may cause unexpected errors
|
||||
String value = null;
|
||||
ConfigSource configSource = null;
|
||||
for (ConfigSource cs : getConfig().getConfigSources()) {
|
||||
if (cs.getOrdinal() < 300) {
|
||||
break; // don't consider anything below standard env properties
|
||||
}
|
||||
value = cs.getValue(mapper.getFrom());
|
||||
if (value != null) {
|
||||
configSource = cs;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (value == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (mapper.isBuildTime() && !options.includeBuildTime) {
|
||||
ignoredBuildTime.add(mapper.getFrom());
|
||||
continue;
|
||||
}
|
||||
if (mapper.isRunTime() && !options.includeRuntime) {
|
||||
ignoredRunTime.add(mapper.getFrom());
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!PropertyMapperParameterConsumer.isExpectedValue(mapper.getExpectedValues(), value)) {
|
||||
throw new NonCliPropertyException(PropertyMapperParameterConsumer.getErrorMessage(mapper.getFrom(),
|
||||
value, mapper.getExpectedValues(), mapper.getExpectedValues()) + ". From ConfigSource " + configSource.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!ignoredBuildTime.isEmpty()) {
|
||||
outputIgnoredProperties(ignoredBuildTime, true, out);
|
||||
} else if (!ignoredRunTime.isEmpty()) {
|
||||
outputIgnoredProperties(ignoredRunTime, false, out);
|
||||
}
|
||||
}
|
||||
|
||||
private static void outputIgnoredProperties(List<String> properties, boolean build, PrintWriter out) {
|
||||
out.write(String.format("The following %s time non-cli properties were found, but will be ignored during %s time: %s\n",
|
||||
build ? "build" : "run", build ? "run" : "build",
|
||||
properties.stream().collect(Collectors.joining(", "))));
|
||||
out.flush();
|
||||
}
|
||||
|
||||
private static boolean hasConfigChanges(CommandLine cmdCommand) {
|
||||
Optional<String> currentProfile = ofNullable(Environment.getProfile());
|
||||
Optional<String> persistedProfile = getBuildTimeProperty("kc.profile");
|
||||
|
@ -379,26 +468,33 @@ public final class Picocli {
|
|||
return cmd;
|
||||
}
|
||||
|
||||
private static IncludeOptions getIncludeOptions(List<String> cliArgs, AbstractCommand abstractCommand, String commandName) {
|
||||
IncludeOptions result = new IncludeOptions();
|
||||
if (abstractCommand == null) {
|
||||
return result;
|
||||
}
|
||||
result.includeRuntime = abstractCommand.includeRuntime();
|
||||
result.includeBuildTime = abstractCommand.includeBuildTime();
|
||||
|
||||
if (!result.includeBuildTime && !result.includeRuntime) {
|
||||
return result;
|
||||
} else if (result.includeRuntime && !result.includeBuildTime && !ShowConfig.NAME.equals(commandName)) {
|
||||
result.includeBuildTime = isRebuilt() || !cliArgs.contains(OPTIMIZED_BUILD_OPTION_LONG);
|
||||
} else if (result.includeBuildTime && !result.includeRuntime) {
|
||||
result.includeRuntime = isRebuildCheck();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void addCommandOptions(List<String> cliArgs, CommandLine command) {
|
||||
if (command != null) {
|
||||
boolean includeBuildTime = false;
|
||||
boolean includeRuntime = false;
|
||||
if (command != null && command.getCommand() instanceof AbstractCommand) {
|
||||
IncludeOptions options = getIncludeOptions(cliArgs, command.getCommand(), command.getCommandName());
|
||||
|
||||
if (command.getCommand() instanceof AbstractCommand) {
|
||||
AbstractCommand abstractCommand = command.getCommand();
|
||||
includeRuntime = abstractCommand.includeRuntime();
|
||||
includeBuildTime = abstractCommand.includeBuildTime();
|
||||
}
|
||||
|
||||
if (!includeBuildTime && !includeRuntime) {
|
||||
if (!options.includeBuildTime && !options.includeRuntime) {
|
||||
return;
|
||||
} else if (includeRuntime && !includeBuildTime && !ShowConfig.NAME.equals(command.getCommandName())) {
|
||||
includeBuildTime = isRebuilt() || !cliArgs.contains(OPTIMIZED_BUILD_OPTION_LONG);
|
||||
} else if (includeBuildTime && !includeRuntime) {
|
||||
includeRuntime = isRebuildCheck();
|
||||
}
|
||||
|
||||
addOptionsToCli(command, includeBuildTime, includeRuntime);
|
||||
addOptionsToCli(command, options.includeBuildTime, options.includeRuntime);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@ package org.keycloak.quarkus.runtime.cli;
|
|||
|
||||
import static org.keycloak.quarkus.runtime.cli.Picocli.ARG_PREFIX;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Collection;
|
||||
import java.util.Stack;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.StreamSupport;
|
||||
|
@ -55,7 +55,7 @@ public final class PropertyMapperParameterConsumer implements CommandLine.IParam
|
|||
|
||||
if (args.isEmpty() || !isOptionValue(args.peek())) {
|
||||
throw new ParameterException(
|
||||
commandLine, "Missing required value for option '" + name + "' (" + argSpec.paramLabel() + ")." + getExpectedValuesMessage(argSpec, option));
|
||||
commandLine, "Missing required value for option '" + name + "' (" + argSpec.paramLabel() + ")." + getExpectedValuesMessage(argSpec.completionCandidates(), option.completionCandidates()));
|
||||
}
|
||||
|
||||
// consumes the value
|
||||
|
@ -63,28 +63,29 @@ public final class PropertyMapperParameterConsumer implements CommandLine.IParam
|
|||
|
||||
if (!args.isEmpty() && isOptionValue(args.peek())) {
|
||||
throw new ParameterException(
|
||||
commandLine, "Option '" + name + "' expects a single value (" + argSpec.paramLabel() + ")" + getExpectedValuesMessage(argSpec, option));
|
||||
commandLine, "Option '" + name + "' expects a single value (" + argSpec.paramLabel() + ")" + getExpectedValuesMessage(argSpec.completionCandidates(), option.completionCandidates()));
|
||||
}
|
||||
|
||||
if (isExpectedValue(option, value)) {
|
||||
if (isExpectedValue(StreamSupport.stream(option.completionCandidates().spliterator(), false).collect(Collectors.toList()), value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
throw new ParameterException(
|
||||
commandLine, "Invalid value for option '" + name + "': " + value + "." + getExpectedValuesMessage(argSpec, option));
|
||||
throw new ParameterException(commandLine, getErrorMessage(name, value, argSpec.completionCandidates(), option.completionCandidates()));
|
||||
}
|
||||
|
||||
static String getErrorMessage(String name, String value, Iterable<String> specCandidates, Iterable<String> optionCandidates) {
|
||||
return "Invalid value for option '" + name + "': " + value + "." + getExpectedValuesMessage(specCandidates, optionCandidates);
|
||||
}
|
||||
|
||||
private boolean isOptionValue(String arg) {
|
||||
return !(arg.startsWith(ARG_PREFIX) || arg.startsWith(Picocli.ARG_SHORT_PREFIX));
|
||||
}
|
||||
|
||||
private String getExpectedValuesMessage(ArgSpec argSpec, OptionSpec option) {
|
||||
return option.completionCandidates().iterator().hasNext() ? " Expected values are: " + String.join(", ", argSpec.completionCandidates()) : "";
|
||||
static String getExpectedValuesMessage(Iterable<String> specCandidates, Iterable<String> optionCandidates) {
|
||||
return optionCandidates.iterator().hasNext() ? " Expected values are: " + String.join(", ", specCandidates) : "";
|
||||
}
|
||||
|
||||
private boolean isExpectedValue(OptionSpec option, String value) {
|
||||
List<String> expectedValues = StreamSupport.stream(option.completionCandidates().spliterator(), false).collect(Collectors.toList());
|
||||
|
||||
static boolean isExpectedValue(Collection<String> expectedValues, String value) {
|
||||
if (expectedValues.isEmpty()) {
|
||||
// accept any
|
||||
return true;
|
||||
|
|
|
@ -1,15 +1,24 @@
|
|||
package org.keycloak.quarkus.runtime.cli;
|
||||
|
||||
import org.keycloak.quarkus.runtime.cli.command.AbstractCommand;
|
||||
import org.keycloak.quarkus.runtime.cli.command.Start;
|
||||
import org.keycloak.quarkus.runtime.configuration.mappers.PropertyMapper;
|
||||
import org.keycloak.quarkus.runtime.configuration.mappers.PropertyMappers;
|
||||
|
||||
import java.io.PrintWriter;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import picocli.CommandLine;
|
||||
import picocli.CommandLine.IParameterExceptionHandler;
|
||||
import picocli.CommandLine.Model.CommandSpec;
|
||||
import picocli.CommandLine.ParameterException;
|
||||
import picocli.CommandLine.UnmatchedArgumentException;
|
||||
import picocli.CommandLine.Model.CommandSpec;
|
||||
|
||||
import java.io.PrintWriter;
|
||||
import static org.keycloak.quarkus.runtime.cli.command.AbstractStartCommand.OPTIMIZED_BUILD_OPTION_LONG;
|
||||
|
||||
public class ShortErrorMessageHandler implements IParameterExceptionHandler {
|
||||
|
||||
@Override
|
||||
public int handleParseException(ParameterException ex, String[] args) {
|
||||
CommandLine cmd = ex.getCommandLine();
|
||||
PrintWriter writer = cmd.getErr();
|
||||
|
@ -20,12 +29,28 @@ public class ShortErrorMessageHandler implements IParameterExceptionHandler {
|
|||
|
||||
String[] unmatched = getUnmatchedPartsByOptionSeparator(uae,"=");
|
||||
String original = uae.getUnmatched().get(0);
|
||||
|
||||
if (unmatched[0].equals(original)) {
|
||||
unmatched = getUnmatchedPartsByOptionSeparator(uae," ");
|
||||
}
|
||||
|
||||
String cliKey = unmatched[0];
|
||||
|
||||
errorMessage = "Unknown option: '" + unmatched[0] + "'";
|
||||
PropertyMapper<?> mapper = PropertyMappers.getMapper(cliKey);
|
||||
|
||||
if (mapper == null || !(cmd.getCommand() instanceof AbstractCommand)) {
|
||||
errorMessage = "Unknown option: '" + cliKey + "'";
|
||||
} else {
|
||||
AbstractCommand command = cmd.getCommand();
|
||||
if (!command.getOptionCategories().contains(mapper.getCategory())) {
|
||||
errorMessage = "Option: '" + cliKey + "' not valid for command " + cmd.getCommandName();
|
||||
} else {
|
||||
if (Stream.of(args).anyMatch(OPTIMIZED_BUILD_OPTION_LONG::equals) && mapper.isBuildTime() && Start.NAME.equals(cmd.getCommandName())) {
|
||||
errorMessage = "Build time option: '" + cliKey + "' not usable with pre-built image and --optimized";
|
||||
} else {
|
||||
errorMessage = (mapper.isRunTime()?"Run time":"Build time") + " option: '" + cliKey + "' not usable with " + cmd.getCommandName();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
writer.println(cmd.getColorScheme().errorText(errorMessage));
|
||||
|
@ -34,9 +59,13 @@ public class ShortErrorMessageHandler implements IParameterExceptionHandler {
|
|||
CommandSpec spec = cmd.getCommandSpec();
|
||||
writer.printf("Try '%s --help' for more information on the available options.%n", spec.qualifiedName());
|
||||
|
||||
return getInvalidInputExitCode(ex, cmd);
|
||||
}
|
||||
|
||||
static int getInvalidInputExitCode(Exception ex, CommandLine cmd) {
|
||||
return cmd.getExitCodeExceptionMapper() != null
|
||||
? cmd.getExitCodeExceptionMapper().getExitCode(ex)
|
||||
: spec.exitCodeOnInvalidInput();
|
||||
: cmd.getCommandSpec().exitCodeOnInvalidInput();
|
||||
}
|
||||
|
||||
private String[] getUnmatchedPartsByOptionSeparator(UnmatchedArgumentException uae, String separator) {
|
||||
|
|
|
@ -20,6 +20,9 @@ package org.keycloak.quarkus.runtime.cli.command;
|
|||
import static org.keycloak.quarkus.runtime.Messages.cliExecutionError;
|
||||
|
||||
import org.keycloak.config.OptionCategory;
|
||||
import org.keycloak.quarkus.runtime.cli.Picocli;
|
||||
import org.keycloak.quarkus.runtime.configuration.ConfigArgsConfigSource;
|
||||
|
||||
import picocli.CommandLine;
|
||||
import picocli.CommandLine.Model.CommandSpec;
|
||||
import picocli.CommandLine.Spec;
|
||||
|
@ -60,4 +63,10 @@ public abstract class AbstractCommand {
|
|||
public List<OptionCategory> getOptionCategories() {
|
||||
return Arrays.asList(OptionCategory.values());
|
||||
}
|
||||
|
||||
protected void validateNonCliConfig() {
|
||||
Picocli.validateNonCliConfig(ConfigArgsConfigSource.getAllCliArgs(), this, spec.commandLine().getOut());
|
||||
}
|
||||
|
||||
public abstract String getName();
|
||||
}
|
||||
|
|
|
@ -29,6 +29,7 @@ public abstract class AbstractStartCommand extends AbstractCommand implements Ru
|
|||
public void run() {
|
||||
doBeforeRun();
|
||||
CommandLine cmd = spec.commandLine();
|
||||
validateNonCliConfig();
|
||||
KeycloakMain.start((ExecutionExceptionHandler) cmd.getExecutionExceptionHandler(), cmd.getErr(), cmd.getParseResult().originalArgs().toArray(new String[0]));
|
||||
}
|
||||
|
||||
|
|
|
@ -72,6 +72,8 @@ public final class Build extends AbstractCommand implements Runnable {
|
|||
exitWithErrorIfDevProfileIsSetAndNotStartDev();
|
||||
|
||||
System.setProperty("quarkus.launch.rebuild", "true");
|
||||
validateNonCliConfig();
|
||||
|
||||
println(spec.commandLine(), "Updating the configuration and installing your custom providers, if any. Please wait.");
|
||||
|
||||
try {
|
||||
|
@ -106,6 +108,7 @@ public final class Build extends AbstractCommand implements Runnable {
|
|||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<OptionCategory> getOptionCategories() {
|
||||
// all options should work for the build command, otherwise re-augmentation might fail due to unknown options
|
||||
return super.getOptionCategories();
|
||||
|
@ -137,4 +140,9 @@ public final class Build extends AbstractCommand implements Runnable {
|
|||
getHomePath().resolve("quarkus-artifact.properties").toFile().delete();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return NAME;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -42,4 +42,9 @@ public final class Export extends AbstractExportImportCommand implements Runnabl
|
|||
optionCategory != OptionCategory.IMPORT).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return NAME;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -42,4 +42,9 @@ public final class Import extends AbstractExportImportCommand implements Runnabl
|
|||
optionCategory != OptionCategory.EXPORT).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return NAME;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -171,4 +171,9 @@ public final class ShowConfig extends AbstractCommand implements Runnable {
|
|||
|| property.startsWith("%"))
|
||||
&& !ignoredPropertyKeys.contains(property);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return NAME;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -73,6 +73,7 @@ public final class Start extends AbstractStartCommand implements Runnable {
|
|||
return Environment.isDevProfile();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<OptionCategory> getOptionCategories() {
|
||||
return super.getOptionCategories().stream().filter(optionCategory -> optionCategory != OptionCategory.EXPORT && optionCategory != OptionCategory.IMPORT).collect(Collectors.toList());
|
||||
}
|
||||
|
@ -81,4 +82,9 @@ public final class Start extends AbstractStartCommand implements Runnable {
|
|||
public boolean includeRuntime() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return NAME;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -58,4 +58,9 @@ public final class StartDev extends AbstractStartCommand implements Runnable {
|
|||
public boolean includeRuntime() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return NAME;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@ package org.keycloak.it.cli.dist;
|
|||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.keycloak.quarkus.runtime.cli.command.AbstractStartCommand.OPTIMIZED_BUILD_OPTION_LONG;
|
||||
import static org.keycloak.quarkus.runtime.cli.command.Main.CONFIG_FILE_LONG_NAME;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.keycloak.config.database.Database;
|
||||
|
@ -30,8 +31,11 @@ import io.quarkus.test.junit.main.Launch;
|
|||
import io.quarkus.test.junit.main.LaunchResult;
|
||||
|
||||
import org.keycloak.it.junit5.extension.RawDistOnly;
|
||||
import org.keycloak.it.junit5.extension.WithEnvVars;
|
||||
import org.keycloak.it.utils.KeycloakDistribution;
|
||||
|
||||
import java.nio.file.Paths;
|
||||
|
||||
@DistributionTest
|
||||
class BuildCommandDistTest {
|
||||
|
||||
|
@ -64,7 +68,22 @@ class BuildCommandDistTest {
|
|||
@Launch({ "build", "--db=postgres", "--db-username=myuser", "--db-password=mypassword", "--http-enabled=true" })
|
||||
void testFailRuntimeOptions(LaunchResult result) {
|
||||
CLIResult cliResult = (CLIResult) result;
|
||||
cliResult.assertError("Unknown option: '--db-username'");
|
||||
cliResult.assertError("Run time option: '--db-username' not usable with build");
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithEnvVars({"KC_DB", "invalid"})
|
||||
@Launch({ "build" })
|
||||
void testFailInvalidOptionInEnv(LaunchResult result) {
|
||||
CLIResult cliResult = (CLIResult) result;
|
||||
cliResult.assertError("Invalid value for option 'kc.db': invalid. Expected values are: dev-file, dev-mem, mariadb, mssql, mysql, oracle, postgres. From ConfigSource KcEnvVarConfigSource");
|
||||
}
|
||||
|
||||
@Test
|
||||
@RawDistOnly(reason = "Raw is enough and we avoid issues with including custom conf file in the container")
|
||||
public void testFailInvalidOptionInConf(KeycloakDistribution distribution) {
|
||||
CLIResult cliResult = distribution.run(CONFIG_FILE_LONG_NAME + "=" + Paths.get("src/test/resources/BuildCommandDistTest/keycloak.conf").toAbsolutePath().normalize(), "build");
|
||||
cliResult.assertError("Invalid value for option 'kc.db': foo. Expected values are: dev-file, dev-mem, mariadb, mssql, mysql, oracle, postgres. From ConfigSource PropertiesConfigSource[source");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
@ -144,14 +144,14 @@ public class LoggingDistTest {
|
|||
void failUnknownHandlersInConfFile(KeycloakDistribution dist) {
|
||||
dist.copyOrReplaceFileFromClasspath("/logging/keycloak.conf", Paths.get("conf", "keycloak.conf"));
|
||||
CLIResult cliResult = dist.run("start-dev");
|
||||
cliResult.assertMessage("Invalid values in list for key: log Values: foo,console. Possible values are a combination of: console,file,gelf");
|
||||
cliResult.assertError("Invalid value for option 'kc.log': foo,console. Expected values are: console, file, gelf.");
|
||||
}
|
||||
|
||||
@Test
|
||||
void failEmptyLogErrorFromConfFileError(KeycloakDistribution dist) {
|
||||
dist.copyOrReplaceFileFromClasspath("/logging/emptylog.conf", Paths.get("conf", "emptylog.conf"));
|
||||
CLIResult cliResult = dist.run(CONFIG_FILE_LONG_NAME+"=../conf/emptylog.conf", "start-dev");
|
||||
cliResult.assertMessage("Value for configuration key 'log' is empty.");
|
||||
cliResult.assertError("Invalid value for option 'kc.log': . Expected values are: console, file, gelf.");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
@ -31,6 +31,7 @@ import org.keycloak.it.junit5.extension.DistributionTest;
|
|||
import io.quarkus.test.junit.main.Launch;
|
||||
import io.quarkus.test.junit.main.LaunchResult;
|
||||
import org.keycloak.it.junit5.extension.RawDistOnly;
|
||||
import org.keycloak.it.junit5.extension.WithEnvVars;
|
||||
import org.keycloak.it.utils.KeycloakDistribution;
|
||||
|
||||
@DistributionTest
|
||||
|
@ -61,7 +62,7 @@ public class StartCommandDistTest {
|
|||
@Launch({ "-v", "start", "--db=dev-mem", OPTIMIZED_BUILD_OPTION_LONG})
|
||||
void failBuildPropertyNotAvailable(LaunchResult result) {
|
||||
CLIResult cliResult = (CLIResult) result;
|
||||
cliResult.assertError("Unknown option: '--db'");
|
||||
cliResult.assertError("Build time option: '--db' not usable with pre-built image and --optimized");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -97,7 +98,15 @@ public class StartCommandDistTest {
|
|||
@Launch({ "start", "--optimized", "--http-enabled=true", "--hostname-strict=false", "--cache=local" })
|
||||
void testStartUsingOptimizedDoesNotAllowBuildOptions(LaunchResult result) {
|
||||
CLIResult cliResult = (CLIResult) result;
|
||||
cliResult.assertError("Unknown option: '--cache'");
|
||||
cliResult.assertError("Build time option: '--cache' not usable with pre-built image and --optimized");
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithEnvVars({"KC_LOG", "invalid"})
|
||||
@Launch({ "start", "--optimized" })
|
||||
void testStartUsingOptimizedInvalidEnvOption(LaunchResult result) {
|
||||
CLIResult cliResult = (CLIResult) result;
|
||||
cliResult.assertError("Invalid value for option 'kc.log': invalid. Expected values are: console, file, gelf. From ConfigSource KcEnvVarConfigSource");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
db=foo
|
|
@ -17,7 +17,9 @@ import org.testcontainers.utility.LazyFuture;
|
|||
import java.io.File;
|
||||
import java.lang.reflect.Field;
|
||||
import java.time.Duration;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
|
@ -38,12 +40,18 @@ public final class DockerKeycloakDistribution implements KeycloakDistribution {
|
|||
private String containerId = null;
|
||||
|
||||
private Executor parallelReaperExecutor = Executors.newSingleThreadExecutor();
|
||||
private Map<String, String> envVars = new HashMap<>();
|
||||
|
||||
public DockerKeycloakDistribution(boolean debug, boolean manualStop, boolean reCreate) {
|
||||
this.debug = debug;
|
||||
this.manualStop = manualStop;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setEnvVar(String name, String value) {
|
||||
this.envVars.put(name, value);
|
||||
}
|
||||
|
||||
private GenericContainer getKeycloakContainer() {
|
||||
File distributionFile = new File("../../dist/" + File.separator + "target" + File.separator + "keycloak-" + Version.VERSION + ".tar.gz");
|
||||
|
||||
|
@ -70,6 +78,7 @@ public final class DockerKeycloakDistribution implements KeycloakDistribution {
|
|||
}
|
||||
|
||||
return new GenericContainer(image)
|
||||
.withEnv(envVars)
|
||||
.withExposedPorts(8080)
|
||||
.withStartupAttempts(1)
|
||||
.withStartupTimeout(Duration.ofSeconds(120))
|
||||
|
@ -102,6 +111,10 @@ public final class DockerKeycloakDistribution implements KeycloakDistribution {
|
|||
cleanupContainer();
|
||||
keycloakContainer = null;
|
||||
LOGGER.warn("Failed to start Keycloak container", cause);
|
||||
} finally {
|
||||
if (!manualStop) {
|
||||
envVars.clear();
|
||||
}
|
||||
}
|
||||
|
||||
trySetRestAssuredPort();
|
||||
|
|
|
@ -15,7 +15,7 @@ https-key-store-file=${kc.home.dir}/conf/keycloak.jks
|
|||
https-key-store-password=secret
|
||||
https-trust-store-file=${kc.home.dir}/conf/keycloak.truststore
|
||||
https-trust-store-password=secret
|
||||
https-client-auth=REQUEST
|
||||
https-client-auth=request
|
||||
|
||||
# Proxy
|
||||
# Using any proxy setting which evaluates the forward proxy header
|
||||
|
|
Loading…
Reference in a new issue