From eec9b4bc31c8818b81d5fb4c2d271737bcdb9bd0 Mon Sep 17 00:00:00 2001 From: Sergey Beryozkin Date: Tue, 24 Aug 2021 19:35:56 +0100 Subject: [PATCH] Share containers in DevServices for Keycloak --- extensions/oidc/deployment/pom.xml | 4 + .../keycloak/DevServicesConfig.java | 19 ++- .../KeycloakDevServicesProcessor.java | 125 ++++++++++++------ .../KeycloakTestResourceLifecycleManager.java | 1 - 4 files changed, 104 insertions(+), 45 deletions(-) diff --git a/extensions/oidc/deployment/pom.xml b/extensions/oidc/deployment/pom.xml index 43c0bf86cfc50..9f7cecad85844 100644 --- a/extensions/oidc/deployment/pom.xml +++ b/extensions/oidc/deployment/pom.xml @@ -63,6 +63,10 @@ io.quarkus quarkus-junit4-mock + + io.quarkus + quarkus-devservices-common + io.quarkus diff --git a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/DevServicesConfig.java b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/DevServicesConfig.java index f4c37befb9b44..765db0f4b8ba5 100644 --- a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/DevServicesConfig.java +++ b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/DevServicesConfig.java @@ -25,7 +25,21 @@ public class DevServicesConfig { * The container image name to use, for container based DevServices providers. */ @ConfigItem(defaultValue = "quay.io/keycloak/keycloak:14.0.0") - public Optional imageName; + public String imageName; + + /** + * Indicates if the Keycloak container managed by Quarkus Dev Services is shared. + * When shared, Quarkus looks for running containers using label-based service discovery. + * If a matching container is found, it is used, and so a second one is not started. + * Otherwise, Dev Services for Keycloak starts a new container. + *

+ * The discovery uses the {@code quarkus-dev-service-label} label. + * The value is configured using the {@code realm-name} property. + *

+ * Container sharing is only used in dev mode. + */ + @ConfigItem(defaultValue = "true") + public boolean shared; /** * The class or file system path to a Keycloak realm file which will be used to initialize Keycloak. @@ -36,8 +50,7 @@ public class DevServicesConfig { /** * The Keycloak realm. * This property will be used to create the realm if the realm file pointed to by the 'realm-path' property does not exist. - * Setting this property is recommended even if realm file exists - * for `quarkus.oidc.auth-server-url` property be correctly calculated. + * It is also used as a label value for service discovery when the {@code shared} property is enabled. */ @ConfigItem(defaultValue = "quarkus") public String realmName; diff --git a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/KeycloakDevServicesProcessor.java b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/KeycloakDevServicesProcessor.java index b1a1274ddbc5d..d81acd129798a 100644 --- a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/KeycloakDevServicesProcessor.java +++ b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/KeycloakDevServicesProcessor.java @@ -15,6 +15,7 @@ import java.util.Map; import java.util.Optional; import java.util.OptionalInt; +import java.util.function.Supplier; import java.util.stream.Collectors; import org.eclipse.microprofile.config.ConfigProvider; @@ -41,8 +42,11 @@ import io.quarkus.deployment.builditem.DevServicesConfigResultBuildItem; import io.quarkus.deployment.builditem.DevServicesSharedNetworkBuildItem; import io.quarkus.deployment.dev.devservices.GlobalDevServicesConfig; +import io.quarkus.devservices.common.ContainerAddress; +import io.quarkus.devservices.common.ContainerLocator; import io.quarkus.oidc.deployment.OidcBuildStep.IsEnabled; import io.quarkus.oidc.deployment.devservices.OidcDevServicesBuildItem; +import io.quarkus.runtime.LaunchMode; import io.quarkus.runtime.configuration.ConfigUtils; import io.vertx.core.Vertx; import io.vertx.core.http.HttpHeaders; @@ -64,7 +68,7 @@ public class KeycloakDevServicesProcessor { private static final String CLIENT_SECRET_CONFIG_KEY = CONFIG_PREFIX + "credentials.secret"; private static final String KEYCLOAK_URL_KEY = "keycloak.url"; - private static final int KEYCLOAK_EXPOSED_PORT = 8080; + private static final int KEYCLOAK_PORT = 8080; private static final String KEYCLOAK_DOCKER_REALM_PATH = "/tmp/realm.json"; private static final String KEYCLOAK_USER_PROP = "KEYCLOAK_USER"; private static final String KEYCLOAK_PASSWORD_PROP = "KEYCLOAK_PASSWORD"; @@ -75,6 +79,14 @@ public class KeycloakDevServicesProcessor { private static final String KEYCLOAK_DB_VENDOR = "H2"; private static final String OIDC_USERS = "oidc.users"; + /** + * Label to add to shared Dev Service for Keycloak running in containers. + * This allows other applications to discover the running service and use it instead of starting a new instance. + */ + private static final String DEV_SERVICE_LABEL = "quarkus-dev-service-keycloak"; + private static final ContainerLocator keycloakDevModeContainerLocator = new ContainerLocator(DEV_SERVICE_LABEL, + KEYCLOAK_PORT); + private static volatile List closeables; private static volatile boolean first = true; private static volatile String capturedKeycloakUrl; @@ -128,7 +140,7 @@ public KeycloakDevServicesConfigBuildItem startKeycloakContainer( return null; } - closeables = Collections.singletonList(startResult.closeable); + closeables = startResult.closeable != null ? Collections.singletonList(startResult.closeable) : null; if (first) { first = false; @@ -170,11 +182,11 @@ public void run() { LOG.info("Dev Services for Keycloak started."); - return prepareConfiguration(!startResult.realmFileExists, devServices); + return prepareConfiguration(startResult.createDefaultRealm, devServices, startResult.shared); } private KeycloakDevServicesConfigBuildItem prepareConfiguration(boolean createRealm, - BuildProducer devServices) { + BuildProducer devServices, boolean shared) { final String authServerUrl = capturedKeycloakUrl + "/realms/" + capturedDevServicesConfiguration.realmName; String oidcClientId = getOidcClientId(); @@ -222,54 +234,81 @@ private StartResult startContainer(boolean useSharedContainer) { return null; } - String imageName = capturedDevServicesConfiguration.imageName.get(); - DockerImageName dockerImageName = DockerImageName.parse(imageName) - .asCompatibleSubstituteFor(imageName); - QuarkusOidcContainer oidcContainer = new QuarkusOidcContainer(dockerImageName, - capturedDevServicesConfiguration.port, - useSharedContainer, - capturedDevServicesConfiguration.realmPath); - - oidcContainer.start(); - - String url = "http://" + oidcContainer.getHost() + ":" + oidcContainer.getPort(); - return new StartResult(url, - oidcContainer.realmFileExists, - new Closeable() { - @Override - public void close() { - oidcContainer.close(); - - LOG.info("Dev Services for Keycloak shut down."); - } - }); + LaunchMode launchMode = LaunchMode.current(); + + final Optional maybeContainerAddress = keycloakDevModeContainerLocator.locateContainer( + capturedDevServicesConfiguration.realmName, + capturedDevServicesConfiguration.shared, + launchMode); + + final Supplier defaultKeycloakContainerSupplier = () -> { + String imageName = capturedDevServicesConfiguration.imageName; + DockerImageName dockerImageName = DockerImageName.parse(imageName) + .asCompatibleSubstituteFor(imageName); + QuarkusOidcContainer oidcContainer = new QuarkusOidcContainer(dockerImageName, + capturedDevServicesConfiguration.port, + useSharedContainer, + capturedDevServicesConfiguration.realmPath, + capturedDevServicesConfiguration.realmName, + capturedDevServicesConfiguration.shared); + + oidcContainer.start(); + + String url = "http://" + oidcContainer.getHost() + ":" + oidcContainer.getPort(); + return new StartResult(url, + !oidcContainer.realmFileExists, + new Closeable() { + @Override + public void close() { + oidcContainer.close(); + + LOG.info("Dev Services for Keycloak shut down."); + } + }, + false); + }; + + return maybeContainerAddress + .map(containerAddress -> new StartResult(getSharedContainerUrl(containerAddress), false, null, true)) + .orElseGet(defaultKeycloakContainerSupplier); + } + + private String getSharedContainerUrl(ContainerAddress containerAddress) { + return "http://" + ("0.0.0.0".equals(containerAddress.getHost()) ? "localhost" : containerAddress.getHost()) + + ":" + containerAddress.getPort(); } private static class StartResult { private final String url; - private final boolean realmFileExists; + private final boolean createDefaultRealm; private final Closeable closeable; + private final boolean shared; - public StartResult(String url, boolean realmFileExists, Closeable closeable) { + public StartResult(String url, boolean createDefaultRealm, Closeable closeable, boolean shared) { this.url = url; - this.realmFileExists = realmFileExists; + this.createDefaultRealm = createDefaultRealm; this.closeable = closeable; + this.shared = shared; } } private static class QuarkusOidcContainer extends GenericContainer { private final OptionalInt fixedExposedPort; private final boolean useSharedNetwork; - private final Optional realm; + private final Optional realmPath; + private final String realmName; private boolean realmFileExists; private String hostName = null; + private final boolean sharedContainer; public QuarkusOidcContainer(DockerImageName dockerImageName, OptionalInt fixedExposedPort, boolean useSharedNetwork, - Optional realm) { + Optional realmPath, String realmName, boolean sharedContainer) { super(dockerImageName); this.fixedExposedPort = fixedExposedPort; this.useSharedNetwork = useSharedNetwork; - this.realm = realm; + this.realmPath = realmPath; + this.realmName = realmName; + this.sharedContainer = sharedContainer; } @Override @@ -284,34 +323,38 @@ protected void configure() { setNetworkAliases(Collections.singletonList(hostName)); } else { if (fixedExposedPort.isPresent()) { - addFixedExposedPort(fixedExposedPort.getAsInt(), KEYCLOAK_EXPOSED_PORT); + addFixedExposedPort(fixedExposedPort.getAsInt(), KEYCLOAK_PORT); } else { - addExposedPort(KEYCLOAK_EXPOSED_PORT); + addExposedPort(KEYCLOAK_PORT); } } + if (sharedContainer && LaunchMode.current() == LaunchMode.DEVELOPMENT) { + withLabel(DEV_SERVICE_LABEL, realmName); + } + addEnv(KEYCLOAK_USER_PROP, KEYCLOAK_ADMIN_USER); addEnv(KEYCLOAK_PASSWORD_PROP, KEYCLOAK_ADMIN_PASSWORD); addEnv(KEYCLOAK_VENDOR_PROP, KEYCLOAK_DB_VENDOR); - if (realm.isPresent()) { - if (Thread.currentThread().getContextClassLoader().getResource(realm.get()) != null) { + if (realmPath.isPresent()) { + if (Thread.currentThread().getContextClassLoader().getResource(realmPath.get()) != null) { realmFileExists = true; - withClasspathResourceMapping(realm.get(), KEYCLOAK_DOCKER_REALM_PATH, BindMode.READ_ONLY); + withClasspathResourceMapping(realmPath.get(), KEYCLOAK_DOCKER_REALM_PATH, BindMode.READ_ONLY); addEnv(KEYCLOAK_IMPORT_PROP, KEYCLOAK_DOCKER_REALM_PATH); } else { - Path filePath = Paths.get(realm.get()); + Path filePath = Paths.get(realmPath.get()); if (Files.exists(filePath)) { realmFileExists = true; - withFileSystemBind(realm.get(), KEYCLOAK_DOCKER_REALM_PATH, BindMode.READ_ONLY); + withFileSystemBind(realmPath.get(), KEYCLOAK_DOCKER_REALM_PATH, BindMode.READ_ONLY); addEnv(KEYCLOAK_IMPORT_PROP, KEYCLOAK_DOCKER_REALM_PATH); } else { - LOG.debugf("Realm %s resource is not available", realm.get()); + LOG.debugf("Realm %s resource is not available", realmPath.get()); } } } - super.setWaitStrategy(Wait.forHttp("/auth").forPort(KEYCLOAK_EXPOSED_PORT)); + super.setWaitStrategy(Wait.forHttp("/auth").forPort(KEYCLOAK_PORT)); } @Override @@ -324,7 +367,7 @@ public String getHost() { public int getPort() { if (useSharedNetwork) { - return KEYCLOAK_EXPOSED_PORT; + return KEYCLOAK_PORT; } if (fixedExposedPort.isPresent()) { return fixedExposedPort.getAsInt(); diff --git a/test-framework/keycloak-server/src/main/java/io/quarkus/test/keycloak/server/KeycloakTestResourceLifecycleManager.java b/test-framework/keycloak-server/src/main/java/io/quarkus/test/keycloak/server/KeycloakTestResourceLifecycleManager.java index f64f8cfac0c3b..7855fa1bf8926 100644 --- a/test-framework/keycloak-server/src/main/java/io/quarkus/test/keycloak/server/KeycloakTestResourceLifecycleManager.java +++ b/test-framework/keycloak-server/src/main/java/io/quarkus/test/keycloak/server/KeycloakTestResourceLifecycleManager.java @@ -38,7 +38,6 @@ public class KeycloakTestResourceLifecycleManager implements QuarkusTestResource private static final String TOKEN_USER_ROLES = System.getProperty("keycloak.token.user-roles", "user"); private static final String TOKEN_ADMIN_ROLES = System.getProperty("keycloak.token.admin-roles", "user,admin"); - private static String KEYCLOAK_TRUSTSTORE_PATH = "keycloak.jks"; private static String KEYCLOAK_TRUSTSTORE_SECRET = "secret"; private static String KEYCLOAK_TLS_KEY = "tls.key";