Improvements to error messages

This commit is contained in:
Pedro Igor 2020-10-07 17:32:36 -03:00
parent d269af1b70
commit 1a1c42c776
6 changed files with 174 additions and 97 deletions

View file

@ -31,7 +31,7 @@
<packaging>pom</packaging> <packaging>pom</packaging>
<properties> <properties>
<quarkus.version>1.8.2.Final</quarkus.version> <quarkus.version>1.9.0.CR1</quarkus.version>
<resteasy.version>4.5.6.Final</resteasy.version> <resteasy.version>4.5.6.Final</resteasy.version>
<jackson.version>2.11.2</jackson.version> <jackson.version>2.11.2</jackson.version>
<jackson.databind.version>${jackson.version}</jackson.databind.version> <jackson.databind.version>${jackson.version}</jackson.databind.version>

View file

@ -17,63 +17,80 @@
package org.keycloak.cli; package org.keycloak.cli;
import static org.keycloak.cli.Picocli.createCommandSpec; import static org.keycloak.cli.Picocli.createCommandLine;
import static org.keycloak.cli.Picocli.error;
import static org.keycloak.cli.Picocli.parseConfigArgs; import static org.keycloak.cli.Picocli.parseConfigArgs;
import static org.keycloak.util.Environment.getProfileOrDefault;
import java.util.Arrays; import java.util.Arrays;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import io.quarkus.runtime.Quarkus;
import org.keycloak.common.Version; import org.keycloak.common.Version;
import io.quarkus.runtime.Quarkus;
import io.quarkus.runtime.annotations.QuarkusMain; import io.quarkus.runtime.annotations.QuarkusMain;
import org.keycloak.util.Environment;
import picocli.CommandLine; import picocli.CommandLine;
/**
* <p>The main entry point, responsible for initialize and run the CLI as well as start the server.
*/
@QuarkusMain(name = "keycloak") @QuarkusMain(name = "keycloak")
public class KeycloakMain { public class KeycloakMain {
public static void main(String args[]) { public static void main(String cliArgs[]) {
System.setProperty("kc.version", Version.VERSION_KEYCLOAK); System.setProperty("kc.version", Version.VERSION_KEYCLOAK);
CommandLine cmd = createCommandLine();
if (args.length != 0) { if (cliArgs.length == 0) {
CommandLine cmd = new CommandLine(createCommandSpec()); // no arguments, just start the server
start(cmd);
cmd.setExecutionExceptionHandler(new CommandLine.IExecutionExceptionHandler() { System.exit(cmd.getCommandSpec().exitCodeOnSuccess());
@Override
public int handleExecutionException(Exception ex, CommandLine commandLine,
CommandLine.ParseResult parseResult) {
commandLine.getErr().println(ex.getMessage());
commandLine.usage(commandLine.getErr());
return commandLine.getCommandSpec().exitCodeOnExecutionException();
}
});
List<String> argsList = new LinkedList<>(Arrays.asList(args));
try {
System.setProperty("kc.config.args", parseConfigArgs(argsList));
CommandLine.ParseResult result = cmd.parseArgs(argsList.toArray(new String[argsList.size()]));
if (!result.hasSubcommand() && (!result.isUsageHelpRequested() && !result.isVersionHelpRequested())) {
argsList.add(0, "start");
}
} catch (CommandLine.UnmatchedArgumentException e) {
if (!cmd.getParseResult().hasSubcommand() && argsList.get(0).startsWith("--")) {
argsList.add(0, "start");
} else {
cmd.getErr().println(e.getMessage());
System.exit(CommandLine.ExitCode.SOFTWARE);
}
} catch (Exception e) {
cmd.getErr().println(e.getMessage());
System.exit(CommandLine.ExitCode.SOFTWARE);
}
System.exit(cmd.execute(argsList.toArray(new String[argsList.size()])));
} }
Quarkus.run(args); // parse arguments and execute any of the configured commands
parseAndRun(cmd, cliArgs);
}
static void start(CommandLine cmd) {
// We should use the method call below to start the server once Quarkus is released with https://github.com/quarkusio/quarkus/pull/12532.
// It will allow us to properly handle exception thrown during startup and at runtime.
// Quarkus.run(null, (integer, throwable) -> {
// error(cmd, String.format("Failed to start server using profile (%s).", getProfileOrDefault("none")), throwable.getCause());
// });
Quarkus.run(null, (integer) -> {
error(cmd, String.format("Failed to start server using profile (%s).", getProfileOrDefault("none")), null);
});
Quarkus.waitForExit(); Quarkus.waitForExit();
} }
private static void parseAndRun(CommandLine cmd, String[] args) {
List<String> cliArgs = new LinkedList<>(Arrays.asList(args));
// set the arguments as a system property so that arguments can be mapped to their respective configuration options
System.setProperty("kc.config.args", parseConfigArgs(cliArgs));
try {
CommandLine.ParseResult result = cmd.parseArgs(cliArgs.toArray(new String[cliArgs.size()]));
// if no command was set, the start command becomes the default
if (!result.hasSubcommand() && (!result.isUsageHelpRequested() && !result.isVersionHelpRequested())) {
cliArgs.add(0, "start");
}
} catch (CommandLine.UnmatchedArgumentException e) {
// if no command was set but options were provided, the start command becomes the default
if (!cmd.getParseResult().hasSubcommand() && cliArgs.get(0).startsWith("--")) {
cliArgs.add(0, "start");
} else {
cmd.getErr().println(e.getMessage());
System.exit(cmd.getCommandSpec().exitCodeOnInvalidInput());
}
} catch (Exception e) {
cmd.getErr().println(e.getMessage());
System.exit(cmd.getCommandSpec().exitCodeOnExecutionException());
}
System.exit(cmd.execute(cliArgs.toArray(new String[cliArgs.size()])));
}
} }

