diff --git a/.github/workflows/operator-ci.yml b/.github/workflows/operator-ci.yml index 3568cae18d..a526a75e19 100644 --- a/.github/workflows/operator-ci.yml +++ b/.github/workflows/operator-ci.yml @@ -65,5 +65,7 @@ jobs: mvn clean verify \ -Dquarkus.container-image.build=true -Dquarkus.container-image.tag=test \ -Dquarkus.kubernetes.deployment-target=kubernetes \ - -Dquarkus.jib.jvm-arguments="-Djava.util.logging.manager=org.jboss.logmanager.LogManager","-Doperator.keycloak.image=keycloak:${GITHUB_SHA}","-Doperator.keycloak.image-pull-policy=Never" \ + -Dquarkus.jib.jvm-arguments="-Djava.util.logging.manager=org.jboss.logmanager.LogManager",\ + "-Doperator.keycloak.image=keycloak:${GITHUB_SHA}", \ + "-Doperator.keycloak.image-pull-policy=Never" \ --no-transfer-progress -Dtest.operator.deployment=remote diff --git a/operator/src/main/java/org/keycloak/operator/Config.java b/operator/src/main/java/org/keycloak/operator/Config.java index e54c356078..9967a7dd61 100644 --- a/operator/src/main/java/org/keycloak/operator/Config.java +++ b/operator/src/main/java/org/keycloak/operator/Config.java @@ -29,5 +29,8 @@ public interface Config { interface Keycloak { String image(); String imagePullPolicy(); + + String initContainerImage(); + String initContainerImagePullPolicy(); } } diff --git a/operator/src/main/java/org/keycloak/operator/Constants.java b/operator/src/main/java/org/keycloak/operator/Constants.java index 7129b6088e..51f4fb29aa 100644 --- a/operator/src/main/java/org/keycloak/operator/Constants.java +++ b/operator/src/main/java/org/keycloak/operator/Constants.java @@ -35,4 +35,11 @@ public final class Constants { public static final Map DEFAULT_DIST_CONFIG = Map.of( "KC_HEALTH_ENABLED", "true" ); + + // Init container + public static final String EXTENSIONS_VOLUME_NAME = "extensions"; + public static final String KEYCLOAK_PROVIDERS_FOLDER = "/opt/keycloak/providers"; + public static final String INIT_CONTAINER_NAME = "keycloak-extensions"; + public static final String INIT_CONTAINER_EXTENSIONS_FOLDER = "/opt/extensions"; + public static final String INIT_CONTAINER_EXTENSIONS_ENV_VAR = "KEYCLOAK_EXTENSIONS"; } diff --git a/operator/src/main/java/org/keycloak/operator/v2alpha1/KeycloakDeployment.java b/operator/src/main/java/org/keycloak/operator/v2alpha1/KeycloakDeployment.java index f83ff4d55f..134f5fc9da 100644 --- a/operator/src/main/java/org/keycloak/operator/v2alpha1/KeycloakDeployment.java +++ b/operator/src/main/java/org/keycloak/operator/v2alpha1/KeycloakDeployment.java @@ -17,8 +17,11 @@ package org.keycloak.operator.v2alpha1; import io.fabric8.kubernetes.api.model.Container; +import io.fabric8.kubernetes.api.model.ContainerBuilder; import io.fabric8.kubernetes.api.model.EnvVarBuilder; import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.api.model.VolumeBuilder; +import io.fabric8.kubernetes.api.model.VolumeMountBuilder; import io.fabric8.kubernetes.api.model.apps.Deployment; import io.fabric8.kubernetes.api.model.apps.DeploymentBuilder; import io.fabric8.kubernetes.client.KubernetesClient; @@ -30,7 +33,9 @@ import org.keycloak.operator.v2alpha1.crds.Keycloak; import org.keycloak.operator.v2alpha1.crds.KeycloakStatusBuilder; import java.net.URL; +import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Optional; import java.util.stream.Collectors; @@ -87,6 +92,60 @@ public class KeycloakDeployment extends OperatorManagedResource { .get(); } + private void addInitContainer(Deployment baseDeployment, List extensions) { + var skipExtensions = Optional + .ofNullable(extensions) + .map(e -> e.isEmpty()) + .orElse(true); + + if (skipExtensions) { + return; + } + + // Add emptyDir Volume + var volumes = baseDeployment.getSpec().getTemplate().getSpec().getVolumes(); + + var extensionVolume = new VolumeBuilder() + .withName(Constants.EXTENSIONS_VOLUME_NAME) + .withNewEmptyDir() + .endEmptyDir() + .build(); + + volumes.add(extensionVolume); + baseDeployment.getSpec().getTemplate().getSpec().setVolumes(volumes); + + // Add the main deployment Volume Mount + var container = baseDeployment.getSpec().getTemplate().getSpec().getContainers().get(0); + var containerVolumeMounts = container.getVolumeMounts(); + + var extensionVM = new VolumeMountBuilder() + .withName(Constants.EXTENSIONS_VOLUME_NAME) + .withMountPath(Constants.KEYCLOAK_PROVIDERS_FOLDER) + .withReadOnly(true) + .build(); + containerVolumeMounts.add(extensionVM); + + container.setVolumeMounts(containerVolumeMounts); + + // Add the Extensions downloader init container + var extensionsValue = extensions.stream().collect(Collectors.joining(",")); + var initContainer = new ContainerBuilder() + .withName(Constants.INIT_CONTAINER_NAME) + .withImage(config.keycloak().initContainerImage()) + .withImagePullPolicy(config.keycloak().initContainerImagePullPolicy()) + .addNewVolumeMount() + .withName(Constants.EXTENSIONS_VOLUME_NAME) + .withMountPath(Constants.INIT_CONTAINER_EXTENSIONS_FOLDER) + .endVolumeMount() + .addNewEnv() + .withName(Constants.INIT_CONTAINER_EXTENSIONS_ENV_VAR) + .withValue(extensionsValue) + .endEnv() + .build(); + + baseDeployment.getSpec().getTemplate().getSpec().setInitContainers(Collections.singletonList(initContainer)); + } + private Deployment createBaseDeployment() { URL url = this.getClass().getResource("/base-keycloak-deployment.yaml"); Deployment baseDeployment = client.apps().deployments().load(url).get(); @@ -111,6 +170,8 @@ public class KeycloakDeployment extends OperatorManagedResource { .map(e -> new EnvVarBuilder().withName(e.getKey()).withValue(e.getValue()).build()) .collect(Collectors.toList())); + addInitContainer(baseDeployment, keycloakCR.getSpec().getExtensions()); + // Set configSecretsNames = new HashSet<>(); // List configEnvVars = serverConfig.entrySet().stream() // .map(e -> { diff --git a/operator/src/main/java/org/keycloak/operator/v2alpha1/crds/KeycloakSpec.java b/operator/src/main/java/org/keycloak/operator/v2alpha1/crds/KeycloakSpec.java index 2409bda3df..04b21d7e6c 100644 --- a/operator/src/main/java/org/keycloak/operator/v2alpha1/crds/KeycloakSpec.java +++ b/operator/src/main/java/org/keycloak/operator/v2alpha1/crds/KeycloakSpec.java @@ -16,6 +16,9 @@ */ package org.keycloak.operator.v2alpha1.crds; +import com.fasterxml.jackson.annotation.JsonPropertyDescription; + +import java.util.List; import java.util.Map; public class KeycloakSpec { @@ -24,6 +27,17 @@ public class KeycloakSpec { private String image; private Map serverConfiguration; + @JsonPropertyDescription("List of URLs to download Keycloak extensions.") + private List extensions; + + public List getExtensions() { + return extensions; + } + + public void setExtensions(List extensions) { + this.extensions = extensions; + } + public int getInstances() { return instances; } diff --git a/operator/src/main/resources/application.properties b/operator/src/main/resources/application.properties index 2561345ada..57c11002c4 100644 --- a/operator/src/main/resources/application.properties +++ b/operator/src/main/resources/application.properties @@ -5,5 +5,8 @@ quarkus.container-image.builder=jib quarkus.operator-sdk.crd.validate=false # Operator config -operator.keycloak.image=quay.io/keycloak/keycloak-x:latest +operator.keycloak.image=quay.io/keycloak/keycloak:latest operator.keycloak.image-pull-policy=Always + +operator.keycloak.init-container-image=quay.io/keycloak/keycloak-init-container:legacy +operator.keycloak.init-container-image-pull-policy=Always diff --git a/operator/src/main/resources/base-keycloak-deployment.yaml b/operator/src/main/resources/base-keycloak-deployment.yaml index 175b8475da..2f6a62646e 100644 --- a/operator/src/main/resources/base-keycloak-deployment.yaml +++ b/operator/src/main/resources/base-keycloak-deployment.yaml @@ -30,14 +30,14 @@ spec: httpGet: path: /health/live port: 8080 - initialDelaySeconds: 15 + initialDelaySeconds: 20 periodSeconds: 2 failureThreshold: 100 readinessProbe: httpGet: path: /health/ready port: 8080 - initialDelaySeconds: 15 + initialDelaySeconds: 20 periodSeconds: 2 failureThreshold: 200 dnsPolicy: ClusterFirst diff --git a/operator/src/test/java/org/keycloak/operator/KeycloakDeploymentE2EIT.java b/operator/src/test/java/org/keycloak/operator/KeycloakDeploymentE2EIT.java index 95b976b5a7..34a1cbcdc8 100644 --- a/operator/src/test/java/org/keycloak/operator/KeycloakDeploymentE2EIT.java +++ b/operator/src/test/java/org/keycloak/operator/KeycloakDeploymentE2EIT.java @@ -9,10 +9,13 @@ import org.junit.jupiter.api.Test; import org.keycloak.operator.v2alpha1.crds.Keycloak; import java.time.Duration; +import java.util.Collections; import java.util.List; import java.util.Map; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.keycloak.operator.Constants.DEFAULT_LABELS; import static org.keycloak.operator.utils.K8sUtils.deployKeycloak; import static org.keycloak.operator.utils.K8sUtils.getDefaultKeycloakDeployment; import static org.keycloak.operator.utils.K8sUtils.waitForKeycloakToBeReady; @@ -112,4 +115,34 @@ public class KeycloakDeploymentE2EIT extends ClusterOperatorTest { } } + @Test + public void testExtensions() { + try { + var kc = getDefaultKeycloakDeployment(); + kc.getSpec().setExtensions( + Collections.singletonList( + "https://github.com/aerogear/keycloak-metrics-spi/releases/download/2.5.3/keycloak-metrics-spi-2.5.3.jar")); + deployKeycloak(k8sclient, kc, true); + + var kcPod = k8sclient + .pods() + .inNamespace(namespace) + .withLabels(DEFAULT_LABELS) + .list() + .getItems() + .get(0); + + Awaitility.await() + .ignoreExceptions() + .untilAsserted(() -> { + var logs = k8sclient.pods().inNamespace(namespace).withName(kcPod.getMetadata().getName()).getLog(); + + assertTrue(logs.contains("metrics-listener (org.jboss.aerogear.keycloak.metrics.MetricsEventListenerFactory) is implementing the internal SPI")); + }); + } catch (Exception e) { + savePodLogs(); + throw e; + } + } + }