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 java.util.List;
|
||||||
|
|
||||||
import jakarta.enterprise.context.ApplicationScoped;
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
|
import picocli.CommandLine.ExitCode;
|
||||||
|
|
||||||
import io.quarkus.runtime.ApplicationLifecycleManager;
|
import io.quarkus.runtime.ApplicationLifecycleManager;
|
||||||
import io.quarkus.runtime.Quarkus;
|
import io.quarkus.runtime.Quarkus;
|
||||||
|
@ -40,6 +41,7 @@ import org.keycloak.Config;
|
||||||
import org.keycloak.models.KeycloakSessionFactory;
|
import org.keycloak.models.KeycloakSessionFactory;
|
||||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||||
import org.keycloak.quarkus.runtime.cli.ExecutionExceptionHandler;
|
import org.keycloak.quarkus.runtime.cli.ExecutionExceptionHandler;
|
||||||
|
import org.keycloak.quarkus.runtime.cli.NonCliPropertyException;
|
||||||
import org.keycloak.quarkus.runtime.cli.Picocli;
|
import org.keycloak.quarkus.runtime.cli.Picocli;
|
||||||
import org.keycloak.common.Version;
|
import org.keycloak.common.Version;
|
||||||
import org.keycloak.quarkus.runtime.cli.command.Start;
|
import org.keycloak.quarkus.runtime.cli.command.Start;
|
||||||
|
@ -75,6 +77,15 @@ public class KeycloakMain implements QuarkusApplication {
|
||||||
|
|
||||||
if (isDevProfileNotAllowed()) {
|
if (isDevProfileNotAllowed()) {
|
||||||
errorHandler.error(errStream, Messages.devProfileNotAllowedError(Start.NAME), null);
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -42,6 +42,11 @@ public final class ExecutionExceptionHandler implements CommandLine.IExecutionEx
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int handleExecutionException(Exception cause, CommandLine cmd, ParseResult parseResult) {
|
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()
|
error(cmd.getErr(), "Failed to run '" + parseResult.subcommands().stream()
|
||||||
.map(ParseResult::commandSpec)
|
.map(ParseResult::commandSpec)
|
||||||
.map(CommandLine.Model.CommandSpec::name)
|
.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 org.keycloak.quarkus.runtime.Environment;
|
||||||
|
|
||||||
import io.smallrye.config.ConfigValue;
|
import io.smallrye.config.ConfigValue;
|
||||||
|
|
||||||
import picocli.CommandLine;
|
import picocli.CommandLine;
|
||||||
|
import picocli.CommandLine.ParameterException;
|
||||||
import picocli.CommandLine.Help.Ansi;
|
import picocli.CommandLine.Help.Ansi;
|
||||||
import picocli.CommandLine.Model.CommandSpec;
|
import picocli.CommandLine.Model.CommandSpec;
|
||||||
import picocli.CommandLine.Model.OptionSpec;
|
import picocli.CommandLine.Model.OptionSpec;
|
||||||
|
@ -82,19 +84,38 @@ public final class Picocli {
|
||||||
public static final String NO_PARAM_LABEL = "none";
|
public static final String NO_PARAM_LABEL = "none";
|
||||||
private static final String ARG_KEY_VALUE_SEPARATOR = "=";
|
private static final String ARG_KEY_VALUE_SEPARATOR = "=";
|
||||||
|
|
||||||
|
private static class IncludeOptions {
|
||||||
|
boolean includeRuntime;
|
||||||
|
boolean includeBuildTime;
|
||||||
|
}
|
||||||
|
|
||||||
private Picocli() {
|
private Picocli() {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void parseAndRun(List<String> cliArgs) {
|
public static void parseAndRun(List<String> cliArgs) {
|
||||||
CommandLine cmd = createCommandLine(cliArgs);
|
CommandLine cmd = createCommandLine(cliArgs);
|
||||||
|
|
||||||
|
String[] argArray = cliArgs.toArray(new String[0]);
|
||||||
if (Environment.isRebuildCheck()) {
|
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);
|
exitOnFailure(exitCode, cmd);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
int exitCode = cmd.execute(cliArgs.toArray(new String[0]));
|
int exitCode = cmd.execute(argArray);
|
||||||
exitOnFailure(exitCode, cmd);
|
exitOnFailure(exitCode, cmd);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -228,6 +249,74 @@ public final class Picocli {
|
||||||
return false;
|
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) {
|
private static boolean hasConfigChanges(CommandLine cmdCommand) {
|
||||||
Optional<String> currentProfile = ofNullable(Environment.getProfile());
|
Optional<String> currentProfile = ofNullable(Environment.getProfile());
|
||||||
Optional<String> persistedProfile = getBuildTimeProperty("kc.profile");
|
Optional<String> persistedProfile = getBuildTimeProperty("kc.profile");
|
||||||
|
@ -379,26 +468,33 @@ public final class Picocli {
|
||||||
return cmd;
|
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) {
|
private static void addCommandOptions(List<String> cliArgs, CommandLine command) {
|
||||||
if (command != null) {
|
if (command != null && command.getCommand() instanceof AbstractCommand) {
|
||||||
boolean includeBuildTime = false;
|
IncludeOptions options = getIncludeOptions(cliArgs, command.getCommand(), command.getCommandName());
|
||||||
boolean includeRuntime = false;
|
|
||||||
|
|
||||||
if (command.getCommand() instanceof AbstractCommand) {
|
if (!options.includeBuildTime && !options.includeRuntime) {
|
||||||
AbstractCommand abstractCommand = command.getCommand();
|
|
||||||
includeRuntime = abstractCommand.includeRuntime();
|
|
||||||
includeBuildTime = abstractCommand.includeBuildTime();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!includeBuildTime && !includeRuntime) {
|
|
||||||
return;
|
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 static org.keycloak.quarkus.runtime.cli.Picocli.ARG_PREFIX;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.Collection;
|
||||||
import java.util.Stack;
|
import java.util.Stack;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import java.util.stream.StreamSupport;
|
import java.util.stream.StreamSupport;
|
||||||
|
@ -55,7 +55,7 @@ public final class PropertyMapperParameterConsumer implements CommandLine.IParam
|
||||||
|
|
||||||
if (args.isEmpty() || !isOptionValue(args.peek())) {
|
if (args.isEmpty() || !isOptionValue(args.peek())) {
|
||||||
throw new ParameterException(
|
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
|
// consumes the value
|
||||||
|
@ -63,28 +63,29 @@ public final class PropertyMapperParameterConsumer implements CommandLine.IParam
|
||||||
|
|
||||||
if (!args.isEmpty() && isOptionValue(args.peek())) {
|
if (!args.isEmpty() && isOptionValue(args.peek())) {
|
||||||
throw new ParameterException(
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new ParameterException(
|
throw new ParameterException(commandLine, getErrorMessage(name, value, argSpec.completionCandidates(), option.completionCandidates()));
|
||||||
commandLine, "Invalid value for option '" + name + "': " + value + "." + getExpectedValuesMessage(argSpec, option));
|
}
|
||||||
|
|
||||||
|
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) {
|
private boolean isOptionValue(String arg) {
|
||||||
return !(arg.startsWith(ARG_PREFIX) || arg.startsWith(Picocli.ARG_SHORT_PREFIX));
|
return !(arg.startsWith(ARG_PREFIX) || arg.startsWith(Picocli.ARG_SHORT_PREFIX));
|
||||||
}
|
}
|
||||||
|
|
||||||
private String getExpectedValuesMessage(ArgSpec argSpec, OptionSpec option) {
|
static String getExpectedValuesMessage(Iterable<String> specCandidates, Iterable<String> optionCandidates) {
|
||||||
return option.completionCandidates().iterator().hasNext() ? " Expected values are: " + String.join(", ", argSpec.completionCandidates()) : "";
|
return optionCandidates.iterator().hasNext() ? " Expected values are: " + String.join(", ", specCandidates) : "";
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isExpectedValue(OptionSpec option, String value) {
|
static boolean isExpectedValue(Collection<String> expectedValues, String value) {
|
||||||
List<String> expectedValues = StreamSupport.stream(option.completionCandidates().spliterator(), false).collect(Collectors.toList());
|
|
||||||
|
|
||||||
if (expectedValues.isEmpty()) {
|
if (expectedValues.isEmpty()) {
|
||||||
// accept any
|
// accept any
|
||||||
return true;
|
return true;
|
||||||
|
|
|
@ -1,15 +1,24 @@
|
||||||
package org.keycloak.quarkus.runtime.cli;
|
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;
|
||||||
import picocli.CommandLine.IParameterExceptionHandler;
|
import picocli.CommandLine.IParameterExceptionHandler;
|
||||||
|
import picocli.CommandLine.Model.CommandSpec;
|
||||||
import picocli.CommandLine.ParameterException;
|
import picocli.CommandLine.ParameterException;
|
||||||
import picocli.CommandLine.UnmatchedArgumentException;
|
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 {
|
public class ShortErrorMessageHandler implements IParameterExceptionHandler {
|
||||||
|
|
||||||
|
@Override
|
||||||
public int handleParseException(ParameterException ex, String[] args) {
|
public int handleParseException(ParameterException ex, String[] args) {
|
||||||
CommandLine cmd = ex.getCommandLine();
|
CommandLine cmd = ex.getCommandLine();
|
||||||
PrintWriter writer = cmd.getErr();
|
PrintWriter writer = cmd.getErr();
|
||||||
|
@ -20,12 +29,28 @@ public class ShortErrorMessageHandler implements IParameterExceptionHandler {
|
||||||
|
|
||||||
String[] unmatched = getUnmatchedPartsByOptionSeparator(uae,"=");
|
String[] unmatched = getUnmatchedPartsByOptionSeparator(uae,"=");
|
||||||
String original = uae.getUnmatched().get(0);
|
String original = uae.getUnmatched().get(0);
|
||||||
|
|
||||||
if (unmatched[0].equals(original)) {
|
if (unmatched[0].equals(original)) {
|
||||||
unmatched = getUnmatchedPartsByOptionSeparator(uae," ");
|
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));
|
writer.println(cmd.getColorScheme().errorText(errorMessage));
|
||||||
|
@ -34,9 +59,13 @@ public class ShortErrorMessageHandler implements IParameterExceptionHandler {
|
||||||
CommandSpec spec = cmd.getCommandSpec();
|
CommandSpec spec = cmd.getCommandSpec();
|
||||||
writer.printf("Try '%s --help' for more information on the available options.%n", spec.qualifiedName());
|
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
|
return cmd.getExitCodeExceptionMapper() != null
|
||||||
? cmd.getExitCodeExceptionMapper().getExitCode(ex)
|
? cmd.getExitCodeExceptionMapper().getExitCode(ex)
|
||||||
: spec.exitCodeOnInvalidInput();
|
: cmd.getCommandSpec().exitCodeOnInvalidInput();
|
||||||
}
|
}
|
||||||
|
|
||||||
private String[] getUnmatchedPartsByOptionSeparator(UnmatchedArgumentException uae, String separator) {
|
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 static org.keycloak.quarkus.runtime.Messages.cliExecutionError;
|
||||||
|
|
||||||
import org.keycloak.config.OptionCategory;
|
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;
|
||||||
import picocli.CommandLine.Model.CommandSpec;
|
import picocli.CommandLine.Model.CommandSpec;
|
||||||
import picocli.CommandLine.Spec;
|
import picocli.CommandLine.Spec;
|
||||||
|
@ -60,4 +63,10 @@ public abstract class AbstractCommand {
|
||||||
public List<OptionCategory> getOptionCategories() {
|
public List<OptionCategory> getOptionCategories() {
|
||||||
return Arrays.asList(OptionCategory.values());
|
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() {
|
public void run() {
|
||||||
doBeforeRun();
|
doBeforeRun();
|
||||||
CommandLine cmd = spec.commandLine();
|
CommandLine cmd = spec.commandLine();
|
||||||
|
validateNonCliConfig();
|
||||||
KeycloakMain.start((ExecutionExceptionHandler) cmd.getExecutionExceptionHandler(), cmd.getErr(), cmd.getParseResult().originalArgs().toArray(new String[0]));
|
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();
|
exitWithErrorIfDevProfileIsSetAndNotStartDev();
|
||||||
|
|
||||||
System.setProperty("quarkus.launch.rebuild", "true");
|
System.setProperty("quarkus.launch.rebuild", "true");
|
||||||
|
validateNonCliConfig();
|
||||||
|
|
||||||
println(spec.commandLine(), "Updating the configuration and installing your custom providers, if any. Please wait.");
|
println(spec.commandLine(), "Updating the configuration and installing your custom providers, if any. Please wait.");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -106,6 +108,7 @@ public final class Build extends AbstractCommand implements Runnable {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public List<OptionCategory> getOptionCategories() {
|
public List<OptionCategory> getOptionCategories() {
|
||||||
// all options should work for the build command, otherwise re-augmentation might fail due to unknown options
|
// all options should work for the build command, otherwise re-augmentation might fail due to unknown options
|
||||||
return super.getOptionCategories();
|
return super.getOptionCategories();
|
||||||
|
@ -137,4 +140,9 @@ public final class Build extends AbstractCommand implements Runnable {
|
||||||
getHomePath().resolve("quarkus-artifact.properties").toFile().delete();
|
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());
|
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());
|
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("%"))
|
|| property.startsWith("%"))
|
||||||
&& !ignoredPropertyKeys.contains(property);
|
&& !ignoredPropertyKeys.contains(property);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return NAME;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -73,6 +73,7 @@ public final class Start extends AbstractStartCommand implements Runnable {
|
||||||
return Environment.isDevProfile();
|
return Environment.isDevProfile();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public List<OptionCategory> getOptionCategories() {
|
public List<OptionCategory> getOptionCategories() {
|
||||||
return super.getOptionCategories().stream().filter(optionCategory -> optionCategory != OptionCategory.EXPORT && optionCategory != OptionCategory.IMPORT).collect(Collectors.toList());
|
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() {
|
public boolean includeRuntime() {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return NAME;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -58,4 +58,9 @@ public final class StartDev extends AbstractStartCommand implements Runnable {
|
||||||
public boolean includeRuntime() {
|
public boolean includeRuntime() {
|
||||||
return true;
|
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.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
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.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.junit.jupiter.api.Test;
|
||||||
import org.keycloak.config.database.Database;
|
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 io.quarkus.test.junit.main.LaunchResult;
|
||||||
|
|
||||||
import org.keycloak.it.junit5.extension.RawDistOnly;
|
import org.keycloak.it.junit5.extension.RawDistOnly;
|
||||||
|
import org.keycloak.it.junit5.extension.WithEnvVars;
|
||||||
import org.keycloak.it.utils.KeycloakDistribution;
|
import org.keycloak.it.utils.KeycloakDistribution;
|
||||||
|
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
|
||||||
@DistributionTest
|
@DistributionTest
|
||||||
class BuildCommandDistTest {
|
class BuildCommandDistTest {
|
||||||
|
|
||||||
|
@ -64,7 +68,22 @@ class BuildCommandDistTest {
|
||||||
@Launch({ "build", "--db=postgres", "--db-username=myuser", "--db-password=mypassword", "--http-enabled=true" })
|
@Launch({ "build", "--db=postgres", "--db-username=myuser", "--db-password=mypassword", "--http-enabled=true" })
|
||||||
void testFailRuntimeOptions(LaunchResult result) {
|
void testFailRuntimeOptions(LaunchResult result) {
|
||||||
CLIResult cliResult = (CLIResult) 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
|
@Test
|
||||||
|
|
|
@ -144,14 +144,14 @@ public class LoggingDistTest {
|
||||||
void failUnknownHandlersInConfFile(KeycloakDistribution dist) {
|
void failUnknownHandlersInConfFile(KeycloakDistribution dist) {
|
||||||
dist.copyOrReplaceFileFromClasspath("/logging/keycloak.conf", Paths.get("conf", "keycloak.conf"));
|
dist.copyOrReplaceFileFromClasspath("/logging/keycloak.conf", Paths.get("conf", "keycloak.conf"));
|
||||||
CLIResult cliResult = dist.run("start-dev");
|
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
|
@Test
|
||||||
void failEmptyLogErrorFromConfFileError(KeycloakDistribution dist) {
|
void failEmptyLogErrorFromConfFileError(KeycloakDistribution dist) {
|
||||||
dist.copyOrReplaceFileFromClasspath("/logging/emptylog.conf", Paths.get("conf", "emptylog.conf"));
|
dist.copyOrReplaceFileFromClasspath("/logging/emptylog.conf", Paths.get("conf", "emptylog.conf"));
|
||||||
CLIResult cliResult = dist.run(CONFIG_FILE_LONG_NAME+"=../conf/emptylog.conf", "start-dev");
|
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
|
@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.Launch;
|
||||||
import io.quarkus.test.junit.main.LaunchResult;
|
import io.quarkus.test.junit.main.LaunchResult;
|
||||||
import org.keycloak.it.junit5.extension.RawDistOnly;
|
import org.keycloak.it.junit5.extension.RawDistOnly;
|
||||||
|
import org.keycloak.it.junit5.extension.WithEnvVars;
|
||||||
import org.keycloak.it.utils.KeycloakDistribution;
|
import org.keycloak.it.utils.KeycloakDistribution;
|
||||||
|
|
||||||
@DistributionTest
|
@DistributionTest
|
||||||
|
@ -61,7 +62,7 @@ public class StartCommandDistTest {
|
||||||
@Launch({ "-v", "start", "--db=dev-mem", OPTIMIZED_BUILD_OPTION_LONG})
|
@Launch({ "-v", "start", "--db=dev-mem", OPTIMIZED_BUILD_OPTION_LONG})
|
||||||
void failBuildPropertyNotAvailable(LaunchResult result) {
|
void failBuildPropertyNotAvailable(LaunchResult result) {
|
||||||
CLIResult cliResult = (CLIResult) 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
|
@Test
|
||||||
|
@ -97,7 +98,15 @@ public class StartCommandDistTest {
|
||||||
@Launch({ "start", "--optimized", "--http-enabled=true", "--hostname-strict=false", "--cache=local" })
|
@Launch({ "start", "--optimized", "--http-enabled=true", "--hostname-strict=false", "--cache=local" })
|
||||||
void testStartUsingOptimizedDoesNotAllowBuildOptions(LaunchResult result) {
|
void testStartUsingOptimizedDoesNotAllowBuildOptions(LaunchResult result) {
|
||||||
CLIResult cliResult = (CLIResult) 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
|
@Test
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
db=foo
|
|
@ -17,7 +17,9 @@ import org.testcontainers.utility.LazyFuture;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.lang.reflect.Field;
|
import java.lang.reflect.Field;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.concurrent.Executor;
|
import java.util.concurrent.Executor;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
|
|
||||||
|
@ -38,12 +40,18 @@ public final class DockerKeycloakDistribution implements KeycloakDistribution {
|
||||||
private String containerId = null;
|
private String containerId = null;
|
||||||
|
|
||||||
private Executor parallelReaperExecutor = Executors.newSingleThreadExecutor();
|
private Executor parallelReaperExecutor = Executors.newSingleThreadExecutor();
|
||||||
|
private Map<String, String> envVars = new HashMap<>();
|
||||||
|
|
||||||
public DockerKeycloakDistribution(boolean debug, boolean manualStop, boolean reCreate) {
|
public DockerKeycloakDistribution(boolean debug, boolean manualStop, boolean reCreate) {
|
||||||
this.debug = debug;
|
this.debug = debug;
|
||||||
this.manualStop = manualStop;
|
this.manualStop = manualStop;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setEnvVar(String name, String value) {
|
||||||
|
this.envVars.put(name, value);
|
||||||
|
}
|
||||||
|
|
||||||
private GenericContainer getKeycloakContainer() {
|
private GenericContainer getKeycloakContainer() {
|
||||||
File distributionFile = new File("../../dist/" + File.separator + "target" + File.separator + "keycloak-" + Version.VERSION + ".tar.gz");
|
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)
|
return new GenericContainer(image)
|
||||||
|
.withEnv(envVars)
|
||||||
.withExposedPorts(8080)
|
.withExposedPorts(8080)
|
||||||
.withStartupAttempts(1)
|
.withStartupAttempts(1)
|
||||||
.withStartupTimeout(Duration.ofSeconds(120))
|
.withStartupTimeout(Duration.ofSeconds(120))
|
||||||
|
@ -102,6 +111,10 @@ public final class DockerKeycloakDistribution implements KeycloakDistribution {
|
||||||
cleanupContainer();
|
cleanupContainer();
|
||||||
keycloakContainer = null;
|
keycloakContainer = null;
|
||||||
LOGGER.warn("Failed to start Keycloak container", cause);
|
LOGGER.warn("Failed to start Keycloak container", cause);
|
||||||
|
} finally {
|
||||||
|
if (!manualStop) {
|
||||||
|
envVars.clear();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
trySetRestAssuredPort();
|
trySetRestAssuredPort();
|
||||||
|
|
|
@ -15,7 +15,7 @@ https-key-store-file=${kc.home.dir}/conf/keycloak.jks
|
||||||
https-key-store-password=secret
|
https-key-store-password=secret
|
||||||
https-trust-store-file=${kc.home.dir}/conf/keycloak.truststore
|
https-trust-store-file=${kc.home.dir}/conf/keycloak.truststore
|
||||||
https-trust-store-password=secret
|
https-trust-store-password=secret
|
||||||
https-client-auth=REQUEST
|
https-client-auth=request
|
||||||
|
|
||||||
# Proxy
|
# Proxy
|
||||||
# Using any proxy setting which evaluates the forward proxy header
|
# Using any proxy setting which evaluates the forward proxy header
|
||||||
|
|
Loading…
Reference in a new issue