From 98366ba55993a1900b62922aa15906d8a51817f2 Mon Sep 17 00:00:00 2001 From: Steven Hawkins Date: Sat, 2 May 2026 11:38:48 -0400 Subject: [PATCH] updates for quarkus:dev mode (#47848) * fix: ensuring dev mode can fully reload the application closes: #47077 Signed-off-by: Steve Hawkins * also removing test launch mode Signed-off-by: Steve Hawkins * refining embedded mode cleanup Signed-off-by: Steve Hawkins # Conflicts: # quarkus/tests/junit5/src/main/java/org/keycloak/Keycloak.java * restoring test launch mode Signed-off-by: Steve Hawkins --------- Signed-off-by: Steve Hawkins --- .../org/keycloak/common/util/Environment.java | 8 +++++ quarkus/README.md | 6 ++-- .../quarkus/deployment/IsIntegrationTest.java | 19 ----------- .../quarkus/deployment/KeycloakProcessor.java | 33 +------------------ .../keycloak/quarkus/runtime/Environment.java | 5 ++- .../quarkus/runtime/KeycloakMain.java | 17 +++++++--- .../quarkus/runtime/cli/PicocliTest.java | 2 ++ .../src/main/java/org/keycloak/Keycloak.java | 9 +++++ .../it/junit5/extension/CLITestExtension.java | 16 --------- 9 files changed, 39 insertions(+), 76 deletions(-) delete mode 100644 quarkus/deployment/src/main/java/org/keycloak/quarkus/deployment/IsIntegrationTest.java diff --git a/common/src/main/java/org/keycloak/common/util/Environment.java b/common/src/main/java/org/keycloak/common/util/Environment.java index ae20ac3c007..3fdb02128ae 100644 --- a/common/src/main/java/org/keycloak/common/util/Environment.java +++ b/common/src/main/java/org/keycloak/common/util/Environment.java @@ -66,6 +66,10 @@ public class Environment { return false; } + /** + * Check if dev mode - note that non server commands may switch to non-server mode + * at runtime. See {@link #isNonServerMode()} + */ public static boolean isDevMode() { return DEV_PROFILE_VALUE.equalsIgnoreCase(getProfile()); } @@ -80,6 +84,10 @@ public class Environment { return System.getenv(ENV_PROFILE); } + /** + * Check if running in non-server mode - valid only as a runtime check. + *
At build time, we build as prod or dev. + */ public static boolean isNonServerMode() { return NON_SERVER_MODE.equalsIgnoreCase(Environment.getProfile()); } diff --git a/quarkus/README.md b/quarkus/README.md index d9a3188922c..bca0613a60e 100644 --- a/quarkus/README.md +++ b/quarkus/README.md @@ -165,14 +165,12 @@ For debugging the build steps right after start, you can suspend the JVM by runn **Expected behavior:** The server will start on **http://localhost:8080**. -When running using `quarkus:dev` you are able to do live coding whenever you change / add code in the `server` module, for example when creating a new custom provider. - There are currently limitations when running in development mode that block us to use all capabilities the Quarkus development mode has to offer. The main limitations you'll find at the moment are: -* Changes are only automatically reflected at runtime if you are changing resources from the `deployment`, `runtime`, and `server` modules. Other modules, such as `keycloak-services` still rely on Hot Swap in Java debuggers to reload classes. +* Changes are only automatically reflected at runtime if you are changing resources from the quarkus `deployment`, `runtime`, and `server` modules. Non-quarkus modules, such as `keycloak-services`, still rely on Hot Swap in Java debuggers to reload classes. * There is nothing in the Dev UI related to the server itself, although you can still change some configuration from there. -* There are some limitations when passing some options when running in dev mode. You should expect more improvements in this area. +* quarkus.args does not honor build time options. Those instead must also be set via -D arguments, or environment variables, and thus require rerunning the quarkus:dev command to change. ### Debugging the server distribution diff --git a/quarkus/deployment/src/main/java/org/keycloak/quarkus/deployment/IsIntegrationTest.java b/quarkus/deployment/src/main/java/org/keycloak/quarkus/deployment/IsIntegrationTest.java deleted file mode 100644 index d2fea70ac18..00000000000 --- a/quarkus/deployment/src/main/java/org/keycloak/quarkus/deployment/IsIntegrationTest.java +++ /dev/null @@ -1,19 +0,0 @@ -package org.keycloak.quarkus.deployment; - -import org.keycloak.quarkus.runtime.Environment; - -import io.quarkus.deployment.IsTest; -import io.quarkus.runtime.LaunchMode; - -public class IsIntegrationTest extends IsTest { - - public IsIntegrationTest(LaunchMode launchMode) { - super(launchMode); - } - - @Override - public boolean getAsBoolean() { - return super.getAsBoolean() && Environment.isTestLaunchMode(); - } - -} diff --git a/quarkus/deployment/src/main/java/org/keycloak/quarkus/deployment/KeycloakProcessor.java b/quarkus/deployment/src/main/java/org/keycloak/quarkus/deployment/KeycloakProcessor.java index 22569227761..135408b44d6 100644 --- a/quarkus/deployment/src/main/java/org/keycloak/quarkus/deployment/KeycloakProcessor.java +++ b/quarkus/deployment/src/main/java/org/keycloak/quarkus/deployment/KeycloakProcessor.java @@ -91,7 +91,6 @@ import org.keycloak.quarkus.runtime.configuration.KeycloakConfigSourceProvider; import org.keycloak.quarkus.runtime.configuration.MicroProfileConfigProvider; import org.keycloak.quarkus.runtime.configuration.PersistedConfigSource; import org.keycloak.quarkus.runtime.configuration.PropertyMappingInterceptor; -import org.keycloak.quarkus.runtime.configuration.mappers.PropertyMapper; import org.keycloak.quarkus.runtime.configuration.mappers.PropertyMappers; import org.keycloak.quarkus.runtime.configuration.mappers.WildcardPropertyMapper; import org.keycloak.quarkus.runtime.integration.QuarkusKeycloakSessionFactory; @@ -130,7 +129,6 @@ import io.quarkus.arc.deployment.AnnotationsTransformerBuildItem; import io.quarkus.arc.deployment.BuildTimeConditionBuildItem; import io.quarkus.arc.deployment.SyntheticBeanBuildItem; import io.quarkus.bootstrap.logging.InitialConfigurator; -import io.quarkus.datasource.deployment.spi.DevServicesDatasourceResultBuildItem; import io.quarkus.datasource.runtime.DataSourcesBuildTimeConfig; import io.quarkus.deployment.IsDevelopment; import io.quarkus.deployment.annotations.BuildProducer; @@ -749,40 +747,11 @@ class KeycloakProcessor { * * @param configSources */ - @BuildStep(onlyIfNot = IsIntegrationTest.class ) + @BuildStep void configureConfigSources(BuildProducer configSources) { configSources.produce(new StaticInitConfigBuilderBuildItem(KeycloakConfigSourceProvider.class.getName())); } - @BuildStep(onlyIf = IsIntegrationTest.class) - void prepareTestEnvironment(BuildProducer< StaticInitConfigBuilderBuildItem> configSources, DevServicesDatasourceResultBuildItem dbConfig) { - configSources.produce(new StaticInitConfigBuilderBuildItem("org.keycloak.quarkus.runtime.configuration.test.TestKeycloakConfigSourceProvider")); - - // we do not enable dev services by default and the DevServicesDatasourceResultBuildItem might not be available when discovering build steps - // Quarkus seems to allow that when the DevServicesDatasourceResultBuildItem is not the only parameter to the build step - // this might be too sensitive and break if Quarkus changes the behavior - if (dbConfig != null && dbConfig.getDefaultDatasource() != null) { - Map configProperties = dbConfig.getDefaultDatasource().getConfigProperties(); - - for (Entry dbConfigProperty : configProperties.entrySet()) { - PropertyMapper mapper = PropertyMappers.getMapper(dbConfigProperty.getKey()); - - if (mapper == null) { - continue; - } - - String kcProperty = mapper.getFrom(); - - if (kcProperty.endsWith("db")) { - // db kind set when running tests - continue; - } - - System.setProperty(kcProperty, dbConfigProperty.getValue()); - } - } - } - /** *

Make the build time configuration available at runtime so that the server can run without having to specify some of * the properties again. diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/Environment.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/Environment.java index ede7477f5db..438022205fe 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/Environment.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/Environment.java @@ -91,6 +91,9 @@ public final class Environment { } } + /** + * Check if the we're currently in or built as dev mode. + */ public static boolean isDevMode() { if (org.keycloak.common.util.Environment.isDevMode()) { return true; @@ -100,7 +103,7 @@ public final class Environment { } public static boolean isDevProfile(){ - return Optional.ofNullable(org.keycloak.common.util.Environment.getProfile()).orElse("").equalsIgnoreCase(org.keycloak.common.util.Environment.DEV_PROFILE_VALUE); + return org.keycloak.common.util.Environment.isDevMode(); } public static boolean isWindows() { diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/KeycloakMain.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/KeycloakMain.java index 8e434ac3343..7ab1fe3730f 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/KeycloakMain.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/KeycloakMain.java @@ -66,11 +66,11 @@ public class KeycloakMain implements QuarkusApplication { ensureForkJoinPoolThreadFactoryHasBeenSetToQuarkus(); InfinispanUtils.ensureVirtualThreadsParallelism(); - System.setProperty("kc.version", Version.VERSION); - Picocli picocli; + Properties clonedProps = null; if (!(Thread.currentThread().getContextClassLoader() instanceof RunnerClassLoader)) { - picocli = new Picocli() { // embedded launch case, avoid System.exit + clonedProps = (Properties) System.getProperties().clone(); + picocli = new Picocli() { // non-script launch case, avoid System.exit @Override public void exit(int exitCode) { Quarkus.asyncExit(exitCode); @@ -79,7 +79,16 @@ public class KeycloakMain implements QuarkusApplication { } else { picocli = new Picocli(); } - main(args, picocli); + + System.setProperty("kc.version", Version.VERSION); + + try { + main(args, picocli); + } finally { + if (clonedProps != null) { + reset(clonedProps); + } + } } public static void reset(Properties systemProperties) { diff --git a/quarkus/runtime/src/test/java/org/keycloak/quarkus/runtime/cli/PicocliTest.java b/quarkus/runtime/src/test/java/org/keycloak/quarkus/runtime/cli/PicocliTest.java index 11f431acb82..d911d0ad367 100644 --- a/quarkus/runtime/src/test/java/org/keycloak/quarkus/runtime/cli/PicocliTest.java +++ b/quarkus/runtime/src/test/java/org/keycloak/quarkus/runtime/cli/PicocliTest.java @@ -157,6 +157,8 @@ public class PicocliTest extends AbstractConfigurationTest { NonRunningPicocli nonRunningPicocli = pseudoLaunch("start-dev"); assertFalse(nonRunningPicocli.getOutString(), nonRunningPicocli.getOutString().toUpperCase().contains("WARN")); assertFalse(nonRunningPicocli.getOutString(), nonRunningPicocli.getOutString().toUpperCase().contains("ERROR")); + onAfter(); + assertFalse(Environment.isDevProfile()); } @Test diff --git a/quarkus/tests/junit5/src/main/java/org/keycloak/Keycloak.java b/quarkus/tests/junit5/src/main/java/org/keycloak/Keycloak.java index 4fe820a0629..63c9f29d49f 100644 --- a/quarkus/tests/junit5/src/main/java/org/keycloak/Keycloak.java +++ b/quarkus/tests/junit5/src/main/java/org/keycloak/Keycloak.java @@ -23,6 +23,7 @@ import java.nio.file.Path; import java.util.ArrayList; import java.util.List; import java.util.Properties; +import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; @@ -178,6 +179,7 @@ public class Keycloak { private List dependencies; private boolean fipsEnabled; private Properties systemProperties; + private CountDownLatch closed; public Keycloak() { this(null, Version.VERSION, List.of(), false); @@ -212,6 +214,8 @@ public class Keycloak { return this; } StartupAction startupAction = action.createInitialRuntimeApplication(); + closed = new CountDownLatch(1); + startupAction.addRuntimeCloseTask(closed::countDown); application = startupAction.runMainClass(args.toArray(new String[0])); return this; @@ -304,6 +308,11 @@ public class Keycloak { } catch (Exception cause) { cause.printStackTrace(); } + try { + closed.await(); // wait for an orderly completion of all cleanup in the other thread + } catch (InterruptedException e) { + e.printStackTrace(); + } } QuarkusConfigFactory.setConfig(null); diff --git a/quarkus/tests/junit5/src/main/java/org/keycloak/it/junit5/extension/CLITestExtension.java b/quarkus/tests/junit5/src/main/java/org/keycloak/it/junit5/extension/CLITestExtension.java index ad5e122d76c..4460ba934fe 100644 --- a/quarkus/tests/junit5/src/main/java/org/keycloak/it/junit5/extension/CLITestExtension.java +++ b/quarkus/tests/junit5/src/main/java/org/keycloak/it/junit5/extension/CLITestExtension.java @@ -22,7 +22,6 @@ import java.lang.annotation.Annotation; import java.lang.reflect.Method; import java.util.Arrays; import java.util.Collections; -import java.util.HashMap; import java.util.List; import java.util.stream.Stream; @@ -34,7 +33,6 @@ import org.keycloak.quarkus.runtime.Environment; import org.keycloak.quarkus.runtime.cli.command.DryRunMixin; import org.keycloak.quarkus.runtime.cli.command.Start; import org.keycloak.quarkus.runtime.cli.command.StartDev; -import org.keycloak.quarkus.runtime.configuration.Configuration; import io.quarkus.deployment.util.FileUtil; import io.quarkus.runtime.configuration.QuarkusConfigFactory; @@ -42,8 +40,6 @@ import io.quarkus.test.junit.QuarkusMainTestExtension; import io.quarkus.test.junit.main.Launch; import io.quarkus.test.junit.main.LaunchResult; import org.junit.jupiter.api.extension.ExtensionContext; -import org.junit.jupiter.api.extension.ExtensionContext.Namespace; -import org.junit.jupiter.api.extension.ExtensionContext.Store; import org.junit.jupiter.api.extension.ParameterContext; import org.junit.jupiter.api.extension.ParameterResolutionException; import org.junit.jupiter.api.extension.ReflectiveInvocationContext; @@ -56,7 +52,6 @@ import static org.keycloak.quarkus.runtime.Environment.forceTestLaunchMode; public class CLITestExtension extends QuarkusMainTestExtension { - private static final String SYS_PROPS = "sys-props"; private KeycloakDistribution dist; private DatabaseContainer databaseContainer; private InfinispanContainer infinispanContainer; @@ -66,7 +61,6 @@ public class CLITestExtension extends QuarkusMainTestExtension { public void beforeEach(ExtensionContext context) throws Exception { DistributionTest distConfig = getDistributionConfig(context); Launch launch = context.getRequiredTestMethod().getAnnotation(Launch.class); - getStore(context).put(SYS_PROPS, new HashMap<>(System.getProperties())); if (launch != null && distConfig == null) { Stream.of(launch.value()).forEach(arg -> { @@ -117,10 +111,6 @@ public class CLITestExtension extends QuarkusMainTestExtension { } } - private Store getStore(ExtensionContext context) { - return context.getStore(Namespace.create(context.getRequiredTestClass(), context.getRequiredTestMethod())); - } - private static Storage getStoreConfig(ExtensionContext context) { return context.getTestClass().get().getDeclaredAnnotation(Storage.class); } @@ -193,12 +183,6 @@ public class CLITestExtension extends QuarkusMainTestExtension { private void reset(DistributionTest distConfig, ExtensionContext context) { QuarkusConfigFactory.setConfig(null); - HashMap props = getStore(context).remove(SYS_PROPS, HashMap.class); - System.getProperties().clear(); - System.getProperties().putAll(props); - // TODO: for in-vm tests this is not all that it takes to reset static state - // may want to call AbstractConfigurationTest.resetConfiguration - Configuration.resetConfig(); if (databaseContainer != null && databaseContainer.isRunning()) { databaseContainer.stop(); databaseContainer = null;