From 7da8a8a2e3839b6f16434164a0d094051d456196 Mon Sep 17 00:00:00 2001 From: Peter Zaoral Date: Fri, 19 Dec 2025 17:55:42 +0100 Subject: [PATCH] feat: add Windows service support (#44496) Closes: #37704 Signed-off-by: Peter Zaoral --- .github/actions/prunsrv-setup/action.yml | 81 +++++ .github/workflows/ci.yml | 5 + .../release_notes/topics/26_5_0.adoc | 8 + docs/guides/server/pinned-guides | 3 +- docs/guides/server/windows-service.adoc | 174 ++++++++++ quarkus/README.md | 19 +- .../keycloak/quarkus/runtime/cli/Picocli.java | 20 ++ .../quarkus/runtime/cli/command/Tools.java | 4 +- .../runtime/cli/command/WindowsService.java | 29 ++ .../cli/command/WindowsServiceInstall.java | 230 +++++++++++++ .../cli/command/WindowsServiceUninstall.java | 105 ++++++ quarkus/tests/integration/pom.xml | 2 +- .../it/cli/dist/HelpCommandDistTest.java | 57 ++-- .../it/cli/dist/WindowsServiceDistTest.java | 314 ++++++++++++++++++ ...andDistTest.testDefaultToHelp.approved.txt | 5 +- .../HelpCommandDistTest.testHelp.approved.txt | 5 +- ...CommandDistTest.testHelpShort.approved.txt | 5 +- ...t.testUpdateCompatibilityHelp.approved.txt | 2 +- quarkus/tests/junit5/pom.xml | 2 +- 19 files changed, 1029 insertions(+), 41 deletions(-) create mode 100644 .github/actions/prunsrv-setup/action.yml create mode 100644 docs/guides/server/windows-service.adoc create mode 100644 quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/WindowsService.java create mode 100644 quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/WindowsServiceInstall.java create mode 100644 quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/WindowsServiceUninstall.java create mode 100644 quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/WindowsServiceDistTest.java diff --git a/.github/actions/prunsrv-setup/action.yml b/.github/actions/prunsrv-setup/action.yml new file mode 100644 index 00000000000..de64588e609 --- /dev/null +++ b/.github/actions/prunsrv-setup/action.yml @@ -0,0 +1,81 @@ +name: Setup Apache Commons Daemon (Procrun) +description: Download and cache Apache Commons Daemon prunsrv.exe for Windows service tests + +inputs: + version: + description: Apache Commons Daemon version (leave empty for latest) + required: false + default: "" + +outputs: + prunsrv-path: + description: Path to the prunsrv.exe executable + value: ${{ steps.setup.outputs.prunsrv-path }} + version: + description: The resolved Apache Commons Daemon version + value: ${{ steps.resolve-version.outputs.version }} + +runs: + using: composite + steps: + - name: Resolve latest version + id: resolve-version + shell: pwsh + run: | + $inputVersion = "${{ inputs.version }}" + if ([string]::IsNullOrWhiteSpace($inputVersion)) { + Write-Host "No version specified, detecting latest version from Apache downloads..." + $indexUrl = "https://downloads.apache.org/commons/daemon/binaries/windows/" + $response = Invoke-WebRequest -Uri $indexUrl -UseBasicParsing + # Parse the HTML to find the zip file and extract version + $match = [regex]::Match($response.Content, 'commons-daemon-(\d+\.\d+\.\d+)-bin-windows\.zip') + if ($match.Success) { + $version = $match.Groups[1].Value + Write-Host "Detected latest version: $version" + } else { + Write-Error "Could not detect latest version from Apache downloads page" + exit 1 + } + } else { + $version = $inputVersion + Write-Host "Using specified version: $version" + } + echo "version=$version" >> $env:GITHUB_OUTPUT + + - uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 + id: cache + name: Cache Apache Commons Daemon + with: + path: ${{ runner.temp }}/commons-daemon + key: commons-daemon-${{ steps.resolve-version.outputs.version }}-windows-amd64 + + - name: Download Apache Commons Daemon + if: steps.cache.outputs.cache-hit != 'true' + shell: pwsh + run: | + $version = "${{ steps.resolve-version.outputs.version }}" + $downloadUrl = "https://downloads.apache.org/commons/daemon/binaries/windows/commons-daemon-${version}-bin-windows.zip" + $zipPath = Join-Path "${{ runner.temp }}" "commons-daemon.zip" + $extractPath = Join-Path "${{ runner.temp }}" "commons-daemon" + + Write-Host "Downloading Apache Commons Daemon $version from $downloadUrl" + Invoke-WebRequest -Uri $downloadUrl -OutFile $zipPath -UseBasicParsing + + Write-Host "Extracting to $extractPath" + Expand-Archive -Path $zipPath -DestinationPath $extractPath -Force + Remove-Item $zipPath -Force + + - name: Setup environment + id: setup + shell: pwsh + run: | + $prunsrvPath = "${{ runner.temp }}/commons-daemon/amd64/prunsrv.exe" + + if (-not (Test-Path $prunsrvPath)) { + Write-Error "prunsrv.exe not found at $prunsrvPath" + exit 1 + } + + Write-Host "Apache Commons Daemon prunsrv.exe found at: $prunsrvPath" + echo "COMMONS_DAEMON_HOME=${{ runner.temp }}/commons-daemon/amd64" >> $env:GITHUB_ENV + echo "prunsrv-path=$prunsrvPath" >> $env:GITHUB_OUTPUT diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fb9afd4dfa2..42b2646fbb2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -277,6 +277,11 @@ jobs: name: Integration test setup uses: ./.github/actions/integration-test-setup + - id: prunsrv-setup + name: Setup Apache Commons Daemon (Procrun) for Windows service tests + if: runner.os == 'Windows' + uses: ./.github/actions/prunsrv-setup + # Smoke tests should cover scenarios that could be broken by changes in other modules that quarkus # test classes and even individual tests are included in the following suites by junit tags # kc.quarkus.tests.groups acts as the tag filter diff --git a/docs/documentation/release_notes/topics/26_5_0.adoc b/docs/documentation/release_notes/topics/26_5_0.adoc index ab9183e53a1..c155734913d 100644 --- a/docs/documentation/release_notes/topics/26_5_0.adoc +++ b/docs/documentation/release_notes/topics/26_5_0.adoc @@ -143,6 +143,14 @@ You can now use a new client certificate lookup provider that is compliant with This enables native support e.g. for Caddy and other reverse proxies that follow the RFC. For details, navigate to link:{server_guide_base_link}/reverseproxy#_enabling_client_certificate_lookup[Enabling Client Certificate Lookup] section of the documentation. +== Running Keycloak as a Windows service + +{project_name} can now be installed and run as a Windows service using Apache Commons Daemon (Procrun). The new `tools windows-service` CLI subcommand simplifies service installation and uninstallation. + +The service runs `kc.bat start` as an external process, ensuring all environment variables and configuration files are respected. This provides seamless integration with the Windows Services management console and enables automatic startup on system boot without requiring a user to be logged on. + +For more information, see the https://www.keycloak.org/server/windows-service[Running Keycloak as a Windows Service] guide. + = Observability == Export traces with custom request headers diff --git a/docs/guides/server/pinned-guides b/docs/guides/server/pinned-guides index 16f15b99318..9bc7ff9863e 100644 --- a/docs/guides/server/pinned-guides +++ b/docs/guides/server/pinned-guides @@ -23,4 +23,5 @@ importExport vault all-config all-provider-config -update-compatibility \ No newline at end of file +update-compatibility +windows-service \ No newline at end of file diff --git a/docs/guides/server/windows-service.adoc b/docs/guides/server/windows-service.adoc new file mode 100644 index 00000000000..eefa627d925 --- /dev/null +++ b/docs/guides/server/windows-service.adoc @@ -0,0 +1,174 @@ +<#import "/templates/guide.adoc" as tmpl> +<#import "/templates/links.adoc" as links> + +<@tmpl.guide + title="Run {project_name} as a Windows Service" + summary="Install and run {project_name} as a Windows service using Apache Commons Daemon."> + +This guide explains how to install and run {project_name} as a Windows service using Apache Commons Daemon. The service runs `kc.bat` in "exe" mode so behavior matches running `kc.bat start` manually. The service runs in "exe" mode, where Procrun executes `kc.bat start` as an external process. Environment variables, such as `KC_*`, along with `conf/keycloak.conf`, are respected. The `kc.bat` script handles augmentation and build logic, ensuring the service behaves exactly like a manual start. + +== Apache Commons Daemon Setup +To run {project_name} as a Windows service, you need the Apache Commons Daemon Procrun binary (`prunsrv.exe`). Download it for your platform: https://downloads.apache.org/commons/daemon/binaries/windows/ + +Then place `prunsrv.exe` into the Keycloak `bin` folder. + +[source,bash] +---- +copy "path\to\prunsrv.exe" "%KEYCLOAK_HOME%\bin\prunsrv.exe" +---- + +Use the amd64 binary for 64-bit and x86 for 32-bit systems. + +== Optional: Pre-build + +Pre-building is optional. If you do not pre-build, Keycloak will build automatically on first start, which takes longer. + +[source,bash] +---- +bin/kc.bat build --db=postgres +---- + +== Installing the Service + +Keycloak includes a `tools windows-service` subcommand to simplify service installation and uninstallation. + +[source,bash] +---- +bin\kc.bat tools windows-service install --help +---- + +[source,bash] +---- +bin\kc.bat tools windows-service uninstall --help +---- + +=== Examples + +Install a basic service (runs as Local System by default): + +[source,bash] +---- +bin\kc.bat tools windows-service install --name keycloak +---- + +Manual startup and longer stop timeout: + +[source,bash] +---- +bin\kc.bat tools windows-service install --startup=manual --stop-timeout=60 +---- + +Delayed auto-start: + +[source,bash] +---- +bin\kc.bat tools windows-service install --startup=delayed +---- + +Custom display name: + +[source,bash] +---- +bin\kc.bat tools windows-service install --name=my-keycloak --display-name="My Keycloak Server" +---- + +Use `--depends-on` to ensure required Windows services start before Keycloak (for example, a local database). By default Apache Commons Daemon may add `Tcpip` and `Afd` network dependencies. + +[source,bash] +---- +bin\kc.bat tools windows-service install --depends-on="postgresql-x64-15;Tcpip;Afd" +---- + +The default is to run the service as the Local System account - `--service-user` and `--service-password` can be omitted (recommended). To run as a specific user, the account must have the "Log on as a service" right. + +[source,bash] +---- +bin\kc.bat tools windows-service install --service-user="DOMAIN\Username" --service-password="password" +---- + +You can supply the service password securely via an environment variable, which is recommended: + +[source,bash] +---- +set KC_SERVICE_PASSWORD=s3cret +bin\kc.bat tools windows-service install --service-user="DOMAIN\Username" +---- + +Start the service: + +[source,bash] +---- +net start keycloak +---- + +Stop the service: + +[source,bash] +---- +net stop keycloak +---- + +Uninstall the service: + +[source,bash] +---- +bin\kc.bat tools windows-service uninstall --name keycloak +---- + +Check status using the Windows Services console (`services.msc`). + +== Logging + +When {project_name} runs as a service, it is recommended to enable file logging - see <@links.server id="logging"/>. + +The service wrapper logs (e.g. `commons-daemon.YYYY-MM-DD.log`) respects the `log-path` option value during service creation. + +== Configuration Changes + +To change runtime configuration: + +1. Stop the service: `net stop keycloak`. +2. Update environment variables or `conf/keycloak.conf`. +3. Optionally re-run build: `bin\kc.bat build [new-options]`. +4. Start the service: `net start keycloak`. + +== Troubleshooting + +=== Access Denied errors + +* Ensure the service runs as Local System (default) or that the specified account has "Log on as a service". + +=== Options defined as environment variables are ignored + +Windows Services run in a separate session (usually as the LocalSystem account) and do not inherit the environment variables of the user who created the service. Define the required `KC_*` environment variables as system-wide environment variables, so they are available to the service. + +=== Forcefully terminate the service + +If the Apache Commons Daemon wrapper becomes unresponsive: + +[source,bash] +---- +taskkill /f /im prunsrv.exe +---- + +Use caution — this will affect all Procrun-managed services on the host. + +== Apache Commons Daemon configuration under the hood + +When you create the service, the following Apache Commons Daemon Procrun settings are applied: + +* StartMode: `exe` (runs `kc.bat` as an external process) +* StartImage: `\bin\kc.bat` +* StartParams: `start` +* StopMode: `exe` +* StopImage: `\bin\kc.bat` +* StopParams: `stop` +* StopTimeout: configurable (default: 30 seconds) + +Service configuration is stored in the Windows Registry under: + +---- +HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Apache Software Foundation\ProcRun 2.0\ +---- + + diff --git a/quarkus/README.md b/quarkus/README.md index 32b19026bb8..a6f9d47cad3 100644 --- a/quarkus/README.md +++ b/quarkus/README.md @@ -121,8 +121,21 @@ Example: ### Updating Expectations -Changing to the help output will cause HelpCommandDistTest to fail. You may use: +Changing the help output will cause HelpCommandDistTest to fail. This test uses [ApprovalTests](https://github.com/approvals/ApprovalTests.Java) which creates `.received.txt` files containing the actual output when tests fail. To update the expected output (see [Approving The Result](https://github.com/approvals/ApprovalTests.Java/blob/master/approvaltests/docs/tutorials/GettingStarted.md#approving-the-result)): - KEYCLOAK_REPLACE_EXPECTED=true ../mvnw clean install -Dtest=HelpCommandDistTest +1. Run the failing test: + ``` + ../mvnw clean install -Dtest=HelpCommandDistTest + ``` -to replace the expected output, then use a diff to ensure the changes look good. +2. Review the generated `.received.txt` files in the test directory and compare them with the `.approved.txt` files. + +3. If the changes look correct, rename the `.received.txt` files to `.approved.txt` to approve the new output: + ``` + # Example for a specific test + mv HelpCommandDistTest.testHelp.received.txt HelpCommandDistTest.testHelp.approved.txt + ``` + +Note: If the files match, the received file will be deleted automatically. You must include the `.approved.` files in source control. + +Alternatively, you can configure an [approval reporter](https://github.com/approvals/ApprovalTests.Java/blob/master/approvaltests/docs/reference/Reporters.md) to use a diff tool for easier comparison. 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 48c54f92a32..0a6ca9592d2 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 @@ -45,6 +45,8 @@ import org.keycloak.quarkus.runtime.cli.command.AbstractCommand; import org.keycloak.quarkus.runtime.cli.command.AbstractNonServerCommand; import org.keycloak.quarkus.runtime.cli.command.Build; import org.keycloak.quarkus.runtime.cli.command.Main; +import org.keycloak.quarkus.runtime.cli.command.Tools; +import org.keycloak.quarkus.runtime.cli.command.WindowsService; import org.keycloak.quarkus.runtime.configuration.ConfigArgsConfigSource; import org.keycloak.quarkus.runtime.configuration.Configuration; import org.keycloak.quarkus.runtime.configuration.DisabledMappersInterceptor; @@ -627,9 +629,27 @@ public class Picocli { cmd.getHelpSectionMap().put(SECTION_KEY_COMMAND_LIST, new SubCommandListRenderer()); cmd.setErr(getErrWriter()); cmd.setOut(getOutWriter()); + + removePlatformSpecificCommands(cmd); + return cmd; } + /** + * Removes platform-specific commands on non-applicable platforms + */ + private void removePlatformSpecificCommands(CommandLine cmd) { + if (!Environment.isWindows()) { + CommandLine toolsCmd = cmd.getSubcommands().get(Tools.NAME); + if (toolsCmd != null) { + CommandLine windowsServiceCmd = toolsCmd.getSubcommands().get(WindowsService.NAME); + if (windowsServiceCmd != null) { + toolsCmd.getCommandSpec().removeSubcommand(WindowsService.NAME); + } + } + } + } + public PrintWriter getErrWriter() { return new PrintWriter(System.err, true); } 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 7aa7c39f155..e01cb40a4d4 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 @@ -19,9 +19,9 @@ package org.keycloak.quarkus.runtime.cli.command; import picocli.CommandLine.Command; -@Command(name = "tools", +@Command(name = Tools.NAME, description = "Utilities for use and interaction with the server.", - subcommands = {Completion.class}) + subcommands = {Completion.class, WindowsService.class}) public class Tools { public static final String NAME = "tools"; diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/WindowsService.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/WindowsService.java new file mode 100644 index 00000000000..6df284dde8a --- /dev/null +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/WindowsService.java @@ -0,0 +1,29 @@ +/* + * Copyright 2025 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.command; + +import picocli.CommandLine.Command; + +@Command(name = WindowsService.NAME, + description = "Manage Keycloak as a Windows service.", + subcommands = {WindowsServiceInstall.class, WindowsServiceUninstall.class}) +public class WindowsService { + + public static final String NAME = "windows-service"; + +} diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/WindowsServiceInstall.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/WindowsServiceInstall.java new file mode 100644 index 00000000000..13a5222da4a --- /dev/null +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/WindowsServiceInstall.java @@ -0,0 +1,230 @@ +/* + * Copyright 2025 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.command; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import org.keycloak.config.LoggingOptions; +import org.keycloak.quarkus.runtime.Environment; +import org.keycloak.quarkus.runtime.configuration.Configuration; + +import picocli.CommandLine; +import picocli.CommandLine.Command; +import picocli.CommandLine.Option; + +@Command(name = WindowsServiceInstall.NAME, + header = "Install Keycloak as a Windows service.", + description = { + "%nInstall a Windows service that runs Keycloak using 'kc.bat start'.", + "", + "This command requires prunsrv.exe to be present in the bin directory.", + "Download it from https://downloads.apache.org/commons/daemon/binaries/windows/", + "", + "The service runs in exe mode, executing kc.bat as an external process.", + "This means all environment variables and configuration files are respected.", + "", + "For faster startup, run 'kc.bat build' before installing the service.", + "Without a pre-build, the first service start will be slower as it builds." + }, + footerHeading = "Examples:", + footer = { " Install with default settings:%n%n" + + " $ ${PARENT-COMMAND-FULL-NAME:-$PARENTCOMMAND} ${COMMAND-NAME}%n%n" + + " Install with custom service name:%n%n" + + " $ ${PARENT-COMMAND-FULL-NAME:-$PARENTCOMMAND} ${COMMAND-NAME} --name=my-keycloak%n%n" + + " Install with dependencies on other services:%n%n" + + " $ ${PARENT-COMMAND-FULL-NAME:-$PARENTCOMMAND} ${COMMAND-NAME} --depends-on=\"postgresql-x64-15;Tcpip\"%n"}) +public class WindowsServiceInstall extends AbstractCommand { + + public static final String NAME = "install"; + + public static final String SERVICE_PASSWORD_ENV = "KC_SERVICE_PASSWORD"; + + private static final String DEFAULT_SERVICE_NAME = "keycloak"; + private static final String DEFAULT_DISPLAY_NAME = "Keycloak Server"; + private static final String DEFAULT_DESCRIPTION = "Keycloak Identity and Access Management"; + + @Option(names = "--name", + description = "The name of the Windows service.", + defaultValue = DEFAULT_SERVICE_NAME) + String serviceName; + + @Option(names = "--display-name", + description = "The display name of the Windows service.", + defaultValue = DEFAULT_DISPLAY_NAME) + String displayName; + + @Option(names = "--description", + description = "The description of the Windows service.", + defaultValue = DEFAULT_DESCRIPTION) + String description; + + @Option(names = "--startup", + description = "Service startup mode: auto, manual.", + defaultValue = "auto") + String startupMode; + + @Option(names = "--service-user", + description = "The user account the service should run as. Defaults to LocalSystem.") + String serviceUser; + + @Option(names = "--service-password", + description = "The password for the service user account. Can also be set via the " + SERVICE_PASSWORD_ENV + " environment variable.") + String servicePassword; + + @Option(names = "--stop-timeout", + description = "Timeout in seconds to wait for service to stop gracefully.", + defaultValue = "30") + Integer stopTimeout; + + @Option(names = "--depends-on", + description = "Services that must start before this service. Separate multiple services with semicolons (e.g., \"postgresql-x64-15;Tcpip\").") + String dependsOn; + + @Override + public String getName() { + return NAME; + } + + @Override + public boolean isHelpAll() { + return false; + } + + @Override + protected void runCommand() { + if (!Environment.isWindows()) { + executionError(spec.commandLine(), "Windows service management is only available on Windows."); + } + + // Check for password from environment variable if not provided via command line + if (servicePassword == null || servicePassword.isEmpty()) { + servicePassword = System.getenv(SERVICE_PASSWORD_ENV); + } + + Path homePath = Environment.getHomePath().orElseThrow(() -> + new CommandLine.ExecutionException(spec.commandLine(), + "Could not determine Keycloak home directory")); + + Path prunsrvPath = homePath.resolve("bin").resolve("prunsrv.exe"); + if (!Files.exists(prunsrvPath)) { + picocli.println("Looking for prunsrv.exe in: " + prunsrvPath); + picocli.println("Download from https://downloads.apache.org/commons/daemon/binaries/windows/"); + executionError(spec.commandLine(), "Apache Commons Daemon (Procrun) executable not found at " + prunsrvPath); + } + + Path kcBatPath = homePath.resolve("bin").resolve("kc.bat"); + if (!Files.exists(kcBatPath)) { + executionError(spec.commandLine(), "kc.bat not found at " + kcBatPath); + } + + // If a custom log file location is set, the service wrapper logs are stored in the same directory + Path logPath; + Optional logFileOption = Configuration.getOptionalKcValue(LoggingOptions.LOG_FILE); + if (logFileOption.isPresent()) { + Path logFile = Path.of(logFileOption.get()); + if (!logFile.isAbsolute()) { + logFile = homePath.resolve(logFile); + } + logPath = logFile.getParent(); + } else { + logPath = homePath.resolve("data").resolve("log"); + } + + try { + Files.createDirectories(logPath); + } catch (IOException e) { + executionError(spec.commandLine(), "Failed to create log directory: " + logPath, e); + } + + picocli.println("Creating Keycloak Windows service '" + serviceName + "'..."); + picocli.println("Service will run: " + kcBatPath + " start"); + + List command = buildPrunsrvCommand(prunsrvPath, homePath, kcBatPath, logPath); + + try { + ProcessBuilder pb = new ProcessBuilder(command); + pb.inheritIO(); + Process process = pb.start(); + int exitCode = process.waitFor(); + + if (exitCode == 0) { + picocli.println("Service '" + serviceName + "' installed successfully."); + if (serviceUser == null) { + picocli.println("Service is configured to run as Local System account."); + } + picocli.println(""); + picocli.println("To start the service, run as Administrator:"); + picocli.println(" net start " + serviceName); + } else { + executionError(spec.commandLine(), + "Failed to install service '" + serviceName + "'. Exit code: " + exitCode); + } + } catch (IOException | InterruptedException e) { + executionError(spec.commandLine(), "Failed to execute prunsrv: " + e.getMessage(), e); + } + } + + private List buildPrunsrvCommand(Path prunsrvPath, Path homePath, Path kcBatPath, Path logPath) { + List cmd = new ArrayList<>(); + cmd.add(prunsrvPath.toString()); + cmd.add("install"); + cmd.add(serviceName); + cmd.add("--DisplayName=" + displayName); + cmd.add("--Description=" + description); + cmd.add("--Startup=" + startupMode); + + // Use exe mode to run kc.bat directly + cmd.add("--StartMode=exe"); + cmd.add("--StartPath=" + homePath); + cmd.add("--StartImage=" + kcBatPath); + cmd.add("--StartParams=start"); + + cmd.add("--StopMode=exe"); + cmd.add("--StopPath=" + homePath); + cmd.add("--StopImage=" + kcBatPath); + cmd.add("--StopParams=stop"); + cmd.add("--StopTimeout=" + stopTimeout); + + // Add service dependencies if specified + if (dependsOn != null && !dependsOn.isEmpty()) { + cmd.add("++DependsOn=" + dependsOn); + } + + cmd.add("--LogPath=" + logPath); + cmd.add("--LogLevel=Info"); + + // Configure service account + if (serviceUser != null && !serviceUser.isEmpty()) { + picocli.println("Configuring service to run as user: " + serviceUser); + cmd.add("--ServiceUser=" + serviceUser); + if (servicePassword != null && !servicePassword.isEmpty()) { + cmd.add("--ServicePassword=" + servicePassword); + } + } else { + picocli.println("Configuring service to run as Local System account"); + cmd.add("--ServiceUser=LocalSystem"); + } + + return cmd; + } +} diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/WindowsServiceUninstall.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/WindowsServiceUninstall.java new file mode 100644 index 00000000000..6d162b40570 --- /dev/null +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/WindowsServiceUninstall.java @@ -0,0 +1,105 @@ +/* + * Copyright 2025 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.command; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; + +import org.keycloak.quarkus.runtime.Environment; + +import picocli.CommandLine; +import picocli.CommandLine.Command; +import picocli.CommandLine.Option; + +@Command(name = WindowsServiceUninstall.NAME, + header = "Uninstall Keycloak Windows service.", + description = { + "%nUninstall Keycloak Windows service installed with 'kc.bat tools windows-service install'.", + "", + "This command requires prunsrv.exe to be present in the bin directory." + }, + footerHeading = "Examples:", + footer = { " Uninstall with default service name:%n%n" + + " $ ${PARENT-COMMAND-FULL-NAME:-$PARENTCOMMAND} ${COMMAND-NAME}%n%n" + + " Uninstall a custom-named service:%n%n" + + " $ ${PARENT-COMMAND-FULL-NAME:-$PARENTCOMMAND} ${COMMAND-NAME} --name=my-keycloak%n"}) +public class WindowsServiceUninstall extends AbstractCommand { + + public static final String NAME = "uninstall"; + + private static final String DEFAULT_SERVICE_NAME = "keycloak"; + + @Option(names = "--name", + description = "The name of the Windows service to uninstall.", + defaultValue = DEFAULT_SERVICE_NAME) + String serviceName; + + @Override + public String getName() { + return NAME; + } + + @Override + public boolean isHelpAll() { + return false; + } + + @Override + protected void runCommand() { + if (!Environment.isWindows()) { + executionError(spec.commandLine(), "Windows service management is only available on Windows."); + } + + Path homePath = Environment.getHomePath().orElseThrow(() -> + new CommandLine.ExecutionException(spec.commandLine(), + "Could not determine Keycloak home directory")); + + Path prunsrvPath = homePath.resolve("bin").resolve("prunsrv.exe"); + if (!Files.exists(prunsrvPath)) { + picocli.println("Looking for prunsrv.exe in: " + prunsrvPath); + picocli.println("Download from https://downloads.apache.org/commons/daemon/binaries/windows/"); + executionError(spec.commandLine(), "Apache Commons Daemon (Procrun) executable not found at " + prunsrvPath); + } + + picocli.println("Deleting Keycloak service '" + serviceName + "'..."); + + List command = new ArrayList<>(); + command.add(prunsrvPath.toString()); + command.add("delete"); + command.add(serviceName); + + try { + ProcessBuilder pb = new ProcessBuilder(command); + pb.inheritIO(); + Process process = pb.start(); + int exitCode = process.waitFor(); + + if (exitCode == 0) { + picocli.println("Service '" + serviceName + "' uninstalled successfully."); + } else { + executionError(spec.commandLine(), + "Failed to uninstall service '" + serviceName + "'. Exit code: " + exitCode); + } + } catch (IOException | InterruptedException e) { + executionError(spec.commandLine(), "Failed to execute prunsrv: " + e.getMessage(), e); + } + } +} diff --git a/quarkus/tests/integration/pom.xml b/quarkus/tests/integration/pom.xml index 10b6d0fb330..1e1a404e5ee 100644 --- a/quarkus/tests/integration/pom.xml +++ b/quarkus/tests/integration/pom.xml @@ -37,7 +37,7 @@ raw !invalid - 14.0.0 + 26.1.0 3.3.0 diff --git a/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/HelpCommandDistTest.java b/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/HelpCommandDistTest.java index 2b45b21905c..07670ce589f 100644 --- a/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/HelpCommandDistTest.java +++ b/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/HelpCommandDistTest.java @@ -17,16 +17,13 @@ package org.keycloak.it.cli.dist; -import java.io.IOException; -import java.nio.charset.StandardCharsets; import java.util.List; -import java.util.Locale; -import java.util.regex.Pattern; import org.keycloak.it.junit5.extension.CLIResult; import org.keycloak.it.junit5.extension.DistributionTest; import org.keycloak.it.junit5.extension.RawDistOnly; import org.keycloak.it.utils.KeycloakDistribution; +import org.keycloak.quarkus.runtime.Environment; import org.keycloak.quarkus.runtime.cli.command.BootstrapAdmin; import org.keycloak.quarkus.runtime.cli.command.BootstrapAdminService; import org.keycloak.quarkus.runtime.cli.command.BootstrapAdminUser; @@ -39,13 +36,14 @@ import org.keycloak.quarkus.runtime.cli.command.UpdateCompatibility; import org.keycloak.quarkus.runtime.cli.command.UpdateCompatibilityCheck; import org.keycloak.quarkus.runtime.cli.command.UpdateCompatibilityMetadata; +import com.spun.util.io.FileUtils; import io.quarkus.test.junit.main.Launch; -import org.apache.commons.io.FileUtils; import org.approvaltests.Approvals; +import org.approvaltests.core.Options; +import org.approvaltests.core.VerifyResult; import org.hamcrest.MatcherAssert; import org.hamcrest.Matchers; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.condition.OS; import static org.keycloak.quarkus.runtime.cli.command.AbstractAutoBuildCommand.OPTIMIZED_BUILD_OPTION_LONG; @@ -53,8 +51,6 @@ import static org.keycloak.quarkus.runtime.cli.command.AbstractAutoBuildCommand. @RawDistOnly(reason = "Verifying the help message output doesn't need long spin-up of docker dist tests.") public class HelpCommandDistTest { - public static final String REPLACE_EXPECTED = "KEYCLOAK_REPLACE_EXPECTED"; - @Test @Launch({}) void testDefaultToHelp(CLIResult cliResult) { @@ -193,7 +189,7 @@ public class HelpCommandDistTest { for (String cmd : List.of("", "start", "start-dev", "build")) { String debugOption = "--debug"; - if (OS.WINDOWS.isCurrentOs()) { + if (Environment.isWindows()) { debugOption = "--debug=8787"; } @@ -213,29 +209,32 @@ public class HelpCommandDistTest { .replaceAll("((Disables|Enables) a set of one or more features. Possible values are: )[^.]{30,}", "$1<...>") .replaceAll("(create a metric.\\s+Possible values are:)[^.]{30,}.[^.]*.", "$1<...>"); - String osName = System.getProperty("os.name"); - if(osName.toLowerCase(Locale.ROOT).contains("windows")) { - // On Windows, all output should have at least one "kc.bat" in it. + if (Environment.isWindows()) { MatcherAssert.assertThat(output, Matchers.containsString("kc.bat")); - output = output.replaceAll("kc.bat", "kc.sh"); - output = output.replaceAll(Pattern.quote("data\\log\\"), "data/log/"); - // line wrap which looks differently due to ".bat" vs. ".sh" - output = output.replaceAll("including\nbuild ", "including build\n"); + output = output + .replace("kc.bat", "kc.sh") + .replace("data\\log\\", "data/log/") + .replace("including\nbuild ", "including build\n"); } - try { - Approvals.verify(output); - } catch (Error cause) { - if ("true".equals(System.getenv(REPLACE_EXPECTED))) { - try { - FileUtils.write(Approvals.createApprovalNamer().getApprovedFile(".txt"), output, - StandardCharsets.UTF_8); - } catch (IOException e) { - throw new RuntimeException("Failed to assert help, and could not replace expected", cause); - } - } else { - throw cause; + // Custom comparator that strips Windows-specific lines from the approved file on non-Windows platforms + Options options = new Options().withComparator((receivedFile, approvedFile) -> { + String received = FileUtils.readFile(receivedFile); + String approved = FileUtils.readFile(approvedFile); + + if (!Environment.isWindows()) { + approved = stripWindowsServiceLines(approved); } - } + return VerifyResult.from(approved.equals(received)); + }); + + Approvals.verify(output, options); + } + + private String stripWindowsServiceLines(String text) { + return text + .replaceAll("(?m)^ {4}windows-service\\s+Manage Keycloak as a Windows service\\.\\R", "") + .replaceAll("(?m)^ {6}install\\s+Install Keycloak as a Windows service\\.\\R", "") + .replaceAll("(?m)^ {6}uninstall\\s+Uninstall Keycloak Windows service\\.\\R", ""); } } diff --git a/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/WindowsServiceDistTest.java b/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/WindowsServiceDistTest.java new file mode 100644 index 00000000000..6849ce21a1d --- /dev/null +++ b/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/WindowsServiceDistTest.java @@ -0,0 +1,314 @@ +/* + * Copyright 2025 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.it.cli.dist; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import org.keycloak.it.junit5.extension.CLIResult; +import org.keycloak.it.junit5.extension.DistributionTest; +import org.keycloak.it.junit5.extension.RawDistOnly; +import org.keycloak.it.utils.KeycloakDistribution; +import org.keycloak.it.utils.RawKeycloakDistribution; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledOnOs; +import org.junit.jupiter.api.condition.OS; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@EnabledOnOs(value = OS.WINDOWS, disabledReason = "Windows service tests are only applicable on Windows") +@DistributionTest +@RawDistOnly(reason = "Windows service management requires raw distribution") +@Tag(DistributionTest.WIN) +public class WindowsServiceDistTest { + + private static final String TEST_SERVICE_NAME_PREFIX = "keycloak-test-"; + private static final int SERVICE_START_TIMEOUT_SECONDS = 60; + private static final int SERVICE_STOP_TIMEOUT_SECONDS = 30; + + private RawKeycloakDistribution rawDist; + private Path distPath; + private String testServiceName; + private boolean serviceCreated = false; + private boolean prunsrvAvailable = false; + + @BeforeEach + void setUp(KeycloakDistribution dist) { + this.rawDist = dist.unwrap(RawKeycloakDistribution.class); + this.distPath = rawDist.getDistPath(); + this.testServiceName = TEST_SERVICE_NAME_PREFIX + System.currentTimeMillis(); + + // Check if prunsrv.exe is available in the distribution + Path prunsrvPath = distPath.resolve("bin").resolve("prunsrv.exe"); + if (!Files.exists(prunsrvPath)) { + String prunsrvSystemPath = findPrunsrvInSystem(); + if (prunsrvSystemPath != null) { + try { + Files.copy(Path.of(prunsrvSystemPath), prunsrvPath, StandardCopyOption.REPLACE_EXISTING); + prunsrvAvailable = true; + } catch (IOException e) { + System.err.println("Could not copy prunsrv.exe to distribution: " + e.getMessage()); + } + } + } else { + prunsrvAvailable = true; + } + } + + @AfterEach + void tearDown() { + if (serviceCreated) { + try { + stopService(); + } catch (Exception e) { + System.err.println("Failed to stop service during cleanup: " + e.getMessage()); + } + try { + deleteService(); + } catch (Exception e) { + System.err.println("Failed to delete service during cleanup: " + e.getMessage()); + } + } + } + + @Test + void testServiceLifecycle() throws Exception { + assertPrunsrvAvailable(); + assertAdminPrivileges(); + + String customDisplayName = "Keycloak Test Service " + testServiceName; + String customDescription = "Keycloak integration test service"; + + rawDist.setProperty("http-enabled", "true"); + rawDist.setProperty("hostname-strict", "false"); + rawDist.setProperty("log", "console,file"); + rawDist.setProperty("log-file", distPath.resolve("log").resolve("keycloak.log").toString()); + + // Install the service with custom name and display name + CLIResult installResult = rawDist.run("tools", "windows-service", "install", + "--name=" + testServiceName, + "--display-name=" + customDisplayName, + "--description=" + customDescription, + "--startup=manual"); + + assertEquals(0, installResult.exitCode(), "Service installation failed: " + installResult.getOutput()); + assertThat(installResult.getOutput(), containsString("installed successfully")); + serviceCreated = true; + assertTrue(isServiceCreated(testServiceName), "Service should be installed"); + + // Verify the display name in service configuration + String serviceInfo = getServiceInfo(testServiceName); + assertThat("Service info should contain custom display name", serviceInfo, containsString(customDisplayName)); + + // Test service start + assertTrue(startService(), "Service should start successfully"); + assertTrue(waitForKeycloakReady(), "Keycloak should be accessible after service start"); + assertEquals("RUNNING", getServiceState(testServiceName), "Service should be in RUNNING state"); + + // Verify log file was created and contains startup message + Path logFile = distPath.resolve("log").resolve("keycloak.log"); + assertTrue(waitForLogFile(logFile), "Log file should be created"); + String logContent = Files.readString(logFile); + assertThat("Log should contain Keycloak startup message", logContent, containsString("Listening on:")); + + // Test service stop + assertTrue(stopService(), "Service should stop successfully"); + assertTrue(waitForServiceStopped(), "Service should be in STOPPED state"); + assertFalse(isKeycloakAccessible(), "Keycloak should not be accessible after service stop"); + + // Test service uninstall + CLIResult uninstallResult = rawDist.run("tools", "windows-service", "uninstall", "--name=" + testServiceName); + assertEquals(0, uninstallResult.exitCode(), "Service uninstallation failed: " + uninstallResult.getOutput()); + assertThat(uninstallResult.getOutput(), containsString("uninstalled successfully")); + serviceCreated = false; + assertFalse(isServiceCreated(testServiceName), "Service should be uninstalled"); + } + + private boolean waitForLogFile(Path logFile) { + try { + org.awaitility.Awaitility.await() + .atMost(SERVICE_START_TIMEOUT_SECONDS, TimeUnit.SECONDS) + .pollInterval(2, TimeUnit.SECONDS) + .until(() -> Files.exists(logFile) && Files.size(logFile) > 0); + return true; + } catch (org.awaitility.core.ConditionTimeoutException e) { + return false; + } + } + + private boolean waitForKeycloakReady() { + try { + org.awaitility.Awaitility.await() + .atMost(SERVICE_START_TIMEOUT_SECONDS, TimeUnit.SECONDS) + .pollInterval(2, TimeUnit.SECONDS) + .until(this::isKeycloakAccessible); + return true; + } catch (org.awaitility.core.ConditionTimeoutException e) { + return false; + } + } + + private void assertPrunsrvAvailable() { + assertTrue(prunsrvAvailable, "prunsrv.exe not available. Download from https://downloads.apache.org/commons/daemon/binaries/windows/"); + } + + private void assertAdminPrivileges() { + try { + ProcessBuilder pb = new ProcessBuilder("net", "session"); + pb.redirectErrorStream(true); + Process process = pb.start(); + // Consume output to prevent blocking + process.getInputStream().transferTo(java.io.OutputStream.nullOutputStream()); + int exitCode = process.waitFor(); + assertEquals(0, exitCode, "Administrator privileges required to run Windows service tests. Run tests from an elevated terminal or IDE."); + } catch (Exception e) { + throw new AssertionError("Could not verify admin privileges: " + e.getMessage(), e); + } + } + + private String findPrunsrvInSystem() { + List possiblePaths = new ArrayList<>(); + + String prunsrvHome = System.getenv("PRUNSRV_HOME"); + if (prunsrvHome != null) { + possiblePaths.add(prunsrvHome + "\\prunsrv.exe"); + } + + String commonsDaemonHome = System.getenv("COMMONS_DAEMON_HOME"); + if (commonsDaemonHome != null) { + possiblePaths.add(commonsDaemonHome + "\\prunsrv.exe"); + } + + possiblePaths.add("C:\\Program Files\\Apache\\commons-daemon\\prunsrv.exe"); + + return possiblePaths.stream() + .filter(path -> Files.exists(Path.of(path))) + .findFirst() + .orElse(null); + } + + private boolean startService() throws IOException, InterruptedException { + ProcessBuilder pb = new ProcessBuilder("net", "start", testServiceName); + pb.redirectErrorStream(true); + Process process = pb.start(); + String output = new String(process.getInputStream().readAllBytes()); + int exitCode = process.waitFor(); + if (exitCode != 0) { + System.err.println("Service start failed with exit code " + exitCode + ": " + output); + } + return exitCode == 0; + } + + private boolean stopService() throws IOException, InterruptedException { + ProcessBuilder pb = new ProcessBuilder("net", "stop", testServiceName); + Process process = pb.start(); + process.waitFor(SERVICE_STOP_TIMEOUT_SECONDS, TimeUnit.SECONDS); + return true; + } + + private void deleteService() { + rawDist.run("tools", "windows-service", "uninstall", "--name=" + testServiceName); + } + + private boolean isServiceCreated(String serviceName) { + try { + ProcessBuilder pb = new ProcessBuilder("sc", "query", serviceName); + Process process = pb.start(); + int exitCode = process.waitFor(); + return exitCode == 0; + } catch (Exception e) { + return false; + } + } + + private String getServiceState(String serviceName) { + try { + ProcessBuilder pb = new ProcessBuilder("sc", "query", serviceName); + Process process = pb.start(); + + try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { + String line; + while ((line = reader.readLine()) != null) { + if (line.contains("STATE")) { + if (line.contains("RUNNING")) return "RUNNING"; + if (line.contains("STOPPED")) return "STOPPED"; + if (line.contains("PAUSED")) return "PAUSED"; + if (line.contains("START_PENDING")) return "START_PENDING"; + if (line.contains("STOP_PENDING")) return "STOP_PENDING"; + } + } + } + process.waitFor(); + } catch (Exception e) { + // ignore + } + return "UNKNOWN"; + } + + private String getServiceInfo(String serviceName) { + try { + ProcessBuilder pb = new ProcessBuilder("sc", "qc", serviceName); + pb.redirectErrorStream(true); + Process process = pb.start(); + String output = new String(process.getInputStream().readAllBytes()); + process.waitFor(); + return output; + } catch (Exception e) { + return ""; + } + } + + private boolean waitForServiceStopped() { + try { + org.awaitility.Awaitility.await() + .atMost(SERVICE_STOP_TIMEOUT_SECONDS, TimeUnit.SECONDS) + .pollInterval(1, TimeUnit.SECONDS) + .until(() -> "STOPPED".equals(getServiceState(testServiceName))); + return true; + } catch (org.awaitility.core.ConditionTimeoutException e) { + return false; + } + } + + private boolean isKeycloakAccessible() { + try { + return given() + .when() + .get("http://localhost:8080/realms/master/") + .getStatusCode() == 200; + } catch (Exception e) { + return false; + } + } +} diff --git a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testDefaultToHelp.approved.txt b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testDefaultToHelp.approved.txt index fbcfc440d4e..d8f0bfeecb2 100644 --- a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testDefaultToHelp.approved.txt +++ b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testDefaultToHelp.approved.txt @@ -27,6 +27,9 @@ Commands: show-config Print out the current configuration. tools Utilities for use and interaction with the server. completion Generate bash/zsh completion script for kc.sh. + windows-service Manage Keycloak as a Windows service. + install Install Keycloak as a Windows service. + uninstall Uninstall Keycloak Windows service. bootstrap-admin Commands for bootstrapping admin access user Add an admin user with a password service Add an admin service account @@ -60,4 +63,4 @@ Examples: production. Use "kc.sh start --help" for the available options when starting the server. -Use "kc.sh --help" for more information about other commands. +Use "kc.sh --help" for more information about other commands. \ No newline at end of file diff --git a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testHelp.approved.txt b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testHelp.approved.txt index fbcfc440d4e..d8f0bfeecb2 100644 --- a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testHelp.approved.txt +++ b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testHelp.approved.txt @@ -27,6 +27,9 @@ Commands: show-config Print out the current configuration. tools Utilities for use and interaction with the server. completion Generate bash/zsh completion script for kc.sh. + windows-service Manage Keycloak as a Windows service. + install Install Keycloak as a Windows service. + uninstall Uninstall Keycloak Windows service. bootstrap-admin Commands for bootstrapping admin access user Add an admin user with a password service Add an admin service account @@ -60,4 +63,4 @@ Examples: production. Use "kc.sh start --help" for the available options when starting the server. -Use "kc.sh --help" for more information about other commands. +Use "kc.sh --help" for more information about other commands. \ No newline at end of file diff --git a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testHelpShort.approved.txt b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testHelpShort.approved.txt index fbcfc440d4e..d8f0bfeecb2 100644 --- a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testHelpShort.approved.txt +++ b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testHelpShort.approved.txt @@ -27,6 +27,9 @@ Commands: show-config Print out the current configuration. tools Utilities for use and interaction with the server. completion Generate bash/zsh completion script for kc.sh. + windows-service Manage Keycloak as a Windows service. + install Install Keycloak as a Windows service. + uninstall Uninstall Keycloak Windows service. bootstrap-admin Commands for bootstrapping admin access user Add an admin user with a password service Add an admin service account @@ -60,4 +63,4 @@ Examples: production. Use "kc.sh start --help" for the available options when starting the server. -Use "kc.sh --help" for more information about other commands. +Use "kc.sh --help" for more information about other commands. \ No newline at end of file diff --git a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testUpdateCompatibilityHelp.approved.txt b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testUpdateCompatibilityHelp.approved.txt index b9a116ebd40..ab5849d439a 100644 --- a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testUpdateCompatibilityHelp.approved.txt +++ b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testUpdateCompatibilityHelp.approved.txt @@ -15,4 +15,4 @@ Commands: configuration. A zero exit code means a rolling update is possible between old and the current metadata. metadata Stores the metadata necessary to determine if a configuration is - compatible. + compatible. \ No newline at end of file diff --git a/quarkus/tests/junit5/pom.xml b/quarkus/tests/junit5/pom.xml index 4f702145ac3..4446135b1f6 100644 --- a/quarkus/tests/junit5/pom.xml +++ b/quarkus/tests/junit5/pom.xml @@ -36,7 +36,7 @@ raw - 14.0.0 + 26.1.0 3.3.0 1.9.10