> getCache() {
+ return cache;
+ }
+
+ /**
+ * Imports a session from an external source into the {@link Cache}.
+ *
+ * If a session already exists in the cache, this method does not insert the {@code session}. The invoker should use
+ * the session returned by this method invocation. When the session is successfully imported, this method returns
+ * null and the {@code session} can be used by the transaction.
+ *
+ * This transaction will keep track of further changes in the session.
+ *
+ * @param realmModel The {@link RealmModel} where the session belong to.
+ * @param key The cache's key.
+ * @param session The session to import.
+ * @param lifespan How long the session stays cached until it is expired and removed.
+ * @param maxIdle How long the session can be idle (without reading or writing) before being removed.
+ * @return The existing cached session. If it returns {@code null}, it means the {@code session} used in the
+ * parameters was cached.
+ */
+ public V importSession(RealmModel realmModel, K key, SessionEntityWrapper session, long lifespan, long maxIdle) {
+ SessionUpdatesList updatesList = updates.get(key);
+ if (updatesList != null) {
+ // exists in transaction, avoid cache operation
+ return updatesList.getEntityWrapper().getEntity();
+ }
+ SessionEntityWrapper existing = cache.putIfAbsent(key, session, lifespan, TimeUnit.MILLISECONDS, maxIdle, TimeUnit.MILLISECONDS);
+ if (existing == null) {
+ // keep track of the imported session for updates
+ updates.put(key, new SessionUpdatesList<>(realmModel, session));
+ return null;
+ }
+ updates.put(key, new SessionUpdatesList<>(realmModel, existing));
+ return existing.getEntity();
+ }
+
+ /**
+ * Imports multiple sessions from an external source into the {@link Cache}.
+ *
+ * If the {@code lifespanFunction} or {@code maxIdleFunction} returns {@link SessionTimeouts#ENTRY_EXPIRED_FLAG},
+ * the session is considered expired and not stored in the cache.
+ *
+ * Also, if one or more sessions already exist in the {@link Cache}, it will not be imported.
+ *
+ * This transaction will keep track of further changes in the sessions.
+ *
+ * @param realmModel The {@link RealmModel} where the sessions belong to.
+ * @param sessions The {@link Map} with the cache's key/session mapping to be imported.
+ * @param lifespanFunction The {@link java.util.function.Function} to compute the lifespan of the session. It
+ * defines how long the session should be stored in the cache until it is removed.
+ * @param maxIdleFunction The {@link java.util.function.Function} to compute the max-idle of the session. It
+ * defines how long the session will be idle before it is removed.
+ */
+ public void importSessionsConcurrently(RealmModel realmModel, Map> sessions, SessionFunction lifespanFunction, SessionFunction maxIdleFunction) {
+ if (sessions.isEmpty()) {
+ //nothing to import
+ return;
+ }
+ var stage = CompletionStages.aggregateCompletionStage();
+ var allSessions = new ConcurrentHashMap>();
+ sessions.forEach((key, session) -> {
+ if (updates.containsKey(key)) {
+ //nothing to import, already exists in transaction
+ return;
+ }
+ var clientModel = session.getClientIfNeeded(realmModel);
+ var sessionEntity = session.getEntity();
+ var lifespan = lifespanFunction.apply(realmModel, clientModel, sessionEntity);
+ var maxIdle = maxIdleFunction.apply(realmModel, clientModel, sessionEntity);
+ if (lifespan == SessionTimeouts.ENTRY_EXPIRED_FLAG || maxIdle == SessionTimeouts.ENTRY_EXPIRED_FLAG) {
+ //nothing to import, already expired
+ return;
+ }
+ var future = cache.putIfAbsentAsync(key, session, lifespan, TimeUnit.MILLISECONDS, maxIdle, TimeUnit.MILLISECONDS);
+ // write result into concurrent hash map because the consumer is invoked in a different thread each time.
+ stage.dependsOn(future.thenAccept(existing -> allSessions.put(key, existing == null ? session : existing)));
+ });
+
+ CompletionStages.join(stage.freeze());
+ allSessions.forEach((key, wrapper) -> updates.put(key, new SessionUpdatesList<>(realmModel, wrapper)));
+ }
+
+ private static SessionEntityWrapper generateNewVersionAndWrapEntity(V entity, Map localMetadata) {
return new SessionEntityWrapper<>(localMetadata, entity);
}
diff --git a/model/infinispan/src/main/java/org/keycloak/spi/infinispan/impl/embedded/CacheConfigurator.java b/model/infinispan/src/main/java/org/keycloak/spi/infinispan/impl/embedded/CacheConfigurator.java
index 311dc96ae29c..f65eb848d252 100644
--- a/model/infinispan/src/main/java/org/keycloak/spi/infinispan/impl/embedded/CacheConfigurator.java
+++ b/model/infinispan/src/main/java/org/keycloak/spi/infinispan/impl/embedded/CacheConfigurator.java
@@ -216,7 +216,7 @@ public static void configureSessionsCachesForPersistentSessions(ConfigurationBui
*/
public static void configureSessionsCachesForVolatileSessions(ConfigurationBuilderHolder holder) {
logger.debug("Configuring session cache (volatile user sessions)");
- for (var name : Arrays.asList(USER_SESSION_CACHE_NAME, CLIENT_SESSION_CACHE_NAME, OFFLINE_USER_SESSION_CACHE_NAME, OFFLINE_CLIENT_SESSION_CACHE_NAME)) {
+ for (var name : Arrays.asList(USER_SESSION_CACHE_NAME, CLIENT_SESSION_CACHE_NAME)) {
var builder = holder.getNamedConfigurationBuilders().get(name);
if (builder == null) {
throw cacheNotFound(name);
@@ -232,6 +232,23 @@ public static void configureSessionsCachesForVolatileSessions(ConfigurationBuild
builder.clustering().hash().numOwners(2);
}
}
+
+ for (var name : Arrays.asList( OFFLINE_USER_SESSION_CACHE_NAME, OFFLINE_CLIENT_SESSION_CACHE_NAME)) {
+ var builder = holder.getNamedConfigurationBuilders().get(name);
+ if (builder == null) {
+ throw cacheNotFound(name);
+ }
+ if (builder.memory().maxCount() == -1) {
+ logger.infof("Offline sessions should have a max count set to avoid excessive memory usage. Setting a default cache limit of 10000 for cache %s.", name);
+ builder.memory().maxCount(10000);
+ }
+ if (builder.clustering().hash().attributes().attribute(HashConfiguration.NUM_OWNERS).get() != 1 &&
+ builder.persistence().stores().stream().noneMatch(p -> p.attributes().attribute(AbstractStoreConfiguration.SHARED).get())
+ ) {
+ logger.infof("Setting a memory limit implies to have exactly one owne. Setting num_owners=1 to avoid data loss.", name);
+ builder.clustering().hash().numOwners(1);
+ }
+ }
}
// private methods below
diff --git a/model/infinispan/src/main/java/org/keycloak/spi/infinispan/impl/embedded/DefaultCacheEmbeddedConfigProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/spi/infinispan/impl/embedded/DefaultCacheEmbeddedConfigProviderFactory.java
index c36092355556..26e846b7628a 100644
--- a/model/infinispan/src/main/java/org/keycloak/spi/infinispan/impl/embedded/DefaultCacheEmbeddedConfigProviderFactory.java
+++ b/model/infinispan/src/main/java/org/keycloak/spi/infinispan/impl/embedded/DefaultCacheEmbeddedConfigProviderFactory.java
@@ -54,6 +54,7 @@
import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.ALL_CACHES_NAME;
import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.CLUSTERED_MAX_COUNT_CACHES;
import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.LOCAL_CACHE_NAMES;
+import static org.keycloak.spi.infinispan.impl.embedded.JGroupsConfigurator.createJGroupsProperties;
/**
* The default implementation of {@link CacheEmbeddedConfigProviderFactory}.
@@ -121,6 +122,7 @@ public List getConfigMetadata() {
Stream.concat(Arrays.stream(LOCAL_CACHE_NAMES), Arrays.stream(CLUSTERED_MAX_COUNT_CACHES))
.forEach(name -> Util.copyFromOption(builder, CacheConfigurator.maxCountConfigKey(name), "max-count", ProviderConfigProperty.INTEGER_TYPE, CachingOptions.maxCountOption(name), false));
createTopologyProperties(builder);
+ createJGroupsProperties(builder);
return builder.build();
}
diff --git a/model/infinispan/src/main/java/org/keycloak/spi/infinispan/impl/embedded/JGroupsConfigurator.java b/model/infinispan/src/main/java/org/keycloak/spi/infinispan/impl/embedded/JGroupsConfigurator.java
index 1653cdbc468e..3af902bfc971 100644
--- a/model/infinispan/src/main/java/org/keycloak/spi/infinispan/impl/embedded/JGroupsConfigurator.java
+++ b/model/infinispan/src/main/java/org/keycloak/spi/infinispan/impl/embedded/JGroupsConfigurator.java
@@ -17,20 +17,31 @@
package org.keycloak.spi.infinispan.impl.embedded;
+import static org.infinispan.configuration.global.TransportConfiguration.STACK;
+import static org.keycloak.config.CachingOptions.CACHE_EMBEDDED_PREFIX;
+
import java.lang.invoke.MethodHandles;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Objects;
+import javax.net.ssl.KeyManager;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLParameters;
+import javax.net.ssl.SSLServerSocket;
+import javax.net.ssl.TrustManager;
+
import org.infinispan.commons.configuration.attributes.Attribute;
import org.infinispan.configuration.global.TransportConfigurationBuilder;
import org.infinispan.configuration.parsing.ConfigurationBuilderHolder;
import org.infinispan.remoting.transport.jgroups.EmbeddedJGroupsChannelConfigurator;
import org.infinispan.remoting.transport.jgroups.JGroupsTransport;
import org.jboss.logging.Logger;
+import org.jgroups.Global;
import org.jgroups.conf.ClassConfigurator;
import org.jgroups.conf.ProtocolConfiguration;
import org.jgroups.protocols.TCP;
@@ -40,6 +51,8 @@
import org.jgroups.util.DefaultSocketFactory;
import org.jgroups.util.SocketFactory;
import org.keycloak.Config;
+import org.keycloak.config.CachingOptions;
+import org.keycloak.config.Option;
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
import org.keycloak.connections.infinispan.InfinispanConnectionSpi;
import org.keycloak.connections.jpa.JpaConnectionProvider;
@@ -48,15 +61,10 @@
import org.keycloak.infinispan.util.InfinispanUtils;
import org.keycloak.jgroups.protocol.KEYCLOAK_JDBC_PING2;
import org.keycloak.models.KeycloakSession;
+import org.keycloak.provider.ProviderConfigProperty;
+import org.keycloak.provider.ProviderConfigurationBuilder;
import org.keycloak.spi.infinispan.JGroupsCertificateProvider;
-
-import javax.net.ssl.KeyManager;
-import javax.net.ssl.SSLContext;
-import javax.net.ssl.SSLParameters;
-import javax.net.ssl.SSLServerSocket;
-import javax.net.ssl.TrustManager;
-
-import static org.infinispan.configuration.global.TransportConfiguration.STACK;
+import org.keycloak.spi.infinispan.impl.Util;
/**
* Utility class to configure JGroups based on the Keycloak configuration.
@@ -88,6 +96,7 @@ public static void configureJGroups(Config.Scope config, ConfigurationBuilderHol
if (stack != null) {
transportOf(holder).stack(stack);
}
+ configureTransport(config);
configureDiscovery(holder, session);
configureTls(holder, session);
warnDeprecatedStack(holder);
@@ -126,6 +135,17 @@ public static void configureTopology(Config.Scope config, ConfigurationBuilderHo
}
}
+ static void createJGroupsProperties(ProviderConfigurationBuilder builder) {
+ Util.copyFromOption(builder, SystemProperties.BIND_ADDRESS.configKey, "address", ProviderConfigProperty.STRING_TYPE, CachingOptions.CACHE_EMBEDDED_NETWORK_BIND_ADDRESS, false);
+ Util.copyFromOption(builder, SystemProperties.BIND_PORT.configKey, "port", ProviderConfigProperty.INTEGER_TYPE, CachingOptions.CACHE_EMBEDDED_NETWORK_BIND_PORT, false);
+ Util.copyFromOption(builder, SystemProperties.EXTERNAL_ADDRESS.configKey, "address", ProviderConfigProperty.STRING_TYPE, CachingOptions.CACHE_EMBEDDED_NETWORK_EXTERNAL_ADDRESS, false);
+ Util.copyFromOption(builder, SystemProperties.EXTERNAL_PORT.configKey, "port", ProviderConfigProperty.INTEGER_TYPE, CachingOptions.CACHE_EMBEDDED_NETWORK_EXTERNAL_PORT, false);
+ }
+
+ private static void configureTransport(Config.Scope config) {
+ Arrays.stream(SystemProperties.values()).forEach(p -> p.set(config));
+ }
+
private static void configureTls(ConfigurationBuilderHolder holder, KeycloakSession session) {
var provider = session.getProvider(JGroupsCertificateProvider.class);
if (provider == null || !provider.isEnabled()) {
@@ -269,4 +289,69 @@ public void afterCreation(Protocol protocol) {
}
}
}
+
+ private enum SystemProperties {
+ BIND_ADDRESS(CachingOptions.CACHE_EMBEDDED_NETWORK_BIND_ADDRESS, Global.BIND_ADDR, "jgroups.bind.address"),
+ BIND_PORT(CachingOptions.CACHE_EMBEDDED_NETWORK_BIND_PORT, Global.BIND_PORT, "jgroups.bind.port"),
+ EXTERNAL_ADDRESS(CachingOptions.CACHE_EMBEDDED_NETWORK_EXTERNAL_ADDRESS, Global.EXTERNAL_ADDR),
+ EXTERNAL_PORT(CachingOptions.CACHE_EMBEDDED_NETWORK_EXTERNAL_PORT, Global.EXTERNAL_PORT);
+
+ final Option> option;
+ final String property;
+ final String altProperty;
+ final String configKey;
+
+ SystemProperties(Option> option, String property) {
+ this(option, property, null);
+ }
+
+ SystemProperties(Option> option, String property, String altProperty) {
+ this.option = option;
+ this.property = property;
+ this.altProperty = altProperty;
+ this.configKey = configKey();
+ }
+
+ void set(Config.Scope config) {
+ String userConfig = fromConfig(config);
+ if (userConfig == null) {
+ // User property is either already set or missing, so do nothing
+ return;
+ }
+ checkPropertyAlreadySet(userConfig, property);
+ if (altProperty != null)
+ checkPropertyAlreadySet(userConfig, altProperty);
+ System.setProperty(property, userConfig);
+ }
+
+ void checkPropertyAlreadySet(String userValue, String property) {
+ String userProp = System.getProperty(property);
+ if (userProp != null) {
+ logger.warnf("Conflicting system property '%s' and CLI arg '%s' set, utilising CLI value '%s'",
+ property, option.getKey(), userValue);
+ System.clearProperty(property);
+ }
+ }
+
+ String fromConfig(Config.Scope config) {
+ if (option.getType() == Integer.class) {
+ Integer val = config.getInt(configKey);
+ return val == null ? null : val.toString();
+ }
+ return config.get(configKey);
+ }
+
+ String configKey() {
+ // Strip the scope from the key and convert to camelCase
+ String key = option.getKey().substring(CACHE_EMBEDDED_PREFIX.length() + 1);
+ StringBuilder sb = new StringBuilder(key);
+ for (int i = 0; i < sb.length(); i++) {
+ if (sb.charAt(i) == '-') {
+ sb.deleteCharAt(i);
+ sb.replace(i, i+1, String.valueOf(Character.toUpperCase(sb.charAt(i))));
+ }
+ }
+ return sb.toString();
+ }
+ }
}
diff --git a/operator/src/main/java/org/keycloak/operator/controllers/KeycloakDeploymentDependentResource.java b/operator/src/main/java/org/keycloak/operator/controllers/KeycloakDeploymentDependentResource.java
index 17a29b9dd7a2..4fe167511f49 100644
--- a/operator/src/main/java/org/keycloak/operator/controllers/KeycloakDeploymentDependentResource.java
+++ b/operator/src/main/java/org/keycloak/operator/controllers/KeycloakDeploymentDependentResource.java
@@ -151,14 +151,18 @@ public StatefulSet desired(Keycloak primary, Context context) {
var existingDeployment = ContextUtils.getCurrentStatefulSet(context).orElse(null);
+ String serviceName = KeycloakDiscoveryServiceDependentResource.getName(primary);
if (existingDeployment != null) {
// copy the existing annotations to keep the status consistent
CRDUtils.findUpdateReason(existingDeployment).ifPresent(r -> baseDeployment.getMetadata().getAnnotations()
.put(Constants.KEYCLOAK_UPDATE_REASON_ANNOTATION, r));
CRDUtils.fetchIsRecreateUpdate(existingDeployment).ifPresent(b -> baseDeployment.getMetadata()
.getAnnotations().put(Constants.KEYCLOAK_RECREATE_UPDATE_ANNOTATION, b.toString()));
+ serviceName = existingDeployment.getSpec().getServiceName();
}
+ baseDeployment.getSpec().setServiceName(serviceName);
+
var updateType = ContextUtils.getUpdateType(context);
if (existingDeployment == null || updateType.isEmpty()) {
@@ -287,7 +291,6 @@ private StatefulSet createBaseDeployment(Keycloak keycloakCR, Context
.editOrNewSpec().withImagePullSecrets(keycloakCR.getSpec().getImagePullSecrets()).endSpec()
.endTemplate()
.withReplicas(keycloakCR.getSpec().getInstances())
- .withServiceName(KeycloakDiscoveryServiceDependentResource.getName(keycloakCR))
.endSpec();
var specBuilder = baseDeploymentBuilder.editSpec().editTemplate().editOrNewSpec();
diff --git a/operator/src/test/java/org/keycloak/operator/testsuite/integration/KeycloakDeploymentTest.java b/operator/src/test/java/org/keycloak/operator/testsuite/integration/KeycloakDeploymentTest.java
index 453d3353e516..d65837b96cf9 100644
--- a/operator/src/test/java/org/keycloak/operator/testsuite/integration/KeycloakDeploymentTest.java
+++ b/operator/src/test/java/org/keycloak/operator/testsuite/integration/KeycloakDeploymentTest.java
@@ -47,7 +47,6 @@
import org.keycloak.operator.crds.v2alpha1.deployment.ValueOrSecret;
import org.keycloak.operator.crds.v2alpha1.deployment.spec.BootstrapAdminSpec;
import org.keycloak.operator.crds.v2alpha1.deployment.spec.HostnameSpecBuilder;
-import org.keycloak.operator.crds.v2alpha1.deployment.spec.ProbeSpec;
import org.keycloak.operator.testsuite.apiserver.DisabledIfApiServerTest;
import org.keycloak.operator.testsuite.unit.WatchedResourcesTest;
import org.keycloak.operator.testsuite.utils.CRAssert;
@@ -213,6 +212,7 @@ public void testConfigInCRTakesPrecedence() {
@Test
public void testDeploymentDurability() {
var kc = getTestKeycloakDeployment(true);
+ KeycloakDeploymentTest.initCustomBootstrapAdminUser(kc);
var deploymentName = kc.getMetadata().getName();
// create a dummy StatefulSet representing the pre-multiinstance state that we'll be forced to delete
@@ -395,14 +395,19 @@ public void testInitialAdminUser() {
@Test
public void testCustomBootstrapAdminUser() {
var kc = getTestKeycloakDeployment(true);
+ String secretName = initCustomBootstrapAdminUser(kc);
+ assertInitialAdminUser(secretName, kc, true);
+ }
+
+ static String initCustomBootstrapAdminUser(Keycloak kc) {
String secretName = "my-secret";
// fluents don't seem to work here because of the inner classes
kc.getSpec().setBootstrapAdminSpec(new BootstrapAdminSpec());
kc.getSpec().getBootstrapAdminSpec().setUser(new BootstrapAdminSpec.User());
kc.getSpec().getBootstrapAdminSpec().getUser().setSecret(secretName);
k8sclient.resource(new SecretBuilder().withNewMetadata().withName(secretName).endMetadata()
- .addToStringData("username", "user").addToStringData("password", "pass20rd").build()).create();
- assertInitialAdminUser(secretName, kc, true);
+ .addToStringData("username", "user").addToStringData("password", "pass20rd").build()).serverSideApply();
+ return secretName;
}
// Reference curl command:
diff --git a/operator/src/test/java/org/keycloak/operator/testsuite/integration/PodTemplateTest.java b/operator/src/test/java/org/keycloak/operator/testsuite/integration/PodTemplateTest.java
index 4d7d0c1e5756..4d6517b4c364 100644
--- a/operator/src/test/java/org/keycloak/operator/testsuite/integration/PodTemplateTest.java
+++ b/operator/src/test/java/org/keycloak/operator/testsuite/integration/PodTemplateTest.java
@@ -22,6 +22,8 @@
import io.fabric8.kubernetes.api.model.NamespaceBuilder;
import io.fabric8.kubernetes.api.model.PodTemplateSpecBuilder;
import io.fabric8.kubernetes.api.model.Secret;
+import io.fabric8.kubernetes.api.model.apps.StatefulSet;
+import io.fabric8.kubernetes.api.model.apps.StatefulSetBuilder;
import io.fabric8.kubernetes.client.dsl.Resource;
import io.fabric8.kubernetes.client.utils.Serialization;
import io.quarkus.logging.Log;
@@ -29,16 +31,20 @@
import org.awaitility.Awaitility;
import org.junit.jupiter.api.Test;
+import org.keycloak.operator.Utils;
import org.keycloak.operator.crds.v2alpha1.deployment.Keycloak;
import org.keycloak.operator.testsuite.apiserver.DisabledIfApiServerTest;
import org.keycloak.operator.testsuite.utils.CRAssert;
import org.keycloak.operator.testsuite.utils.K8sUtils;
import java.util.Collections;
+import java.util.concurrent.TimeUnit;
import static java.util.concurrent.TimeUnit.MINUTES;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.keycloak.operator.crds.v2alpha1.deployment.KeycloakStatusCondition.HAS_ERRORS;
+import static org.keycloak.operator.testsuite.utils.K8sUtils.deployKeycloak;
import static org.keycloak.operator.testsuite.utils.K8sUtils.getResourceFromFile;
@QuarkusTest
@@ -229,4 +235,31 @@ public void testPodTemplateIncorrectImagePullSecretsConfig() {
});
}
+ @Test
+ public void testDeploymentUpgrade() {
+ var kc = getTestKeycloakDeployment(true);
+ kc.getSpec().setInstances(2);
+ // all preconditions must be met, otherwise the operator sdk will remove the existing statefulset
+ KeycloakDeploymentTest.initCustomBootstrapAdminUser(kc);
+
+ // create a dummy StatefulSet representing the 26.0 state that we'll be forced to delete
+ StatefulSet statefulSet = new StatefulSetBuilder().withMetadata(kc.getMetadata()).editMetadata()
+ .addToLabels(Utils.allInstanceLabels(kc)).endMetadata().withNewSpec().withNewSelector()
+ .withMatchLabels(Utils.allInstanceLabels(kc)).endSelector().withReplicas(0)
+ .withNewTemplate().withNewMetadata().withLabels(Utils.allInstanceLabels(kc)).endMetadata()
+ .withNewSpec().addNewContainer().withName("pause").withImage("registry.k8s.io/pause:3.1")
+ .endContainer().endSpec().endTemplate().endSpec().build();
+ var ss = k8sclient.resource(statefulSet).create();
+
+ // start will not be successful because the statefulSet is in the way
+ deployKeycloak(k8sclient, kc, false);
+ // once the statefulset is owned by the keycloak it will be picked up by the informer
+ k8sclient.resource(statefulSet).accept(s -> s.addOwnerReference(k8sclient.resource(kc).get()));
+
+ Awaitility.await().atMost(1, TimeUnit.MINUTES).until(() -> k8sclient.resource(statefulSet).get().getSpec().getReplicas() == 2);
+
+ // we don't expect a recreate - that would indicate the operator sdk saw a precondition failing
+ assertEquals(ss.getMetadata().getUid(), k8sclient.resource(statefulSet).get().getMetadata().getUid());
+ }
+
}
diff --git a/pom.xml b/pom.xml
index 1ffa108e02a0..5854c056b60a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -90,8 +90,8 @@
2.3.230
6.2.13.Final
6.2.13.Final
- 15.0.15.Final
- 5.0.13.Final
+ 15.0.16.Final
+ 5.0.14.Final
${protostream.version}
diff --git a/quarkus/CONTRIBUTING.md b/quarkus/CONTRIBUTING.md
index 7bf6aa66cd1e..37666b49fb2c 100644
--- a/quarkus/CONTRIBUTING.md
+++ b/quarkus/CONTRIBUTING.md
@@ -104,7 +104,7 @@ The `kc.sh|bat` script allows you to remotely debug the distribution. For that,
kc.sh --debug start-dev
```
-By default, the debug port is available at `8787`.
+By default, the debug port is available at `8787`. Additionally, you can specify IPv4 or bracketed IPv6 addresses with optional ports, e.g. `--debug 127.0.0.1`, `--debug 127.0.0.1:8786`, `--debug [::1]`, `--debug [::1]:8785`. Make sure to exercise caution when setting IP addresses in the `--debug` parameter, since a value such as `--debug 0.0.0.0:8787` will expose the debug port to all network interfaces!
An additional environment variable `DEBUG_SUSPEND` can be set to suspend the JVM, when launched in debug mode. The `DEBUG_SUSPEND` variable supports the following values:
diff --git a/quarkus/config-api/src/main/java/org/keycloak/config/CachingOptions.java b/quarkus/config-api/src/main/java/org/keycloak/config/CachingOptions.java
index 0e6a611b0491..439b1b3fffdf 100644
--- a/quarkus/config-api/src/main/java/org/keycloak/config/CachingOptions.java
+++ b/quarkus/config-api/src/main/java/org/keycloak/config/CachingOptions.java
@@ -8,7 +8,7 @@ public class CachingOptions {
public static final String CACHE_CONFIG_FILE_PROPERTY = "cache-config-file";
- private static final String CACHE_EMBEDDED_PREFIX = "cache-embedded";
+ public static final String CACHE_EMBEDDED_PREFIX = "cache-embedded";
private static final String CACHE_EMBEDDED_MTLS_PREFIX = CACHE_EMBEDDED_PREFIX + "-mtls";
public static final String CACHE_EMBEDDED_MTLS_ENABLED_PROPERTY = CACHE_EMBEDDED_MTLS_PREFIX + "-enabled";
public static final String CACHE_EMBEDDED_MTLS_KEYSTORE_FILE_PROPERTY = CACHE_EMBEDDED_MTLS_PREFIX + "-key-store-file";
@@ -16,6 +16,10 @@ public class CachingOptions {
public static final String CACHE_EMBEDDED_MTLS_TRUSTSTORE_FILE_PROPERTY = CACHE_EMBEDDED_MTLS_PREFIX + "-trust-store-file";
public static final String CACHE_EMBEDDED_MTLS_TRUSTSTORE_PASSWORD_PROPERTY = CACHE_EMBEDDED_MTLS_PREFIX + "-trust-store-password";
public static final String CACHE_EMBEDDED_MTLS_ROTATION_PROPERTY = CACHE_EMBEDDED_MTLS_PREFIX + "-rotation-interval-days";
+ public static final String CACHE_EMBEDDED_NETWORK_BIND_ADDRESS_PROPERTY = CACHE_EMBEDDED_PREFIX + "-network-bind-address";
+ public static final String CACHE_EMBEDDED_NETWORK_BIND_PORT_PROPERTY = CACHE_EMBEDDED_PREFIX + "-network-bind-port";
+ public static final String CACHE_EMBEDDED_NETWORK_EXTERNAL_ADDRESS_PROPERTY = CACHE_EMBEDDED_PREFIX + "-network-external-address";
+ public static final String CACHE_EMBEDDED_NETWORK_EXTERNAL_PORT_PROPERTY = CACHE_EMBEDDED_PREFIX + "-network-external-port";
private static final String CACHE_REMOTE_PREFIX = "cache-remote";
public static final String CACHE_REMOTE_HOST_PROPERTY = CACHE_REMOTE_PREFIX + "-host";
@@ -109,6 +113,28 @@ public String toString() {
.description("Rotation period in days of automatic JGroups MTLS certificates.")
.build();
+ public static final Option CACHE_EMBEDDED_NETWORK_BIND_ADDRESS = new OptionBuilder<>(CACHE_EMBEDDED_NETWORK_BIND_ADDRESS_PROPERTY, String.class)
+ .category(OptionCategory.CACHE)
+ .description("IP address used by clustering transport. By default, SITE_LOCAL is used.")
+ .build();
+
+ public static final Option CACHE_EMBEDDED_NETWORK_BIND_PORT = new OptionBuilder<>(CACHE_EMBEDDED_NETWORK_BIND_PORT_PROPERTY, Integer.class)
+ .category(OptionCategory.CACHE)
+ .description("The Port the clustering transport will bind to. By default, port 7800 is used.")
+ .build();
+
+ public static final Option CACHE_EMBEDDED_NETWORK_EXTERNAL_ADDRESS = new OptionBuilder<>(CACHE_EMBEDDED_NETWORK_EXTERNAL_ADDRESS_PROPERTY, String.class)
+ .category(OptionCategory.CACHE)
+ .description("IP address that other instances in the cluster should use to contact this node. Set only if it is " +
+ "different to %s, for example when this instance is behind a firewall.".formatted(CACHE_EMBEDDED_NETWORK_BIND_ADDRESS_PROPERTY))
+ .build();
+
+ public static final Option CACHE_EMBEDDED_NETWORK_EXTERNAL_PORT = new OptionBuilder<>(CACHE_EMBEDDED_NETWORK_EXTERNAL_PORT_PROPERTY, Integer.class)
+ .category(OptionCategory.CACHE)
+ .description("Port that other instances in the cluster should use to contact this node. Set only if it is different " +
+ "to %s, for example when this instance is behind a firewall".formatted(CACHE_EMBEDDED_NETWORK_BIND_PORT_PROPERTY))
+ .build();
+
public static final Option CACHE_REMOTE_HOST = new OptionBuilder<>(CACHE_REMOTE_HOST_PROPERTY, String.class)
.category(OptionCategory.CACHE)
.description("The hostname of the external Infinispan cluster.")
diff --git a/quarkus/config-api/src/main/java/org/keycloak/config/DatabaseOptions.java b/quarkus/config-api/src/main/java/org/keycloak/config/DatabaseOptions.java
index 183fed3b98fc..bd719e018b25 100644
--- a/quarkus/config-api/src/main/java/org/keycloak/config/DatabaseOptions.java
+++ b/quarkus/config-api/src/main/java/org/keycloak/config/DatabaseOptions.java
@@ -106,6 +106,11 @@ public class DatabaseOptions {
.description("Deactivate specific named datasource .")
.build();
+ public static final Option DB_POSTGRESQL_TARGET_SERVER_TYPE = new OptionBuilder<>("db-postgres-target-server-type", String.class)
+ .category(OptionCategory.DATABASE)
+ .hidden()
+ .build();
+
/**
* Options that have their sibling for a named datasource
* Example: for `db-dialect`, `db-dialect-` is created
diff --git a/quarkus/config-api/src/main/java/org/keycloak/config/OptionCategory.java b/quarkus/config-api/src/main/java/org/keycloak/config/OptionCategory.java
index 9564ce73260e..bf6038c39607 100644
--- a/quarkus/config-api/src/main/java/org/keycloak/config/OptionCategory.java
+++ b/quarkus/config-api/src/main/java/org/keycloak/config/OptionCategory.java
@@ -4,7 +4,7 @@ public enum OptionCategory {
CACHE("Cache", 10, ConfigSupportLevel.SUPPORTED),
CONFIG("Config", 15, ConfigSupportLevel.SUPPORTED),
DATABASE("Database", 20, ConfigSupportLevel.SUPPORTED),
- DATABASE_DATASOURCES("Database - additional datasources", 21, ConfigSupportLevel.SUPPORTED),
+ DATABASE_DATASOURCES("Database - additional datasources", 21, ConfigSupportLevel.PREVIEW),
TRANSACTION("Transaction",30, ConfigSupportLevel.SUPPORTED),
FEATURE("Feature", 40, ConfigSupportLevel.SUPPORTED),
HOSTNAME_V2("Hostname v2", 50, ConfigSupportLevel.SUPPORTED),
diff --git a/quarkus/dist/src/main/content/bin/kc.bat b/quarkus/dist/src/main/content/bin/kc.bat
index 437f34e8ceb7..c808a0021a46 100644
--- a/quarkus/dist/src/main/content/bin/kc.bat
+++ b/quarkus/dist/src/main/content/bin/kc.bat
@@ -8,21 +8,22 @@ setlocal
rem Get the program name before using shift as the command modify the variable ~nx0
if "%OS%" == "Windows_NT" (
- set PROGNAME=%~nx0%
+ set PROGNAME=%~nx0%
) else (
- set PROGNAME=kc.bat
+ set PROGNAME=kc.bat
)
if "%OS%" == "Windows_NT" (
- set "DIRNAME=%~dp0"
+ set "DIRNAME=%~dp0"
) else (
- set DIRNAME=.\
+ set DIRNAME=.\
)
set SERVER_OPTS=-Djava.util.logging.manager=org.jboss.logmanager.LogManager -Dquarkus-log-max-startup-records=10000 -Dpicocli.disable.closures=true
set DEBUG_MODE=false
set DEBUG_PORT_VAR=8787
+set DEBUG_ADDRESS=0.0.0.0:%DEBUG_PORT_VAR%
set DEBUG_SUSPEND_VAR=n
set CONFIG_ARGS=
@@ -33,20 +34,29 @@ if "%KEY%" == "" (
goto MAIN
)
if "%KEY%" == "--debug" (
- set DEBUG_MODE=true
- if 1%2 EQU +1%2 (
- set DEBUG_PORT_VAR=%2
- shift
- ) else (
- set DEBUG_PORT_VAR=8787
- )
- shift
- goto READ-ARGS
+ set DEBUG_MODE=true
+ if 1%2 EQU +1%2 (
+ rem Plain port
+ set DEBUG_ADDRESS=0.0.0.0:%2
+ shift
+ ) else (
+ rem IPv4 or IPv6 address with optional port
+ (echo %2 | findstr /R "[0-9].*\." >nul || echo %2 | findstr /R "\[.*:.*\]" >nul) && (
+ (echo %2 | findstr /R "]:[0-9][0-9]*" >nul || echo %2 | findstr /R "^[0-9].*:[0-9][0-9]*" >nul) && (
+ set DEBUG_ADDRESS=%2
+ ) || (
+ set DEBUG_ADDRESS=%2:%DEBUG_PORT_VAR%
+ )
+ shift
+ )
+ )
+ shift
+ goto READ-ARGS
)
if "%KEY%" == "start-dev" (
- set CONFIG_ARGS=%CONFIG_ARGS% --profile=dev %KEY%
- shift
- goto READ-ARGS
+ set CONFIG_ARGS=%CONFIG_ARGS% --profile=dev %KEY%
+ shift
+ goto READ-ARGS
)
set "VALUE=%~2"
set PROBABLY_VALUE=false
@@ -58,25 +68,25 @@ if "%VALUE%" NEQ "" (
)
)
if "%KEY:~0,2%"=="-D" (
- if %PROBABLY_VALUE%==true (
- set SERVER_OPTS=%SERVER_OPTS% %KEY%^=%VALUE%
+ if %PROBABLY_VALUE%==true (
+ set SERVER_OPTS=%SERVER_OPTS% %KEY%^=%VALUE%
+ shift
+ ) else (
+ set SERVER_OPTS=%SERVER_OPTS% %KEY%
+ )
shift
- ) else (
- set SERVER_OPTS=%SERVER_OPTS% %KEY%
- )
- shift
- goto READ-ARGS
+ goto READ-ARGS
)
if not "%KEY:~0,1%"=="-" (
- set CONFIG_ARGS=%CONFIG_ARGS% %1
- shift
- goto READ-ARGS
+ set CONFIG_ARGS=%CONFIG_ARGS% %1
+ shift
+ goto READ-ARGS
)
if %PROBABLY_VALUE%==true (
- set CONFIG_ARGS=%CONFIG_ARGS% %1 %2
- shift
+ set CONFIG_ARGS=%CONFIG_ARGS% %1 %2
+ shift
) else (
- set CONFIG_ARGS=%CONFIG_ARGS% %1
+ set CONFIG_ARGS=%CONFIG_ARGS% %1
)
shift
goto READ-ARGS
@@ -86,91 +96,90 @@ goto READ-ARGS
setlocal EnableDelayedExpansion
if not "x%JAVA_OPTS%" == "x" (
- echo "JAVA_OPTS already set in environment; overriding default settings"
+ echo "JAVA_OPTS already set in environment; overriding default settings"
) else (
- rem The defaults set up Keycloak with '-XX:+UseG1GC -XX:MinHeapFreeRatio=40 -XX:MaxHeapFreeRatio=70 -XX:GCTimeRatio=12 -XX:AdaptiveSizePolicyWeight=10' which proved to provide a good throughput and efficiency in the total memory allocation and CPU overhead.
- rem If the memory is not used, it will be freed. See https://developers.redhat.com/blog/2017/04/04/openjdk-and-containers for details.
- rem To optimize for large heap sizes or for throughput and better response time due to shorter GC pauses, consider ZGC and Shenandoah GC.
- rem As of KC25 and JDK17, G1GC, ZGC and Shenandoah GC seem to be eager to claim the maximum heap size. Tests showed that ZGC might need additional tuning in reclaiming dead objects.
- set "JAVA_OPTS=-XX:MetaspaceSize=96M -XX:MaxMetaspaceSize=256m -Dfile.encoding=UTF-8 -Dsun.stdout.encoding=UTF-8 -Dsun.err.encoding=UTF-8 -Dstdout.encoding=UTF-8 -Dstderr.encoding=UTF-8 -XX:+ExitOnOutOfMemoryError -Djava.security.egd=file:/dev/urandom -XX:+UseG1GC -XX:FlightRecorderOptions=stackdepth=512"
-
- if "x%JAVA_OPTS_KC_HEAP%" == "x" (
- if "!KC_RUN_IN_CONTAINER!" == "true" (
- rem Maximum utilization of the heap is set to 70% of the total container memory
- rem Initial heap size is set to 50% of the total container memory in order to reduce GC executions
- set "JAVA_OPTS_KC_HEAP=-XX:MaxRAMPercentage=70 -XX:MinRAMPercentage=70 -XX:InitialRAMPercentage=50"
+ rem The defaults set up Keycloak with '-XX:+UseG1GC -XX:MinHeapFreeRatio=40 -XX:MaxHeapFreeRatio=70 -XX:GCTimeRatio=12 -XX:AdaptiveSizePolicyWeight=10' which proved to provide a good throughput and efficiency in the total memory allocation and CPU overhead.
+ rem If the memory is not used, it will be freed. See https://developers.redhat.com/blog/2017/04/04/openjdk-and-containers for details.
+ rem To optimize for large heap sizes or for throughput and better response time due to shorter GC pauses, consider ZGC and Shenandoah GC.
+ rem As of KC25 and JDK17, G1GC, ZGC and Shenandoah GC seem to be eager to claim the maximum heap size. Tests showed that ZGC might need additional tuning in reclaiming dead objects.
+ set "JAVA_OPTS=-XX:MetaspaceSize=96M -XX:MaxMetaspaceSize=256m -Dfile.encoding=UTF-8 -Dsun.stdout.encoding=UTF-8 -Dsun.err.encoding=UTF-8 -Dstdout.encoding=UTF-8 -Dstderr.encoding=UTF-8 -XX:+ExitOnOutOfMemoryError -Djava.security.egd=file:/dev/urandom -XX:+UseG1GC -XX:FlightRecorderOptions=stackdepth=512"
+
+ if "x%JAVA_OPTS_KC_HEAP%" == "x" (
+ if "!KC_RUN_IN_CONTAINER!" == "true" (
+ rem Maximum utilization of the heap is set to 70% of the total container memory
+ rem Initial heap size is set to 50% of the total container memory in order to reduce GC executions
+ set "JAVA_OPTS_KC_HEAP=-XX:MaxRAMPercentage=70 -XX:MinRAMPercentage=70 -XX:InitialRAMPercentage=50"
+ ) else (
+ set "JAVA_OPTS_KC_HEAP=-Xms64m -Xmx512m"
+ )
) else (
- set "JAVA_OPTS_KC_HEAP=-Xms64m -Xmx512m"
+ echo "JAVA_OPTS_KC_HEAP already set in environment; overriding default settings"
)
- ) else (
- echo "JAVA_OPTS_KC_HEAP already set in environment; overriding default settings"
- )
-
- set "JAVA_OPTS=!JAVA_OPTS! !JAVA_OPTS_KC_HEAP!"
+ set "JAVA_OPTS=!JAVA_OPTS! !JAVA_OPTS_KC_HEAP!"
)
@REM See also https://github.com/wildfly/wildfly-core/blob/7e5624cf92ebe4b64a4793a8c0b2a340c0d6d363/core-feature-pack/common/src/main/resources/content/bin/common.sh#L57-L60
if not "x%JAVA_ADD_OPENS%" == "x" (
- echo "JAVA_ADD_OPENS already set in environment; overriding default settings"
+ echo "JAVA_ADD_OPENS already set in environment; overriding default settings"
) else (
- set "JAVA_ADD_OPENS=--add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.base/java.util.concurrent=ALL-UNNAMED --add-opens=java.base/java.security=ALL-UNNAMED"
+ set "JAVA_ADD_OPENS=--add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.base/java.util.concurrent=ALL-UNNAMED --add-opens=java.base/java.security=ALL-UNNAMED"
)
set "JAVA_OPTS=%JAVA_OPTS% %JAVA_ADD_OPENS%"
@REM Set the default locale for the JVM to English to prevent locale-specific character variations
if not "x%JAVA_LOCALE%" == "x" (
- echo "JAVA_LOCALE already set in environment; overriding default settings"
+ echo "JAVA_LOCALE already set in environment; overriding default settings"
) else (
- set "JAVA_LOCALE=-Duser.language=en -Duser.country=US"
+ set "JAVA_LOCALE=-Duser.language=en -Duser.country=US"
)
set "JAVA_OPTS=%JAVA_OPTS% %JAVA_LOCALE%"
if not "x%JAVA_OPTS_APPEND%" == "x" (
- echo "Appending additional Java properties to JAVA_OPTS"
- set JAVA_OPTS=%JAVA_OPTS% %JAVA_OPTS_APPEND%
+ echo "Appending additional Java properties to JAVA_OPTS"
+ set JAVA_OPTS=%JAVA_OPTS% %JAVA_OPTS_APPEND%
)
if NOT "x%DEBUG%" == "x" (
- set DEBUG_MODE=%DEBUG%
+ set DEBUG_MODE=%DEBUG%
)
if NOT "x%DEBUG_PORT%" == "x" (
- set DEBUG_PORT_VAR=%DEBUG_PORT%
+ set DEBUG_PORT_VAR=%DEBUG_PORT%
)
if NOT "x%DEBUG_SUSPEND%" == "x" (
- set DEBUG_SUSPEND_VAR=%DEBUG_SUSPEND%
+ set DEBUG_SUSPEND_VAR=%DEBUG_SUSPEND%
)
rem Set debug settings if not already set
if "%DEBUG_MODE%" == "true" (
- echo "%JAVA_OPTS%" | findstr /I "\-agentlib:jdwp" > nul
- if errorlevel == 1 (
- set JAVA_OPTS=%JAVA_OPTS% -agentlib:jdwp=transport=dt_socket,address=%DEBUG_PORT_VAR%,server=y,suspend=%DEBUG_SUSPEND_VAR%
- ) else (
- echo Debug already enabled in JAVA_OPTS, ignoring --debug argument
- )
+ echo "%JAVA_OPTS%" | findstr /I "\-agentlib:jdwp" > nul
+ if errorlevel == 1 (
+ set JAVA_OPTS=%JAVA_OPTS% -agentlib:jdwp=transport=dt_socket,address=%DEBUG_ADDRESS%,server=y,suspend=%DEBUG_SUSPEND_VAR%
+ ) else (
+ echo Debug already enabled in JAVA_OPTS, ignoring --debug argument
+ )
)
rem Setup Keycloak specific properties
set JAVA_OPTS=-Dprogram.name=%PROGNAME% %JAVA_OPTS%
if "x%JAVA%" == "x" (
- if "x%JAVA_HOME%" == "x" (
- set JAVA=java
- echo JAVA_HOME is not set. Unexpected results may occur. 1>&2
- echo Set JAVA_HOME to the directory of your local JDK to avoid this message. 1>&2
- ) else (
- if not exist "%JAVA_HOME%" (
- echo JAVA_HOME "%JAVA_HOME%" path doesn't exist 1>&2
- goto END
+ if "x%JAVA_HOME%" == "x" (
+ set JAVA=java
+ echo JAVA_HOME is not set. Unexpected results may occur. 1>&2
+ echo Set JAVA_HOME to the directory of your local JDK to avoid this message. 1>&2
) else (
- if not exist "%JAVA_HOME%\bin\java.exe" (
- echo "%JAVA_HOME%\bin\java.exe" does not exist 1>&2
+ if not exist "%JAVA_HOME%" (
+ echo JAVA_HOME "%JAVA_HOME%" path doesn't exist 1>&2
goto END
- )
- set "JAVA=%JAVA_HOME%\bin\java"
+ ) else (
+ if not exist "%JAVA_HOME%\bin\java.exe" (
+ echo "%JAVA_HOME%\bin\java.exe" does not exist 1>&2
+ goto END
+ )
+ set "JAVA=%JAVA_HOME%\bin\java"
)
)
)
@@ -196,8 +205,8 @@ if not errorlevel == 1 (
)
if "%PRINT_ENV%" == "true" (
- echo Using JAVA_OPTS: !JAVA_OPTS!
- echo Using JAVA_RUN_OPTS: !JAVA_RUN_OPTS!
+ echo Using JAVA_OPTS: !JAVA_OPTS!
+ echo Using JAVA_RUN_OPTS: !JAVA_RUN_OPTS!
)
set START_SERVER=true
diff --git a/quarkus/dist/src/main/content/bin/kc.sh b/quarkus/dist/src/main/content/bin/kc.sh
index 9a6e62e2dd2e..9c0b946e8522 100644
--- a/quarkus/dist/src/main/content/bin/kc.sh
+++ b/quarkus/dist/src/main/content/bin/kc.sh
@@ -44,6 +44,7 @@ CLASSPATH_OPTS="'$(abs_path "../lib/quarkus-run.jar")'"
DEBUG_MODE="${DEBUG:-false}"
DEBUG_PORT="${DEBUG_PORT:-8787}"
DEBUG_SUSPEND="${DEBUG_SUSPEND:-n}"
+DEBUG_ADDRESS="0.0.0.0:$DEBUG_PORT"
esceval() {
printf '%s\n' "$1" | sed "s/'/'\\\\''/g; 1 s/^/'/; $ s/$/'/"
@@ -55,9 +56,20 @@ do
case "$1" in
--debug)
DEBUG_MODE=true
- if [ -n "$2" ] && expr "$2" : '[0-9]\{0,\}$' >/dev/null; then
- DEBUG_PORT=$2
- shift
+ if [ -n "$2" ]; then
+ # Plain port
+ if echo "$2" | grep -Eq '^[0-9]+$'; then
+ DEBUG_ADDRESS="0.0.0.0:$2"
+ shift
+ # IPv4 or bracketed IPv6 with optional port
+ elif echo "$2" | grep -Eq '^(([0-9.]+)|(\[[0-9A-Fa-f:]+\]))'; then
+ if echo "$2" | grep -Eq ':[0-9]+$'; then
+ DEBUG_ADDRESS="$2"
+ else
+ DEBUG_ADDRESS="$2:$DEBUG_PORT"
+ fi
+ shift
+ fi
fi
;;
--)
@@ -142,7 +154,7 @@ fi
if [ "$DEBUG_MODE" = "true" ]; then
DEBUG_OPT="$(echo "$JAVA_OPTS" | $GREP "\-agentlib:jdwp")"
if [ -z "$DEBUG_OPT" ]; then
- JAVA_OPTS="$JAVA_OPTS -agentlib:jdwp=transport=dt_socket,address=$DEBUG_PORT,server=y,suspend=$DEBUG_SUSPEND"
+ JAVA_OPTS="$JAVA_OPTS -agentlib:jdwp=transport=dt_socket,address=$DEBUG_ADDRESS,server=y,suspend=$DEBUG_SUSPEND"
else
echo "Debug already enabled in JAVA_OPTS, ignoring --debug argument"
fi
@@ -152,7 +164,7 @@ esceval_args() {
while IFS= read -r entry; do
result="$result $(esceval "$entry")"
done
- echo $result
+ echo "$result"
}
JAVA_RUN_OPTS=$(echo "$JAVA_OPTS" | xargs printf '%s\n' | esceval_args)
@@ -166,8 +178,8 @@ if [ "$PRINT_ENV" = "true" ]; then
fi
if [ "$PRE_BUILD" = "true" ]; then
- eval "'$JAVA'" -Dkc.config.build-and-exit=true $JAVA_RUN_OPTS || exit $?
+ eval "'$JAVA'" -Dkc.config.build-and-exit=true "$JAVA_RUN_OPTS" || exit $?
JAVA_RUN_OPTS="-Dkc.config.built=true $JAVA_RUN_OPTS"
fi
-eval exec "'$JAVA'" $JAVA_RUN_OPTS
+eval exec "'$JAVA'" "$JAVA_RUN_OPTS"
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 bc3795d01dd2..ae41030f60df 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
@@ -33,9 +33,7 @@
import org.keycloak.common.Profile;
import org.keycloak.common.util.NetworkUtils;
-import org.keycloak.quarkus.runtime.cli.command.AbstractCommand;
import org.keycloak.quarkus.runtime.configuration.Configuration;
-import org.keycloak.quarkus.runtime.configuration.PersistedConfigSource;
public final class Environment {
@@ -48,8 +46,6 @@ public final class Environment {
public static final String PROD_PROFILE_VALUE = "prod";
public static final String LAUNCH_MODE = "kc.launch.mode";
- private static volatile AbstractCommand parsedCommand;
-
private Environment() {}
public static Boolean isRebuild() {
@@ -109,21 +105,6 @@ public static void setProfile(String profile) {
}
}
- /**
- * Update the profile settings based upon what was set in the system, environment, or optionally persistent values
- */
- public static String updateProfile(boolean usePersistent) {
- String profile = org.keycloak.common.util.Environment.getProfile();
- if(profile == null && usePersistent) {
- profile = PersistedConfigSource.getInstance().getValue(org.keycloak.common.util.Environment.PROFILE);
- }
- if (profile == null) {
- profile = Environment.PROD_PROFILE_VALUE;
- }
- setProfile(profile);
- return profile;
- }
-
public static boolean isDevMode() {
if (org.keycloak.common.util.Environment.isDevMode()) {
return true;
@@ -247,21 +228,6 @@ public synchronized static Profile getCurrentOrCreateFeatureProfile() {
return profile;
}
- /**
- * Get parsed AbstractCommand we obtained from the CLI
- */
- public static Optional getParsedCommand() {
- return Optional.ofNullable(parsedCommand);
- }
-
- public static boolean isParsedCommand(String commandName) {
- return getParsedCommand().filter(f -> f.getName().equals(commandName)).isPresent();
- }
-
- public static void setParsedCommand(AbstractCommand command) {
- Environment.parsedCommand = command;
- }
-
public static void removeHomeDir() {
System.getProperties().remove(KC_HOME_DIR);
}
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 f9d71683a5a5..5211e42e8b3e 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
@@ -31,16 +31,19 @@
import org.keycloak.infinispan.util.InfinispanUtils;
import org.keycloak.quarkus.runtime.configuration.PersistedConfigSource;
+import org.keycloak.quarkus.runtime.integration.jaxrs.QuarkusKeycloakApplication;
import io.quarkus.bootstrap.runner.RunnerClassLoader;
+import io.quarkus.arc.Arc;
import io.quarkus.runtime.ApplicationLifecycleManager;
import io.quarkus.runtime.Quarkus;
import org.jboss.logging.Logger;
-import org.keycloak.quarkus.runtime.cli.ExecutionExceptionHandler;
import org.keycloak.quarkus.runtime.cli.PropertyException;
+import org.keycloak.quarkus.runtime.cli.ExecutionExceptionHandler;
import org.keycloak.quarkus.runtime.cli.Picocli;
import org.keycloak.common.Version;
+import org.keycloak.quarkus.runtime.cli.command.AbstractNonServerCommand;
import org.keycloak.quarkus.runtime.cli.command.DryRunMixin;
import org.keycloak.quarkus.runtime.cli.command.Start;
@@ -54,6 +57,8 @@
@ApplicationScoped
public class KeycloakMain implements QuarkusApplication {
+ private static AbstractNonServerCommand COMMAND;
+
static {
InfinispanUtils.configureVirtualThreads();
}
@@ -92,6 +97,9 @@ public static void main(String[] args, Picocli picocli) {
}
if (cliArgs.isEmpty()) {
+ if (Environment.isRebuildCheck()) {
+ return; // nothing to do - not currently caught by the shell scripts
+ }
cliArgs = new ArrayList<>(cliArgs);
// default to show help message
cliArgs.add("-h");
@@ -128,7 +136,8 @@ private static boolean isFastStart(List cliArgs) {
return cliArgs.size() == 2 && cliArgs.get(0).equals(Start.NAME) && cliArgs.stream().anyMatch(OPTIMIZED_BUILD_OPTION_LONG::equals);
}
- public static void start(Picocli picocli, ExecutionExceptionHandler errorHandler) {
+ public static void start(Picocli picocli, AbstractNonServerCommand command, ExecutionExceptionHandler errorHandler) {
+ COMMAND = command; // it would be nice to not do this statically - start quarkus with an instance of KeycloakMain, rather than a class for example
try {
Quarkus.run(KeycloakMain.class, (exitCode, cause) -> {
if (cause != null) {
@@ -151,6 +160,10 @@ public static void start(Picocli picocli, ExecutionExceptionHandler errorHandler
*/
@Override
public int run(String... args) throws Exception {
+ if (COMMAND != null) {
+ QuarkusKeycloakApplication application = Arc.container().instance(QuarkusKeycloakApplication.class).get();
+ COMMAND.onStart(application);
+ }
if (isTestLaunchMode() || isNonServerMode()) {
// in test mode we exit immediately
// we should be managing this behavior more dynamically depending on the tests requirements (short/long lived)
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 450ec57a8af0..467cc7d6c03b 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
@@ -19,16 +19,13 @@
import static java.lang.String.format;
import static org.keycloak.quarkus.runtime.Environment.getProviderFiles;
-import static org.keycloak.quarkus.runtime.Environment.isDevMode;
import static org.keycloak.quarkus.runtime.Environment.isRebuild;
import static org.keycloak.quarkus.runtime.Environment.isRebuildCheck;
import static org.keycloak.quarkus.runtime.Environment.isRebuilt;
import static org.keycloak.quarkus.runtime.cli.OptionRenderer.decorateDuplicitOptionName;
import static org.keycloak.quarkus.runtime.cli.command.AbstractStartCommand.OPTIMIZED_BUILD_OPTION_LONG;
-import static org.keycloak.quarkus.runtime.configuration.ConfigArgsConfigSource.parseConfigArgs;
import static org.keycloak.quarkus.runtime.configuration.Configuration.isUserModifiable;
import static org.keycloak.quarkus.runtime.configuration.MicroProfileConfigProvider.NS_KEYCLOAK_PREFIX;
-import static org.keycloak.quarkus.runtime.configuration.mappers.PropertyMappers.maskValue;
import static picocli.CommandLine.Model.UsageMessageSpec.SECTION_KEY_COMMAND_LIST;
import java.io.File;
@@ -57,12 +54,9 @@
import org.keycloak.quarkus.runtime.KeycloakMain;
import org.keycloak.quarkus.runtime.Messages;
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.Completion;
import org.keycloak.quarkus.runtime.cli.command.Main;
-import org.keycloak.quarkus.runtime.cli.command.ShowConfig;
-import org.keycloak.quarkus.runtime.cli.command.StartDev;
-import org.keycloak.quarkus.runtime.cli.command.UpdateCompatibility;
import org.keycloak.quarkus.runtime.configuration.ConfigArgsConfigSource;
import org.keycloak.quarkus.runtime.configuration.Configuration;
import org.keycloak.quarkus.runtime.configuration.DisabledMappersInterceptor;
@@ -105,6 +99,7 @@ private static class IncludeOptions {
private final ExecutionExceptionHandler errorHandler = new ExecutionExceptionHandler();
private Set> allowedMappers;
private final List unrecognizedArgs = new ArrayList<>();
+ private Optional parsedCommand = Optional.empty();
public void parseAndRun(List cliArgs) {
// perform two passes over the cli args. First without option validation to determine the current command, then with option validation enabled
@@ -118,17 +113,7 @@ public void parseAndRun(List cliArgs) {
// recreate the command specifically for the current
cmd = createCommandLineForCommand(cliArgs, commandLineList);
- int exitCode;
- if (isRebuildCheck()) {
- CommandLine currentCommand = null;
- if (commandLineList.size() > 1) {
- currentCommand = commandLineList.get(commandLineList.size() - 1);
- }
- exitCode = runReAugmentationIfNeeded(cliArgs, cmd, currentCommand);
- } else {
- PropertyMappers.sanitizeDisabledMappers();
- exitCode = cmd.execute(argArray);
- }
+ int exitCode = cmd.execute(argArray);
exit(exitCode);
} catch (ParameterException parEx) {
@@ -168,15 +153,16 @@ public T set(T value) {
addHelp(currentSpec);
}
+ AbstractCommand currentCommand = null;
if (currentSpec != null) {
CommandLine commandLine = currentSpec.commandLine();
addCommandOptions(cliArgs, commandLine);
if (commandLine != null && commandLine.getCommand() instanceof AbstractCommand ac) {
- // set current parsed command
- Environment.setParsedCommand(ac);
+ currentCommand = ac;
}
}
+ initConfig(currentCommand);
if (isRebuildCheck()) {
// build command should be available when running re-aug
@@ -185,6 +171,10 @@ public T set(T value) {
});
}
+ public Optional getParsedCommand() {
+ return parsedCommand;
+ }
+
private void catchParameterException(ParameterException parEx, CommandLine cmd, String[] args) {
int exitCode;
try {
@@ -205,122 +195,6 @@ public void exit(int exitCode) {
System.exit(exitCode);
}
- private int runReAugmentationIfNeeded(List cliArgs, CommandLine cmd, CommandLine currentCommand) {
- int exitCode = 0;
-
- if (currentCommand == null) {
- return exitCode; // possible if using --version or the user made a mistake
- }
-
- String currentCommandName = currentCommand.getCommandName();
-
- if (shouldSkipRebuild(cliArgs, currentCommandName)) {
- return exitCode;
- }
-
- // TODO: ensure that the config has not yet been initialized
- // - there's currently no good way to do that directly on ConfigProviderResolver
- initProfile(cliArgs, currentCommandName);
-
- if (requiresReAugmentation(currentCommand)) {
- PropertyMappers.sanitizeDisabledMappers();
- exitCode = runReAugmentation(cliArgs, cmd);
- }
-
- return exitCode;
- }
-
- protected void initProfile(List cliArgs, String currentCommandName) {
- if (currentCommandName.equals(StartDev.NAME)) {
- // force the server image to be set with the dev profile
- Environment.forceDevProfile();
- } else {
- Environment.updateProfile(false);
-
- // override from the cli if specified
- parseConfigArgs(cliArgs, (k, v) -> {
- if (k.equals(Main.PROFILE_SHORT_NAME) || k.equals(Main.PROFILE_LONG_NAME)) {
- Environment.setProfile(v);
- }
- }, ignored -> {});
- }
- }
-
- private static boolean shouldSkipRebuild(List cliArgs, String currentCommandName) {
- return cliArgs.contains("--help")
- || cliArgs.contains("-h")
- || cliArgs.contains("--help-all")
- || currentCommandName.equals(Build.NAME)
- || currentCommandName.equals(ShowConfig.NAME)
- || currentCommandName.equals(Completion.NAME)
- || currentCommandName.equals(UpdateCompatibility.NAME);
- }
-
- private static boolean requiresReAugmentation(CommandLine cmdCommand) {
- Map rawPersistedProperties = Configuration.getRawPersistedProperties();
- if (rawPersistedProperties.isEmpty()) {
- return true; // no build yet
- }
- var current = getNonPersistedBuildTimeOptions();
-
- // everything but the optimized value must match
- String key = Configuration.KC_OPTIMIZED;
- Optional.ofNullable(rawPersistedProperties.get(key)).ifPresentOrElse(value -> current.put(key, value), () -> current.remove(key));
- return !rawPersistedProperties.equals(current);
- }
-
- /**
- * checks the raw cli input for possible credentials / properties which should be masked,
- * and masks them.
- * @return a list of potentially masked properties in CLI format, e.g. `--db-password=*******`
- * instead of the actual passwords value.
- */
- private static List getSanitizedRuntimeCliOptions() {
- List properties = new ArrayList<>();
-
- parseConfigArgs(ConfigArgsConfigSource.getAllCliArgs(), (key, value) -> {
- PropertyMapper> mapper = PropertyMappers.getMapperByCliKey(key);
-
- if (mapper == null || mapper.isRunTime()) {
- properties.add(key + "=" + maskValue(value, mapper));
- }
- }, properties::add);
-
- return properties;
- }
-
- private static int runReAugmentation(List cliArgs, CommandLine cmd) {
- if (cmd == null) {
- throw new IllegalStateException("CommandLine is null when trying to run re-augmentation. (CLI args: '%s')".formatted(String.join(", ", cliArgs)));
- }
-
- if (!isDevMode()) {
- cmd.getOut().println("Changes detected in configuration. Updating the server image.");
- if (Configuration.isOptimized()) {
- checkChangesInBuildOptionsDuringAutoBuild(cmd.getOut());
- }
- }
-
- List configArgsList = new ArrayList<>();
- configArgsList.add(Build.NAME);
- parseConfigArgs(cliArgs, (k, v) -> {
- PropertyMapper> mapper = PropertyMappers.getMapperByCliKey(k);
-
- if (mapper != null && mapper.isBuildTime()) {
- configArgsList.add(k + "=" + v);
- }
- }, ignored -> {});
-
- cmd = cmd.setUnmatchedArgumentsAllowed(true);
- int exitCode = cmd.execute(configArgsList.toArray(new String[0]));
-
- if(!isDevMode() && exitCode == cmd.getCommandSpec().exitCodeOnSuccess()) {
- cmd.getOut().printf("Next time you run the server, just run:%n%n\t%s %s %s%n%n", Environment.getCommand(), String.join(" ", getSanitizedRuntimeCliOptions()), OPTIMIZED_BUILD_OPTION_LONG);
- }
-
- return exitCode;
- }
-
private static boolean wasBuildEverRun() {
return !Configuration.getRawPersistedProperties().isEmpty();
}
@@ -623,6 +497,11 @@ public void info(String text) {
getOutWriter().println(defaultColorScheme.apply("INFO: ", Arrays.asList(Style.fg_green, Style.bold)) + text);
}
+ public void error(String text) {
+ ColorScheme defaultColorScheme = picocli.CommandLine.Help.defaultColorScheme(Help.Ansi.AUTO);
+ getErrWriter().println(defaultColorScheme.apply(text, Arrays.asList(Style.fg_red, Style.bold)));
+ }
+
private static void warn(String text, PrintWriter outwriter) {
ColorScheme defaultColorScheme = picocli.CommandLine.Help.defaultColorScheme(Help.Ansi.AUTO);
outwriter.println(defaultColorScheme.apply("WARNING: ", Arrays.asList(Style.fg_yellow, Style.bold)) + text);
@@ -922,8 +801,8 @@ private static String getDecoratedOptionDescription(PropertyMapper> mapper) {
return transformedDesc.toString();
}
- public static void println(CommandLine cmd, String message) {
- cmd.getOut().println(message);
+ public void println(String message) {
+ getOutWriter().println(message);
}
public static List parseArgs(String[] rawArgs) throws PropertyException {
@@ -953,7 +832,7 @@ public static List parseArgs(String[] rawArgs) throws PropertyException
return args;
}
- private static void checkChangesInBuildOptionsDuringAutoBuild(PrintWriter out) {
+ public static void checkChangesInBuildOptionsDuringAutoBuild(PrintWriter out) {
StringBuilder options = new StringBuilder();
checkChangesInBuildOptions((key, oldValue, newValue) -> optionChanged(options, key, oldValue, newValue));
@@ -1011,11 +890,26 @@ private static boolean isIgnoredPersistedOption(String key) {
}
public void start() {
- KeycloakMain.start(this, errorHandler);
+ KeycloakMain.start(this, (AbstractNonServerCommand) this.getParsedCommand()
+ .filter(AbstractNonServerCommand.class::isInstance).orElse(null), this.errorHandler);
}
public void build() throws Throwable {
QuarkusEntryPoint.main();
}
+ public void initConfig(AbstractCommand command) {
+ if (Configuration.isInitialized()) {
+ throw new IllegalStateException("Config should not be initialized until profile is determined");
+ }
+ this.parsedCommand = Optional.ofNullable(command);
+
+ String profile = parsedCommand.map(AbstractCommand::getInitProfile)
+ .orElseGet(() -> Optional.ofNullable(org.keycloak.common.util.Environment.getProfile())
+ .orElse(Environment.PROD_PROFILE_VALUE));
+
+ Environment.setProfile(profile);
+ parsedCommand.ifPresent(PropertyMappers::sanitizeDisabledMappers);
+ }
+
}
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 6c7926bd9aa9..586a622f3326 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
@@ -22,16 +22,19 @@
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
+import java.util.concurrent.Callable;
import org.keycloak.config.OptionCategory;
+import org.keycloak.quarkus.runtime.Environment;
import org.keycloak.quarkus.runtime.cli.Picocli;
import org.keycloak.quarkus.runtime.configuration.ConfigArgsConfigSource;
+import org.keycloak.quarkus.runtime.configuration.PersistedConfigSource;
import picocli.CommandLine;
import picocli.CommandLine.Model.CommandSpec;
import picocli.CommandLine.Spec;
-public abstract class AbstractCommand {
+public abstract class AbstractCommand implements Callable {
@Spec
protected CommandSpec spec; // will be null for "start --optimized"
@@ -45,6 +48,52 @@ protected void executionError(CommandLine cmd, String message, Throwable cause)
cliExecutionError(cmd, message, cause);
}
+ /**
+ * Get the effective profile used when the config is initialized
+ */
+ public String getInitProfile() {
+ String configuredProfile = org.keycloak.common.util.Environment.getProfile();
+ if (configuredProfile != null) {
+ return configuredProfile; // the profile was already set by the cli or even ENV
+ }
+ if (Environment.isRebuildCheck()) {
+ // builds default to prod, if the profile is not overriden via the cli
+ return Environment.PROD_PROFILE_VALUE;
+ }
+ // otherwise take the default profile, or what is persisted, or ultimately prod
+ return Optional.ofNullable(this.getDefaultProfile())
+ .or(() -> Optional.ofNullable(
+ PersistedConfigSource.getInstance().getValue(org.keycloak.common.util.Environment.PROFILE)))
+ .orElse(Environment.PROD_PROFILE_VALUE);
+ }
+
+ @Override
+ public Integer call() {
+ return callCommand().orElseGet(() -> {
+ runCommand();
+ return CommandLine.ExitCode.OK;
+ });
+ }
+
+ /**
+ * An alternative to {@link #runCommand()} that allows for returning an exit code.
+ * If the Optional is empty, {@link #runCommand()} will still be called
+ *
+ * see {@link #call()}
+ */
+ protected Optional callCommand() {
+ return Optional.empty();
+ }
+
+ /**
+ * If {@link #callCommand()} returns an empty {@link Optional}, then this method will be used to run the command. OK will be returned as the exit code after successful completion.
+ *
+ * see {@link #call()}
+ */
+ protected void runCommand() {
+
+ }
+
/**
* Returns true if this command should include runtime options for the CLI.
*/
@@ -80,4 +129,12 @@ public void setPicocli(Picocli picocli) {
this.picocli = picocli;
}
+ /**
+ * The default profile for the command, or null if the persisted profile should be checked first
+ * @return
+ */
+ protected String getDefaultProfile() {
+ return Environment.PROD_PROFILE_VALUE;
+ }
+
}
diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/AbstractNonServerCommand.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/AbstractNonServerCommand.java
index dcdf7d43418e..405c31578acf 100644
--- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/AbstractNonServerCommand.java
+++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/AbstractNonServerCommand.java
@@ -26,19 +26,14 @@
import java.util.List;
import java.util.stream.Collectors;
-public abstract class AbstractNonServerCommand extends AbstractStartCommand implements Runnable {
+public abstract class AbstractNonServerCommand extends AbstractStartCommand {
@CommandLine.Mixin
- OptimizedMixin optimizedMixin;
-
- @CommandLine.Mixin
- HelpAllMixin helpAllMixin;
+ OptimizedMixin optimizedMixin = new OptimizedMixin();
@Override
- public void run() {
- Environment.setProfile(Environment.NON_SERVER_MODE);
-
- super.run();
+ public String getDefaultProfile() {
+ return Environment.NON_SERVER_MODE;
}
@Override
diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/AbstractStartCommand.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/AbstractStartCommand.java
index 98c1cd87d321..c69b1ae9f512 100644
--- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/AbstractStartCommand.java
+++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/AbstractStartCommand.java
@@ -17,27 +17,111 @@
package org.keycloak.quarkus.runtime.cli.command;
-import org.keycloak.config.OptionCategory;
-import org.keycloak.quarkus.runtime.configuration.mappers.HostnameV2PropertyMappers;
-import org.keycloak.quarkus.runtime.configuration.mappers.HttpPropertyMappers;
+import static org.keycloak.quarkus.runtime.Environment.isDevMode;
+import static org.keycloak.quarkus.runtime.Environment.isDevProfile;
+import static org.keycloak.quarkus.runtime.Environment.isRebuildCheck;
+import static org.keycloak.quarkus.runtime.configuration.ConfigArgsConfigSource.parseConfigArgs;
+import static org.keycloak.quarkus.runtime.configuration.mappers.PropertyMappers.maskValue;
+import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;
+import java.util.Map;
+import java.util.Optional;
import java.util.stream.Collectors;
+import org.keycloak.config.OptionCategory;
+import org.keycloak.quarkus.runtime.Environment;
+import org.keycloak.quarkus.runtime.cli.Picocli;
+import org.keycloak.quarkus.runtime.configuration.ConfigArgsConfigSource;
+import org.keycloak.quarkus.runtime.configuration.Configuration;
+import org.keycloak.quarkus.runtime.configuration.mappers.HostnameV2PropertyMappers;
+import org.keycloak.quarkus.runtime.configuration.mappers.HttpPropertyMappers;
+import org.keycloak.quarkus.runtime.configuration.mappers.PropertyMapper;
+import org.keycloak.quarkus.runtime.configuration.mappers.PropertyMappers;
+
import picocli.CommandLine;
import picocli.CommandLine.Help.Ansi;
-import static org.keycloak.quarkus.runtime.Environment.isDevProfile;
+public abstract class AbstractStartCommand extends AbstractCommand {
-public abstract class AbstractStartCommand extends AbstractCommand implements Runnable {
public static final String OPTIMIZED_BUILD_OPTION_LONG = "--optimized";
@CommandLine.Mixin
DryRunMixin dryRunMixin = new DryRunMixin();
+ @CommandLine.Mixin
+ HelpAllMixin helpAllMixin;
+
@Override
- public void run() {
+ protected Optional callCommand() {
+ if (isRebuildCheck()) {
+ if (requiresReAugmentation()) {
+ runReAugmentation();
+ }
+ return Optional.of(CommandLine.ExitCode.OK);
+ }
+ return Optional.empty();
+ }
+
+ static boolean requiresReAugmentation() {
+ Map rawPersistedProperties = Configuration.getRawPersistedProperties();
+ if (rawPersistedProperties.isEmpty()) {
+ return true; // no build yet
+ }
+ var current = Picocli.getNonPersistedBuildTimeOptions();
+
+ // everything but the optimized value must match
+ String key = Configuration.KC_OPTIMIZED;
+ Optional.ofNullable(rawPersistedProperties.get(key)).ifPresentOrElse(value -> current.put(key, value), () -> current.remove(key));
+ return !rawPersistedProperties.equals(current);
+ }
+
+ private void runReAugmentation() {
+ if(!isDevMode()) {
+ spec.commandLine().getOut().println("Changes detected in configuration. Updating the server image.");
+ if (Configuration.isOptimized()) {
+ Picocli.checkChangesInBuildOptionsDuringAutoBuild(spec.commandLine().getOut());
+ }
+ }
+
+ directBuild();
+
+ if(!isDevMode()) {
+ spec.commandLine().getOut().printf("Next time you run the server, just run:%n%n\t%s %s %s%n%n", Environment.getCommand(), String.join(" ", getSanitizedRuntimeCliOptions()), OPTIMIZED_BUILD_OPTION_LONG);
+ }
+ }
+
+ public void directBuild() {
+ Build build = new Build();
+ build.dryRunMixin = this.dryRunMixin;
+ build.setPicocli(picocli);
+ build.spec = spec;
+ build.runCommand();
+ }
+
+ /**
+ * checks the raw cli input for possible credentials / properties which should be masked,
+ * and masks them.
+ * @return a list of potentially masked properties in CLI format, e.g. `--db-password=*******`
+ * instead of the actual passwords value.
+ */
+ private static List getSanitizedRuntimeCliOptions() {
+ List properties = new ArrayList<>();
+
+ parseConfigArgs(ConfigArgsConfigSource.getAllCliArgs(), (key, value) -> {
+ PropertyMapper> mapper = PropertyMappers.getMapperByCliKey(key);
+
+ if (mapper == null || mapper.isRunTime()) {
+ properties.add(key + "=" + maskValue(value, mapper));
+ }
+ }, properties::add);
+
+ return properties;
+ }
+
+ @Override
+ protected void runCommand() {
doBeforeRun();
validateConfig();
@@ -45,11 +129,18 @@ public void run() {
picocli.getOutWriter().println(Ansi.AUTO.string(
"@|bold,red Running the server in development mode. DO NOT use this configuration in production.|@"));
}
- if (!Boolean.TRUE.equals(dryRunMixin.dryRun)) {
+ if (shouldStart() && !Boolean.TRUE.equals(dryRunMixin.dryRun)) {
picocli.start();
}
}
+ /**
+ * Controls whether the command actually starts the server
+ */
+ protected boolean shouldStart() {
+ return true;
+ }
+
protected void doBeforeRun() {
}
@@ -57,8 +148,10 @@ protected void doBeforeRun() {
@Override
protected void validateConfig() {
super.validateConfig(); // we want to run the generic validation here first to check for unknown options
- HttpPropertyMappers.validateConfig();
- HostnameV2PropertyMappers.validateConfig(picocli);
+ if (shouldStart()) { // if not starting, we aren't accepting http requests so no validation needed
+ HttpPropertyMappers.validateConfig();
+ HostnameV2PropertyMappers.validateConfig(picocli);
+ }
}
@Override
diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/AbstractUpdatesCommand.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/AbstractUpdatesCommand.java
index 7aeb782ff1dc..755bf9f822ca 100644
--- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/AbstractUpdatesCommand.java
+++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/AbstractUpdatesCommand.java
@@ -19,51 +19,41 @@
import java.io.File;
import java.util.HashMap;
-import java.util.List;
import java.util.Map;
+import java.util.Optional;
import java.util.ServiceLoader;
-import java.util.function.Predicate;
import org.keycloak.Config;
import org.keycloak.common.Profile;
import org.keycloak.compatibility.CompatibilityMetadataProvider;
import org.keycloak.config.ConfigProviderFactory;
-import org.keycloak.config.OptionCategory;
-import org.keycloak.quarkus.runtime.Environment;
import org.keycloak.quarkus.runtime.cli.PropertyException;
import picocli.CommandLine;
-public abstract class AbstractUpdatesCommand extends AbstractCommand implements Runnable {
+public abstract class AbstractUpdatesCommand extends AbstractStartCommand {
private static final int FEATURE_DISABLED_EXIT_CODE = 4;
@CommandLine.Mixin
- HelpAllMixin helpAllMixin;
-
- @CommandLine.Mixin
- OptimizedMixin optimizedMixin;
+ OptimizedMixin optimizedMixin = new OptimizedMixin();
@Override
- public List getOptionCategories() {
- return super.getOptionCategories().stream()
- .filter(Predicate.not(OptionCategory.EXPORT::equals))
- .filter(Predicate.not(OptionCategory.IMPORT::equals))
- .toList();
+ protected boolean shouldStart() {
+ return false;
}
@Override
- public void run() {
- Environment.updateProfile(true);
- if (!Profile.isAnyVersionOfFeatureEnabled(Profile.Feature.ROLLING_UPDATES_V1)) {
- printFeatureDisabled();
- picocli.exit(FEATURE_DISABLED_EXIT_CODE);
- return;
- }
- loadConfiguration();
- printPreviewWarning();
- validateConfig();
- var exitCode = executeAction();
- picocli.exit(exitCode);
+ protected Optional callCommand() {
+ return super.callCommand().or(() -> {
+ if (!Profile.isAnyVersionOfFeatureEnabled(Profile.Feature.ROLLING_UPDATES_V1)) {
+ printFeatureDisabled();
+ return Optional.of(FEATURE_DISABLED_EXIT_CODE);
+ }
+ loadConfiguration();
+ printPreviewWarning();
+ validateConfig();
+ return Optional.of(executeAction());
+ });
}
abstract int executeAction();
@@ -74,33 +64,14 @@ static void validateFileIsNotDirectory(File file, String option) {
}
}
- void printOut(String message) {
- var cmd = getCommandLine();
- if (cmd.isPresent()) {
- cmd.get().getOut().println(message);
- } else {
- System.out.println(message);
- }
- }
-
- void printError(String message) {
- var cmd = getCommandLine();
- if (cmd.isPresent()) {
- var colorScheme = cmd.get().getColorScheme();
- cmd.get().getErr().println(colorScheme.errorText(message));
- } else {
- System.err.println(message);
- }
- }
-
private void printPreviewWarning() {
if (Profile.isFeatureEnabled(Profile.Feature.ROLLING_UPDATES_V2) && (Profile.Feature.ROLLING_UPDATES_V2.getType() == Profile.Feature.Type.PREVIEW || Profile.Feature.ROLLING_UPDATES_V2.getType() == Profile.Feature.Type.EXPERIMENTAL)) {
- printError("Warning! This command is '" + Profile.Feature.ROLLING_UPDATES_V2.getType() + "' and is not recommended for use in production. It may change or be removed at a future release.");
+ picocli.error("Warning! This command is '" + Profile.Feature.ROLLING_UPDATES_V2.getType() + "' and is not recommended for use in production. It may change or be removed at a future release.");
}
}
void printFeatureDisabled() {
- printError("Unable to use this command. None of the versions of the feature '" + Profile.Feature.ROLLING_UPDATES_V1.getUnversionedKey() + "' is enabled.");
+ picocli.error("Unable to use this command. None of the versions of the feature '" + Profile.Feature.ROLLING_UPDATES_V1.getUnversionedKey() + "' is enabled.");
}
static Map loadAllProviders() {
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 a697f1a98c1d..c013ac89b93b 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
@@ -21,7 +21,6 @@
import static org.keycloak.config.DatabaseOptions.DB;
import static org.keycloak.quarkus.runtime.Environment.getHomePath;
import static org.keycloak.quarkus.runtime.Environment.isDevProfile;
-import static org.keycloak.quarkus.runtime.cli.Picocli.println;
import io.quarkus.runtime.LaunchMode;
@@ -58,7 +57,7 @@
+ " $ ${PARENT-COMMAND-FULL-NAME:-$PARENTCOMMAND} ${COMMAND-NAME} --metrics-enabled=true%n%n"
+ " Change the relative path:%n%n"
+ " $ ${PARENT-COMMAND-FULL-NAME:-$PARENTCOMMAND} ${COMMAND-NAME} --http-relative-path=/auth%n")
-public final class Build extends AbstractCommand implements Runnable {
+public final class Build extends AbstractCommand {
public static final String NAME = "build";
@@ -69,10 +68,7 @@ public final class Build extends AbstractCommand implements Runnable {
DryRunMixin dryRunMixin;
@Override
- public void run() {
- if (org.keycloak.common.util.Environment.getProfile() == null) {
- Environment.setProfile(Environment.PROD_PROFILE_VALUE);
- }
+ protected void runCommand() {
checkProfileAndDb();
// validate before setting that we're rebuilding so that runtime options are still seen
@@ -82,7 +78,7 @@ public void run() {
});
System.setProperty("quarkus.launch.rebuild", "true");
- println(spec.commandLine(), "Updating the configuration and installing your custom providers, if any. Please wait.");
+ picocli.println("Updating the configuration and installing your custom providers, if any. Please wait.");
try {
configureBuildClassLoader();
@@ -95,8 +91,8 @@ public void run() {
}
if (!isDevProfile()) {
- println(spec.commandLine(), "Server configuration updated and persisted. Run the following command to review the configuration:\n");
- println(spec.commandLine(), "\t" + Environment.getCommand() + " show-config\n");
+ picocli.println("Server configuration updated and persisted. Run the following command to review the configuration:\n");
+ picocli.println("\t" + Environment.getCommand() + " show-config\n");
}
} catch (Throwable throwable) {
executionError(spec.commandLine(), "Failed to update server configuration.", throwable);
@@ -119,7 +115,7 @@ public boolean includeBuildTime() {
private void checkProfileAndDb() {
if (Environment.isDevProfile()) {
- String cmd = Environment.getParsedCommand().map(AbstractCommand::getName).orElse(getName());
+ String cmd = picocli.getParsedCommand().map(AbstractCommand::getName).orElse(getName());
// we allow start-dev, and import|export|bootstrap-admin --profile=dev
// but not start --profile=dev, nor build --profile=dev
if (Start.NAME.equals(cmd) || Build.NAME.equals(cmd)) {
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 9bf397c9b4a9..7264af3f17fd 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
@@ -29,7 +29,7 @@
@Command(name = Export.NAME,
header = "Export data from realms to a file or directory.",
description = "%nExport data from realms to a file or directory.")
-public final class Export extends AbstractNonServerCommand implements Runnable {
+public final class Export extends AbstractNonServerCommand {
public static final String NAME = "export";
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 cc606a06b9ef..e1a6b53301e0 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
@@ -29,7 +29,7 @@
@Command(name = Import.NAME,
header = "Import data from a directory or a file.",
description = "%nImport data from a directory or a file.")
-public final class Import extends AbstractNonServerCommand implements Runnable {
+public final class Import extends AbstractNonServerCommand {
public static final String NAME = "import";
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 415d0534ed96..cc90cfd8cec8 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,7 +41,7 @@
@Command(name = "show-config",
header = "Print out the current configuration.",
description = "%nPrint out the current configuration.")
-public final class ShowConfig extends AbstractCommand implements Runnable {
+public final class ShowConfig extends AbstractCommand {
public static final String NAME = "show-config";
private static final List allowedSystemPropertyKeys = List.of(
@@ -54,8 +54,13 @@ public final class ShowConfig extends AbstractCommand implements Runnable {
String filter;
@Override
- public void run() {
- String profile = Environment.updateProfile(true);
+ public String getDefaultProfile() {
+ return null;
+ }
+
+ @Override
+ protected void runCommand() {
+ String profile = org.keycloak.common.util.Environment.getProfile();
spec.commandLine().getOut().printf("Current Mode: %s%n", Environment.getKeycloakModeFromProfile(profile));
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 3221d26dcae8..a6f7b67da573 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
@@ -24,7 +24,6 @@
import org.keycloak.common.profile.ProfileException;
import org.keycloak.quarkus.runtime.cli.Picocli;
import org.keycloak.quarkus.runtime.cli.PropertyException;
-import org.keycloak.quarkus.runtime.configuration.mappers.PropertyMappers;
import picocli.CommandLine;
import picocli.CommandLine.Command;
@@ -37,7 +36,7 @@
footer = "%nBy default, this command tries to update the server configuration by running a '" + Build.NAME + "' before starting the server. You can disable this behavior by using the '" + OPTIMIZED_BUILD_OPTION_LONG + "' option:%n%n"
+ " $ ${PARENT-COMMAND-FULL-NAME:-$PARENTCOMMAND} ${COMMAND-NAME} '" + OPTIMIZED_BUILD_OPTION_LONG + "'%n%n"
+ "By doing that, the server should start faster based on any previous configuration you have set when manually running the '" + Build.NAME + "' command.")
-public final class Start extends AbstractStartCommand implements Runnable {
+public final class Start extends AbstractStartCommand {
public static final String NAME = "start";
@@ -47,12 +46,8 @@ public final class Start extends AbstractStartCommand implements Runnable {
@CommandLine.Mixin
ImportRealmMixin importRealmMixin;
- @CommandLine.Mixin
- HelpAllMixin helpAllMixin;
-
@Override
protected void doBeforeRun() {
- Environment.updateProfile(true);
if (Environment.isDevProfile()) {
throw new PropertyException(Messages.devProfileNotAllowedError(NAME));
}
@@ -71,12 +66,11 @@ public String getName() {
public static void fastStart(Picocli picocli, boolean dryRun) {
try {
Start start = new Start();
- Environment.setParsedCommand(start);
- PropertyMappers.sanitizeDisabledMappers();
start.optimizedMixin.optimized = true;
start.dryRunMixin.dryRun = dryRun;
start.setPicocli(picocli);
- start.run();
+ picocli.initConfig(start);
+ picocli.exit(start.call());
} catch (PropertyException | ProfileException e) {
picocli.usageException(e.getMessage(), e.getCause());
}
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 60af84ae1da7..0a2fee46a9a2 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
@@ -17,11 +17,10 @@
package org.keycloak.quarkus.runtime.cli.command;
-import org.keycloak.quarkus.runtime.Environment;
+import org.keycloak.common.util.Environment;
import picocli.CommandLine;
import picocli.CommandLine.Command;
-import picocli.CommandLine.Mixin;
@Command(name = StartDev.NAME,
header = "Start the server in development mode.",
@@ -30,19 +29,21 @@
},
footer = "%nDo NOT start the server using this command when deploying to production.%n%n"
+ "Use '${PARENT-COMMAND-FULL-NAME:-$PARENTCOMMAND} ${COMMAND-NAME} --help-all' to list all available options, including build options.")
-public final class StartDev extends AbstractStartCommand implements Runnable {
+public final class StartDev extends AbstractStartCommand {
public static final String NAME = "start-dev";
- @Mixin
- HelpAllMixin helpAllMixin;
-
@CommandLine.Mixin
ImportRealmMixin importRealmMixin;
@Override
- protected void doBeforeRun() {
- Environment.forceDevProfile();
+ public String getInitProfile() {
+ return Environment.DEV_PROFILE_VALUE; // only ever dev - could be a validation instead
+ }
+
+ @Override
+ public String getDefaultProfile() {
+ return Environment.DEV_PROFILE_VALUE;
}
@Override
diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/UpdateCompatibilityCheck.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/UpdateCompatibilityCheck.java
index 53c0c2220966..853fdbae22bb 100644
--- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/UpdateCompatibilityCheck.java
+++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/UpdateCompatibilityCheck.java
@@ -55,20 +55,20 @@ int executeAction() {
var id = idIterator.next();
var provider = providers.get(id);
if (provider == null) {
- printError("[%s] Provider not found. Rolling Update is not available.".formatted(id));
+ picocli.error("[%s] Provider not found. Rolling Update is not available.".formatted(id));
return CompatibilityResult.ExitCode.RECREATE.value();
}
var result = provider.isCompatible(Map.copyOf(info.getOrDefault(id, Map.of())));
- result.endMessage().ifPresent(this::printOut);
+ result.endMessage().ifPresent(picocli.getOutWriter()::println);
if (Util.isNotCompatible(result)) {
- result.errorMessage().ifPresent(this::printError);
+ result.errorMessage().ifPresent(picocli::error);
return result.exitCode();
}
}
- printOut("[OK] Rolling Update is available.");
+ picocli.getOutWriter().println("[OK] Rolling Update is available.");
return CompatibilityResult.ExitCode.ROLLING.value();
}
diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/UpdateCompatibilityMetadata.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/UpdateCompatibilityMetadata.java
index 2672cac0e4b5..4d88b203f7db 100644
--- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/UpdateCompatibilityMetadata.java
+++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/UpdateCompatibilityMetadata.java
@@ -83,7 +83,7 @@ private void validateFileParameter() {
private void printToConsole(Map> metadata) {
try {
var json = JsonSerialization.mapper.writerWithDefaultPrettyPrinter().writeValueAsString(metadata);
- printOut("Metadata:%n%s".formatted(json));
+ picocli.getOutWriter().println("Metadata:%n%s".formatted(json));
} catch (JsonProcessingException e) {
throw new PropertyException("Unable to create JSON representation of the metadata", e);
}
diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/Configuration.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/Configuration.java
index 7ee801bb1833..19ee8706f48f 100644
--- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/Configuration.java
+++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/Configuration.java
@@ -89,6 +89,10 @@ public static boolean equals(Option> option, String value) {
.isPresent();
}
+ public static boolean isInitialized() {
+ return config != null;
+ }
+
public static synchronized SmallRyeConfig getConfig() {
if (config == null) {
config = ConfigUtils.emptyConfigBuilder().addDiscoveredSources().build();
diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/CachingPropertyMappers.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/CachingPropertyMappers.java
index 4cf15fd6c1d1..a9aed65de6f4 100644
--- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/CachingPropertyMappers.java
+++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/CachingPropertyMappers.java
@@ -1,5 +1,8 @@
package org.keycloak.quarkus.runtime.configuration.mappers;
+import static org.keycloak.quarkus.runtime.configuration.Configuration.getOptionalKcValue;
+import static org.keycloak.quarkus.runtime.configuration.mappers.PropertyMapper.fromOption;
+
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Paths;
@@ -8,8 +11,6 @@
import java.util.Optional;
import java.util.function.BooleanSupplier;
-import com.google.common.base.CaseFormat;
-import io.smallrye.config.ConfigSourceInterceptorContext;
import org.keycloak.common.Profile;
import org.keycloak.config.CachingOptions;
import org.keycloak.config.Option;
@@ -19,8 +20,9 @@
import org.keycloak.quarkus.runtime.configuration.Configuration;
import org.keycloak.utils.StringUtil;
-import static org.keycloak.quarkus.runtime.configuration.Configuration.getOptionalKcValue;
-import static org.keycloak.quarkus.runtime.configuration.mappers.PropertyMapper.fromOption;
+import com.google.common.base.CaseFormat;
+
+import io.smallrye.config.ConfigSourceInterceptorContext;
final class CachingPropertyMappers {
@@ -98,6 +100,26 @@ public static PropertyMapper>[] getClusteringPropertyMappers() {
.isEnabled(() -> Configuration.isTrue(CachingOptions.CACHE_EMBEDDED_MTLS_ENABLED), "property '%s' is enabled".formatted(CachingOptions.CACHE_EMBEDDED_MTLS_ENABLED.getKey()))
.validator(CachingPropertyMappers::validateCertificateRotationIsPositive)
.build(),
+ fromOption(CachingOptions.CACHE_EMBEDDED_NETWORK_BIND_ADDRESS)
+ .paramLabel("address")
+ .to("kc.spi-cache-embedded--default--network-bind-address")
+ .isEnabled(CachingPropertyMappers::cacheSetToInfinispan, "Infinispan clustered embedded is enabled")
+ .build(),
+ fromOption(CachingOptions.CACHE_EMBEDDED_NETWORK_BIND_PORT)
+ .paramLabel("port")
+ .to("kc.spi-cache-embedded--default--network-bind-port")
+ .isEnabled(CachingPropertyMappers::cacheSetToInfinispan, "Infinispan clustered embedded is enabled")
+ .build(),
+ fromOption(CachingOptions.CACHE_EMBEDDED_NETWORK_EXTERNAL_ADDRESS)
+ .paramLabel("address")
+ .to("kc.spi-cache-embedded--default--network-external-address")
+ .isEnabled(CachingPropertyMappers::cacheSetToInfinispan, "Infinispan clustered embedded is enabled")
+ .build(),
+ fromOption(CachingOptions.CACHE_EMBEDDED_NETWORK_EXTERNAL_PORT)
+ .paramLabel("port")
+ .isEnabled(CachingPropertyMappers::cacheSetToInfinispan, "Infinispan clustered embedded is enabled")
+ .to("kc.spi-cache-embedded--default--network-external-port")
+ .build(),
fromOption(CachingOptions.CACHE_REMOTE_HOST)
.paramLabel("hostname")
.to("kc.spi-cache-remote--default--hostname")
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 8c12ffa3e441..986ffce47c4c 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
@@ -15,10 +15,12 @@
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.Supplier;
+import static org.keycloak.config.DatabaseOptions.DB;
import static org.keycloak.config.DatabaseOptions.OPTIONS_DATASOURCES;
import static org.keycloak.config.DatabaseOptions.getDatasourceOption;
import static org.keycloak.config.DatabaseOptions.getKeyForDatasource;
@@ -51,6 +53,11 @@ public static PropertyMapper>[] getDatabasePropertyMappers() {
.mapFrom(DatabaseOptions.DB, DatabasePropertyMappers::getDatabaseUrl)
.paramLabel("jdbc-url")
.build(),
+ fromOption(DatabaseOptions.DB_POSTGRESQL_TARGET_SERVER_TYPE)
+ .to("quarkus.datasource.jdbc.additional-jdbc-properties.targetServerType")
+ .mapFrom(DatabaseOptions.DB, DatabasePropertyMappers::getPostgresqlTargetServerType)
+ .isEnabled(() -> getPostgresqlTargetServerType(Configuration.getConfigValue(DB).getValue(), null) != null)
+ .build(),
fromOption(DatabaseOptions.DB_URL_HOST)
.paramLabel("hostname")
.build(),
@@ -114,6 +121,33 @@ public static PropertyMapper>[] getDatabasePropertyMappers() {
.description("Used for internal purposes of H2 database.")
.build();
+ private static String getPostgresqlTargetServerType(String db, ConfigSourceInterceptorContext context) {
+ Database.Vendor vendor = Database.getVendor(db).orElse(null);
+ if (vendor != Database.Vendor.POSTGRES) {
+ return null;
+ }
+
+ String dbDriver = Configuration.getConfigValue(DatabaseOptions.DB_DRIVER).getValue();
+ String dbUrl = Configuration.getConfigValue(DatabaseOptions.DB_URL).getValue();
+ String dbUrlProperties = Configuration.getConfigValue(DatabaseOptions.DB_URL_PROPERTIES).getValue();
+
+ if (!Objects.equals(Database.getDriver(db, true).orElse(null), dbDriver) &&
+ !Objects.equals(Database.getDriver(db, false).orElse(null), dbDriver)) {
+ // Custom JDBC-Driver, for example, AWS JDBC Wrapper.
+ return null;
+ }
+ if (dbUrlProperties != null && dbUrl != null && dbUrl.contains("${kc.db-url-properties:}") && dbUrlProperties.contains("targetServerType")) {
+ // targetServerType already set to same or different value in db-url-properties, ignore
+ return null;
+ }
+ if (dbUrl != null && dbUrl.contains("targetServerType")) {
+ // targetServerType already set to same or different value in db-url, ignore
+ return null;
+ }
+ log.debug("setting targetServerType for PostgreSQL to 'primary'");
+ return "primary";
+ }
+
private static String getDatabaseUrl(String name, String value, ConfigSourceInterceptorContext c) {
return Database.getDefaultUrl(name, value).orElse(null);
}
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 80b1e5a2ae4d..7519c1b74d9f 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
@@ -93,8 +93,7 @@ public static PropertyMapper>[] getHttpPropertyMappers() {
.build(),
fromOption(HttpOptions.HTTPS_CERTIFICATES_RELOAD_PERIOD)
.to("quarkus.http.ssl.certificate.reload-period")
- // -1 means no reload
- .transformer((value, context) -> "-1".equals(value) ? null : value)
+ .transformer(HttpPropertyMappers::transformNegativeReloadPeriod)
.paramLabel("reload period")
.build(),
fromOption(HttpOptions.HTTPS_CERTIFICATE_FILE)
@@ -178,6 +177,11 @@ private static String getHttpEnabledTransformer(String value, ConfigSourceInterc
return isHttpEnabled(value) ? "enabled" : "disabled";
}
+ static String transformNegativeReloadPeriod(String value, ConfigSourceInterceptorContext context) {
+ // -1 means no reload
+ return "-1".equals(value) ? null : value;
+ }
+
private static boolean isHttpEnabled(String value) {
if (Environment.isDevMode() || Environment.isNonServerMode()) {
return true;
diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/ManagementPropertyMappers.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/ManagementPropertyMappers.java
index 7815bc6d9e23..e973a097419d 100644
--- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/ManagementPropertyMappers.java
+++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/ManagementPropertyMappers.java
@@ -72,8 +72,7 @@ public static PropertyMapper>[] getManagementPropertyMappers() {
fromOption(ManagementOptions.HTTPS_MANAGEMENT_CERTIFICATES_RELOAD_PERIOD)
.mapFrom(HttpOptions.HTTPS_CERTIFICATES_RELOAD_PERIOD)
.to("quarkus.management.ssl.certificate.reload-period")
- // -1 means no reload
- .transformer((value, context) -> "-1".equals(value) ? null : value)
+ .transformer(HttpPropertyMappers::transformNegativeReloadPeriod)
.paramLabel("reload period")
.build(),
fromOption(ManagementOptions.HTTPS_MANAGEMENT_CERTIFICATE_FILE)
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 bb432027b513..2f76d86203fc 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
@@ -258,7 +258,8 @@ private ConfigValue transformValue(String name, ConfigValue configValue, ConfigS
String mappedValue = value;
boolean mapped = false;
- var theMapper = parentValue ? this.parentMapper : this.mapper;
+ // use parent mapper/transformer when no mapper is explicitly specified in .mapFrom()
+ var theMapper = parentValue && parentMapper != null ? this.parentMapper : this.mapper;
if (theMapper != null && (!name.equals(getFrom()) || parentValue)) {
mappedValue = theMapper.map(getNamedProperty().orElse(null), value, context);
mapped = true;
diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/PropertyMappers.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/PropertyMappers.java
index 3fc5e9214dc0..ee914a182f52 100644
--- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/PropertyMappers.java
+++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/PropertyMappers.java
@@ -12,7 +12,6 @@
import org.keycloak.quarkus.runtime.cli.Picocli;
import org.keycloak.quarkus.runtime.cli.PropertyException;
import org.keycloak.quarkus.runtime.cli.command.AbstractCommand;
-import org.keycloak.quarkus.runtime.cli.command.Build;
import org.keycloak.quarkus.runtime.cli.command.ShowConfig;
import org.keycloak.quarkus.runtime.configuration.DisabledMappersInterceptor;
import org.keycloak.quarkus.runtime.configuration.MicroProfileConfigProvider;
@@ -34,7 +33,6 @@
import java.util.Set;
import java.util.stream.Collectors;
-import static org.keycloak.quarkus.runtime.Environment.isParsedCommand;
import static org.keycloak.quarkus.runtime.Environment.isRebuild;
import static org.keycloak.quarkus.runtime.Environment.isRebuildCheck;
import static org.keycloak.quarkus.runtime.configuration.KeycloakConfigSourceProvider.isKeyStoreConfigSource;
@@ -138,8 +136,8 @@ public static Map> getDisabledBuildTimeMappers() {
/**
* Removes all disabled mappers from the runtime/buildtime mappers
*/
- public static void sanitizeDisabledMappers() {
- MAPPERS.sanitizeDisabledMappers();
+ public static void sanitizeDisabledMappers(AbstractCommand command) {
+ MAPPERS.sanitizeDisabledMappers(command);
}
public static String maskValue(String value, PropertyMapper> mapper) {
@@ -226,11 +224,8 @@ public static boolean isDisabledMapper(String property) {
return getDisabledMapper(property).isPresent() && getMapper(property) == null;
}
- private static Set> filterDeniedCategories(List> mappers) {
- final var allowedCategories = Environment.getParsedCommand()
- .map(AbstractCommand::getOptionCategories)
- .map(EnumSet::copyOf)
- .orElseGet(() -> EnumSet.allOf(OptionCategory.class));
+ private static Set> filterDeniedCategories(List> mappers, AbstractCommand command) {
+ final var allowedCategories = EnumSet.copyOf(command.getOptionCategories());
return mappers.stream().filter(f -> allowedCategories.contains(f.getCategory())).collect(Collectors.toSet());
}
@@ -326,11 +321,7 @@ public Set> getWildcardMappers() {
return Collections.unmodifiableSet(wildcardMappers);
}
- public void sanitizeDisabledMappers() {
- if (Environment.getParsedCommand().isEmpty()) {
- return; // do not sanitize when no command is present
- }
-
+ public void sanitizeDisabledMappers(AbstractCommand command) {
DisabledMappersInterceptor.runWithDisabled(() -> { // We need to have the whole configuration available
// Initialize profile in order to check state of features. Disable Persisted CS for re-augmentation
@@ -343,22 +334,22 @@ public void sanitizeDisabledMappers() {
sanitizeMappers(buildTimeMappers, disabledBuildTimeMappers);
sanitizeMappers(runtimeTimeMappers, disabledRuntimeMappers);
- assertDuplicatedMappers();
+ assertDuplicatedMappers(command);
});
}
- private void assertDuplicatedMappers() {
+ private void assertDuplicatedMappers(AbstractCommand command) {
final var duplicatedMappers = entrySet().stream()
.filter(e -> CollectionUtil.isNotEmpty(e.getValue()))
.filter(e -> e.getValue().size() > 1)
.toList();
- final var isBuildPhase = isRebuild() || isRebuildCheck() || isParsedCommand(Build.NAME);
- final var allowedForCommand = isParsedCommand(ShowConfig.NAME);
+ final var isBuildPhase = isRebuild() || isRebuildCheck() || command.includeBuildTime();
+ final var allowedForCommand = ShowConfig.NAME.equals(command.getName());
if (!duplicatedMappers.isEmpty()) {
duplicatedMappers.forEach(f -> {
- final var filteredMappers = filterDeniedCategories(f.getValue());
+ final var filteredMappers = filterDeniedCategories(f.getValue(), command);
if (filteredMappers.size() > 1) {
final var areBuildTimeMappers = filteredMappers.stream().anyMatch(PropertyMapper::isBuildTime);
diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/WildcardPropertyMapper.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/WildcardPropertyMapper.java
index 83da1cc8202d..885dfd9224a4 100644
--- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/WildcardPropertyMapper.java
+++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/WildcardPropertyMapper.java
@@ -9,10 +9,9 @@
import java.util.regex.Pattern;
import java.util.stream.Stream;
+import org.keycloak.config.LoggingOptions;
import org.keycloak.config.Option;
-import org.keycloak.quarkus.runtime.configuration.MicroProfileConfigProvider;
-import io.smallrye.config.ConfigSourceInterceptorContext;
import io.smallrye.config.ConfigValue;
import static org.keycloak.quarkus.runtime.configuration.MicroProfileConfigProvider.NS_KEYCLOAK_PREFIX;
@@ -30,6 +29,7 @@ public class WildcardPropertyMapper extends PropertyMapper {
private final String fromPrefix;
private String toPrefix;
private String toSuffix;
+ private Character replacementChar = null;
public WildcardPropertyMapper(Option option, String to, BooleanSupplier enabled, String enabledWhen, ValueMapper mapper, String mapFrom, ValueMapper parentMapper,
String paramLabel, boolean mask, BiConsumer, ConfigValue> validator,
@@ -42,6 +42,10 @@ public WildcardPropertyMapper(Option option, String to, BooleanSupplier enabl
throw new IllegalArgumentException("Invalid wildcard from format. Wildcard must be at the end of the option.");
}
+ if (option == LoggingOptions.LOG_LEVEL_CATEGORY) {
+ replacementChar = '.';
+ }
+
if (to != null) {
if (!to.startsWith(NS_QUARKUS_PREFIX) && !to.startsWith(NS_KEYCLOAK_PREFIX)) {
throw new IllegalArgumentException("Wildcards should map to Quarkus or Keycloak options (option '%s' mapped to '%s'). If not, PropertyMappers logic will need adjusted".formatted(option.getKey(), to));
@@ -119,7 +123,10 @@ public boolean matchesWildcardOptionName(String name) {
public Optional getKcKeyForEnvKey(String envKey, String transformedKey) {
if (transformedKey.startsWith(fromPrefix)) {
- return Optional.ofNullable(getFrom(envKey.substring(fromPrefix.length()).toLowerCase().replace("_", ".")));
+ if (replacementChar != null) {
+ return Optional.ofNullable(getFrom(envKey.substring(fromPrefix.length()).toLowerCase().replace('_', replacementChar)));
+ }
+ return Optional.of(transformedKey);
}
return Optional.empty();
}
diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/jaxrs/QuarkusKeycloakApplication.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/jaxrs/QuarkusKeycloakApplication.java
index b3ca09965901..85d6a296f8db 100644
--- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/jaxrs/QuarkusKeycloakApplication.java
+++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/jaxrs/QuarkusKeycloakApplication.java
@@ -21,8 +21,6 @@
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.platform.Platform;
-import org.keycloak.quarkus.runtime.Environment;
-import org.keycloak.quarkus.runtime.cli.command.AbstractNonServerCommand;
import org.keycloak.quarkus.runtime.configuration.Configuration;
import org.keycloak.quarkus.runtime.configuration.MicroProfileConfigProvider;
import org.keycloak.quarkus.runtime.configuration.PropertyMappingInterceptor;
@@ -50,11 +48,6 @@ void onStartupEvent(@Observes StartupEvent event) {
QuarkusPlatform platform = (QuarkusPlatform) Platform.getPlatform();
platform.started();
startup();
- Environment.getParsedCommand().ifPresent(ac -> {
- if (ac instanceof AbstractNonServerCommand) {
- ((AbstractNonServerCommand)ac).onStart(this);
- }
- });
}
void onShutdownEvent(@Observes ShutdownEvent event) {
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 e4ce136cac8f..c4bec4e17910 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
@@ -33,7 +33,6 @@
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
-import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.function.Consumer;
@@ -45,8 +44,10 @@
import org.keycloak.config.LoggingOptions;
import org.keycloak.quarkus.runtime.Environment;
import org.keycloak.quarkus.runtime.KeycloakMain;
+import org.keycloak.quarkus.runtime.cli.command.AbstractCommand;
import org.keycloak.quarkus.runtime.configuration.AbstractConfigurationTest;
-import org.keycloak.quarkus.runtime.configuration.ConfigArgsConfigSource;
+import org.keycloak.quarkus.runtime.configuration.Configuration;
+import org.keycloak.quarkus.runtime.configuration.KeycloakConfigSourceProvider;
import io.smallrye.config.SmallRyeConfig;
import picocli.CommandLine;
@@ -100,9 +101,10 @@ public void start() {
}
@Override
- protected void initProfile(List cliArgs, String currentCommandName) {
- super.initProfile(cliArgs, currentCommandName);
- config = createConfig();
+ public void initConfig(AbstractCommand command) {
+ KeycloakConfigSourceProvider.reload();
+ super.initConfig(command);
+ config = Configuration.getConfig();
}
@Override
@@ -115,22 +117,43 @@ public void build() throws Throwable {
NonRunningPicocli pseudoLaunch(String... args) {
NonRunningPicocli nonRunningPicocli = new NonRunningPicocli();
- ConfigArgsConfigSource.setCliArgs(args);
- nonRunningPicocli.config = createConfig();
KeycloakMain.main(args, nonRunningPicocli);
return nonRunningPicocli;
}
+ @Test
+ public void testUnbuiltHelp() {
+ NonRunningPicocli nonRunningPicocli = pseudoLaunch("bootstrap-admin");
+ assertTrue(nonRunningPicocli.getErrString().contains("Missing required subcommand"));
+ }
+
+ @Test
+ public void testProfileForHelp() {
+ NonRunningPicocli nonRunningPicocli = pseudoLaunch("-pf=dev", "bootstrap-admin", "-h");
+ assertEquals("dev", nonRunningPicocli.config.getConfigValue("kc.profile").getValue());
+ }
+
+ @Test
+ public void testCleanStartDev() {
+ NonRunningPicocli nonRunningPicocli = pseudoLaunch("start-dev");
+ assertFalse(nonRunningPicocli.getOutString(), nonRunningPicocli.getOutString().toUpperCase().contains("WARN"));
+ assertFalse(nonRunningPicocli.getOutString(), nonRunningPicocli.getOutString().toUpperCase().contains("ERROR"));
+ }
+
@Test
public void testNegativeArgument() {
NonRunningPicocli nonRunningPicocli = pseudoLaunch("start-dev");
assertEquals(CommandLine.ExitCode.OK, nonRunningPicocli.exitCode);
assertEquals("1h",
nonRunningPicocli.config.getConfigValue("quarkus.http.ssl.certificate.reload-period").getValue());
+ assertEquals("1h",
+ nonRunningPicocli.config.getConfigValue("quarkus.management.ssl.certificate.reload-period").getValue());
+ onAfter();
nonRunningPicocli = pseudoLaunch("start-dev", "--https-certificates-reload-period=-1");
assertEquals(CommandLine.ExitCode.OK, nonRunningPicocli.exitCode);
assertNull(nonRunningPicocli.config.getConfigValue("quarkus.http.ssl.certificate.reload-period").getValue());
+ assertNull(nonRunningPicocli.config.getConfigValue("quarkus.management.ssl.certificate.reload-period").getValue());
}
@Test
@@ -140,10 +163,12 @@ public void testNegativeArgumentMgmtInterfaceCertReload() {
assertEquals("1h",
nonRunningPicocli.config.getConfigValue("quarkus.management.ssl.certificate.reload-period").getValue());
+ onAfter();
nonRunningPicocli = pseudoLaunch("start-dev", "--https-management-certificates-reload-period=-1");
assertEquals(CommandLine.ExitCode.OK, nonRunningPicocli.exitCode);
assertNull(nonRunningPicocli.config.getConfigValue("quarkus.management.ssl.certificate.reload-period").getValue());
+ onAfter();
nonRunningPicocli = pseudoLaunch("start-dev", "--https-certificates-reload-period=5m");
assertEquals(CommandLine.ExitCode.OK, nonRunningPicocli.exitCode);
assertEquals("5m",
@@ -287,7 +312,7 @@ public void spiRuntimeWarnWithBuild() {
@Test
public void failBuildDev() {
- NonRunningPicocli nonRunningPicocli = pseudoLaunch("--profile=dev", "build");
+ NonRunningPicocli nonRunningPicocli = pseudoLaunch("--profile=dev", "build", "--verbose");
assertThat(nonRunningPicocli.getErrString(), containsString("You can not 'build' the server in development mode."));
assertEquals(CommandLine.ExitCode.SOFTWARE, nonRunningPicocli.exitCode);
}
@@ -341,7 +366,7 @@ private NonRunningPicocli build(Consumer outChecker, String... args) {
Environment.setRebuildCheck(); // auto-build
}
NonRunningPicocli nonRunningPicocli = pseudoLaunch(args);
- assertTrue(nonRunningPicocli.reaug);
+ assertTrue(nonRunningPicocli.getErrString(), nonRunningPicocli.reaug);
assertEquals(nonRunningPicocli.getErrString(), CommandLine.ExitCode.OK, nonRunningPicocli.exitCode);
outChecker.accept(nonRunningPicocli.getOutString());
onAfter();
@@ -401,7 +426,7 @@ public void testNoReaugFromDevToDevExport() {
@Test
public void testReaugFromDevToProdExport() {
- build("start-dev");
+ build("start-dev", "-v");
Environment.setRebuildCheck(); // will be reset by the system properties logic
NonRunningPicocli nonRunningPicocli = pseudoLaunch("export", "--db=dev-file", "--file=file");
@@ -429,7 +454,7 @@ public void fastStartOptimizedSucceeds() {
System.setProperty("kc.hostname-strict", "false");
NonRunningPicocli nonRunningPicocli = pseudoLaunch("start", "--optimized");
- assertEquals(Integer.MAX_VALUE, nonRunningPicocli.exitCode); // "running" state
+ assertEquals(CommandLine.ExitCode.OK, nonRunningPicocli.exitCode);
}
@Test
@@ -478,10 +503,12 @@ public void syslogMaxLengthMemorySize() {
NonRunningPicocli nonRunningPicocli = pseudoLaunch("start-dev", "--log=syslog", "--log-syslog-max-length=60k");
assertEquals(CommandLine.ExitCode.OK, nonRunningPicocli.exitCode);
assertEquals("60k", nonRunningPicocli.config.getConfigValue("quarkus.log.syslog.max-length").getValue());
+ onAfter();
nonRunningPicocli = pseudoLaunch("start-dev", "--log=syslog");
assertEquals(CommandLine.ExitCode.OK, nonRunningPicocli.exitCode);
assertThat(nonRunningPicocli.config.getConfigValue("quarkus.log.syslog.max-length").getValue(), nullValue());
+ onAfter();
nonRunningPicocli = pseudoLaunch("start-dev", "--log=syslog", "--log-syslog-max-length=wrong");
assertEquals(CommandLine.ExitCode.USAGE, nonRunningPicocli.exitCode);
@@ -496,18 +523,22 @@ public void syslogCountingFraming() {
assertThat(nonRunningPicocli.getErrString(), containsString(
"Invalid value for option '--log-syslog-counting-framing': TRUE. Expected values are: true, false, protocol-dependent"));
+ onAfter();
nonRunningPicocli = pseudoLaunch("start-dev", "--log=syslog", "--log-syslog-counting-framing=true");
assertThat(nonRunningPicocli.exitCode, is(CommandLine.ExitCode.OK));
assertThat(nonRunningPicocli.config.getConfigValue("quarkus.log.syslog.use-counting-framing").getValue(), is("true"));
+ onAfter();
nonRunningPicocli = pseudoLaunch("start-dev", "--log=syslog", "--log-syslog-counting-framing=false");
assertThat(nonRunningPicocli.exitCode, is(CommandLine.ExitCode.OK));
assertThat(nonRunningPicocli.config.getConfigValue("quarkus.log.syslog.use-counting-framing").getValue(), is("false"));
+ onAfter();
nonRunningPicocli = pseudoLaunch("start-dev", "--log=syslog", "--log-syslog-protocol=ssl-tcp", "--log-syslog-counting-framing=protocol-dependent");
assertThat(nonRunningPicocli.exitCode, is(CommandLine.ExitCode.OK));
assertThat(nonRunningPicocli.config.getConfigValue("quarkus.log.syslog.use-counting-framing").getValue(), is("true"));
+ onAfter();
nonRunningPicocli = pseudoLaunch("start-dev", "--log=syslog", "--log-syslog-counting-framing=wrong");
assertThat(nonRunningPicocli.exitCode, is(CommandLine.ExitCode.USAGE));
assertThat(nonRunningPicocli.getErrString(), containsString(
@@ -569,6 +600,7 @@ public void logConsoleJsonFormat() {
NonRunningPicocli nonRunningPicocli = pseudoLaunch("start-dev", "--log-console-output=json", "--log-console-json-format=invalid");
assertEquals(CommandLine.ExitCode.USAGE, nonRunningPicocli.exitCode);
assertThat(nonRunningPicocli.getErrString(), containsString("Invalid value for option '--log-console-json-format': invalid. Expected values are: default, ecs"));
+ onAfter();
nonRunningPicocli = pseudoLaunch("start-dev", "--log-console-output=json", "--log-console-json-format=ecs");
assertEquals(CommandLine.ExitCode.OK, nonRunningPicocli.exitCode);
@@ -586,6 +618,7 @@ public void logFileJsonFormat() {
NonRunningPicocli nonRunningPicocli = pseudoLaunch("start-dev", "--log-console-output=json", "--log-console-json-format=invalid");
assertEquals(CommandLine.ExitCode.USAGE, nonRunningPicocli.exitCode);
assertThat(nonRunningPicocli.getErrString(), containsString("Invalid value for option '--log-console-json-format': invalid. Expected values are: default, ecs"));
+ onAfter();
nonRunningPicocli = pseudoLaunch("start-dev", "--log-console-output=json", "--log-console-json-format=ecs");
assertEquals(CommandLine.ExitCode.OK, nonRunningPicocli.exitCode);
@@ -603,6 +636,7 @@ public void logSyslogJsonFormat() {
NonRunningPicocli nonRunningPicocli = pseudoLaunch("start-dev", "--log=syslog", "--log-syslog-output=json", "--log-syslog-json-format=invalid");
assertEquals(CommandLine.ExitCode.USAGE, nonRunningPicocli.exitCode);
assertThat(nonRunningPicocli.getErrString(), containsString("Invalid value for option '--log-syslog-json-format': invalid. Expected values are: default, ecs"));
+ onAfter();
nonRunningPicocli = pseudoLaunch("start-dev", "--log=syslog", "--log-syslog-output=json", "--log-syslog-json-format=ecs");
assertEquals(CommandLine.ExitCode.OK, nonRunningPicocli.exitCode);
@@ -674,10 +708,12 @@ public void logAsyncDisabledParent() {
assertEquals(CommandLine.ExitCode.USAGE, nonRunningPicocli.exitCode);
assertThat(nonRunningPicocli.getErrString(), containsString("Disabled option: '--log-console-async'. Available only when Console log handler is activated"));
+ onAfter();
nonRunningPicocli = pseudoLaunch("start-dev", "--log=console", "--log-file-async=true");
assertEquals(CommandLine.ExitCode.USAGE, nonRunningPicocli.exitCode);
assertThat(nonRunningPicocli.getErrString(), containsString("Disabled option: '--log-file-async'. Available only when File log handler is activated"));
+ onAfter();
nonRunningPicocli = pseudoLaunch("start-dev", "--log=file", "--log-syslog-async=true");
assertEquals(CommandLine.ExitCode.USAGE, nonRunningPicocli.exitCode);
assertThat(nonRunningPicocli.getErrString(), containsString("Disabled option: '--log-syslog-async'. Available only when Syslog is activated"));
@@ -727,6 +763,7 @@ private void assertLogAsyncHandlerDisabledOptions(LoggingOptions.Handler logHand
assertEquals(CommandLine.ExitCode.USAGE, nonRunningPicocli.exitCode);
assertThat(nonRunningPicocli.getErrString(), containsString("Invalid value for option '--log-%s-async-queue-length': 'invalid' is not an int".formatted(logHandlerOptionsName)));
+ onAfter();
nonRunningPicocli = pseudoLaunch("start-dev", "--log=%s".formatted(logHandlerName), "--log-%s-async-queue-length=768".formatted(logHandlerOptionsName));
assertEquals(CommandLine.ExitCode.USAGE, nonRunningPicocli.exitCode);
assertThat(nonRunningPicocli.getErrString(), containsString("Disabled option: '--log-%s-async-queue-length'. Available only when %s is activated and asynchronous logging is enabled".formatted(logHandlerOptionsName, logHandlerFullName)));
@@ -796,6 +833,13 @@ public void datasourcesNotAllowedChar(){
assertThat(nonRunningPicocli.getErrString(), containsString("Unknown option: '--db-kind-'"));
}
+ @Test
+ public void updateCommandValidation(){
+ NonRunningPicocli nonRunningPicocli = pseudoLaunch("update-compatibility","check");
+ assertEquals(CommandLine.ExitCode.USAGE, nonRunningPicocli.exitCode);
+ assertThat(nonRunningPicocli.getErrString(), containsString("Missing required argument: --file"));
+ }
+
@Test
public void errorSpiBuildtimeChanged() {
putEnvVar("KC_SPI_EVENTS_LISTENER__PROVIDER", "jboss-logging");
diff --git a/quarkus/runtime/src/test/java/org/keycloak/quarkus/runtime/configuration/AbstractConfigurationTest.java b/quarkus/runtime/src/test/java/org/keycloak/quarkus/runtime/configuration/AbstractConfigurationTest.java
index ff44dcd6c253..c9d0bb86422d 100644
--- a/quarkus/runtime/src/test/java/org/keycloak/quarkus/runtime/configuration/AbstractConfigurationTest.java
+++ b/quarkus/runtime/src/test/java/org/keycloak/quarkus/runtime/configuration/AbstractConfigurationTest.java
@@ -63,7 +63,6 @@ public static void removeEnvVar(String name) {
public static void setSystemProperty(String key, String value, Runnable runnable) {
System.setProperty(key, value);
- createConfig();
try {
runnable.run();
} finally {
diff --git a/quarkus/runtime/src/test/java/org/keycloak/quarkus/runtime/configuration/ConfigurationTest.java b/quarkus/runtime/src/test/java/org/keycloak/quarkus/runtime/configuration/ConfigurationTest.java
index 39eb6af72bc9..f9c25c4fb63b 100644
--- a/quarkus/runtime/src/test/java/org/keycloak/quarkus/runtime/configuration/ConfigurationTest.java
+++ b/quarkus/runtime/src/test/java/org/keycloak/quarkus/runtime/configuration/ConfigurationTest.java
@@ -45,7 +45,6 @@
import org.junit.Test;
import org.keycloak.Config;
import org.keycloak.config.CachingOptions;
-import org.keycloak.quarkus.runtime.configuration.ConfigArgsConfigSource;
import org.keycloak.quarkus.runtime.configuration.mappers.HttpPropertyMappers;
import org.keycloak.quarkus.runtime.Environment;
import org.keycloak.quarkus.runtime.vault.FilesKeystoreVaultProviderFactory;
@@ -355,6 +354,24 @@ public void testDatabaseProperties() {
config = createConfig();
assertEquals("test-schema", config.getConfigValue("kc.db-schema").getValue());
assertEquals("test-schema", config.getConfigValue("kc.db-schema").getValue());
+
+ ConfigArgsConfigSource.setCliArgs("--db=postgres");
+ config = createConfig();
+ assertEquals("primary", config.getConfigValue("quarkus.datasource.jdbc.additional-jdbc-properties.targetServerType").getValue());
+
+
+ ConfigArgsConfigSource.setCliArgs("--db=postgres", "--db-url-properties=?targetServerType=any");
+ config = createConfig();
+ assertNull(config.getConfigValue("quarkus.datasource.jdbc.additional-jdbc-properties.targetServerType").getValue());
+ assertEquals("jdbc:postgresql://localhost:5432/keycloak?targetServerType=any", config.getConfigValue("quarkus.datasource.jdbc.url").getValue());
+
+ ConfigArgsConfigSource.setCliArgs("--db=postgres", "--db-driver=software.amazon.jdbc.Driver");
+ config = createConfig();
+ assertNull(config.getConfigValue("quarkus.datasource.jdbc.additional-jdbc-properties.targetServerType").getValue());
+
+ ConfigArgsConfigSource.setCliArgs("--db=postgres", "--db-url=jdbc:postgresql://localhost:5432/keycloak?targetServerType=any");
+ config = createConfig();
+ assertNull(config.getConfigValue("quarkus.datasource.jdbc.additional-jdbc-properties.targetServerType").getValue());
}
// KEYCLOAK-15632
@@ -504,11 +521,23 @@ public void testKeystoreConfigSourcePropertyMapping() {
@Test
public void testReloadPeriod() {
ConfigArgsConfigSource.setCliArgs("");
- assertEquals("1h", createConfig().getConfigValue("quarkus.http.ssl.certificate.reload-period").getValue());
+ initConfig();
+ assertExternalConfig(Map.of(
+ "quarkus.http.ssl.certificate.reload-period", "1h",
+ "quarkus.management.ssl.certificate.reload-period", "1h"
+ ));
+
ConfigArgsConfigSource.setCliArgs("--https-certificates-reload-period=-1");
- assertNull(createConfig().getConfigValue("quarkus.http.ssl.certificate.reload-period").getValue());
+ initConfig();
+ assertExternalConfigNull("quarkus.http.ssl.certificate.reload-period");
+ assertExternalConfigNull("quarkus.management.ssl.certificate.reload-period");
+
ConfigArgsConfigSource.setCliArgs("--https-certificates-reload-period=2h");
- assertEquals("2h", createConfig().getConfigValue("quarkus.http.ssl.certificate.reload-period").getValue());
+ initConfig();
+ assertExternalConfig(Map.of(
+ "quarkus.http.ssl.certificate.reload-period", "2h",
+ "quarkus.management.ssl.certificate.reload-period", "2h"
+ ));
}
@Test
diff --git a/quarkus/runtime/src/test/java/org/keycloak/quarkus/runtime/configuration/DatasourcesConfigurationTest.java b/quarkus/runtime/src/test/java/org/keycloak/quarkus/runtime/configuration/DatasourcesConfigurationTest.java
index b593412a04ef..b289dcf9231b 100644
--- a/quarkus/runtime/src/test/java/org/keycloak/quarkus/runtime/configuration/DatasourcesConfigurationTest.java
+++ b/quarkus/runtime/src/test/java/org/keycloak/quarkus/runtime/configuration/DatasourcesConfigurationTest.java
@@ -8,14 +8,14 @@
import org.hibernate.dialect.PostgreSQLDialect;
import org.junit.Test;
import org.keycloak.quarkus.runtime.Environment;
-import org.keycloak.quarkus.runtime.configuration.ConfigArgsConfigSource;
-import org.keycloak.quarkus.runtime.configuration.Configuration;
import org.mariadb.jdbc.MariaDbDataSource;
import org.postgresql.xa.PGXADataSource;
import java.util.Map;
+import static org.hamcrest.CoreMatchers.hasItem;
import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
@@ -387,4 +387,53 @@ public void poolSizeInherit() {
"quarkus.datasource.\"users\".jdbc.max-size", "115"
));
}
+
+ @Test
+ public void envVarsHandling() {
+ putEnvVars(Map.of(
+ "KC_DB_KIND_USER_STORE", "postgres",
+ "KC_DB_URL_FULL_USER_STORE", "jdbc:postgresql://localhost/KEYCLOAK",
+ "KC_DB_USERNAME_USER_STORE", "my-username",
+ "KC_DB_KIND_MY_STORE", "mariadb"
+ ));
+ initConfig();
+
+ assertConfig(Map.of(
+ "db-kind-user-store", "postgres",
+ "db-url-full-user-store", "jdbc:postgresql://localhost/KEYCLOAK",
+ "db-username-user-store", "my-username",
+ "db-kind-my-store", "mariadb"
+ ));
+
+ assertExternalConfig(Map.of(
+ "quarkus.datasource.\"user-store\".db-kind", "postgresql",
+ "quarkus.datasource.\"user-store\".jdbc.url", "jdbc:postgresql://localhost/KEYCLOAK",
+ "quarkus.datasource.\"user-store\".username", "my-username",
+ "quarkus.datasource.\"my-store\".db-kind", "mariadb"
+ ));
+
+ assertThat(Configuration.getPropertyNames(), hasItem("quarkus.datasource.\"my-store\".db-kind"));
+ assertThat(Configuration.getPropertyNames(), not(hasItem("quarkus.datasource.\"my.store\".db-kind")));
+ }
+
+ @Test
+ public void envVarsSpecialChars() {
+ putEnvVars(Map.of(
+ "KC_USER_STORE_DB_KIND", "mariadb",
+ "KCKEY_USER_STORE_DB_KIND", "db-kind-user_store$something",
+ "KC_CLIENT_STORE_PW", "password",
+ "KCKEY_CLIENT_STORE_PW", "db-password-client.store_123"
+ ));
+ initConfig();
+
+ assertConfig(Map.of(
+ "db-kind-user_store$something", "mariadb",
+ "db-password-client.store_123", "password"
+ ));
+
+ assertExternalConfig(Map.of(
+ "quarkus.datasource.\"user_store$something\".db-kind", "mariadb",
+ "quarkus.datasource.\"client.store_123\".password", "password"
+ ));
+ }
}
diff --git a/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/BuildAndStartDistTest.java b/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/BuildAndStartDistTest.java
index 29907a834a16..4e908278759a 100644
--- a/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/BuildAndStartDistTest.java
+++ b/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/BuildAndStartDistTest.java
@@ -61,7 +61,7 @@ void testBuildAndStart(KeycloakDistribution dist) {
cliResult.assertBuild();
cliResult = rawDist.run("start", OPTIMIZED_BUILD_OPTION_LONG);
cliResult.assertNoBuild();
- assertTrue(cliResult.getErrorOutput().isBlank());
+ assertTrue(cliResult.getErrorOutput().isBlank(), cliResult.getErrorOutput());
// running start without optimized flag should not cause a build
cliResult = rawDist.run("start");
cliResult.assertNoBuild();
diff --git a/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/ClusterConfigDistTest.java b/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/ClusterConfigDistTest.java
index f9bcce729e0d..1b5f9fff099d 100644
--- a/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/ClusterConfigDistTest.java
+++ b/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/ClusterConfigDistTest.java
@@ -47,6 +47,47 @@ void changeClusterSetting(CLIResult result) {
result.assertMessage("ISPN000078: Starting JGroups channel `ISPN` with stack `jdbc-ping`");
}
+ @Test
+ @Launch({ "start-dev", "--cache=ispn", "--cache-embedded-network-bind-address=127.0.0.1","--cache-embedded-network-bind-port=7801", "--cache-embedded-network-external-address=127.0.0.2", "--cache-embedded-network-external-port=7802"})
+ void changeBindAndExternalAddress(CLIResult result) {
+ result.assertClusteredCache();
+ result.assertMessage("physical addresses are `[127.0.0.2:7802]`");
+ result.assertMessage("ISPN000078: Starting JGroups channel `ISPN` with stack `jdbc-ping`");
+ }
+
+ @Test
+ @Launch({ "start-dev", "--cache=ispn", "--cache-embedded-network-bind-address=127.0.0.1", "-Djgroups.bind.address=127.0.0.2", "-Djgroups.bind_addr=127.0.0.3"})
+ void testJGroupsBindAddressPropertyAlsoExists(CLIResult result) {
+ result.assertClusteredCache();
+ result.assertMessage("Conflicting system property 'jgroups.bind.address' and CLI arg 'cache-embedded-network-bind-address' set, utilising CLI value '127.0.0.1'");
+ result.assertMessage("Conflicting system property 'jgroups.bind_addr' and CLI arg 'cache-embedded-network-bind-address' set, utilising CLI value '127.0.0.1'");
+ result.assertMessage("physical addresses are `[127.0.0.1:7800]`");
+ result.assertMessage("ISPN000078: Starting JGroups channel `ISPN` with stack `jdbc-ping`");
+ }
+
+ @Test
+ @Launch({ "start-dev", "--cache=ispn", "-Djgroups.bind.address=127.0.0.2", "-Djgroups.bind.port=7801"})
+ void testJGroupsBindAddressProperty(CLIResult result) {
+ result.assertClusteredCache();
+ result.assertMessage("physical addresses are `[127.0.0.2:7801]`");
+ result.assertMessage("ISPN000078: Starting JGroups channel `ISPN` with stack `jdbc-ping`");
+ }
+
+ @Test
+ @Launch({ "start-dev", "--cache=ispn", "--cache-embedded-network-bind-address=match-address:127.0.0.*"})
+ void testBindSiteMatches(CLIResult result) {
+ result.assertClusteredCache();
+ result.assertMessage("physical addresses are `[127.0.0.");
+ result.assertMessage("ISPN000078: Starting JGroups channel `ISPN` with stack `jdbc-ping`");
+ }
+
+ @Test
+ @Launch({ "start-dev", "--cache=ispn", "--cache-embedded-network-bind-address=SITE_LOCAL"})
+ void testBindSiteLocal(CLIResult result) {
+ result.assertClusteredCache();
+ result.assertMessage("ISPN000078: Starting JGroups channel `ISPN` with stack `jdbc-ping`");
+ }
+
@Test
@Launch({ "start-dev", "--cache=ispn", "--cache-stack=jdbc-ping-udp"})
void testJdbcPingTCP(CLIResult result) {
diff --git a/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/StartCommandDistTest.java b/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/StartCommandDistTest.java
index ac49cc6497d8..aaf42eaa5b97 100644
--- a/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/StartCommandDistTest.java
+++ b/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/StartCommandDistTest.java
@@ -96,7 +96,7 @@ void noErrorSpiBuildtimeNotChanged(KeycloakDistribution dist) {
@Test
@Launch({ "--profile=dev", "start", "--db=dev-file" })
void failUsingDevProfile(CLIResult cliResult) {
- assertTrue(cliResult.getErrorOutput().contains("ERROR: You can not 'start' the server in development mode. Please re-build the server first, using 'kc.sh build' for the default production mode."),
+ assertTrue(cliResult.getErrorOutput().contains("You can not 'start' the server in development mode. Please re-build the server first, using 'kc.sh build' for the default production mode."),
() -> "The Output:\n" + cliResult.getErrorOutput() + "doesn't contains the expected string.");
}
diff --git a/quarkus/tests/integration/src/test/java/org/keycloak/it/storage/database/dist/DatasourcesDistTest.java b/quarkus/tests/integration/src/test/java/org/keycloak/it/storage/database/dist/DatasourcesDistTest.java
index 14ab29933892..4bfb3b2d94c4 100644
--- a/quarkus/tests/integration/src/test/java/org/keycloak/it/storage/database/dist/DatasourcesDistTest.java
+++ b/quarkus/tests/integration/src/test/java/org/keycloak/it/storage/database/dist/DatasourcesDistTest.java
@@ -6,6 +6,7 @@
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.junit5.extension.WithEnvVars;
import picocli.CommandLine;
@DistributionTest
@@ -27,4 +28,13 @@ public void multipleDatasourcesPrint(CLIResult result) {
result.assertMessage("Multiple datasources are specified: clients, , users");
result.assertBuild();
}
+
+ @Test
+ @WithEnvVars({"KC_DB_KIND_USERS", "postgres", "KC_DB_KIND_MY_AWESOME_CLIENTS", "mariadb"})
+ @Launch({"build"})
+ public void specifiedViaEnvVars(CLIResult result) {
+ result.assertMessage("Multiple datasources are specified: , my-awesome-clients, users");
+ result.assertBuild();
+ }
+
}
diff --git a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testBootstrapAdminService.approved.txt b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testBootstrapAdminService.approved.txt
index 44f8cf381d42..2820e70a4005 100644
--- a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testBootstrapAdminService.approved.txt
+++ b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testBootstrapAdminService.approved.txt
@@ -70,52 +70,54 @@ Database:
--db-username
The username of the database user.
-Database - additional datasources:
+Database - additional datasources (Preview):
--db-active-
- Deactivate specific named datasource . Default: true.
+ Preview: Deactivate specific named datasource . Default: true.
--db-driver-
- Used for named . The fully qualified class name of the JDBC
- driver. If not set, a default driver is set accordingly to the chosen
+ Preview: Used for named . The fully qualified class name of the
+ JDBC driver. If not set, a default driver is set accordingly to the chosen
database.
--db-kind-
- Used for named . The database vendor. In production mode the
- default value of 'dev-file' is deprecated, you should explicitly specify the
- db instead. Possible values are: dev-file, dev-mem, mariadb, mssql, mysql,
- oracle, postgres. Default: dev-file.
+ Preview: Used for named . The database vendor. In production mode
+ the default value of 'dev-file' is deprecated, you should explicitly specify
+ the db instead. Possible values are: dev-file, dev-mem, mariadb, mssql,
+ mysql, oracle, postgres. Default: dev-file.
--db-password-
- Used for named . The password of the database user.
+ Preview: Used for named . The password of the database user.
--db-pool-initial-size-
- Used for named . The initial size of the connection pool.
+ Preview: Used for named . The initial size of the connection pool.
--db-pool-max-size-
- Used for named . The maximum size of the connection pool. Default:
- 100.
+ Preview: Used for named . The maximum size of the connection pool.
+ Default: 100.
--db-pool-min-size-
- Used for named . The minimal size of the connection pool.
+ Preview: Used for named . The minimal size of the connection pool.
--db-schema-
- Used for named . The database schema to be used.
+ Preview: Used for named . The database schema to be used.
--db-url-database-
- Used for named . Sets the database name of the default JDBC URL of
- the chosen vendor. If the `db-url` option is set, this option is ignored.
+ Preview: Used for named . Sets the database name of the default
+ JDBC URL of the chosen vendor. If the `db-url` option is set, this option is
+ ignored.
--db-url-full-
- Used for named . The full database JDBC URL. If not provided, a
- default URL is set based on the selected database vendor. For instance, if
- using 'postgres', the default JDBC URL would be 'jdbc:postgresql:
- //localhost/keycloak'.
+ Preview: Used for named . The full database JDBC URL. If not
+ provided, a default URL is set based on the selected database vendor. For
+ instance, if using 'postgres', the default JDBC URL would be 'jdbc:
+ postgresql://localhost/keycloak'.
--db-url-host-
- Used for named . Sets the hostname of the default JDBC URL of the
- chosen vendor. If the `db-url` option is set, this option is ignored.
+ Preview: Used for named . Sets the hostname of the default JDBC
+ URL of the chosen vendor. If the `db-url` option is set, this option is
+ ignored.
--db-url-port-
- Used for named . Sets the port of the default JDBC URL of the
- chosen vendor. If the `db-url` option is set, this option is ignored.
+ Preview: Used for named . Sets the port of the default JDBC URL of
+ the chosen vendor. If the `db-url` option is set, this option is ignored.
--db-url-properties-
- Used for named . Sets the properties of the default JDBC URL of
- the chosen vendor. Make sure to set the properties accordingly to the format
- expected by the database vendor, as well as appending the right character at
- the beginning of this property value. If the `db-url` option is set, this
- option is ignored.
+ Preview: Used for named . Sets the properties of the default JDBC
+ URL of the chosen vendor. Make sure to set the properties accordingly to the
+ format expected by the database vendor, as well as appending the right
+ character at the beginning of this property value. If the `db-url` option is
+ set, this option is ignored.
--db-username-
- Used for named . The username of the database user.
+ Preview: Used for named . The username of the database user.
Transaction:
diff --git a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testBootstrapAdminUser.approved.txt b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testBootstrapAdminUser.approved.txt
index 1090c8783614..7f4ab78b4430 100644
--- a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testBootstrapAdminUser.approved.txt
+++ b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testBootstrapAdminUser.approved.txt
@@ -72,52 +72,54 @@ Database:
--db-username
The username of the database user.
-Database - additional datasources:
+Database - additional datasources (Preview):
--db-active-
- Deactivate specific named datasource . Default: true.
+ Preview: Deactivate specific named datasource . Default: true.
--db-driver-
- Used for named . The fully qualified class name of the JDBC
- driver. If not set, a default driver is set accordingly to the chosen
+ Preview: Used for named . The fully qualified class name of the
+ JDBC driver. If not set, a default driver is set accordingly to the chosen
database.
--db-kind-
- Used for named . The database vendor. In production mode the
- default value of 'dev-file' is deprecated, you should explicitly specify the
- db instead. Possible values are: dev-file, dev-mem, mariadb, mssql, mysql,
- oracle, postgres. Default: dev-file.
+ Preview: Used for named . The database vendor. In production mode
+ the default value of 'dev-file' is deprecated, you should explicitly specify
+ the db instead. Possible values are: dev-file, dev-mem, mariadb, mssql,
+ mysql, oracle, postgres. Default: dev-file.
--db-password-
- Used for named . The password of the database user.
+ Preview: Used for named . The password of the database user.
--db-pool-initial-size-
- Used for named . The initial size of the connection pool.
+ Preview: Used for named . The initial size of the connection pool.
--db-pool-max-size-
- Used for named . The maximum size of the connection pool. Default:
- 100.
+ Preview: Used for named . The maximum size of the connection pool.
+ Default: 100.
--db-pool-min-size-
- Used for named . The minimal size of the connection pool.
+ Preview: Used for named . The minimal size of the connection pool.
--db-schema-