diff --git a/quarkus/deployment/src/main/java/org/keycloak/quarkus/deployment/KeycloakProcessor.java b/quarkus/deployment/src/main/java/org/keycloak/quarkus/deployment/KeycloakProcessor.java
index 0a87bdfe74..2f0b15b8d1 100644
--- a/quarkus/deployment/src/main/java/org/keycloak/quarkus/deployment/KeycloakProcessor.java
+++ b/quarkus/deployment/src/main/java/org/keycloak/quarkus/deployment/KeycloakProcessor.java
@@ -241,7 +241,7 @@ class KeycloakProcessor {
}
}
- for (File jar : getProviderFiles()) {
+ for (File jar : getProviderFiles().values()) {
properties.put(String.format("kc.provider.file.%s.last-modified", jar.getName()), String.valueOf(jar.lastModified()));
}
diff --git a/quarkus/runtime/src/main/java/org/keycloak/KeycloakMain.java b/quarkus/runtime/src/main/java/org/keycloak/KeycloakMain.java
new file mode 100644
index 0000000000..15a40656c8
--- /dev/null
+++ b/quarkus/runtime/src/main/java/org/keycloak/KeycloakMain.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2020 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;
+
+import static org.keycloak.cli.Picocli.error;
+import static org.keycloak.cli.Picocli.parseAndRun;
+import static org.keycloak.util.Environment.getProfileOrDefault;
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import io.quarkus.runtime.ApplicationLifecycleManager;
+import io.quarkus.runtime.Quarkus;
+
+import org.keycloak.cli.Picocli;
+import org.keycloak.common.Version;
+
+import io.quarkus.runtime.QuarkusApplication;
+import io.quarkus.runtime.annotations.QuarkusMain;
+
+import org.keycloak.util.Environment;
+
+/**
+ *
The main entry point, responsible for initialize and run the CLI as well as start the server.
+ */
+@QuarkusMain(name = "keycloak")
+public class KeycloakMain implements QuarkusApplication {
+
+ public static void main(String[] args) {
+ System.setProperty("kc.version", Version.VERSION_KEYCLOAK);
+ List cliArgs = new ArrayList<>(Arrays.asList(args));
+ System.setProperty(Environment.CLI_ARGS, Picocli.parseConfigArgs(cliArgs));
+
+ if (cliArgs.isEmpty()) {
+ // no arguments, just start the server without running picocli
+ start(cliArgs, new PrintWriter(System.err));
+ return;
+ }
+
+ // parse arguments and execute any of the configured commands
+ parseAndRun(cliArgs);
+ }
+
+ public static void start(List cliArgs, PrintWriter errorWriter) {
+ try {
+ Quarkus.run(KeycloakMain.class, (integer, cause) -> {
+ if (cause != null) {
+ error(cliArgs, errorWriter,
+ String.format("Failed to start server using profile (%s)", getProfileOrDefault("none")),
+ cause.getCause());
+ }
+ });
+ } catch (Throwable cause) {
+ error(cliArgs, errorWriter,
+ String.format("Unexpected error when starting the server using profile (%s)", getProfileOrDefault("none")),
+ cause.getCause());
+ }
+ }
+
+ /**
+ * Should be called after the server is fully initialized
+ */
+ @Override
+ public int run(String... args) throws Exception {
+ Quarkus.waitForExit();
+ return ApplicationLifecycleManager.getExitCode();
+ }
+}
diff --git a/quarkus/runtime/src/main/java/org/keycloak/cli/ExecutionExceptionHandler.java b/quarkus/runtime/src/main/java/org/keycloak/cli/ExecutionExceptionHandler.java
new file mode 100644
index 0000000000..a958a49681
--- /dev/null
+++ b/quarkus/runtime/src/main/java/org/keycloak/cli/ExecutionExceptionHandler.java
@@ -0,0 +1,30 @@
+/*
+ * 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.cli;
+
+import picocli.CommandLine;
+
+public class ExecutionExceptionHandler implements 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();
+ }
+}
diff --git a/quarkus/runtime/src/main/java/org/keycloak/cli/KeycloakMain.java b/quarkus/runtime/src/main/java/org/keycloak/cli/KeycloakMain.java
deleted file mode 100644
index 199a93d02c..0000000000
--- a/quarkus/runtime/src/main/java/org/keycloak/cli/KeycloakMain.java
+++ /dev/null
@@ -1,247 +0,0 @@
-/*
- * Copyright 2020 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.cli;
-
-import static org.keycloak.cli.MainCommand.BUILD_COMMAND;
-import static org.keycloak.cli.MainCommand.START_COMMAND;
-import static org.keycloak.cli.Picocli.createCommandLine;
-import static org.keycloak.cli.Picocli.error;
-import static org.keycloak.cli.Picocli.getCliArgs;
-import static org.keycloak.cli.Picocli.parseConfigArgs;
-import static org.keycloak.configuration.Configuration.getConfig;
-import static org.keycloak.configuration.PropertyMappers.isBuildTimeProperty;
-import static org.keycloak.util.Environment.getProfileOrDefault;
-import static org.keycloak.util.Environment.isDevMode;
-
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.InputStream;
-import java.io.PrintWriter;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-import java.util.Properties;
-
-import io.quarkus.runtime.Quarkus;
-import org.keycloak.common.Version;
-
-import io.quarkus.runtime.annotations.QuarkusMain;
-
-import org.keycloak.configuration.KeycloakConfigSourceProvider;
-import org.keycloak.util.Environment;
-
-import picocli.CommandLine;
-
-/**
- * The main entry point, responsible for initialize and run the CLI as well as start the server.
- *
- *
For optimal startup of the server, the server should be configured first by running the {@link MainCommand#reAugment(Boolean)}
- * command so that subsequent server starts, without any args, do not need to build the CLI, which is costly.
- */
-@QuarkusMain(name = "keycloak")
-public class KeycloakMain {
-
- public static void main(String[] args) {
- System.setProperty("kc.version", Version.VERSION_KEYCLOAK);
- List cliArgs = new ArrayList<>(Arrays.asList(args));
- System.setProperty(Environment.CLI_ARGS, parseConfigArgs(cliArgs));
-
- if (cliArgs.isEmpty()) {
- // no arguments, just start the server
- start(cliArgs, new PrintWriter(System.err));
- if (!isDevMode()) {
- System.exit(CommandLine.ExitCode.OK);
- }
- return;
- }
-
- // parse arguments and execute any of the configured commands
- parseAndRun(cliArgs);
- }
-
- static void start(CommandLine cmd) {
- start(getCliArgs(cmd), cmd.getErr());
- }
-
- private static void start(List cliArgs, PrintWriter errorWriter) {
- try {
- Quarkus.run(null, (integer, cause) -> {
- if (cause != null) {
- error(cliArgs, errorWriter,
- String.format("Failed to start server using profile (%s)", getProfileOrDefault("none")),
- cause.getCause());
- }
- });
- } catch (Throwable cause) {
- error(cliArgs, errorWriter,
- String.format("Unexpected error when starting the server using profile (%s)", getProfileOrDefault("none")),
- cause.getCause());
- }
-
- Quarkus.waitForExit();
- }
-
- private static void parseAndRun(List cliArgs) {
- CommandLine cmd = createCommandLine(cliArgs);
-
- try {
- CommandLine.ParseResult result = cmd.parseArgs(cliArgs.toArray(new String[0]));
-
- if (!result.hasSubcommand() && !result.isUsageHelpRequested() && !result.isVersionHelpRequested()) {
- // if no command was set, the start command becomes the default
- cliArgs.add(0, START_COMMAND);
- }
- } 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());
- }
-
- runReAugmentationIfNeeded(cliArgs, cmd);
-
- int exitCode = cmd.execute(cliArgs.toArray(new String[0]));
-
- if (!isDevMode()) {
- System.exit(exitCode);
- }
- }
-
- private static void runReAugmentationIfNeeded(List cliArgs, CommandLine cmd) {
- if (cliArgs.contains("--auto-build")) {
- if (requiresReAugmentation(cliArgs)) {
- runReAugmentation(cliArgs, cmd);
- }
-
- if (Boolean.getBoolean("kc.config.rebuild-and-exit")) {
- System.exit(cmd.getCommandSpec().exitCodeOnSuccess());
- }
- }
- }
-
- private static boolean requiresReAugmentation(List cliArgs) {
- if (hasConfigChanges()) {
- System.out.printf("Changes detected in configuration. Updating the server image.\n");
-
- List suggestedArgs = cliArgs.subList(1, cliArgs.size());
-
- suggestedArgs.removeAll(Arrays.asList("--verbose", "--help"));
-
- System.out.printf("For an optional runtime and bypass this step, please run the '" + BUILD_COMMAND + "' command prior to starting the server:\n\n\t%s config %s\n",
- Environment.getCommand(),
- String.join(" ", suggestedArgs) + "\n");
-
- return true;
- }
-
- return hasProviderChanges();
- }
-
- private static void runReAugmentation(List cliArgs, CommandLine cmd) {
- if (MainCommand.START_DEV_COMMAND.equals(cliArgs.get(0))) {
- String profile = Environment.getProfile();
-
- if (profile == null) {
- // force the server image to be set with the dev profile
- Environment.forceDevProfile();
- }
- }
-
- List configArgsList = new ArrayList<>(cliArgs);
-
- if (!configArgsList.get(0).startsWith("--")) {
- configArgsList.remove(0);
- }
-
- configArgsList.remove("--auto-build");
- configArgsList.add(0, BUILD_COMMAND);
-
- cmd.execute(configArgsList.toArray(new String[0]));
-
- System.out.printf("Next time you run the server, just run:\n\n\t%s\n\n", Environment.getCommand());
- }
-
- public static boolean hasProviderChanges() {
- File propertiesFile = KeycloakConfigSourceProvider.getPersistedConfigFile().toFile();
- File[] providerFiles = Environment.getProviderFiles();
-
- if (!propertiesFile.exists()) {
- return providerFiles.length > 0;
- }
-
- Properties properties = new Properties();
-
- try (InputStream is = new FileInputStream(propertiesFile)) {
- properties.load(is);
- } catch (Exception e) {
- throw new RuntimeException("Failed to load persisted properties", e);
- }
-
- for (String key : properties.stringPropertyNames()) {
- if (key.startsWith("kc.provider.file")) {
- if (providerFiles.length == 0) {
- return true;
- }
-
- String fileName = key.substring("kc.provider.file".length() + 1, key.lastIndexOf('.'));
- String lastModified = properties.getProperty(key);
-
- for (File file : providerFiles) {
- if (file.getName().equals(fileName) && !lastModified.equals(String.valueOf(file.lastModified()))) {
- return true;
- }
- }
-
- return false;
- }
- }
-
- return providerFiles.length > 0;
- }
-
- public static boolean hasConfigChanges() {
- for (String propertyName : getConfig().getPropertyNames()) {
- // only check keycloak build-time properties
- if (!isBuildTimeProperty(propertyName)) {
- continue;
- }
-
- // try to resolve any property set using profiles
- if (propertyName.startsWith("%")) {
- propertyName = propertyName.substring(propertyName.indexOf('.') + 1);
- }
-
- String currentValue = Environment.getBuiltTimeProperty(propertyName).orElse(null);
- String newValue = getConfig().getConfigValue(propertyName).getValue();
-
- if (newValue != null && !newValue.equalsIgnoreCase(currentValue)) {
- // changes to a single property are enough to indicate changes to configuration
- return true;
- }
- }
-
- return false;
- }
-
-}
diff --git a/quarkus/runtime/src/main/java/org/keycloak/cli/MainCommand.java b/quarkus/runtime/src/main/java/org/keycloak/cli/MainCommand.java
deleted file mode 100644
index 4a979febfa..0000000000
--- a/quarkus/runtime/src/main/java/org/keycloak/cli/MainCommand.java
+++ /dev/null
@@ -1,221 +0,0 @@
-/*
- * Copyright 2020 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.cli;
-
-import static org.keycloak.cli.Picocli.error;
-import static org.keycloak.cli.Picocli.println;
-import static org.keycloak.exportimport.ExportImportConfig.ACTION_EXPORT;
-import static org.keycloak.exportimport.ExportImportConfig.ACTION_IMPORT;
-import static org.keycloak.exportimport.Strategy.IGNORE_EXISTING;
-import static org.keycloak.exportimport.Strategy.OVERWRITE_EXISTING;
-
-import io.quarkus.bootstrap.runner.RunnerClassLoader;
-import org.keycloak.configuration.KeycloakConfigSourceProvider;
-
-import io.quarkus.bootstrap.runner.QuarkusEntryPoint;
-
-import org.keycloak.util.Environment;
-import picocli.CommandLine;
-import picocli.CommandLine.Command;
-import picocli.CommandLine.Model.CommandSpec;
-import picocli.CommandLine.Option;
-import picocli.CommandLine.Spec;
-
-@Command(name = "keycloak",
- usageHelpWidth = 150,
- header = "Keycloak - Open Source Identity and Access Management%n%nFind more information at: https://www.keycloak.org/%n",
- description = "Use this command-line tool to manage your Keycloak cluster%n", footerHeading = "%nUse \"${COMMAND-NAME} --help\" for more information about a command.%nUse \"${COMMAND-NAME} options\" for a list of all command-line options.",
- footer = "%nby Red Hat",
- optionListHeading = "Configuration Options%n%n",
- commandListHeading = "%nCommands%n%n",
- version = {
- "Keycloak ${sys:kc.version}",
- "JVM: ${java.version} (${java.vendor} ${java.vm.name} ${java.vm.version})",
- "OS: ${os.name} ${os.version} ${os.arch}"
-})
-public class MainCommand {
-
- static final String START_DEV_COMMAND = "start-dev";
- static final String START_COMMAND = "start";
- static final String BUILD_COMMAND = "build";
- public static final String AUTO_BUILD_OPTION = "--auto-build";
-
- public static boolean isStartDevCommand(CommandSpec commandSpec) {
- return START_DEV_COMMAND.equals(commandSpec.name());
- }
-
- @Spec
- CommandSpec spec;
-
- @Option(names = { "--help" }, description = "This help message.", usageHelp = true)
- boolean help;
-
- @Option(names = { "--version" }, description = "Show version information", versionHelp = true)
- boolean version;
-
- @Option(names = "--profile", arity = "1", description = "Set the profile. Use 'dev' profile to enable development mode.", scope = CommandLine.ScopeType.INHERIT)
- public void setProfile(String profile) {
- System.setProperty("kc.profile", profile);
- System.setProperty("quarkus.profile", profile);
- }
-
- @Option(names = "--config-file", arity = "1", description = "Set the path to a configuration file.", paramLabel = "", scope = CommandLine.ScopeType.INHERIT)
- public void setConfigFile(String path) {
- System.setProperty(KeycloakConfigSourceProvider.KEYCLOAK_CONFIG_FILE_PROP, path);
- }
-
- @Command(name = BUILD_COMMAND,
- description = "%nCreates a new and optimized server image based on the options passed to this command. Once created, configuration will be read from the server image and the server can be started without passing the same options again. Some configuration options require this command to be executed in order to actually change a configuration. For instance, the database vendor.%n",
- mixinStandardHelpOptions = true,
- usageHelpAutoWidth = true,
- optionListHeading = "%nOptions%n",
- parameterListHeading = "Available Commands%n")
- 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");
- println(spec.commandLine(), "Updating the configuration and installing your custom providers, if any. Please wait.");
-
- try {
- beforeReaugmentationOnWindows();
- QuarkusEntryPoint.main();
- println(spec.commandLine(), "Server configuration updated and persisted. Run the following command to review the configuration:\n");
- println(spec.commandLine(), "\t" + Environment.getCommand() + " show-config\n");
- } catch (Throwable throwable) {
- error(spec.commandLine(), "Failed to update server configuration.", throwable);
- }
- }
-
- private void beforeReaugmentationOnWindows() throws Exception {
- // On Windows, files generated during re-augmentation are locked and can't be re-created.
- // To workaround this behavior, we reset the internal cache of the runner classloader and force files
- // to be closed prior to re-augmenting the application
- // See KEYCLOAK-16218
- if (Environment.isWindows()) {
- ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
-
- if (classLoader instanceof RunnerClassLoader) {
- RunnerClassLoader.class.cast(classLoader).resetInternalCaches();
- }
- }
- }
-
- @Command(name = START_DEV_COMMAND,
- description = "%nStart the server in development mode.%n",
- mixinStandardHelpOptions = true,
- optionListHeading = "%nOptions%n",
- parameterListHeading = "Available Commands%n")
- public void startDev(@Option(names = "--verbose", description = "Print out more details when running this command.", required = false) Boolean verbose,
- @Option(names = AUTO_BUILD_OPTION, description = "Automatically detects whether the server configuration changed and a new server image must be built prior to starting the server. This option provides an alternative to manually running the '" + BUILD_COMMAND + "' prior to starting the server. Use this configuration carefully in production as it might impact the startup time.", required = false) Boolean autoConfig) {
- Environment.forceDevProfile();
- CommandLine cmd = spec.commandLine();
-
- cmd.getOut().printf("Running the server in dev mode. DO NOT run the '%s' command in production.\n", START_DEV_COMMAND);
-
- KeycloakMain.start(cmd);
- }
-
- @Command(name = "export",
- description = "%nExport data from realms to a file or directory.%n",
- mixinStandardHelpOptions = true,
- showDefaultValues = true,
- optionListHeading = "%nOptions%n",
- parameterListHeading = "Available Commands%n")
- public void runExport(@Option(names = "--dir", arity = "1", description = "Set the path to a directory where files will be created with the exported data.", paramLabel = "") String toDir,
- @Option(names = "--file", arity = "1", description = "Set the path to a file that will be created with the exported data.", paramLabel = "") String toFile,
- @Option(names = "--realm", arity = "1", description = "Set the name of the realm to export", paramLabel = "") 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 = "", defaultValue = "different_files") String users,
- @Option(names = "--users-per-file", arity = "1", description = "Set the number of users per file. It’s used only if --users=different_files.", paramLabel = "", defaultValue = "50") Integer usersPerFile,
- @Option(names = "--verbose", description = "Print out more details when running this command.", required = false) Boolean verbose) {
- System.setProperty("keycloak.migration.usersExportStrategy", users.toUpperCase());
-
- if (usersPerFile != null) {
- System.setProperty("keycloak.migration.usersPerFile", usersPerFile.toString());
- }
-
- runImportExport(ACTION_EXPORT, toDir, toFile, realm, verbose);
- }
-
- @Command(name = "import",
- description = "%nImport data from a directory or a file.%n",
- mixinStandardHelpOptions = true,
- showDefaultValues = true,
- optionListHeading = "%nOptions%n",
- parameterListHeading = "Available Commands%n")
- public void runImport(@Option(names = "--dir", arity = "1", description = "Set the path to a directory containing the files with the data to import", paramLabel = "") String toDir,
- @Option(names = "--file", arity = "1", description = "Set the path to a file with the data to import.", paramLabel = "") String toFile,
- @Option(names = "--realm", arity = "1", description = "Set the name of the realm to import", paramLabel = "") 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 = "--verbose", description = "Print out more details when running this command.", required = false) Boolean verbose) {
- System.setProperty("keycloak.migration.strategy", override ? OVERWRITE_EXISTING.name() : IGNORE_EXISTING.name());
-
- runImportExport(ACTION_IMPORT, toDir, toFile, realm, verbose);
- }
-
- @Command(name = START_COMMAND,
- description = "%nStart the server.%n",
- mixinStandardHelpOptions = true,
- usageHelpAutoWidth = true,
- optionListHeading = "%nOptions%n",
- parameterListHeading = "Available Commands%n")
- public void start(
- @Option(names = "--show-config", arity = "0..1",
- description = "Print out the configuration options when starting the server.",
- fallbackValue = "show-config") String showConfig,
- @Option(names = "--verbose", description = "Print out more details when running this command.", required = false) Boolean verbose,
- @Option(names = AUTO_BUILD_OPTION, description = "Automatically detects whether the server configuration changed and a new server image must be built prior to starting the server. This option provides an alternative to manually running the '" + BUILD_COMMAND + "' prior to starting the server. Use this configuration carefully in production as it might impact the startup time.", required = false) Boolean autoConfig) {
- if ("show-config".equals(showConfig)) {
- System.setProperty("kc.show.config.runtime", Boolean.TRUE.toString());
- System.setProperty("kc.show.config", "all");
- } else if (showConfig != null) {
- throw new CommandLine.UnmatchedArgumentException(spec.commandLine(), "Invalid argument: " + showConfig);
- }
- KeycloakMain.start(spec.commandLine());
- }
-
- @Command(name = "show-config",
- description = "Print out the current configuration.",
- mixinStandardHelpOptions = true,
- optionListHeading = "%nOptions%n",
- parameterListHeading = "Available Commands%n")
- public void showConfiguration(
- @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);
- ShowConfigCommand.run();
- }
-
- private void runImportExport(String action, String toDir, String toFile, String realm, Boolean verbose) {
- System.setProperty("keycloak.migration.action", action);
-
- if (toDir != null) {
- System.setProperty("keycloak.migration.provider", "dir");
- System.setProperty("keycloak.migration.dir", toDir);
- } else if (toFile != null) {
- System.setProperty("keycloak.migration.provider", "singleFile");
- System.setProperty("keycloak.migration.file", toFile);
- } else {
- error(spec.commandLine(), "Must specify either --dir or --file options.");
- }
-
- if (realm != null) {
- System.setProperty("keycloak.migration.realmName", realm);
- }
-
- setProfile(Environment.IMPORT_EXPORT_MODE);
- start(null, verbose, false);
- }
-}
diff --git a/quarkus/runtime/src/main/java/org/keycloak/cli/Picocli.java b/quarkus/runtime/src/main/java/org/keycloak/cli/Picocli.java
index 15f9070303..ee8e795fe9 100644
--- a/quarkus/runtime/src/main/java/org/keycloak/cli/Picocli.java
+++ b/quarkus/runtime/src/main/java/org/keycloak/cli/Picocli.java
@@ -17,20 +17,33 @@
package org.keycloak.cli;
-import static org.keycloak.cli.MainCommand.BUILD_COMMAND;
-import static org.keycloak.cli.MainCommand.START_COMMAND;
-import static org.keycloak.cli.MainCommand.START_DEV_COMMAND;
+import static org.keycloak.configuration.Configuration.getConfig;
+import static org.keycloak.configuration.PropertyMappers.isBuildTimeProperty;
+import static org.keycloak.util.Environment.isDevMode;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.InputStream;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import java.util.Set;
import java.util.function.IntFunction;
+import java.util.stream.Collectors;
import org.jboss.logging.Logger;
+import org.keycloak.cli.command.AbstractStartCommand;
+import org.keycloak.cli.command.Build;
+import org.keycloak.cli.command.Main;
+import org.keycloak.cli.command.Start;
+import org.keycloak.cli.command.StartDev;
import org.keycloak.common.Profile;
+import org.keycloak.configuration.KeycloakConfigSourceProvider;
import org.keycloak.configuration.PropertyMapper;
import org.keycloak.configuration.PropertyMappers;
import org.keycloak.platform.Platform;
@@ -39,43 +52,197 @@ import org.keycloak.provider.quarkus.QuarkusPlatform;
import org.keycloak.util.Environment;
import picocli.CommandLine;
-final class Picocli {
+public final class Picocli {
private static final Logger logger = Logger.getLogger(Picocli.class);
private static final String ARG_SEPARATOR = ";;";
private static final String ARG_PREFIX = "--";
- static CommandLine createCommandLine(List cliArgs) {
- CommandLine.Model.CommandSpec spec = CommandLine.Model.CommandSpec.forAnnotatedObject(new MainCommand())
+ private Picocli() {
+ }
+
+ public static void parseAndRun(List cliArgs) {
+ CommandLine cmd = createCommandLine(cliArgs);
+
+ try {
+ CommandLine.ParseResult result = cmd.parseArgs(cliArgs.toArray(new String[0]));
+
+ if (!result.hasSubcommand() && !result.isUsageHelpRequested() && !result.isVersionHelpRequested()) {
+ // if no command was set, the start command becomes the default
+ cliArgs.add(0, Start.NAME);
+ }
+ } 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());
+ }
+
+ runReAugmentationIfNeeded(cliArgs, cmd);
+
+ int exitCode = cmd.execute(cliArgs.toArray(new String[0]));
+
+ if (isDevMode()) {
+ // do not exit if running in dev mode, otherwise quarkus dev mode will exit when running from IDE
+ return;
+ }
+
+ System.exit(exitCode);
+ }
+
+ private static void runReAugmentationIfNeeded(List cliArgs, CommandLine cmd) {
+ if (cliArgs.contains(AbstractStartCommand.AUTO_BUILD_OPTION)) {
+ if (requiresReAugmentation(cliArgs, cmd)) {
+ runReAugmentation(cliArgs, cmd);
+ }
+
+ if (Boolean.getBoolean("kc.config.rebuild-and-exit")) {
+ System.exit(cmd.getCommandSpec().exitCodeOnSuccess());
+ }
+ }
+ }
+
+ private static boolean requiresReAugmentation(List cliArgs, CommandLine cmd) {
+ if (hasConfigChanges()) {
+ cmd.getOut().println("Changes detected in configuration. Updating the server image.");
+
+ List suggestedArgs = cliArgs.subList(1, cliArgs.size());
+
+ suggestedArgs.removeAll(Arrays.asList("--verbose", "--help"));
+
+ cmd.getOut().printf("For an optional runtime and bypass this step, please run the '%s' command prior to starting the server:%n%n\t%s config %s%n",
+ Build.NAME,
+ Environment.getCommand(),
+ String.join(" ", suggestedArgs) + "\n");
+
+ return true;
+ }
+
+ return hasProviderChanges();
+ }
+
+ private static void runReAugmentation(List cliArgs, CommandLine cmd) {
+ if (StartDev.NAME.equals(cliArgs.get(0))) {
+ String profile = Environment.getProfile();
+
+ if (profile == null) {
+ // force the server image to be set with the dev profile
+ Environment.forceDevProfile();
+ }
+ }
+
+ List configArgsList = new ArrayList<>(cliArgs);
+
+ if (!configArgsList.get(0).startsWith("--")) {
+ configArgsList.remove(0);
+ }
+
+ configArgsList.remove("--auto-build");
+ configArgsList.add(0, Build.NAME);
+
+ cmd.execute(configArgsList.toArray(new String[0]));
+
+ cmd.getOut().printf("Next time you run the server, just run:%n%n\t%s%n%n", Environment.getCommand());
+ }
+
+ private static boolean hasProviderChanges() {
+ File propertiesFile = KeycloakConfigSourceProvider.getPersistedConfigFile().toFile();
+ Map deployedProviders = Environment.getProviderFiles();
+
+ if (!propertiesFile.exists()) {
+ return !deployedProviders.isEmpty();
+ }
+
+ Properties properties = new Properties();
+
+ try (InputStream is = new FileInputStream(propertiesFile)) {
+ properties.load(is);
+ } catch (Exception e) {
+ throw new RuntimeException("Failed to load persisted properties", e);
+ }
+
+ Set providerKeys = properties.stringPropertyNames().stream().filter(Picocli::isProviderKey).collect(
+ Collectors.toSet());
+
+ if (deployedProviders.size() != providerKeys.size()) {
+ return true;
+ }
+
+ for (String key : providerKeys) {
+ String fileName = key.substring("kc.provider.file".length() + 1, key.lastIndexOf('.'));
+
+ if (!deployedProviders.containsKey(fileName)) {
+ return true;
+ }
+
+ File file = deployedProviders.get(fileName);
+ String lastModified = properties.getProperty(key);
+
+ if (!lastModified.equals(String.valueOf(file.lastModified()))) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private static boolean hasConfigChanges() {
+ for (String propertyName : getConfig().getPropertyNames()) {
+ // only check keycloak build-time properties
+ if (!isBuildTimeProperty(propertyName)) {
+ continue;
+ }
+
+ // try to resolve any property set using profiles
+ if (propertyName.startsWith("%")) {
+ propertyName = propertyName.substring(propertyName.indexOf('.') + 1);
+ }
+
+ String currentValue = Environment.getBuiltTimeProperty(propertyName).orElse(null);
+ String newValue = getConfig().getConfigValue(propertyName).getValue();
+
+ if (newValue != null && !newValue.equalsIgnoreCase(currentValue)) {
+ // changes to a single property are enough to indicate changes to configuration
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private static boolean isProviderKey(String key) {
+ return key.startsWith("kc.provider.file");
+ }
+
+ private static CommandLine createCommandLine(List cliArgs) {
+ CommandLine.Model.CommandSpec spec = CommandLine.Model.CommandSpec.forAnnotatedObject(new Main())
.name(Environment.getCommand());
- boolean addBuildOptionsToStartCommand = cliArgs.contains(MainCommand.AUTO_BUILD_OPTION);
+ boolean addBuildOptionsToStartCommand = cliArgs.contains(AbstractStartCommand.AUTO_BUILD_OPTION);
- addOption(spec, START_COMMAND, addBuildOptionsToStartCommand);
- addOption(spec, START_DEV_COMMAND, true);
- addOption(spec, BUILD_COMMAND, true);
+ addOption(spec, Start.NAME, addBuildOptionsToStartCommand);
+ addOption(spec, StartDev.NAME, true);
+ addOption(spec, Build.NAME, true);
for (Profile.Feature feature : Profile.Feature.values()) {
- addOption(spec.subcommands().get(BUILD_COMMAND).getCommandSpec(), "--features-" + feature.name().toLowerCase(),
+ addOption(spec.subcommands().get(Build.NAME).getCommandSpec(), "--features-" + feature.name().toLowerCase(),
"Enables the " + feature.name() + " feature. Set enabled to enable the feature or disabled otherwise.");
}
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();
- }
- });
+ cmd.setExecutionExceptionHandler(new ExecutionExceptionHandler());
return cmd;
}
- static String parseConfigArgs(List argsList) {
+ public static String parseConfigArgs(List argsList) {
StringBuilder options = new StringBuilder();
Iterator iterator = argsList.iterator();
@@ -130,7 +297,7 @@ final class Picocli {
.type(String.class).build());
}
- static List getCliArgs(CommandLine cmd) {
+ public static List getCliArgs(CommandLine cmd) {
CommandLine.ParseResult parseResult = cmd.getParseResult();
if (parseResult == null) {
@@ -140,7 +307,7 @@ final class Picocli {
return parseResult.expandedArgs();
}
- static void error(List cliArgs, PrintWriter errorWriter, String message, Throwable throwable) {
+ public static void error(List cliArgs, PrintWriter errorWriter, String message, Throwable throwable) {
logError(errorWriter, "ERROR: " + message);
if (throwable != null) {
@@ -173,15 +340,15 @@ final class Picocli {
System.exit(1);
}
- static void error(CommandLine cmd, String message, Throwable throwable) {
+ public static void error(CommandLine cmd, String message, Throwable throwable) {
error(getCliArgs(cmd), cmd.getErr(), message, throwable);
}
- static void error(CommandLine cmd, String message) {
+ public static void error(CommandLine cmd, String message) {
error(getCliArgs(cmd), cmd.getErr(), message, null);
}
- static void println(CommandLine cmd, String message) {
+ public static void println(CommandLine cmd, String message) {
cmd.getOut().println(message);
}
diff --git a/quarkus/runtime/src/main/java/org/keycloak/cli/command/AbstractCommand.java b/quarkus/runtime/src/main/java/org/keycloak/cli/command/AbstractCommand.java
new file mode 100644
index 0000000000..675edf8fb7
--- /dev/null
+++ b/quarkus/runtime/src/main/java/org/keycloak/cli/command/AbstractCommand.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2020 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.cli.command;
+
+import org.keycloak.util.Environment;
+
+import picocli.CommandLine;
+
+public abstract class AbstractCommand {
+
+ @CommandLine.Spec
+ protected CommandLine.Model.CommandSpec spec;
+
+ @CommandLine.Option(names = "--profile", arity = "1", description = "Set the profile. Use 'dev' profile to enable development mode.", scope = CommandLine.ScopeType.INHERIT)
+ public void setProfile(String profile) {
+ Environment.setProfile(profile);
+ }
+}
diff --git a/quarkus/runtime/src/main/java/org/keycloak/cli/command/AbstractExportImportCommand.java b/quarkus/runtime/src/main/java/org/keycloak/cli/command/AbstractExportImportCommand.java
new file mode 100644
index 0000000000..8790ad844a
--- /dev/null
+++ b/quarkus/runtime/src/main/java/org/keycloak/cli/command/AbstractExportImportCommand.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2020 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.cli.command;
+
+import static org.keycloak.cli.Picocli.error;
+
+import org.keycloak.util.Environment;
+
+import picocli.CommandLine;
+
+public abstract class AbstractExportImportCommand extends AbstractCommand implements Runnable {
+
+ private final String action;
+
+ @CommandLine.Option(names = "--dir", arity = "1", description = "Set the path to a directory where files will be created with the exported data.", paramLabel = "") String toDir;
+ @CommandLine.Option(names = "--file", arity = "1", description = "Set the path to a file that will be created with the exported data.", paramLabel = "") String toFile;
+ @CommandLine.Option(names = "--realm", arity = "1", description = "Set the name of the realm to export", paramLabel = "") String realm;
+
+ protected AbstractExportImportCommand(String action) {
+ this.action = action;
+ }
+
+ @Override
+ public void run() {
+ doBeforeRun();
+ System.setProperty("keycloak.migration.action", action);
+
+ if (toDir != null) {
+ System.setProperty("keycloak.migration.provider", "dir");
+ System.setProperty("keycloak.migration.dir", toDir);
+ } else if (toFile != null) {
+ System.setProperty("keycloak.migration.provider", "singleFile");
+ System.setProperty("keycloak.migration.file", toFile);
+ } else {
+ error(spec.commandLine(), "Must specify either --dir or --file options.");
+ }
+
+ if (realm != null) {
+ System.setProperty("keycloak.migration.realmName", realm);
+ }
+
+ Environment.setProfile(Environment.IMPORT_EXPORT_MODE);
+
+ new CommandLine(new Main()).execute("start");
+ }
+
+ protected void doBeforeRun() {
+
+ }
+}
diff --git a/quarkus/runtime/src/main/java/org/keycloak/cli/command/AbstractStartCommand.java b/quarkus/runtime/src/main/java/org/keycloak/cli/command/AbstractStartCommand.java
new file mode 100644
index 0000000000..2901c7b4e6
--- /dev/null
+++ b/quarkus/runtime/src/main/java/org/keycloak/cli/command/AbstractStartCommand.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2020 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.cli.command;
+
+import org.keycloak.KeycloakMain;
+
+import picocli.CommandLine;
+
+public abstract class AbstractStartCommand extends AbstractCommand implements Runnable {
+
+ public static final String AUTO_BUILD_OPTION = "--auto-build";
+
+ @CommandLine.Option(names = AUTO_BUILD_OPTION, description = "Automatically detects whether the server configuration changed and a new server image must be built prior to starting the server. This option provides an alternative to manually running the '" + Build.NAME + "' prior to starting the server. Use this configuration carefully in production as it might impact the startup time.", required = false)
+ Boolean autoConfig;
+
+ @Override
+ public void run() {
+ doBeforeRun();
+ CommandLine cmd = spec.commandLine();
+ KeycloakMain.start(cmd.getParseResult().expandedArgs(), cmd.getErr());
+ }
+
+ protected void doBeforeRun() {
+
+ }
+}
diff --git a/quarkus/runtime/src/main/java/org/keycloak/cli/command/Build.java b/quarkus/runtime/src/main/java/org/keycloak/cli/command/Build.java
new file mode 100644
index 0000000000..b10b7e9f6d
--- /dev/null
+++ b/quarkus/runtime/src/main/java/org/keycloak/cli/command/Build.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2020 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.cli.command;
+
+import static org.keycloak.cli.Picocli.error;
+import static org.keycloak.cli.Picocli.println;
+
+import org.keycloak.util.Environment;
+
+import io.quarkus.bootstrap.runner.QuarkusEntryPoint;
+import io.quarkus.bootstrap.runner.RunnerClassLoader;
+import picocli.CommandLine;
+
+@CommandLine.Command(name = Build.NAME,
+ description = "%nCreates a new and optimized server image based on the options passed to this command. Once created, configuration will be read from the server image and the server can be started without passing the same options again. Some configuration options require this command to be executed in order to actually change a configuration. For instance, the database vendor.%n",
+ mixinStandardHelpOptions = true,
+ usageHelpAutoWidth = true,
+ optionListHeading = "%nOptions%n",
+ parameterListHeading = "Available Commands%n")
+public final class Build extends AbstractCommand implements Runnable {
+
+ public static final String NAME = "build";
+
+ @Override
+ public void run() {
+ System.setProperty("quarkus.launch.rebuild", "true");
+ println(spec.commandLine(), "Updating the configuration and installing your custom providers, if any. Please wait.");
+
+ try {
+ beforeReaugmentationOnWindows();
+ QuarkusEntryPoint.main();
+ println(spec.commandLine(), "Server configuration updated and persisted. Run the following command to review the configuration:\n");
+ println(spec.commandLine(), "\t" + Environment.getCommand() + " show-config\n");
+ } catch (Throwable throwable) {
+ error(spec.commandLine(), "Failed to update server configuration.", throwable);
+ }
+ }
+
+ private void beforeReaugmentationOnWindows() {
+ // On Windows, files generated during re-augmentation are locked and can't be re-created.
+ // To workaround this behavior, we reset the internal cache of the runner classloader and force files
+ // to be closed prior to re-augmenting the application
+ // See KEYCLOAK-16218
+ if (Environment.isWindows()) {
+ ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
+
+ if (classLoader instanceof RunnerClassLoader) {
+ RunnerClassLoader.class.cast(classLoader).resetInternalCaches();
+ }
+ }
+ }
+}
diff --git a/quarkus/runtime/src/main/java/org/keycloak/cli/command/Export.java b/quarkus/runtime/src/main/java/org/keycloak/cli/command/Export.java
new file mode 100644
index 0000000000..fa04b6928a
--- /dev/null
+++ b/quarkus/runtime/src/main/java/org/keycloak/cli/command/Export.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2020 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.cli.command;
+
+import static org.keycloak.exportimport.ExportImportConfig.ACTION_EXPORT;
+
+import picocli.CommandLine;
+
+@CommandLine.Command(name = "export",
+ description = "%nExport data from realms to a file or directory.%n",
+ mixinStandardHelpOptions = true,
+ showDefaultValues = true,
+ optionListHeading = "%nOptions%n",
+ parameterListHeading = "Available Commands%n")
+public final class Export extends AbstractExportImportCommand implements Runnable {
+
+ @CommandLine.Option(names = "--users", arity = "1", description = "Set how users should be exported. Possible values are: skip, realm_file, same_file, different_files.", paramLabel = "", defaultValue = "different_files") String users;
+ @CommandLine.Option(names = "--users-per-file", arity = "1", description = "Set the number of users per file. It’s used only if --users=different_files.", paramLabel = "", defaultValue = "50") Integer usersPerFile;
+
+ public Export() {
+ super(ACTION_EXPORT);
+ }
+
+ @Override
+ protected void doBeforeRun() {
+ System.setProperty("keycloak.migration.usersExportStrategy", users.toUpperCase());
+
+ if (usersPerFile != null) {
+ System.setProperty("keycloak.migration.usersPerFile", usersPerFile.toString());
+ }
+ }
+}
diff --git a/quarkus/runtime/src/main/java/org/keycloak/cli/command/Import.java b/quarkus/runtime/src/main/java/org/keycloak/cli/command/Import.java
new file mode 100644
index 0000000000..43699c09b8
--- /dev/null
+++ b/quarkus/runtime/src/main/java/org/keycloak/cli/command/Import.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2020 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.cli.command;
+
+import static org.keycloak.exportimport.ExportImportConfig.ACTION_IMPORT;
+import static org.keycloak.exportimport.Strategy.IGNORE_EXISTING;
+import static org.keycloak.exportimport.Strategy.OVERWRITE_EXISTING;
+
+import picocli.CommandLine;
+
+@CommandLine.Command(name = "import",
+ description = "%nImport data from a directory or a file.%n",
+ mixinStandardHelpOptions = true,
+ showDefaultValues = true,
+ optionListHeading = "%nOptions%n",
+ parameterListHeading = "Available Commands%n")
+public final class Import extends AbstractExportImportCommand implements Runnable {
+
+ @CommandLine.Option(names = "--override", arity = "1", description = "Set if existing data should be skipped or overridden.", paramLabel = "false", defaultValue = "true") boolean override;
+
+ public Import() {
+ super(ACTION_IMPORT);
+ }
+
+ @Override
+ protected void doBeforeRun() {
+ System.setProperty("keycloak.migration.strategy", override ? OVERWRITE_EXISTING.name() : IGNORE_EXISTING.name());
+ }
+}
diff --git a/quarkus/runtime/src/main/java/org/keycloak/cli/command/Main.java b/quarkus/runtime/src/main/java/org/keycloak/cli/command/Main.java
new file mode 100644
index 0000000000..87f6ed26bb
--- /dev/null
+++ b/quarkus/runtime/src/main/java/org/keycloak/cli/command/Main.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2020 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.cli.command;
+
+import org.keycloak.configuration.KeycloakConfigSourceProvider;
+
+import picocli.CommandLine;
+import picocli.CommandLine.Command;
+import picocli.CommandLine.Option;
+
+@Command(name = "keycloak",
+ usageHelpWidth = 150,
+ header = "Keycloak - Open Source Identity and Access Management%n%nFind more information at: https://www.keycloak.org/%n",
+ description = "Use this command-line tool to manage your Keycloak cluster%n", footerHeading = "%nUse \"${COMMAND-NAME} --help\" for more information about a command.%nUse \"${COMMAND-NAME} options\" for a list of all command-line options.",
+ footer = "%nby Red Hat",
+ optionListHeading = "Configuration Options%n%n",
+ commandListHeading = "%nCommands%n%n",
+ version = {
+ "Keycloak ${sys:kc.version}",
+ "JVM: ${java.version} (${java.vendor} ${java.vm.name} ${java.vm.version})",
+ "OS: ${os.name} ${os.version} ${os.arch}"
+ },
+ subcommands = {
+ Build.class,
+ Start.class,
+ StartDev.class,
+ Export.class,
+ Import.class,
+ ShowConfig.class
+ }
+)
+public final class Main {
+
+ @Option(names = { "--help" }, description = "This help message.", usageHelp = true)
+ boolean help;
+
+ @Option(names = { "--version" }, description = "Show version information", versionHelp = true)
+ boolean version;
+
+ @CommandLine.Option(names = "--verbose", description = "Print out more details when running this command. Useful for troubleshooting if some unexpected error occurs.", required = false,
+ scope = CommandLine.ScopeType.INHERIT) Boolean verbose;
+
+ @Option(names = "--config-file", arity = "1", description = "Set the path to a configuration file.", paramLabel = "")
+ public void setConfigFile(String path) {
+ System.setProperty(KeycloakConfigSourceProvider.KEYCLOAK_CONFIG_FILE_PROP, path);
+ }
+}
diff --git a/quarkus/runtime/src/main/java/org/keycloak/cli/ShowConfigCommand.java b/quarkus/runtime/src/main/java/org/keycloak/cli/command/ShowConfig.java
similarity index 57%
rename from quarkus/runtime/src/main/java/org/keycloak/cli/ShowConfigCommand.java
rename to quarkus/runtime/src/main/java/org/keycloak/cli/command/ShowConfig.java
index ba031144ad..4f1f87ba60 100644
--- a/quarkus/runtime/src/main/java/org/keycloak/cli/ShowConfigCommand.java
+++ b/quarkus/runtime/src/main/java/org/keycloak/cli/command/ShowConfig.java
@@ -15,7 +15,7 @@
* limitations under the License.
*/
-package org.keycloak.cli;
+package org.keycloak.cli.command;
import static java.lang.Boolean.parseBoolean;
import static org.keycloak.configuration.Configuration.getConfigValue;
@@ -31,60 +31,40 @@ import java.util.function.BiConsumer;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
-
import org.keycloak.configuration.MicroProfileConfigProvider;
import org.keycloak.configuration.PersistedConfigSource;
import org.keycloak.configuration.PropertyMappers;
import org.keycloak.util.Environment;
import io.smallrye.config.ConfigValue;
+import picocli.CommandLine;
-public final class ShowConfigCommand {
+@CommandLine.Command(name = "show-config",
+ description = "Print out the current configuration.",
+ mixinStandardHelpOptions = true,
+ optionListHeading = "%nOptions%n",
+ parameterListHeading = "Available Commands%n")
+public final class ShowConfig extends AbstractCommand implements Runnable {
- public static void run() {
+ @CommandLine.Parameters(paramLabel = "filter", defaultValue = "none", description = "Show all configuration options. Use 'all' to show all options.") String filter;
+
+ @Override
+ public void run() {
+ System.setProperty("kc.show.config", filter);
String configArgs = System.getProperty("kc.show.config");
if (configArgs != null) {
Map> properties = getPropertiesByGroup();
- Set uniqueNames = new HashSet<>();
String profile = getProfile();
- System.out.printf("Current Profile: %s%n", profile == null ? "none" : profile);
-
- System.out.println("Runtime Configuration:");
- properties.get(MicroProfileConfigProvider.NS_KEYCLOAK).stream().sorted()
- .filter(name -> {
- String canonicalFormat = canonicalFormat(name);
-
- if (!canonicalFormat.equals(name)) {
- return uniqueNames.add(canonicalFormat);
- }
- return uniqueNames.add(name);
- })
- .forEachOrdered(ShowConfigCommand::printProperty);
+ printRunTimeConfig(properties, profile);
if (configArgs.equalsIgnoreCase("all")) {
- Set profiles = properties.get("%");
-
- if (profiles != null) {
- profiles.stream()
- .sorted()
- .collect(Collectors.groupingBy(s -> s.substring(1, s.indexOf('.'))))
- .forEach((p, properties1) -> {
- if (p.equals(profile)) {
- System.out.printf("Profile \"%s\" Configuration (%s):%n", p,
- p.equals(profile) ? "current" : "");
- } else {
- System.out.printf("Profile \"%s\" Configuration:%n", p);
- }
+ printAllProfilesConfig(properties, profile);
- properties1.stream().sorted().forEachOrdered(ShowConfigCommand::printProperty);
- });
- }
-
- System.out.println("Quarkus Configuration:");
+ spec.commandLine().getOut().println("Quarkus Configuration:");
properties.get(MicroProfileConfigProvider.NS_QUARKUS).stream().sorted()
- .forEachOrdered(ShowConfigCommand::printProperty);
+ .forEachOrdered(this::printProperty);
}
if (!parseBoolean(System.getProperty("kc.show.config.runtime", Boolean.FALSE.toString()))) {
@@ -93,6 +73,44 @@ public final class ShowConfigCommand {
}
}
+ private void printRunTimeConfig(Map> properties, String profile) {
+ Set uniqueNames = new HashSet<>();
+
+ spec.commandLine().getOut().printf("Current Profile: %s%n", profile == null ? "none" : profile);
+
+ spec.commandLine().getOut().println("Runtime Configuration:");
+ properties.get(MicroProfileConfigProvider.NS_KEYCLOAK).stream().sorted()
+ .filter(name -> {
+ String canonicalFormat = canonicalFormat(name);
+
+ if (!canonicalFormat.equals(name)) {
+ return uniqueNames.add(canonicalFormat);
+ }
+ return uniqueNames.add(name);
+ })
+ .forEachOrdered(this::printProperty);
+ }
+
+ private void printAllProfilesConfig(Map> properties, String profile) {
+ Set profiles = properties.get("%");
+
+ if (profiles != null) {
+ profiles.stream()
+ .sorted()
+ .collect(Collectors.groupingBy(s -> s.substring(1, s.indexOf('.'))))
+ .forEach((p, properties1) -> {
+ if (p.equals(profile)) {
+ spec.commandLine().getOut().printf("Profile \"%s\" Configuration (%s):%n", p,
+ p.equals(profile) ? "current" : "");
+ } else {
+ spec.commandLine().getOut().printf("Profile \"%s\" Configuration:%n", p);
+ }
+
+ properties1.stream().sorted().forEachOrdered(this::printProperty);
+ });
+ }
+ }
+
private static String getProfile() {
String profile = Environment.getProfile();
@@ -106,8 +124,8 @@ public final class ShowConfigCommand {
private static Map> getPropertiesByGroup() {
Map> properties = StreamSupport
.stream(getPropertyNames().spliterator(), false)
- .filter(ShowConfigCommand::filterByGroup)
- .collect(Collectors.groupingBy(ShowConfigCommand::groupProperties, Collectors.toSet()));
+ .filter(ShowConfig::filterByGroup)
+ .collect(Collectors.groupingBy(ShowConfig::groupProperties, Collectors.toSet()));
StreamSupport.stream(getPropertyNames().spliterator(), false)
.filter(new Predicate() {
@@ -122,8 +140,8 @@ public final class ShowConfigCommand {
return PersistedConfigSource.NAME.equals(configValue.getConfigSourceName());
}
})
- .filter(ShowConfigCommand::filterByGroup)
- .collect(Collectors.groupingBy(ShowConfigCommand::groupProperties, Collectors.toSet()))
+ .filter(ShowConfig::filterByGroup)
+ .collect(Collectors.groupingBy(ShowConfig::groupProperties, Collectors.toSet()))
.forEach(new BiConsumer>() {
@Override
public void accept(String group, Set propertyNames) {
@@ -134,20 +152,20 @@ public final class ShowConfigCommand {
return properties;
}
- private static void printProperty(String property) {
+ private void printProperty(String property) {
String canonicalFormat = PropertyMappers.canonicalFormat(property);
ConfigValue configValue = getConfigValue(canonicalFormat);
if (configValue.getValue() == null) {
configValue = getConfigValue(property);
}
-
-
+
+
if (configValue.getValue() == null) {
return;
}
- System.out.printf("\t%s = %s (%s)%n", configValue.getName(), formatValue(configValue.getName(), configValue.getValue()), configValue.getConfigSourceName());
+ spec.commandLine().getOut().printf("\t%s = %s (%s)%n", configValue.getName(), formatValue(configValue.getName(), configValue.getValue()), configValue.getConfigSourceName());
}
private static String groupProperties(String property) {
diff --git a/quarkus/runtime/src/main/java/org/keycloak/cli/command/Start.java b/quarkus/runtime/src/main/java/org/keycloak/cli/command/Start.java
new file mode 100644
index 0000000000..cf5196f81d
--- /dev/null
+++ b/quarkus/runtime/src/main/java/org/keycloak/cli/command/Start.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2020 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.cli.command;
+
+import picocli.CommandLine;
+
+@CommandLine.Command(name = Start.NAME,
+ description = "%nStart the server.%n",
+ mixinStandardHelpOptions = true,
+ usageHelpAutoWidth = true,
+ optionListHeading = "%nOptions%n",
+ parameterListHeading = "Available Commands%n")
+public final class Start extends AbstractStartCommand implements Runnable {
+
+ public static final String NAME = "start";
+}
diff --git a/quarkus/runtime/src/main/java/org/keycloak/cli/command/StartDev.java b/quarkus/runtime/src/main/java/org/keycloak/cli/command/StartDev.java
new file mode 100644
index 0000000000..5df12edbf5
--- /dev/null
+++ b/quarkus/runtime/src/main/java/org/keycloak/cli/command/StartDev.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2020 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.cli.command;
+
+import org.keycloak.util.Environment;
+
+import picocli.CommandLine;
+
+@CommandLine.Command(name = StartDev.NAME,
+ description = "%nStart the server in development mode.%n",
+ mixinStandardHelpOptions = true,
+ optionListHeading = "%nOptions%n",
+ parameterListHeading = "Available Commands%n")
+public final class StartDev extends AbstractStartCommand implements Runnable {
+
+ public static final String NAME = "start-dev";
+
+ @Override
+ protected void doBeforeRun() {
+ Environment.forceDevProfile();
+ spec.commandLine().getOut().printf("Running the server in dev mode. DO NOT run the '%s' command in production.%n", NAME);
+ }
+}
diff --git a/quarkus/runtime/src/main/java/org/keycloak/configuration/Configuration.java b/quarkus/runtime/src/main/java/org/keycloak/configuration/Configuration.java
index 1f2fd414fd..294bdd94f3 100644
--- a/quarkus/runtime/src/main/java/org/keycloak/configuration/Configuration.java
+++ b/quarkus/runtime/src/main/java/org/keycloak/configuration/Configuration.java
@@ -46,7 +46,7 @@ public final class Configuration {
String profile = Environment.getProfile();
if (profile == null) {
- profile = getConfig().getRawValue("kc.profile");
+ profile = getConfig().getRawValue(Environment.PROFILE);
}
value = KeycloakConfigSourceProvider.PERSISTED_CONFIG_SOURCE.getValue("%" + profile + "." + name);
diff --git a/quarkus/runtime/src/main/java/org/keycloak/configuration/PropertyMappers.java b/quarkus/runtime/src/main/java/org/keycloak/configuration/PropertyMappers.java
index 6a15175b0c..ea91ef8c3c 100644
--- a/quarkus/runtime/src/main/java/org/keycloak/configuration/PropertyMappers.java
+++ b/quarkus/runtime/src/main/java/org/keycloak/configuration/PropertyMappers.java
@@ -173,7 +173,7 @@ public final class PropertyMappers {
&& !Environment.CLI_ARGS.equals(name)
&& !"kc.home.dir".equals(name)
&& !"kc.config.file".equals(name)
- && !"kc.profile".equals(name)
+ && !Environment.PROFILE.equals(name)
&& !"kc.show.config".equals(name)
&& !"kc.show.config.runtime".equals(name)
&& !PropertyMappers.toCLIFormat("kc.config.file").equals(name);
diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/KeycloakRecorder.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/KeycloakRecorder.java
index bbd8d55a61..dbf233e102 100644
--- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/KeycloakRecorder.java
+++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/KeycloakRecorder.java
@@ -18,23 +18,14 @@
package org.keycloak.quarkus;
import static org.keycloak.configuration.Configuration.getBuiltTimeProperty;
-import static org.keycloak.configuration.Configuration.getConfig;
-import static org.keycloak.configuration.Configuration.getConfigValue;
import java.util.List;
import java.util.Map;
-import java.util.function.Predicate;
-import java.util.stream.StreamSupport;
-import io.smallrye.config.ConfigValue;
import org.jboss.logging.Logger;
import org.keycloak.QuarkusKeycloakSessionFactory;
-import org.keycloak.cli.ShowConfigCommand;
import org.keycloak.common.Profile;
import org.keycloak.configuration.Configuration;
-import org.keycloak.configuration.MicroProfileConfigProvider;
-import org.keycloak.configuration.PersistedConfigSource;
-import org.keycloak.configuration.PropertyMappers;
import org.keycloak.connections.liquibase.FastServiceLocator;
import org.keycloak.connections.liquibase.KeycloakLogger;
import org.keycloak.provider.Provider;
@@ -44,7 +35,6 @@ import org.keycloak.provider.Spi;
import io.quarkus.runtime.annotations.Recorder;
import liquibase.logging.LogFactory;
import liquibase.servicelocator.ServiceLocator;
-import org.keycloak.util.Environment;
@Recorder
public class KeycloakRecorder {
@@ -84,14 +74,6 @@ public class KeycloakRecorder {
QuarkusKeycloakSessionFactory.setInstance(new QuarkusKeycloakSessionFactory(factories, defaultProviders, preConfiguredProviders, reaugmented));
}
- /**
- * This method should be executed during static init so that the configuration is printed (if demanded) based on the properties
- * set from the previous reaugmentation
- */
- public void showConfig() {
- ShowConfigCommand.run();
- }
-
public static Profile createProfile() {
return new Profile(new Profile.PropertyResolver() {
@Override
diff --git a/quarkus/runtime/src/main/java/org/keycloak/util/Environment.java b/quarkus/runtime/src/main/java/org/keycloak/util/Environment.java
index b1d25b856b..c2ea900d12 100644
--- a/quarkus/runtime/src/main/java/org/keycloak/util/Environment.java
+++ b/quarkus/runtime/src/main/java/org/keycloak/util/Environment.java
@@ -21,7 +21,12 @@ import java.io.File;
import java.io.FilenameFilter;
import java.nio.file.Path;
import java.nio.file.Paths;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Map;
import java.util.Optional;
+import java.util.function.Function;
+import java.util.stream.Collectors;
import io.quarkus.runtime.LaunchMode;
import io.quarkus.runtime.configuration.ProfileManager;
@@ -32,6 +37,10 @@ public final class Environment {
public static final String IMPORT_EXPORT_MODE = "import_export";
public static final String CLI_ARGS = "kc.config.args";
+ public static final String PROFILE ="kc.profile";
+ public static final String ENV_PROFILE ="KC_PROFILE";
+
+ private Environment() {}
public static Boolean isRebuild() {
return Boolean.getBoolean("quarkus.launch.rebuild");
@@ -69,9 +78,9 @@ public final class Environment {
}
if (isWindows()) {
- return "kc.bat";
+ return "./kc.bat";
}
- return "kc.sh";
+ return "./kc.sh";
}
public static String getConfigArgs() {
@@ -79,15 +88,20 @@ public final class Environment {
}
public static String getProfile() {
- String profile = System.getProperty("kc.profile");
+ String profile = System.getProperty(PROFILE);
if (profile == null) {
- profile = System.getenv("KC_PROFILE");
+ profile = System.getenv(ENV_PROFILE);
}
return profile;
}
+ public static void setProfile(String profile) {
+ System.setProperty(PROFILE, profile);
+ System.setProperty("quarkus.profile", profile);
+ }
+
public static String getProfileOrDefault(String defaultProfile) {
String profile = getProfile();
@@ -126,15 +140,15 @@ public final class Environment {
}
public static void forceDevProfile() {
- System.setProperty("kc.profile", "dev");
+ System.setProperty(PROFILE, "dev");
System.setProperty("quarkus.profile", "dev");
}
- public static File[] getProviderFiles() {
+ public static Map getProviderFiles() {
Path providersPath = Environment.getProvidersPath();
if (providersPath == null) {
- return new File[] {};
+ return Collections.emptyMap();
}
File providersDir = providersPath.toFile();
@@ -143,11 +157,11 @@ public final class Environment {
throw new RuntimeException("The 'providers' directory does not exist or is not a valid directory.");
}
- return providersDir.listFiles(new FilenameFilter() {
+ return Arrays.stream(providersDir.listFiles(new FilenameFilter() {
@Override
public boolean accept(File dir, String name) {
return name.endsWith(".jar");
}
- });
+ })).collect(Collectors.toMap(File::getName, Function.identity()));
}
}
diff --git a/quarkus/runtime/src/test/java/org/keycloak/provider/quarkus/ConfigurationTest.java b/quarkus/runtime/src/test/java/org/keycloak/provider/quarkus/ConfigurationTest.java
index 1d38e395f2..c34dc6209b 100644
--- a/quarkus/runtime/src/test/java/org/keycloak/provider/quarkus/ConfigurationTest.java
+++ b/quarkus/runtime/src/test/java/org/keycloak/provider/quarkus/ConfigurationTest.java
@@ -41,6 +41,7 @@ import org.keycloak.configuration.MicroProfileConfigProvider;
import io.quarkus.runtime.configuration.ConfigUtils;
import io.smallrye.config.SmallRyeConfigProviderResolver;
+import org.keycloak.util.Environment;
public class ConfigurationTest {
@@ -142,7 +143,7 @@ public class ConfigurationTest {
@Test
public void testKeycloakProfilePropertySubstitution() {
- System.setProperty("kc.profile", "user-profile");
+ System.setProperty(Environment.PROFILE, "user-profile");
assertEquals("http://filepropprofile.unittest", initConfig("hostname", "default").get("frontendUrl"));
}
@@ -257,11 +258,11 @@ public class ConfigurationTest {
Assert.assertEquals("cluster-default.xml", initConfig("connectionsInfinispan", "quarkus").get("configFile"));
// If explicitly set, then it is always used regardless of the profile
- System.clearProperty("kc.profile");
+ System.clearProperty(Environment.PROFILE);
System.setProperty(CLI_ARGS, "--cluster=foo");
Assert.assertEquals("cluster-foo.xml", initConfig("connectionsInfinispan", "quarkus").get("configFile"));
- System.setProperty("kc.profile", "dev");
+ System.setProperty(Environment.PROFILE, "dev");
Assert.assertEquals("cluster-foo.xml", initConfig("connectionsInfinispan", "quarkus").get("configFile"));
System.setProperty(CLI_ARGS, "--cluster-stack=foo");