Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import java.util.List;

import jakarta.enterprise.context.ApplicationScoped;
import picocli.CommandLine.ExitCode;

import io.quarkus.runtime.ApplicationLifecycleManager;
import io.quarkus.runtime.Quarkus;
Expand All @@ -40,6 +41,7 @@
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;
Expand Down Expand Up @@ -75,6 +77,15 @@ public static void main(String[] args) {

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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ public ExecutionExceptionHandler() {}

@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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,9 @@
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;
Expand All @@ -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);
}

Expand Down Expand Up @@ -228,6 +249,74 @@ private static boolean hasProviderChanges() {
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());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One last nitpick. :) We could use better naming of config sources in the error messages to avoid stuff like:

... From ConfigSource KcEnvVarConfigSource

We can't probably rely on the getName() as we don't own all config sources so we can't change the names there. We'd might need some sort of mapping.

But we already use the same naming e.g. in show-config. So definitely follow-up (if anything).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, mentioned this as well in #23797 (comment) - so we'll capture that as a follow-up.

}
}
}

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",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It'd be nice if we could use logger here to allow users to filter this? But as already discussed offline, it seems not something we can easily achieve as deferred logging sometimes swallow messages.

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");
Expand Down Expand Up @@ -379,26 +468,33 @@ public static CommandLine createCommandLine(List<String> cliArgs) {
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.getCommand() instanceof AbstractCommand) {
AbstractCommand abstractCommand = command.getCommand();
includeRuntime = abstractCommand.includeRuntime();
includeBuildTime = abstractCommand.includeBuildTime();
}
if (command != null && command.getCommand() instanceof AbstractCommand) {
IncludeOptions options = getIncludeOptions(cliArgs, command.getCommand(), command.getCommandName());

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);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

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;
Expand Down Expand Up @@ -55,36 +55,37 @@ private void validateOption(Stack<String> args, ArgSpec argSpec, CommandSpec com

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
String value = args.pop();

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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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();
Expand All @@ -20,12 +29,28 @@ public int handleParseException(ParameterException ex, String[] args) {

String[] unmatched = getUnmatchedPartsByOptionSeparator(uae,"=");
String original = uae.getUnmatched().get(0);

if (unmatched[0].equals(original)) {
unmatched = getUnmatchedPartsByOptionSeparator(uae," ");
}

String cliKey = unmatched[0];

PropertyMapper<?> mapper = PropertyMappers.getMapper(cliKey);

errorMessage = "Unknown option: '" + unmatched[0] + "'";
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();
}
}
}
Comment thread
vmuzikar marked this conversation as resolved.
Outdated
}

writer.println(cmd.getColorScheme().errorText(errorMessage));
Expand All @@ -34,9 +59,13 @@ public int handleParseException(ParameterException ex, String[] args) {
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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@
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;
Expand Down Expand Up @@ -60,4 +63,10 @@ public boolean includeBuildTime() {
public List<OptionCategory> getOptionCategories() {
return Arrays.asList(OptionCategory.values());
}

protected void validateNonCliConfig() {
Picocli.validateNonCliConfig(ConfigArgsConfigSource.getAllCliArgs(), this, spec.commandLine().getOut());
}

public abstract String getName();
}
Loading