Skip to content

Commit

Permalink
Share containers in DevServices for Keycloak
Browse files Browse the repository at this point in the history
  • Loading branch information
sberyozkin committed Aug 25, 2021
1 parent 946c94b commit 6bf8ac4
Show file tree
Hide file tree
Showing 5 changed files with 116 additions and 45 deletions.
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,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<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 realm-name} property.
* <p>
* 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.
Expand All @@ -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;
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 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";
Expand All @@ -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<Closeable> closeables;
private static volatile boolean first = true;
private static volatile String capturedKeycloakUrl;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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<DevServicesConfigResultBuildItem> devServices) {
BuildProducer<DevServicesConfigResultBuildItem> devServices, boolean shared) {
final String authServerUrl = capturedKeycloakUrl + "/realms/" + capturedDevServicesConfiguration.realmName;

String oidcClientId = getOidcClientId();
Expand Down Expand Up @@ -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<ContainerAddress> maybeContainerAddress = keycloakDevModeContainerLocator.locateContainer(
capturedDevServicesConfiguration.realmName,
capturedDevServicesConfiguration.shared,
launchMode);

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.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<String> realm;
private final Optional<String> 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<String> realm) {
Optional<String> 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
Expand All @@ -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
Expand All @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;

import org.eclipse.microprofile.config.ConfigProvider;
import org.eclipse.microprofile.jwt.JsonWebToken;

import io.quarkus.oidc.IdToken;
Expand All @@ -18,11 +19,20 @@ public class ProtectedResource {
@IdToken
JsonWebToken idToken;

@Inject
JsonWebToken accessToken;

@GET
public String getName() {
return idToken.getName();
}

@GET
@Path("accessToken")
public String getNameFromAccessToken() {
return accessToken.getName() + ":" + ConfigProvider.getConfig().getValue("keycloak.container.shared", String.class);
}

@GET
@Path("tenant/{id}")
public String getTenantName(@PathParam("id") String tenantId) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -56,6 +55,7 @@ public class KeycloakTestResourceLifecycleManager implements QuarkusTestResource
@SuppressWarnings("resource")
@Override
public Map<String, String> start() {

String keycloakDockerImage;
if (KEYCLOAK_DOCKER_IMAGE != null) {
keycloakDockerImage = KEYCLOAK_DOCKER_IMAGE;
Expand Down Expand Up @@ -227,6 +227,7 @@ public void stop() {
.delete(KEYCLOAK_SERVER_URL + "/admin/realms/" + KEYCLOAK_REALM).then().statusCode(204);

keycloak.stop();
keycloak.close();
}

private static List<String> getAdminRoles() {
Expand Down

0 comments on commit 6bf8ac4

Please sign in to comment.