Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Share containers in DevServices for Keycloak #19631

Merged
merged 1 commit into from
Aug 27, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions extensions/oidc/deployment/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit4-mock</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-devservices-common</artifactId>
</dependency>
<!-- Test dependencies -->
<dependency>
<groupId>io.quarkus</groupId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,34 @@ 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<String> 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.
* <p>
* The discovery uses the {@code quarkus-dev-service-label} label.
* The value is configured using the {@code service-name} property.
* <p>
* Container sharing is only used in dev mode.
*/
@ConfigItem(defaultValue = "true")
public boolean shared;

/**
* The value of the {@code quarkus-dev-service-keycloak} label attached to the started container.
* This property is used when {@code shared} is set to {@code true}.
* In this case, before starting a container, Dev Services for Keycloak looks for a container with the
* {@code quarkus-dev-service-keycloak} label
* set to the configured value. If found, it will use this container instead of starting a new one. Otherwise it
* starts a new container with the {@code quarkus-dev-service-keycloak} label set to the specified value.
* <p>
* Container sharing is only used in dev mode.
*/
@ConfigItem(defaultValue = "quarkus")
public String serviceName;

/**
* The class or file system path to a Keycloak realm file which will be used to initialize Keycloak.
Expand All @@ -41,8 +68,6 @@ 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.
*/
@ConfigItem(defaultValue = "quarkus")
public String realmName;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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 JAVA_OPTS = "JAVA_OPTS";
private static final String KEYCLOAK_DOCKER_REALM_PATH = "/tmp/realm.json";
private static final String KEYCLOAK_USER_PROP = "KEYCLOAK_USER";
Expand All @@ -76,6 +80,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<Closeable> closeables;
private static volatile boolean first = true;
private static volatile String capturedKeycloakUrl;
Expand Down Expand Up @@ -129,7 +141,7 @@ public KeycloakDevServicesConfigBuildItem startKeycloakContainer(
return null;
}

closeables = Collections.singletonList(startResult.closeable);
closeables = startResult.closeable != null ? Collections.singletonList(startResult.closeable) : null;

if (first) {
first = false;
Expand Down Expand Up @@ -171,11 +183,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<DevServicesConfigResultBuildItem> devServices) {
BuildProducer<DevServicesConfigResultBuildItem> devServices, boolean shared) {
final String authServerUrl = capturedKeycloakUrl + "/realms/" + capturedDevServicesConfiguration.realmName;

String oidcClientId = getOidcClientId();
Expand Down Expand Up @@ -223,55 +235,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, capturedDevServicesConfiguration.javaOpts);

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.");
}
});
final Optional<ContainerAddress> maybeContainerAddress = keycloakDevModeContainerLocator.locateContainer(
capturedDevServicesConfiguration.realmName,
capturedDevServicesConfiguration.shared,
LaunchMode.current());

final Supplier<StartResult> defaultKeycloakContainerSupplier = () -> {
String imageName = capturedDevServicesConfiguration.imageName;
DockerImageName dockerImageName = DockerImageName.parse(imageName)
.asCompatibleSubstituteFor(imageName);
QuarkusOidcContainer oidcContainer = new QuarkusOidcContainer(dockerImageName,
capturedDevServicesConfiguration.port,
useSharedContainer,
capturedDevServicesConfiguration.realmPath,
capturedDevServicesConfiguration.serviceName,
capturedDevServicesConfiguration.shared,
capturedDevServicesConfiguration.javaOpts);

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<String> realm;
private final Optional<String> realmPath;
private final String containerLabelValue;
private final Optional<String> javaOpts;
private final boolean sharedContainer;
private boolean realmFileExists;
private String hostName = null;
private final Optional<String> javaOpts;

public QuarkusOidcContainer(DockerImageName dockerImageName, OptionalInt fixedExposedPort, boolean useSharedNetwork,
Optional<String> realm, Optional<String> javaOpts) {
Optional<String> realmPath, String containerLabelValue, boolean sharedContainer, Optional<String> javaOpts) {
super(dockerImageName);
this.fixedExposedPort = fixedExposedPort;
this.useSharedNetwork = useSharedNetwork;
this.realm = realm;
this.realmPath = realmPath;
this.containerLabelValue = containerLabelValue;
this.sharedContainer = sharedContainer;
this.javaOpts = javaOpts;
}

Expand All @@ -287,37 +325,41 @@ 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, containerLabelValue);
}

addEnv(KEYCLOAK_USER_PROP, KEYCLOAK_ADMIN_USER);
addEnv(KEYCLOAK_PASSWORD_PROP, KEYCLOAK_ADMIN_PASSWORD);
addEnv(KEYCLOAK_VENDOR_PROP, KEYCLOAK_DB_VENDOR);
if (javaOpts.isPresent()) {
addEnv(JAVA_OPTS, javaOpts.get());
}

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
Expand All @@ -330,7 +372,7 @@ public String getHost() {

public int getPort() {
if (useSharedNetwork) {
return KEYCLOAK_EXPOSED_PORT;
return KEYCLOAK_PORT;
}
if (fixedExposedPort.isPresent()) {
return fixedExposedPort.getAsInt();
Expand Down