diff --git a/bom/application/pom.xml b/bom/application/pom.xml
index 0372530525214..c25ede4dfc633 100644
--- a/bom/application/pom.xml
+++ b/bom/application/pom.xml
@@ -3593,6 +3593,11 @@
hamcrest
${hamcrest.version}
+
+ me.escoffier.certs
+ certificate-generator-junit5
+ 0.3.0
+
org.antlr
diff --git a/docs/src/main/asciidoc/http-reference.adoc b/docs/src/main/asciidoc/http-reference.adoc
index 22c9db0150bcc..dbab826dc68cb 100644
--- a/docs/src/main/asciidoc/http-reference.adoc
+++ b/docs/src/main/asciidoc/http-reference.adoc
@@ -239,8 +239,8 @@ Configure the `quarkus.http.ssl.certificate.reload-period` property to specify t
[source, properties]
----
-quarkus.http.ssl.certificate.files=/mount/certs/cert.pem
-quarkus.http.ssl.certificate.key-files=/mount/certs/key.pem
+quarkus.http.ssl.certificate.files=/mount/certs/tls.crt
+quarkus.http.ssl.certificate.key-files=/mount/certs/tls.key
quarkus.http.ssl.certificate.reload-period=1h
----
diff --git a/docs/src/main/asciidoc/management-interface-reference.adoc b/docs/src/main/asciidoc/management-interface-reference.adoc
index 345592ca61f9d..0f245b3875795 100644
--- a/docs/src/main/asciidoc/management-interface-reference.adoc
+++ b/docs/src/main/asciidoc/management-interface-reference.adoc
@@ -58,6 +58,20 @@ quarkus.management.ssl.certificate.key-store-file=server-keystore.jks
quarkus.management.ssl.certificate.key-store-password=secret
----
+Key store, trust store and certificate files can be reloaded periodically.
+Configure the `quarkus.management.ssl.certificate.reload-period` property to specify the interval at which the certificates should be reloaded:
+
+[source, properties]
+----
+quarkus.http.management.certificate.files=/mount/certs/tls.crt
+quarkus.http.management.certificate.key-files=/mount/certs/tls.key
+quarkus.http.management.certificate.reload-period=1h
+----
+
+The files are reloaded from the same location as they were initially loaded from.
+If there is no content change, the reloading is a no-op.
+It the reloading fails, the server will continue to use the previous certificates.
+
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.
diff --git a/extensions/vertx-http/deployment/pom.xml b/extensions/vertx-http/deployment/pom.xml
index 14d44cbf77b92..4bbe58e889033 100644
--- a/extensions/vertx-http/deployment/pom.xml
+++ b/extensions/vertx-http/deployment/pom.xml
@@ -33,7 +33,7 @@
io.quarkus
quarkus-kubernetes-spi
-
+
io.quarkus
@@ -64,7 +64,7 @@
com.fasterxml.jackson.datatype
jackson-datatype-jdk8
-
+
io.quarkus
@@ -111,6 +111,12 @@
vertx-web-client
test
+
+
+ me.escoffier.certs
+ certificate-generator-junit5
+ test
+
diff --git a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/certReload/MainHttpServerTlsCertificateReloadTest.java b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/certReload/MainHttpServerTlsCertificateReloadTest.java
new file mode 100644
index 0000000000000..51b3354b1eccb
--- /dev/null
+++ b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/certReload/MainHttpServerTlsCertificateReloadTest.java
@@ -0,0 +1,160 @@
+package io.quarkus.vertx.http.certReload;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URL;
+import java.nio.file.Files;
+import java.security.cert.X509Certificate;
+import java.time.Duration;
+import java.util.UUID;
+
+import javax.net.ssl.SSLHandshakeException;
+
+import jakarta.enterprise.event.Observes;
+import jakarta.inject.Inject;
+
+import org.eclipse.microprofile.config.inject.ConfigProperty;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.DisabledOnOs;
+import org.junit.jupiter.api.condition.OS;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import io.quarkus.test.QuarkusUnitTest;
+import io.quarkus.test.common.http.TestHTTPResource;
+import io.quarkus.vertx.http.runtime.options.TlsCertificateReloader;
+import io.vertx.core.Vertx;
+import io.vertx.core.buffer.Buffer;
+import io.vertx.core.http.HttpClientOptions;
+import io.vertx.core.http.HttpClientRequest;
+import io.vertx.core.http.HttpClientResponse;
+import io.vertx.core.http.HttpMethod;
+import io.vertx.core.net.PemTrustOptions;
+import io.vertx.ext.web.Router;
+import me.escoffier.certs.Format;
+import me.escoffier.certs.junit5.Certificate;
+import me.escoffier.certs.junit5.Certificates;
+
+@Certificates(baseDir = "target/certificates", certificates = {
+ @Certificate(name = "reload-A", formats = Format.PEM),
+ @Certificate(name = "reload-B", formats = Format.PEM, duration = 365),
+})
+@DisabledOnOs(OS.WINDOWS)
+public class MainHttpServerTlsCertificateReloadTest {
+
+ @TestHTTPResource(value = "/hello", ssl = true)
+ URL url;
+
+ public static final File temp = new File("target/test-certificates-" + UUID.randomUUID());
+
+ @RegisterExtension
+ static final QuarkusUnitTest config = new QuarkusUnitTest()
+ .withApplicationRoot((jar) -> jar.addClasses(MyBean.class))
+ .overrideConfigKey("quarkus.http.ssl.insecure-requests", "redirect")
+ .overrideConfigKey("quarkus.http.ssl.certificate.reload-period", "30s")
+ .overrideConfigKey("quarkus.http.ssl.certificate.files", temp.getAbsolutePath() + "/tls.crt")
+ .overrideConfigKey("quarkus.http.ssl.certificate.key-files", temp.getAbsolutePath() + "/tls.key")
+ .overrideConfigKey("loc", temp.getAbsolutePath())
+ .setBeforeAllCustomizer(() -> {
+ try {
+ // Prepare a random directory to store the certificates.
+ temp.mkdirs();
+ Files.copy(new File("target/certificates/reload-A.crt").toPath(),
+ new File(temp, "/tls.crt").toPath());
+ Files.copy(new File("target/certificates/reload-A.key").toPath(),
+ new File(temp, "/tls.key").toPath());
+ Files.copy(new File("target/certificates/reload-A-ca.crt").toPath(),
+ new File(temp, "/ca.crt").toPath());
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ })
+ .setAfterAllCustomizer(() -> {
+ try {
+ Files.deleteIfExists(new File(temp, "/tls.crt").toPath());
+ Files.deleteIfExists(new File(temp, "/tls.key").toPath());
+ Files.deleteIfExists(new File(temp, "/ca.crt").toPath());
+ Files.deleteIfExists(temp.toPath());
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ });
+
+ @Inject
+ Vertx vertx;
+
+ @ConfigProperty(name = "loc")
+ File certs;
+
+ @Test
+ void test() throws IOException {
+ var options = new HttpClientOptions()
+ .setSsl(true)
+ .setDefaultPort(url.getPort())
+ .setDefaultHost(url.getHost())
+ .setTrustOptions(new PemTrustOptions().addCertPath("target/certificates/reload-A-ca.crt"));
+
+ String response1 = vertx.createHttpClient(options)
+ .request(HttpMethod.GET, "/hello")
+ .flatMap(HttpClientRequest::send)
+ .flatMap(HttpClientResponse::body)
+ .map(Buffer::toString)
+ .toCompletionStage().toCompletableFuture().join();
+
+ // Update certs
+ Files.copy(new File("target/certificates/reload-B.crt").toPath(),
+ new File(certs, "/tls.crt").toPath(), java.nio.file.StandardCopyOption.REPLACE_EXISTING);
+ Files.copy(new File("target/certificates/reload-B.key").toPath(),
+ new File(certs, "/tls.key").toPath(), java.nio.file.StandardCopyOption.REPLACE_EXISTING);
+
+ // Trigger the reload
+ TlsCertificateReloader.reload().await().atMost(Duration.ofSeconds(10));
+
+ // The client truststore is not updated, thus it should fail.
+ assertThatThrownBy(() -> vertx.createHttpClient(options)
+ .request(HttpMethod.GET, "/hello")
+ .flatMap(HttpClientRequest::send)
+ .flatMap(HttpClientResponse::body)
+ .map(Buffer::toString)
+ .toCompletionStage().toCompletableFuture().join()).hasCauseInstanceOf(SSLHandshakeException.class);
+
+ var options2 = new HttpClientOptions(options)
+ .setTrustOptions(new PemTrustOptions().addCertPath("target/certificates/reload-B-ca.crt"));
+
+ var response2 = vertx.createHttpClient(options2)
+ .request(HttpMethod.GET, "/hello")
+ .flatMap(HttpClientRequest::send)
+ .flatMap(HttpClientResponse::body)
+ .map(Buffer::toString)
+ .toCompletionStage().toCompletableFuture().join();
+
+ assertThat(response1).isNotEqualTo(response2); // Because cert duration are different.
+
+ // Trigger another reload
+ TlsCertificateReloader.reload().await().atMost(Duration.ofSeconds(10));
+
+ var response3 = vertx.createHttpClient(options2)
+ .request(HttpMethod.GET, "/hello")
+ .flatMap(HttpClientRequest::send)
+ .flatMap(HttpClientResponse::body)
+ .map(Buffer::toString)
+ .toCompletionStage().toCompletableFuture().join();
+
+ assertThat(response2).isEqualTo(response3);
+ }
+
+ public static class MyBean {
+
+ public void onStart(@Observes Router router) {
+ router.get("/hello").handler(rc -> {
+ var exp = ((X509Certificate) rc.request().connection().sslSession().getLocalCertificates()[0]).getNotAfter()
+ .toInstant().toEpochMilli();
+ rc.response().end("Hello " + exp);
+ });
+ }
+
+ }
+
+}
diff --git a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/certReload/MainHttpServerTlsPKCS12CertificateReloadTest.java b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/certReload/MainHttpServerTlsPKCS12CertificateReloadTest.java
new file mode 100644
index 0000000000000..fcd81cf1d284f
--- /dev/null
+++ b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/certReload/MainHttpServerTlsPKCS12CertificateReloadTest.java
@@ -0,0 +1,153 @@
+package io.quarkus.vertx.http.certReload;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URL;
+import java.nio.file.Files;
+import java.security.cert.X509Certificate;
+import java.time.Duration;
+import java.util.UUID;
+
+import javax.net.ssl.SSLHandshakeException;
+
+import jakarta.enterprise.event.Observes;
+import jakarta.inject.Inject;
+
+import org.eclipse.microprofile.config.inject.ConfigProperty;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.DisabledOnOs;
+import org.junit.jupiter.api.condition.OS;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import io.quarkus.test.QuarkusUnitTest;
+import io.quarkus.test.common.http.TestHTTPResource;
+import io.quarkus.vertx.http.runtime.options.TlsCertificateReloader;
+import io.vertx.core.Vertx;
+import io.vertx.core.buffer.Buffer;
+import io.vertx.core.http.HttpClientOptions;
+import io.vertx.core.http.HttpClientRequest;
+import io.vertx.core.http.HttpClientResponse;
+import io.vertx.core.http.HttpMethod;
+import io.vertx.core.net.PfxOptions;
+import io.vertx.ext.web.Router;
+import me.escoffier.certs.Format;
+import me.escoffier.certs.junit5.Certificate;
+import me.escoffier.certs.junit5.Certificates;
+
+@Certificates(baseDir = "target/certificates", certificates = {
+ @Certificate(name = "reload-A", formats = Format.PKCS12, password = "password"),
+ @Certificate(name = "reload-B", formats = Format.PKCS12, password = "password", duration = 365),
+})
+@DisabledOnOs(OS.WINDOWS)
+public class MainHttpServerTlsPKCS12CertificateReloadTest {
+
+ @TestHTTPResource(value = "/hello", ssl = true)
+ URL url;
+
+ public static final File temp = new File("target/test-certificates-" + UUID.randomUUID());
+
+ @RegisterExtension
+ static final QuarkusUnitTest config = new QuarkusUnitTest()
+ .withApplicationRoot((jar) -> jar.addClasses(MyBean.class))
+ .overrideConfigKey("quarkus.http.ssl.insecure-requests", "redirect")
+ .overrideConfigKey("quarkus.http.ssl.certificate.reload-period", "30s")
+ .overrideConfigKey("quarkus.http.ssl.certificate.key-store-file", temp.getAbsolutePath() + "/tls.p12")
+ .overrideConfigKey("loc", temp.getAbsolutePath())
+ .setBeforeAllCustomizer(() -> {
+ try {
+ // Prepare a random directory to store the certificates.
+ temp.mkdirs();
+ Files.copy(new File("target/certificates/reload-A-keystore.p12").toPath(),
+ new File(temp, "/tls.p12").toPath());
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ })
+ .setAfterAllCustomizer(() -> {
+ try {
+ Files.deleteIfExists(new File(temp, "/tls.p12").toPath());
+ Files.deleteIfExists(temp.toPath());
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ });
+
+ @Inject
+ Vertx vertx;
+
+ @ConfigProperty(name = "loc")
+ File certs;
+
+ @Test
+ void test() throws IOException {
+ var options = new HttpClientOptions()
+ .setSsl(true)
+ .setDefaultPort(url.getPort())
+ .setDefaultHost(url.getHost())
+ .setTrustOptions(
+ new PfxOptions().setPath("target/certificates/reload-A-truststore.p12").setPassword("password"));
+
+ String response1 = vertx.createHttpClient(options)
+ .request(HttpMethod.GET, "/hello")
+ .flatMap(HttpClientRequest::send)
+ .flatMap(HttpClientResponse::body)
+ .map(Buffer::toString)
+ .toCompletionStage().toCompletableFuture().join();
+
+ // Update certs
+ Files.copy(new File("target/certificates/reload-B-keystore.p12").toPath(),
+ new File(certs, "/tls.p12").toPath(), java.nio.file.StandardCopyOption.REPLACE_EXISTING);
+
+ // Trigger the reload
+ TlsCertificateReloader.reload().await().atMost(Duration.ofSeconds(10));
+
+ // The client truststore is not updated, thus it should fail.
+ assertThatThrownBy(() -> vertx.createHttpClient(options)
+ .request(HttpMethod.GET, "/hello")
+ .flatMap(HttpClientRequest::send)
+ .flatMap(HttpClientResponse::body)
+ .map(Buffer::toString)
+ .toCompletionStage().toCompletableFuture().join()).hasCauseInstanceOf(SSLHandshakeException.class);
+
+ var options2 = new HttpClientOptions(options)
+ .setTrustOptions(
+ new PfxOptions().setPath("target/certificates/reload-B-truststore.p12").setPassword("password"));
+
+ var response2 = vertx.createHttpClient(options2)
+ .request(HttpMethod.GET, "/hello")
+ .flatMap(HttpClientRequest::send)
+ .flatMap(HttpClientResponse::body)
+ .map(Buffer::toString)
+ .toCompletionStage().toCompletableFuture().join();
+
+ assertThat(response1).isNotEqualTo(response2); // Because cert duration are different.
+
+ // Trigger another reload
+ TlsCertificateReloader.reload().await().atMost(Duration.ofSeconds(10));
+
+ var response3 = vertx.createHttpClient(options2)
+ .request(HttpMethod.GET, "/hello")
+ .flatMap(HttpClientRequest::send)
+ .flatMap(HttpClientResponse::body)
+ .map(Buffer::toString)
+ .toCompletionStage().toCompletableFuture().join();
+
+ assertThat(response2).isEqualTo(response3);
+ }
+
+ public static class MyBean {
+
+ public void onStart(@Observes Router router) {
+ router.get("/hello").handler(rc -> {
+ var exp = ((X509Certificate) rc.request().connection().sslSession().getLocalCertificates()[0]).getNotAfter()
+ .toInstant().toEpochMilli();
+ rc.response().end("Hello " + exp);
+ });
+ }
+
+ }
+
+}
diff --git a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/certReload/ManagementHttpServerTlsCertificateReloadTest.java b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/certReload/ManagementHttpServerTlsCertificateReloadTest.java
new file mode 100644
index 0000000000000..b62898abfaf6e
--- /dev/null
+++ b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/certReload/ManagementHttpServerTlsCertificateReloadTest.java
@@ -0,0 +1,174 @@
+package io.quarkus.vertx.http.certReload;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.security.cert.X509Certificate;
+import java.time.Duration;
+import java.util.UUID;
+import java.util.function.Consumer;
+
+import javax.net.ssl.SSLHandshakeException;
+
+import jakarta.inject.Inject;
+
+import org.eclipse.microprofile.config.inject.ConfigProperty;
+import org.jboss.shrinkwrap.api.asset.StringAsset;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.DisabledOnOs;
+import org.junit.jupiter.api.condition.OS;
+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.quarkus.vertx.http.runtime.options.TlsCertificateReloader;
+import io.vertx.core.Handler;
+import io.vertx.core.Vertx;
+import io.vertx.core.buffer.Buffer;
+import io.vertx.core.http.HttpClientOptions;
+import io.vertx.core.http.HttpClientRequest;
+import io.vertx.core.http.HttpClientResponse;
+import io.vertx.core.http.HttpMethod;
+import io.vertx.core.net.PemTrustOptions;
+import io.vertx.ext.web.RoutingContext;
+import me.escoffier.certs.Format;
+import me.escoffier.certs.junit5.Certificate;
+import me.escoffier.certs.junit5.Certificates;
+
+@Certificates(baseDir = "target/certificates", certificates = {
+ @Certificate(name = "reload-C", formats = Format.PEM),
+ @Certificate(name = "reload-D", formats = Format.PEM, duration = 365),
+})
+@DisabledOnOs(OS.WINDOWS)
+public class ManagementHttpServerTlsCertificateReloadTest {
+
+ public static final File temp = new File("target/test-certificates-" + UUID.randomUUID());
+
+ private static final String APP_PROPS = """
+ quarkus.management.enabled=true
+ quarkus.management.ssl.certificate.reload-period=30s
+ quarkus.management.ssl.certificate.files=%s
+ quarkus.management.ssl.certificate.key-files=%s
+
+ loc=%s
+ """.formatted(temp.getAbsolutePath() + "/tls.crt", temp.getAbsolutePath() + "/tls.key", temp.getAbsolutePath());
+
+ @RegisterExtension
+ static final QuarkusUnitTest config = new QuarkusUnitTest()
+ .withApplicationRoot((jar) -> jar
+ .addAsResource(new StringAsset(APP_PROPS), "application.properties"))
+ .setBeforeAllCustomizer(() -> {
+ try {
+ // Prepare a random directory to store the certificates.
+ temp.mkdirs();
+ Files.copy(new File("target/certificates/reload-C.crt").toPath(),
+ new File(temp, "/tls.crt").toPath());
+ Files.copy(new File("target/certificates/reload-C.key").toPath(),
+ new File(temp, "/tls.key").toPath());
+ Files.copy(new File("target/certificates/reload-C-ca.crt").toPath(),
+ new File(temp, "/ca.crt").toPath());
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ })
+ .addBuildChainCustomizer(buildCustomizer())
+ .setAfterAllCustomizer(() -> {
+ try {
+ Files.deleteIfExists(new File(temp, "/tls.crt").toPath());
+ Files.deleteIfExists(new File(temp, "/tls.key").toPath());
+ Files.deleteIfExists(new File(temp, "/ca.crt").toPath());
+ Files.deleteIfExists(temp.toPath());
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ });
+
+ 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("/hello")
+ .handler(new MyHandler())
+ .build());
+ }
+ }).produces(RouteBuildItem.class)
+ .consumes(NonApplicationRootPathBuildItem.class)
+ .build();
+ }
+ };
+ }
+
+ @Inject
+ Vertx vertx;
+
+ @ConfigProperty(name = "loc")
+ File certs;
+
+ @Test
+ void test() throws IOException {
+ var options = new HttpClientOptions()
+ .setSsl(true)
+ .setDefaultPort(9001) // Management interface test port
+ .setDefaultHost("localhost")
+ .setTrustOptions(new PemTrustOptions().addCertPath(new File(certs, "/ca.crt").getAbsolutePath()));
+
+ String response1 = vertx.createHttpClient(options)
+ .request(HttpMethod.GET, "/q/hello")
+ .flatMap(HttpClientRequest::send)
+ .flatMap(HttpClientResponse::body)
+ .map(Buffer::toString)
+ .toCompletionStage().toCompletableFuture().join();
+
+ // Update certs
+ Files.copy(new File("target/certificates/reload-D.crt").toPath(),
+ new File(certs, "/tls.crt").toPath(), java.nio.file.StandardCopyOption.REPLACE_EXISTING);
+ Files.copy(new File("target/certificates/reload-D.key").toPath(),
+ new File(certs, "/tls.key").toPath(), java.nio.file.StandardCopyOption.REPLACE_EXISTING);
+
+ // Trigger the reload
+ TlsCertificateReloader.reload().await().atMost(Duration.ofSeconds(10));
+
+ // The client truststore is not updated, thus it should fail.
+ assertThatThrownBy(() -> vertx.createHttpClient(options)
+ .request(HttpMethod.GET, "/hello")
+ .flatMap(HttpClientRequest::send)
+ .flatMap(HttpClientResponse::body)
+ .map(Buffer::toString)
+ .toCompletionStage().toCompletableFuture().join()).hasCauseInstanceOf(SSLHandshakeException.class);
+
+ var options2 = new HttpClientOptions(options)
+ .setTrustOptions(new PemTrustOptions().addCertPath("target/certificates/reload-D-ca.crt"));
+
+ var response2 = vertx.createHttpClient(options2)
+ .request(HttpMethod.GET, "/hello")
+ .flatMap(HttpClientRequest::send)
+ .flatMap(HttpClientResponse::body)
+ .map(Buffer::toString)
+ .toCompletionStage().toCompletableFuture().join();
+
+ assertThat(response1).isNotEqualTo(response2); // Because cert duration are different.
+ }
+
+ public static class MyHandler implements Handler {
+ @Override
+ public void handle(RoutingContext rc) {
+ var exp = ((X509Certificate) rc.request().connection().sslSession().getLocalCertificates()[0]).getNotAfter()
+ .toInstant().toEpochMilli();
+ rc.response().end("Hello Management " + exp);
+ }
+ }
+
+}
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 cd61b39fedda1..14d2b3a8a802c 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
@@ -60,7 +60,7 @@
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.quarkus.vertx.http.runtime.options.TlsCertificateReloadUtils;
+import io.quarkus.vertx.http.runtime.options.TlsCertificateReloader;
import io.smallrye.common.vertx.VertxContext;
import io.vertx.core.AbstractVerticle;
import io.vertx.core.AsyncResult;
@@ -171,7 +171,7 @@ private boolean uriValid(HttpServerRequest httpServerRequest) {
private static HttpServerOptions httpMainDomainSocketOptions;
private static HttpServerOptions httpManagementServerOptions;
- private static final List taskIds = new CopyOnWriteArrayList<>();
+ private static final List refresTaskIds = new CopyOnWriteArrayList<>();
final HttpBuildTimeConfig httpBuildTimeConfig;
final ManagementInterfaceBuildTimeConfig managementBuildTimeConfig;
final RuntimeValue httpConfiguration;
@@ -634,11 +634,11 @@ private static CompletableFuture initializeManagementInterface(Vertx
new IllegalStateException("Unable to start the management interface", ar.cause()));
} else {
if (httpManagementServerOptions.isSsl()
- && managementConfig.ssl.certificate.reloadPeriod.isPresent()) {
- long l = TlsCertificateReloadUtils.handleCertificateReloading(
+ && (managementConfig.ssl.certificate.reloadPeriod.isPresent())) {
+ long l = TlsCertificateReloader.initCertReloadingAction(
vertx, ar.result(), httpManagementServerOptions, managementConfig.ssl);
if (l != -1) {
- taskIds.add(l);
+ refresTaskIds.add(l);
}
}
@@ -822,8 +822,8 @@ public void handle(AsyncResult event) {
// shutdown the management interface
try {
- for (Long id : taskIds) {
- vertx.cancelTimer(id);
+ for (Long id : refresTaskIds) {
+ TlsCertificateReloader.unschedule(vertx, id);
}
if (managementServer != null && !isVertxClose) {
managementServer.close(handler);
@@ -1204,8 +1204,8 @@ public void handle(AsyncResult event) {
portSystemProperties.set(schema, actualPort, launchMode);
}
- if (https && quarkusConfig.ssl.certificate.reloadPeriod.isPresent()) {
- long l = TlsCertificateReloadUtils.handleCertificateReloading(
+ if (https && (quarkusConfig.ssl.certificate.reloadPeriod.isPresent())) {
+ long l = TlsCertificateReloader.initCertReloadingAction(
vertx, httpsServer, httpsOptions, quarkusConfig.ssl);
if (l != -1) {
reloadingTasks.add(l);
@@ -1226,7 +1226,7 @@ public void handle(AsyncResult event) {
public void stop(Promise stopFuture) {
for (Long id : reloadingTasks) {
- vertx.cancelTimer(id);
+ TlsCertificateReloader.unschedule(vertx, id);
}
final AtomicInteger remainingCount = new AtomicInteger(0);
diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/options/TlsCertificateReloadUtils.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/options/TlsCertificateReloader.java
similarity index 64%
rename from extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/options/TlsCertificateReloadUtils.java
rename to extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/options/TlsCertificateReloader.java
index 4b1512775268d..00566ad5ab2a8 100644
--- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/options/TlsCertificateReloadUtils.java
+++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/options/TlsCertificateReloader.java
@@ -7,11 +7,15 @@
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
+import java.util.concurrent.CopyOnWriteArrayList;
import java.util.function.Function;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
import org.jboss.logging.Logger;
import io.quarkus.vertx.http.runtime.ServerSslConfig;
+import io.smallrye.mutiny.Uni;
import io.vertx.core.AsyncResult;
import io.vertx.core.Future;
import io.vertx.core.Handler;
@@ -26,18 +30,18 @@
/**
* Utility class to handle TLS certificate reloading.
*/
-public class TlsCertificateReloadUtils {
+public class TlsCertificateReloader {
- public static long handleCertificateReloading(Vertx vertx, HttpServer server,
+ /**
+ * A structure storing the reload tasks.
+ */
+ private static final List TASKS = new CopyOnWriteArrayList<>();
+
+ private static final Logger LOGGER = Logger.getLogger(TlsCertificateReloader.class);
+
+ public static long initCertReloadingAction(Vertx vertx, HttpServer server,
HttpServerOptions options, ServerSslConfig configuration) {
- // Validation
- if (configuration.certificate.reloadPeriod.isEmpty()) {
- return -1;
- }
- if (configuration.certificate.reloadPeriod.get().toMillis() < 30_000) {
- throw new IllegalArgumentException(
- "Unable to configure TLS reloading - The reload period cannot be less than 30 seconds");
- }
+
if (options == null) {
throw new IllegalArgumentException("Unable to configure TLS reloading - The HTTP server options were not provided");
}
@@ -46,12 +50,23 @@ public static long handleCertificateReloading(Vertx vertx, HttpServer server,
throw new IllegalArgumentException("Unable to configure TLS reloading - TLS/SSL is not enabled on the server");
}
- Logger log = Logger.getLogger(TlsCertificateReloadUtils.class);
- return vertx.setPeriodic(configuration.certificate.reloadPeriod.get().toMillis(), new Handler() {
+ long period;
+ // Validation
+ if (configuration.certificate.reloadPeriod.isPresent()) {
+ if (configuration.certificate.reloadPeriod.get().toMillis() < 30_000) {
+ throw new IllegalArgumentException(
+ "Unable to configure TLS reloading - The reload period cannot be less than 30 seconds");
+ }
+ period = configuration.certificate.reloadPeriod.get().toMillis();
+ } else {
+ return -1;
+ }
+
+ Supplier> task = new Supplier>() {
@Override
- public void handle(Long id) {
+ public Uni get() {
- vertx.executeBlocking(new Callable() {
+ Future future = vertx.executeBlocking(new Callable() {
@Override
public SSLOptions call() throws Exception {
// We are reading files - must be done on a worker thread.
@@ -76,17 +91,51 @@ public Future apply(SSLOptions res) {
@Override
public void handle(AsyncResult ar) {
if (ar.failed()) {
- log.error("Unable to reload the TLS certificate, keeping the current one.", ar.cause());
+ LOGGER.error("Unable to reload the TLS certificate, keeping the current one.", ar.cause());
} else {
if (ar.result()) {
- log.debug("TLS certificates updated");
+ LOGGER.debug("TLS certificates updated");
}
// Not updated, no change.
}
}
});
+
+ return Uni.createFrom().completionStage(future.toCompletionStage());
+ }
+ };
+
+ long id = vertx.setPeriodic(configuration.certificate.reloadPeriod.get().toMillis(), new Handler() {
+ @Override
+ public void handle(Long id) {
+ //noinspection ResultOfMethodCallIgnored
+ task.get().subscribeAsCompletionStage();
}
});
+
+ TASKS.add(new ReloadCertificateTask(id, task));
+ return id;
+ }
+
+ public static void unschedule(Vertx vertx, long id) {
+ vertx.cancelTimer(id);
+ for (ReloadCertificateTask task : TASKS) {
+ if (task.it == id) {
+ TASKS.remove(task);
+ break;
+ }
+ }
+ }
+
+ /**
+ * Trigger all the reload tasks.
+ * This method is NOT part of the public API, and is only used for testing purpose.
+ *
+ * @return a Uni that is completed when all the reload tasks have been executed
+ */
+ public static Uni reload() {
+ var unis = TASKS.stream().map(ReloadCertificateTask::action).map(Supplier::get).collect(Collectors.toList());
+ return Uni.join().all(unis).andFailFast().replaceWithVoid();
}
private static SSLOptions reloadFileContent(SSLOptions ssl, ServerSslConfig configuration) throws IOException {
@@ -133,4 +182,8 @@ private static SSLOptions reloadFileContent(SSLOptions ssl, ServerSslConfig conf
return copy;
}
+
+ record ReloadCertificateTask(long it, Supplier> action) {
+
+ }
}