Skip to content

Commit

Permalink
Enable usage of random port for the Management Interface
Browse files Browse the repository at this point in the history
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,...).
  • Loading branch information
cescoffier committed Mar 21, 2024
1 parent 886b78e commit d0d5ef5
Show file tree
Hide file tree
Showing 8 changed files with 257 additions and 27 deletions.
21 changes: 21 additions & 0 deletions docs/src/main/asciidoc/management-interface-reference.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Check warning on line 266 in docs/src/main/asciidoc/management-interface-reference.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.TermsSuggestions] Depending on the context, consider using 'by using' or 'that uses' rather than 'using'. Raw Output: {"message": "[Quarkus.TermsSuggestions] Depending on the context, consider using 'by using' or 'that uses' rather than 'using'.", "location": {"path": "docs/src/main/asciidoc/management-interface-reference.adoc", "range": {"start": {"line": 266, "column": 65}}}, "severity": "INFO"}

[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`.

Check warning on line 276 in docs/src/main/asciidoc/management-interface-reference.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.TermsWarnings] Consider using 'Therefore' rather than 'Thus' unless updating existing content that uses the term. Raw Output: {"message": "[Quarkus.TermsWarnings] Consider using 'Therefore' rather than 'Thus' unless updating existing content that uses the term.", "location": {"path": "docs/src/main/asciidoc/management-interface-reference.adoc", "range": {"start": {"line": 276, "column": 1}}}, "severity": "WARNING"}

`@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
----
Original file line number Diff line number Diff line change
@@ -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<BuildChainBuilder> buildCustomizer() {
return new Consumer<BuildChainBuilder>() {
@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<RoutingContext> {
@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
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,28 @@
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;
import java.io.IOException;
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;
Expand All @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -172,6 +191,7 @@ private boolean uriValid(HttpServerRequest httpServerRequest) {
private static HttpServerOptions httpManagementServerOptions;

private static final List<Long> refresTaskIds = new CopyOnWriteArrayList<>();

final HttpBuildTimeConfig httpBuildTimeConfig;
final ManagementInterfaceBuildTimeConfig managementBuildTimeConfig;
final RuntimeValue<HttpConfiguration> httpConfiguration;
Expand Down Expand Up @@ -629,7 +649,6 @@ private static CompletableFuture<HttpServer> initializeManagementInterface(Vertx
}

if (httpManagementServerOptions != null) {

vertx.createHttpServer(httpManagementServerOptions)
.requestHandler(managementRouter)
.listen(ar -> {
Expand All @@ -647,9 +666,21 @@ private static CompletableFuture<HttpServer> 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<Void> completion) {
managementPortSystemProperties.restore();
completion.complete();
}
});
}
managementInterfaceFuture.complete(ar.result());
}
});

} else {
managementInterfaceFuture.complete(null);
}
Expand All @@ -672,8 +703,7 @@ private static CompletableFuture<String> initializeMainHttpServer(Vertx vertx, H
httpMainDomainSocketOptions = createDomainSocketOptions(httpBuildTimeConfig, httpConfiguration,
websocketSubProtocols);
HttpServerOptions tmpSslConfig = HttpServerOptionsUtils.createSslOptions(httpBuildTimeConfig, httpConfiguration,
launchMode,
websocketSubProtocols);
launchMode, websocketSubProtocols);

// Customize
if (Arc.container() != null) {
Expand Down Expand Up @@ -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);
Expand All @@ -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);

Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, String> 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<String, String> 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:}}"));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,34 @@
/**
* Indicates that a field should be injected with a resource that is pre-configured
* to use the correct test URL.
*
* <p>
* This could be a String or URL object, or some other HTTP/Websocket based client.
*
* <p>
* This mechanism is pluggable, via {@link TestHTTPResourceProvider}
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
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;
}
Loading

0 comments on commit d0d5ef5

Please sign in to comment.