From d0d5ef58c91c2f477f4083f31a680ba27db435e0 Mon Sep 17 00:00:00 2001 From: Clement Escoffier Date: Wed, 20 Mar 2024 15:25:58 +0100 Subject: [PATCH] Enable usage of random port for the Management Interface In testing scenarios, setting the management interface port to a random value can be beneficial. This can be achieved by configuring `quarkus.management.test-port` to `0`. However, previously, retrieving the actual port in tests was not feasible. This commit addresses this limitation by introducing the following enhancements: - It stores the actual management port in a system property when it differs from the configured port. - It enables the injection of the actual management port using @TestHTTPResource(management=true,...). --- .../management-interface-reference.adoc | 21 ++++ .../ManagementAndPrimaryOnPortZeroTest.java | 113 ++++++++++++++++++ .../management/ManagementAndRootPathTest.java | 7 +- .../vertx/http/runtime/VertxHttpRecorder.java | 48 ++++++-- .../options/HttpServerOptionsUtils.java | 21 +++- .../http/TestHTTPConfigSourceProvider.java | 11 +- .../test/common/http/TestHTTPResource.java | 18 ++- .../common/http/TestHTTPResourceManager.java | 45 +++++-- 8 files changed, 257 insertions(+), 27 deletions(-) create mode 100644 extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/management/ManagementAndPrimaryOnPortZeroTest.java diff --git a/docs/src/main/asciidoc/management-interface-reference.adoc b/docs/src/main/asciidoc/management-interface-reference.adoc index 0f245b3875795..124f558b95c92 100644 --- a/docs/src/main/asciidoc/management-interface-reference.adoc +++ b/docs/src/main/asciidoc/management-interface-reference.adoc @@ -260,3 +260,24 @@ quarkus.management.auth.permission.metrics.policy=authenticated ---- More details about Basic authentication in Quarkus can be found in the xref:security-basic-authentication-howto.adoc[Basic authentication guide]. + +== Injecting management URL in tests + +When testing your application, you can inject the management URL using the `@TestHTTPResource` annotation: + +[source,java] +---- +@TestHTTPResource(value="/management", management=true) +URL management; +---- + +The `management` attribute is set to `true` to indicate that the injected URL is for the management interface. +The `context-root` is automatically added. +Thus, in the previous example, the injected URL is `http://localhost:9001/q/management`. + +`@TestHTTPResource` is particularly useful when setting the management `test-port` to 0, which indicates that the system will assign a random port to the management interface: + +[source, properties] +----] +quarkus.management.test-port=0 +---- \ No newline at end of file diff --git a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/management/ManagementAndPrimaryOnPortZeroTest.java b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/management/ManagementAndPrimaryOnPortZeroTest.java new file mode 100644 index 0000000000000..feb94b9010f29 --- /dev/null +++ b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/management/ManagementAndPrimaryOnPortZeroTest.java @@ -0,0 +1,113 @@ +package io.quarkus.vertx.http.management; + +import java.net.URL; +import java.util.function.Consumer; + +import jakarta.enterprise.event.Observes; +import jakarta.inject.Singleton; + +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.hamcrest.Matchers; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.Assertions; +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.test.common.http.TestHTTPResource; +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.Router; +import io.vertx.ext.web.RoutingContext; + +public class ManagementAndPrimaryOnPortZeroTest { + private static final String APP_PROPS = """ + quarkus.management.enabled=true + quarkus.management.test-port=0 + quarkus.http.test-port=0 + """; + + @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") + .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("Hello management"); + } + } + + @TestHTTPResource(value = "/route") + URL url; + + @TestHTTPResource(value = "/management", management = true) + URL management; + + @ConfigProperty(name = "quarkus.management.test-port") + int managementPort; + + @ConfigProperty(name = "quarkus.http.test-port") + int primaryPort; + + @Test + public void test() { + Assertions.assertNotEquals(url.getPort(), management.getPort()); + Assertions.assertEquals(url.getPort(), primaryPort); + Assertions.assertEquals(management.getPort(), managementPort); + + for (int i = 0; i < 10; i++) { + RestAssured.given().get(url.toExternalForm()).then().body(Matchers.is("Hello primary")); + } + + for (int i = 0; i < 10; i++) { + RestAssured.given().get(management.toExternalForm()).then().body(Matchers.is("Hello management")); + } + + } + + @Singleton + static class MyObserver { + + void register(@Observes Router router) { + router.get("/route").handler(rc -> rc.response().end("Hello primary")); + } + + void test(@Observes String event) { + //Do Nothing + } + + } +} 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 index 7fc85520f8fb3..e47c3adf2d6ef 100644 --- 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 @@ -21,9 +21,10 @@ 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"; + private static final String APP_PROPS = """ + quarkus.management.enabled=true + quarkus.management.root-path=/management + """; @RegisterExtension static final QuarkusUnitTest config = new QuarkusUnitTest() 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 ac5ca3a408310..7254355921017 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,6 +1,8 @@ package io.quarkus.vertx.http.runtime; import static io.quarkus.vertx.core.runtime.context.VertxContextSafetyToggle.setContextSafe; +import static io.quarkus.vertx.http.runtime.options.HttpServerOptionsUtils.RANDOM_PORT_MAIN_HTTP; +import static io.quarkus.vertx.http.runtime.options.HttpServerOptionsUtils.RANDOM_PORT_MANAGEMENT; import static io.quarkus.vertx.http.runtime.options.HttpServerOptionsUtils.getInsecureRequestStrategy; import java.io.File; @@ -8,8 +10,19 @@ import java.net.BindException; import java.net.URI; import java.net.URISyntaxException; -import java.util.*; -import java.util.concurrent.*; +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.TreeMap; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executor; +import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.*; import java.util.regex.Pattern; @@ -35,7 +48,12 @@ import io.quarkus.netty.runtime.virtual.VirtualAddress; import io.quarkus.netty.runtime.virtual.VirtualChannel; import io.quarkus.netty.runtime.virtual.VirtualServerChannel; -import io.quarkus.runtime.*; +import io.quarkus.runtime.LaunchMode; +import io.quarkus.runtime.LiveReloadConfig; +import io.quarkus.runtime.QuarkusBindException; +import io.quarkus.runtime.RuntimeValue; +import io.quarkus.runtime.ShutdownContext; +import io.quarkus.runtime.ThreadPoolConfig; import io.quarkus.runtime.annotations.Recorder; import io.quarkus.runtime.configuration.ConfigInstantiator; import io.quarkus.runtime.configuration.ConfigUtils; @@ -64,6 +82,7 @@ import io.smallrye.common.vertx.VertxContext; import io.vertx.core.AbstractVerticle; import io.vertx.core.AsyncResult; +import io.vertx.core.Closeable; import io.vertx.core.Context; import io.vertx.core.DeploymentOptions; import io.vertx.core.Handler; @@ -172,6 +191,7 @@ private boolean uriValid(HttpServerRequest httpServerRequest) { private static HttpServerOptions httpManagementServerOptions; private static final List refresTaskIds = new CopyOnWriteArrayList<>(); + final HttpBuildTimeConfig httpBuildTimeConfig; final ManagementInterfaceBuildTimeConfig managementBuildTimeConfig; final RuntimeValue httpConfiguration; @@ -629,7 +649,6 @@ private static CompletableFuture initializeManagementInterface(Vertx } if (httpManagementServerOptions != null) { - vertx.createHttpServer(httpManagementServerOptions) .requestHandler(managementRouter) .listen(ar -> { @@ -647,9 +666,21 @@ private static CompletableFuture initializeManagementInterface(Vertx } actualManagementPort = ar.result().actualPort(); + if (actualManagementPort != httpManagementServerOptions.getPort()) { + var managementPortSystemProperties = new PortSystemProperties(); + managementPortSystemProperties.set("management", actualManagementPort, launchMode); + ((VertxInternal) vertx).addCloseHook(new Closeable() { + @Override + public void close(Promise completion) { + managementPortSystemProperties.restore(); + completion.complete(); + } + }); + } managementInterfaceFuture.complete(ar.result()); } }); + } else { managementInterfaceFuture.complete(null); } @@ -672,8 +703,7 @@ private static CompletableFuture initializeMainHttpServer(Vertx vertx, H httpMainDomainSocketOptions = createDomainSocketOptions(httpBuildTimeConfig, httpConfiguration, websocketSubProtocols); HttpServerOptions tmpSslConfig = HttpServerOptionsUtils.createSslOptions(httpBuildTimeConfig, httpConfiguration, - launchMode, - websocketSubProtocols); + launchMode, websocketSubProtocols); // Customize if (Arc.container() != null) { @@ -891,7 +921,7 @@ private static void setHttpServerTiming(boolean httpDisabled, HttpServerOptions if (managementConfig != null) { serverListeningMessage.append( String.format(". Management interface listening on http%s://%s:%s.", managementConfig.isSsl() ? "s" : "", - managementConfig.getHost(), managementConfig.getPort())); + managementConfig.getHost(), actualManagementPort)); } Timing.setHttpServer(serverListeningMessage.toString(), auxiliaryApplication); @@ -906,7 +936,7 @@ private static HttpServerOptions createHttpServerOptions( // TODO other config properties HttpServerOptions options = new HttpServerOptions(); int port = httpConfiguration.determinePort(launchMode); - options.setPort(port == 0 ? -1 : port); + options.setPort(port == 0 ? RANDOM_PORT_MAIN_HTTP : port); HttpServerOptionsUtils.applyCommonOptions(options, buildTimeConfig, httpConfiguration, websocketSubProtocols); @@ -921,7 +951,7 @@ private static HttpServerOptions createHttpServerOptionsForManagementInterface( } HttpServerOptions options = new HttpServerOptions(); int port = httpConfiguration.determinePort(launchMode); - options.setPort(port == 0 ? -1 : port); + options.setPort(port == 0 ? RANDOM_PORT_MANAGEMENT : port); HttpServerOptionsUtils.applyCommonOptionsForManagementInterface(options, buildTimeConfig, httpConfiguration, websocketSubProtocols); 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 index a16c972884260..e36e7475bf317 100644 --- 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 @@ -31,6 +31,21 @@ @SuppressWarnings("OptionalIsPresent") public class HttpServerOptionsUtils { + /** + * When the http port is set to 0, replace it by this value to let Vert.x choose a random port + */ + public static final int RANDOM_PORT_MAIN_HTTP = -1; + + /** + * When the https port is set to 0, replace it by this value to let Vert.x choose a random port + */ + public static final int RANDOM_PORT_MAIN_TLS = -2; + + /** + * When the management port is set to 0, replace it by this value to let Vert.x choose a random port + */ + public static final int RANDOM_PORT_MANAGEMENT = -3; + /** * Get an {@code HttpServerOptions} for this server configuration, or null if SSL should not be enabled */ @@ -108,7 +123,7 @@ public static HttpServerOptions createSslOptions(HttpBuildTimeConfig buildTimeCo 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.setPort(sslPort == 0 ? RANDOM_PORT_MAIN_TLS : sslPort); serverOptions.setClientAuth(buildTimeConfig.tlsClientAuth); applyCommonOptions(serverOptions, buildTimeConfig, httpConfiguration, websocketSubProtocols); @@ -194,8 +209,8 @@ public static HttpServerOptions createSslOptionsForManagementInterface(Managemen 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.setPort(sslPort == 0 ? RANDOM_PORT_MANAGEMENT : sslPort); serverOptions.setClientAuth(buildTimeConfig.tlsClientAuth); applyCommonOptionsForManagementInterface(serverOptions, buildTimeConfig, httpConfiguration, websocketSubProtocols); diff --git a/test-framework/common/src/main/java/io/quarkus/test/common/http/TestHTTPConfigSourceProvider.java b/test-framework/common/src/main/java/io/quarkus/test/common/http/TestHTTPConfigSourceProvider.java index fe689675d00c7..346823c776e4c 100644 --- a/test-framework/common/src/main/java/io/quarkus/test/common/http/TestHTTPConfigSourceProvider.java +++ b/test-framework/common/src/main/java/io/quarkus/test/common/http/TestHTTPConfigSourceProvider.java @@ -16,11 +16,20 @@ public class TestHTTPConfigSourceProvider implements ConfigSourceProvider { static final String TEST_URL_VALUE = "http://${quarkus.http.host:localhost}:${quarkus.http.test-port:8081}${quarkus.http.root-path:${quarkus.servlet.context-path:}}"; static final String TEST_URL_KEY = "test.url"; + static final String TEST_MANAGEMENT_URL_VALUE = "http://${quarkus.management.host:localhost}:${quarkus.management.test-port:9001}${quarkus.management.root-path:/q}"; + static final String TEST_MANAGEMENT_URL_KEY = "test.management.url"; + static final String TEST_URL_SSL_VALUE = "https://${quarkus.http.host:localhost}:${quarkus.http.test-ssl-port:8444}${quarkus.http.root-path:${quarkus.servlet.context-path:}}"; static final String TEST_URL_SSL_KEY = "test.url.ssl"; - static final Map entries = Map.of(TEST_URL_KEY, sanitizeURL(TEST_URL_VALUE), + static final String TEST_MANAGEMENT_URL_SSL_VALUE = "https://${quarkus.management.host:localhost}:${quarkus.management.test-ssl-port:9001}${quarkus.management.root-path:/q}"; + static final String TEST_MANAGEMENT_URL_SSL_KEY = "test.management.url.ssl"; + + static final Map entries = Map.of( + TEST_URL_KEY, sanitizeURL(TEST_URL_VALUE), TEST_URL_SSL_KEY, sanitizeURL(TEST_URL_SSL_VALUE), + TEST_MANAGEMENT_URL_KEY, sanitizeURL(TEST_MANAGEMENT_URL_VALUE), + TEST_MANAGEMENT_URL_SSL_KEY, sanitizeURL(TEST_MANAGEMENT_URL_SSL_VALUE), "%dev." + TEST_URL_KEY, sanitizeURL( "http://${quarkus.http.host:localhost}:${quarkus.http.test-port:8080}${quarkus.http.root-path:${quarkus.servlet.context-path:}}")); diff --git a/test-framework/common/src/main/java/io/quarkus/test/common/http/TestHTTPResource.java b/test-framework/common/src/main/java/io/quarkus/test/common/http/TestHTTPResource.java index 1761fbc391689..03026bfd6e0a2 100644 --- a/test-framework/common/src/main/java/io/quarkus/test/common/http/TestHTTPResource.java +++ b/test-framework/common/src/main/java/io/quarkus/test/common/http/TestHTTPResource.java @@ -8,9 +8,9 @@ /** * Indicates that a field should be injected with a resource that is pre-configured * to use the correct test URL. - * + *

* This could be a String or URL object, or some other HTTP/Websocket based client. - * + *

* This mechanism is pluggable, via {@link TestHTTPResourceProvider} */ @Retention(RetentionPolicy.RUNTIME) @@ -18,14 +18,24 @@ public @interface TestHTTPResource { /** - * * @return The path part of the URL */ String value() default ""; /** - * * @return If the URL should use the HTTPS protocol and SSL port + * @deprecated use #tls instead */ + @Deprecated boolean ssl() default false; + + /** + * @return if the url should use the management interface + */ + boolean management() default false; + + /** + * @return If the URL should use the HTTPS protocol and TLS port + */ + boolean tls() default false; } diff --git a/test-framework/common/src/main/java/io/quarkus/test/common/http/TestHTTPResourceManager.java b/test-framework/common/src/main/java/io/quarkus/test/common/http/TestHTTPResourceManager.java index 8e177e77ce235..87b1e87ad91b6 100644 --- a/test-framework/common/src/main/java/io/quarkus/test/common/http/TestHTTPResourceManager.java +++ b/test-framework/common/src/main/java/io/quarkus/test/common/http/TestHTTPResourceManager.java @@ -25,10 +25,24 @@ public static String getUri() { } } + public static String getManagementUri() { + try { + return sanitizeUri(ConfigProvider.getConfig().getValue("test.management.url", String.class)); + } catch (IllegalStateException e) { + //massive hack for dev mode tests, dev mode has not started yet + //so we don't have any way to load this correctly from config + return "http://localhost:9000"; + } + } + public static String getSslUri() { return sanitizeUri(ConfigProvider.getConfig().getValue("test.url.ssl", String.class)); } + public static String getManagementSslUri() { + return sanitizeUri(ConfigProvider.getConfig().getValue("test.management.url.ssl", String.class)); + } + private static String sanitizeUri(String result) { if ((result != null) && result.endsWith("/")) { return result.substring(0, result.length() - 1); @@ -62,6 +76,7 @@ public static void inject(Object testCase, List, String>> endp } String path = resource.value(); String endpointPath = null; + boolean management = resource.management(); TestHTTPEndpoint endpointAnnotation = f.getAnnotation(TestHTTPEndpoint.class); if (endpointAnnotation != null) { for (Function, String> func : endpointProviders) { @@ -86,17 +101,33 @@ public static void inject(Object testCase, List, String>> endp path = endpointPath; } String val; - if (resource.ssl()) { - if (path.startsWith("/")) { - val = getSslUri() + path; + if (resource.ssl() || resource.tls()) { + if (management) { + if (path.startsWith("/")) { + val = getManagementSslUri() + path; + } else { + val = getManagementSslUri() + "/" + path; + } } else { - val = getSslUri() + "/" + path; + if (path.startsWith("/")) { + val = getSslUri() + path; + } else { + val = getSslUri() + "/" + path; + } } } else { - if (path.startsWith("/")) { - val = getUri() + path; + if (management) { + if (path.startsWith("/")) { + val = getManagementUri() + path; + } else { + val = getManagementUri() + "/" + path; + } } else { - val = getUri() + "/" + path; + if (path.startsWith("/")) { + val = getUri() + path; + } else { + val = getUri() + "/" + path; + } } } f.setAccessible(true);