From 7f048300fe52a55ea99527538ad9d57b4a543ee4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Barto=C5=A1?= Date: Wed, 3 Apr 2024 16:18:44 +0200 Subject: [PATCH] Support management port for health and metrics (#27629) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Support management port for health and metrics Closes #19334 Signed-off-by: Martin Bartoš * Deprecate option Signed-off-by: Martin Bartoš * Remove relativePath first-class citizen, rename ManagementSpec Signed-off-by: Martin Bartoš * Fix KeycloakDistConfiguratorTest Signed-off-by: Martin Bartoš --------- Signed-off-by: Martin Bartoš --- .../java/org/keycloak/operator/Constants.java | 3 + .../KeycloakDeploymentDependentResource.java | 34 ++- .../controllers/KeycloakDistConfigurator.java | 7 + .../KeycloakServiceDependentResource.java | 28 +- .../v2alpha1/deployment/KeycloakSpec.java | 13 + .../deployment/spec/HttpManagementSpec.java | 38 +++ .../integration/KeycloakDeploymentTest.java | 25 +- .../integration/KeycloakServicesTest.java | 3 - .../testsuite/unit/CRSerializationTest.java | 5 + .../unit/KeycloakDistConfiguratorTest.java | 9 + .../testsuite/unit/PodTemplateTest.java | 28 +- .../test-serialization-keycloak-cr.yml | 2 + .../keycloak/config/ManagementOptions.java | 104 +++++++ .../org/keycloak/config/OptionCategory.java | 3 +- quarkus/container/Dockerfile | 1 + .../deployment/IsManagementEnabled.java | 28 ++ .../quarkus/deployment/KeycloakProcessor.java | 19 +- .../KeycloakMetricsConfigurationTest.java | 59 ++++ .../KeycloakNegativeHealthCheckTest.java | 3 + .../health/KeycloakPathConfigurationTest.java | 48 ++- .../health/KeycloakReadyHealthCheckTest.java | 12 +- .../quarkus/runtime/KeycloakRecorder.java | 7 + .../runtime/configuration/Configuration.java | 4 + .../mappers/ManagementPropertyMappers.java | 146 +++++++++ .../mappers/PropertyMappers.java | 1 + .../configuration/test/ConfigurationTest.java | 6 +- .../test/ManagementConfigurationTest.java | 277 ++++++++++++++++++ .../keycloak/it/cli/dist/HealthDistTest.java | 12 +- .../it/cli/dist/ManagementDistTest.java | 167 +++++++++++ .../it/cli/dist/ManagementHttpsDistTest.java | 65 ++++ .../keycloak/it/cli/dist/MetricsDistTest.java | 10 +- .../cli/dist/QuarkusPropertiesDistTest.java | 2 + ...CommandDistTest.testBuildHelp.approved.txt | 17 ++ ...ommandDistTest.testExportHelp.approved.txt | 51 +++- ...andDistTest.testExportHelpAll.approved.txt | 51 +++- ...ommandDistTest.testImportHelp.approved.txt | 51 +++- ...andDistTest.testImportHelpAll.approved.txt | 51 +++- ...mandDistTest.testStartDevHelp.approved.txt | 51 +++- ...dDistTest.testStartDevHelpAll.approved.txt | 51 +++- ...CommandDistTest.testStartHelp.approved.txt | 51 +++- ...mandDistTest.testStartHelpAll.approved.txt | 51 +++- ...stTest.testStartOptimizedHelp.approved.txt | 37 ++- ...est.testStartOptimizedHelpAll.approved.txt | 37 ++- quarkus/tests/junit5/pom.xml | 4 + .../it/junit5/extension/DistributionTest.java | 10 + .../it/junit5/extension/DistributionType.java | 14 +- .../KeycloakDistributionDecorator.java | 10 + .../it/utils/DockerKeycloakDistribution.java | 48 +-- .../it/utils/KeycloakDistribution.java | 4 + .../it/utils/RawKeycloakDistribution.java | 27 +- .../AbstractQuarkusDeployableContainer.java | 2 + .../KeycloakQuarkusConfiguration.java | 11 +- .../base/src/test/resources/arquillian.xml | 1 + 53 files changed, 1634 insertions(+), 165 deletions(-) create mode 100644 operator/src/main/java/org/keycloak/operator/crds/v2alpha1/deployment/spec/HttpManagementSpec.java create mode 100644 quarkus/config-api/src/main/java/org/keycloak/config/ManagementOptions.java create mode 100644 quarkus/deployment/src/main/java/org/keycloak/quarkus/deployment/IsManagementEnabled.java create mode 100644 quarkus/deployment/src/test/java/test/org/keycloak/quarkus/services/health/KeycloakMetricsConfigurationTest.java create mode 100644 quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/ManagementPropertyMappers.java create mode 100644 quarkus/runtime/src/test/java/org/keycloak/quarkus/runtime/configuration/test/ManagementConfigurationTest.java create mode 100644 quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/ManagementDistTest.java create mode 100644 quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/ManagementHttpsDistTest.java diff --git a/operator/src/main/java/org/keycloak/operator/Constants.java b/operator/src/main/java/org/keycloak/operator/Constants.java index a5cc90a374..a581dfff94 100644 --- a/operator/src/main/java/org/keycloak/operator/Constants.java +++ b/operator/src/main/java/org/keycloak/operator/Constants.java @@ -60,6 +60,8 @@ public final class Constants { public static final Integer KEYCLOAK_DISCOVERY_SERVICE_PORT = 7800; public static final String KEYCLOAK_DISCOVERY_TCP_PORT_NAME = "tcp"; public static final String KEYCLOAK_DISCOVERY_SERVICE_SUFFIX = "-discovery"; + public static final Integer KEYCLOAK_MANAGEMENT_PORT = 9000; + public static final String KEYCLOAK_MANAGEMENT_PORT_NAME = "management"; public static final String KEYCLOAK_INGRESS_SUFFIX = "-ingress"; @@ -72,4 +74,5 @@ public final class Constants { public static final String CACHE_CONFIG_FOLDER = CONFIG_FOLDER + "/" + CACHE_CONFIG_SUBFOLDER; public static final String KEYCLOAK_HTTP_RELATIVE_PATH_KEY = "http-relative-path"; + public static final String KEYCLOAK_HTTP_MANAGEMENT_RELATIVE_PATH_KEY = "http-management-relative-path"; } 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 f9d376732f..0afaadb754 100644 --- a/operator/src/main/java/org/keycloak/operator/controllers/KeycloakDeploymentDependentResource.java +++ b/operator/src/main/java/org/keycloak/operator/controllers/KeycloakDeploymentDependentResource.java @@ -22,7 +22,6 @@ import io.fabric8.kubernetes.api.model.EnvVar; import io.fabric8.kubernetes.api.model.EnvVarBuilder; import io.fabric8.kubernetes.api.model.EnvVarSource; import io.fabric8.kubernetes.api.model.EnvVarSourceBuilder; -import io.fabric8.kubernetes.api.model.PodResourceClaim; import io.fabric8.kubernetes.api.model.PodSpec; import io.fabric8.kubernetes.api.model.PodTemplateSpec; import io.fabric8.kubernetes.api.model.Secret; @@ -45,6 +44,7 @@ import org.keycloak.operator.crds.v2alpha1.deployment.Keycloak; import org.keycloak.operator.crds.v2alpha1.deployment.KeycloakSpec; import org.keycloak.operator.crds.v2alpha1.deployment.ValueOrSecret; import org.keycloak.operator.crds.v2alpha1.deployment.spec.CacheSpec; +import org.keycloak.operator.crds.v2alpha1.deployment.spec.HttpManagementSpec; import org.keycloak.operator.crds.v2alpha1.deployment.spec.Truststore; import org.keycloak.operator.crds.v2alpha1.deployment.spec.TruststoreSource; import org.keycloak.operator.crds.v2alpha1.deployment.spec.UnsupportedSpec; @@ -284,12 +284,13 @@ public class KeycloakDeploymentDependentResource extends CRUDKubernetesDependent containerBuilder.addToArgs(0, getJGroupsParameter(keycloakCR)); // probes - var tlsConfigured = isTlsConfigured(keycloakCR); - var protocol = !tlsConfigured ? "HTTP" : "HTTPS"; - var kcPort = KeycloakServiceDependentResource.getServicePort(tlsConfigured, keycloakCR); - - // Relative path ends with '/' - var kcRelativePath = readConfigurationValue(Constants.KEYCLOAK_HTTP_RELATIVE_PATH_KEY, keycloakCR, context) + var protocol = isTlsConfigured(keycloakCR) ? "HTTPS" : "HTTP"; + var port = Optional.ofNullable(keycloakCR.getSpec()) + .map(KeycloakSpec::getHttpManagementSpec) + .map(HttpManagementSpec::getPort) + .orElse(Constants.KEYCLOAK_MANAGEMENT_PORT); + var relativePath = readConfigurationValue(Constants.KEYCLOAK_HTTP_MANAGEMENT_RELATIVE_PATH_KEY, keycloakCR, context) + .or(() -> readConfigurationValue(Constants.KEYCLOAK_HTTP_RELATIVE_PATH_KEY, keycloakCR, context)) .map(path -> !path.endsWith("/") ? path + "/" : path) .orElse("/"); @@ -299,8 +300,8 @@ public class KeycloakDeploymentDependentResource extends CRUDKubernetesDependent .withFailureThreshold(3) .withNewHttpGet() .withScheme(protocol) - .withNewPort(kcPort) - .withPath(kcRelativePath + "health/ready") + .withNewPort(port) + .withPath(relativePath + "health/ready") .endHttpGet() .endReadinessProbe(); } @@ -310,8 +311,8 @@ public class KeycloakDeploymentDependentResource extends CRUDKubernetesDependent .withFailureThreshold(3) .withNewHttpGet() .withScheme(protocol) - .withNewPort(kcPort) - .withPath(kcRelativePath + "health/live") + .withNewPort(port) + .withPath(relativePath + "health/live") .endHttpGet() .endLivenessProbe(); } @@ -321,14 +322,14 @@ public class KeycloakDeploymentDependentResource extends CRUDKubernetesDependent .withFailureThreshold(600) .withNewHttpGet() .withScheme(protocol) - .withNewPort(kcPort) - .withPath(kcRelativePath + "health/started") + .withNewPort(port) + .withPath(relativePath + "health/started") .endHttpGet() .endStartupProbe(); } // add in ports - there's no merging being done here - StatefulSet baseDeployment = containerBuilder + final StatefulSet baseDeployment = containerBuilder .addNewPort() .withName(Constants.KEYCLOAK_HTTPS_PORT_NAME) .withContainerPort(Constants.KEYCLOAK_HTTPS_PORT) @@ -339,6 +340,11 @@ public class KeycloakDeploymentDependentResource extends CRUDKubernetesDependent .withContainerPort(Constants.KEYCLOAK_HTTP_PORT) .withProtocol(Constants.KEYCLOAK_SERVICE_PROTOCOL) .endPort() + .addNewPort() + .withName(Constants.KEYCLOAK_MANAGEMENT_PORT_NAME) + .withContainerPort(Constants.KEYCLOAK_MANAGEMENT_PORT) + .withProtocol(Constants.KEYCLOAK_SERVICE_PROTOCOL) + .endPort() .endContainer().endSpec().endTemplate().endSpec().build(); return baseDeployment; diff --git a/operator/src/main/java/org/keycloak/operator/controllers/KeycloakDistConfigurator.java b/operator/src/main/java/org/keycloak/operator/controllers/KeycloakDistConfigurator.java index a742d47e8e..50d5d73bde 100644 --- a/operator/src/main/java/org/keycloak/operator/controllers/KeycloakDistConfigurator.java +++ b/operator/src/main/java/org/keycloak/operator/controllers/KeycloakDistConfigurator.java @@ -32,6 +32,7 @@ import org.keycloak.operator.crds.v2alpha1.deployment.spec.DatabaseSpec; import org.keycloak.operator.crds.v2alpha1.deployment.spec.FeatureSpec; import org.keycloak.operator.crds.v2alpha1.deployment.spec.HostnameSpec; import org.keycloak.operator.crds.v2alpha1.deployment.spec.HttpSpec; +import org.keycloak.operator.crds.v2alpha1.deployment.spec.HttpManagementSpec; import org.keycloak.operator.crds.v2alpha1.deployment.spec.ProxySpec; import org.keycloak.operator.crds.v2alpha1.deployment.spec.TransactionsSpec; @@ -70,6 +71,7 @@ public class KeycloakDistConfigurator { configureDatabase(); configureCache(); configureProxy(); + configureManagement(); } /** @@ -137,6 +139,11 @@ public class KeycloakDistConfigurator { .mapOption("proxy-headers", ProxySpec::getHeaders); } + void configureManagement() { + optionMapper(keycloakCR -> keycloakCR.getSpec().getHttpManagementSpec()) + .mapOption("http-management-port", HttpManagementSpec::getPort); + } + /* ---------- END of configuration of first-class citizen fields ---------- */ /** diff --git a/operator/src/main/java/org/keycloak/operator/controllers/KeycloakServiceDependentResource.java b/operator/src/main/java/org/keycloak/operator/controllers/KeycloakServiceDependentResource.java index c545f817fc..63c9fd2a89 100644 --- a/operator/src/main/java/org/keycloak/operator/controllers/KeycloakServiceDependentResource.java +++ b/operator/src/main/java/org/keycloak/operator/controllers/KeycloakServiceDependentResource.java @@ -29,7 +29,9 @@ import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDep import org.keycloak.operator.Constants; import org.keycloak.operator.Utils; import org.keycloak.operator.crds.v2alpha1.deployment.Keycloak; +import org.keycloak.operator.crds.v2alpha1.deployment.KeycloakSpec; import org.keycloak.operator.crds.v2alpha1.deployment.spec.HttpSpec; +import org.keycloak.operator.crds.v2alpha1.deployment.spec.HttpManagementSpec; import java.util.Optional; @@ -56,13 +58,31 @@ public class KeycloakServiceDependentResource extends CRUDKubernetesDependentRes Optional httpSpec = Optional.ofNullable(keycloak.getSpec().getHttpSpec()); boolean httpEnabled = httpSpec.map(HttpSpec::getHttpEnabled).orElse(false); if (!tlsConfigured || httpEnabled) { - builder.addNewPort().withPort(getServicePort(false, keycloak)).withName(Constants.KEYCLOAK_HTTP_PORT_NAME) - .withProtocol(Constants.KEYCLOAK_SERVICE_PROTOCOL).endPort(); + builder.addNewPort() + .withPort(getServicePort(false, keycloak)) + .withName(Constants.KEYCLOAK_HTTP_PORT_NAME) + .withProtocol(Constants.KEYCLOAK_SERVICE_PROTOCOL) + .endPort(); } if (tlsConfigured) { - builder.addNewPort().withPort(getServicePort(true, keycloak)).withName(Constants.KEYCLOAK_HTTPS_PORT_NAME) - .withProtocol(Constants.KEYCLOAK_SERVICE_PROTOCOL).endPort(); + builder.addNewPort() + .withPort(getServicePort(true, keycloak)) + .withName(Constants.KEYCLOAK_HTTPS_PORT_NAME) + .withProtocol(Constants.KEYCLOAK_SERVICE_PROTOCOL) + .endPort(); } + + var managementPort = Optional.ofNullable(keycloak.getSpec()) + .map(KeycloakSpec::getHttpManagementSpec) + .map(HttpManagementSpec::getPort) + .orElse(Constants.KEYCLOAK_MANAGEMENT_PORT); + + builder.addNewPort() + .withPort(managementPort) + .withName(Constants.KEYCLOAK_MANAGEMENT_PORT_NAME) + .withProtocol(Constants.KEYCLOAK_SERVICE_PROTOCOL) + .endPort(); + return builder.build(); } diff --git a/operator/src/main/java/org/keycloak/operator/crds/v2alpha1/deployment/KeycloakSpec.java b/operator/src/main/java/org/keycloak/operator/crds/v2alpha1/deployment/KeycloakSpec.java index bd0b67612b..5b4711d15f 100644 --- a/operator/src/main/java/org/keycloak/operator/crds/v2alpha1/deployment/KeycloakSpec.java +++ b/operator/src/main/java/org/keycloak/operator/crds/v2alpha1/deployment/KeycloakSpec.java @@ -24,6 +24,7 @@ import org.keycloak.operator.crds.v2alpha1.deployment.spec.CacheSpec; import org.keycloak.operator.crds.v2alpha1.deployment.spec.DatabaseSpec; import org.keycloak.operator.crds.v2alpha1.deployment.spec.FeatureSpec; import org.keycloak.operator.crds.v2alpha1.deployment.spec.HostnameSpec; +import org.keycloak.operator.crds.v2alpha1.deployment.spec.HttpManagementSpec; import org.keycloak.operator.crds.v2alpha1.deployment.spec.HttpSpec; import org.keycloak.operator.crds.v2alpha1.deployment.spec.IngressSpec; import org.keycloak.operator.crds.v2alpha1.deployment.spec.ProxySpec; @@ -105,6 +106,10 @@ public class KeycloakSpec { @JsonPropertyDescription("In this section you can configure Keycloak's reverse proxy setting") private ProxySpec proxySpec; + @JsonProperty("httpManagement") + @JsonPropertyDescription("In this section you can configure Keycloak's management interface setting.") + private HttpManagementSpec httpManagementSpec; + public HttpSpec getHttpSpec() { return httpSpec; } @@ -185,6 +190,14 @@ public class KeycloakSpec { this.imagePullSecrets = imagePullSecrets; } + public HttpManagementSpec getHttpManagementSpec() { + return httpManagementSpec; + } + + public void setHttpManagementSpec(HttpManagementSpec httpManagementSpec) { + this.httpManagementSpec = httpManagementSpec; + } + public List getAdditionalOptions() { if (this.additionalOptions == null) { this.additionalOptions = new ArrayList<>(); diff --git a/operator/src/main/java/org/keycloak/operator/crds/v2alpha1/deployment/spec/HttpManagementSpec.java b/operator/src/main/java/org/keycloak/operator/crds/v2alpha1/deployment/spec/HttpManagementSpec.java new file mode 100644 index 0000000000..dc95787331 --- /dev/null +++ b/operator/src/main/java/org/keycloak/operator/crds/v2alpha1/deployment/spec/HttpManagementSpec.java @@ -0,0 +1,38 @@ +/* + * Copyright 2024 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.operator.crds.v2alpha1.deployment.spec; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonPropertyDescription; +import io.sundr.builder.annotations.Buildable; +import org.keycloak.operator.Constants; + +@JsonInclude(JsonInclude.Include.NON_NULL) +@Buildable(editableEnabled = false, builderPackage = "io.fabric8.kubernetes.api.builder") +public class HttpManagementSpec { + + @JsonPropertyDescription("Port of the management interface.") + private Integer port = Constants.KEYCLOAK_MANAGEMENT_PORT; + + public Integer getPort() { + return port; + } + + public void setPort(Integer port) { + this.port = port; + } +} 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 4ba466aebb..87c97681da 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 @@ -287,6 +287,7 @@ public class KeycloakDeploymentTest extends BaseOperatorTest { deployKeycloak(k8sclient, kc, true); assertKeycloakAccessibleViaService(kc, false, Constants.KEYCLOAK_HTTP_PORT); + assertManagementInterfaceAccessibleViaService(kc, false); } @Test @@ -296,6 +297,9 @@ public class KeycloakDeploymentTest extends BaseOperatorTest { deployKeycloak(k8sclient, kc, true); assertKeycloakAccessibleViaService(kc, false, Constants.KEYCLOAK_HTTP_PORT); + + // if TLS is enabled, management interface should use https + assertManagementInterfaceAccessibleViaService(kc, true); } @Test @@ -723,7 +727,7 @@ public class KeycloakDeploymentTest extends BaseOperatorTest { String serviceName = KeycloakServiceDependentResource.getServiceName(kc); assertThat(k8sclient.resources(Service.class).withName(serviceName).require().getSpec().getPorts() - .stream().map(ServicePort::getName).anyMatch(protocol::equals)); + .stream().map(ServicePort::getName).anyMatch(protocol::equals)).isTrue(); String url = protocol + "://" + serviceName + "." + namespace + ":" + port + "/admin/master/console/"; Log.info("Checking url: " + url); @@ -734,4 +738,23 @@ public class KeycloakDeploymentTest extends BaseOperatorTest { assertEquals("200", curlOutput); }); } + + private void assertManagementInterfaceAccessibleViaService(Keycloak kc, boolean https) { + Awaitility.await() + .ignoreExceptions() + .untilAsserted(() -> { + String serviceName = KeycloakServiceDependentResource.getServiceName(kc); + assertThat(k8sclient.resources(Service.class).withName(serviceName).require().getSpec().getPorts() + .stream().map(ServicePort::getName).anyMatch(Constants.KEYCLOAK_MANAGEMENT_PORT_NAME::equals)).isTrue(); + + String protocol = https ? "https" : "http"; + String url = protocol + "://" + serviceName + "." + namespace + ":" + Constants.KEYCLOAK_MANAGEMENT_PORT; + Log.info("Checking url: " + url); + + var curlOutput = K8sUtils.inClusterCurl(k8sclient, namespace, url); + Log.info("Curl Output: " + curlOutput); + + assertEquals("200", curlOutput); + }); + } } diff --git a/operator/src/test/java/org/keycloak/operator/testsuite/integration/KeycloakServicesTest.java b/operator/src/test/java/org/keycloak/operator/testsuite/integration/KeycloakServicesTest.java index 13e28954bf..0697a51bc4 100644 --- a/operator/src/test/java/org/keycloak/operator/testsuite/integration/KeycloakServicesTest.java +++ b/operator/src/test/java/org/keycloak/operator/testsuite/integration/KeycloakServicesTest.java @@ -61,9 +61,6 @@ public class KeycloakServicesTest extends BaseOperatorTest { currentService.getSpec().setSessionAffinity("ClientIP"); var origSpecs = new ServiceSpecBuilder(currentService.getSpec()).build(); // deep copy - // a managed change - currentService.getSpec().getPorts().get(0).setName(null); - currentService.getMetadata().getLabels().putAll(labels); currentService.getMetadata().setResourceVersion(null); diff --git a/operator/src/test/java/org/keycloak/operator/testsuite/unit/CRSerializationTest.java b/operator/src/test/java/org/keycloak/operator/testsuite/unit/CRSerializationTest.java index fb64c0ba5d..ae9980e417 100644 --- a/operator/src/test/java/org/keycloak/operator/testsuite/unit/CRSerializationTest.java +++ b/operator/src/test/java/org/keycloak/operator/testsuite/unit/CRSerializationTest.java @@ -26,6 +26,7 @@ import org.keycloak.operator.crds.v2alpha1.deployment.ValueOrSecret; import org.keycloak.operator.crds.v2alpha1.deployment.spec.DatabaseSpec; import org.keycloak.operator.crds.v2alpha1.deployment.spec.FeatureSpec; import org.keycloak.operator.crds.v2alpha1.deployment.spec.HostnameSpec; +import org.keycloak.operator.crds.v2alpha1.deployment.spec.HttpManagementSpec; import org.keycloak.operator.crds.v2alpha1.deployment.spec.TransactionsSpec; import org.keycloak.operator.crds.v2alpha1.realmimport.KeycloakRealmImport; import org.keycloak.operator.testsuite.utils.K8sUtils; @@ -89,6 +90,10 @@ public class CRSerializationTest { assertEquals("usernameSecretKey", databaseSpec.getUsernameSecret().getKey()); assertEquals("passwordSecret", databaseSpec.getPasswordSecret().getName()); assertEquals("passwordSecretKey", databaseSpec.getPasswordSecret().getKey()); + + HttpManagementSpec managementSpec = keycloak.getSpec().getHttpManagementSpec(); + assertNotNull(managementSpec); + assertEquals(9003, managementSpec.getPort()); } @Test diff --git a/operator/src/test/java/org/keycloak/operator/testsuite/unit/KeycloakDistConfiguratorTest.java b/operator/src/test/java/org/keycloak/operator/testsuite/unit/KeycloakDistConfiguratorTest.java index 895253ed2b..aed62eadf6 100644 --- a/operator/src/test/java/org/keycloak/operator/testsuite/unit/KeycloakDistConfiguratorTest.java +++ b/operator/src/test/java/org/keycloak/operator/testsuite/unit/KeycloakDistConfiguratorTest.java @@ -144,6 +144,15 @@ public class KeycloakDistConfiguratorTest { testFirstClassCitizen(expectedValues); } + @Test + public void management() { + final Map expectedValues = new HashMap<>(Map.of( + "http-management-port", "9003" + )); + + testFirstClassCitizen(expectedValues); + } + /* UTILS */ private void testFirstClassCitizen(Map expectedValues) { diff --git a/operator/src/test/java/org/keycloak/operator/testsuite/unit/PodTemplateTest.java b/operator/src/test/java/org/keycloak/operator/testsuite/unit/PodTemplateTest.java index 9aea4ab41f..2e2141d3b1 100644 --- a/operator/src/test/java/org/keycloak/operator/testsuite/unit/PodTemplateTest.java +++ b/operator/src/test/java/org/keycloak/operator/testsuite/unit/PodTemplateTest.java @@ -55,6 +55,7 @@ import jakarta.inject.Inject; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; @QuarkusTest @@ -341,7 +342,7 @@ public class PodTemplateTest { @Test public void testRelativePathHealthProbes() { final Function setUpRelativePath = (path) -> getDeployment(null, new StatefulSet(), - spec -> spec.withAdditionalOptions(new ValueOrSecret("http-relative-path", path))) + spec -> spec.withAdditionalOptions(new ValueOrSecret("http-management-relative-path", path))) .getSpec() .getTemplate() .getSpec() @@ -351,18 +352,22 @@ public class PodTemplateTest { var first = setUpRelativePath.apply("/"); assertEquals("/health/ready", first.getReadinessProbe().getHttpGet().getPath()); assertEquals("/health/live", first.getLivenessProbe().getHttpGet().getPath()); + assertEquals("/health/started", first.getStartupProbe().getHttpGet().getPath()); var second = setUpRelativePath.apply("some"); assertEquals("some/health/ready", second.getReadinessProbe().getHttpGet().getPath()); assertEquals("some/health/live", second.getLivenessProbe().getHttpGet().getPath()); + assertEquals("some/health/started", second.getStartupProbe().getHttpGet().getPath()); var third = setUpRelativePath.apply(""); assertEquals("/health/ready", third.getReadinessProbe().getHttpGet().getPath()); assertEquals("/health/live", third.getLivenessProbe().getHttpGet().getPath()); + assertEquals("/health/started", third.getStartupProbe().getHttpGet().getPath()); var fourth = setUpRelativePath.apply("/some/"); assertEquals("/some/health/ready", fourth.getReadinessProbe().getHttpGet().getPath()); assertEquals("/some/health/live", fourth.getLivenessProbe().getHttpGet().getPath()); + assertEquals("/some/health/started", fourth.getStartupProbe().getHttpGet().getPath()); } @Test @@ -372,10 +377,27 @@ public class PodTemplateTest { // Act var podTemplate = getDeployment(additionalPodTemplate).getSpec().getTemplate(); + var container = podTemplate.getSpec().getContainers().get(0); // Assert - assertThat(podTemplate.getSpec().getContainers().get(0).getArgs()).doesNotContain(KeycloakDeploymentDependentResource.OPTIMIZED_ARG); - assertThat(podTemplate.getSpec().getContainers().get(0).getEnv().stream().anyMatch(envVar -> envVar.getName().equals(KeycloakDeploymentDependentResource.KC_TRUSTSTORE_PATHS))); + assertNotNull(container); + assertThat(container.getArgs()).doesNotContain(KeycloakDeploymentDependentResource.OPTIMIZED_ARG); + assertThat(container.getEnv().stream()).anyMatch(envVar -> envVar.getName().equals(KeycloakDeploymentDependentResource.KC_TRUSTSTORE_PATHS)); + + var readiness = container.getReadinessProbe().getHttpGet(); + assertNotNull(readiness); + assertThat(readiness.getPath()).isEqualTo("/health/ready"); + assertThat(readiness.getPort().getIntVal()).isEqualTo(Constants.KEYCLOAK_MANAGEMENT_PORT); + + var liveness = container.getLivenessProbe().getHttpGet(); + assertNotNull(liveness); + assertThat(liveness.getPath()).isEqualTo("/health/live"); + assertThat(liveness.getPort().getIntVal()).isEqualTo(Constants.KEYCLOAK_MANAGEMENT_PORT); + + var startup = container.getStartupProbe().getHttpGet(); + assertNotNull(startup); + assertThat(startup.getPath()).isEqualTo("/health/started"); + assertThat(startup.getPort().getIntVal()).isEqualTo(Constants.KEYCLOAK_MANAGEMENT_PORT); } @Test diff --git a/operator/src/test/resources/test-serialization-keycloak-cr.yml b/operator/src/test/resources/test-serialization-keycloak-cr.yml index f6e5e48629..22a622418a 100644 --- a/operator/src/test/resources/test-serialization-keycloak-cr.yml +++ b/operator/src/test/resources/test-serialization-keycloak-cr.yml @@ -69,6 +69,8 @@ spec: x: secret: name: my-secret + httpManagement: + port: 9003 unsupported: podTemplate: metadata: diff --git a/quarkus/config-api/src/main/java/org/keycloak/config/ManagementOptions.java b/quarkus/config-api/src/main/java/org/keycloak/config/ManagementOptions.java new file mode 100644 index 0000000000..a15cfbe33a --- /dev/null +++ b/quarkus/config-api/src/main/java/org/keycloak/config/ManagementOptions.java @@ -0,0 +1,104 @@ +/* + * Copyright 2024 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.config; + +import java.io.File; +import java.util.List; + +/** + * Options for the management interface that handles management endpoints (f.e. health and metrics endpoints) + */ +public class ManagementOptions { + + public static final Option LEGACY_OBSERVABILITY_INTERFACE = new OptionBuilder<>("legacy-observability-interface", Boolean.class) + .category(OptionCategory.MANAGEMENT) + .deprecated() + .description("If metrics/health endpoints should be exposed on the main HTTP server (not recommended). If set to true, the management interface is disabled.") + .defaultValue(Boolean.FALSE) + .buildTime(true) + .build(); + + public static final Option HTTP_MANAGEMENT_RELATIVE_PATH = new OptionBuilder<>("http-management-relative-path", String.class) + .category(OptionCategory.MANAGEMENT) + .description("Set the path relative to '/' for serving resources from management interface. The path must start with a '/'. If not given, the value is inherited from HTTP options.") + .defaultValue("/") + .buildTime(true) + .build(); + + public static final Option HTTP_MANAGEMENT_PORT = new OptionBuilder<>("http-management-port", Integer.class) + .category(OptionCategory.MANAGEMENT) + .description("Port of the management interface.") + .defaultValue(9000) + .build(); + + public static final Option HTTP_MANAGEMENT_HOST = new OptionBuilder<>("http-management-host", String.class) + .hidden() + .category(OptionCategory.MANAGEMENT) + .description("Host of the management interface. If not given, the value is inherited from HTTP options.") + .defaultValue("0.0.0.0") + .build(); + + //HTTPS + public static final Option HTTPS_MANAGEMENT_CLIENT_AUTH = new OptionBuilder<>("https-management-client-auth", HttpOptions.ClientAuth.class) + .category(OptionCategory.MANAGEMENT) + .description("Configures the management interface to require/request client authentication. If not given, the value is inherited from HTTP options.") + .defaultValue(HttpOptions.ClientAuth.none) + .buildTime(true) + .build(); + + public static final Option HTTPS_MANAGEMENT_CIPHER_SUITES = new OptionBuilder<>("https-management-cipher-suites", String.class) + .hidden() + .category(OptionCategory.MANAGEMENT) + .description("The cipher suites to use for the management server. If not given, the value is inherited from HTTP options.") + .hidden() + .build(); + + public static final Option> HTTPS_MANAGEMENT_PROTOCOLS = OptionBuilder.listOptionBuilder("https-management-protocols", String.class) + .hidden() + .category(OptionCategory.MANAGEMENT) + .description("The list of protocols to explicitly enable for the management server. If not given, the value is inherited from HTTP options.") + .defaultValue(List.of("TLSv1.3,TLSv1.2")) + .hidden() + .build(); + + public static final Option HTTPS_MANAGEMENT_CERTIFICATE_FILE = new OptionBuilder<>("https-management-certificate-file", File.class) + .category(OptionCategory.MANAGEMENT) + .description("The file path to a server certificate or certificate chain in PEM format for the management server. If not given, the value is inherited from HTTP options.") + .build(); + + public static final Option HTTPS_MANAGEMENT_CERTIFICATE_KEY_FILE = new OptionBuilder<>("https-management-certificate-key-file", File.class) + .category(OptionCategory.MANAGEMENT) + .description("The file path to a private key in PEM format for the management server. If not given, the value is inherited from HTTP options.") + .build(); + + public static final Option HTTPS_MANAGEMENT_KEY_STORE_FILE = new OptionBuilder<>("https-management-key-store-file", File.class) + .category(OptionCategory.MANAGEMENT) + .description("The key store which holds the certificate information instead of specifying separate files for the management server. If not given, the value is inherited from HTTP options.") + .build(); + + public static final Option HTTPS_MANAGEMENT_KEY_STORE_PASSWORD = new OptionBuilder<>("https-management-key-store-password", String.class) + .category(OptionCategory.MANAGEMENT) + .description("The password of the key store file for the management server. If not given, the value is inherited from HTTP options.") + .defaultValue("password") + .build(); + + public static final Option HTTPS_MANAGEMENT_KEY_STORE_TYPE = new OptionBuilder<>("https-management-key-store-type", String.class) + .hidden() + .category(OptionCategory.MANAGEMENT) + .description("The type of the key store file for the management server. If not given, the value is inherited from HTTP options.") + .build(); +} 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 084b499477..584432e8ce 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 @@ -3,13 +3,14 @@ package org.keycloak.config; public enum OptionCategory { // ordered by name asc CACHE("Cache", 10, ConfigSupportLevel.SUPPORTED), + CONFIG("Config", 15, ConfigSupportLevel.SUPPORTED), DATABASE("Database", 20, ConfigSupportLevel.SUPPORTED), TRANSACTION("Transaction",30, ConfigSupportLevel.SUPPORTED), FEATURE("Feature", 40, ConfigSupportLevel.SUPPORTED), HOSTNAME("Hostname", 50, ConfigSupportLevel.SUPPORTED), HTTP("HTTP(S)", 60, ConfigSupportLevel.SUPPORTED), HEALTH("Health", 70, ConfigSupportLevel.SUPPORTED), - CONFIG("Config", 75, ConfigSupportLevel.SUPPORTED), + MANAGEMENT("Management", 75, ConfigSupportLevel.SUPPORTED), METRICS("Metrics", 80, ConfigSupportLevel.SUPPORTED), PROXY("Proxy", 90, ConfigSupportLevel.SUPPORTED), VAULT("Vault", 100, ConfigSupportLevel.SUPPORTED), diff --git a/quarkus/container/Dockerfile b/quarkus/container/Dockerfile index 0fe77a56d3..846fbceb8b 100644 --- a/quarkus/container/Dockerfile +++ b/quarkus/container/Dockerfile @@ -35,5 +35,6 @@ USER 1000 EXPOSE 8080 EXPOSE 8443 +EXPOSE 9000 ENTRYPOINT [ "/opt/keycloak/bin/kc.sh" ] diff --git a/quarkus/deployment/src/main/java/org/keycloak/quarkus/deployment/IsManagementEnabled.java b/quarkus/deployment/src/main/java/org/keycloak/quarkus/deployment/IsManagementEnabled.java new file mode 100644 index 0000000000..bfd2661bd5 --- /dev/null +++ b/quarkus/deployment/src/main/java/org/keycloak/quarkus/deployment/IsManagementEnabled.java @@ -0,0 +1,28 @@ +/* + * Copyright 2024 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.quarkus.deployment; + +import org.keycloak.quarkus.runtime.configuration.mappers.ManagementPropertyMappers; + +import java.util.function.BooleanSupplier; + +public class IsManagementEnabled implements BooleanSupplier { + @Override + public boolean getAsBoolean() { + return ManagementPropertyMappers.isManagementEnabled(); + } +} diff --git a/quarkus/deployment/src/main/java/org/keycloak/quarkus/deployment/KeycloakProcessor.java b/quarkus/deployment/src/main/java/org/keycloak/quarkus/deployment/KeycloakProcessor.java index 107bff180e..c0da58af69 100644 --- a/quarkus/deployment/src/main/java/org/keycloak/quarkus/deployment/KeycloakProcessor.java +++ b/quarkus/deployment/src/main/java/org/keycloak/quarkus/deployment/KeycloakProcessor.java @@ -45,6 +45,8 @@ import io.quarkus.hibernate.orm.deployment.spi.AdditionalJpaModelBuildItem; import io.quarkus.resteasy.reactive.server.spi.MethodScannerBuildItem; import io.quarkus.runtime.configuration.ConfigurationException; import io.quarkus.runtime.configuration.ProfileManager; +import io.quarkus.vertx.http.deployment.NonApplicationRootPathBuildItem; +import io.quarkus.vertx.http.deployment.RouteBuildItem; import io.smallrye.config.ConfigValue; import org.eclipse.microprofile.health.Readiness; import org.hibernate.cfg.AvailableSettings; @@ -69,6 +71,7 @@ import org.keycloak.common.crypto.FipsMode; import org.keycloak.common.util.StreamUtil; import org.keycloak.config.DatabaseOptions; import org.keycloak.config.HealthOptions; +import org.keycloak.config.ManagementOptions; import org.keycloak.config.MetricsOptions; import org.keycloak.config.SecurityOptions; import org.keycloak.connections.jpa.DefaultJpaConnectionProviderFactory; @@ -131,7 +134,6 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.util.ArrayList; import java.util.Collection; -import java.util.Comparator; import java.util.Enumeration; import java.util.HashMap; import java.util.List; @@ -232,6 +234,21 @@ class KeycloakProcessor { recorder.configureProfile(profile.getName(), profile.getFeatures()); } + @Record(ExecutionTime.STATIC_INIT) + @BuildStep(onlyIf = IsManagementEnabled.class) + @Consume(ConfigBuildItem.class) + void configureManagementInterface(BuildProducer routes, + NonApplicationRootPathBuildItem nonApplicationRootPathBuildItem, + KeycloakRecorder recorder) { + final var path = Configuration.getOptionalKcValue(ManagementOptions.HTTP_MANAGEMENT_RELATIVE_PATH.getKey()).orElse("/"); + + routes.produce(nonApplicationRootPathBuildItem.routeBuilder() + .management() + .route(path) + .handler(recorder.getManagementHandler()) + .build()); + } + @Record(ExecutionTime.STATIC_INIT) @BuildStep @Consume(ConfigBuildItem.class) diff --git a/quarkus/deployment/src/test/java/test/org/keycloak/quarkus/services/health/KeycloakMetricsConfigurationTest.java b/quarkus/deployment/src/test/java/test/org/keycloak/quarkus/services/health/KeycloakMetricsConfigurationTest.java new file mode 100644 index 0000000000..852c2e4d64 --- /dev/null +++ b/quarkus/deployment/src/test/java/test/org/keycloak/quarkus/services/health/KeycloakMetricsConfigurationTest.java @@ -0,0 +1,59 @@ +/* + * Copyright 2020 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package test.org.keycloak.quarkus.services.health; + +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import static io.restassured.RestAssured.given; + +class KeycloakMetricsConfigurationTest { + + @BeforeEach + void setUpPort() { + RestAssured.port = 9001; + } + + @RegisterExtension + static final QuarkusUnitTest test = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addAsResource("keycloak.conf", "META-INF/keycloak.conf")) + .overrideConfigKey("quarkus.micrometer.export.prometheus.path", "/prom/metrics"); + + @Test + void testMetrics() { + given().basePath("/") + .when().get("prom/metrics") + .then() + .statusCode(200); + } + + @Test + void testWrongMetricsEndpoints() { + given().basePath("/") + .when().get("metrics") + .then() + // Metrics are available under `/prom/metrics` (see quarkus.micrometer.export.prometheus.path) + // so /metrics should return 404. + .statusCode(404); + } +} diff --git a/quarkus/deployment/src/test/java/test/org/keycloak/quarkus/services/health/KeycloakNegativeHealthCheckTest.java b/quarkus/deployment/src/test/java/test/org/keycloak/quarkus/services/health/KeycloakNegativeHealthCheckTest.java index 4394b94aab..1fd852bc45 100644 --- a/quarkus/deployment/src/test/java/test/org/keycloak/quarkus/services/health/KeycloakNegativeHealthCheckTest.java +++ b/quarkus/deployment/src/test/java/test/org/keycloak/quarkus/services/health/KeycloakNegativeHealthCheckTest.java @@ -18,6 +18,7 @@ package test.org.keycloak.quarkus.services.health; import io.agroal.api.AgroalDataSource; import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; import org.hamcrest.Matchers; import org.jboss.shrinkwrap.api.ShrinkWrap; import org.jboss.shrinkwrap.api.spec.JavaArchive; @@ -41,6 +42,8 @@ public class KeycloakNegativeHealthCheckTest { @Test public void testReadinessDown() { agroalDataSource.close(); + + RestAssured.port = 9001; given() .when().get("/health/ready") .then() diff --git a/quarkus/deployment/src/test/java/test/org/keycloak/quarkus/services/health/KeycloakPathConfigurationTest.java b/quarkus/deployment/src/test/java/test/org/keycloak/quarkus/services/health/KeycloakPathConfigurationTest.java index 5eb4db07ad..993f3d0500 100644 --- a/quarkus/deployment/src/test/java/test/org/keycloak/quarkus/services/health/KeycloakPathConfigurationTest.java +++ b/quarkus/deployment/src/test/java/test/org/keycloak/quarkus/services/health/KeycloakPathConfigurationTest.java @@ -17,8 +17,10 @@ package test.org.keycloak.quarkus.services.health; import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; import org.jboss.shrinkwrap.api.ShrinkWrap; import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; @@ -26,40 +28,18 @@ import static io.restassured.RestAssured.given; class KeycloakPathConfigurationTest { + @BeforeEach + void setUpPort() { + RestAssured.port = 9001; + } + @RegisterExtension static final QuarkusUnitTest test = new QuarkusUnitTest() .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) .addAsResource("keycloak.conf", "META-INF/keycloak.conf")) .overrideConfigKey("kc.http-relative-path","/auth") - .overrideConfigKey("quarkus.http.non-application-root-path", "/q") .overrideConfigKey("quarkus.micrometer.export.prometheus.path", "/prom/metrics"); - - @Test - void testHealth() { - given().basePath("/") - .when().get("q/health") - .then() - .statusCode(200); - } - - @Test - void testWrongHealthEndpoints() { - given().basePath("/") - .when().get("health") - .then() - // Health is available under `/q/health` (see non-application-root-path), - // so /health should return 404. - .statusCode(404); - - given().basePath("/") - .when().get("auth/health") - .then() - // Health is available under `/q/health` (see non-application-root-path), - // so /auth/health one should return 404. - .statusCode(404); - } - @Test void testMetrics() { given().basePath("/") @@ -68,6 +48,20 @@ class KeycloakPathConfigurationTest { .statusCode(200); } + @Test + void testHealth() { + given().basePath("/") + .when().get("health") + .then() + // Health is available under `/auth/health` (see http-relative-path), + .statusCode(404); + + given().basePath("/") + .when().get("auth/health") + .then() + .statusCode(200); + } + @Test void testWrongMetricsEndpoints() { given().basePath("/") diff --git a/quarkus/deployment/src/test/java/test/org/keycloak/quarkus/services/health/KeycloakReadyHealthCheckTest.java b/quarkus/deployment/src/test/java/test/org/keycloak/quarkus/services/health/KeycloakReadyHealthCheckTest.java index 8eff3cb4b0..f32b96fd3a 100644 --- a/quarkus/deployment/src/test/java/test/org/keycloak/quarkus/services/health/KeycloakReadyHealthCheckTest.java +++ b/quarkus/deployment/src/test/java/test/org/keycloak/quarkus/services/health/KeycloakReadyHealthCheckTest.java @@ -17,19 +17,23 @@ package test.org.keycloak.quarkus.services.health; import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; import org.hamcrest.Matchers; import org.jboss.shrinkwrap.api.ShrinkWrap; import org.jboss.shrinkwrap.api.spec.JavaArchive; -import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; -import java.sql.SQLException; - import static io.restassured.RestAssured.given; public class KeycloakReadyHealthCheckTest { + @BeforeEach + void setUpPort() { + RestAssured.port = 9001; + } + @RegisterExtension static final QuarkusUnitTest test = new QuarkusUnitTest() .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) @@ -45,7 +49,7 @@ public class KeycloakReadyHealthCheckTest { } @Test - public void testReadinessUp() throws SQLException { + public void testReadinessUp() { given() .when().get("/health/ready") .then() diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/KeycloakRecorder.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/KeycloakRecorder.java index d5ffa75a88..b42dbe55d0 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/KeycloakRecorder.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/KeycloakRecorder.java @@ -30,6 +30,8 @@ import io.quarkus.agroal.DataSource; import io.quarkus.arc.Arc; import io.quarkus.arc.InstanceHandle; import io.quarkus.hibernate.orm.runtime.integration.HibernateOrmIntegrationRuntimeInitListener; +import io.vertx.core.Handler; +import io.vertx.ext.web.RoutingContext; import liquibase.Scope; import org.hibernate.cfg.AvailableSettings; @@ -73,6 +75,11 @@ public class KeycloakRecorder { Profile.init(profileName, features); } + // default handler for the management interface + public Handler getManagementHandler() { + return routingContext -> routingContext.response().end("Keycloak Management Interface"); + } + public void configureTruststore() { String[] truststores = Configuration.getOptionalKcValue(TruststoreOptions.TRUSTSTORE_PATHS.getKey()) .map(s -> s.split(",")).orElse(new String[0]); 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 7764458a93..468d40631f 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 @@ -52,6 +52,10 @@ public final class Configuration { return getOptionalBooleanValue(NS_KEYCLOAK_PREFIX + option.getKey()).orElse(false); } + public static boolean isTrue(String propertyName) { + return getOptionalBooleanValue(propertyName).orElse(false); + } + public static boolean contains(Option option, String value) { return getOptionalValue(NS_KEYCLOAK_PREFIX + option.getKey()) .filter(f -> f.contains(value)) 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 new file mode 100644 index 0000000000..9bf4a7e789 --- /dev/null +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/ManagementPropertyMappers.java @@ -0,0 +1,146 @@ +/* + * Copyright 2024 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.quarkus.runtime.configuration.mappers; + +import io.smallrye.config.ConfigSourceInterceptorContext; +import org.keycloak.config.HttpOptions; +import org.keycloak.config.ManagementOptions; +import org.keycloak.quarkus.runtime.Messages; +import org.keycloak.quarkus.runtime.cli.PropertyException; +import org.keycloak.quarkus.runtime.configuration.Configuration; + +import java.util.Optional; + +import static org.keycloak.quarkus.runtime.configuration.Configuration.isTrue; +import static org.keycloak.quarkus.runtime.configuration.mappers.PropertyMapper.fromOption; + +public class ManagementPropertyMappers { + private static final String MANAGEMENT_ENABLED_MSG = "Management interface is enabled"; + + private ManagementPropertyMappers() { + } + + public static PropertyMapper[] getManagementPropertyMappers() { + return new PropertyMapper[]{ + fromOption(ManagementOptions.LEGACY_OBSERVABILITY_INTERFACE) + .to("quarkus.management.enabled") // ATM, the management interface state is only based on the legacy-observability-interface property + .paramLabel(Boolean.TRUE + "|" + Boolean.FALSE) + .transformer(ManagementPropertyMappers::managementEnabledTransformer) + .build(), + fromOption(ManagementOptions.HTTP_MANAGEMENT_RELATIVE_PATH) + .isEnabled(ManagementPropertyMappers::isManagementEnabled, MANAGEMENT_ENABLED_MSG) + .mapFrom(HttpOptions.HTTP_RELATIVE_PATH.getKey()) + .to("quarkus.management.root-path") + .paramLabel("path") + .build(), + fromOption(ManagementOptions.HTTP_MANAGEMENT_PORT) + .isEnabled(ManagementPropertyMappers::isManagementEnabled, MANAGEMENT_ENABLED_MSG) + .to("quarkus.management.port") + .paramLabel("port") + .build(), + fromOption(ManagementOptions.HTTP_MANAGEMENT_HOST) + .isEnabled(ManagementPropertyMappers::isManagementEnabled, MANAGEMENT_ENABLED_MSG) + .mapFrom(HttpOptions.HTTP_HOST.getKey()) + .to("quarkus.management.host") + .paramLabel("host") + .build(), + // HTTPS + fromOption(ManagementOptions.HTTPS_MANAGEMENT_CLIENT_AUTH) + .isEnabled(ManagementPropertyMappers::isManagementEnabled, MANAGEMENT_ENABLED_MSG) + .mapFrom(HttpOptions.HTTPS_CLIENT_AUTH.getKey()) + .to("quarkus.management.ssl.client-auth") + .paramLabel("auth") + .build(), + fromOption(ManagementOptions.HTTPS_MANAGEMENT_CIPHER_SUITES) + .isEnabled(ManagementPropertyMappers::isManagementEnabled, MANAGEMENT_ENABLED_MSG) + .mapFrom(HttpOptions.HTTPS_CIPHER_SUITES.getKey()) + .to("quarkus.management.ssl.cipher-suites") + .paramLabel("ciphers") + .build(), + fromOption(ManagementOptions.HTTPS_MANAGEMENT_PROTOCOLS) + .isEnabled(ManagementPropertyMappers::isManagementEnabled, MANAGEMENT_ENABLED_MSG) + .mapFrom(HttpOptions.HTTPS_PROTOCOLS.getKey()) + .to("quarkus.management.ssl.protocols") + .paramLabel("protocols") + .build(), + fromOption(ManagementOptions.HTTPS_MANAGEMENT_CERTIFICATE_FILE) + .isEnabled(ManagementPropertyMappers::isManagementEnabled, MANAGEMENT_ENABLED_MSG) + .mapFrom(HttpOptions.HTTPS_CERTIFICATE_FILE.getKey()) + .to("quarkus.management.ssl.certificate.files") + .validator((mapper, value) -> validateTlsProperties()) + .paramLabel("file") + .build(), + fromOption(ManagementOptions.HTTPS_MANAGEMENT_CERTIFICATE_KEY_FILE) + .isEnabled(ManagementPropertyMappers::isManagementEnabled, MANAGEMENT_ENABLED_MSG) + .mapFrom(HttpOptions.HTTPS_CERTIFICATE_KEY_FILE.getKey()) + .to("quarkus.management.ssl.certificate.key-files") + .validator((mapper, value) -> validateTlsProperties()) + .paramLabel("file") + .build(), + fromOption(ManagementOptions.HTTPS_MANAGEMENT_KEY_STORE_FILE) + .isEnabled(ManagementPropertyMappers::isManagementEnabled, MANAGEMENT_ENABLED_MSG) + .mapFrom(HttpOptions.HTTPS_KEY_STORE_FILE.getKey()) + .to("quarkus.management.ssl.certificate.key-store-file") + .validator((mapper, value) -> validateTlsProperties()) + .paramLabel("file") + .build(), + fromOption(ManagementOptions.HTTPS_MANAGEMENT_KEY_STORE_PASSWORD) + .isEnabled(ManagementPropertyMappers::isManagementEnabled, MANAGEMENT_ENABLED_MSG) + .mapFrom(HttpOptions.HTTPS_KEY_STORE_PASSWORD.getKey()) + .to("quarkus.management.ssl.certificate.key-store-password") + .validator((mapper, value) -> validateTlsProperties()) + .paramLabel("password") + .isMasked(true) + .build(), + fromOption(ManagementOptions.HTTPS_MANAGEMENT_KEY_STORE_TYPE) + .isEnabled(ManagementPropertyMappers::isManagementEnabled, MANAGEMENT_ENABLED_MSG) + .mapFrom(HttpOptions.HTTPS_KEY_STORE_TYPE.getKey()) + .to("quarkus.management.ssl.certificate.key-store-file-type") + .transformer((value, config) -> value.or(() -> Configuration.getOptionalKcValue(HttpOptions.HTTPS_KEY_STORE_TYPE.getKey()))) + .paramLabel("type") + .build(), + }; + } + + public static boolean isManagementEnabled() { + return isTrue("quarkus.management.enabled"); + } + + public static boolean isManagementTlsEnabled() { + var key = Configuration.getOptionalKcValue(ManagementOptions.HTTPS_MANAGEMENT_CERTIFICATE_KEY_FILE.getKey()); + var cert = Configuration.getOptionalKcValue(ManagementOptions.HTTPS_MANAGEMENT_CERTIFICATE_FILE.getKey()); + if (key.isPresent() && cert.isPresent()) return true; + + var keystore = Configuration.getOptionalKcValue(ManagementOptions.HTTPS_MANAGEMENT_KEY_STORE_FILE.getKey()); + return keystore.isPresent(); + } + + private static void validateTlsProperties() { + var isHttpEnabled = Configuration.isTrue(HttpOptions.HTTP_ENABLED); + if (!isHttpEnabled && !isManagementTlsEnabled()) { + throw new PropertyException(Messages.httpsConfigurationNotSet()); + } + } + + private static Optional managementEnabledTransformer(Optional value, ConfigSourceInterceptorContext ctx) { + if (value.isPresent()) { + var b = Boolean.parseBoolean(value.get()); + return Optional.of(Boolean.toString(!b)); // negate the output + } + return Optional.of(Boolean.TRUE.toString()); + } +} 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 7f551efd00..4107e6263d 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 @@ -52,6 +52,7 @@ public final class PropertyMappers { MAPPERS.addAll(HttpPropertyMappers.getHttpPropertyMappers()); MAPPERS.addAll(HealthPropertyMappers.getHealthPropertyMappers()); MAPPERS.addAll(ConfigKeystorePropertyMappers.getConfigKeystorePropertyMappers()); + MAPPERS.addAll(ManagementPropertyMappers.getManagementPropertyMappers()); MAPPERS.addAll(MetricsPropertyMappers.getMetricsPropertyMappers()); MAPPERS.addAll(ProxyPropertyMappers.getProxyPropertyMappers()); MAPPERS.addAll(VaultPropertyMappers.getVaultPropertyMappers()); diff --git a/quarkus/runtime/src/test/java/org/keycloak/quarkus/runtime/configuration/test/ConfigurationTest.java b/quarkus/runtime/src/test/java/org/keycloak/quarkus/runtime/configuration/test/ConfigurationTest.java index 7521c7db2c..ad4f9db8e2 100644 --- a/quarkus/runtime/src/test/java/org/keycloak/quarkus/runtime/configuration/test/ConfigurationTest.java +++ b/quarkus/runtime/src/test/java/org/keycloak/quarkus/runtime/configuration/test/ConfigurationTest.java @@ -77,6 +77,10 @@ public class ConfigurationTest { } } + public static void putEnvVars(Map map) { + map.forEach(ConfigurationTest::putEnvVar); + } + @SuppressWarnings("unchecked") public static void removeEnvVar(String name) { Map env = System.getenv(); @@ -573,7 +577,7 @@ public class ConfigurationTest { assertEquals("secret", secret.getValue()); } - private Config.Scope initConfig(String... scope) { + protected Config.Scope initConfig(String... scope) { Config.init(new MicroProfileConfigProvider(createConfig())); return Config.scope(scope); } diff --git a/quarkus/runtime/src/test/java/org/keycloak/quarkus/runtime/configuration/test/ManagementConfigurationTest.java b/quarkus/runtime/src/test/java/org/keycloak/quarkus/runtime/configuration/test/ManagementConfigurationTest.java new file mode 100644 index 0000000000..9d5bbf3adc --- /dev/null +++ b/quarkus/runtime/src/test/java/org/keycloak/quarkus/runtime/configuration/test/ManagementConfigurationTest.java @@ -0,0 +1,277 @@ +/* + * Copyright 2024 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.quarkus.runtime.configuration.test; + +import org.junit.Test; +import org.keycloak.quarkus.runtime.configuration.Configuration; +import org.keycloak.quarkus.runtime.configuration.mappers.ManagementPropertyMappers; + +import java.util.Map; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.MatcherAssert.assertThat; + +public class ManagementConfigurationTest extends ConfigurationTest { + + @Test + public void managementDefaults() { + initConfig(); + + assertConfig(Map.of( + "http-management-port", "9000", + "http-management-relative-path", "/", + "http-management-host", "0.0.0.0" + )); + + assertManagementEnabled(true); + assertManagementHttpsEnabled(false); + } + + @Test + public void managementBasicChanges() { + putEnvVars(Map.of( + "KC_HTTP_MANAGEMENT_PORT", "9999", + "KC_HTTP_MANAGEMENT_RELATIVE_PATH", "/management2", + "KC_HTTP_MANAGEMENT_HOST", "somehost" + )); + + initConfig(); + + assertConfig(Map.of( + "http-management-port", "9999", + "http-management-relative-path", "/management2", + "http-relative-path", "/", + "http-management-host", "somehost" + )); + assertManagementEnabled(true); + } + + @Test + public void managementRelativePath() { + putEnvVar("KC_HTTP_RELATIVE_PATH", "/management3"); + + initConfig(); + + assertConfig(Map.of( + "http-management-relative-path", "/management3", + "http-relative-path", "/management3" + )); + assertManagementEnabled(true); + } + + @Test + public void managementHttpsValues() { + putEnvVars(Map.of( + "KC_HTTP_MANAGEMENT_HOST", "host1", + "KC_HTTPS_MANAGEMENT_CLIENT_AUTH", "requested", + "KC_HTTPS_MANAGEMENT_CIPHER_SUITES", "some-cipher-suite1", + "KC_HTTPS_MANAGEMENT_PROTOCOLS", "TLSv1.3", + "KC_HTTPS_MANAGEMENT_CERTIFICATE_FILE", "/some/path/s.crt.pem", + "KC_HTTPS_MANAGEMENT_CERTIFICATE_KEY_FILE", "/some/path/s.key.pem", + "KC_HTTPS_MANAGEMENT_KEY_STORE_FILE", "keystore123.p12", + "KC_HTTPS_MANAGEMENT_KEY_STORE_PASSWORD", "ultra-password123", + "KC_HTTPS_MANAGEMENT_KEY_STORE_TYPE", "BCFKS-0.1" + )); + + initConfig(); + + assertConfig(Map.of( + "http-management-host", "host1", + "https-management-client-auth", "requested", + "https-management-cipher-suites", "some-cipher-suite1", + "https-management-protocols", "TLSv1.3", + "https-management-certificate-file", "/some/path/s.crt.pem", + "https-management-certificate-key-file", "/some/path/s.key.pem", + "https-management-key-store-file", "keystore123.p12", + "https-management-key-store-password", "ultra-password123", + "https-management-key-store-type", "BCFKS-0.1" + )); + assertManagementEnabled(true); + assertManagementHttpsEnabled(true); + } + + @Test + public void managementMappedValues() { + putEnvVars(Map.of( + "KC_HTTP_HOST", "host123", + "KC_HTTPS_CLIENT_AUTH", "required", + "KC_HTTPS_CIPHER_SUITES", "some-cipher-suite", + "KC_HTTPS_PROTOCOLS", "TLSv1.2", + "KC_HTTPS_CERTIFICATE_FILE", "/some/path/srv.crt.pem", + "KC_HTTPS_CERTIFICATE_KEY_FILE", "/some/path/srv.key.pem", + "KC_HTTPS_KEY_STORE_FILE", "keystore.p12", + "KC_HTTPS_KEY_STORE_PASSWORD", "ultra-password", + "KC_HTTPS_KEY_STORE_TYPE", "BCFKS" + )); + + initConfig(); + + assertConfig(Map.of( + "http-management-host", "host123", + "https-management-client-auth", "required", + "https-management-cipher-suites", "some-cipher-suite", + "https-management-protocols", "TLSv1.2", + "https-management-certificate-file", "/some/path/srv.crt.pem", + "https-management-certificate-key-file", "/some/path/srv.key.pem", + "https-management-key-store-file", "keystore.p12", + "https-management-key-store-password", "ultra-password", + "https-management-key-store-type", "BCFKS" + )); + assertManagementEnabled(true); + assertManagementHttpsEnabled(true); + } + + @Test + public void managementDefaultHttps() { + putEnvVars(Map.of( + "KC_HTTPS_CERTIFICATE_FILE", "/some/path/srv.crt.pem", + "KC_HTTPS_CERTIFICATE_KEY_FILE", "/some/path/srv.key.pem" + )); + + initConfig(); + + assertConfig(Map.of( + "https-certificate-file", "/some/path/srv.crt.pem", + "https-certificate-key-file", "/some/path/srv.key.pem", + "https-management-certificate-file", "/some/path/srv.crt.pem", + "https-management-certificate-key-file", "/some/path/srv.key.pem" + )); + assertManagementEnabled(true); + assertManagementHttpsEnabled(true); + } + + @Test + public void managementDefaultHttpsManagementProps() { + putEnvVars(Map.of( + "KC_HTTPS_MANAGEMENT_CERTIFICATE_FILE", "/some/path/srv.crt.pem", + "KC_HTTPS_MANAGEMENT_CERTIFICATE_KEY_FILE", "/some/path/srv.key.pem" + )); + + initConfig(); + + assertConfig(Map.of( + "https-management-certificate-file", "/some/path/srv.crt.pem", + "https-management-certificate-key-file", "/some/path/srv.key.pem" + )); + assertManagementEnabled(true); + assertManagementHttpsEnabled(true); + } + + @Test + public void managementDefaultHttpsCertDisabled() { + putEnvVar("KC_HTTPS_CERTIFICATE_FILE", "/some/path/srv.crt.pem"); + + initConfig(); + + assertConfig("https-management-certificate-file", "/some/path/srv.crt.pem"); + assertManagementEnabled(true); + assertManagementHttpsEnabled(false); + } + + @Test + public void managementDefaultHttpsKeyDisabled() { + putEnvVar("KC_HTTPS_CERTIFICATE_KEY_FILE", "/some/path/srv.key.pem"); + + initConfig(); + + assertConfig("https-management-certificate-key-file", "/some/path/srv.key.pem"); + assertManagementEnabled(true); + assertManagementHttpsEnabled(false); + } + + @Test + public void managementEnabledDefaultHttpsKeystore(){ + putEnvVar("KC_HTTPS_KEY_STORE_FILE", "keystore.p12"); + + initConfig(); + + assertConfig(Map.of( + "https-key-store-file", "keystore.p12", + "https-management-key-store-file", "keystore.p12" + )); + assertManagementEnabled(true); + assertManagementHttpsEnabled(true); + } + + @Test + public void fipsKeystoreType(){ + putEnvVar("KC_FIPS_MODE", "strict"); + + initConfig(); + + assertConfig(Map.of( + "https-key-store-type", "BCFKS", + "https-management-key-store-type", "BCFKS" + )); + assertManagementEnabled(true); + } + + @Test + public void keystoreType(){ + putEnvVars(Map.of( + "KC_HTTPS_KEY_STORE_TYPE", "pkcs12", + "KC_HTTPS_MANAGEMENT_KEY_STORE_TYPE", "BCFKS" + )); + + initConfig(); + + assertConfig(Map.of( + "https-key-store-type", "pkcs12", + "https-management-key-store-type", "BCFKS" + )); + assertManagementEnabled(true); + } + + @Test + public void legacyObservabilityInterface() { + putEnvVar("KC_LEGACY_OBSERVABILITY_INTERFACE", "true"); + + initConfig(); + + assertConfig("legacy-observability-interface", "true"); + assertManagementEnabled(false); + } + + @Test + public void legacyObservabilityInterfaceFalse() { + putEnvVar("KC_LEGACY_OBSERVABILITY_INTERFACE", "false"); + + initConfig(); + + assertConfig("legacy-observability-interface", "false"); + assertManagementEnabled(true); + } + + private void assertManagementEnabled(boolean expected) { + assertThat("Expected value for Management interface state is different", ManagementPropertyMappers.isManagementEnabled(), is(expected)); + } + + private void assertManagementHttpsEnabled(boolean expected) { + assertThat("Expected value for Management HTTPS is different", ManagementPropertyMappers.isManagementTlsEnabled(), is(expected)); + } + + private void assertConfig(String key, String expectedValue) { + var value = Configuration.getKcConfigValue(key).getValue(); + assertThat(String.format("Value is null for key '%s'", key), value, notNullValue()); + assertThat(String.format("Different value for key '%s'", key), value, is(expectedValue)); + } + + private void assertConfig(Map expectedValues) { + expectedValues.forEach(this::assertConfig); + } +} \ No newline at end of file diff --git a/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/HealthDistTest.java b/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/HealthDistTest.java index 2c83578e62..1ae011690e 100644 --- a/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/HealthDistTest.java +++ b/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/HealthDistTest.java @@ -29,7 +29,9 @@ import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; -@DistributionTest(keepAlive =true) +@DistributionTest(keepAlive = true, + requestPort = 9000, + containerExposedPorts = {8080, 9000}) public class HealthDistTest { @Test @@ -83,7 +85,7 @@ public class HealthDistTest { @Test void testUsingRelativePath(KeycloakDistribution distribution) { for (String relativePath : List.of("/auth", "/auth/", "auth")) { - distribution.run("start-dev", "--health-enabled=true", "--http-relative-path=" + relativePath); + distribution.run("start-dev", "--health-enabled=true", "--http-management-relative-path=" + relativePath); if (!relativePath.endsWith("/")) { relativePath = relativePath + "/"; } @@ -95,7 +97,7 @@ public class HealthDistTest { @Test void testMultipleRequests(KeycloakDistribution distribution) throws Exception { for (String relativePath : List.of("/", "/auth/", "auth")) { - distribution.run("start-dev", "--health-enabled=true", "--http-relative-path=" + relativePath); + distribution.run("start-dev", "--health-enabled=true", "--http-management-relative-path=" + relativePath); CompletableFuture future = CompletableFuture.completedFuture(null); for (int i = 0; i < 3; i++) { @@ -123,7 +125,9 @@ public class HealthDistTest { @Test @Launch({ "start-dev", "--features=multi-site" }) - void testLoadBalancerCheck() { + void testLoadBalancerCheck(KeycloakDistribution distribution) { + distribution.setRequestPort(8080); + when().get("/lb-check").then() .statusCode(200); } diff --git a/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/ManagementDistTest.java b/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/ManagementDistTest.java new file mode 100644 index 0000000000..5a46ebd50b --- /dev/null +++ b/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/ManagementDistTest.java @@ -0,0 +1,167 @@ +/* + * Copyright 2024 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.it.cli.dist; + +import io.quarkus.test.junit.main.Launch; +import io.quarkus.test.junit.main.LaunchResult; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.keycloak.it.junit5.extension.CLIResult; +import org.keycloak.it.junit5.extension.DistributionTest; +import org.keycloak.it.junit5.extension.DistributionType; +import org.keycloak.it.utils.KeycloakDistribution; + +import java.net.SocketException; + +import static io.restassured.RestAssured.when; +import static org.hamcrest.CoreMatchers.is; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@DistributionTest(keepAlive = true, + defaultOptions = {"--health-enabled=true", "--metrics-enabled=true"}, + requestPort = 9000, + containerExposedPorts = {9000, 8080, 9005}) +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class ManagementDistTest { + + @Test + @Order(1) + @Launch({"start", "--hostname=hostname", "--http-enabled=false"}) + void testManagementNoHttps(LaunchResult result) { + CLIResult cliResult = (CLIResult) result; + cliResult.assertNoMessage("Management interface listening on"); + cliResult.assertError("Key material not provided to setup HTTPS. Please configure your keys/certificates or start the server in development mode."); + } + + @Test + @Order(2) + @Launch({"start-dev", "--legacy-observability-interface=true"}) + void testManagementDisabled(LaunchResult result, KeycloakDistribution distribution) { + CLIResult cliResult = (CLIResult) result; + cliResult.assertNoMessage("Management interface listening on"); + + assertThrows(SocketException.class, () -> when().get("/"), "Connection refused must be thrown"); + assertThrows(SocketException.class, () -> when().get("/health"), "Connection refused must be thrown"); + + distribution.setRequestPort(8080); + + when().get("/health").then() + .statusCode(200); + } + + @Test + @Order(3) + @Launch({"start-dev"}) + void testManagementEnabled(LaunchResult result) { + CLIResult cliResult = (CLIResult) result; + cliResult.assertMessage("Management interface listening on http://0.0.0.0:9000"); + + when().get("/").then() + .statusCode(200) + .and() + .body(is("Keycloak Management Interface")); + when().get("/health").then() + .statusCode(200); + when().get("/health/live").then() + .statusCode(200); + when().get("/health/ready").then() + .statusCode(200); + when().get("/metrics").then() + .statusCode(200); + } + + @Test + @Launch({"start-dev", "--http-management-port=9005"}) + void testManagementDifferentPort(LaunchResult result, KeycloakDistribution distribution) { + CLIResult cliResult = (CLIResult) result; + cliResult.assertMessage("Management interface listening on http://0.0.0.0:9005"); + + distribution.setRequestPort(9005); + + when().get("/").then() + .statusCode(200) + .and() + .body(is("Keycloak Management Interface")); + when().get("/health").then() + .statusCode(200); + when().get("/health/live").then() + .statusCode(200); + when().get("/health/ready").then() + .statusCode(200); + when().get("/metrics").then() + .statusCode(200); + } + + @Test + @Launch({"start-dev", "--http-relative-path=/management2"}) + void testManagementInheritedRelativePath(LaunchResult result) { + assertRelativePath(result, "/management2"); + } + + @Test + @Launch({"start-dev", "--http-management-relative-path=/management"}) + void testManagementDifferentRelativePath(LaunchResult result) { + assertRelativePath(result, "/management"); + } + + private void assertRelativePath(LaunchResult result, String relativePath) { + CLIResult cliResult = (CLIResult) result; + cliResult.assertMessage("Management interface listening on http://0.0.0.0:9000"); + + when().get(relativePath).then() + .statusCode(200) + .and() + .body(is("Keycloak Management Interface")); + when().get(relativePath + "/health").then() + .statusCode(200); + when().get("/health").then() + .statusCode(404); + when().get(relativePath + "/health/live").then() + .statusCode(200); + when().get(relativePath + "/health/ready").then() + .statusCode(200); + when().get(relativePath + "/metrics").then() + .statusCode(200); + when().get("/metrics").then() + .statusCode(404); + } + + @Test + @Launch({"start-dev", "--http-management-host=localhost"}) + void testManagementDifferentHost(LaunchResult result) { + CLIResult cliResult = (CLIResult) result; + cliResult.assertMessage("Management interface listening on http://localhost:9000"); + + // If running in container, we cannot access the localhost due to network host settings + if (DistributionType.isContainerDist()) return; + + when().get("/").then() + .statusCode(200) + .and() + .body(is("Keycloak Management Interface")); + when().get("/health").then() + .statusCode(200); + when().get("/health/live").then() + .statusCode(200); + when().get("/health/ready").then() + .statusCode(200); + when().get("/metrics").then() + .statusCode(200); + } +} diff --git a/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/ManagementHttpsDistTest.java b/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/ManagementHttpsDistTest.java new file mode 100644 index 0000000000..1924052acb --- /dev/null +++ b/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/ManagementHttpsDistTest.java @@ -0,0 +1,65 @@ +/* + * Copyright 2024 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.it.cli.dist; + +import io.quarkus.test.junit.main.Launch; +import io.quarkus.test.junit.main.LaunchResult; +import io.restassured.RestAssured; +import io.restassured.config.RedirectConfig; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.keycloak.it.junit5.extension.CLIResult; +import org.keycloak.it.junit5.extension.DistributionTest; +import org.keycloak.it.junit5.extension.RawDistOnly; + +import static io.restassured.RestAssured.when; +import static org.hamcrest.CoreMatchers.is; + +@DistributionTest(keepAlive = true, + enableTls = true, + defaultOptions = {"--health-enabled=true", "--metrics-enabled=true"}, + requestPort = 9000) +@RawDistOnly(reason = "We do not test TLS in containers") +public class ManagementHttpsDistTest { + + @BeforeEach + public void setRestAssuredHttps() { + RestAssured.useRelaxedHTTPSValidation(); + RestAssured.config = RestAssured.config.redirect(RedirectConfig.redirectConfig().followRedirects(false)); + } + + @Test + @Launch({"start-dev"}) + public void simpleHttpsStartDev(LaunchResult result) { + CLIResult cliResult = (CLIResult) result; + var url = "https://localhost:9000"; + cliResult.assertMessage("Management interface listening on https://0.0.0.0:9000"); + + when().get(url).then() + .statusCode(200) + .and() + .body(is("Keycloak Management Interface")); + when().get(url + "/health").then() + .statusCode(200); + when().get(url + "/health/live").then() + .statusCode(200); + when().get(url + "/health/ready").then() + .statusCode(200); + when().get(url + "/metrics").then() + .statusCode(200); + } +} diff --git a/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/MetricsDistTest.java b/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/MetricsDistTest.java index bfc486996b..285e5e9fe9 100644 --- a/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/MetricsDistTest.java +++ b/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/MetricsDistTest.java @@ -30,7 +30,9 @@ import org.keycloak.it.utils.KeycloakDistribution; import io.quarkus.test.junit.main.Launch; -@DistributionTest(keepAlive =true) +@DistributionTest(keepAlive = true, + requestPort = 9000, + containerExposedPorts = {8080, 9000}) public class MetricsDistTest { @Test @@ -67,7 +69,7 @@ public class MetricsDistTest { @Test void testUsingRelativePath(KeycloakDistribution distribution) { for (String relativePath : List.of("/auth", "/auth/", "auth")) { - distribution.run("start-dev", "--metrics-enabled=true", "--http-relative-path=" + relativePath); + distribution.run("start-dev", "--metrics-enabled=true", "--http-management-relative-path=" + relativePath); if (!relativePath.endsWith("/")) { relativePath = relativePath + "/"; } @@ -79,8 +81,8 @@ public class MetricsDistTest { @Test void testMultipleRequests(KeycloakDistribution distribution) throws Exception { for (String relativePath : List.of("/", "/auth/", "auth")) { - distribution.run("start-dev", "--metrics-enabled=true", "--http-relative-path=" + relativePath); - CompletableFuture future = CompletableFuture.completedFuture(null); + distribution.run("start-dev", "--metrics-enabled=true", "--http-management-relative-path=" + relativePath); + CompletableFuture future = CompletableFuture.completedFuture(null); for (int i = 0; i < 3; i++) { future = CompletableFuture.allOf(CompletableFuture.runAsync(new Runnable() { diff --git a/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/QuarkusPropertiesDistTest.java b/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/QuarkusPropertiesDistTest.java index 9d2f4b452f..9a19f4ddaa 100644 --- a/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/QuarkusPropertiesDistTest.java +++ b/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/QuarkusPropertiesDistTest.java @@ -26,6 +26,7 @@ import static org.keycloak.quarkus.runtime.cli.command.AbstractStartCommand.OPTI import java.util.Optional; import java.util.function.Consumer; +import io.restassured.RestAssured; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.MethodOrderer.OrderAnnotation; import org.junit.jupiter.api.Order; @@ -121,6 +122,7 @@ public class QuarkusPropertiesDistTest { void testUnknownQuarkusBuildTimePropertyApplied(LaunchResult result) { CLIResult cliResult = (CLIResult) result; cliResult.assertNoBuild(); + RestAssured.port = 9000; when().get("/metrics").then().statusCode(200) .body(containsString("jvm_gc_")); } diff --git a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testBuildHelp.approved.txt b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testBuildHelp.approved.txt index 05fbb8e5d1..ce5514a6c3 100644 --- a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testBuildHelp.approved.txt +++ b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testBuildHelp.approved.txt @@ -66,6 +66,23 @@ Health: are available at the '/health', '/health/ready' and '/health/live' endpoints. Default: false. +Management: + +--http-management-relative-path + Set the path relative to '/' for serving resources from management interface. + The path must start with a '/'. If not given, the value is inherited from + HTTP options. Default: /. Available only when Management interface is + enabled. +--https-management-client-auth + Configures the management interface to require/request client authentication. + If not given, the value is inherited from HTTP options. Possible values are: + none, request, required. Default: none. Available only when Management + interface is enabled. +--legacy-observability-interface + DEPRECATED. If metrics/health endpoints should be exposed on the main HTTP + server (not recommended). If set to true, the management interface is + disabled. Default: false. + Metrics: --metrics-enabled diff --git a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testExportHelp.approved.txt b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testExportHelp.approved.txt index 901fb8c6ed..daf3b95b8b 100644 --- a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testExportHelp.approved.txt +++ b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testExportHelp.approved.txt @@ -14,6 +14,15 @@ Options: built a server image using the 'build' command. -v, --verbose Print out error details when running this command. +Config: + +--config-keystore + Specifies a path to the KeyStore Configuration Source. +--config-keystore-password + Specifies a password to the KeyStore Configuration Source. +--config-keystore-type + Specifies a type of the KeyStore Configuration Source. Default: PKCS12. + Database: --db The database vendor. Possible values are: dev-file, dev-mem, mariadb, mssql, @@ -59,14 +68,42 @@ Feature: --features-disabled Disables a set of one or more features. Possible values are: <...>. -Config: +Management: ---config-keystore - Specifies a path to the KeyStore Configuration Source. ---config-keystore-password - Specifies a password to the KeyStore Configuration Source. ---config-keystore-type - Specifies a type of the KeyStore Configuration Source. Default: PKCS12. +--http-management-port + Port of the management interface. Default: 9000. Available only when + Management interface is enabled. +--http-management-relative-path + Set the path relative to '/' for serving resources from management interface. + The path must start with a '/'. If not given, the value is inherited from + HTTP options. Default: /. Available only when Management interface is + enabled. +--https-management-certificate-file + The file path to a server certificate or certificate chain in PEM format for + the management server. If not given, the value is inherited from HTTP + options. Available only when Management interface is enabled. +--https-management-certificate-key-file + The file path to a private key in PEM format for the management server. If not + given, the value is inherited from HTTP options. Available only when + Management interface is enabled. +--https-management-client-auth + Configures the management interface to require/request client authentication. + If not given, the value is inherited from HTTP options. Possible values are: + none, request, required. Default: none. Available only when Management + interface is enabled. +--https-management-key-store-file + The key store which holds the certificate information instead of specifying + separate files for the management server. If not given, the value is + inherited from HTTP options. Available only when Management interface is + enabled. +--https-management-key-store-password + The password of the key store file for the management server. If not given, + the value is inherited from HTTP options. Default: password. Available only + when Management interface is enabled. +--legacy-observability-interface + DEPRECATED. If metrics/health endpoints should be exposed on the main HTTP + server (not recommended). If set to true, the management interface is + disabled. Default: false. Logging: diff --git a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testExportHelpAll.approved.txt b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testExportHelpAll.approved.txt index 4ad2fab2cb..67d7183575 100644 --- a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testExportHelpAll.approved.txt +++ b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testExportHelpAll.approved.txt @@ -14,6 +14,15 @@ Options: built a server image using the 'build' command. -v, --verbose Print out error details when running this command. +Config: + +--config-keystore + Specifies a path to the KeyStore Configuration Source. +--config-keystore-password + Specifies a password to the KeyStore Configuration Source. +--config-keystore-type + Specifies a type of the KeyStore Configuration Source. Default: PKCS12. + Database: --db The database vendor. Possible values are: dev-file, dev-mem, mariadb, mssql, @@ -59,14 +68,42 @@ Feature: --features-disabled Disables a set of one or more features. Possible values are: <...>. -Config: +Management: ---config-keystore - Specifies a path to the KeyStore Configuration Source. ---config-keystore-password - Specifies a password to the KeyStore Configuration Source. ---config-keystore-type - Specifies a type of the KeyStore Configuration Source. Default: PKCS12. +--http-management-port + Port of the management interface. Default: 9000. Available only when + Management interface is enabled. +--http-management-relative-path + Set the path relative to '/' for serving resources from management interface. + The path must start with a '/'. If not given, the value is inherited from + HTTP options. Default: /. Available only when Management interface is + enabled. +--https-management-certificate-file + The file path to a server certificate or certificate chain in PEM format for + the management server. If not given, the value is inherited from HTTP + options. Available only when Management interface is enabled. +--https-management-certificate-key-file + The file path to a private key in PEM format for the management server. If not + given, the value is inherited from HTTP options. Available only when + Management interface is enabled. +--https-management-client-auth + Configures the management interface to require/request client authentication. + If not given, the value is inherited from HTTP options. Possible values are: + none, request, required. Default: none. Available only when Management + interface is enabled. +--https-management-key-store-file + The key store which holds the certificate information instead of specifying + separate files for the management server. If not given, the value is + inherited from HTTP options. Available only when Management interface is + enabled. +--https-management-key-store-password + The password of the key store file for the management server. If not given, + the value is inherited from HTTP options. Default: password. Available only + when Management interface is enabled. +--legacy-observability-interface + DEPRECATED. If metrics/health endpoints should be exposed on the main HTTP + server (not recommended). If set to true, the management interface is + disabled. Default: false. Logging: diff --git a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testImportHelp.approved.txt b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testImportHelp.approved.txt index 9dd9abbdd9..cd5563b01c 100644 --- a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testImportHelp.approved.txt +++ b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testImportHelp.approved.txt @@ -14,6 +14,15 @@ Options: built a server image using the 'build' command. -v, --verbose Print out error details when running this command. +Config: + +--config-keystore + Specifies a path to the KeyStore Configuration Source. +--config-keystore-password + Specifies a password to the KeyStore Configuration Source. +--config-keystore-type + Specifies a type of the KeyStore Configuration Source. Default: PKCS12. + Database: --db The database vendor. Possible values are: dev-file, dev-mem, mariadb, mssql, @@ -59,14 +68,42 @@ Feature: --features-disabled Disables a set of one or more features. Possible values are: <...>. -Config: +Management: ---config-keystore - Specifies a path to the KeyStore Configuration Source. ---config-keystore-password - Specifies a password to the KeyStore Configuration Source. ---config-keystore-type - Specifies a type of the KeyStore Configuration Source. Default: PKCS12. +--http-management-port + Port of the management interface. Default: 9000. Available only when + Management interface is enabled. +--http-management-relative-path + Set the path relative to '/' for serving resources from management interface. + The path must start with a '/'. If not given, the value is inherited from + HTTP options. Default: /. Available only when Management interface is + enabled. +--https-management-certificate-file + The file path to a server certificate or certificate chain in PEM format for + the management server. If not given, the value is inherited from HTTP + options. Available only when Management interface is enabled. +--https-management-certificate-key-file + The file path to a private key in PEM format for the management server. If not + given, the value is inherited from HTTP options. Available only when + Management interface is enabled. +--https-management-client-auth + Configures the management interface to require/request client authentication. + If not given, the value is inherited from HTTP options. Possible values are: + none, request, required. Default: none. Available only when Management + interface is enabled. +--https-management-key-store-file + The key store which holds the certificate information instead of specifying + separate files for the management server. If not given, the value is + inherited from HTTP options. Available only when Management interface is + enabled. +--https-management-key-store-password + The password of the key store file for the management server. If not given, + the value is inherited from HTTP options. Default: password. Available only + when Management interface is enabled. +--legacy-observability-interface + DEPRECATED. If metrics/health endpoints should be exposed on the main HTTP + server (not recommended). If set to true, the management interface is + disabled. Default: false. Logging: diff --git a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testImportHelpAll.approved.txt b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testImportHelpAll.approved.txt index c7d7b7684a..83007d6d95 100644 --- a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testImportHelpAll.approved.txt +++ b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testImportHelpAll.approved.txt @@ -14,6 +14,15 @@ Options: built a server image using the 'build' command. -v, --verbose Print out error details when running this command. +Config: + +--config-keystore + Specifies a path to the KeyStore Configuration Source. +--config-keystore-password + Specifies a password to the KeyStore Configuration Source. +--config-keystore-type + Specifies a type of the KeyStore Configuration Source. Default: PKCS12. + Database: --db The database vendor. Possible values are: dev-file, dev-mem, mariadb, mssql, @@ -59,14 +68,42 @@ Feature: --features-disabled Disables a set of one or more features. Possible values are: <...>. -Config: +Management: ---config-keystore - Specifies a path to the KeyStore Configuration Source. ---config-keystore-password - Specifies a password to the KeyStore Configuration Source. ---config-keystore-type - Specifies a type of the KeyStore Configuration Source. Default: PKCS12. +--http-management-port + Port of the management interface. Default: 9000. Available only when + Management interface is enabled. +--http-management-relative-path + Set the path relative to '/' for serving resources from management interface. + The path must start with a '/'. If not given, the value is inherited from + HTTP options. Default: /. Available only when Management interface is + enabled. +--https-management-certificate-file + The file path to a server certificate or certificate chain in PEM format for + the management server. If not given, the value is inherited from HTTP + options. Available only when Management interface is enabled. +--https-management-certificate-key-file + The file path to a private key in PEM format for the management server. If not + given, the value is inherited from HTTP options. Available only when + Management interface is enabled. +--https-management-client-auth + Configures the management interface to require/request client authentication. + If not given, the value is inherited from HTTP options. Possible values are: + none, request, required. Default: none. Available only when Management + interface is enabled. +--https-management-key-store-file + The key store which holds the certificate information instead of specifying + separate files for the management server. If not given, the value is + inherited from HTTP options. Available only when Management interface is + enabled. +--https-management-key-store-password + The password of the key store file for the management server. If not given, + the value is inherited from HTTP options. Default: password. Available only + when Management interface is enabled. +--legacy-observability-interface + DEPRECATED. If metrics/health endpoints should be exposed on the main HTTP + server (not recommended). If set to true, the management interface is + disabled. Default: false. Logging: diff --git a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartDevHelp.approved.txt b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartDevHelp.approved.txt index 2e149375a3..d19725cba5 100644 --- a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartDevHelp.approved.txt +++ b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartDevHelp.approved.txt @@ -66,6 +66,15 @@ Cache: This option only takes effect if 'cache' is set to 'ispn'. Default: udp. Possible values are: tcp, udp, kubernetes, ec2, azure, google. +Config: + +--config-keystore + Specifies a path to the KeyStore Configuration Source. +--config-keystore-password + Specifies a password to the KeyStore Configuration Source. +--config-keystore-type + Specifies a type of the KeyStore Configuration Source. Default: PKCS12. + Database: --db The database vendor. Possible values are: dev-file, dev-mem, mariadb, mssql, @@ -198,14 +207,42 @@ Health: are available at the '/health', '/health/ready' and '/health/live' endpoints. Default: false. -Config: +Management: ---config-keystore - Specifies a path to the KeyStore Configuration Source. ---config-keystore-password - Specifies a password to the KeyStore Configuration Source. ---config-keystore-type - Specifies a type of the KeyStore Configuration Source. Default: PKCS12. +--http-management-port + Port of the management interface. Default: 9000. Available only when + Management interface is enabled. +--http-management-relative-path + Set the path relative to '/' for serving resources from management interface. + The path must start with a '/'. If not given, the value is inherited from + HTTP options. Default: /. Available only when Management interface is + enabled. +--https-management-certificate-file + The file path to a server certificate or certificate chain in PEM format for + the management server. If not given, the value is inherited from HTTP + options. Available only when Management interface is enabled. +--https-management-certificate-key-file + The file path to a private key in PEM format for the management server. If not + given, the value is inherited from HTTP options. Available only when + Management interface is enabled. +--https-management-client-auth + Configures the management interface to require/request client authentication. + If not given, the value is inherited from HTTP options. Possible values are: + none, request, required. Default: none. Available only when Management + interface is enabled. +--https-management-key-store-file + The key store which holds the certificate information instead of specifying + separate files for the management server. If not given, the value is + inherited from HTTP options. Available only when Management interface is + enabled. +--https-management-key-store-password + The password of the key store file for the management server. If not given, + the value is inherited from HTTP options. Default: password. Available only + when Management interface is enabled. +--legacy-observability-interface + DEPRECATED. If metrics/health endpoints should be exposed on the main HTTP + server (not recommended). If set to true, the management interface is + disabled. Default: false. Metrics: diff --git a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartDevHelpAll.approved.txt b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartDevHelpAll.approved.txt index d3edb2eaf5..4b40bb321f 100644 --- a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartDevHelpAll.approved.txt +++ b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartDevHelpAll.approved.txt @@ -69,6 +69,15 @@ Cache: This option only takes effect if 'cache' is set to 'ispn'. Default: udp. Possible values are: tcp, udp, kubernetes, ec2, azure, google. +Config: + +--config-keystore + Specifies a path to the KeyStore Configuration Source. +--config-keystore-password + Specifies a password to the KeyStore Configuration Source. +--config-keystore-type + Specifies a type of the KeyStore Configuration Source. Default: PKCS12. + Database: --db The database vendor. Possible values are: dev-file, dev-mem, mariadb, mssql, @@ -201,14 +210,42 @@ Health: are available at the '/health', '/health/ready' and '/health/live' endpoints. Default: false. -Config: +Management: ---config-keystore - Specifies a path to the KeyStore Configuration Source. ---config-keystore-password - Specifies a password to the KeyStore Configuration Source. ---config-keystore-type - Specifies a type of the KeyStore Configuration Source. Default: PKCS12. +--http-management-port + Port of the management interface. Default: 9000. Available only when + Management interface is enabled. +--http-management-relative-path + Set the path relative to '/' for serving resources from management interface. + The path must start with a '/'. If not given, the value is inherited from + HTTP options. Default: /. Available only when Management interface is + enabled. +--https-management-certificate-file + The file path to a server certificate or certificate chain in PEM format for + the management server. If not given, the value is inherited from HTTP + options. Available only when Management interface is enabled. +--https-management-certificate-key-file + The file path to a private key in PEM format for the management server. If not + given, the value is inherited from HTTP options. Available only when + Management interface is enabled. +--https-management-client-auth + Configures the management interface to require/request client authentication. + If not given, the value is inherited from HTTP options. Possible values are: + none, request, required. Default: none. Available only when Management + interface is enabled. +--https-management-key-store-file + The key store which holds the certificate information instead of specifying + separate files for the management server. If not given, the value is + inherited from HTTP options. Available only when Management interface is + enabled. +--https-management-key-store-password + The password of the key store file for the management server. If not given, + the value is inherited from HTTP options. Default: password. Available only + when Management interface is enabled. +--legacy-observability-interface + DEPRECATED. If metrics/health endpoints should be exposed on the main HTTP + server (not recommended). If set to true, the management interface is + disabled. Default: false. Metrics: diff --git a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartHelp.approved.txt b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartHelp.approved.txt index b9102fe53c..9307700f6c 100644 --- a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartHelp.approved.txt +++ b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartHelp.approved.txt @@ -67,6 +67,15 @@ Cache: This option only takes effect if 'cache' is set to 'ispn'. Default: udp. Possible values are: tcp, udp, kubernetes, ec2, azure, google. +Config: + +--config-keystore + Specifies a path to the KeyStore Configuration Source. +--config-keystore-password + Specifies a password to the KeyStore Configuration Source. +--config-keystore-type + Specifies a type of the KeyStore Configuration Source. Default: PKCS12. + Database: --db The database vendor. Possible values are: dev-file, dev-mem, mariadb, mssql, @@ -199,14 +208,42 @@ Health: are available at the '/health', '/health/ready' and '/health/live' endpoints. Default: false. -Config: +Management: ---config-keystore - Specifies a path to the KeyStore Configuration Source. ---config-keystore-password - Specifies a password to the KeyStore Configuration Source. ---config-keystore-type - Specifies a type of the KeyStore Configuration Source. Default: PKCS12. +--http-management-port + Port of the management interface. Default: 9000. Available only when + Management interface is enabled. +--http-management-relative-path + Set the path relative to '/' for serving resources from management interface. + The path must start with a '/'. If not given, the value is inherited from + HTTP options. Default: /. Available only when Management interface is + enabled. +--https-management-certificate-file + The file path to a server certificate or certificate chain in PEM format for + the management server. If not given, the value is inherited from HTTP + options. Available only when Management interface is enabled. +--https-management-certificate-key-file + The file path to a private key in PEM format for the management server. If not + given, the value is inherited from HTTP options. Available only when + Management interface is enabled. +--https-management-client-auth + Configures the management interface to require/request client authentication. + If not given, the value is inherited from HTTP options. Possible values are: + none, request, required. Default: none. Available only when Management + interface is enabled. +--https-management-key-store-file + The key store which holds the certificate information instead of specifying + separate files for the management server. If not given, the value is + inherited from HTTP options. Available only when Management interface is + enabled. +--https-management-key-store-password + The password of the key store file for the management server. If not given, + the value is inherited from HTTP options. Default: password. Available only + when Management interface is enabled. +--legacy-observability-interface + DEPRECATED. If metrics/health endpoints should be exposed on the main HTTP + server (not recommended). If set to true, the management interface is + disabled. Default: false. Metrics: diff --git a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartHelpAll.approved.txt b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartHelpAll.approved.txt index 1d66274f6a..567ab031fb 100644 --- a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartHelpAll.approved.txt +++ b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartHelpAll.approved.txt @@ -70,6 +70,15 @@ Cache: This option only takes effect if 'cache' is set to 'ispn'. Default: udp. Possible values are: tcp, udp, kubernetes, ec2, azure, google. +Config: + +--config-keystore + Specifies a path to the KeyStore Configuration Source. +--config-keystore-password + Specifies a password to the KeyStore Configuration Source. +--config-keystore-type + Specifies a type of the KeyStore Configuration Source. Default: PKCS12. + Database: --db The database vendor. Possible values are: dev-file, dev-mem, mariadb, mssql, @@ -202,14 +211,42 @@ Health: are available at the '/health', '/health/ready' and '/health/live' endpoints. Default: false. -Config: +Management: ---config-keystore - Specifies a path to the KeyStore Configuration Source. ---config-keystore-password - Specifies a password to the KeyStore Configuration Source. ---config-keystore-type - Specifies a type of the KeyStore Configuration Source. Default: PKCS12. +--http-management-port + Port of the management interface. Default: 9000. Available only when + Management interface is enabled. +--http-management-relative-path + Set the path relative to '/' for serving resources from management interface. + The path must start with a '/'. If not given, the value is inherited from + HTTP options. Default: /. Available only when Management interface is + enabled. +--https-management-certificate-file + The file path to a server certificate or certificate chain in PEM format for + the management server. If not given, the value is inherited from HTTP + options. Available only when Management interface is enabled. +--https-management-certificate-key-file + The file path to a private key in PEM format for the management server. If not + given, the value is inherited from HTTP options. Available only when + Management interface is enabled. +--https-management-client-auth + Configures the management interface to require/request client authentication. + If not given, the value is inherited from HTTP options. Possible values are: + none, request, required. Default: none. Available only when Management + interface is enabled. +--https-management-key-store-file + The key store which holds the certificate information instead of specifying + separate files for the management server. If not given, the value is + inherited from HTTP options. Available only when Management interface is + enabled. +--https-management-key-store-password + The password of the key store file for the management server. If not given, + the value is inherited from HTTP options. Default: password. Available only + when Management interface is enabled. +--legacy-observability-interface + DEPRECATED. If metrics/health endpoints should be exposed on the main HTTP + server (not recommended). If set to true, the management interface is + disabled. Default: false. Metrics: diff --git a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartOptimizedHelp.approved.txt b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartOptimizedHelp.approved.txt index 3177ae5610..f3881922c2 100644 --- a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartOptimizedHelp.approved.txt +++ b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartOptimizedHelp.approved.txt @@ -55,6 +55,15 @@ Cache: specified, 'cache-remote-host' and 'cache-remote-password' are required as well and the related configuration in XML file should not be present. +Config: + +--config-keystore + Specifies a path to the KeyStore Configuration Source. +--config-keystore-password + Specifies a password to the KeyStore Configuration Source. +--config-keystore-type + Specifies a type of the KeyStore Configuration Source. Default: PKCS12. + Database: --db-password @@ -159,14 +168,28 @@ HTTP(S): 'strict' and no value is set, it defaults to 'BCFKS'. Use the System Truststore instead, see the docs for details. -Config: +Management: ---config-keystore - Specifies a path to the KeyStore Configuration Source. ---config-keystore-password - Specifies a password to the KeyStore Configuration Source. ---config-keystore-type - Specifies a type of the KeyStore Configuration Source. Default: PKCS12. +--http-management-port + Port of the management interface. Default: 9000. Available only when + Management interface is enabled. +--https-management-certificate-file + The file path to a server certificate or certificate chain in PEM format for + the management server. If not given, the value is inherited from HTTP + options. Available only when Management interface is enabled. +--https-management-certificate-key-file + The file path to a private key in PEM format for the management server. If not + given, the value is inherited from HTTP options. Available only when + Management interface is enabled. +--https-management-key-store-file + The key store which holds the certificate information instead of specifying + separate files for the management server. If not given, the value is + inherited from HTTP options. Available only when Management interface is + enabled. +--https-management-key-store-password + The password of the key store file for the management server. If not given, + the value is inherited from HTTP options. Default: password. Available only + when Management interface is enabled. Proxy: diff --git a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartOptimizedHelpAll.approved.txt b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartOptimizedHelpAll.approved.txt index c6fa9959ac..dada1624ae 100644 --- a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartOptimizedHelpAll.approved.txt +++ b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartOptimizedHelpAll.approved.txt @@ -58,6 +58,15 @@ Cache: specified, 'cache-remote-host' and 'cache-remote-password' are required as well and the related configuration in XML file should not be present. +Config: + +--config-keystore + Specifies a path to the KeyStore Configuration Source. +--config-keystore-password + Specifies a password to the KeyStore Configuration Source. +--config-keystore-type + Specifies a type of the KeyStore Configuration Source. Default: PKCS12. + Database: --db-password @@ -162,14 +171,28 @@ HTTP(S): 'strict' and no value is set, it defaults to 'BCFKS'. Use the System Truststore instead, see the docs for details. -Config: +Management: ---config-keystore - Specifies a path to the KeyStore Configuration Source. ---config-keystore-password - Specifies a password to the KeyStore Configuration Source. ---config-keystore-type - Specifies a type of the KeyStore Configuration Source. Default: PKCS12. +--http-management-port + Port of the management interface. Default: 9000. Available only when + Management interface is enabled. +--https-management-certificate-file + The file path to a server certificate or certificate chain in PEM format for + the management server. If not given, the value is inherited from HTTP + options. Available only when Management interface is enabled. +--https-management-certificate-key-file + The file path to a private key in PEM format for the management server. If not + given, the value is inherited from HTTP options. Available only when + Management interface is enabled. +--https-management-key-store-file + The key store which holds the certificate information instead of specifying + separate files for the management server. If not given, the value is + inherited from HTTP options. Available only when Management interface is + enabled. +--https-management-key-store-password + The password of the key store file for the management server. If not given, + the value is inherited from HTTP options. Default: password. Available only + when Management interface is enabled. Proxy: diff --git a/quarkus/tests/junit5/pom.xml b/quarkus/tests/junit5/pom.xml index 94b7d0e775..ef3d7f360b 100644 --- a/quarkus/tests/junit5/pom.xml +++ b/quarkus/tests/junit5/pom.xml @@ -60,6 +60,10 @@ ${junit.version} compile + + io.rest-assured + rest-assured + io.quarkus quarkus-junit5-internal diff --git a/quarkus/tests/junit5/src/main/java/org/keycloak/it/junit5/extension/DistributionTest.java b/quarkus/tests/junit5/src/main/java/org/keycloak/it/junit5/extension/DistributionTest.java index 2c3cf6b400..dfd2623da5 100644 --- a/quarkus/tests/junit5/src/main/java/org/keycloak/it/junit5/extension/DistributionTest.java +++ b/quarkus/tests/junit5/src/main/java/org/keycloak/it/junit5/extension/DistributionTest.java @@ -64,5 +64,15 @@ public @interface DistributionTest { * If any option must be set when starting the server. */ String[] defaultOptions() default {}; + + /** + * Exposed ports when container is used + */ + int[] containerExposedPorts() default {8080}; + + /** + * Default port for making HTTP requests with RestAssured + */ + int requestPort() default 8080; } diff --git a/quarkus/tests/junit5/src/main/java/org/keycloak/it/junit5/extension/DistributionType.java b/quarkus/tests/junit5/src/main/java/org/keycloak/it/junit5/extension/DistributionType.java index 97ea149ca9..bfe85dd9ef 100644 --- a/quarkus/tests/junit5/src/main/java/org/keycloak/it/junit5/extension/DistributionType.java +++ b/quarkus/tests/junit5/src/main/java/org/keycloak/it/junit5/extension/DistributionType.java @@ -33,7 +33,8 @@ public enum DistributionType { return new DockerKeycloakDistribution( config.debug(), config.keepAlive(), - !DistributionTest.ReInstall.NEVER.equals(config.reInstall())); + config.requestPort(), + config.containerExposedPorts()); } private static KeycloakDistribution createRawDistribution(DistributionTest config) { @@ -42,7 +43,8 @@ public enum DistributionType { config.keepAlive(), config.enableTls(), !DistributionTest.ReInstall.NEVER.equals(config.reInstall()), - config.removeBuildOptionsAfterBuild()); + config.removeBuildOptionsAfterBuild(), + config.requestPort()); } private final Function factory; @@ -65,6 +67,14 @@ public enum DistributionType { } } + public static boolean isContainerDist() { + return DistributionType.getCurrent().map(f -> f.equals(DistributionType.DOCKER)).orElse(false); + } + + public static boolean isRawDist() { + return DistributionType.getCurrent().map(f -> f.equals(DistributionType.RAW)).orElse(false); + } + public KeycloakDistribution newInstance(DistributionTest config) { return factory.apply(config); } diff --git a/quarkus/tests/junit5/src/main/java/org/keycloak/it/junit5/extension/KeycloakDistributionDecorator.java b/quarkus/tests/junit5/src/main/java/org/keycloak/it/junit5/extension/KeycloakDistributionDecorator.java index 715b1a0025..85704f9b9b 100644 --- a/quarkus/tests/junit5/src/main/java/org/keycloak/it/junit5/extension/KeycloakDistributionDecorator.java +++ b/quarkus/tests/junit5/src/main/java/org/keycloak/it/junit5/extension/KeycloakDistributionDecorator.java @@ -126,6 +126,16 @@ public class KeycloakDistributionDecorator implements KeycloakDistribution { delegate.assertStopped(); } + @Override + public void setRequestPort() { + delegate.setRequestPort(); + } + + @Override + public void setRequestPort(int port) { + delegate.setRequestPort(port); + } + @Override public D unwrap(Class type) { if (!KeycloakDistribution.class.isAssignableFrom(type)) { diff --git a/quarkus/tests/junit5/src/main/java/org/keycloak/it/utils/DockerKeycloakDistribution.java b/quarkus/tests/junit5/src/main/java/org/keycloak/it/utils/DockerKeycloakDistribution.java index 782ad990af..c4c62e7b96 100644 --- a/quarkus/tests/junit5/src/main/java/org/keycloak/it/utils/DockerKeycloakDistribution.java +++ b/quarkus/tests/junit5/src/main/java/org/keycloak/it/utils/DockerKeycloakDistribution.java @@ -1,6 +1,8 @@ package org.keycloak.it.utils; import com.github.dockerjava.api.DockerClient; +import com.github.dockerjava.api.exception.NotFoundException; +import io.restassured.RestAssured; import org.jboss.logging.Logger; import org.keycloak.common.Version; import org.keycloak.it.junit5.extension.CLIResult; @@ -15,36 +17,41 @@ import org.testcontainers.utility.DockerImageName; import org.testcontainers.utility.LazyFuture; import java.io.File; -import java.lang.reflect.Field; import java.time.Duration; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.Executor; import java.util.concurrent.Executors; +import java.util.stream.IntStream; public final class DockerKeycloakDistribution implements KeycloakDistribution { private static final Logger LOGGER = Logger.getLogger(DockerKeycloakDistribution.class); - private boolean debug; - private boolean manualStop; + private final boolean debug; + private final boolean manualStop; + private final int requestPort; + private final Integer[] exposedPorts; + private int exitCode = -1; private String stdout = ""; private String stderr = ""; private ToStringConsumer backupConsumer = new ToStringConsumer(); - private File dockerScriptFile = new File("../../container/ubi-null.sh"); + private final File dockerScriptFile = new File("../../container/ubi-null.sh"); private GenericContainer keycloakContainer = null; private String containerId = null; - private Executor parallelReaperExecutor = Executors.newSingleThreadExecutor(); - private Map envVars = new HashMap<>(); + private final Executor parallelReaperExecutor = Executors.newSingleThreadExecutor(); + private final Map envVars = new HashMap<>(); - public DockerKeycloakDistribution(boolean debug, boolean manualStop, boolean reCreate) { + public DockerKeycloakDistribution(boolean debug, boolean manualStop, int requestPort, int[] exposedPorts) { this.debug = debug; this.manualStop = manualStop; + this.requestPort = requestPort; + this.exposedPorts = IntStream.of(exposedPorts).boxed().toArray(Integer[]::new); } @Override @@ -79,10 +86,10 @@ public final class DockerKeycloakDistribution implements KeycloakDistribution { return new GenericContainer<>(image) .withEnv(envVars) - .withExposedPorts(8080) + .withExposedPorts(exposedPorts) .withStartupAttempts(1) .withStartupTimeout(Duration.ofSeconds(120)) - .waitingFor(Wait.forListeningPort()); + .waitingFor(Wait.forListeningPorts(8080)); } @Override @@ -118,20 +125,20 @@ public final class DockerKeycloakDistribution implements KeycloakDistribution { } } - trySetRestAssuredPort(); + setRequestPort(); return CLIResult.create(getOutputStream(), getErrorStream(), getExitCode()); } - private void trySetRestAssuredPort() { - try { - ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); - Class restAssured = classLoader.loadClass("io.restassured.RestAssured"); - Field port = restAssured.getDeclaredField("port"); - port.set(null, keycloakContainer.getMappedPort(8080)); - } catch (Exception ignore) { - // keeping the workaround to set the container port to restassured - // TODO: better way to expose the port to tests + @Override + public void setRequestPort() { + setRequestPort(requestPort); + } + + @Override + public void setRequestPort(int port) { + if (keycloakContainer != null) { + RestAssured.port = keycloakContainer.getMappedPort(port); } } @@ -186,9 +193,12 @@ public final class DockerKeycloakDistribution implements KeycloakDistribution { @Override public void run() { try { + if (containerId == null) return; DockerClient dockerClient = DockerClientFactory.lazyClient(); dockerClient.killContainerCmd(containerId).exec(); dockerClient.removeContainerCmd(containerId).withRemoveVolumes(true).withForce(true).exec(); + } catch (NotFoundException notFound) { + LOGGER.debug("Container is already cleaned up, no additional cleanup required"); } catch (Exception cause) { throw new RuntimeException("Failed to stop and remove container", cause); } diff --git a/quarkus/tests/junit5/src/main/java/org/keycloak/it/utils/KeycloakDistribution.java b/quarkus/tests/junit5/src/main/java/org/keycloak/it/utils/KeycloakDistribution.java index df079c5d56..bdbd38655c 100644 --- a/quarkus/tests/junit5/src/main/java/org/keycloak/it/utils/KeycloakDistribution.java +++ b/quarkus/tests/junit5/src/main/java/org/keycloak/it/utils/KeycloakDistribution.java @@ -29,6 +29,10 @@ public interface KeycloakDistribution { void assertStopped(); + void setRequestPort(); + + void setRequestPort(int port); + default String[] getCliArgs(List arguments) { throw new RuntimeException("Not implemented"); } diff --git a/quarkus/tests/junit5/src/main/java/org/keycloak/it/utils/RawKeycloakDistribution.java b/quarkus/tests/junit5/src/main/java/org/keycloak/it/utils/RawKeycloakDistribution.java index d8333a0ec4..8a988fa1b3 100644 --- a/quarkus/tests/junit5/src/main/java/org/keycloak/it/utils/RawKeycloakDistribution.java +++ b/quarkus/tests/junit5/src/main/java/org/keycloak/it/utils/RawKeycloakDistribution.java @@ -60,6 +60,7 @@ import javax.net.ssl.X509TrustManager; import io.quarkus.deployment.util.FileUtil; import io.quarkus.fs.util.ZipUtils; +import io.restassured.RestAssured; import org.awaitility.Awaitility; import org.jboss.logging.Logger; import org.jboss.shrinkwrap.api.ShrinkWrap; @@ -91,20 +92,22 @@ public final class RawKeycloakDistribution implements KeycloakDistribution { private String relativePath; private int httpPort; private int httpsPort; - private boolean debug; - private boolean enableTls; - private boolean reCreate; - private boolean removeBuildOptionsAfterBuild; + private final boolean debug; + private final boolean enableTls; + private final boolean reCreate; + private final boolean removeBuildOptionsAfterBuild; + private final int requestPort; private ExecutorService outputExecutor; private boolean inited = false; - private Map envVars = new HashMap<>(); + private final Map envVars = new HashMap<>(); - public RawKeycloakDistribution(boolean debug, boolean manualStop, boolean enableTls, boolean reCreate, boolean removeBuildOptionsAfterBuild) { + public RawKeycloakDistribution(boolean debug, boolean manualStop, boolean enableTls, boolean reCreate, boolean removeBuildOptionsAfterBuild, int requestPort) { this.debug = debug; this.manualStop = manualStop; this.enableTls = enableTls; this.reCreate = reCreate; this.removeBuildOptionsAfterBuild = removeBuildOptionsAfterBuild; + this.requestPort = requestPort; this.distPath = prepareDistribution(); } @@ -145,6 +148,8 @@ public final class RawKeycloakDistribution implements KeycloakDistribution { } } + setRequestPort(); + return CLIResult.create(getOutputStream(), getErrorStream(), getExitCode()); } @@ -276,6 +281,16 @@ public final class RawKeycloakDistribution implements KeycloakDistribution { } } + @Override + public void setRequestPort() { + setRequestPort(requestPort); + } + + @Override + public void setRequestPort(int port) { + RestAssured.port = port; + } + private void waitForReadiness() throws MalformedURLException { waitForReadiness("http", httpPort); diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/containers/AbstractQuarkusDeployableContainer.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/containers/AbstractQuarkusDeployableContainer.java index deb57ee126..b0e586f60d 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/containers/AbstractQuarkusDeployableContainer.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/containers/AbstractQuarkusDeployableContainer.java @@ -164,6 +164,8 @@ public abstract class AbstractQuarkusDeployableContainer implements DeployableCo if (suiteContext.get().isAuthServerMigrationEnabled()) { commands.add("--hostname-strict=false"); commands.add("--hostname-strict-https=false"); + } else { // Do not set management port for older versions of Keycloak for migration tests - available since Keycloak ~22 + commands.add("--http-management-port=" + configuration.getManagementPort()); } if (configuration.getRoute() != null) { diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/containers/KeycloakQuarkusConfiguration.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/containers/KeycloakQuarkusConfiguration.java index b57e896e19..ef56290fb9 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/containers/KeycloakQuarkusConfiguration.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/containers/KeycloakQuarkusConfiguration.java @@ -24,6 +24,7 @@ public class KeycloakQuarkusConfiguration implements ContainerConfiguration { private int bindHttpPort = 8080; private int bindHttpsPortOffset = 0; private int bindHttpsPort = Integer.getInteger("auth.server.https.port", 8543); + private int managementPort = 9000; private String keystoreFile = System.getProperty("auth.server.keystore"); @@ -59,7 +60,7 @@ public class KeycloakQuarkusConfiguration implements ContainerConfiguration { int newHttpsPort = baseHttpsPort + bindHttpsPortOffset; setBindHttpsPort(newHttpsPort); - log.info("Keycloak will listen for http on port: " + newPort + " and for https on port: " + newHttpsPort); + log.infof("Keycloak will listen for http on port: %d, for https on port: %d, and for management on port: %d\n", newPort, newHttpsPort, managementPort); if (this.keycloakConfigPropertyOverrides != null) { try { @@ -103,6 +104,14 @@ public class KeycloakQuarkusConfiguration implements ContainerConfiguration { this.bindHttpPort = bindHttpPort; } + public int getManagementPort() { + return managementPort; + } + + public void setManagementPort(int managementPort) { + this.managementPort = managementPort; + } + public String getKeystoreFile() { return keystoreFile; } diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/arquillian.xml b/testsuite/integration-arquillian/tests/base/src/test/resources/arquillian.xml index eef4d00414..e776bd764c 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/resources/arquillian.xml +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/arquillian.xml @@ -693,6 +693,7 @@ ${auth.server.https.port} 2 2 + 9001 node2 ${quarkus.remote} ha