View file

@ -17,10 +17,13 @@
package org.keycloak.cli; package org.keycloak.cli;
import static org.keycloak.cli.Picocli.error;
import static org.keycloak.cli.Picocli.errorAndExit;
import static org.keycloak.cli.Picocli.println;
import org.keycloak.configuration.KeycloakConfigSourceProvider; import org.keycloak.configuration.KeycloakConfigSourceProvider;
import io.quarkus.bootstrap.runner.QuarkusEntryPoint; import io.quarkus.bootstrap.runner.QuarkusEntryPoint;
import io.quarkus.runtime.Quarkus;
import org.keycloak.util.Environment; import org.keycloak.util.Environment;
import picocli.CommandLine; import picocli.CommandLine;
import picocli.CommandLine.Command; import picocli.CommandLine.Command;
@ -68,30 +71,15 @@ public class MainCommand {
usageHelpAutoWidth = true, usageHelpAutoWidth = true,
optionListHeading = "%nOptions%n", optionListHeading = "%nOptions%n",
parameterListHeading = "Available Commands%n") parameterListHeading = "Available Commands%n")
public void reAugment(@Option(names = "--verbose", description = "Print out more details when running this command.", required = false) Boolean debug) { public void reAugment(@Option(names = "--verbose", description = "Print out more details when running this command.", required = false) Boolean verbose) {
System.setProperty("quarkus.launch.rebuild", "true"); System.setProperty("quarkus.launch.rebuild", "true");
println("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 {
QuarkusEntryPoint.main(); QuarkusEntryPoint.main();
println("Server configuration updated and persisted. Run the following command to review the configuration:\n"); println(spec.commandLine(), "Server configuration updated and persisted. Run the following command to review the configuration:\n");
println("\t" + Environment.getCommand() + " show-config\n"); println(spec.commandLine(), "\t" + Environment.getCommand() + " show-config\n");
} catch (Throwable throwable) { } catch (Throwable throwable) {
String message = throwable.getMessage(); error(spec.commandLine(), "Failed to update server configuration.", throwable);
Throwable cause = throwable.getCause();
if (cause != null) {
message = cause.getMessage();
}
error("Failed to update server configuration: " + message);
if (debug == null) {
errorAndExit("For more details run the same command passing the '--verbose' option.");
} else {
error("Details:");
throwable.printStackTrace();
System.exit(spec.exitCodeOnExecutionException());
}
} }
} }
@ -100,10 +88,10 @@ public class MainCommand {
mixinStandardHelpOptions = true, mixinStandardHelpOptions = true,
optionListHeading = "%nOptions%n", optionListHeading = "%nOptions%n",
parameterListHeading = "Available Commands%n") parameterListHeading = "Available Commands%n")
public void startDev() { public void startDev(@Option(names = "--verbose", description = "Print out more details when running this command.", required = false) Boolean verbose) {
System.setProperty("kc.profile", "dev"); System.setProperty("kc.profile", "dev");
System.setProperty("quarkus.profile", "dev"); System.setProperty("quarkus.profile", "dev");
start(); KeycloakMain.start(spec.commandLine());
} }
@Command(name = "export", @Command(name = "export",
@ -116,7 +104,8 @@ public class MainCommand {
@Option(names = "--file", arity = "1", description = "Set the path to a file that will be created with the exported data.", paramLabel = "<path>") String toFile, @Option(names = "--file", arity = "1", description = "Set the path to a file that will be created with the exported data.", paramLabel = "<path>") String toFile,
@Option(names = "--realm", arity = "1", description = "Set the name of the realm to export", paramLabel = "<realm>") String realm, @Option(names = "--realm", arity = "1", description = "Set the name of the realm to export", paramLabel = "<realm>") String realm,
@Option(names = "--users", arity = "1", description = "Set how users should be exported. Possible values are: skip, realm_file, same_file, different_files.", paramLabel = "<strategy>", defaultValue = "different_files") String users, @Option(names = "--users", arity = "1", description = "Set how users should be exported. Possible values are: skip, realm_file, same_file, different_files.", paramLabel = "<strategy>", defaultValue = "different_files") String users,
@Option(names = "--users-per-file", arity = "1", description = "Set the number of users per file. Its used only if --users=different_files.", paramLabel = "<number>", defaultValue = "50") Integer usersPerFile) { @Option(names = "--users-per-file", arity = "1", description = "Set the number of users per file. Its used only if --users=different_files.", paramLabel = "<number>", defaultValue = "50") Integer usersPerFile,
@Option(names = "--verbose", description = "Print out more details when running this command.", required = false) Boolean verbose) {
System.setProperty("keycloak.migration.action", "export"); System.setProperty("keycloak.migration.action", "export");
if (toDir != null) { if (toDir != null) {
@ -126,7 +115,7 @@ public class MainCommand {
System.setProperty("keycloak.migration.provider", "singleFile"); System.setProperty("keycloak.migration.provider", "singleFile");
System.setProperty("keycloak.migration.file", toFile); System.setProperty("keycloak.migration.file", toFile);
} else { } else {
errorAndExit("Must specify either --dir or --file options."); errorAndExit(spec.commandLine(), "Must specify either --dir or --file options.");
} }
System.setProperty("keycloak.migration.usersExportStrategy", users.toUpperCase()); System.setProperty("keycloak.migration.usersExportStrategy", users.toUpperCase());
@ -138,7 +127,7 @@ public class MainCommand {
if (realm != null) { if (realm != null) {
System.setProperty("keycloak.migration.realmName", realm); System.setProperty("keycloak.migration.realmName", realm);
} }
start(); KeycloakMain.start(spec.commandLine());
} }
@Command(name = "import", @Command(name = "import",
@ -150,7 +139,8 @@ public class MainCommand {
public void runImport(@Option(names = "--dir", arity = "1", description = "Set the path to a directory containing the files with the data to import", paramLabel = "<path>") String toDir, public void runImport(@Option(names = "--dir", arity = "1", description = "Set the path to a directory containing the files with the data to import", paramLabel = "<path>") String toDir,
@Option(names = "--file", arity = "1", description = "Set the path to a file with the data to import.", paramLabel = "<path>") String toFile, @Option(names = "--file", arity = "1", description = "Set the path to a file with the data to import.", paramLabel = "<path>") String toFile,
@Option(names = "--realm", arity = "1", description = "Set the name of the realm to import", paramLabel = "<realm>") String realm, @Option(names = "--realm", arity = "1", description = "Set the name of the realm to import", paramLabel = "<realm>") String realm,
@Option(names = "--override", arity = "1", description = "Set if existing data should be skipped or overridden.", paramLabel = "false", defaultValue = "true") boolean override) { @Option(names = "--override", arity = "1", description = "Set if existing data should be skipped or overridden.", paramLabel = "false", defaultValue = "true") boolean override,
@Option(names = "--verbose", description = "Print out more details when running this command.", required = false) Boolean verbose) {
System.setProperty("keycloak.migration.action", "import"); System.setProperty("keycloak.migration.action", "import");
if (toDir != null) { if (toDir != null) {
System.setProperty("keycloak.migration.provider", "dir"); System.setProperty("keycloak.migration.provider", "dir");
@ -159,7 +149,7 @@ public class MainCommand {
System.setProperty("keycloak.migration.provider", "singleFile"); System.setProperty("keycloak.migration.provider", "singleFile");
System.setProperty("keycloak.migration.file", toFile); System.setProperty("keycloak.migration.file", toFile);
} else { } else {
errorAndExit("Must specify either --dir or --file options."); errorAndExit(spec.commandLine(), "Must specify either --dir or --file options.");
} }
if (realm != null) { if (realm != null) {
@ -168,7 +158,7 @@ public class MainCommand {
System.setProperty("keycloak.migration.strategy", override ? "OVERWRITE_EXISTING" : "IGNORE_EXISTING"); System.setProperty("keycloak.migration.strategy", override ? "OVERWRITE_EXISTING" : "IGNORE_EXISTING");
start(); KeycloakMain.start(spec.commandLine());
} }
@Command(name = "start", @Command(name = "start",
@ -179,14 +169,15 @@ public class MainCommand {
parameterListHeading = "Available Commands%n") parameterListHeading = "Available Commands%n")
public void start( public void start(
@CommandLine.Parameters(paramLabel = "show-config", arity = "0..1", @CommandLine.Parameters(paramLabel = "show-config", arity = "0..1",
description = "Print out the configuration options when starting the server.") String showConfig) { description = "Print out the configuration options when starting the server.") String showConfig,
@Option(names = "--verbose", description = "Print out more details when running this command.", required = false) Boolean verbose) {
if ("show-config".equals(showConfig)) { if ("show-config".equals(showConfig)) {
System.setProperty("kc.show.config.runtime", Boolean.TRUE.toString()); System.setProperty("kc.show.config.runtime", Boolean.TRUE.toString());
System.setProperty("kc.show.config", "all"); System.setProperty("kc.show.config", "all");
} else if (showConfig != null) { } else if (showConfig != null) {
throw new CommandLine.UnmatchedArgumentException(spec.commandLine(), "Invalid argument: " + showConfig); throw new CommandLine.UnmatchedArgumentException(spec.commandLine(), "Invalid argument: " + showConfig);
} }
start(); KeycloakMain.start(spec.commandLine());
} }
@Command(name = "show-config", @Command(name = "show-config",
@ -195,26 +186,9 @@ public class MainCommand {
optionListHeading = "%nOptions%n", optionListHeading = "%nOptions%n",
parameterListHeading = "Available Commands%n") parameterListHeading = "Available Commands%n")
public void showConfiguration( public void showConfiguration(
@CommandLine.Parameters(paramLabel = "filter", defaultValue = "none", description = "Show all configuration options. Use 'all' to show all options.") String filter) { @CommandLine.Parameters(paramLabel = "filter", defaultValue = "none", description = "Show all configuration options. Use 'all' to show all options.") String filter,
@Option(names = "--verbose", description = "Print out more details when running this command.", required = false) Boolean verbose) {
System.setProperty("kc.show.config", filter); System.setProperty("kc.show.config", filter);
start(); KeycloakMain.start(spec.commandLine());
}
private void start() {
Quarkus.run();
Quarkus.waitForExit();
}
private void println(String message) {
spec.commandLine().getOut().println(message);
}
private void errorAndExit(String message) {
error(message);
System.exit(CommandLine.ExitCode.SOFTWARE);
}
private void error(String message) {
spec.commandLine().getErr().println(message);
} }
} }

View file

@ -18,10 +18,12 @@
package org.keycloak.cli; package org.keycloak.cli;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.function.IntFunction; import java.util.function.IntFunction;
import io.quarkus.runtime.Quarkus;
import org.keycloak.common.Profile; import org.keycloak.common.Profile;
import org.keycloak.configuration.PropertyMapper; import org.keycloak.configuration.PropertyMapper;
import org.keycloak.configuration.PropertyMappers; import org.keycloak.configuration.PropertyMappers;
@ -30,11 +32,12 @@ import picocli.CommandLine;
final class Picocli { final class Picocli {
static CommandLine.Model.CommandSpec createCommandSpec() { static CommandLine createCommandLine() {
CommandLine.Model.CommandSpec spec = CommandLine.Model.CommandSpec.forAnnotatedObject(new MainCommand()) CommandLine.Model.CommandSpec spec = CommandLine.Model.CommandSpec.forAnnotatedObject(new MainCommand())
.name(Environment.getCommand()); .name(Environment.getCommand());
addOption(spec, "start", PropertyMappers.getRuntimeMappers()); addOption(spec, "start", PropertyMappers.getRuntimeMappers());
addOption(spec, "start-dev", PropertyMappers.getRuntimeMappers());
addOption(spec, "config", PropertyMappers.getRuntimeMappers()); addOption(spec, "config", PropertyMappers.getRuntimeMappers());
addOption(spec, "config", PropertyMappers.getBuiltTimeMappers()); addOption(spec, "config", PropertyMappers.getBuiltTimeMappers());
addOption(spec.subcommands().get("config").getCommandSpec(), "--features", "Enables a group of features. Possible values are: " addOption(spec.subcommands().get("config").getCommandSpec(), "--features", "Enables a group of features. Possible values are: "
@ -45,7 +48,20 @@ final class Picocli {
addOption(spec.subcommands().get("config").getCommandSpec(), "--features-" + feature.name().toLowerCase(), addOption(spec.subcommands().get("config").getCommandSpec(), "--features-" + feature.name().toLowerCase(),
"Enables the " + feature.name() + " feature. Set enabled to enable the feature or disabled otherwise."); "Enables the " + feature.name() + " feature. Set enabled to enable the feature or disabled otherwise.");
} }
return spec;
CommandLine cmd = new CommandLine(spec);
cmd.setExecutionExceptionHandler(new CommandLine.IExecutionExceptionHandler() {
@Override
public int handleExecutionException(Exception ex, CommandLine commandLine,
CommandLine.ParseResult parseResult) {
commandLine.getErr().println(ex.getMessage());
commandLine.usage(commandLine.getErr());
return commandLine.getCommandSpec().exitCodeOnExecutionException();
}
});
return cmd;
} }
static String parseConfigArgs(List<String> argsList) { static String parseConfigArgs(List<String> argsList) {
@ -92,4 +108,47 @@ final class Picocli {
.paramLabel("<value>") .paramLabel("<value>")
.type(String.class).build()); .type(String.class).build());
} }
static List<String> getCliArgs(CommandLine cmd) {
CommandLine.ParseResult parseResult = cmd.getParseResult();
if (parseResult == null) {
return Collections.emptyList();
}
return parseResult.expandedArgs();
}
static void errorAndExit(CommandLine cmd, String message) {
error(cmd, message, null);
}
static void error(CommandLine cmd, String message, Throwable throwable) {
List<String> cliArgs = getCliArgs(cmd);
cmd.getErr().println("ERROR: " + message);
if (throwable != null) {
Throwable cause = throwable;
do {
if (cause.getMessage() != null) {
cmd.getErr().println(String.format("ERROR: %s", cause.getMessage()));
}
} while ((cause = cause.getCause())!= null);
if (cliArgs.stream().anyMatch((arg) -> "--verbose".equals(arg))) {
cmd.getErr().println("ERROR: Details:");
throwable.printStackTrace();
} else {
cmd.getErr().println("For more details run the same command passing the '--verbose' option.");
}
}
System.exit(cmd.getCommandSpec().exitCodeOnExecutionException());
}
static void println(CommandLine cmd, String message) {
cmd.getOut().println(message);
}
} }

View file

@ -109,9 +109,9 @@ public final class PropertyMappers {
case "postgres-10": case "postgres-10":
return "io.quarkus.hibernate.orm.runtime.dialect.QuarkusPostgreSQL10Dialect"; return "io.quarkus.hibernate.orm.runtime.dialect.QuarkusPostgreSQL10Dialect";
} }
throw invalidDatabaseVendor(db, "h2-file", "h2-mem", "mariadb", "postgres", "postgres-95", "postgres-10"); return null;
}, "The database vendor. Possible values are: h2-mem, h2-file, mariadb, postgres95, postgres10."); }, null);
create("db", "quarkus.datasource.driver", (db, context) -> { create("db", "quarkus.datasource.jdbc.driver", (db, context) -> {
switch (db.toLowerCase()) { switch (db.toLowerCase()) {
case "h2-file": case "h2-file":
case "h2-mem": case "h2-mem":
@ -124,8 +124,21 @@ public final class PropertyMappers {
} }
return null; return null;
}, null); }, null);
create("db", "quarkus.datasource.db-kind", (db, context) -> {
switch (db.toLowerCase()) {
case "h2-file":
case "h2-mem":
return "h2";
case "mariadb":
return "mariadb";
case "postgres-95":
case "postgres-10":
return "postgresql";
}
throw invalidDatabaseVendor(db, "h2-file", "h2-mem", "mariadb", "postgres", "postgres-95", "postgres-10");
}, "The database vendor. Possible values are: h2-mem, h2-file, mariadb, postgres95, postgres10.");
create("db", "quarkus.datasource.jdbc.transactions", (db, context) -> "xa", null); create("db", "quarkus.datasource.jdbc.transactions", (db, context) -> "xa", null);
create("db.url", "db", "quarkus.datasource.url", (db, context) -> { create("db.url", "db", "quarkus.datasource.jdbc.url", (db, context) -> {
switch (db.toLowerCase()) { switch (db.toLowerCase()) {
case "h2-file": case "h2-file":
return "jdbc:h2:file:${kc.home.dir:${kc.db.url.path:~}}/${kc.data.dir:data}/keycloakdb${kc.db.url.properties:;;AUTO_SERVER=TRUE}"; return "jdbc:h2:file:${kc.home.dir:${kc.db.url.path:~}}/${kc.data.dir:data}/keycloakdb${kc.db.url.properties:;;AUTO_SERVER=TRUE}";

View file

@ -56,6 +56,20 @@ public final class Environment {
return profile; return profile;
} }
public static String getProfileOrDefault(String defaultProfile) {
String profile = System.getProperty("kc.profile");
if (profile == null) {
profile = System.getenv("KC_PROFILE");
}
if (profile == null) {
profile = defaultProfile;
}
return profile;
}
public static Optional<String> getBuiltTimeProperty(String name) { public static Optional<String> getBuiltTimeProperty(String name) {
String value = KeycloakRecorder.getBuiltTimeProperty(name); String value = KeycloakRecorder.getBuiltTimeProperty(name);