diff --git a/.github/native-tests.json b/.github/native-tests.json index ef0aa10f132ef..cb8dbea659b1d 100644 --- a/.github/native-tests.json +++ b/.github/native-tests.json @@ -93,7 +93,7 @@ { "category": "HTTP", "timeout": 95, - "test-modules": "elytron-resteasy, resteasy-jackson, elytron-resteasy-reactive, resteasy-mutiny, resteasy-reactive-kotlin/standard, vertx, vertx-http, vertx-web, vertx-web-jackson, vertx-graphql, virtual-http, rest-client, rest-client-reactive, rest-client-reactive-stork, rest-client-reactive-multipart, websockets", + "test-modules": "elytron-resteasy, resteasy-jackson, elytron-resteasy-reactive, resteasy-mutiny, resteasy-reactive-kotlin/standard, vertx, vertx-http, vertx-web, vertx-web-jackson, vertx-graphql, virtual-http, rest-client, rest-client-reactive, rest-client-reactive-stork, rest-client-reactive-multipart, websockets, management-interface", "os-name": "ubuntu-latest" }, { diff --git a/docs/src/main/asciidoc/http-reference.adoc b/docs/src/main/asciidoc/http-reference.adoc index 55d3cf1c658de..c60c868cc9a30 100644 --- a/docs/src/main/asciidoc/http-reference.adoc +++ b/docs/src/main/asciidoc/http-reference.adoc @@ -112,6 +112,16 @@ As an example, if an extension configures a `service` path, that endpoint will b The link:https://quarkus.io/blog/path-resolution-in-quarkus/[Path Resolution in Quarkus] blog post further explains how path resolution works for both user and extension defined paths. +[IMPORTANT] +.Management Interface +==== +`quarkus.http.root-path` is only used for the main HTTP server. +If you enabled the management interface (using the `quarkus.management.enabled=true` property), you can configure the root path of the management interface using: +`quarkus.management.root-path`. + +Refer to the xref:./management-interface-reference.adoc[management interface reference] for more information. +==== + [[ssl]] == Supporting secure connections with SSL @@ -261,7 +271,7 @@ include::{generated-dir}/config/quarkus-vertx-http-config-group-filter-config.ad == Support 100-Continue in vert.x In order to support `100-continue`, the `quarkus.http.handle-100-continue-automatically` option needs to be enabled explicitly -For additional information check https://datatracker.ietf.org/doc/html/rfc7231#section-5.1.1[100-continue= and the related +For additional information check https://datatracker.ietf.org/doc/html/rfc7231#section-5.1.1[100-continue] and the related https://vertx.io/docs/apidocs/io/vertx/core/http/HttpServerOptions.html#DEFAULT_HANDLE_100_CONTINE_AUTOMATICALLY[Vert.x documentation]. [source,properties] diff --git a/docs/src/main/asciidoc/management-interface-reference.adoc b/docs/src/main/asciidoc/management-interface-reference.adoc new file mode 100644 index 0000000000000..7b1ddadd5ace2 --- /dev/null +++ b/docs/src/main/asciidoc/management-interface-reference.adoc @@ -0,0 +1,180 @@ +//// +This guide is maintained in the main Quarkus repository +and pull requests should be submitted there: +https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc +//// += Management interface reference +include::_attributes.adoc[] +:categories: observability +:summary: Management interface configuration +:numbered: +:sectnums: +:sectnumlevels: 4 + +By default, Quarkus exposes the _management_ endpoints under `/q` on the main HTTP server. +The same HTTP server provides the application endpoints and the management endpoints. + +This document presents how you can use a separate HTTP server (bound to a different network interface and port) for the management endpoints. +It avoids exposing these endpoints on the main server and, therefore, prevents undesired accesses. + +== Enabling the management interface + +To enable the management interface, use the following **build-time** property: + +[source, properties] +---- +quarkus.management.enabled=true +---- + +By default, management endpoints will be exposed on: `http://0.0.0.0:9000/q`. +For example, `http://0.0.0.0:9000/q/health/ready` for the readiness probe. + +SmallRye Health Checks, SmallRye Metrics, and Micrometer endpoints will be declared as management endpoints when the management interface is enabled. + +== Configure the host, port and scheme + +By default, the management interface is exposed on the interface: `0.0.0.0` (all interfaces) and on the port `9000` (`9001` in test mode). +It does not use TLS (`https`) by default. + +You can configure the host, ports, and TLS certificates using the following properties: + +* `quarkus.management.host` - the interface / host +* `quarkus.management.port` - the port +* `quarkus.management.test-port` - the port to use in test mode +* `quarkus.management.ssl` - the TLS configuration, xref:http-reference#ssl[same as for the main HTTP server]. + +Here is a configuration example exposing the management interface on _https://localhost:9002_: + +[source, properties] +---- +quarkus.management.enabled=true +quarkus.management.host=localhost +quarkus.management.port=9002 +quarkus.management.ssl.certificate.key-store-file=server-keystore.jks +quarkus.management.ssl.certificate.key-store-password=secret +---- + +IMPORTANT: Unlike the main HTTP server, the management interface does not handle _http_ and _https_ at the same time. +If _https_ is configured, plain HTTP requests will be rejected. + +== Configure the root path + +Management endpoints are configured differently than standard HTTP endpoints. +They use a unique root path, which is `/q` by default. +This management root path can be configured using the `quarkus.management.root-path property`. +For example, if you want to expose the management endpoints under `/management` use: + +[source, properties] +---- +quarkus.management.enabled=true +quarkus.management.root-path=/management +---- + +The mounting rules of the management endpoints slightly differ from the ones used when using the main HTTP server: + +* Management endpoints configured using a _relative_ path (not starting with `/`) will be served from the configured root path. +For example, if the endpoint path is `health` and the root path is `management`, the resulting path is `/management/health` +* Management endpoints configured using an _absolute_ path (starting with `/`) will be served from the root. +For example, if the endpoint path is `/health`, the resulting path is `/health`, regardless of the root path +* The management interface does not use the HTTP root path from the main HTTP server. + +[IMPORTANT] +==== +The `quarkus.http.root-path` property is only applied to the main HTTP server and not to the management interface. +In addition, the `quarkus.http.non-application-root-path` property is not used for endpoint exposed on the management interface. +==== + +== Create a management endpoint + +SmallRye Health Checks, SmallRye Metrics, and Micrometer endpoints will be declared as management endpoints when the management interface is enabled. + +NOTE: if you do not enable the management interface, these endpoints will be served using the main HTTP server (under `/q` by default). + +Extensions can create a management endpoint by defining a _non application_ route and calling `management()` method: + +[source, java] +---- +@BuildStep +void createManagementRoute(BuildProducer routes, + NonApplicationRootPathBuildItem nonApplicationRootPathBuildItem, + MyRecorder recorder) { + + routes.produce(nonApplicationRootPathBuildItem.routeBuilder() + .management() // Must be called BEFORE the routeFunction method + .routeFunction("my-path", recorder.route()) + .handler(recorder.getHandler()) + .blockingRoute() + .build()); + //... +} +---- + +If the management interface is enabled, the endpoint will be exposed on: `http://0.0.0.0:9000/q/my-path`. +Otherwise, it will be exposed on: `http://localhost:8080/q/my-path`. + +IMPORTANT: Management endpoints can only be declared by extensions and not from the application code. + +== Management Interface Configuration + +include::{generated-dir}/config/quarkus-management-management-management-interface-build-time-config.adoc[leveloffset=+1, opts=optional] + +include::{generated-dir}/config/quarkus-management-management-management-interface-configuration.adoc[leveloffset=+1, opts=optional] + + +[[reverse-proxy]] +== Running behind a reverse proxy + + +Quarkus can be accessed through proxies that generate headers (e.g. `X-Forwarded-Host`) to preserve information about the original request. +Quarkus can be configured to automatically update information like protocol, host, port and URI to use the values from those headers. + +IMPORTANT: Activating this feature can expose the server to security issues like information spoofing. +Activate it only when running behind a reverse proxy. + +To set up this feature for the management interface, include the following lines in `src/main/resources/application.properties`: +[source,properties] +---- +quarkus.management.proxy.proxy-address-forwarding=true +---- + +To constrain this behavior to the standard `Forwarded` header (and ignore `X-Forwarded` variants) by setting `quarkus.management.proxy.allow-forwarded` in `src/main/resources/application.properties`: +[source,properties] +---- +quarkus.management.proxy.allow-forwarded=true +---- + +Alternatively, you can prefer `X-Forwarded-*` headers using the following configuration in `src/main/resources/application.properties` (note `allow-x-forwarded` instead of `allow-forwarded`): +[source,properties] +---- +quarkus.management.proxy.proxy-address-forwarding=true +quarkus.management.proxy.allow-x-forwarded=true +quarkus.management.proxy.enable-forwarded-host=true +quarkus.management.proxy.enable-forwarded-prefix=true +---- + +Supported forwarding address headers are: + +* `Forwarded` +* `X-Forwarded-Proto` +* `X-Forwarded-Host` +* `X-Forwarded-Port` +* `X-Forwarded-Ssl` +* `X-Forwarded-Prefix` + +If both header variants (`Forwarded` and `X-Forwarded-*`) are enabled, the `Forwarded` header will have precedence. + +IMPORTANT: Using both `Forwarded` and `X-Forwarded` headers can have security implications as it may allow clients to forge requests with a header that is not overwritten by the proxy. + +Ensure that your proxy is configured to strip unexpected `Forwarded` or `X-Forwarded-*` headers from the client request. + +== Kubernetes + +When Quarkus generates the Kubernetes metadata, it checks if the management interface is enabled and configures the probes accordingly. +The resulting descriptor defines the main HTTP port (named `http`) and the management port (named `management`). +Health probes (using HTTP actions) and Prometheus scrape URLs are configured using the `management` port. + +[IMPORTANT] +.KNative +==== +Until https://github.com/knative/serving/issues/8471[KNative#8471] is resolved, you cannot use the management interface, as KNative does not support containers will multiple exposed ports. +==== \ No newline at end of file diff --git a/docs/src/main/asciidoc/micrometer.adoc b/docs/src/main/asciidoc/micrometer.adoc index c8cbffcb4f40f..b264eb2f62da7 100644 --- a/docs/src/main/asciidoc/micrometer.adoc +++ b/docs/src/main/asciidoc/micrometer.adoc @@ -188,6 +188,15 @@ When should you use a Gauge? Only if you can't use something else. Never gauge s less straight-forward to use than counters. If what you are measuring can be counted (because the value always increments), use a counter instead. +[NOTE] +.Management interface +==== +By default, the metrics are exposed on the main HTTP server. +You can expose them on a separate network interface and port by enabling the management interface with the +`quarkus.management.enabled=true` property. +Refer to the xref:./management-interface-reference.adoc[management interface reference] for more information. +==== + === Counters Counters are used to measure values that only increase. In the example below, you will count the number of times you @@ -598,6 +607,27 @@ implementation("org.eclipse.microprofile.metrics:microprofile-metrics-api") NOTE: The MP Metrics API compatibility layer will be moved to a different extension in the future. +== Management interface + +By default, the metrics are exposed on the main HTTP server. +You can expose them on a separate network interface and port by setting `quarkus.management.enabled=true` in your application configuration. +Note that this property is a build-time property. +The value cannot be overridden at runtime. + +If you enable the management interface without customizing the management network interface and port, the metrics are exposed under: `http://0.0.0.0:9000/q/metrics`. +You can configure the path of each exposed format using: +[source, properties] +---- +quarkus.micrometer.export.json.enabled=true # Enable json metrics +quarkus.micrometer.export.json.path=metrics/json +quarkus.micrometer.export.prometheus.path=metrics/prometheus +---- + +With such a configuration, the json metrics will be available from `http://0.0.0.0:9000/q/metrics/json`. +The prometheus metrics will be available from `http://0.0.0.0:9000/q/metrics/prometheus`. + +Refer to the xref:./management-interface-reference.adoc[management interface reference] for more information. + == Configuration Reference include::{generated-dir}/config/quarkus-micrometer.adoc[opts=optional, leveloffset=+1] diff --git a/docs/src/main/asciidoc/smallrye-health.adoc b/docs/src/main/asciidoc/smallrye-health.adoc index 55bab4d8c1b74..3bffce9dbe0d1 100644 --- a/docs/src/main/asciidoc/smallrye-health.adoc +++ b/docs/src/main/asciidoc/smallrye-health.adoc @@ -100,6 +100,15 @@ The general `status` of the health check is computed as a logical AND of all the declared health check procedures. The `checks` array is empty as we have not specified any health check procedure yet so let's define some. +[NOTE] +.Management interface +==== +By default, the health checks are exposed on the main HTTP server. +You can expose them on a separate network interface and port by enabling the management interface with the +`quarkus.management.enabled=true` property. +Refer to the xref:./management-interface-reference.adoc[management interface reference] for more information. +==== + == Creating your first health check In this section, we create our first simple health check procedure. @@ -410,6 +419,18 @@ The Quarkus `smallrye-health` extension ships with `health-ui` and enables it by image:health-ui-screenshot01.png[alt=Health UI] +== Management interface + +By default, the health checks are exposed on the main HTTP server. +You can expose them on a separate network interface and port by setting `quarkus.management.enabled=true` in your application configuration. +Note that this property is a build-time property. +The value cannot be overridden at runtime. + +If you enable the management interface without customizing the management network interface and port, the health checks are exposed under: `http://0.0.0.0:9000/q/health`. +You can configure the _path_ (the `health` segment in the previous URL) using the `quarkus.smallrye-health.root-path` property. + +Refer to the xref:./management-interface-reference.adoc[management interface reference] for more information. + == Conclusion SmallRye Health provides a way for your application to distribute information diff --git a/docs/src/main/asciidoc/smallrye-metrics.adoc b/docs/src/main/asciidoc/smallrye-metrics.adoc index f1874e40b926c..075204401271d 100644 --- a/docs/src/main/asciidoc/smallrye-metrics.adoc +++ b/docs/src/main/asciidoc/smallrye-metrics.adoc @@ -228,6 +228,15 @@ You will receive a response such as: NOTE: If you prefer an OpenMetrics export rather than the JSON format, remove the `-H"Accept: application/json"` argument from your command line. +[NOTE] +.Management interface +==== +By default, the metrics are exposed on the main HTTP server. +You can expose them on a separate network interface and port by enabling the management interface with the +`quarkus.management.enabled=true` property. +Refer to the xref:./management-interface-reference.adoc[management interface reference] for more information. +==== + .Configuration Reference include::{generated-dir}/config/quarkus-smallrye-metrics.adoc[opts=optional, leveloffset=+1] diff --git a/extensions/kubernetes/kind/deployment/src/main/java/io/quarkus/kind/deployment/KindProcessor.java b/extensions/kubernetes/kind/deployment/src/main/java/io/quarkus/kind/deployment/KindProcessor.java index 2c7ccf92ebcb4..e23fa6be20d85 100644 --- a/extensions/kubernetes/kind/deployment/src/main/java/io/quarkus/kind/deployment/KindProcessor.java +++ b/extensions/kubernetes/kind/deployment/src/main/java/io/quarkus/kind/deployment/KindProcessor.java @@ -131,4 +131,4 @@ public void postBuild(ContainerImageInfoBuildItem image, List createDecorators(ApplicationInfoBuildItem applic startupPath, roles, roleBindings, customProjectRoot); } -} +} \ No newline at end of file diff --git a/extensions/kubernetes/spi/src/main/java/io/quarkus/kubernetes/spi/KubernetesProbePortNameBuildItem.java b/extensions/kubernetes/spi/src/main/java/io/quarkus/kubernetes/spi/KubernetesProbePortNameBuildItem.java index 18b31fd7cced2..cfb1f92a2e556 100644 --- a/extensions/kubernetes/spi/src/main/java/io/quarkus/kubernetes/spi/KubernetesProbePortNameBuildItem.java +++ b/extensions/kubernetes/spi/src/main/java/io/quarkus/kubernetes/spi/KubernetesProbePortNameBuildItem.java @@ -5,7 +5,7 @@ /** * A build item for selecting which port to use for probes using an {@literal HTTP get} action. */ -public class KubernetesProbePortNameBuildItem extends SimpleBuildItem { +public final class KubernetesProbePortNameBuildItem extends SimpleBuildItem { private final String name; diff --git a/extensions/kubernetes/vanilla/deployment/pom.xml b/extensions/kubernetes/vanilla/deployment/pom.xml index 8c1d9f4b0c8d7..cb2668a662062 100644 --- a/extensions/kubernetes/vanilla/deployment/pom.xml +++ b/extensions/kubernetes/vanilla/deployment/pom.xml @@ -25,6 +25,10 @@ io.quarkus quarkus-kubernetes-spi + + io.quarkus + quarkus-vertx-http-deployment-spi + io.quarkus quarkus-kubernetes-client-internal-deployment diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/DevClusterHelper.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/DevClusterHelper.java index e6d17260dc07b..4a7d0ffe8055f 100644 --- a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/DevClusterHelper.java +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/DevClusterHelper.java @@ -159,4 +159,4 @@ private static int getStablePortNumberInRange(String input, int min, int max) { throw new RuntimeException("Unable to generate stable port number from input string: '" + input + "'", e); } } -} +} \ No newline at end of file diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KnativeProcessor.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KnativeProcessor.java index 02e174e2257e3..5524fe655658a 100644 --- a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KnativeProcessor.java +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KnativeProcessor.java @@ -367,4 +367,4 @@ private static List createVolumeDecorators(Optional }); return result; } -} +} \ No newline at end of file diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KubernetesCommonHelper.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KubernetesCommonHelper.java index 2a7b4cd40b7a4..38ec88c14d0cd 100644 --- a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KubernetesCommonHelper.java +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KubernetesCommonHelper.java @@ -809,4 +809,4 @@ private static Map verifyPorts(List ku } return result; } -} +} \ No newline at end of file diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/OpenshiftProcessor.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/OpenshiftProcessor.java index e7cf73b078c4d..009e0f590296b 100644 --- a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/OpenshiftProcessor.java +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/OpenshiftProcessor.java @@ -372,4 +372,4 @@ void externalizeInitTasks( decorators); } } -} +} \ No newline at end of file diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/VanillaKubernetesProcessor.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/VanillaKubernetesProcessor.java index 6a6aa9cf56dda..f96f6de5007bd 100644 --- a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/VanillaKubernetesProcessor.java +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/VanillaKubernetesProcessor.java @@ -286,4 +286,4 @@ void externalizeInitTasks( decorators); } } -} +} \ No newline at end of file diff --git a/extensions/micrometer/deployment/src/main/java/io/quarkus/micrometer/deployment/export/JsonRegistryProcessor.java b/extensions/micrometer/deployment/src/main/java/io/quarkus/micrometer/deployment/export/JsonRegistryProcessor.java index 5721d763dce70..125c2b5505239 100644 --- a/extensions/micrometer/deployment/src/main/java/io/quarkus/micrometer/deployment/export/JsonRegistryProcessor.java +++ b/extensions/micrometer/deployment/src/main/java/io/quarkus/micrometer/deployment/export/JsonRegistryProcessor.java @@ -10,6 +10,7 @@ import io.quarkus.deployment.annotations.BuildSteps; import io.quarkus.deployment.annotations.ExecutionTime; import io.quarkus.deployment.annotations.Record; +import io.quarkus.deployment.builditem.LaunchModeBuildItem; import io.quarkus.micrometer.deployment.MicrometerRegistryProviderBuildItem; import io.quarkus.micrometer.runtime.config.MicrometerConfig; import io.quarkus.micrometer.runtime.export.JsonMeterRegistryProvider; @@ -17,6 +18,7 @@ import io.quarkus.micrometer.runtime.registry.json.JsonMeterRegistry; import io.quarkus.vertx.http.deployment.NonApplicationRootPathBuildItem; import io.quarkus.vertx.http.deployment.RouteBuildItem; +import io.quarkus.vertx.http.runtime.management.ManagementInterfaceBuildTimeConfig; @BuildSteps(onlyIf = JsonRegistryProcessor.JsonRegistryEnabled.class) public class JsonRegistryProcessor { @@ -39,6 +41,8 @@ public void initializeJsonRegistry(MicrometerConfig config, BuildProducer additionalBeans, BuildProducer registries, NonApplicationRootPathBuildItem nonApplicationRootPathBuildItem, + ManagementInterfaceBuildTimeConfig managementInterfaceBuildTimeConfig, + LaunchModeBuildItem launchModeBuildItem, JsonRecorder recorder) { additionalBeans.produce(AdditionalBeanBuildItem.builder() .addBeanClass(JsonMeterRegistryProvider.class) @@ -46,16 +50,18 @@ public void initializeJsonRegistry(MicrometerConfig config, registryProviders.produce(new MicrometerRegistryProviderBuildItem(JsonMeterRegistry.class)); routes.produce(nonApplicationRootPathBuildItem.routeBuilder() + .management() .routeFunction(config.export.json.path, recorder.route()) .routeConfigKey("quarkus.micrometer.export.json.path") .handler(recorder.getHandler()) .blockingRoute() .build()); - log.debug("Initialized a JSON meter registry on path=" - + nonApplicationRootPathBuildItem.resolvePath(config.export.json.path)); + var path = nonApplicationRootPathBuildItem.resolveManagementPath(config.export.json.path, + managementInterfaceBuildTimeConfig, launchModeBuildItem); + log.debug("Initialized a JSON meter registry on path=" + path); - registries.produce(new RegistryBuildItem("JSON", nonApplicationRootPathBuildItem.resolvePath(config.export.json.path))); + registries.produce(new RegistryBuildItem("JSON", path)); } } diff --git a/extensions/micrometer/deployment/src/main/java/io/quarkus/micrometer/deployment/export/PrometheusRegistryProcessor.java b/extensions/micrometer/deployment/src/main/java/io/quarkus/micrometer/deployment/export/PrometheusRegistryProcessor.java index 99b087153d15c..4fcd30d4ce294 100644 --- a/extensions/micrometer/deployment/src/main/java/io/quarkus/micrometer/deployment/export/PrometheusRegistryProcessor.java +++ b/extensions/micrometer/deployment/src/main/java/io/quarkus/micrometer/deployment/export/PrometheusRegistryProcessor.java @@ -11,6 +11,7 @@ import io.quarkus.deployment.annotations.BuildSteps; import io.quarkus.deployment.annotations.ExecutionTime; import io.quarkus.deployment.annotations.Record; +import io.quarkus.deployment.builditem.LaunchModeBuildItem; import io.quarkus.micrometer.deployment.MicrometerRegistryProviderBuildItem; import io.quarkus.micrometer.runtime.MicrometerRecorder; import io.quarkus.micrometer.runtime.config.MicrometerConfig; @@ -20,6 +21,7 @@ import io.quarkus.micrometer.runtime.export.PrometheusRecorder; import io.quarkus.vertx.http.deployment.NonApplicationRootPathBuildItem; import io.quarkus.vertx.http.deployment.RouteBuildItem; +import io.quarkus.vertx.http.runtime.management.ManagementInterfaceBuildTimeConfig; /** * Add support for the Prometheus Meter Registry. Note that the registry may not @@ -89,6 +91,8 @@ void createPrometheusRoute(BuildProducer routes, BuildProducer registries, MicrometerConfig mConfig, NonApplicationRootPathBuildItem nonApplicationRootPathBuildItem, + ManagementInterfaceBuildTimeConfig managementInterfaceBuildTimeConfig, + LaunchModeBuildItem launchModeBuildItem, PrometheusRecorder recorder) { PrometheusConfigGroup pConfig = mConfig.export.prometheus; @@ -96,6 +100,7 @@ void createPrometheusRoute(BuildProducer routes, // Exact match for resources matched to the root path routes.produce(nonApplicationRootPathBuildItem.routeBuilder() + .management() .routeFunction(pConfig.path, recorder.route()) .routeConfigKey("quarkus.micrometer.export.prometheus.path") .handler(recorder.getHandler()) @@ -105,6 +110,7 @@ void createPrometheusRoute(BuildProducer routes, // Match paths that begin with the deployment path routes.produce(nonApplicationRootPathBuildItem.routeBuilder() + .management() .routeFunction(pConfig.path + (pConfig.path.endsWith("/") ? "*" : "/*"), recorder.route()) .handler(recorder.getHandler()) .blockingRoute() @@ -112,14 +118,18 @@ void createPrometheusRoute(BuildProducer routes, // Fallback paths (for non text/plain requests) routes.produce(nonApplicationRootPathBuildItem.routeBuilder() + .management() .routeFunction(pConfig.path, recorder.fallbackRoute()) .handler(recorder.getFallbackHandler()) .build()); routes.produce(nonApplicationRootPathBuildItem.routeBuilder() + .management() .routeFunction(pConfig.path + (pConfig.path.endsWith("/") ? "*" : "/*"), recorder.fallbackRoute()) .handler(recorder.getFallbackHandler()) .build()); - registries.produce(new RegistryBuildItem("Prometheus", nonApplicationRootPathBuildItem.resolvePath(pConfig.path))); + var path = nonApplicationRootPathBuildItem.resolveManagementPath(pConfig.path, + managementInterfaceBuildTimeConfig, launchModeBuildItem); + registries.produce(new RegistryBuildItem("Prometheus", path)); } } diff --git a/extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/deployment/export/PrometheusEnabledOnManagementInterfaceTest.java b/extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/deployment/export/PrometheusEnabledOnManagementInterfaceTest.java new file mode 100644 index 0000000000000..577a423699806 --- /dev/null +++ b/extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/deployment/export/PrometheusEnabledOnManagementInterfaceTest.java @@ -0,0 +1,35 @@ +package io.quarkus.micrometer.deployment.export; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; + +public class PrometheusEnabledOnManagementInterfaceTest { + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .setFlatClassPath(true) + .withConfigurationResource("test-logging.properties") + .overrideConfigKey("quarkus.micrometer.binder-enabled-default", "false") + .overrideConfigKey("quarkus.micrometer.export.prometheus.enabled", "true") + .overrideConfigKey("quarkus.micrometer.registry-enabled-default", "false") + .overrideConfigKey("quarkus.redis.devservices.enabled", "false") + .overrideConfigKey("quarkus.management.enabled", "true") + .withEmptyApplication(); + + @Test + public void metricsEndpoint() { + RestAssured.given() + .accept("application/json") + .get("http://0.0.0.0:9001/q/metrics") + .then() + .log().all() + .statusCode(406); + + RestAssured.given() + .get("http://0.0.0.0:9001/q/metrics") + .then() + .statusCode(200); + } +} diff --git a/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/config/JsonConfigGroup.java b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/config/JsonConfigGroup.java index 3d2f3caeb78f2..05997b5f534c6 100644 --- a/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/config/JsonConfigGroup.java +++ b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/config/JsonConfigGroup.java @@ -17,6 +17,10 @@ public class JsonConfigGroup implements MicrometerConfig.CapabilityEnabled { /** * The path for the JSON metrics endpoint. * The default value is {@code metrics}. + * + * By default, this value will be resolved as a path relative to `${quarkus.http.non-application-root-path}`. + * If the management interface is enabled, the value will be resolved as a path relative to + * `${quarkus.management.root-path}`. */ @ConfigItem(defaultValue = "metrics") public String path; diff --git a/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/config/PrometheusConfigGroup.java b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/config/PrometheusConfigGroup.java index 007affa398d36..6f1f80ab251da 100644 --- a/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/config/PrometheusConfigGroup.java +++ b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/config/PrometheusConfigGroup.java @@ -25,6 +25,12 @@ public class PrometheusConfigGroup implements MicrometerConfig.CapabilityEnabled * If an absolute path is specified (`/metrics`), the prometheus endpoint will be served * from the configured path. * + * If the management interface is enabled, the value will be resolved as a path relative to + * `${quarkus.management.root-path}` (`q` by default), e.g. + * `http://${quarkus.management.host}:${quarkus.management.port}/${quarkus.management.root-path}/metrics`. + * If an absolute path is specified (`/metrics`), the prometheus endpoint will be served from the configured path, e.g. + * `http://${quarkus.management.host}:${quarkus.management.port}/metrics`. + * * @asciidoclet */ @ConfigItem(defaultValue = "metrics") diff --git a/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/devconsole/BodyHandlerBean.java b/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/devconsole/BodyHandlerBean.java index 02d5eeb1003a4..755c74b85db70 100644 --- a/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/devconsole/BodyHandlerBean.java +++ b/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/devconsole/BodyHandlerBean.java @@ -8,6 +8,7 @@ import io.quarkus.vertx.http.runtime.HttpBuildTimeConfig; import io.quarkus.vertx.http.runtime.HttpConfiguration; import io.quarkus.vertx.http.runtime.VertxHttpRecorder; +import io.quarkus.vertx.http.runtime.management.ManagementInterfaceBuildTimeConfig; import io.vertx.core.Handler; import io.vertx.ext.web.Router; import io.vertx.ext.web.RoutingContext; @@ -19,7 +20,8 @@ void setup(@Observes Router router) { HttpConfiguration httpConfiguration = new HttpConfiguration(); ConfigInstantiator.handleObject(httpConfiguration); Handler bodyHandler = new VertxHttpRecorder(new HttpBuildTimeConfig(), - new RuntimeValue<>(httpConfiguration)) + new ManagementInterfaceBuildTimeConfig(), + new RuntimeValue<>(httpConfiguration), null) .createBodyHandler(); router.route().order(Integer.MIN_VALUE + 1).handler(new Handler() { @Override diff --git a/extensions/smallrye-health/deployment/src/main/java/io/quarkus/smallrye/health/deployment/SmallRyeHealthConfig.java b/extensions/smallrye-health/deployment/src/main/java/io/quarkus/smallrye/health/deployment/SmallRyeHealthConfig.java index f1c88cdea58c7..ee1f47b536bab 100644 --- a/extensions/smallrye-health/deployment/src/main/java/io/quarkus/smallrye/health/deployment/SmallRyeHealthConfig.java +++ b/extensions/smallrye-health/deployment/src/main/java/io/quarkus/smallrye/health/deployment/SmallRyeHealthConfig.java @@ -13,6 +13,8 @@ public class SmallRyeHealthConfig { /** * Root path for health-checking endpoints. * By default, this value will be resolved as a path relative to `${quarkus.http.non-application-root-path}`. + * If the management interface is enabled, the value will be resolved as a path relative to + * `${quarkus.management.root-path}`. */ @ConfigItem(defaultValue = "health") String rootPath; diff --git a/extensions/smallrye-health/deployment/src/main/java/io/quarkus/smallrye/health/deployment/SmallRyeHealthDevUiProcessor.java b/extensions/smallrye-health/deployment/src/main/java/io/quarkus/smallrye/health/deployment/SmallRyeHealthDevUiProcessor.java index fe1a130c3a10e..92a736eec4608 100644 --- a/extensions/smallrye-health/deployment/src/main/java/io/quarkus/smallrye/health/deployment/SmallRyeHealthDevUiProcessor.java +++ b/extensions/smallrye-health/deployment/src/main/java/io/quarkus/smallrye/health/deployment/SmallRyeHealthDevUiProcessor.java @@ -4,10 +4,12 @@ import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.annotations.ExecutionTime; import io.quarkus.deployment.annotations.Record; +import io.quarkus.deployment.builditem.LaunchModeBuildItem; import io.quarkus.devui.spi.page.CardPageBuildItem; import io.quarkus.devui.spi.page.Page; import io.quarkus.smallrye.health.runtime.SmallRyeHealthRecorder; import io.quarkus.vertx.http.deployment.NonApplicationRootPathBuildItem; +import io.quarkus.vertx.http.runtime.management.ManagementInterfaceBuildTimeConfig; /** * This processor is responsible for the dev ui widget. @@ -18,12 +20,16 @@ public class SmallRyeHealthDevUiProcessor { @Record(ExecutionTime.STATIC_INIT) CardPageBuildItem create(NonApplicationRootPathBuildItem nonApplicationRootPathBuildItem, SmallRyeHealthConfig config, - SmallRyeHealthRecorder recorder) { + ManagementInterfaceBuildTimeConfig managementInterfaceBuildTimeConfig, + LaunchModeBuildItem launchModeBuildItem, + SmallRyeHealthRecorder unused) { CardPageBuildItem pageBuildItem = new CardPageBuildItem("Smallrye Health"); + var path = nonApplicationRootPathBuildItem.resolveManagementPath(config.rootPath, + managementInterfaceBuildTimeConfig, launchModeBuildItem); pageBuildItem.addPage(Page.externalPageBuilder("Health") .icon("font-awesome-solid:heart-circle-bolt") - .url(nonApplicationRootPathBuildItem.resolvePath(config.rootPath)) + .url(path) .isJsonContent()); pageBuildItem.addPage(Page.externalPageBuilder("Health UI") diff --git a/extensions/smallrye-health/deployment/src/main/java/io/quarkus/smallrye/health/deployment/SmallRyeHealthProcessor.java b/extensions/smallrye-health/deployment/src/main/java/io/quarkus/smallrye/health/deployment/SmallRyeHealthProcessor.java index 3ae05debeed90..450d6ba69bed3 100644 --- a/extensions/smallrye-health/deployment/src/main/java/io/quarkus/smallrye/health/deployment/SmallRyeHealthProcessor.java +++ b/extensions/smallrye-health/deployment/src/main/java/io/quarkus/smallrye/health/deployment/SmallRyeHealthProcessor.java @@ -1,7 +1,5 @@ package io.quarkus.smallrye.health.deployment; -import static io.quarkus.arc.processor.Annotations.getAnnotations; - import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; @@ -49,6 +47,7 @@ import io.quarkus.kubernetes.spi.KubernetesHealthLivenessPathBuildItem; import io.quarkus.kubernetes.spi.KubernetesHealthReadinessPathBuildItem; import io.quarkus.kubernetes.spi.KubernetesHealthStartupPathBuildItem; +import io.quarkus.kubernetes.spi.KubernetesProbePortNameBuildItem; import io.quarkus.maven.dependency.GACT; import io.quarkus.runtime.configuration.ConfigurationException; import io.quarkus.smallrye.health.deployment.spi.HealthBuildItem; @@ -69,6 +68,7 @@ import io.quarkus.vertx.http.deployment.webjar.WebJarBuildItem; import io.quarkus.vertx.http.deployment.webjar.WebJarResourcesFilter; import io.quarkus.vertx.http.deployment.webjar.WebJarResultsBuildItem; +import io.quarkus.vertx.http.runtime.management.ManagementInterfaceBuildTimeConfig; import io.smallrye.health.SmallRyeHealthReporter; import io.smallrye.health.api.HealthGroup; import io.smallrye.health.api.HealthGroups; @@ -197,6 +197,7 @@ public void defineHealthRoutes(BuildProducer routes, // Register the health handler routes.produce(nonApplicationRootPathBuildItem.routeBuilder() + .management() .route(healthConfig.rootPath) .routeConfigKey("quarkus.smallrye-health.root-path") .handler(new SmallRyeHealthHandler()) @@ -206,6 +207,7 @@ public void defineHealthRoutes(BuildProducer routes, // Register the liveness handler routes.produce(nonApplicationRootPathBuildItem.routeBuilder() + .management() .nestedRoute(healthConfig.rootPath, healthConfig.livenessPath) .handler(new SmallRyeLivenessHandler()) .displayOnNotFoundPage() @@ -214,6 +216,7 @@ public void defineHealthRoutes(BuildProducer routes, // Register the readiness handler routes.produce(nonApplicationRootPathBuildItem.routeBuilder() + .management() .nestedRoute(healthConfig.rootPath, healthConfig.readinessPath) .handler(new SmallRyeReadinessHandler()) .displayOnNotFoundPage() @@ -235,6 +238,7 @@ public void defineHealthRoutes(BuildProducer routes, // Register the health group handlers routes.produce(nonApplicationRootPathBuildItem.routeBuilder() + .management() .nestedRoute(healthConfig.rootPath, healthConfig.groupPath) .handler(new SmallRyeHealthGroupHandler()) .displayOnNotFoundPage() @@ -243,6 +247,7 @@ public void defineHealthRoutes(BuildProducer routes, SmallRyeIndividualHealthGroupHandler handler = new SmallRyeIndividualHealthGroupHandler(); routes.produce(nonApplicationRootPathBuildItem.routeBuilder() + .management() .nestedRoute(healthConfig.rootPath, healthConfig.groupPath + "/*") .handler(handler) .displayOnNotFoundPage() @@ -251,6 +256,7 @@ public void defineHealthRoutes(BuildProducer routes, // Register the wellness handler routes.produce(nonApplicationRootPathBuildItem.routeBuilder() + .management() .nestedRoute(healthConfig.rootPath, healthConfig.wellnessPath) .handler(new SmallRyeWellnessHandler()) .displayOnNotFoundPage() @@ -259,6 +265,7 @@ public void defineHealthRoutes(BuildProducer routes, // Register the startup handler routes.produce(nonApplicationRootPathBuildItem.routeBuilder() + .management() .nestedRoute(healthConfig.rootPath, healthConfig.startupPath) .handler(new SmallRyeStartupHandler()) .displayOnNotFoundPage() @@ -287,17 +294,17 @@ public void processSmallRyeHealthConfigValues(SmallRyeHealthConfig healthConfig, @BuildStep(onlyIf = OpenAPIIncluded.class) public void includeInOpenAPIEndpoint(BuildProducer openAPIProducer, NonApplicationRootPathBuildItem nonApplicationRootPathBuildItem, + ManagementInterfaceBuildTimeConfig managementInterfaceBuildTimeConfig, Capabilities capabilities, SmallRyeHealthConfig healthConfig) { // Add to OpenAPI if OpenAPI is available - if (capabilities.isPresent(Capability.SMALLRYE_OPENAPI)) { + if (capabilities.isPresent(Capability.SMALLRYE_OPENAPI) && !managementInterfaceBuildTimeConfig.enabled) { String healthRootPath = nonApplicationRootPathBuildItem.resolvePath(healthConfig.rootPath); - HealthOpenAPIFilter filter = new HealthOpenAPIFilter(healthRootPath, - nonApplicationRootPathBuildItem.resolveNestedPath(healthRootPath, healthConfig.livenessPath), - nonApplicationRootPathBuildItem.resolveNestedPath(healthRootPath, healthConfig.readinessPath), - nonApplicationRootPathBuildItem.resolveNestedPath(healthRootPath, healthConfig.startupPath)); + nonApplicationRootPathBuildItem.resolveManagementNestedPath(healthRootPath, healthConfig.livenessPath), + nonApplicationRootPathBuildItem.resolveManagementNestedPath(healthRootPath, healthConfig.readinessPath), + nonApplicationRootPathBuildItem.resolveManagementNestedPath(healthRootPath, healthConfig.startupPath)); openAPIProducer.produce(new AddToOpenAPIDefinitionBuildItem(filter)); } @@ -329,19 +336,29 @@ private void warnIfJaxRsPathUsed(IndexView index, DotName healthAnnotation) { @BuildStep public void kubernetes(NonApplicationRootPathBuildItem nonApplicationRootPathBuildItem, SmallRyeHealthConfig healthConfig, + ManagementInterfaceBuildTimeConfig managementInterfaceBuildTimeConfig, BuildProducer livenessPathItemProducer, BuildProducer readinessPathItemProducer, - BuildProducer startupPathItemProducer) { + BuildProducer startupPathItemProducer, + BuildProducer port) { + + if (managementInterfaceBuildTimeConfig.enabled) { + // Switch to the "management" port + port.produce(new KubernetesProbePortNameBuildItem("management")); + } livenessPathItemProducer.produce( new KubernetesHealthLivenessPathBuildItem( - nonApplicationRootPathBuildItem.resolveNestedPath(healthConfig.rootPath, healthConfig.livenessPath))); + nonApplicationRootPathBuildItem.resolveManagementNestedPath(healthConfig.rootPath, + healthConfig.livenessPath))); readinessPathItemProducer.produce( new KubernetesHealthReadinessPathBuildItem( - nonApplicationRootPathBuildItem.resolveNestedPath(healthConfig.rootPath, healthConfig.readinessPath))); + nonApplicationRootPathBuildItem.resolveManagementNestedPath(healthConfig.rootPath, + healthConfig.readinessPath))); startupPathItemProducer.produce( new KubernetesHealthStartupPathBuildItem( - nonApplicationRootPathBuildItem.resolveNestedPath(healthConfig.rootPath, healthConfig.startupPath))); + nonApplicationRootPathBuildItem.resolveManagementNestedPath(healthConfig.rootPath, + healthConfig.startupPath))); } @BuildStep @@ -353,6 +370,7 @@ ShutdownListenerBuildItem shutdownListener() { @BuildStep void registerUiExtension( NonApplicationRootPathBuildItem nonApplicationRootPathBuildItem, + ManagementInterfaceBuildTimeConfig managementInterfaceBuildTimeConfig, SmallRyeHealthConfig healthConfig, LaunchModeBuildItem launchModeBuildItem, BuildProducer webJarBuildProducer) { @@ -365,7 +383,8 @@ void registerUiExtension( Set.of("quarkus.smallrye-health.root-path-ui")); } - String healthPath = nonApplicationRootPathBuildItem.resolvePath(healthConfig.rootPath); + String healthPath = nonApplicationRootPathBuildItem.resolveManagementPath(healthConfig.rootPath, + managementInterfaceBuildTimeConfig, launchModeBuildItem); webJarBuildProducer.produce( WebJarBuildItem.builder().artifactKey(HEALTH_UI_WEBJAR_ARTIFACT_KEY) // @@ -413,6 +432,7 @@ void registerHealthUiHandler( Handler handler = recorder.uiHandler(result.getFinalDestination(), healthUiPath, result.getWebRootConfigurations(), runtimeConfig, shutdownContext); + // The health ui is not a management route. routeProducer.produce(nonApplicationRootPathBuildItem.routeBuilder() .route(healthConfig.ui.rootPath) .displayOnNotFoundPage("Health UI") diff --git a/extensions/smallrye-health/deployment/src/test/java/io/quarkus/smallrye/health/test/HealthCheckOnManagementInterfaceDisabledTest.java b/extensions/smallrye-health/deployment/src/test/java/io/quarkus/smallrye/health/test/HealthCheckOnManagementInterfaceDisabledTest.java new file mode 100644 index 0000000000000..33f48e3d16cbb --- /dev/null +++ b/extensions/smallrye-health/deployment/src/test/java/io/quarkus/smallrye/health/test/HealthCheckOnManagementInterfaceDisabledTest.java @@ -0,0 +1,57 @@ +package io.quarkus.smallrye.health.test; + +import static io.restassured.RestAssured.when; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.is; + +import org.eclipse.microprofile.health.HealthCheck; +import org.eclipse.microprofile.health.HealthCheckResponse; +import org.eclipse.microprofile.health.Liveness; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; +import io.restassured.parsing.Parser; + +public class HealthCheckOnManagementInterfaceDisabledTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(MyCheck.class)) + .overrideConfigKey("quarkus.management.enabled", "false"); + + @Test + public void testHealth() { + try { + RestAssured.defaultParser = Parser.JSON; + when().get("/q/health/live").then() + .body("status", is("UP"), + "checks.status", contains("UP"), + "checks.name", containsInAnyOrder("my-check")); + when().get("/q/health/live").then() + .body("status", is("DOWN"), + "checks.status", contains("DOWN"), + "checks.name", containsInAnyOrder("my-check")); + } finally { + RestAssured.reset(); + } + } + + @Liveness + static class MyCheck implements HealthCheck { + + volatile int counter = 0; + + @Override + public HealthCheckResponse call() { + if (++counter > 1) { + return HealthCheckResponse.builder().down().name("my-check").build(); + } + return HealthCheckResponse.builder().up().name("my-check").build(); + } + } + +} diff --git a/extensions/smallrye-health/deployment/src/test/java/io/quarkus/smallrye/health/test/HealthCheckOnManagementInterfaceTest.java b/extensions/smallrye-health/deployment/src/test/java/io/quarkus/smallrye/health/test/HealthCheckOnManagementInterfaceTest.java new file mode 100644 index 0000000000000..8a5a168ff3597 --- /dev/null +++ b/extensions/smallrye-health/deployment/src/test/java/io/quarkus/smallrye/health/test/HealthCheckOnManagementInterfaceTest.java @@ -0,0 +1,58 @@ +package io.quarkus.smallrye.health.test; + +import static io.restassured.RestAssured.get; +import static io.restassured.RestAssured.when; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.is; + +import org.eclipse.microprofile.health.HealthCheck; +import org.eclipse.microprofile.health.HealthCheckResponse; +import org.eclipse.microprofile.health.Liveness; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; +import io.restassured.parsing.Parser; + +public class HealthCheckOnManagementInterfaceTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(MyCheck.class)) + .overrideConfigKey("quarkus.management.enabled", "true"); + + @Test + public void testHealth() { + try { + RestAssured.defaultParser = Parser.JSON; + when().get("http://0.0.0.0:9001/q/health/live").then() + .body("status", is("UP"), + "checks.status", contains("UP"), + "checks.name", containsInAnyOrder("my-check")); + when().get("http://0.0.0.0:9001/q/health/live").then() + .body("status", is("DOWN"), + "checks.status", contains("DOWN"), + "checks.name", containsInAnyOrder("my-check")); + } finally { + RestAssured.reset(); + } + } + + @Liveness + static class MyCheck implements HealthCheck { + + volatile int counter = 0; + + @Override + public HealthCheckResponse call() { + if (++counter > 1) { + return HealthCheckResponse.builder().down().name("my-check").build(); + } + return HealthCheckResponse.builder().up().name("my-check").build(); + } + } + +} diff --git a/extensions/smallrye-health/deployment/src/test/java/io/quarkus/smallrye/health/test/HealthCheckOnManagementInterfaceWithAbsoluteRootPathTest.java b/extensions/smallrye-health/deployment/src/test/java/io/quarkus/smallrye/health/test/HealthCheckOnManagementInterfaceWithAbsoluteRootPathTest.java new file mode 100644 index 0000000000000..eac5c437bcc6e --- /dev/null +++ b/extensions/smallrye-health/deployment/src/test/java/io/quarkus/smallrye/health/test/HealthCheckOnManagementInterfaceWithAbsoluteRootPathTest.java @@ -0,0 +1,58 @@ +package io.quarkus.smallrye.health.test; + +import static io.restassured.RestAssured.when; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.is; + +import org.eclipse.microprofile.health.HealthCheck; +import org.eclipse.microprofile.health.HealthCheckResponse; +import org.eclipse.microprofile.health.Liveness; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; +import io.restassured.parsing.Parser; + +public class HealthCheckOnManagementInterfaceWithAbsoluteRootPathTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(MyCheck.class)) + .overrideConfigKey("quarkus.management.enabled", "true") + .overrideConfigKey("quarkus.smallrye-health.root-path", "/sante"); + + @Test + public void testHealth() { + try { + RestAssured.defaultParser = Parser.JSON; + when().get("http://0.0.0.0:9001/sante/live").then() + .body("status", is("UP"), + "checks.status", contains("UP"), + "checks.name", containsInAnyOrder("my-check")); + when().get("http://0.0.0.0:9001/sante/live").then() + .body("status", is("DOWN"), + "checks.status", contains("DOWN"), + "checks.name", containsInAnyOrder("my-check")); + } finally { + RestAssured.reset(); + } + } + + @Liveness + static class MyCheck implements HealthCheck { + + volatile int counter = 0; + + @Override + public HealthCheckResponse call() { + if (++counter > 1) { + return HealthCheckResponse.builder().down().name("my-check").build(); + } + return HealthCheckResponse.builder().up().name("my-check").build(); + } + } + +} diff --git a/extensions/smallrye-health/deployment/src/test/java/io/quarkus/smallrye/health/test/HealthCheckOnManagementInterfaceWithRelativeRootPathTest.java b/extensions/smallrye-health/deployment/src/test/java/io/quarkus/smallrye/health/test/HealthCheckOnManagementInterfaceWithRelativeRootPathTest.java new file mode 100644 index 0000000000000..24143522a39bd --- /dev/null +++ b/extensions/smallrye-health/deployment/src/test/java/io/quarkus/smallrye/health/test/HealthCheckOnManagementInterfaceWithRelativeRootPathTest.java @@ -0,0 +1,59 @@ +package io.quarkus.smallrye.health.test; + +import static io.restassured.RestAssured.when; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.is; + +import org.eclipse.microprofile.health.HealthCheck; +import org.eclipse.microprofile.health.HealthCheckResponse; +import org.eclipse.microprofile.health.Liveness; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; +import io.restassured.parsing.Parser; + +public class HealthCheckOnManagementInterfaceWithRelativeRootPathTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(MyCheck.class)) + .overrideConfigKey("quarkus.management.enabled", "true") + .overrideConfigKey("quarkus.management.root-path", "/management") + .overrideConfigKey("quarkus.smallrye-health.root-path", "sante"); + + @Test + public void testHealth() { + try { + RestAssured.defaultParser = Parser.JSON; + when().get("http://0.0.0.0:9001/management/sante/live").then() + .body("status", is("UP"), + "checks.status", contains("UP"), + "checks.name", containsInAnyOrder("my-check")); + when().get("http://0.0.0.0:9001/management/sante/live").then() + .body("status", is("DOWN"), + "checks.status", contains("DOWN"), + "checks.name", containsInAnyOrder("my-check")); + } finally { + RestAssured.reset(); + } + } + + @Liveness + static class MyCheck implements HealthCheck { + + volatile int counter = 0; + + @Override + public HealthCheckResponse call() { + if (++counter > 1) { + return HealthCheckResponse.builder().down().name("my-check").build(); + } + return HealthCheckResponse.builder().up().name("my-check").build(); + } + } + +} diff --git a/extensions/smallrye-metrics/deployment/src/main/java/io/quarkus/smallrye/metrics/deployment/SmallRyeMetricsProcessor.java b/extensions/smallrye-metrics/deployment/src/main/java/io/quarkus/smallrye/metrics/deployment/SmallRyeMetricsProcessor.java index a1d836621b22f..2294662349da7 100644 --- a/extensions/smallrye-metrics/deployment/src/main/java/io/quarkus/smallrye/metrics/deployment/SmallRyeMetricsProcessor.java +++ b/extensions/smallrye-metrics/deployment/src/main/java/io/quarkus/smallrye/metrics/deployment/SmallRyeMetricsProcessor.java @@ -102,6 +102,9 @@ static final class SmallRyeMetricsConfig { /** * The path to the metrics handler. + * By default, this value will be resolved as a path relative to `${quarkus.http.non-application-root-path}`. + * If the management interface is enabled, the value will be resolved as a path relative to + * `${quarkus.management.root-path}`. */ @ConfigItem(defaultValue = "metrics") String path; @@ -162,11 +165,13 @@ void createRoute(BuildProducer routes, displayableEndpoints.produce(new NotFoundPageDisplayableEndpointBuildItem(metrics.path)); } routes.produce(frameworkRoot.routeBuilder() + .management() .route(metrics.path + (metrics.path.endsWith("/") ? "*" : "/*")) .handler(recorder.handler(frameworkRoot.resolvePath(metrics.path))) .blockingRoute() .build()); routes.produce(frameworkRoot.routeBuilder() + .management() .route(metrics.path) .routeConfigKey("quarkus.smallrye-metrics.path") .handler(recorder.handler(frameworkRoot.resolvePath(metrics.path))) diff --git a/extensions/vertx-http/deployment-spi/src/main/java/io/quarkus/vertx/http/deployment/spi/UseManagementInterfaceBuildItem.java b/extensions/vertx-http/deployment-spi/src/main/java/io/quarkus/vertx/http/deployment/spi/UseManagementInterfaceBuildItem.java new file mode 100644 index 0000000000000..40099c3c44439 --- /dev/null +++ b/extensions/vertx-http/deployment-spi/src/main/java/io/quarkus/vertx/http/deployment/spi/UseManagementInterfaceBuildItem.java @@ -0,0 +1,10 @@ +package io.quarkus.vertx.http.deployment.spi; + +import io.quarkus.builder.item.SimpleBuildItem; + +/** + * Marker build item indicating that the application uses a separate interface:port for the management endpoints such + * as metrics, health and prometheus. + */ +public final class UseManagementInterfaceBuildItem extends SimpleBuildItem { +} diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/HttpRootPathBuildItem.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/HttpRootPathBuildItem.java index d1d74aa653d10..98ef3b6f76e6b 100644 --- a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/HttpRootPathBuildItem.java +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/HttpRootPathBuildItem.java @@ -210,7 +210,7 @@ public Builder routeConfigKey(String attributeName) { @Override public RouteBuildItem build() { - return new RouteBuildItem(this, routeType, routerType); + return new RouteBuildItem(this, routeType, routerType, isManagement); } @Override @@ -218,6 +218,12 @@ protected ConfiguredPathInfo getRouteConfigInfo() { return super.getRouteConfigInfo(); } + @Override + public Builder management() { + super.management(); + return this; + } + @Override protected NotFoundPageDisplayableEndpointBuildItem getNotFoundEndpoint() { return super.getNotFoundEndpoint(); diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/NonApplicationRootPathBuildItem.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/NonApplicationRootPathBuildItem.java index 43d82e0db017b..c39040cd9cf7d 100644 --- a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/NonApplicationRootPathBuildItem.java +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/NonApplicationRootPathBuildItem.java @@ -4,17 +4,25 @@ import java.util.function.Consumer; import java.util.function.Function; +import org.eclipse.microprofile.config.Config; +import org.eclipse.microprofile.config.ConfigProvider; + import io.quarkus.builder.item.SimpleBuildItem; +import io.quarkus.deployment.builditem.LaunchModeBuildItem; import io.quarkus.deployment.util.UriNormalizationUtil; import io.quarkus.vertx.http.deployment.devmode.NotFoundPageDisplayableEndpointBuildItem; import io.quarkus.vertx.http.deployment.devmode.console.ConfiguredPathInfo; import io.quarkus.vertx.http.runtime.HandlerType; +import io.quarkus.vertx.http.runtime.management.ManagementInterfaceBuildTimeConfig; import io.vertx.core.Handler; import io.vertx.ext.web.Route; import io.vertx.ext.web.Router; import io.vertx.ext.web.RoutingContext; public final class NonApplicationRootPathBuildItem extends SimpleBuildItem { + + // TODO Should be handle the management root path? + /** * Normalized of quarkus.http.root-path. * Must end in a slash @@ -26,6 +34,11 @@ public final class NonApplicationRootPathBuildItem extends SimpleBuildItem { */ final URI nonApplicationRootPath; + /** + * Normalized from quarkus.management.root-path + */ + final URI managementRootPath; + /** * Non-Application root path is distinct from HTTP root path. */ @@ -33,12 +46,14 @@ public final class NonApplicationRootPathBuildItem extends SimpleBuildItem { final boolean attachedToMainRouter; - public NonApplicationRootPathBuildItem(String httpRootPath, String nonApplicationRootPath) { + public NonApplicationRootPathBuildItem(String httpRootPath, String nonApplicationRootPath, String managementRootPath) { // Presume value always starts with a slash and is normalized this.httpRootPath = UriNormalizationUtil.toURI(httpRootPath, true); this.nonApplicationRootPath = UriNormalizationUtil.normalizeWithBase(this.httpRootPath, nonApplicationRootPath, true); + this.managementRootPath = managementRootPath == null ? null + : UriNormalizationUtil.toURI("/" + managementRootPath, true); this.dedicatedRouterRequired = !this.nonApplicationRootPath.getPath().equals(this.httpRootPath.getPath()); @@ -95,6 +110,18 @@ public String getNonApplicationRootPath() { return nonApplicationRootPath.getPath(); } + /** + * @return the normalized root path for the mangement endpoints. {@code getNonApplicationRootPath()} if the + * management interface is disabled. + */ + public String getManagementRootPath() { + if (managementRootPath != null) { + return managementRootPath.getPath(); + } else { + return getNonApplicationRootPath(); + } + } + /** * Resolve path into an absolute path. * If path is relative, it will be resolved against `quarkus.http.non-application-root-path`. @@ -130,8 +157,8 @@ public String getNonApplicationRootPath() { * * @param path Path to be resolved to an absolute path. * @return An absolute path not ending with a slash - * @see UriNormalizationUtil#normalizeWithBase(URI, String, boolean) * @throws IllegalArgumentException if path is null or empty + * @see UriNormalizationUtil#normalizeWithBase(URI, String, boolean) */ public String resolvePath(String path) { if (path == null || path.trim().isEmpty()) { @@ -140,13 +167,52 @@ public String resolvePath(String path) { return UriNormalizationUtil.normalizeWithBase(nonApplicationRootPath, path, false).getPath(); } + public String resolveManagementPath(String path, ManagementInterfaceBuildTimeConfig managementInterfaceBuildTimeConfig, + LaunchModeBuildItem mode) { + if (path == null || path.trim().isEmpty()) { + throw new IllegalArgumentException("Specified path can not be empty"); + } + if (!managementInterfaceBuildTimeConfig.enabled) { + if (managementRootPath != null) { + return UriNormalizationUtil.normalizeWithBase(managementRootPath, path, false).getPath(); + } + return UriNormalizationUtil.normalizeWithBase(nonApplicationRootPath, path, false).getPath(); + } else { + // Best effort + String prefix = getManagementUrlPrefix(mode); + if (managementRootPath != null) { + return prefix + UriNormalizationUtil.normalizeWithBase(managementRootPath, path, false).getPath(); + } else { + return prefix + path; + } + } + } + + /** + * Best effort to deduce the URL prefix (scheme, host, port) of the management interface. + * + * @param mode the mode, influencing the default port + * @return the prefix + */ + public static String getManagementUrlPrefix(LaunchModeBuildItem mode) { + Config config = ConfigProvider.getConfig(); + var managementHost = config.getOptionalValue("quarkus.management.host", String.class).orElse("0.0.0.0"); + var managementPort = config.getOptionalValue("quarkus.management.port", Integer.class).orElse(9000); + if (mode.isTest()) { + managementPort = config.getOptionalValue("quarkus.management.test-port", Integer.class).orElse(9001); + } + var isHttps = isTLsConfigured(config); + + return (isHttps ? "https://" : "http://") + managementHost + ":" + managementPort; + } + /** * Resolve a base path and a sub-resource against the non-application root. * This will call resolvePath on the base path (to establish a fully-resolved, * absolute path), and then will resolve the subRoute against that resolved path. * This allows a configured subpath to be configured (consistently) * as an absolute URI. - * + *

* Given {@literal quarkus.http.root-path=/} and * {@literal quarkus.http.non-application-root-path="q"} *

    @@ -158,9 +224,9 @@ public String resolvePath(String path) { * * @param path Path to be resolved to an absolute path. * @return An absolute path not ending with a slash + * @throws IllegalArgumentException if path is null or empty * @see UriNormalizationUtil#normalizeWithBase(URI, String, boolean) * @see #resolvePath(String) - * @throws IllegalArgumentException if path is null or empty */ public String resolveNestedPath(String path, String subRoute) { if (path == null || path.trim().isEmpty()) { @@ -170,6 +236,19 @@ public String resolveNestedPath(String path, String subRoute) { return UriNormalizationUtil.normalizeWithBase(base, subRoute, false).getPath(); } + public String resolveManagementNestedPath(String path, String subRoute) { + if (path == null || path.trim().isEmpty()) { + throw new IllegalArgumentException("Specified path can not be empty"); + } + URI base; + if (managementRootPath != null) { + base = UriNormalizationUtil.normalizeWithBase(managementRootPath, path, true); + } else { + base = UriNormalizationUtil.normalizeWithBase(nonApplicationRootPath, path, true); + } + return UriNormalizationUtil.normalizeWithBase(base, subRoute, false).getPath(); + } + public Builder routeBuilder() { return new Builder(this); } @@ -194,8 +273,21 @@ public Builder routeFunction(Function routeFunction) { } public Builder routeFunction(String route, Consumer routeFunction) { - route = super.absolutePath = buildItem.resolvePath(route); + if (isManagement && this.buildItem.managementRootPath != null) { + // The logic is slightly different when the management interface is enabled, as we have a single + // router mounted at the root. + if (route.startsWith("/")) { + this.path = route; + } else { + this.path = buildItem.getManagementRootPath() + route; + } + this.routerType = RouteBuildItem.RouteType.ABSOLUTE_ROUTE; + super.routeFunction(this.path, routeFunction); + return this; + } + + route = super.absolutePath = buildItem.resolvePath(route); boolean isFrameworkRoute = buildItem.dedicatedRouterRequired && route.startsWith(buildItem.getNonApplicationRootPath()); @@ -278,7 +370,7 @@ public Builder routeConfigKey(String attributeName) { @Override public RouteBuildItem build() { - return new RouteBuildItem(this, routeType, routerType); + return new RouteBuildItem(this, routeType, routerType, isManagement); } @Override @@ -288,7 +380,50 @@ protected ConfiguredPathInfo getRouteConfigInfo() { @Override protected NotFoundPageDisplayableEndpointBuildItem getNotFoundEndpoint() { - return super.getNotFoundEndpoint(); + if (!displayOnNotFoundPage) { + return null; + } + if (isManagement && buildItem.managementRootPath != null) { + return null; // Exposed on the management interface, so not exposed. + } + if (notFoundPagePath == null) { + throw new RuntimeException("Cannot display " + routeFunction + + " on not found page as no explicit path was specified and a route function is in use"); + } + if (absolutePath != null) { + return new NotFoundPageDisplayableEndpointBuildItem(absolutePath, notFoundPageTitle, true); + } + return new NotFoundPageDisplayableEndpointBuildItem(notFoundPagePath, notFoundPageTitle, false); + } + + @Override + public Builder management() { + super.management(); + return this; } } + + /** + * Best effort to check if the management interface is using {@code https}. + * + * @param config the config + * @return {@code true} if the management interface configuration contains a key or a certificate (indicating TLS) + */ + private static boolean isTLsConfigured(Config config) { + var hasCert = config.getOptionalValue("quarkus.management.ssl.certificate.file", String.class).isPresent(); + var hasKey = config.getOptionalValue("quarkus.management.ssl.certificate.key-file", String.class).isPresent(); + + var hasKeys = config.getOptionalValue("quarkus.management.ssl.certificate.key-files", String.class).isPresent(); + var hasCerts = config.getOptionalValue("quarkus.management.ssl.certificate.files", String.class).isPresent(); + + var hasProvider = config.getOptionalValue("quarkus.management.ssl.certificate.credential-provider", String.class) + .isPresent(); + var hasProviderName = config + .getOptionalValue("quarkus.management.ssl.certificate.credential-provider-name", String.class).isPresent(); + + var hasKeyStore = config.getOptionalValue("quarkus.management.ssl.certificate.key-store-file", String.class) + .isPresent(); + + return hasCerts || hasKeys || hasCert || hasKey || hasProvider || hasProviderName || hasKeyStore; + } } diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/RouteBuildItem.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/RouteBuildItem.java index 4f75d3b374a22..f4f985baee246 100644 --- a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/RouteBuildItem.java +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/RouteBuildItem.java @@ -21,6 +21,8 @@ public static Builder builder() { return new Builder(); } + private final boolean management; + private final Function routeFunction; private final Handler handler; private final HandlerType type; @@ -29,9 +31,10 @@ public static Builder builder() { private final NotFoundPageDisplayableEndpointBuildItem notFoundPageDisplayableEndpoint; private final ConfiguredPathInfo configuredPathInfo; - RouteBuildItem(Builder builder, RouteType routeType, RouteType routerType) { + RouteBuildItem(Builder builder, RouteType routeType, RouteType routerType, boolean management) { this.routeFunction = builder.routeFunction; this.handler = builder.handler; + this.management = management; this.type = builder.type; this.routeType = routeType; this.routerType = routerType; @@ -79,6 +82,15 @@ public ConfiguredPathInfo getConfiguredPathInfo() { return configuredPathInfo; } + /** + * @return {@code true} if the route is exposing a management endpoint. + * It matters when using a different interface/port for the management endpoints, as these routes will only + * be accessible from that different interface/port. + */ + public boolean isManagement() { + return management; + } + public enum RouteType { FRAMEWORK_ROUTE, APPLICATION_ROUTE, @@ -100,6 +112,8 @@ public static class Builder { protected String routeConfigKey; protected String absolutePath; + protected boolean isManagement; + /** * {@link #routeFunction(String, Consumer)} should be used instead * @@ -195,12 +209,17 @@ public Builder routeConfigKey(String attributeName) { return this; } + public Builder management() { + this.isManagement = true; + return this; + } + public RouteBuildItem build() { if (routeFunction == null) { throw new IllegalStateException( "'RouteBuildItem$Builder.routeFunction' was not set. Ensure that one of the builder methods that result in it being set is called"); } - return new RouteBuildItem(this, APPLICATION_ROUTE, APPLICATION_ROUTE); + return new RouteBuildItem(this, APPLICATION_ROUTE, APPLICATION_ROUTE, isManagement); } protected ConfiguredPathInfo getRouteConfigInfo() { @@ -212,9 +231,9 @@ protected ConfiguredPathInfo getRouteConfigInfo() { + " as no explicit path was specified and a route function is in use"); } if (absolutePath != null) { - return new ConfiguredPathInfo(routeConfigKey, absolutePath, true); + return new ConfiguredPathInfo(routeConfigKey, absolutePath, true, isManagement); } - return new ConfiguredPathInfo(routeConfigKey, routePath, false); + return new ConfiguredPathInfo(routeConfigKey, routePath, false, isManagement); } protected NotFoundPageDisplayableEndpointBuildItem getNotFoundEndpoint() { diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/VertxHttpProcessor.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/VertxHttpProcessor.java index 32effe3f7e844..8868fd59db1fe 100644 --- a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/VertxHttpProcessor.java +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/VertxHttpProcessor.java @@ -53,6 +53,7 @@ import io.quarkus.vertx.http.deployment.devmode.HttpRemoteDevClientProvider; import io.quarkus.vertx.http.deployment.devmode.NotFoundPageDisplayableEndpointBuildItem; import io.quarkus.vertx.http.deployment.spi.FrameworkEndpointsBuildItem; +import io.quarkus.vertx.http.deployment.spi.UseManagementInterfaceBuildItem; import io.quarkus.vertx.http.runtime.BasicRoute; import io.quarkus.vertx.http.runtime.CurrentRequestProducer; import io.quarkus.vertx.http.runtime.CurrentVertxRequest; @@ -64,6 +65,7 @@ import io.quarkus.vertx.http.runtime.cors.CORSRecorder; import io.quarkus.vertx.http.runtime.filters.Filter; import io.quarkus.vertx.http.runtime.filters.GracefulShutdownFilter; +import io.quarkus.vertx.http.runtime.management.ManagementInterfaceBuildTimeConfig; import io.vertx.core.Handler; import io.vertx.core.http.impl.Http1xServerRequest; import io.vertx.core.impl.VertxImpl; @@ -88,18 +90,26 @@ HttpRootPathBuildItem httpRoot(HttpBuildTimeConfig httpBuildTimeConfig) { } @BuildStep - NonApplicationRootPathBuildItem frameworkRoot(HttpBuildTimeConfig httpBuildTimeConfig) { - return new NonApplicationRootPathBuildItem(httpBuildTimeConfig.rootPath, httpBuildTimeConfig.nonApplicationRootPath); + NonApplicationRootPathBuildItem frameworkRoot(HttpBuildTimeConfig httpBuildTimeConfig, + ManagementInterfaceBuildTimeConfig managementBuildTimeConfig) { + String mrp = null; + if (managementBuildTimeConfig.enabled) { + mrp = managementBuildTimeConfig.rootPath; + } + return new NonApplicationRootPathBuildItem(httpBuildTimeConfig.rootPath, httpBuildTimeConfig.nonApplicationRootPath, + mrp); } @BuildStep FrameworkEndpointsBuildItem frameworkEndpoints(NonApplicationRootPathBuildItem nonApplicationRootPath, + ManagementInterfaceBuildTimeConfig managementInterfaceBuildTimeConfig, LaunchModeBuildItem launchModeBuildItem, List routes) { List frameworkEndpoints = new ArrayList<>(); for (RouteBuildItem route : routes) { if (FRAMEWORK_ROUTE.equals(route.getRouteType())) { if (route.getConfiguredPathInfo() != null) { - frameworkEndpoints.add(route.getConfiguredPathInfo().getEndpointPath(nonApplicationRootPath)); + frameworkEndpoints.add(route.getConfiguredPathInfo().getEndpointPath(nonApplicationRootPath, + managementInterfaceBuildTimeConfig, launchModeBuildItem)); continue; } if (route.getRouteFunction() != null && route.getRouteFunction() instanceof BasicRoute) { @@ -135,6 +145,14 @@ UnremovableBeanBuildItem shouldNotRemoveHttpServerOptionsCustomizers() { return UnremovableBeanBuildItem.beanTypes(HttpServerOptionsCustomizer.class); } + @BuildStep + UseManagementInterfaceBuildItem useManagementInterfaceBuildItem(ManagementInterfaceBuildTimeConfig config) { + if (config.enabled) { + return new UseManagementInterfaceBuildItem(); + } + return null; + } + /** * Workaround for https://github.com/quarkusio/quarkus/issues/4720 by filtering Vertx multiple instance warning in dev * mode. @@ -165,6 +183,16 @@ public void kubernetes(BuildProducer kubernetesPorts) { kubernetesPorts.produce(new KubernetesPortBuildItem(port, "http")); } + @BuildStep + public KubernetesPortBuildItem kubernetesForManagement( + ManagementInterfaceBuildTimeConfig managementInterfaceBuildTimeConfig) { + if (managementInterfaceBuildTimeConfig.enabled) { + int port = ConfigProvider.getConfig().getOptionalValue("quarkus.management.port", Integer.class).orElse(9000); + return new KubernetesPortBuildItem(port, "management"); + } + return null; + } + @BuildStep void notFoundRoutes( List routes, @@ -203,6 +231,7 @@ VertxWebRouterBuildItem initializeRouter(VertxHttpRecorder recorder, CoreVertxBuildItem vertx, List routes, HttpBuildTimeConfig httpBuildTimeConfig, + ManagementInterfaceBuildTimeConfig managementBuildTimeConfig, NonApplicationRootPathBuildItem nonApplicationRootPath, ShutdownContextBuildItem shutdown) { @@ -210,19 +239,28 @@ VertxWebRouterBuildItem initializeRouter(VertxHttpRecorder recorder, RuntimeValue mutinyRouter = initialRouter.getMutinyRouter(); RuntimeValue frameworkRouter = null; RuntimeValue mainRouter = null; + RuntimeValue managementRouter = null; List redirectRoutes = new ArrayList<>(); boolean frameworkRouterCreated = false; boolean mainRouterCreated = false; + boolean managementRouterCreated = false; + + boolean isManagementInterfaceEnabled = managementBuildTimeConfig.enabled; for (RouteBuildItem route : routes) { - if (nonApplicationRootPath.isDedicatedRouterRequired() && route.isRouterFramework()) { + if (route.isManagement() && isManagementInterfaceEnabled) { + if (!managementRouterCreated) { + managementRouter = recorder.initializeRouter(vertx.getVertx()); + managementRouterCreated = true; + } + recorder.addRoute(managementRouter, route.getRouteFunction(), route.getHandler(), route.getType()); + } else if (nonApplicationRootPath.isDedicatedRouterRequired() && route.isRouterFramework()) { // Non-application endpoints on a separate path if (!frameworkRouterCreated) { frameworkRouter = recorder.initializeRouter(vertx.getVertx()); frameworkRouterCreated = true; } - recorder.addRoute(frameworkRouter, route.getRouteFunction(), route.getHandler(), route.getType()); } else if (route.isRouterAbsolute()) { // Add Route to "/" @@ -248,7 +286,7 @@ VertxWebRouterBuildItem initializeRouter(VertxHttpRecorder recorder, } } - return new VertxWebRouterBuildItem(httpRouteRouter, mainRouter, frameworkRouter, mutinyRouter); + return new VertxWebRouterBuildItem(httpRouteRouter, mainRouter, frameworkRouter, managementRouter, mutinyRouter); } @BuildStep @@ -325,6 +363,7 @@ ServiceStartBuildItem finalizeRouter( defaultRoute.map(DefaultRouteBuildItem::getRoute).orElse(null), listOfFilters, vertx.getVertx(), lrc, mainRouter, httpRouteRouter.getHttpRouter(), httpRouteRouter.getMutinyRouter(), httpRouteRouter.getFrameworkRouter(), + httpRouteRouter.getManagementRouter(), httpRootPathBuildItem.getRootPath(), nonApplicationRootPathBuildItem.getNonApplicationRootPath(), launchMode.getLaunchMode(), diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/VertxWebRouterBuildItem.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/VertxWebRouterBuildItem.java index 391a48592fdee..f66cd2154c48a 100644 --- a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/VertxWebRouterBuildItem.java +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/VertxWebRouterBuildItem.java @@ -10,12 +10,16 @@ public final class VertxWebRouterBuildItem extends SimpleBuildItem { private final RuntimeValue mainRouter; private final RuntimeValue frameworkRouter; private final RuntimeValue mutinyRouter; + private final RuntimeValue managementRouter; VertxWebRouterBuildItem(RuntimeValue httpRouter, RuntimeValue mainRouter, - RuntimeValue frameworkRouter, RuntimeValue mutinyRouter) { + RuntimeValue frameworkRouter, + RuntimeValue managementRouter, + RuntimeValue mutinyRouter) { this.httpRouter = httpRouter; this.mainRouter = mainRouter; this.frameworkRouter = frameworkRouter; + this.managementRouter = managementRouter; // Can be null if the management interface is disabled this.mutinyRouter = mutinyRouter; } @@ -45,4 +49,13 @@ RuntimeValue getMainRouter() { RuntimeValue getFrameworkRouter() { return frameworkRouter; } + + /** + * Will be {@code null} if the management interface is disabled. + * + * @return RuntimeValue + */ + RuntimeValue getManagementRouter() { + return managementRouter; + } } diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/console/ConfiguredPathInfo.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/console/ConfiguredPathInfo.java index a522178d15ab9..cea49d8df6854 100644 --- a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/console/ConfiguredPathInfo.java +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/console/ConfiguredPathInfo.java @@ -1,18 +1,22 @@ package io.quarkus.vertx.http.deployment.devmode.console; +import io.quarkus.deployment.builditem.LaunchModeBuildItem; import io.quarkus.runtime.TemplateHtmlBuilder; import io.quarkus.vertx.http.deployment.HttpRootPathBuildItem; import io.quarkus.vertx.http.deployment.NonApplicationRootPathBuildItem; +import io.quarkus.vertx.http.runtime.management.ManagementInterfaceBuildTimeConfig; public class ConfiguredPathInfo { private final String name; private final String endpointPath; private final boolean absolutePath; + private final boolean management; - public ConfiguredPathInfo(String name, String endpointPath, boolean isAbsolutePath) { + public ConfiguredPathInfo(String name, String endpointPath, boolean isAbsolutePath, boolean management) { this.name = name; this.endpointPath = endpointPath; this.absolutePath = isAbsolutePath; + this.management = management; } public String getName() { @@ -27,7 +31,12 @@ public String getEndpointPath(HttpRootPathBuildItem httpRoot) { } } - public String getEndpointPath(NonApplicationRootPathBuildItem nonAppRoot) { + public String getEndpointPath(NonApplicationRootPathBuildItem nonAppRoot, ManagementInterfaceBuildTimeConfig mibt, + LaunchModeBuildItem mode) { + if (management && mibt.enabled) { + var prefix = NonApplicationRootPathBuildItem.getManagementUrlPrefix(mode); + return prefix + endpointPath; + } if (absolutePath) { return endpointPath; } else { diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/console/DevConsoleProcessor.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/console/DevConsoleProcessor.java index 5afe466d5054a..fec79b47321cf 100644 --- a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/console/DevConsoleProcessor.java +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/console/DevConsoleProcessor.java @@ -102,6 +102,7 @@ import io.quarkus.vertx.http.runtime.devmode.RuntimeDevConsoleRoute; import io.quarkus.vertx.http.runtime.logstream.LogStreamRecorder; import io.quarkus.vertx.http.runtime.logstream.WebSocketLogHandler; +import io.quarkus.vertx.http.runtime.management.ManagementInterfaceBuildTimeConfig; import io.smallrye.common.vertx.VertxContext; import io.smallrye.config.common.utils.StringUtil; import io.vertx.core.Handler; @@ -340,6 +341,7 @@ public ServiceStartBuildItem setupDeploymentSideHandling(List allRoutes, List routes, NonApplicationRootPathBuildItem nonApplicationRootPathBuildItem, + ManagementInterfaceBuildTimeConfig managementInterfaceBuildTimeConfig, List configDescriptionBuildItems, LaunchModeBuildItem launchModeBuildItem) { if (launchModeBuildItem.getDevModeType().orElse(null) != DevModeType.LOCAL) { @@ -352,6 +354,7 @@ public ServiceStartBuildItem setupDeploymentSideHandling(List devTemplatePaths, BuildSystemTargetBuildItem buildSystemTargetBuildItem, Optional effectiveIdeBuildItem, NonApplicationRootPathBuildItem nonApplicationRootPathBuildItem, + ManagementInterfaceBuildTimeConfig managementInterfaceBuildTimeConfig, List configDescriptionBuildItems, LaunchModeBuildItem launchModeBuildItem) { EngineBuilder builder = Engine.builder().addDefaults(); @@ -618,8 +622,9 @@ private Engine buildEngine(List devTemplatePaths, for (RouteBuildItem item : allRoutes) { ConfiguredPathInfo resolvedPathBuildItem = item.getConfiguredPathInfo(); if (resolvedPathBuildItem != null) { - resolvedPaths.put(resolvedPathBuildItem.getName(), - resolvedPathBuildItem.getEndpointPath(nonApplicationRootPathBuildItem)); + String path = resolvedPathBuildItem.getEndpointPath(nonApplicationRootPathBuildItem, + managementInterfaceBuildTimeConfig, launchModeBuildItem); + resolvedPaths.put(resolvedPathBuildItem.getName(), path); } } diff --git a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/NonApplicationAndRootPathTest.java b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/NonApplicationAndRootPathTest.java index 243bcca9b71c9..36f3fac9e9128 100644 --- a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/NonApplicationAndRootPathTest.java +++ b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/NonApplicationAndRootPathTest.java @@ -64,7 +64,8 @@ public void handle(RoutingContext routingContext) { @Test public void testNonApplicationEndpointDirect() { // Note RestAssured knows the path prefix is /api - RestAssured.given().get("/q/non-app-relative").then().statusCode(200).body(Matchers.equalTo("/api/q/non-app-relative")); + RestAssured.given().get("/q/non-app-relative") + .then().statusCode(200).body(Matchers.equalTo("/api/q/non-app-relative")); } @Singleton diff --git a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/deployment/NonApplicationRootPathBuildItemTest.java b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/deployment/NonApplicationRootPathBuildItemTest.java index 4844ba60a7ba8..90796a00e584c 100644 --- a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/deployment/NonApplicationRootPathBuildItemTest.java +++ b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/deployment/NonApplicationRootPathBuildItemTest.java @@ -1,12 +1,19 @@ package io.quarkus.vertx.http.deployment; +import java.util.Optional; + import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import io.quarkus.deployment.builditem.LaunchModeBuildItem; +import io.quarkus.runtime.LaunchMode; +import io.quarkus.vertx.http.runtime.management.ManagementInterfaceBuildTimeConfig; + public class NonApplicationRootPathBuildItemTest { + @Test void testResolvePathWithSlashRelativeQ() { - NonApplicationRootPathBuildItem buildItem = new NonApplicationRootPathBuildItem("/", "q"); + NonApplicationRootPathBuildItem buildItem = new NonApplicationRootPathBuildItem("/", "q", null); Assertions.assertTrue(buildItem.isDedicatedRouterRequired()); Assertions.assertTrue(buildItem.attachedToMainRouter); Assertions.assertEquals("/q/", buildItem.getVertxRouterPath()); @@ -24,7 +31,7 @@ void testResolvePathWithSlashRelativeQ() { @Test void testResolvePathWithSlashAbsoluteQ() { - NonApplicationRootPathBuildItem buildItem = new NonApplicationRootPathBuildItem("/", "/q"); + NonApplicationRootPathBuildItem buildItem = new NonApplicationRootPathBuildItem("/", "/q", null); Assertions.assertTrue(buildItem.isDedicatedRouterRequired()); Assertions.assertTrue(buildItem.attachedToMainRouter); Assertions.assertEquals("/q/", buildItem.getVertxRouterPath()); @@ -38,7 +45,7 @@ void testResolvePathWithSlashAbsoluteQ() { @Test void testResolvePathWithSlashAppWithRelativeQ() { - NonApplicationRootPathBuildItem buildItem = new NonApplicationRootPathBuildItem("/app", "q"); + NonApplicationRootPathBuildItem buildItem = new NonApplicationRootPathBuildItem("/app", "q", null); Assertions.assertTrue(buildItem.isDedicatedRouterRequired()); Assertions.assertTrue(buildItem.attachedToMainRouter); Assertions.assertEquals("/q/", buildItem.getVertxRouterPath()); @@ -52,7 +59,7 @@ void testResolvePathWithSlashAppWithRelativeQ() { @Test void testResolvePathWithSlashAppWithAbsoluteQ() { - NonApplicationRootPathBuildItem buildItem = new NonApplicationRootPathBuildItem("/app", "/q"); + NonApplicationRootPathBuildItem buildItem = new NonApplicationRootPathBuildItem("/app", "/q", null); Assertions.assertTrue(buildItem.isDedicatedRouterRequired()); Assertions.assertFalse(buildItem.attachedToMainRouter); Assertions.assertEquals("/q/", buildItem.getVertxRouterPath()); @@ -66,7 +73,7 @@ void testResolvePathWithSlashAppWithAbsoluteQ() { @Test void testResolvePathWithSlashEmpty() { - NonApplicationRootPathBuildItem buildItem = new NonApplicationRootPathBuildItem("/", ""); + NonApplicationRootPathBuildItem buildItem = new NonApplicationRootPathBuildItem("/", "", null); Assertions.assertFalse(buildItem.isDedicatedRouterRequired()); Assertions.assertTrue(buildItem.attachedToMainRouter); Assertions.assertEquals("/", buildItem.getVertxRouterPath()); @@ -80,7 +87,7 @@ void testResolvePathWithSlashEmpty() { @Test void testResolvePathWithSlashWithSlash() { - NonApplicationRootPathBuildItem buildItem = new NonApplicationRootPathBuildItem("/", "/"); + NonApplicationRootPathBuildItem buildItem = new NonApplicationRootPathBuildItem("/", "/", null); Assertions.assertFalse(buildItem.isDedicatedRouterRequired()); Assertions.assertTrue(buildItem.attachedToMainRouter); Assertions.assertEquals("/", buildItem.getVertxRouterPath()); @@ -94,8 +101,155 @@ void testResolvePathWithSlashWithSlash() { @Test void testResolvePathWithSlashWithSlashQWithWildcards() { - NonApplicationRootPathBuildItem buildItem = new NonApplicationRootPathBuildItem("/", "/q"); + NonApplicationRootPathBuildItem buildItem = new NonApplicationRootPathBuildItem("/", "/q", null); Assertions.assertEquals("/q/foo/*", buildItem.resolvePath("foo/*")); Assertions.assertEquals("/foo/*", buildItem.resolvePath("/foo/*")); } + + @Test + void testResolveManagementPathWithRelativeRootPath() { + ManagementInterfaceBuildTimeConfig managementInterfaceBuildTimeConfig = new ManagementInterfaceBuildTimeConfig(); + managementInterfaceBuildTimeConfig.enabled = true; + managementInterfaceBuildTimeConfig.rootPath = "management"; + + LaunchModeBuildItem launchModeBuildItem = new LaunchModeBuildItem(LaunchMode.NORMAL, Optional.empty(), false, + Optional.empty(), false); + + NonApplicationRootPathBuildItem buildItem = new NonApplicationRootPathBuildItem("/", "q", + managementInterfaceBuildTimeConfig.rootPath); + Assertions.assertEquals("/management/", buildItem.getManagementRootPath()); + Assertions.assertEquals("http://0.0.0.0:9000/management/foo", + buildItem.resolveManagementPath("foo", managementInterfaceBuildTimeConfig, launchModeBuildItem)); + Assertions.assertEquals("http://0.0.0.0:9000/management/foo/sub/path", + buildItem.resolveManagementPath("foo/sub/path", managementInterfaceBuildTimeConfig, launchModeBuildItem)); + Assertions.assertEquals("http://0.0.0.0:9000/foo", + buildItem.resolveManagementPath("/foo", managementInterfaceBuildTimeConfig, launchModeBuildItem)); + Assertions.assertEquals("http://0.0.0.0:9000/foo/sub/path", + buildItem.resolveManagementPath("/foo/sub/path", managementInterfaceBuildTimeConfig, launchModeBuildItem)); + Assertions.assertThrows(IllegalArgumentException.class, + () -> buildItem.resolveManagementPath("../foo", managementInterfaceBuildTimeConfig, launchModeBuildItem)); + Assertions.assertThrows(IllegalArgumentException.class, + () -> buildItem.resolveManagementPath("", managementInterfaceBuildTimeConfig, launchModeBuildItem)); + } + + @Test + void testResolveManagementPathWithRelativeRootPathInTestMode() { + ManagementInterfaceBuildTimeConfig managementInterfaceBuildTimeConfig = new ManagementInterfaceBuildTimeConfig(); + managementInterfaceBuildTimeConfig.enabled = true; + managementInterfaceBuildTimeConfig.rootPath = "management"; + + LaunchModeBuildItem launchModeBuildItem = new LaunchModeBuildItem(LaunchMode.NORMAL, Optional.empty(), false, + Optional.empty(), true); + + NonApplicationRootPathBuildItem buildItem = new NonApplicationRootPathBuildItem("/", "q", + managementInterfaceBuildTimeConfig.rootPath); + Assertions.assertEquals("/management/", buildItem.getManagementRootPath()); + Assertions.assertEquals("http://0.0.0.0:9001/management/foo", + buildItem.resolveManagementPath("foo", managementInterfaceBuildTimeConfig, launchModeBuildItem)); + Assertions.assertEquals("http://0.0.0.0:9001/management/foo/sub/path", + buildItem.resolveManagementPath("foo/sub/path", managementInterfaceBuildTimeConfig, launchModeBuildItem)); + Assertions.assertEquals("http://0.0.0.0:9001/foo", + buildItem.resolveManagementPath("/foo", managementInterfaceBuildTimeConfig, launchModeBuildItem)); + Assertions.assertEquals("http://0.0.0.0:9001/foo/sub/path", + buildItem.resolveManagementPath("/foo/sub/path", managementInterfaceBuildTimeConfig, launchModeBuildItem)); + Assertions.assertThrows(IllegalArgumentException.class, + () -> buildItem.resolveManagementPath("../foo", managementInterfaceBuildTimeConfig, launchModeBuildItem)); + Assertions.assertThrows(IllegalArgumentException.class, + () -> buildItem.resolveManagementPath("", managementInterfaceBuildTimeConfig, launchModeBuildItem)); + } + + @Test + void testResolveManagementPathWithRelativeRootPathAndWithManagementDisabled() { + ManagementInterfaceBuildTimeConfig managementInterfaceBuildTimeConfig = new ManagementInterfaceBuildTimeConfig(); + managementInterfaceBuildTimeConfig.enabled = false; + managementInterfaceBuildTimeConfig.rootPath = "management"; + + LaunchModeBuildItem launchModeBuildItem = new LaunchModeBuildItem(LaunchMode.NORMAL, Optional.empty(), false, + Optional.empty(), false); + + NonApplicationRootPathBuildItem buildItem = new NonApplicationRootPathBuildItem("/", "q", null); + + Assertions.assertEquals("/q/", buildItem.getManagementRootPath()); + Assertions.assertEquals("/q/foo", + buildItem.resolveManagementPath("foo", managementInterfaceBuildTimeConfig, launchModeBuildItem)); + Assertions.assertEquals("/q/foo/sub/path", + buildItem.resolveManagementPath("foo/sub/path", managementInterfaceBuildTimeConfig, launchModeBuildItem)); + Assertions.assertEquals("/foo", + buildItem.resolveManagementPath("/foo", managementInterfaceBuildTimeConfig, launchModeBuildItem)); + Assertions.assertEquals("/foo/sub/path", + buildItem.resolveManagementPath("/foo/sub/path", managementInterfaceBuildTimeConfig, launchModeBuildItem)); + Assertions.assertThrows(IllegalArgumentException.class, + () -> buildItem.resolveManagementPath("../foo", managementInterfaceBuildTimeConfig, launchModeBuildItem)); + Assertions.assertThrows(IllegalArgumentException.class, + () -> buildItem.resolveManagementPath("", managementInterfaceBuildTimeConfig, launchModeBuildItem)); + } + + @Test + void testResolveManagementPathWithAbsoluteRootPath() { + ManagementInterfaceBuildTimeConfig managementInterfaceBuildTimeConfig = new ManagementInterfaceBuildTimeConfig(); + managementInterfaceBuildTimeConfig.enabled = true; + managementInterfaceBuildTimeConfig.rootPath = "/management"; + + LaunchModeBuildItem launchModeBuildItem = new LaunchModeBuildItem(LaunchMode.NORMAL, Optional.empty(), false, + Optional.empty(), false); + + NonApplicationRootPathBuildItem buildItem = new NonApplicationRootPathBuildItem("/", "/q", + managementInterfaceBuildTimeConfig.rootPath); + Assertions.assertEquals("/management/", buildItem.getManagementRootPath()); + Assertions.assertEquals("http://0.0.0.0:9000/management/foo", + buildItem.resolveManagementPath("foo", managementInterfaceBuildTimeConfig, launchModeBuildItem)); + Assertions.assertEquals("http://0.0.0.0:9000/management/foo/sub/path", + buildItem.resolveManagementPath("foo/sub/path", managementInterfaceBuildTimeConfig, launchModeBuildItem)); + Assertions.assertEquals("http://0.0.0.0:9000/foo", + buildItem.resolveManagementPath("/foo", managementInterfaceBuildTimeConfig, launchModeBuildItem)); + Assertions.assertEquals("http://0.0.0.0:9000/foo/sub/path", + buildItem.resolveManagementPath("/foo/sub/path", managementInterfaceBuildTimeConfig, launchModeBuildItem)); + Assertions.assertThrows(IllegalArgumentException.class, + () -> buildItem.resolveManagementPath("../foo", managementInterfaceBuildTimeConfig, launchModeBuildItem)); + Assertions.assertThrows(IllegalArgumentException.class, + () -> buildItem.resolveManagementPath("", managementInterfaceBuildTimeConfig, launchModeBuildItem)); + } + + @Test + void testResolveManagementPathWithEmptyRootPath() { + ManagementInterfaceBuildTimeConfig managementInterfaceBuildTimeConfig = new ManagementInterfaceBuildTimeConfig(); + managementInterfaceBuildTimeConfig.enabled = true; + managementInterfaceBuildTimeConfig.rootPath = ""; + + LaunchModeBuildItem launchModeBuildItem = new LaunchModeBuildItem(LaunchMode.NORMAL, Optional.empty(), false, + Optional.empty(), false); + + NonApplicationRootPathBuildItem buildItem = new NonApplicationRootPathBuildItem("/", "/q", + managementInterfaceBuildTimeConfig.rootPath); + Assertions.assertEquals("/", buildItem.getManagementRootPath()); + Assertions.assertEquals("http://0.0.0.0:9000/foo", + buildItem.resolveManagementPath("foo", managementInterfaceBuildTimeConfig, launchModeBuildItem)); + Assertions.assertEquals("http://0.0.0.0:9000/foo/sub/path", + buildItem.resolveManagementPath("foo/sub/path", managementInterfaceBuildTimeConfig, launchModeBuildItem)); + Assertions.assertEquals("http://0.0.0.0:9000/foo", + buildItem.resolveManagementPath("/foo", managementInterfaceBuildTimeConfig, launchModeBuildItem)); + Assertions.assertEquals("http://0.0.0.0:9000/foo/sub/path", + buildItem.resolveManagementPath("/foo/sub/path", managementInterfaceBuildTimeConfig, launchModeBuildItem)); + Assertions.assertThrows(IllegalArgumentException.class, + () -> buildItem.resolveManagementPath("../foo", managementInterfaceBuildTimeConfig, launchModeBuildItem)); + Assertions.assertThrows(IllegalArgumentException.class, + () -> buildItem.resolveManagementPath("", managementInterfaceBuildTimeConfig, launchModeBuildItem)); + } + + @Test + void testResolveManagementPathWithWithWildcards() { + ManagementInterfaceBuildTimeConfig managementInterfaceBuildTimeConfig = new ManagementInterfaceBuildTimeConfig(); + managementInterfaceBuildTimeConfig.enabled = true; + managementInterfaceBuildTimeConfig.rootPath = "/management"; + + LaunchModeBuildItem launchModeBuildItem = new LaunchModeBuildItem(LaunchMode.NORMAL, Optional.empty(), false, + Optional.empty(), false); + + NonApplicationRootPathBuildItem buildItem = new NonApplicationRootPathBuildItem("/", "/q", + managementInterfaceBuildTimeConfig.rootPath); + Assertions.assertEquals("http://0.0.0.0:9000/management/foo/*", + buildItem.resolveManagementPath("foo/*", managementInterfaceBuildTimeConfig, launchModeBuildItem)); + Assertions.assertEquals("http://0.0.0.0:9000/foo/*", + buildItem.resolveManagementPath("/foo/*", managementInterfaceBuildTimeConfig, launchModeBuildItem)); + } } diff --git a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/devconsole/BodyHandlerBean.java b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/devconsole/BodyHandlerBean.java index 116db1c8a43f1..7d355f2aa9a2d 100644 --- a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/devconsole/BodyHandlerBean.java +++ b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/devconsole/BodyHandlerBean.java @@ -8,6 +8,7 @@ import io.quarkus.vertx.http.runtime.HttpBuildTimeConfig; import io.quarkus.vertx.http.runtime.HttpConfiguration; import io.quarkus.vertx.http.runtime.VertxHttpRecorder; +import io.quarkus.vertx.http.runtime.management.ManagementInterfaceBuildTimeConfig; import io.vertx.core.Handler; import io.vertx.ext.web.Router; import io.vertx.ext.web.RoutingContext; @@ -19,7 +20,8 @@ void setup(@Observes Router router) { HttpConfiguration httpConfiguration = new HttpConfiguration(); ConfigInstantiator.handleObject(httpConfiguration); Handler bodyHandler = new VertxHttpRecorder(new HttpBuildTimeConfig(), - new RuntimeValue<>(httpConfiguration)) + new ManagementInterfaceBuildTimeConfig(), + new RuntimeValue<>(httpConfiguration), null) .createBodyHandler(); router.route().order(Integer.MIN_VALUE + 1).handler(new Handler() { @Override diff --git a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/management/ManagementAndRootPathTest.java b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/management/ManagementAndRootPathTest.java new file mode 100644 index 0000000000000..1e9fe401909c9 --- /dev/null +++ b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/management/ManagementAndRootPathTest.java @@ -0,0 +1,82 @@ +package io.quarkus.vertx.http.management; + +import java.util.function.Consumer; + +import javax.inject.Singleton; + +import jakarta.enterprise.event.Observes; + +import org.hamcrest.Matchers; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.builder.BuildChainBuilder; +import io.quarkus.builder.BuildContext; +import io.quarkus.builder.BuildStep; +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.vertx.http.deployment.NonApplicationRootPathBuildItem; +import io.quarkus.vertx.http.deployment.RouteBuildItem; +import io.restassured.RestAssured; +import io.vertx.core.Handler; +import io.vertx.ext.web.RoutingContext; + +public class ManagementAndRootPathTest { + private static final String APP_PROPS = "" + + "quarkus.management.enabled=true\n" + + "quarkus.management.root-path=/management\n"; + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addAsResource(new StringAsset(APP_PROPS), "application.properties") + .addClasses(MyObserver.class)) + .addBuildChainCustomizer(buildCustomizer()); + + static Consumer buildCustomizer() { + return new Consumer() { + @Override + public void accept(BuildChainBuilder builder) { + builder.addBuildStep(new BuildStep() { + @Override + public void execute(BuildContext context) { + NonApplicationRootPathBuildItem buildItem = context.consume(NonApplicationRootPathBuildItem.class); + context.produce(buildItem.routeBuilder() + .management() + .route("management-relative") + .handler(new MyHandler()) + .blockingRoute() + .build()); + } + }).produces(RouteBuildItem.class) + .consumes(NonApplicationRootPathBuildItem.class) + .build(); + } + }; + } + + public static class MyHandler implements Handler { + @Override + public void handle(RoutingContext routingContext) { + routingContext.response() + .setStatusCode(200) + .end(routingContext.request().path()); + } + } + + @Test + public void testNonApplicationEndpointDirect() { + // Note RestAssured knows the path prefix is /api + RestAssured.given().get("http://0.0.0.0:9001/management/management-relative") + .then().statusCode(200).body(Matchers.equalTo("/management/management-relative")); + } + + @Singleton + static class MyObserver { + + void test(@Observes String event) { + //Do Nothing + } + + } +} diff --git a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/management/ManagementWithJksTest.java b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/management/ManagementWithJksTest.java new file mode 100644 index 0000000000000..580fd44b27066 --- /dev/null +++ b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/management/ManagementWithJksTest.java @@ -0,0 +1,89 @@ +package io.quarkus.vertx.http.management; + +import java.io.File; +import java.util.function.Consumer; + +import javax.inject.Singleton; + +import jakarta.enterprise.event.Observes; + +import org.assertj.core.api.Assertions; +import org.hamcrest.Matchers; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.builder.BuildChainBuilder; +import io.quarkus.builder.BuildContext; +import io.quarkus.builder.BuildStep; +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.vertx.http.deployment.NonApplicationRootPathBuildItem; +import io.quarkus.vertx.http.deployment.RouteBuildItem; +import io.restassured.RestAssured; +import io.vertx.core.Handler; +import io.vertx.ext.web.RoutingContext; + +public class ManagementWithJksTest { + private static final String APP_PROPS = "" + + "quarkus.management.enabled=true\n" + + "quarkus.management.root-path=/management\n" + + "quarkus.management.ssl.certificate.key-store-file=server-keystore.jks\n" + + "quarkus.management.ssl.certificate.key-store-password=secret\n"; + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addAsResource(new StringAsset(APP_PROPS), "application.properties") + .addAsResource(new File("src/test/resources/conf/server-keystore.jks"), "server-keystore.jks") + .addClasses(MyObserver.class)) + .addBuildChainCustomizer(buildCustomizer()); + + static Consumer buildCustomizer() { + return new Consumer() { + @Override + public void accept(BuildChainBuilder builder) { + builder.addBuildStep(new BuildStep() { + @Override + public void execute(BuildContext context) { + NonApplicationRootPathBuildItem buildItem = context.consume(NonApplicationRootPathBuildItem.class); + context.produce(buildItem.routeBuilder() + .management() + .route("my-route") + .handler(new MyHandler()) + .blockingRoute() + .build()); + } + }).produces(RouteBuildItem.class) + .consumes(NonApplicationRootPathBuildItem.class) + .build(); + } + }; + } + + public static class MyHandler implements Handler { + @Override + public void handle(RoutingContext rc) { + Assertions.assertThat(rc.request().connection().isSsl()).isTrue(); + Assertions.assertThat(rc.request().isSSL()).isTrue(); + Assertions.assertThat(rc.request().connection().sslSession()).isNotNull(); + rc.response().end("ssl"); + } + } + + @Test + public void testSslWithJks() { + RestAssured.given() + .relaxedHTTPSValidation() + .get("https://0.0.0.0:9001/management/my-route") + .then().statusCode(200).body(Matchers.equalTo("ssl")); + } + + @Singleton + static class MyObserver { + + void test(@Observes String event) { + //Do Nothing + } + + } +} diff --git a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/management/ManagementWithMainServerDisabledTest.java b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/management/ManagementWithMainServerDisabledTest.java new file mode 100644 index 0000000000000..9694283a305a9 --- /dev/null +++ b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/management/ManagementWithMainServerDisabledTest.java @@ -0,0 +1,81 @@ +package io.quarkus.vertx.http.management; + +import java.util.function.Consumer; + +import javax.inject.Singleton; + +import jakarta.enterprise.event.Observes; + +import org.hamcrest.Matchers; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.builder.BuildChainBuilder; +import io.quarkus.builder.BuildContext; +import io.quarkus.builder.BuildStep; +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.vertx.http.deployment.NonApplicationRootPathBuildItem; +import io.quarkus.vertx.http.deployment.RouteBuildItem; +import io.restassured.RestAssured; +import io.vertx.core.Handler; +import io.vertx.ext.web.RoutingContext; + +public class ManagementWithMainServerDisabledTest { + private static final String APP_PROPS = "" + + "quarkus.management.enabled=true\n" + + "quarkus.management.root-path=/management\n" + + "quarkus.http.host-enabled=false\n"; + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addAsResource(new StringAsset(APP_PROPS), "application.properties") + .addClasses(MyObserver.class)) + .addBuildChainCustomizer(buildCustomizer()); + + static Consumer buildCustomizer() { + return new Consumer() { + @Override + public void accept(BuildChainBuilder builder) { + builder.addBuildStep(new BuildStep() { + @Override + public void execute(BuildContext context) { + NonApplicationRootPathBuildItem buildItem = context.consume(NonApplicationRootPathBuildItem.class); + context.produce(buildItem.routeBuilder() + .management() + .route("my-route") + .handler(new MyHandler()) + .blockingRoute() + .build()); + } + }).produces(RouteBuildItem.class) + .consumes(NonApplicationRootPathBuildItem.class) + .build(); + } + }; + } + + public static class MyHandler implements Handler { + @Override + public void handle(RoutingContext rc) { + rc.response().end("ok"); + } + } + + @Test + public void testManagementWithoutMain() { + RestAssured.given() + .get("http://0.0.0.0:9001/management/my-route") + .then().statusCode(200).body(Matchers.equalTo("ok")); + } + + @Singleton + static class MyObserver { + + void test(@Observes String event) { + //Do Nothing + } + + } +} diff --git a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/management/ManagementWithP12Test.java b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/management/ManagementWithP12Test.java new file mode 100644 index 0000000000000..0f730779f0856 --- /dev/null +++ b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/management/ManagementWithP12Test.java @@ -0,0 +1,89 @@ +package io.quarkus.vertx.http.management; + +import java.io.File; +import java.util.function.Consumer; + +import javax.inject.Singleton; + +import jakarta.enterprise.event.Observes; + +import org.assertj.core.api.Assertions; +import org.hamcrest.Matchers; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.builder.BuildChainBuilder; +import io.quarkus.builder.BuildContext; +import io.quarkus.builder.BuildStep; +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.vertx.http.deployment.NonApplicationRootPathBuildItem; +import io.quarkus.vertx.http.deployment.RouteBuildItem; +import io.restassured.RestAssured; +import io.vertx.core.Handler; +import io.vertx.ext.web.RoutingContext; + +public class ManagementWithP12Test { + private static final String APP_PROPS = "" + + "quarkus.management.enabled=true\n" + + "quarkus.management.root-path=/management\n" + + "quarkus.management.ssl.certificate.key-store-file=server-keystore.p12\n" + + "quarkus.management.ssl.certificate.key-store-password=secret\n"; + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addAsResource(new StringAsset(APP_PROPS), "application.properties") + .addAsResource(new File("src/test/resources/conf/server-keystore.p12"), "server-keystore.p12") + .addClasses(MyObserver.class)) + .addBuildChainCustomizer(buildCustomizer()); + + static Consumer buildCustomizer() { + return new Consumer() { + @Override + public void accept(BuildChainBuilder builder) { + builder.addBuildStep(new BuildStep() { + @Override + public void execute(BuildContext context) { + NonApplicationRootPathBuildItem buildItem = context.consume(NonApplicationRootPathBuildItem.class); + context.produce(buildItem.routeBuilder() + .management() + .route("my-route") + .handler(new MyHandler()) + .blockingRoute() + .build()); + } + }).produces(RouteBuildItem.class) + .consumes(NonApplicationRootPathBuildItem.class) + .build(); + } + }; + } + + public static class MyHandler implements Handler { + @Override + public void handle(RoutingContext rc) { + Assertions.assertThat(rc.request().connection().isSsl()).isTrue(); + Assertions.assertThat(rc.request().isSSL()).isTrue(); + Assertions.assertThat(rc.request().connection().sslSession()).isNotNull(); + rc.response().end("ssl"); + } + } + + @Test + public void testSslWithP12() { + RestAssured.given() + .relaxedHTTPSValidation() + .get("https://0.0.0.0:9001/management/my-route") + .then().statusCode(200).body(Matchers.equalTo("ssl")); + } + + @Singleton + static class MyObserver { + + void test(@Observes String event) { + //Do Nothing + } + + } +} diff --git a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/management/ManagementWithPemTest.java b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/management/ManagementWithPemTest.java new file mode 100644 index 0000000000000..ef4b24d1638a1 --- /dev/null +++ b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/management/ManagementWithPemTest.java @@ -0,0 +1,90 @@ +package io.quarkus.vertx.http.management; + +import java.io.File; +import java.util.function.Consumer; + +import javax.inject.Singleton; + +import jakarta.enterprise.event.Observes; + +import org.assertj.core.api.Assertions; +import org.hamcrest.Matchers; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.builder.BuildChainBuilder; +import io.quarkus.builder.BuildContext; +import io.quarkus.builder.BuildStep; +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.vertx.http.deployment.NonApplicationRootPathBuildItem; +import io.quarkus.vertx.http.deployment.RouteBuildItem; +import io.restassured.RestAssured; +import io.vertx.core.Handler; +import io.vertx.ext.web.RoutingContext; + +public class ManagementWithPemTest { + private static final String APP_PROPS = "" + + "quarkus.management.enabled=true\n" + + "quarkus.management.root-path=/management\n" + + "quarkus.management.ssl.certificate.files=server-cert.pem\n" + + "quarkus.management.ssl.certificate.key-files=server-key.pem\n"; + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addAsResource(new StringAsset(APP_PROPS), "application.properties") + .addAsResource(new File("src/test/resources/conf/server-key.pem"), "server-key.pem") + .addAsResource(new File("src/test/resources/conf/server-cert.pem"), "server-cert.pem") + .addClasses(MyObserver.class)) + .addBuildChainCustomizer(buildCustomizer()); + + static Consumer buildCustomizer() { + return new Consumer() { + @Override + public void accept(BuildChainBuilder builder) { + builder.addBuildStep(new BuildStep() { + @Override + public void execute(BuildContext context) { + NonApplicationRootPathBuildItem buildItem = context.consume(NonApplicationRootPathBuildItem.class); + context.produce(buildItem.routeBuilder() + .management() + .route("my-route") + .handler(new MyHandler()) + .blockingRoute() + .build()); + } + }).produces(RouteBuildItem.class) + .consumes(NonApplicationRootPathBuildItem.class) + .build(); + } + }; + } + + public static class MyHandler implements Handler { + @Override + public void handle(RoutingContext rc) { + Assertions.assertThat(rc.request().connection().isSsl()).isTrue(); + Assertions.assertThat(rc.request().isSSL()).isTrue(); + Assertions.assertThat(rc.request().connection().sslSession()).isNotNull(); + rc.response().end("ssl"); + } + } + + @Test + public void testSslWithPem() { + RestAssured.given() + .relaxedHTTPSValidation() + .get("https://0.0.0.0:9001/management/my-route") + .then().statusCode(200).body(Matchers.equalTo("ssl")); + } + + @Singleton + static class MyObserver { + + void test(@Observes String event) { + //Do Nothing + } + + } +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/ForwardedProxyHandler.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/ForwardedProxyHandler.java index 19ae4a6270a26..6ba0cb9d1cc07 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/ForwardedProxyHandler.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/ForwardedProxyHandler.java @@ -22,7 +22,7 @@ * Restricts who can send `Forwarded`, `X-Forwarded` or `X-Forwarded-*` headers to trusted proxies * configured through {@link ProxyConfig#trustedProxies}. */ -class ForwardedProxyHandler implements Handler { +public class ForwardedProxyHandler implements Handler { private static final Logger LOGGER = Logger.getLogger(ForwardedProxyHandler.class.getName()); @@ -34,7 +34,7 @@ class ForwardedProxyHandler implements Handler { private final ForwardingProxyOptions forwardingProxyOptions; - ForwardedProxyHandler(TrustedProxyCheck.TrustedProxyCheckBuilder proxyCheckBuilder, + public ForwardedProxyHandler(TrustedProxyCheck.TrustedProxyCheckBuilder proxyCheckBuilder, Supplier vertx, Handler delegate, ForwardingProxyOptions forwardingProxyOptions) { this.proxyCheckBuilder = proxyCheckBuilder; diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/ForwardedServerRequestWrapper.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/ForwardedServerRequestWrapper.java index 7e93a5a193aad..d4da4d5c7f44d 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/ForwardedServerRequestWrapper.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/ForwardedServerRequestWrapper.java @@ -29,7 +29,7 @@ import io.vertx.core.net.NetSocket; import io.vertx.core.net.SocketAddress; -class ForwardedServerRequestWrapper extends HttpServerRequestWrapper implements HttpServerRequest { +public class ForwardedServerRequestWrapper extends HttpServerRequestWrapper implements HttpServerRequest { private final ForwardedParser forwardedParser; private boolean modified; @@ -40,7 +40,7 @@ class ForwardedServerRequestWrapper extends HttpServerRequestWrapper implements private String uri; private String absoluteURI; - ForwardedServerRequestWrapper(HttpServerRequest request, ForwardingProxyOptions forwardingProxyOptions, + public ForwardedServerRequestWrapper(HttpServerRequest request, ForwardingProxyOptions forwardingProxyOptions, TrustedProxyCheck trustedProxyCheck) { super((HttpServerRequestInternal) request); forwardedParser = new ForwardedParser(delegate, forwardingProxyOptions, trustedProxyCheck); diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/ForwardingProxyOptions.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/ForwardingProxyOptions.java index be0e6604859d8..e7c0cf032a6fb 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/ForwardingProxyOptions.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/ForwardingProxyOptions.java @@ -7,14 +7,14 @@ import io.quarkus.vertx.http.runtime.TrustedProxyCheck.TrustedProxyCheckPart; public class ForwardingProxyOptions { - final boolean proxyAddressForwarding; + public final boolean proxyAddressForwarding; final boolean allowForwarded; final boolean allowXForwarded; final boolean enableForwardedHost; final boolean enableForwardedPrefix; final AsciiString forwardedHostHeader; final AsciiString forwardedPrefixHeader; - final TrustedProxyCheckBuilder trustedProxyCheckBuilder; + public final TrustedProxyCheckBuilder trustedProxyCheckBuilder; public ForwardingProxyOptions(final boolean proxyAddressForwarding, final boolean allowForwarded, @@ -34,18 +34,18 @@ public ForwardingProxyOptions(final boolean proxyAddressForwarding, this.trustedProxyCheckBuilder = trustedProxyCheckBuilder; } - public static ForwardingProxyOptions from(HttpConfiguration httpConfiguration) { - final boolean proxyAddressForwarding = httpConfiguration.proxy.proxyAddressForwarding; - final boolean allowForwarded = httpConfiguration.proxy.allowForwarded; - final boolean allowXForwarded = httpConfiguration.proxy.allowXForwarded.orElse(!allowForwarded); + public static ForwardingProxyOptions from(ProxyConfig proxy) { + final boolean proxyAddressForwarding = proxy.proxyAddressForwarding; + final boolean allowForwarded = proxy.allowForwarded; + final boolean allowXForwarded = proxy.allowXForwarded.orElse(!allowForwarded); - final boolean enableForwardedHost = httpConfiguration.proxy.enableForwardedHost; - final boolean enableForwardedPrefix = httpConfiguration.proxy.enableForwardedPrefix; - final AsciiString forwardedPrefixHeader = AsciiString.cached(httpConfiguration.proxy.forwardedPrefixHeader); - final AsciiString forwardedHostHeader = AsciiString.cached(httpConfiguration.proxy.forwardedHostHeader); + final boolean enableForwardedHost = proxy.enableForwardedHost; + final boolean enableForwardedPrefix = proxy.enableForwardedPrefix; + final AsciiString forwardedPrefixHeader = AsciiString.cached(proxy.forwardedPrefixHeader); + final AsciiString forwardedHostHeader = AsciiString.cached(proxy.forwardedHostHeader); - final List parts = httpConfiguration.proxy.trustedProxies - .isPresent() ? List.copyOf(httpConfiguration.proxy.trustedProxies.get()) : List.of(); + final List parts = proxy.trustedProxies + .isPresent() ? List.copyOf(proxy.trustedProxies.get()) : List.of(); final var proxyCheckBuilder = (!allowXForwarded && !allowForwarded) || parts.isEmpty() ? null : TrustedProxyCheckBuilder.builder(parts); diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/HttpBuildTimeConfig.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/HttpBuildTimeConfig.java index 0443874178509..e672c5e7e0769 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/HttpBuildTimeConfig.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/HttpBuildTimeConfig.java @@ -52,6 +52,9 @@ public class HttpBuildTimeConfig { * * `${quarkus.http.root-path}` -> Setting this path to the same value as HTTP root path disables * this root path. All extension-provided endpoints will be served from `${quarkus.http.root-path}`. * + * If the management interface is enabled, the root path for the endpoints exposed on the management interface + * is configured using the `quarkus.management.root-path` property instead of this property. + * * @asciidoclet */ @ConfigItem(defaultValue = "q") diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/ResumingRequestWrapper.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/ResumingRequestWrapper.java index f30dbe982d058..4375618f3201d 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/ResumingRequestWrapper.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/ResumingRequestWrapper.java @@ -7,11 +7,11 @@ import io.vertx.core.http.impl.HttpServerRequestInternal; import io.vertx.core.http.impl.HttpServerRequestWrapper; -class ResumingRequestWrapper extends HttpServerRequestWrapper { +public class ResumingRequestWrapper extends HttpServerRequestWrapper { private boolean userSetState; - ResumingRequestWrapper(HttpServerRequest request) { + public ResumingRequestWrapper(HttpServerRequest request) { super((HttpServerRequestInternal) request); } diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/TrustedProxyCheck.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/TrustedProxyCheck.java index 1c33d410a3acc..2fa0f75f678df 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/TrustedProxyCheck.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/TrustedProxyCheck.java @@ -10,7 +10,7 @@ import io.vertx.core.net.SocketAddress; -interface TrustedProxyCheck { +public interface TrustedProxyCheck { static TrustedProxyCheck allowAll() { return new TrustedProxyCheck() { diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/VertxHttpRecorder.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/VertxHttpRecorder.java index a3516d34f8d95..48650f63b328f 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/VertxHttpRecorder.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/VertxHttpRecorder.java @@ -1,23 +1,17 @@ package io.quarkus.vertx.http.runtime; import static io.quarkus.vertx.core.runtime.context.VertxContextSafetyToggle.setContextSafe; -import static io.quarkus.vertx.core.runtime.context.VertxContextSafetyToggle.setCurrentContextSafe; -import static io.quarkus.vertx.http.runtime.TrustedProxyCheck.allowAll; import java.io.File; import java.io.IOException; -import java.io.InputStream; import java.net.BindException; import java.net.URI; import java.net.URISyntaxException; -import java.nio.file.Files; -import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; -import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.Optional; @@ -26,7 +20,6 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; -import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.BiConsumer; import java.util.function.BiFunction; @@ -50,8 +43,6 @@ import io.quarkus.arc.InstanceHandle; import io.quarkus.arc.runtime.BeanContainer; import io.quarkus.bootstrap.runner.Timing; -import io.quarkus.credentials.CredentialsProvider; -import io.quarkus.credentials.runtime.CredentialsProviderFinder; import io.quarkus.dev.spi.DevModeType; import io.quarkus.dev.spi.HotReplacementContext; import io.quarkus.netty.runtime.virtual.VirtualAddress; @@ -64,15 +55,12 @@ import io.quarkus.runtime.ShutdownContext; import io.quarkus.runtime.annotations.Recorder; import io.quarkus.runtime.configuration.ConfigInstantiator; -import io.quarkus.runtime.configuration.ConfigurationException; import io.quarkus.runtime.configuration.MemorySize; import io.quarkus.runtime.shutdown.ShutdownConfig; -import io.quarkus.runtime.util.ClassPathUtils; import io.quarkus.vertx.core.runtime.VertxCoreRecorder; import io.quarkus.vertx.core.runtime.config.VertxConfiguration; import io.quarkus.vertx.http.HttpServerOptionsCustomizer; import io.quarkus.vertx.http.runtime.HttpConfiguration.InsecureRequests; -import io.quarkus.vertx.http.runtime.TrustedProxyCheck.TrustedProxyCheckBuilder; import io.quarkus.vertx.http.runtime.devmode.RemoteSyncHandler; import io.quarkus.vertx.http.runtime.devmode.VertxHttpHotReplacementSetup; import io.quarkus.vertx.http.runtime.filters.Filter; @@ -83,6 +71,10 @@ import io.quarkus.vertx.http.runtime.filters.accesslog.AccessLogReceiver; import io.quarkus.vertx.http.runtime.filters.accesslog.DefaultAccessLogReceiver; import io.quarkus.vertx.http.runtime.filters.accesslog.JBossLoggingAccessLogReceiver; +import io.quarkus.vertx.http.runtime.management.ManagementInterfaceBuildTimeConfig; +import io.quarkus.vertx.http.runtime.management.ManagementInterfaceConfiguration; +import io.quarkus.vertx.http.runtime.options.HttpServerCommonHandlers; +import io.quarkus.vertx.http.runtime.options.HttpServerOptionsUtils; import io.smallrye.common.vertx.VertxContext; import io.vertx.core.AbstractVerticle; import io.vertx.core.AsyncResult; @@ -92,7 +84,6 @@ import io.vertx.core.Promise; import io.vertx.core.Verticle; import io.vertx.core.Vertx; -import io.vertx.core.buffer.Buffer; import io.vertx.core.http.Cookie; import io.vertx.core.http.CookieSameSite; import io.vertx.core.http.HttpConnection; @@ -101,15 +92,11 @@ import io.vertx.core.http.HttpServer; import io.vertx.core.http.HttpServerOptions; import io.vertx.core.http.HttpServerRequest; -import io.vertx.core.http.HttpVersion; import io.vertx.core.http.impl.Http1xServerConnection; import io.vertx.core.impl.ContextInternal; import io.vertx.core.impl.EventLoopContext; import io.vertx.core.impl.Utils; import io.vertx.core.impl.VertxInternal; -import io.vertx.core.net.JdkSSLEngineOptions; -import io.vertx.core.net.KeyStoreOptions; -import io.vertx.core.net.PemKeyCertOptions; import io.vertx.core.net.SocketAddress; import io.vertx.core.net.impl.ConnectionBase; import io.vertx.core.net.impl.VertxHandler; @@ -117,6 +104,7 @@ import io.vertx.ext.web.Router; import io.vertx.ext.web.RoutingContext; import io.vertx.ext.web.handler.BodyHandler; +import io.vertx.ext.web.handler.CorsHandler; @Recorder public class VertxHttpRecorder { @@ -160,6 +148,8 @@ public class VertxHttpRecorder { private static volatile int actualHttpPort = -1; private static volatile int actualHttpsPort = -1; + private static volatile int actualManagementPort = -1; + public static final String GET = "GET"; private static final Handler ACTUAL_ROOT = new Handler() { @@ -206,12 +196,25 @@ private boolean uriValid(HttpServerRequest httpServerRequest) { } } }; + private static HttpServerOptions httpMainSslServerOptions; + private static HttpServerOptions httpMainServerOptions; + private static HttpServerOptions httpMainDomainSocketOptions; + private static HttpServerOptions httpManagementServerOptions; final HttpBuildTimeConfig httpBuildTimeConfig; + final ManagementInterfaceBuildTimeConfig managementBuildTimeConfig; final RuntimeValue httpConfiguration; - public VertxHttpRecorder(HttpBuildTimeConfig httpBuildTimeConfig, RuntimeValue httpConfiguration) { + final RuntimeValue managementConfiguration; + private static volatile Handler managementRouter; + + public VertxHttpRecorder(HttpBuildTimeConfig httpBuildTimeConfig, + ManagementInterfaceBuildTimeConfig managementBuildTimeConfig, + RuntimeValue httpConfiguration, + RuntimeValue managementConfiguration) { this.httpBuildTimeConfig = httpBuildTimeConfig; this.httpConfiguration = httpConfiguration; + this.managementBuildTimeConfig = managementBuildTimeConfig; + this.managementConfiguration = managementConfiguration; } public static void setHotReplacement(Handler handler, HotReplacementContext hrc) { @@ -253,8 +256,12 @@ public static void startServerAfterFailedStart() { try { HttpBuildTimeConfig buildConfig = new HttpBuildTimeConfig(); ConfigInstantiator.handleObject(buildConfig); + ManagementInterfaceBuildTimeConfig managementBuildTimeConfig = new ManagementInterfaceBuildTimeConfig(); + ConfigInstantiator.handleObject(managementBuildTimeConfig); HttpConfiguration config = new HttpConfiguration(); ConfigInstantiator.handleObject(config); + ManagementInterfaceConfiguration managementConfig = new ManagementInterfaceConfiguration(); + ConfigInstantiator.handleObject(managementConfig); if (config.host == null) { //HttpHostConfigSource does not come into play here config.host = "localhost"; @@ -274,12 +281,13 @@ public static void startServerAfterFailedStart() { rootHandler = root; //we can't really do - doServerStart(vertx, buildConfig, config, LaunchMode.DEVELOPMENT, new Supplier() { - @Override - public Integer get() { - return ProcessorInfo.availableProcessors(); //this is dev mode, so the number of IO threads not always being 100% correct does not really matter in this case - } - }, null, false); + doServerStart(vertx, buildConfig, managementBuildTimeConfig, null, config, managementConfig, LaunchMode.DEVELOPMENT, + new Supplier() { + @Override + public Integer get() { + return ProcessorInfo.availableProcessors(); //this is dev mode, so the number of IO threads not always being 100% correct does not really matter in this case + } + }, null, false); } catch (Exception e) { throw new RuntimeException(e); } @@ -320,11 +328,15 @@ public void startServer(Supplier vertx, ShutdownContext shutdown, }); } HttpConfiguration httpConfiguration = this.httpConfiguration.getValue(); - if (startSocket && (httpConfiguration.hostEnabled || httpConfiguration.domainSocketEnabled)) { + ManagementInterfaceConfiguration managementConfig = this.managementConfiguration == null ? null + : this.managementConfiguration.getValue(); + if (startSocket && (httpConfiguration.hostEnabled || httpConfiguration.domainSocketEnabled + || managementConfig.hostEnabled || managementConfig.domainSocketEnabled)) { // Start the server if (closeTask == null) { - doServerStart(vertx.get(), httpBuildTimeConfig, httpConfiguration, launchMode, ioThreads, - websocketSubProtocols, auxiliaryApplication); + doServerStart(vertx.get(), httpBuildTimeConfig, managementBuildTimeConfig, managementRouter, + httpConfiguration, managementConfig, launchMode, ioThreads, websocketSubProtocols, + auxiliaryApplication); if (launchMode != LaunchMode.DEVELOPMENT) { shutdown.addShutdownTask(closeTask); } else { @@ -348,7 +360,7 @@ public void finalizeRouter(BeanContainer container, Consumer defaultRoute List filterList, Supplier vertx, LiveReloadConfig liveReloadConfig, Optional> mainRouterRuntimeValue, RuntimeValue httpRouterRuntimeValue, RuntimeValue mutinyRouter, - RuntimeValue frameworkRouter, + RuntimeValue frameworkRouter, RuntimeValue managementRouter, String rootPath, String nonRootPath, LaunchMode launchMode, boolean requireBodyHandler, Handler bodyHandler, @@ -388,18 +400,7 @@ public void finalizeRouter(BeanContainer container, Consumer defaultRoute defaultRouteHandler.accept(httpRouteRouter.route().order(DEFAULT_ROUTE_ORDER)); } - if (httpBuildTimeConfig.enableCompression) { - httpRouteRouter.route().order(0).handler(new Handler() { - @Override - public void handle(RoutingContext ctx) { - // Add "Content-Encoding: identity" header that disables the compression - // This header can be removed to enable the compression - ctx.response().putHeader(HttpHeaders.CONTENT_ENCODING, HttpHeaders.IDENTITY); - ctx.next(); - } - }); - } - + applyCompression(httpBuildTimeConfig.enableCompression, httpRouteRouter); httpRouteRouter.route().last().failureHandler( new QuarkusErrorHandler(launchMode.isDevOrTest(), httpConfiguration.unhandledErrorContentTypeDefault)); @@ -415,101 +416,12 @@ public void handle(RoutingContext routingContext) { }); } - if (httpConfiguration.limits.maxBodySize.isPresent()) { - long limit = httpConfiguration.limits.maxBodySize.get().asLongValue(); - Long limitObj = limit; - httpRouteRouter.route().order(-2).handler(new Handler() { - @Override - public void handle(RoutingContext event) { - String lengthString = event.request().headers().get(HttpHeaderNames.CONTENT_LENGTH); - - if (lengthString != null) { - long length = Long.parseLong(lengthString); - if (length > limit) { - event.response().headers().add(HttpHeaderNames.CONNECTION, "close"); - event.response().setStatusCode(HttpResponseStatus.REQUEST_ENTITY_TOO_LARGE.code()); - event.response().endHandler(new Handler() { - @Override - public void handle(Void e) { - event.request().connection().close(); - } - }); - event.response().end(); - return; - } - } else { - event.put(MAX_REQUEST_SIZE_KEY, limitObj); - } - event.next(); - } - }); - } + HttpServerCommonHandlers.enforceMaxBodySize(httpConfiguration.limits, httpRouteRouter); // Filter Configuration per path var filtersInConfig = httpConfiguration.filter; - if (!filtersInConfig.isEmpty()) { - for (var entry : filtersInConfig.entrySet()) { - var filterConfig = entry.getValue(); - var matches = filterConfig.matches; - var order = filterConfig.order.orElse(Integer.MIN_VALUE); - var methods = filterConfig.methods; - var headers = filterConfig.header; - if (methods.isEmpty()) { - httpRouteRouter.routeWithRegex(matches) - .order(order) - .handler(new Handler() { - @Override - public void handle(RoutingContext event) { - event.response().headers().setAll(headers); - event.next(); - } - }); - } else { - for (var method : methods.get()) { - httpRouteRouter.routeWithRegex(HttpMethod.valueOf(method.toUpperCase(Locale.ROOT)), matches) - .order(order) - .handler(new Handler() { - @Override - public void handle(RoutingContext event) { - event.response().headers().setAll(headers); - event.next(); - } - }); - } - } - } - } + HttpServerCommonHandlers.applyFilters(filtersInConfig, httpRouteRouter); // Headers sent on any request, regardless of the response - Map headers = httpConfiguration.header; - if (!headers.isEmpty()) { - // Creates a handler for each header entry - for (Map.Entry entry : headers.entrySet()) { - var name = entry.getKey(); - var config = entry.getValue(); - if (config.methods.isEmpty()) { - httpRouteRouter.route(config.path) - .order(Integer.MIN_VALUE) - .handler(new Handler() { - @Override - public void handle(RoutingContext event) { - event.response().headers().set(name, config.value); - event.next(); - } - }); - } else { - for (String method : config.methods.get()) { - httpRouteRouter.route(HttpMethod.valueOf(method.toUpperCase(Locale.ROOT)), config.path) - .order(Integer.MIN_VALUE) - .handler(new Handler() { - @Override - public void handle(RoutingContext event) { - event.response().headers().add(name, config.value); - event.next(); - } - }); - } - } - } - } + HttpServerCommonHandlers.applyHeaders(httpConfiguration.header, httpRouteRouter); Handler root; if (rootPath.equals("/")) { @@ -529,6 +441,7 @@ public void handle(RoutingContext event) { Router mainRouter = mainRouterRuntimeValue.isPresent() ? mainRouterRuntimeValue.get().getValue() : Router.router(vertx.get()); mainRouter.mountSubRouter(rootPath, httpRouteRouter); + if (hotReplacementHandler != null) { ClassLoader currentCl = Thread.currentThread().getContextClassLoader(); mainRouter.route().order(Integer.MIN_VALUE).handler(new Handler() { @@ -542,25 +455,9 @@ public void handle(RoutingContext event) { root = mainRouter; } - if (httpConfiguration.proxy.proxyAddressForwarding) { - warnIfProxyAddressForwardingAllowedWithMultipleHeaders(httpConfiguration); - final ForwardingProxyOptions forwardingProxyOptions = ForwardingProxyOptions.from(httpConfiguration); - final TrustedProxyCheckBuilder proxyCheckBuilder = forwardingProxyOptions.trustedProxyCheckBuilder; - Handler delegate = root; - if (proxyCheckBuilder == null) { - // no proxy check => we do not restrict who can send `X-Forwarded` or `X-Forwarded-*` headers - final TrustedProxyCheck allowAllProxyCheck = allowAll(); - root = new Handler() { - @Override - public void handle(HttpServerRequest event) { - delegate.handle(new ForwardedServerRequestWrapper(event, forwardingProxyOptions, allowAllProxyCheck)); - } - }; - } else { - // restrict who can send `Forwarded`, `X-Forwarded` or `X-Forwarded-*` headers - root = new ForwardedProxyHandler(proxyCheckBuilder, vertx, delegate, forwardingProxyOptions); - } - } + warnIfProxyAddressForwardingAllowedWithMultipleHeaders(httpConfiguration.proxy); + root = HttpServerCommonHandlers.applyProxy(httpConfiguration.proxy, root, vertx); + boolean quarkusWrapperNeeded = false; if (shutdownConfig.isShutdownTimeoutSet()) { @@ -614,27 +511,7 @@ public void handle(HttpServerRequest event) { } Handler delegate = root; - root = new Handler() { - @Override - public void handle(HttpServerRequest event) { - if (!VertxContext.isOnDuplicatedContext()) { - // Vert.x should call us on a duplicated context. - // But in the case of pipelined requests, it does not. - // See https://github.com/quarkusio/quarkus/issues/24626. - Context context = VertxContext.createNewDuplicatedContext(); - context.runOnContext(new Handler() { - @Override - public void handle(Void x) { - setCurrentContextSafe(true); - delegate.handle(new ResumingRequestWrapper(event)); - } - }); - } else { - setCurrentContextSafe(true); - delegate.handle(new ResumingRequestWrapper(event)); - } - } - }; + root = HttpServerCommonHandlers.enforceDuplicatedContext(delegate); if (httpConfiguration.recordRequestStartTime) { httpRouteRouter.route().order(Integer.MIN_VALUE).handler(new Handler() { @Override @@ -649,13 +526,44 @@ public void handle(RoutingContext event) { root = remoteSyncHandler = new RemoteSyncHandler(liveReloadConfig.password.get(), root, hotReplacementContext); } rootHandler = root; + + if (managementRouter != null && managementRouter.getValue() != null) { + // Add body handler and cors handler + var mr = managementRouter.getValue(); + mr.route().order(Integer.MIN_VALUE).handler(createBodyHandlerForManagementInterface()); + // We can use "*" here as the management interface is not expected to be used publicly. + mr.route().order(Integer.MIN_VALUE).handler(CorsHandler.create().addOrigin("*")); + + HttpServerCommonHandlers.applyFilters(managementConfiguration.getValue().filter, mr); + HttpServerCommonHandlers.applyHeaders(managementConfiguration.getValue().header, mr); + HttpServerCommonHandlers.enforceMaxBodySize(managementConfiguration.getValue().limits, mr); + applyCompression(managementBuildTimeConfig.enableCompression, mr); + + Handler handler = HttpServerCommonHandlers.enforceDuplicatedContext(mr); + handler = HttpServerCommonHandlers.applyProxy(managementConfiguration.getValue().proxy, handler, vertx); + + VertxHttpRecorder.managementRouter = handler; + } + } + + private void applyCompression(boolean enableCompression, Router httpRouteRouter) { + if (enableCompression) { + httpRouteRouter.route().order(0).handler(new Handler() { + @Override + public void handle(RoutingContext ctx) { + // Add "Content-Encoding: identity" header that disables the compression + // This header can be removed to enable the compression + ctx.response().putHeader(HttpHeaders.CONTENT_ENCODING, HttpHeaders.IDENTITY); + ctx.next(); + } + }); + } } - private void warnIfProxyAddressForwardingAllowedWithMultipleHeaders(HttpConfiguration httpConfiguration) { - ProxyConfig proxyConfig = httpConfiguration.proxy; + private void warnIfProxyAddressForwardingAllowedWithMultipleHeaders(ProxyConfig proxyConfig) { boolean proxyAddressForwardingActivated = proxyConfig.proxyAddressForwarding; boolean forwardedActivated = proxyConfig.allowForwarded; - boolean xForwardedActivated = httpConfiguration.proxy.allowXForwarded.orElse(!forwardedActivated); + boolean xForwardedActivated = proxyConfig.allowXForwarded.orElse(!forwardedActivated); if (proxyAddressForwardingActivated && forwardedActivated && xForwardedActivated) { LOGGER.warn( @@ -666,16 +574,95 @@ private void warnIfProxyAddressForwardingAllowedWithMultipleHeaders(HttpConfigur } } - private static void doServerStart(Vertx vertx, HttpBuildTimeConfig httpBuildTimeConfig, - HttpConfiguration httpConfiguration, LaunchMode launchMode, - Supplier eventLoops, List websocketSubProtocols, boolean auxiliaryApplication) throws IOException { + private static CompletableFuture initializeManagementInterfaceWithDomainSocket(Vertx vertx, + ManagementInterfaceBuildTimeConfig managementBuildTimeConfig, Handler managementRouter, + ManagementInterfaceConfiguration managementConfig, + List websocketSubProtocols) { + CompletableFuture managementInterfaceDomainSocketFuture = new CompletableFuture<>(); + if (!managementBuildTimeConfig.enabled || managementRouter == null || managementConfig == null) { + managementInterfaceDomainSocketFuture.complete(null); + return managementInterfaceDomainSocketFuture; + } + + HttpServerOptions domainSocketOptionsForManagement = createDomainSocketOptionsForManagementInterface( + managementBuildTimeConfig, managementConfig, + websocketSubProtocols); + if (domainSocketOptionsForManagement != null) { + vertx.createHttpServer(domainSocketOptionsForManagement) + .requestHandler(managementRouter) + .listen(ar -> { + if (ar.failed()) { + managementInterfaceDomainSocketFuture.completeExceptionally( + new IllegalStateException( + "Unable to start the management interface on the " + + domainSocketOptionsForManagement.getHost() + " domain socket", + ar.cause())); + } else { + managementInterfaceDomainSocketFuture.complete(ar.result()); + } + }); + } else { + managementInterfaceDomainSocketFuture.complete(null); + } + return managementInterfaceDomainSocketFuture; + } + + private static CompletableFuture initializeManagementInterface(Vertx vertx, + ManagementInterfaceBuildTimeConfig managementBuildTimeConfig, Handler managementRouter, + ManagementInterfaceConfiguration managementConfig, + LaunchMode launchMode, + List websocketSubProtocols) throws IOException { + httpManagementServerOptions = null; + CompletableFuture managementInterfaceFuture = new CompletableFuture<>(); + if (!managementBuildTimeConfig.enabled || managementRouter == null || managementConfig == null) { + managementInterfaceFuture.complete(null); + return managementInterfaceFuture; + } + + HttpServerOptions httpServerOptionsForManagement = createHttpServerOptionsForManagementInterface( + managementBuildTimeConfig, managementConfig, launchMode, + websocketSubProtocols); + httpManagementServerOptions = HttpServerOptionsUtils.createSslOptionsForManagementInterface( + managementBuildTimeConfig, managementConfig, launchMode, + websocketSubProtocols); + if (httpManagementServerOptions != null && httpManagementServerOptions.getKeyCertOptions() == null) { + httpManagementServerOptions = httpServerOptionsForManagement; + } + + if (httpManagementServerOptions != null) { + vertx.createHttpServer(httpManagementServerOptions) + .requestHandler(managementRouter) + .listen(ar -> { + if (ar.failed()) { + managementInterfaceFuture.completeExceptionally( + new IllegalStateException("Unable to start the management interface", ar.cause())); + } else { + actualManagementPort = ar.result().actualPort(); + managementInterfaceFuture.complete(ar.result()); + } + }); + } else { + managementInterfaceFuture.complete(null); + } + return managementInterfaceFuture; + } + + private static CompletableFuture initializeMainHttpServer(Vertx vertx, HttpBuildTimeConfig httpBuildTimeConfig, + HttpConfiguration httpConfiguration, + LaunchMode launchMode, + Supplier eventLoops, List websocketSubProtocols) throws IOException { + + if (!httpConfiguration.hostEnabled) { + return CompletableFuture.completedFuture(null); + } // Http server configuration - HttpServerOptions httpServerOptions = createHttpServerOptions(httpBuildTimeConfig, httpConfiguration, launchMode, + httpMainServerOptions = createHttpServerOptions(httpBuildTimeConfig, httpConfiguration, launchMode, websocketSubProtocols); - HttpServerOptions domainSocketOptions = createDomainSocketOptions(httpBuildTimeConfig, httpConfiguration, + httpMainDomainSocketOptions = createDomainSocketOptions(httpBuildTimeConfig, httpConfiguration, websocketSubProtocols); - HttpServerOptions tmpSslConfig = createSslOptions(httpBuildTimeConfig, httpConfiguration, launchMode, + HttpServerOptions tmpSslConfig = HttpServerOptionsUtils.createSslOptions(httpBuildTimeConfig, httpConfiguration, + launchMode, websocketSubProtocols); // Customize @@ -684,14 +671,14 @@ private static void doServerStart(Vertx vertx, HttpBuildTimeConfig httpBuildTime .listAll(HttpServerOptionsCustomizer.class); for (InstanceHandle instance : instances) { HttpServerOptionsCustomizer customizer = instance.get(); - if (httpServerOptions != null) { - customizer.customizeHttpServer(httpServerOptions); + if (httpMainServerOptions != null) { + customizer.customizeHttpServer(httpMainServerOptions); } if (tmpSslConfig != null) { customizer.customizeHttpsServer(tmpSslConfig); } - if (domainSocketOptions != null) { - customizer.customizeDomainSocketServer(domainSocketOptions); + if (httpMainDomainSocketOptions != null) { + customizer.customizeDomainSocketServer(httpMainDomainSocketOptions); } } } @@ -700,9 +687,10 @@ private static void doServerStart(Vertx vertx, HttpBuildTimeConfig httpBuildTime if (tmpSslConfig != null && tmpSslConfig.getKeyCertOptions() == null) { tmpSslConfig = null; } - final HttpServerOptions sslConfig = tmpSslConfig; + httpMainSslServerOptions = tmpSslConfig; - if (httpConfiguration.insecureRequests != HttpConfiguration.InsecureRequests.ENABLED && sslConfig == null) { + if (httpConfiguration.insecureRequests != HttpConfiguration.InsecureRequests.ENABLED + && httpMainSslServerOptions == null) { throw new IllegalStateException("Cannot set quarkus.http.redirect-insecure-requests without enabling SSL."); } @@ -716,11 +704,13 @@ private static void doServerStart(Vertx vertx, HttpBuildTimeConfig httpBuildTime ioThreads = eventLoopCount; } CompletableFuture futureResult = new CompletableFuture<>(); + AtomicInteger connectionCount = new AtomicInteger(); vertx.deployVerticle(new Supplier() { @Override public Verticle get() { - return new WebDeploymentVerticle(httpServerOptions, sslConfig, domainSocketOptions, launchMode, + return new WebDeploymentVerticle(httpMainServerOptions, httpMainSslServerOptions, httpMainDomainSocketOptions, + launchMode, httpConfiguration.insecureRequests, httpConfiguration, connectionCount); } }, new DeploymentOptions().setInstances(ioThreads), new Handler>() { @@ -731,13 +721,15 @@ public void handle(AsyncResult event) { if (effectiveCause instanceof BindException) { List portsUsed = Collections.emptyList(); - if ((sslConfig == null) && (httpServerOptions != null)) { - portsUsed = List.of(httpServerOptions.getPort()); - } else if ((httpConfiguration.insecureRequests == InsecureRequests.DISABLED) && (sslConfig != null)) { - portsUsed = List.of(sslConfig.getPort()); - } else if ((sslConfig != null) && (httpConfiguration.insecureRequests == InsecureRequests.ENABLED) - && (httpServerOptions != null)) { - portsUsed = List.of(httpServerOptions.getPort(), sslConfig.getPort()); + if ((httpMainSslServerOptions == null) && (httpMainServerOptions != null)) { + portsUsed = List.of(httpMainServerOptions.getPort()); + } else if ((httpConfiguration.insecureRequests == InsecureRequests.DISABLED) + && (httpMainSslServerOptions != null)) { + portsUsed = List.of(httpMainSslServerOptions.getPort()); + } else if ((httpMainSslServerOptions != null) + && (httpConfiguration.insecureRequests == InsecureRequests.ENABLED) + && (httpMainServerOptions != null)) { + portsUsed = List.of(httpMainServerOptions.getPort(), httpMainSslServerOptions.getPort()); } effectiveCause = new QuarkusBindException((BindException) effectiveCause, portsUsed); @@ -748,51 +740,108 @@ public void handle(AsyncResult event) { } } }); + + return futureResult; + } + + private static void doServerStart(Vertx vertx, HttpBuildTimeConfig httpBuildTimeConfig, + ManagementInterfaceBuildTimeConfig managementBuildTimeConfig, Handler managementRouter, + HttpConfiguration httpConfiguration, ManagementInterfaceConfiguration managementConfig, + LaunchMode launchMode, + Supplier eventLoops, List websocketSubProtocols, boolean auxiliaryApplication) throws IOException { + + var mainServerFuture = initializeMainHttpServer(vertx, httpBuildTimeConfig, httpConfiguration, launchMode, eventLoops, + websocketSubProtocols); + var managementInterfaceFuture = initializeManagementInterface(vertx, managementBuildTimeConfig, managementRouter, + managementConfig, launchMode, websocketSubProtocols); + var managementInterfaceDomainSocketFuture = initializeManagementInterfaceWithDomainSocket(vertx, + managementBuildTimeConfig, managementRouter, managementConfig, websocketSubProtocols); + try { - String deploymentId = futureResult.get(); - VertxCoreRecorder.setWebDeploymentId(deploymentId); + String deploymentIdIfAny = mainServerFuture.get(); + + HttpServer tmpManagementServer = null; + HttpServer tmpManagementServerUsingDomainSocket = null; + if (managementRouter != null) { + tmpManagementServer = managementInterfaceFuture.get(); + tmpManagementServerUsingDomainSocket = managementInterfaceDomainSocketFuture.get(); + } + HttpServer managementServer = tmpManagementServer; + HttpServer managementServerDomainSocket = tmpManagementServerUsingDomainSocket; + if (deploymentIdIfAny != null) { + VertxCoreRecorder.setWebDeploymentId(deploymentIdIfAny); + } closeTask = new Runnable() { @Override public synchronized void run() { //guard against this being run twice if (closeTask == this) { - if (vertx.deploymentIDs().contains(deploymentId)) { - CountDownLatch latch = new CountDownLatch(1); + boolean isVertxClose = ((VertxInternal) vertx).closeFuture().future().isComplete(); + int count = 0; + if (deploymentIdIfAny != null && vertx.deploymentIDs().contains(deploymentIdIfAny)) { + count++; + } + if (managementServer != null && !isVertxClose) { + count++; + } + if (managementServerDomainSocket != null && !isVertxClose) { + count++; + } + + CountDownLatch latch = new CountDownLatch(count); + var handler = new Handler>() { + @Override + public void handle(AsyncResult event) { + latch.countDown(); + } + }; + + // shutdown main HTTP server + if (deploymentIdIfAny != null) { try { - vertx.undeploy(deploymentId, new Handler>() { - @Override - public void handle(AsyncResult event) { - latch.countDown(); - } - }); + vertx.undeploy(deploymentIdIfAny, handler); } catch (Exception e) { LOGGER.warn("Failed to undeploy deployment ", e); } - try { - latch.await(); - } catch (InterruptedException e) { - throw new RuntimeException(e); + } + + // shutdown the management interface + try { + if (managementServer != null && !isVertxClose) { + managementServer.close(handler); + } + if (managementServerDomainSocket != null && !isVertxClose) { + managementServerDomainSocket.close(handler); } + } catch (Exception e) { + LOGGER.warn("Unable to shutdown the management interface quietly", e); } - closeTask = null; - if (remoteSyncHandler != null) { - remoteSyncHandler.close(); - remoteSyncHandler = null; + + try { + latch.await(); + } catch (InterruptedException e) { + throw new RuntimeException(e); } } + closeTask = null; + if (remoteSyncHandler != null) { + remoteSyncHandler.close(); + remoteSyncHandler = null; + } } }; } catch (InterruptedException | ExecutionException e) { throw new RuntimeException("Unable to start HTTP server", e); } - setHttpServerTiming(httpConfiguration.insecureRequests, httpServerOptions, sslConfig, domainSocketOptions, - auxiliaryApplication); + setHttpServerTiming(httpConfiguration.insecureRequests, httpMainServerOptions, httpMainSslServerOptions, + httpMainDomainSocketOptions, + auxiliaryApplication, httpManagementServerOptions); } private static void setHttpServerTiming(InsecureRequests insecureRequests, HttpServerOptions httpServerOptions, HttpServerOptions sslConfig, - HttpServerOptions domainSocketOptions, boolean auxiliaryApplication) { + HttpServerOptions domainSocketOptions, boolean auxiliaryApplication, HttpServerOptions managementConfig) { StringBuilder serverListeningMessage = new StringBuilder("Listening on: "); int socketCount = 0; @@ -816,239 +865,43 @@ private static void setHttpServerTiming(InsecureRequests insecureRequests, HttpS } serverListeningMessage.append(String.format("unix:%s", domainSocketOptions.getHost())); } + if (managementConfig != null) { + serverListeningMessage.append( + String.format(". Management interface listening on http%s://%s:%s.", managementConfig.isSsl() ? "s" : "", + managementConfig.getHost(), managementConfig.getPort())); + } + Timing.setHttpServer(serverListeningMessage.toString(), auxiliaryApplication); } - /** - * Get an {@code HttpServerOptions} for this server configuration, or null if SSL should not be enabled - */ - private static HttpServerOptions createSslOptions(HttpBuildTimeConfig buildTimeConfig, HttpConfiguration httpConfiguration, - LaunchMode launchMode, List websocketSubProtocols) - throws IOException { + private static HttpServerOptions createHttpServerOptions( + HttpBuildTimeConfig buildTimeConfig, HttpConfiguration httpConfiguration, + LaunchMode launchMode, List websocketSubProtocols) { if (!httpConfiguration.hostEnabled) { return null; } + // TODO other config properties + HttpServerOptions options = new HttpServerOptions(); + int port = httpConfiguration.determinePort(launchMode); + options.setPort(port == 0 ? -1 : port); - ServerSslConfig sslConfig = httpConfiguration.ssl; - - final Optional certFile = sslConfig.certificate.file; - final Optional keyFile = sslConfig.certificate.keyFile; - final List keys = new ArrayList<>(); - final List certificates = new ArrayList<>(); - if (sslConfig.certificate.keyFiles.isPresent()) { - keys.addAll(sslConfig.certificate.keyFiles.get()); - } - if (sslConfig.certificate.files.isPresent()) { - certificates.addAll(sslConfig.certificate.files.get()); - } - if (keyFile.isPresent()) { - keys.add(keyFile.get()); - } - if (certFile.isPresent()) { - certificates.add(certFile.get()); - } - - // credentials provider - Map credentials = Map.of(); - if (sslConfig.certificate.credentialsProvider.isPresent()) { - String beanName = sslConfig.certificate.credentialsProviderName.orElse(null); - CredentialsProvider credentialsProvider = CredentialsProviderFinder.find(beanName); - String name = sslConfig.certificate.credentialsProvider.get(); - credentials = credentialsProvider.getCredentials(name); - } - final Optional keyStoreFile = sslConfig.certificate.keyStoreFile; - final Optional keyStorePassword = getCredential(sslConfig.certificate.keyStorePassword, credentials, - sslConfig.certificate.keyStorePasswordKey); - final Optional keyStoreKeyPassword = getCredential(sslConfig.certificate.keyStoreKeyPassword, credentials, - sslConfig.certificate.keyStoreKeyPasswordKey); - final Optional trustStoreFile = sslConfig.certificate.trustStoreFile; - final Optional trustStorePassword = getCredential(sslConfig.certificate.trustStorePassword, credentials, - sslConfig.certificate.trustStorePasswordKey); - final HttpServerOptions serverOptions = new HttpServerOptions(); - - //ssl - if (JdkSSLEngineOptions.isAlpnAvailable()) { - serverOptions.setUseAlpn(httpConfiguration.http2); - if (httpConfiguration.http2) { - serverOptions.setAlpnVersions(Arrays.asList(HttpVersion.HTTP_2, HttpVersion.HTTP_1_1)); - } - } - setIdleTimeout(httpConfiguration, serverOptions); - - if (!certificates.isEmpty() && !keys.isEmpty()) { - createPemKeyCertOptions(certificates, keys, serverOptions); - } else if (keyStoreFile.isPresent()) { - KeyStoreOptions options = createKeyStoreOptions( - keyStoreFile.get(), - keyStorePassword.orElse("password"), - sslConfig.certificate.keyStoreFileType, - sslConfig.certificate.keyStoreProvider, - sslConfig.certificate.keyStoreKeyAlias, - keyStoreKeyPassword); - serverOptions.setKeyCertOptions(options); - } - - if (trustStoreFile.isPresent()) { - if (!trustStorePassword.isPresent()) { - throw new IllegalArgumentException("No trust store password provided"); - } - KeyStoreOptions options = createKeyStoreOptions( - trustStoreFile.get(), - trustStorePassword.get(), - sslConfig.certificate.trustStoreFileType, - sslConfig.certificate.trustStoreProvider, - sslConfig.certificate.trustStoreCertAlias, - Optional.empty()); - serverOptions.setTrustOptions(options); - } - - for (String cipher : sslConfig.cipherSuites.orElse(Collections.emptyList())) { - serverOptions.addEnabledCipherSuite(cipher); - } - - for (String protocol : sslConfig.protocols) { - if (!protocol.isEmpty()) { - serverOptions.addEnabledSecureTransportProtocol(protocol); - } - } - serverOptions.setSsl(true); - serverOptions.setSni(sslConfig.sni); - int sslPort = httpConfiguration.determineSslPort(launchMode); - // -2 instead of -1 (see http) to have vert.x assign two different random ports if both http and https shall be random - serverOptions.setPort(sslPort == 0 ? -2 : sslPort); - serverOptions.setClientAuth(buildTimeConfig.tlsClientAuth); - - applyCommonOptions(serverOptions, buildTimeConfig, httpConfiguration, websocketSubProtocols); - - return serverOptions; - } - - private static Optional getCredential(Optional password, Map credentials, - Optional passwordKey) { - if (password.isPresent()) { - return password; - } - - if (passwordKey.isPresent()) { - return Optional.ofNullable(credentials.get(passwordKey.get())); - } else { - return Optional.empty(); - } - } - - private static void applyCommonOptions(HttpServerOptions httpServerOptions, - HttpBuildTimeConfig buildTimeConfig, - HttpConfiguration httpConfiguration, - List websocketSubProtocols) { - httpServerOptions.setHost(httpConfiguration.host); - setIdleTimeout(httpConfiguration, httpServerOptions); - httpServerOptions.setMaxHeaderSize(httpConfiguration.limits.maxHeaderSize.asBigInteger().intValueExact()); - httpServerOptions.setMaxChunkSize(httpConfiguration.limits.maxChunkSize.asBigInteger().intValueExact()); - httpServerOptions.setMaxFormAttributeSize(httpConfiguration.limits.maxFormAttributeSize.asBigInteger().intValueExact()); - httpServerOptions.setWebSocketSubProtocols(websocketSubProtocols); - httpServerOptions.setReusePort(httpConfiguration.soReusePort); - httpServerOptions.setTcpQuickAck(httpConfiguration.tcpQuickAck); - httpServerOptions.setTcpCork(httpConfiguration.tcpCork); - httpServerOptions.setAcceptBacklog(httpConfiguration.acceptBacklog); - httpServerOptions.setTcpFastOpen(httpConfiguration.tcpFastOpen); - httpServerOptions.setCompressionSupported(buildTimeConfig.enableCompression); - if (buildTimeConfig.compressionLevel.isPresent()) { - httpServerOptions.setCompressionLevel(buildTimeConfig.compressionLevel.getAsInt()); - } - httpServerOptions.setDecompressionSupported(buildTimeConfig.enableDecompression); - httpServerOptions.setMaxInitialLineLength(httpConfiguration.limits.maxInitialLineLength); - httpServerOptions.setHandle100ContinueAutomatically(httpConfiguration.handle100ContinueAutomatically); - } - - private static KeyStoreOptions createKeyStoreOptions(Path path, String password, Optional fileType, - Optional provider, Optional alias, Optional aliasPassword) throws IOException { - final String type; - if (fileType.isPresent()) { - type = fileType.get().toLowerCase(); - } else { - type = findKeystoreFileType(path); - } + HttpServerOptionsUtils.applyCommonOptions(options, buildTimeConfig, httpConfiguration, websocketSubProtocols); - byte[] data = getFileContent(path); - KeyStoreOptions options = new KeyStoreOptions() - .setPassword(password) - .setValue(Buffer.buffer(data)) - .setType(type.toUpperCase()) - .setProvider(provider.orElse(null)) - .setAlias(alias.orElse(null)) - .setAliasPassword(aliasPassword.orElse(null)); return options; } - private static byte[] getFileContent(Path path) throws IOException { - byte[] data; - final InputStream resource = Thread.currentThread().getContextClassLoader() - .getResourceAsStream(ClassPathUtils.toResourceName(path)); - if (resource != null) { - try (InputStream is = resource) { - data = doRead(is); - } - } else { - try (InputStream is = Files.newInputStream(path)) { - data = doRead(is); - } - } - return data; - } - - private static void createPemKeyCertOptions(List certFile, List keyFile, - HttpServerOptions serverOptions) throws IOException { - - if (certFile.size() != keyFile.size()) { - throw new ConfigurationException("Invalid certificate configuration - `files` and `keyFiles` must have the " - + "same number of elements"); - } - - List certificates = new ArrayList<>(); - List keys = new ArrayList<>(); - - for (Path p : certFile) { - final byte[] cert = getFileContent(p); - certificates.add(Buffer.buffer(cert)); - } - - for (Path p : keyFile) { - final byte[] key = getFileContent(p); - keys.add(Buffer.buffer(key)); - } - - PemKeyCertOptions pemKeyCertOptions = new PemKeyCertOptions() - .setCertValues(certificates) - .setKeyValues(keys); - serverOptions.setPemKeyCertOptions(pemKeyCertOptions); - } - - private static String findKeystoreFileType(Path storePath) { - final String pathName = storePath.toString(); - if (pathName.endsWith(".p12") || pathName.endsWith(".pkcs12") || pathName.endsWith(".pfx")) { - return "pkcs12"; - } else { - // assume jks - return "jks"; - } - } - - private static byte[] doRead(InputStream is) throws IOException { - return is.readAllBytes(); - } - - private static HttpServerOptions createHttpServerOptions( - HttpBuildTimeConfig buildTimeConfig, HttpConfiguration httpConfiguration, + private static HttpServerOptions createHttpServerOptionsForManagementInterface( + ManagementInterfaceBuildTimeConfig buildTimeConfig, ManagementInterfaceConfiguration httpConfiguration, LaunchMode launchMode, List websocketSubProtocols) { if (!httpConfiguration.hostEnabled) { return null; } - // TODO other config properties HttpServerOptions options = new HttpServerOptions(); int port = httpConfiguration.determinePort(launchMode); options.setPort(port == 0 ? -1 : port); - applyCommonOptions(options, buildTimeConfig, httpConfiguration, websocketSubProtocols); + HttpServerOptionsUtils.applyCommonOptionsForManagementInterface(options, buildTimeConfig, httpConfiguration, + websocketSubProtocols); return options; } @@ -1061,7 +914,7 @@ private static HttpServerOptions createDomainSocketOptions( } HttpServerOptions options = new HttpServerOptions(); - applyCommonOptions(options, buildTimeConfig, httpConfiguration, websocketSubProtocols); + HttpServerOptionsUtils.applyCommonOptions(options, buildTimeConfig, httpConfiguration, websocketSubProtocols); // Override the host (0.0.0.0 by default) with the configured domain socket. options.setHost(httpConfiguration.domainSocket); @@ -1077,25 +930,42 @@ private static HttpServerOptions createDomainSocketOptions( return options; } - private static void setIdleTimeout(HttpConfiguration httpConfiguration, HttpServerOptions options) { - int idleTimeout = (int) httpConfiguration.idleTimeout.toMillis(); - options.setIdleTimeout(idleTimeout); - options.setIdleTimeoutUnit(TimeUnit.MILLISECONDS); + private static HttpServerOptions createDomainSocketOptionsForManagementInterface( + ManagementInterfaceBuildTimeConfig buildTimeConfig, ManagementInterfaceConfiguration httpConfiguration, + List websocketSubProtocols) { + if (!httpConfiguration.domainSocketEnabled) { + return null; + } + HttpServerOptions options = new HttpServerOptions(); + + HttpServerOptionsUtils.applyCommonOptionsForManagementInterface(options, buildTimeConfig, httpConfiguration, + websocketSubProtocols); + // Override the host (0.0.0.0 by default) with the configured domain socket. + options.setHost(httpConfiguration.domainSocket); + + // Check if we can write into the domain socket directory + // We can do this check using a blocking API as the execution is done from the main thread (not an I/O thread) + File file = new File(httpConfiguration.domainSocket); + if (!file.getParentFile().canWrite()) { + LOGGER.warnf( + "Unable to write in the domain socket directory (`%s`). Binding to the socket is likely going to fail.", + httpConfiguration.domainSocket); + } + + return options; } public void addRoute(RuntimeValue router, Function route, Handler handler, - HandlerType blocking) { + HandlerType type) { Route vr = route.apply(router.getValue()); - - if (blocking == HandlerType.BLOCKING) { + if (type == HandlerType.BLOCKING) { vr.blockingHandler(handler, false); - } else if (blocking == HandlerType.FAILURE) { + } else if (type == HandlerType.FAILURE) { vr.failureHandler(handler); } else { vr.handler(handler); } - } public void setNonApplicationRedirectHandler(String nonApplicationPath, String rootPath) { @@ -1417,6 +1287,7 @@ public void afterRestore(org.crac.Context context) throws Ex p.future().onComplete(event -> latch.countDown()); latch.await(); } + } protected static ServerBootstrap virtualBootstrap; @@ -1486,13 +1357,11 @@ public static Object getCurrentApplicationState() { return rootHandler; } - public Handler createBodyHandler() { + private static Handler configureAndGetBody(Optional maxBodySize, BodyConfig bodyConfig) { BodyHandler bodyHandler = BodyHandler.create(); - Optional maxBodySize = httpConfiguration.getValue().limits.maxBodySize; if (maxBodySize.isPresent()) { bodyHandler.setBodyLimit(maxBodySize.get().asLongValue()); } - final BodyConfig bodyConfig = httpConfiguration.getValue().body; bodyHandler.setHandleFileUploads(bodyConfig.handleFileUploads); bodyHandler.setUploadsDirectory(bodyConfig.uploadsDirectory); bodyHandler.setDeleteUploadedFilesOnEnd(bodyConfig.deleteUploadedFilesOnEnd); @@ -1536,6 +1405,16 @@ public void run() { }; } + public Handler createBodyHandler() { + Optional maxBodySize = httpConfiguration.getValue().limits.maxBodySize; + return configureAndGetBody(maxBodySize, httpConfiguration.getValue().body); + } + + public Handler createBodyHandlerForManagementInterface() { + Optional maxBodySize = managementConfiguration.getValue().limits.maxBodySize; + return configureAndGetBody(maxBodySize, managementConfiguration.getValue().body); + } + private static final List CAN_HAVE_BODY = Arrays.asList(HttpMethod.POST, HttpMethod.PUT, HttpMethod.PATCH, HttpMethod.DELETE); diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/management/ManagementInterfaceBuildTimeConfig.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/management/ManagementInterfaceBuildTimeConfig.java new file mode 100644 index 0000000000000..d80e38c36b944 --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/management/ManagementInterfaceBuildTimeConfig.java @@ -0,0 +1,65 @@ +package io.quarkus.vertx.http.runtime.management; + +import java.util.OptionalInt; + +import io.quarkus.runtime.annotations.ConfigItem; +import io.quarkus.runtime.annotations.ConfigPhase; +import io.quarkus.runtime.annotations.ConfigRoot; +import io.vertx.core.http.ClientAuth; + +/** + * Management interface configuration. + */ +@ConfigRoot(name = "management", phase = ConfigPhase.BUILD_AND_RUN_TIME_FIXED) +public class ManagementInterfaceBuildTimeConfig { + + /** + * Enables / Disables the usage of a separate interface/port to expose the management endpoints. + * If sets to {@code true}, the management endpoints will be exposed to a different HTTP server. + * This avoids exposing the management endpoints on a publicly available server. + */ + @ConfigItem(defaultValue = "false") + public boolean enabled; + + /** + * Configures the engine to require/request client authentication. + * NONE, REQUEST, REQUIRED + */ + @ConfigItem(name = "ssl.client-auth", defaultValue = "NONE") + public ClientAuth tlsClientAuth; + + /** + * A common root path for management endpoints. Various extension-provided management endpoints such as metrics + * and health are deployed under this path by default. + */ + @ConfigItem(defaultValue = "/q") + public String rootPath; + + /** + * If responses should be compressed. + *

    + * Note that this will attempt to compress all responses, to avoid compressing + * already compressed content (such as images) you need to set the following header: + *

    + * Content-Encoding: identity + *

    + * Which will tell vert.x not to compress the response. + */ + @ConfigItem + public boolean enableCompression; + + /** + * When enabled, vert.x will decompress the request's body if it's compressed. + *

    + * Note that the compression format (e.g., gzip) must be specified in the Content-Encoding header + * in the request. + */ + @ConfigItem + public boolean enableDecompression; + + /** + * The compression level used when compression support is enabled. + */ + @ConfigItem + public OptionalInt compressionLevel; +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/management/ManagementInterfaceConfiguration.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/management/ManagementInterfaceConfiguration.java new file mode 100644 index 0000000000000..9d77f458d1c90 --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/management/ManagementInterfaceConfiguration.java @@ -0,0 +1,120 @@ +package io.quarkus.vertx.http.runtime.management; + +import java.time.Duration; +import java.util.Map; +import java.util.Optional; + +import io.quarkus.runtime.LaunchMode; +import io.quarkus.runtime.annotations.ConfigItem; +import io.quarkus.runtime.annotations.ConfigPhase; +import io.quarkus.runtime.annotations.ConfigRoot; +import io.quarkus.vertx.http.runtime.BodyConfig; +import io.quarkus.vertx.http.runtime.FilterConfig; +import io.quarkus.vertx.http.runtime.HeaderConfig; +import io.quarkus.vertx.http.runtime.ProxyConfig; +import io.quarkus.vertx.http.runtime.ServerLimitsConfig; +import io.quarkus.vertx.http.runtime.ServerSslConfig; + +/** + * Configures the management interface. + * Note that the management interface must be enabled using the + * {@link ManagementInterfaceBuildTimeConfig#enabled} build-time property. + */ +@ConfigRoot(phase = ConfigPhase.RUN_TIME, name = "management") +public class ManagementInterfaceConfiguration { + + /** + * The HTTP port + */ + @ConfigItem(defaultValue = "9000") + public int port; + + /** + * The HTTP port + */ + @ConfigItem(defaultValue = "9001") + public int testPort; + + /** + * The HTTP host + * + * Defaults to 0.0.0.0 + * + * Defaulting to 0.0.0.0 makes it easier to deploy Quarkus to container, however it + * is not suitable for dev/test mode as other people on the network can connect to your + * development machine. + */ + @ConfigItem + public Optional host; + + /** + * Enable listening to host:port + */ + @ConfigItem(defaultValue = "true") + public boolean hostEnabled; + + /** + * The SSL config + */ + public ServerSslConfig ssl; + + /** + * When set to {@code true}, the HTTP server automatically sends `100 CONTINUE` + * response when the request expects it (with the `Expect: 100-Continue` header). + */ + @ConfigItem(defaultValue = "false", name = "handle-100-continue-automatically") + public boolean handle100ContinueAutomatically; + + /** + * Server limits configuration + */ + public ServerLimitsConfig limits; + + /** + * Http connection idle timeout + */ + @ConfigItem(defaultValue = "30M", name = "idle-timeout") + public Duration idleTimeout; + + /** + * Request body related settings + */ + public BodyConfig body; + + /** + * The accept backlog, this is how many connections can be waiting to be accepted before connections start being rejected + */ + @ConfigItem(defaultValue = "-1") + public int acceptBacklog; + + /** + * Path to a unix domain socket + */ + @ConfigItem(defaultValue = "/var/run/io.quarkus.management.socket") + public String domainSocket; + + /** + * Enable listening to host:port + */ + @ConfigItem + public boolean domainSocketEnabled; + + /** + * Additional HTTP Headers always sent in the response + */ + @ConfigItem + public Map header; + + /** + * Additional HTTP configuration per path + */ + @ConfigItem + public Map filter; + + public ProxyConfig proxy; + + public int determinePort(LaunchMode launchMode) { + return launchMode == LaunchMode.TEST ? testPort : port; + } + +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/options/HttpServerCommonHandlers.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/options/HttpServerCommonHandlers.java new file mode 100644 index 0000000000000..562611de71eb4 --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/options/HttpServerCommonHandlers.java @@ -0,0 +1,177 @@ +package io.quarkus.vertx.http.runtime.options; + +import static io.quarkus.vertx.core.runtime.context.VertxContextSafetyToggle.setCurrentContextSafe; +import static io.quarkus.vertx.http.runtime.TrustedProxyCheck.allowAll; + +import java.util.Locale; +import java.util.Map; +import java.util.function.Supplier; + +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.quarkus.vertx.http.runtime.FilterConfig; +import io.quarkus.vertx.http.runtime.ForwardedProxyHandler; +import io.quarkus.vertx.http.runtime.ForwardedServerRequestWrapper; +import io.quarkus.vertx.http.runtime.ForwardingProxyOptions; +import io.quarkus.vertx.http.runtime.HeaderConfig; +import io.quarkus.vertx.http.runtime.ProxyConfig; +import io.quarkus.vertx.http.runtime.ResumingRequestWrapper; +import io.quarkus.vertx.http.runtime.ServerLimitsConfig; +import io.quarkus.vertx.http.runtime.TrustedProxyCheck; +import io.quarkus.vertx.http.runtime.VertxHttpRecorder; +import io.smallrye.common.vertx.VertxContext; +import io.vertx.core.Context; +import io.vertx.core.Handler; +import io.vertx.core.Vertx; +import io.vertx.core.http.HttpMethod; +import io.vertx.core.http.HttpServerRequest; +import io.vertx.ext.web.Router; +import io.vertx.ext.web.RoutingContext; + +public class HttpServerCommonHandlers { + public static void enforceMaxBodySize(ServerLimitsConfig limits, Router httpRouteRouter) { + if (limits.maxBodySize.isPresent()) { + long limit = limits.maxBodySize.get().asLongValue(); + Long limitObj = limit; + httpRouteRouter.route().order(-2).handler(new Handler() { + @Override + public void handle(RoutingContext event) { + String lengthString = event.request().headers().get(HttpHeaderNames.CONTENT_LENGTH); + + if (lengthString != null) { + long length = Long.parseLong(lengthString); + if (length > limit) { + event.response().headers().add(HttpHeaderNames.CONNECTION, "close"); + event.response().setStatusCode(HttpResponseStatus.REQUEST_ENTITY_TOO_LARGE.code()); + event.response().endHandler(new Handler() { + @Override + public void handle(Void e) { + event.request().connection().close(); + } + }); + event.response().end(); + return; + } + } else { + event.put(VertxHttpRecorder.MAX_REQUEST_SIZE_KEY, limitObj); + } + event.next(); + } + }); + } + } + + public static Handler enforceDuplicatedContext(Handler delegate) { + return new Handler() { + @Override + public void handle(HttpServerRequest event) { + if (!VertxContext.isOnDuplicatedContext()) { + // Vert.x should call us on a duplicated context. + // But in the case of pipelined requests, it does not. + // See https://github.com/quarkusio/quarkus/issues/24626. + Context context = VertxContext.createNewDuplicatedContext(); + context.runOnContext(new Handler() { + @Override + public void handle(Void x) { + setCurrentContextSafe(true); + delegate.handle(new ResumingRequestWrapper(event)); + } + }); + } else { + setCurrentContextSafe(true); + delegate.handle(new ResumingRequestWrapper(event)); + } + } + }; + } + + public static Handler applyProxy(ProxyConfig proxyConfig, Handler root, + Supplier vertx) { + if (proxyConfig.proxyAddressForwarding) { + final ForwardingProxyOptions forwardingProxyOptions = ForwardingProxyOptions.from(proxyConfig); + final TrustedProxyCheck.TrustedProxyCheckBuilder proxyCheckBuilder = forwardingProxyOptions.trustedProxyCheckBuilder; + if (proxyCheckBuilder == null) { + // no proxy check => we do not restrict who can send `X-Forwarded` or `X-Forwarded-*` headers + final TrustedProxyCheck allowAllProxyCheck = allowAll(); + return new Handler() { + @Override + public void handle(HttpServerRequest event) { + root.handle(new ForwardedServerRequestWrapper(event, forwardingProxyOptions, allowAllProxyCheck)); + } + }; + } else { + // restrict who can send `Forwarded`, `X-Forwarded` or `X-Forwarded-*` headers + return new ForwardedProxyHandler(proxyCheckBuilder, vertx, root, forwardingProxyOptions); + } + } + return root; + } + + public static void applyFilters(Map filtersInConfig, Router httpRouteRouter) { + if (!filtersInConfig.isEmpty()) { + for (var entry : filtersInConfig.entrySet()) { + var filterConfig = entry.getValue(); + var matches = filterConfig.matches; + var order = filterConfig.order.orElse(Integer.MIN_VALUE); + var methods = filterConfig.methods; + var headers = filterConfig.header; + if (methods.isEmpty()) { + httpRouteRouter.routeWithRegex(matches) + .order(order) + .handler(new Handler() { + @Override + public void handle(RoutingContext event) { + event.response().headers().setAll(headers); + event.next(); + } + }); + } else { + for (var method : methods.get()) { + httpRouteRouter.routeWithRegex(HttpMethod.valueOf(method.toUpperCase(Locale.ROOT)), matches) + .order(order) + .handler(new Handler() { + @Override + public void handle(RoutingContext event) { + event.response().headers().setAll(headers); + event.next(); + } + }); + } + } + } + } + } + + public static void applyHeaders(Map headers, Router httpRouteRouter) { + if (!headers.isEmpty()) { + // Creates a handler for each header entry + for (Map.Entry entry : headers.entrySet()) { + var name = entry.getKey(); + var config = entry.getValue(); + if (config.methods.isEmpty()) { + httpRouteRouter.route(config.path) + .order(Integer.MIN_VALUE) + .handler(new Handler() { + @Override + public void handle(RoutingContext event) { + event.response().headers().set(name, config.value); + event.next(); + } + }); + } else { + for (String method : config.methods.get()) { + httpRouteRouter.route(HttpMethod.valueOf(method.toUpperCase(Locale.ROOT)), config.path) + .order(Integer.MIN_VALUE) + .handler(new Handler() { + @Override + public void handle(RoutingContext event) { + event.response().headers().add(name, config.value); + event.next(); + } + }); + } + } + } + } + } +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/options/HttpServerOptionsUtils.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/options/HttpServerOptionsUtils.java new file mode 100644 index 0000000000000..b9c522270fb30 --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/options/HttpServerOptionsUtils.java @@ -0,0 +1,386 @@ +package io.quarkus.vertx.http.runtime.options; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.TimeUnit; + +import io.quarkus.credentials.CredentialsProvider; +import io.quarkus.credentials.runtime.CredentialsProviderFinder; +import io.quarkus.runtime.LaunchMode; +import io.quarkus.runtime.configuration.ConfigurationException; +import io.quarkus.runtime.util.ClassPathUtils; +import io.quarkus.vertx.http.runtime.HttpBuildTimeConfig; +import io.quarkus.vertx.http.runtime.HttpConfiguration; +import io.quarkus.vertx.http.runtime.ServerSslConfig; +import io.quarkus.vertx.http.runtime.management.ManagementInterfaceBuildTimeConfig; +import io.quarkus.vertx.http.runtime.management.ManagementInterfaceConfiguration; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.http.HttpServerOptions; +import io.vertx.core.http.HttpVersion; +import io.vertx.core.net.JdkSSLEngineOptions; +import io.vertx.core.net.KeyStoreOptions; +import io.vertx.core.net.PemKeyCertOptions; + +public class HttpServerOptionsUtils { + + /** + * Get an {@code HttpServerOptions} for this server configuration, or null if SSL should not be enabled + */ + public static HttpServerOptions createSslOptions(HttpBuildTimeConfig buildTimeConfig, HttpConfiguration httpConfiguration, + LaunchMode launchMode, List websocketSubProtocols) + throws IOException { + if (!httpConfiguration.hostEnabled) { + return null; + } + + ServerSslConfig sslConfig = httpConfiguration.ssl; + + final Optional certFile = sslConfig.certificate.file; + final Optional keyFile = sslConfig.certificate.keyFile; + final List keys = new ArrayList<>(); + final List certificates = new ArrayList<>(); + if (sslConfig.certificate.keyFiles.isPresent()) { + keys.addAll(sslConfig.certificate.keyFiles.get()); + } + if (sslConfig.certificate.files.isPresent()) { + certificates.addAll(sslConfig.certificate.files.get()); + } + if (keyFile.isPresent()) { + keys.add(keyFile.get()); + } + if (certFile.isPresent()) { + certificates.add(certFile.get()); + } + + // credentials provider + Map credentials = Map.of(); + if (sslConfig.certificate.credentialsProvider.isPresent()) { + String beanName = sslConfig.certificate.credentialsProviderName.orElse(null); + CredentialsProvider credentialsProvider = CredentialsProviderFinder.find(beanName); + String name = sslConfig.certificate.credentialsProvider.get(); + credentials = credentialsProvider.getCredentials(name); + } + final Optional keyStoreFile = sslConfig.certificate.keyStoreFile; + final Optional keyStorePassword = getCredential(sslConfig.certificate.keyStorePassword, credentials, + sslConfig.certificate.keyStorePasswordKey); + final Optional keyStoreKeyPassword = getCredential(sslConfig.certificate.keyStoreKeyPassword, credentials, + sslConfig.certificate.keyStoreKeyPasswordKey); + final Optional trustStoreFile = sslConfig.certificate.trustStoreFile; + final Optional trustStorePassword = getCredential(sslConfig.certificate.trustStorePassword, credentials, + sslConfig.certificate.trustStorePasswordKey); + final HttpServerOptions serverOptions = new HttpServerOptions(); + + //ssl + if (JdkSSLEngineOptions.isAlpnAvailable()) { + serverOptions.setUseAlpn(httpConfiguration.http2); + if (httpConfiguration.http2) { + serverOptions.setAlpnVersions(Arrays.asList(HttpVersion.HTTP_2, HttpVersion.HTTP_1_1)); + } + } + setIdleTimeout(httpConfiguration, serverOptions); + + if (!certificates.isEmpty() && !keys.isEmpty()) { + createPemKeyCertOptions(certificates, keys, serverOptions); + } else if (keyStoreFile.isPresent()) { + KeyStoreOptions options = createKeyStoreOptions( + keyStoreFile.get(), + keyStorePassword.orElse("password"), + sslConfig.certificate.keyStoreFileType, + sslConfig.certificate.keyStoreProvider, + sslConfig.certificate.keyStoreKeyAlias, + keyStoreKeyPassword); + serverOptions.setKeyCertOptions(options); + } + + if (trustStoreFile.isPresent()) { + if (!trustStorePassword.isPresent()) { + throw new IllegalArgumentException("No trust store password provided"); + } + KeyStoreOptions options = createKeyStoreOptions( + trustStoreFile.get(), + trustStorePassword.get(), + sslConfig.certificate.trustStoreFileType, + sslConfig.certificate.trustStoreProvider, + sslConfig.certificate.trustStoreCertAlias, + Optional.empty()); + serverOptions.setTrustOptions(options); + } + + for (String cipher : sslConfig.cipherSuites.orElse(Collections.emptyList())) { + serverOptions.addEnabledCipherSuite(cipher); + } + + for (String protocol : sslConfig.protocols) { + if (!protocol.isEmpty()) { + serverOptions.addEnabledSecureTransportProtocol(protocol); + } + } + serverOptions.setSsl(true); + serverOptions.setSni(sslConfig.sni); + int sslPort = httpConfiguration.determineSslPort(launchMode); + // -2 instead of -1 (see http) to have vert.x assign two different random ports if both http and https shall be random + serverOptions.setPort(sslPort == 0 ? -2 : sslPort); + serverOptions.setClientAuth(buildTimeConfig.tlsClientAuth); + + applyCommonOptions(serverOptions, buildTimeConfig, httpConfiguration, websocketSubProtocols); + + return serverOptions; + } + + /** + * Get an {@code HttpServerOptions} for this server configuration, or null if SSL should not be enabled + */ + public static HttpServerOptions createSslOptionsForManagementInterface(ManagementInterfaceBuildTimeConfig buildTimeConfig, + ManagementInterfaceConfiguration httpConfiguration, + LaunchMode launchMode, List websocketSubProtocols) + throws IOException { + if (!httpConfiguration.hostEnabled) { + return null; + } + + ServerSslConfig sslConfig = httpConfiguration.ssl; + + final Optional certFile = sslConfig.certificate.file; + final Optional keyFile = sslConfig.certificate.keyFile; + final List keys = new ArrayList<>(); + final List certificates = new ArrayList<>(); + if (sslConfig.certificate.keyFiles.isPresent()) { + keys.addAll(sslConfig.certificate.keyFiles.get()); + } + if (sslConfig.certificate.files.isPresent()) { + certificates.addAll(sslConfig.certificate.files.get()); + } + if (keyFile.isPresent()) { + keys.add(keyFile.get()); + } + if (certFile.isPresent()) { + certificates.add(certFile.get()); + } + + // credentials provider + Map credentials = Map.of(); + if (sslConfig.certificate.credentialsProvider.isPresent()) { + String beanName = sslConfig.certificate.credentialsProviderName.orElse(null); + CredentialsProvider credentialsProvider = CredentialsProviderFinder.find(beanName); + String name = sslConfig.certificate.credentialsProvider.get(); + credentials = credentialsProvider.getCredentials(name); + } + final Optional keyStoreFile = sslConfig.certificate.keyStoreFile; + final Optional keyStorePassword = getCredential(sslConfig.certificate.keyStorePassword, credentials, + sslConfig.certificate.keyStorePasswordKey); + final Optional keyStoreKeyPassword = getCredential(sslConfig.certificate.keyStoreKeyPassword, credentials, + sslConfig.certificate.keyStoreKeyPasswordKey); + final Optional trustStoreFile = sslConfig.certificate.trustStoreFile; + final Optional trustStorePassword = getCredential(sslConfig.certificate.trustStorePassword, credentials, + sslConfig.certificate.trustStorePasswordKey); + final HttpServerOptions serverOptions = new HttpServerOptions(); + + //ssl + if (JdkSSLEngineOptions.isAlpnAvailable()) { + serverOptions.setUseAlpn(true); + serverOptions.setAlpnVersions(Arrays.asList(HttpVersion.HTTP_2, HttpVersion.HTTP_1_1)); + } + int idleTimeout = (int) httpConfiguration.idleTimeout.toMillis(); + serverOptions.setIdleTimeout(idleTimeout); + serverOptions.setIdleTimeoutUnit(TimeUnit.MILLISECONDS); + + if (!certificates.isEmpty() && !keys.isEmpty()) { + createPemKeyCertOptions(certificates, keys, serverOptions); + } else if (keyStoreFile.isPresent()) { + KeyStoreOptions options = createKeyStoreOptions( + keyStoreFile.get(), + keyStorePassword.orElse("password"), + sslConfig.certificate.keyStoreFileType, + sslConfig.certificate.keyStoreProvider, + sslConfig.certificate.keyStoreKeyAlias, + keyStoreKeyPassword); + serverOptions.setKeyCertOptions(options); + } + + if (trustStoreFile.isPresent()) { + if (!trustStorePassword.isPresent()) { + throw new IllegalArgumentException("No trust store password provided"); + } + KeyStoreOptions options = createKeyStoreOptions( + trustStoreFile.get(), + trustStorePassword.get(), + sslConfig.certificate.trustStoreFileType, + sslConfig.certificate.trustStoreProvider, + sslConfig.certificate.trustStoreCertAlias, + Optional.empty()); + serverOptions.setTrustOptions(options); + } + + for (String cipher : sslConfig.cipherSuites.orElse(Collections.emptyList())) { + serverOptions.addEnabledCipherSuite(cipher); + } + + for (String protocol : sslConfig.protocols) { + if (!protocol.isEmpty()) { + serverOptions.addEnabledSecureTransportProtocol(protocol); + } + } + serverOptions.setSsl(true); + serverOptions.setSni(sslConfig.sni); + int sslPort = httpConfiguration.determinePort(launchMode); + // -2 instead of -1 (see http) to have vert.x assign two different random ports if both http and https shall be random + serverOptions.setPort(sslPort == 0 ? -2 : sslPort); + serverOptions.setClientAuth(buildTimeConfig.tlsClientAuth); + + applyCommonOptionsForManagementInterface(serverOptions, buildTimeConfig, httpConfiguration, websocketSubProtocols); + + return serverOptions; + } + + private static Optional getCredential(Optional password, Map credentials, + Optional passwordKey) { + if (password.isPresent()) { + return password; + } + + if (passwordKey.isPresent()) { + return Optional.ofNullable(credentials.get(passwordKey.get())); + } else { + return Optional.empty(); + } + } + + public static void applyCommonOptions(HttpServerOptions httpServerOptions, + HttpBuildTimeConfig buildTimeConfig, + HttpConfiguration httpConfiguration, + List websocketSubProtocols) { + httpServerOptions.setHost(httpConfiguration.host); + setIdleTimeout(httpConfiguration, httpServerOptions); + httpServerOptions.setMaxHeaderSize(httpConfiguration.limits.maxHeaderSize.asBigInteger().intValueExact()); + httpServerOptions.setMaxChunkSize(httpConfiguration.limits.maxChunkSize.asBigInteger().intValueExact()); + httpServerOptions.setMaxFormAttributeSize(httpConfiguration.limits.maxFormAttributeSize.asBigInteger().intValueExact()); + httpServerOptions.setWebSocketSubProtocols(websocketSubProtocols); + httpServerOptions.setReusePort(httpConfiguration.soReusePort); + httpServerOptions.setTcpQuickAck(httpConfiguration.tcpQuickAck); + httpServerOptions.setTcpCork(httpConfiguration.tcpCork); + httpServerOptions.setAcceptBacklog(httpConfiguration.acceptBacklog); + httpServerOptions.setTcpFastOpen(httpConfiguration.tcpFastOpen); + httpServerOptions.setCompressionSupported(buildTimeConfig.enableCompression); + if (buildTimeConfig.compressionLevel.isPresent()) { + httpServerOptions.setCompressionLevel(buildTimeConfig.compressionLevel.getAsInt()); + } + httpServerOptions.setDecompressionSupported(buildTimeConfig.enableDecompression); + httpServerOptions.setMaxInitialLineLength(httpConfiguration.limits.maxInitialLineLength); + httpServerOptions.setHandle100ContinueAutomatically(httpConfiguration.handle100ContinueAutomatically); + } + + public static void applyCommonOptionsForManagementInterface(HttpServerOptions options, + ManagementInterfaceBuildTimeConfig buildTimeConfig, + ManagementInterfaceConfiguration httpConfiguration, + List websocketSubProtocols) { + options.setHost(httpConfiguration.host.orElse("0.0.0.0")); + + int idleTimeout = (int) httpConfiguration.idleTimeout.toMillis(); + options.setIdleTimeout(idleTimeout); + options.setIdleTimeoutUnit(TimeUnit.MILLISECONDS); + + options.setMaxHeaderSize(httpConfiguration.limits.maxHeaderSize.asBigInteger().intValueExact()); + options.setMaxChunkSize(httpConfiguration.limits.maxChunkSize.asBigInteger().intValueExact()); + options.setMaxFormAttributeSize(httpConfiguration.limits.maxFormAttributeSize.asBigInteger().intValueExact()); + options.setMaxInitialLineLength(httpConfiguration.limits.maxInitialLineLength); + options.setWebSocketSubProtocols(websocketSubProtocols); + options.setAcceptBacklog(httpConfiguration.acceptBacklog); + options.setCompressionSupported(buildTimeConfig.enableCompression); + if (buildTimeConfig.compressionLevel.isPresent()) { + options.setCompressionLevel(buildTimeConfig.compressionLevel.getAsInt()); + } + options.setDecompressionSupported(buildTimeConfig.enableDecompression); + options.setHandle100ContinueAutomatically(httpConfiguration.handle100ContinueAutomatically); + } + + private static KeyStoreOptions createKeyStoreOptions(Path path, String password, Optional fileType, + Optional provider, Optional alias, Optional aliasPassword) throws IOException { + final String type; + if (fileType.isPresent()) { + type = fileType.get().toLowerCase(); + } else { + type = findKeystoreFileType(path); + } + + byte[] data = getFileContent(path); + KeyStoreOptions options = new KeyStoreOptions() + .setPassword(password) + .setValue(Buffer.buffer(data)) + .setType(type.toUpperCase()) + .setProvider(provider.orElse(null)) + .setAlias(alias.orElse(null)) + .setAliasPassword(aliasPassword.orElse(null)); + return options; + } + + private static byte[] getFileContent(Path path) throws IOException { + byte[] data; + final InputStream resource = Thread.currentThread().getContextClassLoader() + .getResourceAsStream(ClassPathUtils.toResourceName(path)); + if (resource != null) { + try (InputStream is = resource) { + data = doRead(is); + } + } else { + try (InputStream is = Files.newInputStream(path)) { + data = doRead(is); + } + } + return data; + } + + private static void createPemKeyCertOptions(List certFile, List keyFile, + HttpServerOptions serverOptions) throws IOException { + + if (certFile.size() != keyFile.size()) { + throw new ConfigurationException("Invalid certificate configuration - `files` and `keyFiles` must have the " + + "same number of elements"); + } + + List certificates = new ArrayList<>(); + List keys = new ArrayList<>(); + + for (Path p : certFile) { + final byte[] cert = getFileContent(p); + certificates.add(Buffer.buffer(cert)); + } + + for (Path p : keyFile) { + final byte[] key = getFileContent(p); + keys.add(Buffer.buffer(key)); + } + + PemKeyCertOptions pemKeyCertOptions = new PemKeyCertOptions() + .setCertValues(certificates) + .setKeyValues(keys); + serverOptions.setPemKeyCertOptions(pemKeyCertOptions); + } + + private static String findKeystoreFileType(Path storePath) { + final String pathName = storePath.toString(); + if (pathName.endsWith(".p12") || pathName.endsWith(".pkcs12") || pathName.endsWith(".pfx")) { + return "pkcs12"; + } else { + // assume jks + return "jks"; + } + } + + private static byte[] doRead(InputStream is) throws IOException { + return is.readAllBytes(); + } + + private static void setIdleTimeout(HttpConfiguration httpConfiguration, HttpServerOptions options) { + int idleTimeout = (int) httpConfiguration.idleTimeout.toMillis(); + options.setIdleTimeout(idleTimeout); + options.setIdleTimeoutUnit(TimeUnit.MILLISECONDS); + } +} diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KnativeWithHealthUsingManagementInterfaceTest.java b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KnativeWithHealthUsingManagementInterfaceTest.java new file mode 100644 index 0000000000000..be58114f745dd --- /dev/null +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KnativeWithHealthUsingManagementInterfaceTest.java @@ -0,0 +1,87 @@ +package io.quarkus.it.kubernetes; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.fabric8.knative.serving.v1.Service; +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.api.model.Probe; +import io.quarkus.builder.Version; +import io.quarkus.maven.dependency.Dependency; +import io.quarkus.test.ProdBuildResults; +import io.quarkus.test.ProdModeTestResults; +import io.quarkus.test.QuarkusProdModeTest; + +public class KnativeWithHealthUsingManagementInterfaceTest { + + @RegisterExtension + static final QuarkusProdModeTest config = new QuarkusProdModeTest() + .withApplicationRoot((jar) -> jar.addClasses(GreetingResource.class)) + .setApplicationName("knative-health") + .setApplicationVersion("0.1-SNAPSHOT") + .withConfigurationResource("knative-with-health-and-management.properties") + .setForcedDependencies(List.of(Dependency.of("io.quarkus", "quarkus-smallrye-health", Version.getVersion()))); + + @ProdBuildResults + private ProdModeTestResults prodModeTestResults; + + @Test + public void assertGeneratedResources() throws IOException { + Path kubernetesDir = prodModeTestResults.getBuildDir().resolve("kubernetes"); + assertThat(kubernetesDir) + .isDirectoryContaining(p -> p.getFileName().endsWith("knative.json")) + .isDirectoryContaining(p -> p.getFileName().endsWith("knative.yml")) + .satisfies(p -> assertThat(p.toFile().listFiles()).hasSize(2)); + + List kubernetesList = DeserializationUtil + .deserializeAsList(kubernetesDir.resolve("knative.yml")); + + assertThat(kubernetesList).filteredOn(i -> "Service".equals(i.getKind())).singleElement().satisfies(i -> { + assertThat(i).isInstanceOfSatisfying(Service.class, s -> { + assertThat(s.getSpec()).satisfies(spec -> { + assertThat(s.getMetadata()).satisfies(m -> { + assertThat(m.getNamespace()).isNull(); + }); + + assertThat(spec.getTemplate()).satisfies(template -> { + assertThat(template.getSpec()).satisfies(templateSpec -> { + assertThat(templateSpec.getContainers()).hasSize(1).singleElement().satisfies(c -> { + assertThat(c.getPorts()).hasSize(1).anySatisfy(p -> { + assertThat(p.getName()).isEqualTo("http1"); + }); + assertThat(c.getReadinessProbe()).isNotNull().satisfies(p -> { + assertThat(p.getInitialDelaySeconds()).isEqualTo(5); + assertProbePath(p, "/q/health/ready"); + + assertNotNull(p.getHttpGet()); + assertNull(p.getHttpGet().getPort()); + }); + assertThat(c.getLivenessProbe()).isNotNull().satisfies(p -> { + assertThat(p.getInitialDelaySeconds()).isEqualTo(20); + assertProbePath(p, "/q/health/live"); + + assertNotNull(p.getHttpGet()); + assertNull(p.getHttpGet().getPort()); + }); + }); + }); + }); + }); + }); + }); + } + + private void assertProbePath(Probe p, String expectedPath) { + assertThat(p.getHttpGet()).satisfies(h -> { + assertThat(h.getPath()).isEqualTo(expectedPath); + }); + } +} diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithHealthUsingManagementInterfaceTest.java b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithHealthUsingManagementInterfaceTest.java new file mode 100644 index 0000000000000..27c8667af1908 --- /dev/null +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithHealthUsingManagementInterfaceTest.java @@ -0,0 +1,100 @@ +package io.quarkus.it.kubernetes; + +import static io.restassured.RestAssured.given; +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.api.model.Probe; +import io.fabric8.kubernetes.api.model.apps.Deployment; +import io.quarkus.builder.Version; +import io.quarkus.maven.dependency.Dependency; +import io.quarkus.test.LogFile; +import io.quarkus.test.ProdBuildResults; +import io.quarkus.test.ProdModeTestResults; +import io.quarkus.test.QuarkusProdModeTest; + +public class KubernetesWithHealthUsingManagementInterfaceTest { + + @RegisterExtension + static final QuarkusProdModeTest config = new QuarkusProdModeTest() + .withApplicationRoot((jar) -> jar.addClasses(GreetingResource.class)) + .setApplicationName("health") + .setApplicationVersion("0.1-SNAPSHOT") + .setRun(true) + .setLogFileName("k8s.log") + .withConfigurationResource("kubernetes-with-health-and-management.properties") + .setForcedDependencies(List.of( + Dependency.of("io.quarkus", "quarkus-smallrye-health", Version.getVersion()))); + + @ProdBuildResults + private ProdModeTestResults prodModeTestResults; + + @LogFile + private Path logfile; + + @Test + public void assertApplicationRuns() { + assertThat(logfile).isRegularFile().hasFileName("k8s.log"); + TestUtil.assertLogFileContents(logfile, "kubernetes", "health"); + + given() + .when().get("/greeting") + .then() + .statusCode(200) + .body(is("hello")); + } + + @Test + public void assertGeneratedResources() throws IOException { + final Path kubernetesDir = prodModeTestResults.getBuildDir().resolve("kubernetes"); + assertThat(kubernetesDir) + .isDirectoryContaining(p -> p.getFileName().endsWith("kubernetes.json")) + .isDirectoryContaining(p -> p.getFileName().endsWith("kubernetes.yml")); + List kubernetesList = DeserializationUtil + .deserializeAsList(kubernetesDir.resolve("kubernetes.yml")); + assertThat(kubernetesList.get(0)).isInstanceOfSatisfying(Deployment.class, d -> { + assertThat(d.getMetadata()).satisfies(m -> { + assertThat(m.getName()).isEqualTo("health"); + }); + + assertThat(d.getSpec()).satisfies(deploymentSpec -> { + assertThat(deploymentSpec.getTemplate()).satisfies(t -> { + assertThat(t.getSpec()).satisfies(podSpec -> { + assertThat(podSpec.getContainers()).singleElement().satisfies(container -> { + assertThat(container.getReadinessProbe()).isNotNull().satisfies(p -> { + assertThat(p.getInitialDelaySeconds()).isEqualTo(5); + assertProbePath(p, "/q/health/ready"); + + assertNotNull(p.getHttpGet()); + assertEquals(p.getHttpGet().getPort().getIntVal(), 9000); + }); + assertThat(container.getLivenessProbe()).isNotNull().satisfies(p -> { + assertThat(p.getInitialDelaySeconds()).isEqualTo(20); + assertProbePath(p, "/liveness"); + + assertNotNull(p.getHttpGet()); + assertEquals(p.getHttpGet().getPort().getIntVal(), 9000); + }); + }); + }); + }); + }); + }); + } + + private void assertProbePath(Probe p, String expectedPath) { + assertThat(p.getHttpGet()).satisfies(h -> { + assertThat(h.getPath()).isEqualTo(expectedPath); + }); + } +} diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/OpenshiftWithHealthUsingManagementInterfaceTest.java b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/OpenshiftWithHealthUsingManagementInterfaceTest.java new file mode 100644 index 0000000000000..f46b3487ef657 --- /dev/null +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/OpenshiftWithHealthUsingManagementInterfaceTest.java @@ -0,0 +1,89 @@ +package io.quarkus.it.kubernetes; + +import static io.restassured.RestAssured.given; +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.is; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.api.model.IntOrString; +import io.fabric8.kubernetes.api.model.PodSpec; +import io.quarkus.builder.Version; +import io.quarkus.maven.dependency.Dependency; +import io.quarkus.test.LogFile; +import io.quarkus.test.ProdBuildResults; +import io.quarkus.test.ProdModeTestResults; +import io.quarkus.test.QuarkusProdModeTest; + +public class OpenshiftWithHealthUsingManagementInterfaceTest { + + @RegisterExtension + static final QuarkusProdModeTest config = new QuarkusProdModeTest() + .withApplicationRoot((jar) -> jar.addClasses(GreetingResource.class)) + .setApplicationName("openshift-health") + .setApplicationVersion("0.1-SNAPSHOT") + .setRun(true) + .setLogFileName("k8s.log") + .withConfigurationResource("openshift-with-health-and-management.properties") + .setForcedDependencies(List.of(Dependency.of("io.quarkus", "quarkus-smallrye-health", Version.getVersion()))); + + @ProdBuildResults + private ProdModeTestResults prodModeTestResults; + + @LogFile + private Path logfile; + + @Test + public void assertApplicationRuns() { + assertThat(logfile).isRegularFile().hasFileName("k8s.log"); + TestUtil.assertLogFileContents(logfile, "kubernetes", "health"); + + given() + .when().get("/greeting") + .then() + .statusCode(200) + .body(is("hello")); + } + + @Test + public void assertGeneratedResources() throws IOException { + Path kubernetesDir = prodModeTestResults.getBuildDir().resolve("kubernetes"); + assertThat(kubernetesDir) + .isDirectoryContaining(p -> p.getFileName().endsWith("openshift.json")) + .isDirectoryContaining(p -> p.getFileName().endsWith("openshift.yml")); + List openshiftList = DeserializationUtil + .deserializeAsList(kubernetesDir.resolve("openshift.yml")); + + assertThat(openshiftList).filteredOn(h -> "DeploymentConfig".equals(h.getKind())).singleElement().satisfies(h -> { + assertThat(h.getMetadata()).satisfies(m -> { + assertThat(m.getName()).isEqualTo("openshift-health"); + }); + assertThat(h).extracting("spec").extracting("template").extracting("spec").isInstanceOfSatisfying(PodSpec.class, + podSpec -> { + assertThat(podSpec.getContainers()).singleElement().satisfies(container -> { + assertThat(container.getReadinessProbe()).isNotNull().satisfies(p -> { + assertThat(p.getPeriodSeconds()).isEqualTo(15); + assertThat(p.getHttpGet()).satisfies(h1 -> { + assertThat(h1.getPath()).isEqualTo("/q/health/ready"); + assertThat(h1.getPort()).isEqualTo(new IntOrString(9000)); + }); + }); + assertThat(container.getLivenessProbe()).isNotNull().satisfies(p -> { + assertThat(p.getPeriodSeconds()).isEqualTo(10); + assertThat(p.getHttpGet()).isNull(); + assertThat(p.getExec()).satisfies(e -> { + assertThat(e.getCommand()).containsOnly("kill"); + }); + }); + }); + }); + }); + } + +} diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/knative-with-health-and-management.properties b/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/knative-with-health-and-management.properties new file mode 100644 index 0000000000000..69e3a940c946e --- /dev/null +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/knative-with-health-and-management.properties @@ -0,0 +1,5 @@ +quarkus.http.port=9090 +quarkus.kubernetes.deployment-target=knative +quarkus.knative.liveness-probe.initial-delay=20s + +quarkus.management.enabled=true diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-with-health-and-management.properties b/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-with-health-and-management.properties new file mode 100644 index 0000000000000..a84cf575f58a4 --- /dev/null +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-with-health-and-management.properties @@ -0,0 +1,4 @@ +quarkus.http.port=9090 +quarkus.management.enabled=true +quarkus.kubernetes.liveness-probe.initial-delay=20s +quarkus.smallrye-health.liveness-path=/liveness diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/openshift-with-health-and-management.properties b/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/openshift-with-health-and-management.properties new file mode 100644 index 0000000000000..b60483f17faa6 --- /dev/null +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/openshift-with-health-and-management.properties @@ -0,0 +1,5 @@ +quarkus.kubernetes.deployment-target=openshift +quarkus.openshift.readiness-probe.period=15s +quarkus.openshift.liveness-probe.exec-action=kill + +quarkus.management.enabled=true \ No newline at end of file diff --git a/integration-tests/management-interface/pom.xml b/integration-tests/management-interface/pom.xml new file mode 100644 index 0000000000000..15c9ab9fd250d --- /dev/null +++ b/integration-tests/management-interface/pom.xml @@ -0,0 +1,102 @@ + + + 4.0.0 + + + quarkus-integration-tests-parent + io.quarkus + 999-SNAPSHOT + + + quarkus-integration-test-management-interface + Quarkus - Integration Tests - Management Interface + + + + io.quarkus + quarkus-resteasy-reactive-jackson + + + io.quarkus + quarkus-smallrye-health + + + io.quarkus + quarkus-micrometer-registry-prometheus + + + io.quarkus + quarkus-junit5 + test + + + io.rest-assured + rest-assured + test + + + org.awaitility + awaitility + test + + + + + io.quarkus + quarkus-resteasy-reactive-jackson-deployment + ${project.version} + pom + test + + + * + * + + + + + io.quarkus + quarkus-micrometer-registry-prometheus-deployment + ${project.version} + pom + test + + + * + * + + + + + io.quarkus + quarkus-smallrye-health-deployment + ${project.version} + pom + test + + + * + * + + + + + + + + + io.quarkus + quarkus-maven-plugin + + + + build + + + + + + + diff --git a/integration-tests/management-interface/src/main/java/io/quarkus/it/management/GreetingResource.java b/integration-tests/management-interface/src/main/java/io/quarkus/it/management/GreetingResource.java new file mode 100644 index 0000000000000..dd83f77b59f52 --- /dev/null +++ b/integration-tests/management-interface/src/main/java/io/quarkus/it/management/GreetingResource.java @@ -0,0 +1,13 @@ +package io.quarkus.it.management; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +@Path("/hello") +public class GreetingResource { + + @GET + public String hello() { + return "hello"; + } +} diff --git a/integration-tests/management-interface/src/main/resources/application.properties b/integration-tests/management-interface/src/main/resources/application.properties new file mode 100644 index 0000000000000..882657a8ebb1e --- /dev/null +++ b/integration-tests/management-interface/src/main/resources/application.properties @@ -0,0 +1 @@ +quarkus.management.enabled=true \ No newline at end of file diff --git a/integration-tests/management-interface/src/test/java/io/quarkus/it/management/ManagementInterfaceIT.java b/integration-tests/management-interface/src/test/java/io/quarkus/it/management/ManagementInterfaceIT.java new file mode 100644 index 0000000000000..66760f8e08096 --- /dev/null +++ b/integration-tests/management-interface/src/test/java/io/quarkus/it/management/ManagementInterfaceIT.java @@ -0,0 +1,12 @@ +package io.quarkus.it.management; + +import io.quarkus.test.junit.QuarkusIntegrationTest; + +@QuarkusIntegrationTest +public class ManagementInterfaceIT extends ManagementInterfaceTestCase { + + @Override + protected String getPrefix() { + return "http://localhost:9000"; // ITs run in prod mode. + } +} diff --git a/integration-tests/management-interface/src/test/java/io/quarkus/it/management/ManagementInterfaceTestCase.java b/integration-tests/management-interface/src/test/java/io/quarkus/it/management/ManagementInterfaceTestCase.java new file mode 100644 index 0000000000000..703d74bda9aa3 --- /dev/null +++ b/integration-tests/management-interface/src/test/java/io/quarkus/it/management/ManagementInterfaceTestCase.java @@ -0,0 +1,36 @@ +package io.quarkus.it.management; + +import org.hamcrest.Matchers; +import org.junit.jupiter.api.Test; + +import io.quarkus.test.junit.QuarkusTest; +import io.restassured.RestAssured; + +@QuarkusTest +public class ManagementInterfaceTestCase { + + protected String getPrefix() { + return "http://localhost:9001"; + } + + @Test + void verifyThatHealthChecksAreExposedOnManagementInterface() { + RestAssured.get(getPrefix() + "/q/health") + .then().statusCode(200) + .body(Matchers.containsString("UP")); + + RestAssured.get("/q/health") + .then().statusCode(404); + } + + @Test + void verifyThatMetricsAreExposedOnManagementInterface() { + RestAssured.get(getPrefix() + "/q/metrics") + .then().statusCode(200) + .body(Matchers.containsString("http_server_bytes_read")); + + RestAssured.get("/q/metrics") + .then().statusCode(404); + } + +} diff --git a/integration-tests/pom.xml b/integration-tests/pom.xml index 42662af0b9f60..69c1057e25fdf 100644 --- a/integration-tests/pom.xml +++ b/integration-tests/pom.xml @@ -356,6 +356,7 @@ google-cloud-functions-http google-cloud-functions istio + management-interface