From e99c4188ea243925980b3c4fcccbb861d71c7ccb Mon Sep 17 00:00:00 2001 From: Clement Escoffier Date: Fri, 20 Jan 2023 14:39:17 +0100 Subject: [PATCH] Adds management interface support. It allows exposing selected routes (management routes) to a different HTTP server. It avoids exposing these management routes on the main HTTP server, which could lead to leaks and undesired access to these endpoints. Enabling/Disabling the management interface is a build-time property. However, the interface, port, and SSL... are runtime values. The management interface is not intended to be used using native transport (as high concurrency is rarely a need for these endpoints). Also, the access log and same site cookie are not supported yet. The management interface does not expose plain and secured endpoints. It's either using HTTP or HTTPS. At the moment are considered management routes: * health routes (but not the health-ui) * prometheus routes * metrics routes The management interface is, when enabled, exposed on the port 9000 (9001 in test mode). When deploying to Kubernetes and Openshift, the `management` port is also exposed. The Prometheus scrape url and the health checks probes are configured to use that `management` port. The documentation and configuration javadoc of the SmallRye Metrics, SmallRye health, Micrometer, Vert.x HTTP extensions has been extended to mention the configuration differences when the management interface is enabled. Typically, the health/metrics root paths are not resolved from the same root. The non-application endpoint paths are not resolved the same way. When using the dev ui (old and new) with the management interface enabled, the paths are resolved accordingly (for the health and prometheus extensions). --- .github/native-tests.json | 2 +- docs/src/main/asciidoc/http-reference.adoc | 12 +- .../management-interface-reference.adoc | 180 +++++ docs/src/main/asciidoc/micrometer.adoc | 30 + docs/src/main/asciidoc/smallrye-health.adoc | 21 + docs/src/main/asciidoc/smallrye-metrics.adoc | 9 + .../kind/deployment/KindProcessor.java | 2 +- .../deployment/MinikubeProcessor.java | 2 +- .../spi/KubernetesProbePortNameBuildItem.java | 2 +- .../kubernetes/vanilla/deployment/pom.xml | 4 + .../deployment/DevClusterHelper.java | 2 +- .../deployment/KnativeProcessor.java | 2 +- .../deployment/KubernetesCommonHelper.java | 2 +- .../deployment/OpenshiftProcessor.java | 2 +- .../VanillaKubernetesProcessor.java | 2 +- .../export/JsonRegistryProcessor.java | 12 +- .../export/PrometheusRegistryProcessor.java | 12 +- ...theusEnabledOnManagementInterfaceTest.java | 35 + .../runtime/config/JsonConfigGroup.java | 4 + .../runtime/config/PrometheusConfigGroup.java | 6 + .../test/devconsole/BodyHandlerBean.java | 4 +- .../deployment/SmallRyeHealthConfig.java | 2 + .../SmallRyeHealthDevUiProcessor.java | 10 +- .../deployment/SmallRyeHealthProcessor.java | 44 +- ...heckOnManagementInterfaceDisabledTest.java | 57 ++ .../HealthCheckOnManagementInterfaceTest.java | 58 ++ ...mentInterfaceWithAbsoluteRootPathTest.java | 58 ++ ...mentInterfaceWithRelativeRootPathTest.java | 59 ++ .../deployment/SmallRyeMetricsProcessor.java | 5 + .../spi/UseManagementInterfaceBuildItem.java | 10 + .../deployment/HttpRootPathBuildItem.java | 8 +- .../NonApplicationRootPathBuildItem.java | 149 +++- .../vertx/http/deployment/RouteBuildItem.java | 27 +- .../http/deployment/VertxHttpProcessor.java | 51 +- .../deployment/VertxWebRouterBuildItem.java | 15 +- .../devmode/console/ConfiguredPathInfo.java | 13 +- .../devmode/console/DevConsoleProcessor.java | 9 +- .../http/NonApplicationAndRootPathTest.java | 3 +- .../NonApplicationRootPathBuildItemTest.java | 168 +++- .../http/devconsole/BodyHandlerBean.java | 4 +- .../management/ManagementAndRootPathTest.java | 82 ++ .../management/ManagementWithJksTest.java | 89 ++ .../ManagementWithMainServerDisabledTest.java | 81 ++ .../management/ManagementWithP12Test.java | 89 ++ .../management/ManagementWithPemTest.java | 90 +++ .../http/runtime/ForwardedProxyHandler.java | 4 +- .../ForwardedServerRequestWrapper.java | 4 +- .../http/runtime/ForwardingProxyOptions.java | 24 +- .../http/runtime/HttpBuildTimeConfig.java | 3 + .../http/runtime/ResumingRequestWrapper.java | 4 +- .../vertx/http/runtime/TrustedProxyCheck.java | 2 +- .../vertx/http/runtime/VertxHttpRecorder.java | 765 ++++++++---------- .../ManagementInterfaceBuildTimeConfig.java | 65 ++ .../ManagementInterfaceConfiguration.java | 120 +++ .../options/HttpServerCommonHandlers.java | 177 ++++ .../options/HttpServerOptionsUtils.java | 386 +++++++++ ...ithHealthUsingManagementInterfaceTest.java | 87 ++ ...ithHealthUsingManagementInterfaceTest.java | 100 +++ ...ithHealthUsingManagementInterfaceTest.java | 89 ++ ...tive-with-health-and-management.properties | 5 + ...etes-with-health-and-management.properties | 4 + ...hift-with-health-and-management.properties | 5 + .../management-interface/pom.xml | 102 +++ .../it/management/GreetingResource.java | 13 + .../src/main/resources/application.properties | 1 + .../it/management/ManagementInterfaceIT.java | 12 + .../ManagementInterfaceTestCase.java | 36 + integration-tests/pom.xml | 1 + 68 files changed, 3014 insertions(+), 523 deletions(-) create mode 100644 docs/src/main/asciidoc/management-interface-reference.adoc create mode 100644 extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/deployment/export/PrometheusEnabledOnManagementInterfaceTest.java create mode 100644 extensions/smallrye-health/deployment/src/test/java/io/quarkus/smallrye/health/test/HealthCheckOnManagementInterfaceDisabledTest.java create mode 100644 extensions/smallrye-health/deployment/src/test/java/io/quarkus/smallrye/health/test/HealthCheckOnManagementInterfaceTest.java create mode 100644 extensions/smallrye-health/deployment/src/test/java/io/quarkus/smallrye/health/test/HealthCheckOnManagementInterfaceWithAbsoluteRootPathTest.java create mode 100644 extensions/smallrye-health/deployment/src/test/java/io/quarkus/smallrye/health/test/HealthCheckOnManagementInterfaceWithRelativeRootPathTest.java create mode 100644 extensions/vertx-http/deployment-spi/src/main/java/io/quarkus/vertx/http/deployment/spi/UseManagementInterfaceBuildItem.java create mode 100644 extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/management/ManagementAndRootPathTest.java create mode 100644 extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/management/ManagementWithJksTest.java create mode 100644 extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/management/ManagementWithMainServerDisabledTest.java create mode 100644 extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/management/ManagementWithP12Test.java create mode 100644 extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/management/ManagementWithPemTest.java create mode 100644 extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/management/ManagementInterfaceBuildTimeConfig.java create mode 100644 extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/management/ManagementInterfaceConfiguration.java create mode 100644 extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/options/HttpServerCommonHandlers.java create mode 100644 extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/options/HttpServerOptionsUtils.java create mode 100644 integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KnativeWithHealthUsingManagementInterfaceTest.java create mode 100644 integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithHealthUsingManagementInterfaceTest.java create mode 100644 integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/OpenshiftWithHealthUsingManagementInterfaceTest.java create mode 100644 integration-tests/kubernetes/quarkus-standard-way/src/test/resources/knative-with-health-and-management.properties create mode 100644 integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-with-health-and-management.properties create mode 100644 integration-tests/kubernetes/quarkus-standard-way/src/test/resources/openshift-with-health-and-management.properties create mode 100644 integration-tests/management-interface/pom.xml create mode 100644 integration-tests/management-interface/src/main/java/io/quarkus/it/management/GreetingResource.java create mode 100644 integration-tests/management-interface/src/main/resources/application.properties create mode 100644 integration-tests/management-interface/src/test/java/io/quarkus/it/management/ManagementInterfaceIT.java create mode 100644 integration-tests/management-interface/src/test/java/io/quarkus/it/management/ManagementInterfaceTestCase.java 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