updates for quarkus:dev mode (#47848)

* fix: ensuring dev mode can fully reload the application

closes: #47077

Signed-off-by: Steve Hawkins <shawkins@redhat.com>

* also removing test launch mode

Signed-off-by: Steve Hawkins <shawkins@redhat.com>

* refining embedded mode cleanup

Signed-off-by: Steve Hawkins <shawkins@redhat.com>
# Conflicts:
#	quarkus/tests/junit5/src/main/java/org/keycloak/Keycloak.java

* restoring test launch mode

Signed-off-by: Steve Hawkins <shawkins@redhat.com>

---------

Signed-off-by: Steve Hawkins <shawkins@redhat.com>
This commit is contained in:
Steven Hawkins
2026-05-02 11:38:48 -04:00
committed by GitHub
parent e45bd9d6af
commit 98366ba559
9 changed files with 39 additions and 76 deletions
@@ -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.
* <br>At build time, we build as prod or dev.
*/
public static boolean isNonServerMode() {
return NON_SERVER_MODE.equalsIgnoreCase(Environment.getProfile());
}
+2 -4
View File
@@ -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
@@ -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();
}
}
@@ -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<StaticInitConfigBuilderBuildItem> 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<String, String> configProperties = dbConfig.getDefaultDatasource().getConfigProperties();
for (Entry<String, String> 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());
}
}
}
/**
* <p>Make the build time configuration available at runtime so that the server can run without having to specify some of
* the properties again.
@@ -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() {
@@ -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) {
@@ -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
@@ -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<Dependency> 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);
@@ -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;