diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/Help.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/Help.java new file mode 100644 index 0000000000..2626c2c5ea --- /dev/null +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/Help.java @@ -0,0 +1,80 @@ +/* + * Copyright 2021 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.quarkus.runtime.cli; + +import static picocli.CommandLine.Help.Column.Overflow.SPAN; +import static picocli.CommandLine.Help.Column.Overflow.WRAP; + +import org.keycloak.utils.StringUtil; + +import picocli.CommandLine; + +public class Help extends CommandLine.Help { + + private static final int HELP_WIDTH = 100; + + public Help(CommandLine.Model.CommandSpec commandSpec, ColorScheme colorScheme) { + super(commandSpec, colorScheme); + } + + @Override + public Layout createDefaultLayout() { + return new Layout(colorScheme(), createTextTable(), createDefaultOptionRenderer(), createDefaultParameterRenderer()); + } + + private TextTable createTextTable() { + int longOptionsColumnWidth = commandSpec().commandLine().getUsageHelpLongOptionsMaxWidth(); + int descriptionWidth = HELP_WIDTH - longOptionsColumnWidth; + + // save space by using only two columns with better control over how option names and description are rendered + // for now, no support for required options + // picocli has a limit of 2 chars for shortnames, we do not + TextTable textTable = TextTable.forColumns(colorScheme(), + new Column(longOptionsColumnWidth, 0, SPAN), // " -cf, --config-file" + new Column(descriptionWidth, 1, WRAP)); + + textTable.setAdjustLineBreaksForWideCJKCharacters(commandSpec().usageMessage().adjustLineBreaksForWideCJKCharacters()); + + return textTable; + } + + @Override + public IOptionRenderer createDefaultOptionRenderer() { + return new OptionRenderer(); + } + + @Override + public String createHeading(String text, Object... params) { + if (StringUtil.isBlank(text)) { + return super.createHeading(text, params); + } + return super.createHeading("%n@|bold " + text + "|@%n%n", params); + } + + @Override + public IParameterRenderer createDefaultParameterRenderer() { + return new IParameterRenderer() { + @Override + public Ansi.Text[][] render(CommandLine.Model.PositionalParamSpec param, + IParamLabelRenderer parameterLabelRenderer, ColorScheme scheme) { + // we do our own formatting of parameters and labels when rendering optionsq + return new Ansi.Text[0][]; + } + }; + } +} diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/OptionRenderer.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/OptionRenderer.java new file mode 100644 index 0000000000..bbe70996e2 --- /dev/null +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/OptionRenderer.java @@ -0,0 +1,86 @@ +/* + * Copyright 2021 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.quarkus.runtime.cli; + +import static org.keycloak.quarkus.runtime.cli.Picocli.NO_PARAM_LABEL; +import static picocli.CommandLine.Help.Ansi.OFF; + +import org.keycloak.utils.StringUtil; + +import picocli.CommandLine; +import picocli.CommandLine.Help.Ansi.Text; +import picocli.CommandLine.Help.ColorScheme; +import picocli.CommandLine.Help.IParamLabelRenderer; +import picocli.CommandLine.Model.OptionSpec; + +public class OptionRenderer implements CommandLine.Help.IOptionRenderer { + + private static final String OPTION_NAME_SEPARATOR = ", "; + private static final Text EMPTY_TEXT = OFF.text(""); + + @Override + public Text[][] render(OptionSpec option, IParamLabelRenderer paramLabelRenderer, ColorScheme scheme) { + String[] names = option.names(); + + if (names.length > 2) { + throw new CommandLine.PicocliException("Options should have 2 names at most."); + } + + Text shortName = names.length > 1 ? scheme.optionText(names[0]) : EMPTY_TEXT; + Text longName = createLongName(option, scheme); + Text[][] result = new Text[1][]; + String[] descriptions = option.description(); + + // for better formatting, only a single line is expected in the description + // formatting is done by customizations to the text table + if (descriptions.length > 1) { + throw new CommandLine.PicocliException("Option[" + option + "] description should have a single line."); + } + + Text description = scheme.text(descriptions[0]); + + if (EMPTY_TEXT.equals(shortName)) { + result[0] = new Text[] { longName, description }; + } else { + result[0] = new Text[] { shortName.concat(OPTION_NAME_SEPARATOR).concat(longName), description }; + } + + return result; + } + + private Text createLongName(OptionSpec option, ColorScheme scheme) { + Text name = scheme.optionText(option.longestName()); + String paramLabel = formatParamLabel(option); + + if (StringUtil.isNotBlank(paramLabel) && !NO_PARAM_LABEL.equals(paramLabel) && !option.usageHelp() && !option.versionHelp()) { + name = name.concat(" ").concat(paramLabel); + } + + return name; + } + + private String formatParamLabel(OptionSpec option) { + String label = option.paramLabel(); + + if (label.startsWith("<") || NO_PARAM_LABEL.equals(label)) { + return label; + } + + return "<" + label + ">"; + } +} diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/Picocli.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/Picocli.java index 1764c82d1a..0471e59047 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/Picocli.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/Picocli.java @@ -71,6 +71,7 @@ public final class Picocli { public static final char ARG_KEY_VALUE_SEPARATOR = '='; public static final Pattern ARG_SPLIT = Pattern.compile(";;"); public static final Pattern ARG_KEY_VALUE_SPLIT = Pattern.compile("="); + public static final String NO_PARAM_LABEL = "none"; private Picocli() { } @@ -256,12 +257,11 @@ public final class Picocli { private static CommandLine createCommandLine(List cliArgs) { CommandSpec spec = CommandSpec.forAnnotatedObject(new Main()) .name(Environment.getCommand()); + boolean isStartCommand = cliArgs.size() == 1 && cliArgs.contains(Start.NAME); // avoid unnecessary processing when starting the server if (!isStartCommand) { - spec.usageMessage().width(100); - addOption(spec, Start.NAME, hasAutoBuildOption(cliArgs)); addOption(spec, StartDev.NAME, true); addOption(spec, Build.NAME, true); @@ -272,6 +272,7 @@ public final class Picocli { cmd.setExecutionExceptionHandler(new ExecutionExceptionHandler()); if (!isStartCommand) { + cmd.setHelpFactory(Help::new); cmd.getHelpSectionMap().put(SECTION_KEY_COMMAND_LIST, new SubCommandListRenderer()); } @@ -339,17 +340,19 @@ public final class Picocli { featureGroupBuilder.addArg(OptionSpec.builder(new String[] {"-ft", "--features"}) .description("Enables a group of features. Possible values are: " + String.join(",", featuresExpectedValues)) - .paramLabel("") + .paramLabel("feature") .completionCandidates(featuresExpectedValues) .type(String.class) .build()); + List expectedValues = asList("enabled", "disabled"); + for (Profile.Feature feature : Profile.Feature.values()) { featureGroupBuilder.addArg(OptionSpec.builder("--features-" + feature.name().toLowerCase()) .description("Enables the " + feature.name() + " feature.") - .paramLabel("[enabled|disabled]") + .paramLabel(String.join("|", expectedValues)) .type(String.class) - .completionCandidates(Arrays.asList("enabled", "disabled")) + .completionCandidates(expectedValues) .build()); } @@ -386,7 +389,7 @@ public final class Picocli { argGroupBuilder.addArg(OptionSpec.builder(name) .defaultValue(defaultValue) .description(description + (defaultValue == null ? "" : " Default: ${DEFAULT-VALUE}.")) - .paramLabel("<" + name.substring(2) + ">") + .paramLabel(mapper.getParamLabel()) .completionCandidates(mapper.getExpectedValues()) .type(String.class) .build()); diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/SubCommandListRenderer.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/SubCommandListRenderer.java index 6577ce3f58..e66b98cf7e 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/SubCommandListRenderer.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/SubCommandListRenderer.java @@ -31,7 +31,8 @@ import picocli.CommandLine.Model.UsageMessageSpec; * A {@link picocli.CommandLine.IHelpSectionRenderer} based on Quarkus CLI to show subcommands in help messages. */ class SubCommandListRenderer implements CommandLine.IHelpSectionRenderer { - // @Override + + @Override public String render(Help help) { CommandSpec spec = help.commandSpec(); if (spec.subcommands().isEmpty()) { diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/AbstractCommand.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/AbstractCommand.java index 14c7c0561c..0ecc89cfc4 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/AbstractCommand.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/AbstractCommand.java @@ -44,7 +44,7 @@ public abstract class AbstractCommand { @Option(names = { "-cf", "--config-file" }, arity = "1", description = "Set the path to a configuration file. By default, configuration properties are read from the \"keycloak.properties\" file in the \"conf\" directory.", - paramLabel = "", + paramLabel = "file", scope = CommandLine.ScopeType.INHERIT) public void setConfigFile(String path) { System.setProperty(KeycloakConfigSourceProvider.KEYCLOAK_CONFIG_FILE_PROP, path); diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/Build.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/Build.java index 96250ab5bf..e8647a86b1 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/Build.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/Build.java @@ -42,8 +42,8 @@ import picocli.CommandLine.Command; "", "Consider running this command before running the server in production for an optimal runtime." }, - footerHeading = "%nExamples:%n%n" - + " Optimize the server based on a profile configuration:%n%n" + footerHeading = "Examples:", + footer = " Optimize the server based on a profile configuration:%n%n" + " $ ${PARENT-COMMAND-FULL-NAME:-$PARENTCOMMAND} ${COMMAND-NAME} --profile=prod%n%n" + " Change database settings:%n%n" + " $ ${PARENT-COMMAND-FULL-NAME:-$PARENTCOMMAND} ${COMMAND-NAME} --db=postgres [--db-url][--db-username][--db-password]%n%n" @@ -55,9 +55,10 @@ import picocli.CommandLine.Command; + " $ ${PARENT-COMMAND-FULL-NAME:-$PARENTCOMMAND} ${COMMAND-NAME} --metrics-enabled=true%n%n" + "You can also use the \"--auto-build\" option when starting the server to avoid running this command every time you change a configuration:%n%n" + " $ ${PARENT-COMMAND-FULL-NAME:-$PARENTCOMMAND} start --auto-build %n%n" - + "By doing that you have an additional overhead when the server is starting.%n%n", + + "By doing that you have an additional overhead when the server is starting.", abbreviateSynopsis = true, - optionListHeading = "%nOptions%n%n") + optionListHeading = "Options:", + commandListHeading = "Commands:") public final class Build extends AbstractCommand implements Runnable { public static final String NAME = "build"; diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/Completion.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/Completion.java index 59c0af722e..e6b6ef1d49 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/Completion.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/Completion.java @@ -22,16 +22,14 @@ import picocli.CommandLine.Command; @Command(name = "completion", header = "Generate bash/zsh completion script for ${ROOT-COMMAND-NAME:-the root command of this command}.", - headerHeading = "%n", - commandListHeading = "%nCommands:%n", - synopsisHeading = "%nUsage: ", - optionListHeading = "Options:%n", + optionListHeading = "Options:", + commandListHeading = "Commands:", description = { + "", "Generate bash/zsh completion script for ${ROOT-COMMAND-NAME:-the root command of this command}.%n" + "Run the following command to give `${ROOT-COMMAND-NAME:-$PARENTCOMMAND}` TAB completion in the current shell:", "", - " source <(${PARENT-COMMAND-FULL-NAME:-$PARENTCOMMAND} ${COMMAND-NAME})", - ""}, + " source <(${PARENT-COMMAND-FULL-NAME:-$PARENTCOMMAND} ${COMMAND-NAME})"}, abbreviateSynopsis = true) public class Completion extends AutoComplete.GenerateCompletion { } diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/Export.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/Export.java index 3de005ad5f..b9c471d678 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/Export.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/Export.java @@ -23,11 +23,12 @@ import picocli.CommandLine.Command; import picocli.CommandLine.Option; @Command(name = "export", - description = "Export data from realms to a file or directory.", + header = "Export data from realms to a file or directory.", + description = "%nExport data from realms to a file or directory.", showDefaultValues = true, abbreviateSynopsis = true, - optionListHeading = "%nOptions%n", - parameterListHeading = "Available Commands%n") + optionListHeading = "Options:", + commandListHeading = "Commands:") public final class Export extends AbstractExportImportCommand implements Runnable { @Option(names = "--users", diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/Import.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/Import.java index 71a5188e29..f79961b36a 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/Import.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/Import.java @@ -25,11 +25,12 @@ import picocli.CommandLine.Command; import picocli.CommandLine.Option; @Command(name = "import", - description = "Import data from a directory or a file.", + header = "Import data from a directory or a file.", + description = "%nImport data from a directory or a file.", showDefaultValues = true, abbreviateSynopsis = true, - optionListHeading = "%nOptions%n", - parameterListHeading = "Available Commands%n") + optionListHeading = "Options:", + commandListHeading = "Commands:") public final class Import extends AbstractExportImportCommand implements Runnable { @Option(names = "--override", diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/Main.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/Main.java index 923baf01f7..477e53f5ad 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/Main.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/Main.java @@ -17,6 +17,8 @@ package org.keycloak.quarkus.runtime.cli.command; +import static org.keycloak.quarkus.runtime.cli.Picocli.NO_PARAM_LABEL; + import picocli.CommandLine.Command; import picocli.CommandLine.Option; import picocli.CommandLine.ScopeType; @@ -25,12 +27,11 @@ import picocli.CommandLine.ScopeType; header = { "Keycloak - Open Source Identity and Access Management", "", - "Find more information at: https://www.keycloak.org/docs/latest", - "" + "Find more information at: https://www.keycloak.org/docs/latest" }, - description = "%nUse this command-line tool to manage your Keycloak cluster.%n", - footerHeading = "%nExamples:%n%n" - + " Start the server in development mode for local development or testing:%n%n" + description = "%nUse this command-line tool to manage your Keycloak cluster.", + footerHeading = "Examples:", + footer = { " Start the server in development mode for local development or testing:%n%n" + " $ ${COMMAND-NAME} start-dev%n%n" + " Building an optimized server runtime:%n%n" + " $ ${COMMAND-NAME} build %n%n" @@ -38,19 +39,18 @@ import picocli.CommandLine.ScopeType; + " $ ${COMMAND-NAME} start %n%n" + " Enable auto-completion to bash/zsh:%n%n" + " $ source <(${COMMAND-NAME} tools completion)%n%n" - + " Please, take a look at the documentation for more details before deploying in production.%n", - footer = { + + " Please, take a look at the documentation for more details before deploying in production.", "", "Use \"${COMMAND-NAME} start --help\" for the available options when starting the server.", - "Use \"${COMMAND-NAME} --help\" for more information about other commands.", + "Use \"${COMMAND-NAME} --help\" for more information about other commands." }, version = { "Keycloak ${sys:kc.version}", "JVM: ${java.version} (${java.vendor} ${java.vm.name} ${java.vm.version})", "OS: ${os.name} ${os.version} ${os.arch}" }, - optionListHeading = "Options%n%n", - commandListHeading = "%nCommands%n%n", + optionListHeading = "Options:", + commandListHeading = "Commands:", abbreviateSynopsis = true, subcommands = { Build.class, @@ -79,8 +79,8 @@ public final class Main { boolean version; @Option(names = { "-v", "--verbose" }, - description = "Print out more details when running this command. Useful for troubleshooting if some unexpected error occurs.", - required = false, + description = "Print out error details when running this command.", + paramLabel = NO_PARAM_LABEL, scope = ScopeType.INHERIT) Boolean verbose; } \ No newline at end of file diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/ShowConfig.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/ShowConfig.java index 18599dcca2..5e56230540 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/ShowConfig.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/ShowConfig.java @@ -41,10 +41,10 @@ import picocli.CommandLine.Command; import picocli.CommandLine.Parameters; @Command(name = "show-config", - description = "Print out the current configuration.", + header = "Print out the current configuration.", + description = "%nPrint out the current configuration.", abbreviateSynopsis = true, - optionListHeading = "%nOptions%n", - parameterListHeading = "Available Commands%n") + optionListHeading = "Options:") public final class ShowConfig extends AbstractCommand implements Runnable { @Parameters( diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/Start.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/Start.java index a4f2da65ac..b86adda99c 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/Start.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/Start.java @@ -17,18 +17,23 @@ package org.keycloak.quarkus.runtime.cli.command; +import static org.keycloak.quarkus.runtime.cli.Picocli.NO_PARAM_LABEL; + +import org.keycloak.quarkus.runtime.cli.Picocli; + import picocli.CommandLine; import picocli.CommandLine.Command; @Command(name = Start.NAME, - header = "Start the server.%n", + header = "Start the server.", description = { "%nUse this command to run the server in production." }, - footerHeading = "%nYou may use the \"--auto-build\" option when starting the server to avoid running the \"build\" command everytime you need to change a static property:%n%n" + footer = "%nYou may use the \"--auto-build\" option when starting the server to avoid running the \"build\" command everytime you need to change a static property:%n%n" + " $ ${PARENT-COMMAND-FULL-NAME:-$PARENTCOMMAND} ${COMMAND-NAME} --auto-build %n%n" - + "By doing that you have an additional overhead when the server is starting. Run \"${PARENT-COMMAND-FULL-NAME:-$PARENTCOMMAND} build -h\" for more details.%n%n", - optionListHeading = "%nOptions%n%n", + + "By doing that you have an additional overhead when the server is starting. Run \"${PARENT-COMMAND-FULL-NAME:-$PARENTCOMMAND} build -h\" for more details.", + optionListHeading = "Options:", + commandListHeading = "Commands:", abbreviateSynopsis = true) public final class Start extends AbstractStartCommand implements Runnable { @@ -38,6 +43,7 @@ public final class Start extends AbstractStartCommand implements Runnable { 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.", + paramLabel = NO_PARAM_LABEL, order = 1) Boolean autoConfig; } diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/StartDev.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/StartDev.java index 21729117ec..475cb8cd21 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/StartDev.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/StartDev.java @@ -27,8 +27,9 @@ import picocli.CommandLine.Command; description = { "%nUse this command if you want to run the server locally for development or testing purposes.", }, - footerHeading = "%nDo NOT start the server using this command when deploying to production.%n%n", - optionListHeading = "%nOptions%n%n", + footer = "%nDo NOT start the server using this command when deploying to production.", + optionListHeading = "Options:", + commandListHeading = "Commands:", abbreviateSynopsis = true) public final class StartDev extends AbstractStartCommand implements Runnable { diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/Tools.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/Tools.java index d30d4f7cd9..453d80e9f2 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/Tools.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/Tools.java @@ -17,13 +17,19 @@ package org.keycloak.quarkus.runtime.cli.command; +import picocli.CommandLine; import picocli.CommandLine.Command; @Command(name = "tools", - description = "Utilities for use and interaction with the server.", + description = "%nUtilities for use and interaction with the server.", abbreviateSynopsis = true, - optionListHeading = "%nOptions%n", - parameterListHeading = "Available Commands%n", + commandListHeading = "Commands:", + optionListHeading = "Options:", subcommands = {Completion.class}) public class Tools { + + @CommandLine.Option(names = { "-h", "--help" }, + description = "This help message.", + usageHelp = true) + boolean help; } diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/ClusteringPropertyMappers.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/ClusteringPropertyMappers.java index 766da8f4c2..01ce8ebcf9 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/ClusteringPropertyMappers.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/ClusteringPropertyMappers.java @@ -17,6 +17,7 @@ final class ClusteringPropertyMappers { "configuration file prefixed with the 'cluster-` inside the distribution configuration directory. " + "Value 'local' effectively disables clustering and use infinispan caches in the local mode. " + "Value 'default' enables clustering for infinispan caches.") + .paramLabel("mode") .isBuildTimeProperty(true) .expectedValues(Arrays.asList("local", "default")) .build(), @@ -24,6 +25,7 @@ final class ClusteringPropertyMappers { .to("kc.spi.connections-infinispan.quarkus.stack") .description("Define the default stack to use for cluster communication and node discovery.") .defaultValue("udp") + .paramLabel("stack") .isBuildTimeProperty(true) .expectedValues(Arrays.asList("tcp", "udp", "kubernetes", "ec2", "azure", "google")) .build() diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/ConfigCategory.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/ConfigCategory.java index 710c116a7a..4b4e834ea2 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/ConfigCategory.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/ConfigCategory.java @@ -2,15 +2,15 @@ package org.keycloak.quarkus.runtime.configuration.mappers; public enum ConfigCategory { // ordered by name asc - CLUSTERING("%nCluster:%n%n", 10), - DATABASE("%nDatabase:%n%n", 20), - FEATURE("%nFeature:%n%n", 30), - HOSTNAME("%nHostname:%n%n", 40), - HTTP("%nHTTP/TLS:%n%n", 50), - METRICS("%nMetrics:%n%n", 60), - PROXY("%nProxy:%n%n", 70), - VAULT("%nVault:%n%n", 80), - GENERAL("%nGeneral:%n%n", 999); + CLUSTERING("Cluster:", 10), + DATABASE("Database:", 20), + FEATURE("Feature:", 30), + HOSTNAME("Hostname:", 40), + HTTP("HTTP/TLS:", 50), + METRICS("Metrics:", 60), + PROXY("Proxy:", 70), + VAULT("Vault:", 80), + GENERAL("General:", 999); private final String heading; diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/DatabasePropertyMappers.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/DatabasePropertyMappers.java index 1e96ceb107..d50ed4c987 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/DatabasePropertyMappers.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/DatabasePropertyMappers.java @@ -31,6 +31,7 @@ final class DatabasePropertyMappers { .isBuildTimeProperty(true) .transformer(getSupportedDbValue()) .description("The database vendor. Possible values are: " + String.join(",", supportedDatabaseVendors)) + .paramLabel("vendor") .expectedValues(Arrays.asList(supportedDatabaseVendors)) .build(), builder().from("db") @@ -45,32 +46,39 @@ final class DatabasePropertyMappers { "For instance, if using 'postgres', the JDBC URL would be 'jdbc:postgresql://localhost/keycloak'. " + "The host, database and properties can be overridden by setting the following system properties," + " respectively: -Dkc.db.url.host, -Dkc.db.url.database, -Dkc.db.url.properties.") + .paramLabel("jdbc-url") .build(), builder().from("db.username") .to("quarkus.datasource.username") .description("The username of the database user.") + .paramLabel("username") .build(), builder().from("db.password") .to("quarkus.datasource.password") .description("The password of the database user.") + .paramLabel("password") .isMasked(true) .build(), builder().from("db.schema") .to("quarkus.datasource.schema") .description("The database schema to be used.") + .paramLabel("schema") .build(), builder().from("db.pool.initial-size") .to("quarkus.datasource.jdbc.initial-size") .description("The initial size of the connection pool.") + .paramLabel("size") .build(), builder().from("db.pool.min-size") .to("quarkus.datasource.jdbc.min-size") .description("The minimal size of the connection pool.") + .paramLabel("size") .build(), builder().from("db.pool.max-size") .to("quarkus.datasource.jdbc.max-size") .defaultValue(String.valueOf(100)) .description("The maximum size of the connection pool.") + .paramLabel("size") .build() }; } diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/HostnamePropertyMappers.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/HostnamePropertyMappers.java index 1c3a8b7ede..42efd8e9ff 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/HostnamePropertyMappers.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/HostnamePropertyMappers.java @@ -12,14 +12,17 @@ final class HostnamePropertyMappers { builder().from("hostname-frontend-url") .to("kc.spi.hostname.default.frontend-url") .description("The URL that should be used to serve frontend requests that are usually sent through a public domain.") + .paramLabel("url") .build(), builder().from("hostname-admin-url") .to("kc.spi.hostname.default.admin-url") .description("The URL that should be used to expose the admin endpoints and console.") + .paramLabel("url") .build(), builder().from("hostname-force-backend-url-to-frontend-url") .to("kc.spi.hostname.default.force-backend-url-to-frontend-url") .description("Forces backend requests to go through the URL defined as the frontend-url. Defaults to false. Possible values are true or false.") + .paramLabel(Boolean.TRUE + "|" + Boolean.FALSE) .expectedValues(Arrays.asList(Boolean.TRUE.toString(), Boolean.FALSE.toString())) .build() }; diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/HttpPropertyMappers.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/HttpPropertyMappers.java index fed733f719..4e59721707 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/HttpPropertyMappers.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/HttpPropertyMappers.java @@ -23,67 +23,81 @@ final class HttpPropertyMappers { .defaultValue(Boolean.FALSE.toString()) .transformer(HttpPropertyMappers::getHttpEnabledTransformer) .description("Enables the HTTP listener.") + .paramLabel(Boolean.TRUE + "|" + Boolean.FALSE) .expectedValues(Arrays.asList(Boolean.TRUE.toString(), Boolean.FALSE.toString())) .build(), builder().from("http.host") .to("quarkus.http.host") .defaultValue("0.0.0.0") .description("The used HTTP Host.") + .paramLabel("host") .build(), builder().from("http.port") .to("quarkus.http.port") .defaultValue(String.valueOf(8080)) .description("The used HTTP port.") + .paramLabel("port") .build(), builder().from("https.port") .to("quarkus.http.ssl-port") .defaultValue(String.valueOf(8443)) .description("The used HTTPS port.") + .paramLabel("port") .build(), builder().from("https.client-auth") .to("quarkus.http.ssl.client-auth") .defaultValue("none") .description("Configures the server to require/request client authentication. Possible Values: none, request, required.") + .paramLabel("auth") .expectedValues(Arrays.asList("none", "request", "required")) .build(), builder().from("https.cipher-suites") .to("quarkus.http.ssl.cipher-suites") .description("The cipher suites to use. If none is given, a reasonable default is selected.") + .paramLabel("ciphers") .build(), builder().from("https.protocols") .to("quarkus.http.ssl.protocols") .description("The list of protocols to explicitly enable.") + .paramLabel("protocols") .build(), builder().from("https.certificate.file") .to("quarkus.http.ssl.certificate.file") .description("The file path to a server certificate or certificate chain in PEM format.") + .paramLabel("file") .build(), builder().from("https.certificate.key-file") .to("quarkus.http.ssl.certificate.key-file") .description("The file path to a private key in PEM format.") + .paramLabel("file") .build(), builder().from("https.certificate.key-store-file") .to("quarkus.http.ssl.certificate.key-store-file") .defaultValue(getDefaultKeystorePathValue()) .description("The key store which holds the certificate information instead of specifying separate files.") + .paramLabel("file") .build(), builder().from("https.certificate.key-store-password") .to("quarkus.http.ssl.certificate.key-store-password") .description("The password of the key store file. If not given, the default (\"password\") is used.") + .paramLabel("password") .isMasked(true) .build(), builder().from("https.certificate.key-store-file-type") .to("quarkus.http.ssl.certificate.key-store-file-type") .description("The type of the key store file. " + "If not given, the type is automatically detected based on the file name.") + .paramLabel("type") .build(), builder().from("https.certificate.trust-store-file") .to("quarkus.http.ssl.certificate.trust-store-file") .description("The trust store which holds the certificate information of the certificates to trust.") + .paramLabel("file") .build(), builder().from("https.certificate.trust-store-password") .to("quarkus.http.ssl.certificate.trust-store-password") .description("The password of the trust store file.") + .paramLabel("password") .isMasked(true) .build(), builder().from("https.certificate.trust-store-file-type") @@ -91,6 +105,7 @@ final class HttpPropertyMappers { .defaultValue(getDefaultKeystorePathValue()) .description("The type of the trust store file. " + "If not given, the type is automatically detected based on the file name.") + .paramLabel("type") .build() }; diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/MetricsPropertyMappers.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/MetricsPropertyMappers.java index 66513533ad..8f8f63d9e0 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/MetricsPropertyMappers.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/MetricsPropertyMappers.java @@ -14,6 +14,7 @@ final class MetricsPropertyMappers { .isBuildTimeProperty(true) .defaultValue(Boolean.FALSE.toString()) .description("If the server should expose metrics and healthcheck. If enabled, metrics are available at the '/metrics' endpoint and healthcheck at the '/health' endpoint.") + .paramLabel(Boolean.TRUE + "|" + Boolean.FALSE) .expectedValues(Arrays.asList(Boolean.TRUE.toString(), Boolean.FALSE.toString())) .build() }; diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/PropertyMapper.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/PropertyMapper.java index 2e565f1b19..92dafcfe7e 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/PropertyMapper.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/PropertyMapper.java @@ -25,7 +25,8 @@ import org.keycloak.quarkus.runtime.configuration.MicroProfileConfigProvider; public class PropertyMapper { - static PropertyMapper IDENTITY = new PropertyMapper(null, null, null, null, null,false,null,false,Collections.emptyList(),null) { + static PropertyMapper IDENTITY = new PropertyMapper(null, null, null, null, null, + false,null, null, false,Collections.emptyList(),null) { @Override public ConfigValue getOrDefault(String name, ConfigSourceInterceptorContext context, ConfigValue current) { return current; @@ -42,9 +43,10 @@ public class PropertyMapper { private final boolean mask; private final Iterable expectedValues; private final ConfigCategory category; + private final String paramLabel; PropertyMapper(String from, String to, String defaultValue, BiFunction mapper, - String mapFrom, boolean buildTime, String description, boolean mask, Iterable expectedValues, ConfigCategory category) { + String mapFrom, boolean buildTime, String description, String paramLabel, boolean mask, Iterable expectedValues, ConfigCategory category) { this.from = MicroProfileConfigProvider.NS_KEYCLOAK_PREFIX + from; this.to = to; this.defaultValue = defaultValue; @@ -52,6 +54,7 @@ public class PropertyMapper { this.mapFrom = mapFrom; this.buildTime = buildTime; this.description = description; + this.paramLabel = paramLabel; this.mask = mask; this.expectedValues = expectedValues == null ? Collections.emptyList() : expectedValues; this.category = category != null ? category : ConfigCategory.GENERAL; @@ -174,6 +177,10 @@ public class PropertyMapper { return to; } + public String getParamLabel() { + return paramLabel; + } + public static class Builder { private String from; @@ -186,6 +193,7 @@ public class PropertyMapper { private boolean isBuildTimeProperty = false; private boolean isMasked = false; private ConfigCategory category = ConfigCategory.GENERAL; + private String paramLabel; public Builder(ConfigCategory category) { this.category = category; @@ -222,6 +230,11 @@ public class PropertyMapper { return this; } + public Builder paramLabel(String label) { + this.paramLabel = label; + return this; + } + public Builder mapFrom(String mapFrom) { this.mapFrom = mapFrom; return this; @@ -248,7 +261,7 @@ public class PropertyMapper { } public PropertyMapper build() { - return new PropertyMapper(from, to, defaultValue, mapper, mapFrom, isBuildTimeProperty, description, isMasked, expectedValues, category); + return new PropertyMapper(from, to, defaultValue, mapper, mapFrom, isBuildTimeProperty, description, paramLabel, isMasked, expectedValues, category); } } } diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/ProxyPropertyMappers.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/ProxyPropertyMappers.java index e364fa93e2..a7c1b782ed 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/ProxyPropertyMappers.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/ProxyPropertyMappers.java @@ -22,6 +22,7 @@ final class ProxyPropertyMappers { .expectedValues(Arrays.asList(possibleProxyValues)) .description("The proxy address forwarding mode if the server is behind a reverse proxy. " + "Possible values are: " + String.join(",",possibleProxyValues)) + .paramLabel("mode") .category(ConfigCategory.PROXY) .build() }; diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/VaultPropertyMappers.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/VaultPropertyMappers.java index 37636063c4..3bca243f9f 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/VaultPropertyMappers.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/VaultPropertyMappers.java @@ -11,6 +11,7 @@ final class VaultPropertyMappers { .from("vault.file.path") .to("kc.spi.vault.files-plaintext.dir") .description("If set, secrets can be obtained by reading the content of files within the given path.") + .paramLabel("dir") .build(), builder() .from("vault.hashicorp.") @@ -21,6 +22,7 @@ final class VaultPropertyMappers { .from("vault.hashicorp.paths") .to("kc.spi.vault.hashicorp.paths") .description("A set of one or more paths that should be used when looking up secrets.") + .paramLabel("paths") .build() }; }