From be30dbddfe8a7386840ba0d3fa9fa325ce0c2e95 Mon Sep 17 00:00:00 2001 From: Geoffrey GREBERT Date: Fri, 23 Aug 2024 00:57:00 +0200 Subject: [PATCH] proxy for Temporal UI --- .../ROOT/pages/includes/quarkus-temporal.adoc | 4 +- extension/deployment/pom.xml | 4 ++ .../deployment/TemporalProcessor.java | 2 +- .../deployment/devui/TemporalContainer.java | 13 +++- .../devui/TemporalDevUIProcessor.java | 60 ++++++++++++++++-- .../devui/TemporalDevserviceConfig.java | 2 + .../devui/TemporalDevserviceProcessor.java | 47 +++++++++++++- .../deployment/devui/TemporalUiConfig.java | 18 ++++++ extension/runtime/pom.xml | 8 +++ .../temporal/devui/TemporalUiProxy.java | 63 +++++++++++++++++++ 10 files changed, 208 insertions(+), 13 deletions(-) create mode 100644 extension/deployment/src/main/java/io/quarkiverse/temporal/deployment/devui/TemporalUiConfig.java create mode 100644 extension/runtime/src/main/java/io/quarkiverse/temporal/devui/TemporalUiProxy.java diff --git a/docs/modules/ROOT/pages/includes/quarkus-temporal.adoc b/docs/modules/ROOT/pages/includes/quarkus-temporal.adoc index 26ccecf..de8177e 100644 --- a/docs/modules/ROOT/pages/includes/quarkus-temporal.adoc +++ b/docs/modules/ROOT/pages/includes/quarkus-temporal.adoc @@ -24,7 +24,7 @@ ifndef::add-copy-button-to-env-var[] Environment variable: `+++QUARKUS_TEMPORAL_ENABLE_MOCK+++` endif::add-copy-button-to-env-var[] --|boolean -|`true` +|`false` a|icon:lock[title=Fixed at build time] [[quarkus-temporal_quarkus-temporal-start-workers]]`link:#quarkus-temporal_quarkus-temporal-start-workers[quarkus.temporal.start-workers]` @@ -857,7 +857,7 @@ Environment variable: `+++QUARKUS_TEMPORAL_WORKFLOW_RETRIES_INITIAL_INTERVAL+++` endif::add-copy-button-to-env-var[] --|link:https://docs.oracle.com/javase/8/docs/api/java/time/Duration.html[Duration] link:#duration-note-anchor-{summaryTableId}[icon:question-circle[title=More information about the Duration format]] -|`1S` +|`1s` a| [[quarkus-temporal_quarkus-temporal-workflow-retries-backoff-coefficient]]`link:#quarkus-temporal_quarkus-temporal-workflow-retries-backoff-coefficient[quarkus.temporal.workflow.retries.backoff-coefficient]` diff --git a/extension/deployment/pom.xml b/extension/deployment/pom.xml index 6cac614..b5d9110 100644 --- a/extension/deployment/pom.xml +++ b/extension/deployment/pom.xml @@ -25,6 +25,10 @@ io.quarkus quarkus-devservices-deployment + + io.quarkus + quarkus-vertx-http-deployment + org.testcontainers testcontainers diff --git a/extension/deployment/src/main/java/io/quarkiverse/temporal/deployment/TemporalProcessor.java b/extension/deployment/src/main/java/io/quarkiverse/temporal/deployment/TemporalProcessor.java index d08a451..93f3276 100644 --- a/extension/deployment/src/main/java/io/quarkiverse/temporal/deployment/TemporalProcessor.java +++ b/extension/deployment/src/main/java/io/quarkiverse/temporal/deployment/TemporalProcessor.java @@ -71,7 +71,7 @@ public class TemporalProcessor { public static final DotName CONTEXT_PROPAGATOR = DotName.createSimple(ContextPropagator.class); - private static final String FEATURE = "temporal"; + public static final String FEATURE = "temporal"; @BuildStep FeatureBuildItem feature() { diff --git a/extension/deployment/src/main/java/io/quarkiverse/temporal/deployment/devui/TemporalContainer.java b/extension/deployment/src/main/java/io/quarkiverse/temporal/deployment/devui/TemporalContainer.java index 919fbd8..22b0430 100644 --- a/extension/deployment/src/main/java/io/quarkiverse/temporal/deployment/devui/TemporalContainer.java +++ b/extension/deployment/src/main/java/io/quarkiverse/temporal/deployment/devui/TemporalContainer.java @@ -12,11 +12,15 @@ public class TemporalContainer extends GenericContainer { private final TemporalDevserviceConfig config; private String serviceName; + private String label; + private String path; // private String hostname; - public TemporalContainer(DockerImageName dockerImageName, TemporalDevserviceConfig config) { + public TemporalContainer(DockerImageName dockerImageName, TemporalDevserviceConfig config, String path, String label) { super(dockerImageName); this.config = config; + this.label = label; + this.path = path; this.serviceName = "temporal"; } @@ -26,16 +30,19 @@ protected void configure() { withCreateContainerCmdModifier(cmd -> { cmd.withEntrypoint("/usr/local/bin/temporal"); - cmd.withCmd("server", "start-dev", "--ip", "0.0.0.0"); + cmd.withCmd("server", "start-dev", "--ip", "0.0.0.0", "--ui-public-path", path); }); withExposedPorts(SERVER_EXPOSED_PORT, UI_EXPOSED_PORT); - withLabel("quarkus-devservice-temporal", serviceName); + withLabel(label, serviceName); withReuse(config.reuse()); // hostname = ConfigureUtil.configureSharedNetwork(this, "temporal-" + serviceName); } + public String getUiUrl() { + return "http://" + getHost() + ":" + getMappedPort(UI_EXPOSED_PORT) + path; + } } diff --git a/extension/deployment/src/main/java/io/quarkiverse/temporal/deployment/devui/TemporalDevUIProcessor.java b/extension/deployment/src/main/java/io/quarkiverse/temporal/deployment/devui/TemporalDevUIProcessor.java index 28a25ea..052fa99 100644 --- a/extension/deployment/src/main/java/io/quarkiverse/temporal/deployment/devui/TemporalDevUIProcessor.java +++ b/extension/deployment/src/main/java/io/quarkiverse/temporal/deployment/devui/TemporalDevUIProcessor.java @@ -2,28 +2,44 @@ import java.util.List; import java.util.Objects; +import java.util.Optional; import java.util.stream.Collectors; +import io.quarkiverse.temporal.deployment.TemporalProcessor; import io.quarkiverse.temporal.deployment.WorkerBuildItem; import io.quarkiverse.temporal.deployment.WorkflowBuildItem; import io.quarkus.deployment.IsDevelopment; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.annotations.BuildSteps; +import io.quarkus.deployment.builditem.LaunchModeBuildItem; +import io.quarkus.deployment.dev.devservices.GlobalDevServicesConfig; import io.quarkus.devui.spi.page.CardPageBuildItem; import io.quarkus.devui.spi.page.ExternalPageBuilder; import io.quarkus.devui.spi.page.Page; import io.quarkus.devui.spi.page.PageBuilder; import io.quarkus.devui.spi.page.TableDataPageBuilder; +import io.quarkus.vertx.http.deployment.NonApplicationRootPathBuildItem; +import io.quarkus.vertx.http.runtime.management.ManagementInterfaceBuildTimeConfig; import io.temporal.client.WorkflowClient; /** * Dev UI card for displaying important details such Temporal version. */ +@BuildSteps(onlyIf = IsDevelopment.class) public class TemporalDevUIProcessor { - @BuildStep(onlyIf = IsDevelopment.class) - void createCard(BuildProducer cardPageBuildItemBuildProducer, List workflows, - List workers) { + @BuildStep + void createCard( + BuildProducer cardPageBuildItemBuildProducer, + List workflows, + List workers, + GlobalDevServicesConfig globalDevServicesConfig, + TemporalUiConfig uiConfig, + TemporalDevserviceConfig temporalDevserviceConfig, + ManagementInterfaceBuildTimeConfig managementInterfaceBuildTimeConfig, + LaunchModeBuildItem launchMode, + NonApplicationRootPathBuildItem nonApplicationRootPathBuildItem) { final CardPageBuildItem card = new CardPageBuildItem(); final PageBuilder versionPage = Page.externalPageBuilder("Version") @@ -58,11 +74,47 @@ void createCard(BuildProducer cardPageBuildItemBuildProducer, card.addBuildTimeData("workers", workers.stream().map(WorkerBuildTimeData::new).collect(Collectors.toList())); + uiPage(uiConfig.url(), temporalDevserviceConfig, managementInterfaceBuildTimeConfig, launchMode, + nonApplicationRootPathBuildItem, card); + card.setCustomCard("qwc-temporal-card.js"); cardPageBuildItemBuildProducer.produce(card); } + private void uiPage( + Optional configPath, + TemporalDevserviceConfig temporalDevserviceConfig, + ManagementInterfaceBuildTimeConfig managementInterfaceBuildTimeConfig, + LaunchModeBuildItem launchMode, + NonApplicationRootPathBuildItem nonApplicationRootPathBuildItem, + CardPageBuildItem card) { + var path = configPath; + + // check if the UI url is set in the config or if the devservice is enabled + if (!path.isPresent() && Boolean.TRUE.equals(temporalDevserviceConfig.enabled())) { + var defaultBasePath = nonApplicationRootPathBuildItem.resolveManagementPath( + TemporalProcessor.FEATURE, + managementInterfaceBuildTimeConfig, + launchMode); + + path = Optional.of(defaultBasePath); + } + + // if the path is not set, we don't have a UI to link to + if (!path.isPresent()) { + return; + } + + // add the UI page + final PageBuilder uiPage = Page.externalPageBuilder("UI") + .icon("font-awesome-solid:desktop") + .url(path.get(), path.get()) + .isHtmlContent(); + + card.addPage(uiPage); + } + static class WorkflowBuildTimeData { WorkflowBuildTimeData(WorkflowBuildItem item) { this.name = item.workflow.getName().replaceAll("\\B\\w+(\\.[a-z])", "$1"); @@ -88,7 +140,7 @@ static class WorkerBuildTimeData { this.workflows = item.workflows.stream().map(workflow -> workflow.getName().replaceAll("\\B\\w+(\\.[a-z])", "$1")) .collect(Collectors.toList()); this.activities = item.activities.stream() - .map(activities -> activities.getName().replaceAll("\\B\\w+(\\.[a-z])", "$1")).collect(Collectors.toList()); + .map(activity -> activity.getName().replaceAll("\\B\\w+(\\.[a-z])", "$1")).collect(Collectors.toList()); } private final String name; diff --git a/extension/deployment/src/main/java/io/quarkiverse/temporal/deployment/devui/TemporalDevserviceConfig.java b/extension/deployment/src/main/java/io/quarkiverse/temporal/deployment/devui/TemporalDevserviceConfig.java index 0ff0872..c87ac16 100644 --- a/extension/deployment/src/main/java/io/quarkiverse/temporal/deployment/devui/TemporalDevserviceConfig.java +++ b/extension/deployment/src/main/java/io/quarkiverse/temporal/deployment/devui/TemporalDevserviceConfig.java @@ -18,12 +18,14 @@ public interface TemporalDevserviceConfig { /** * The image to use for the Temporal Devservice. */ + // @WithDefault("temporaliotest/auto-setup") @WithDefault("temporalio/auto-setup") String image(); /** * The version of the image to use for the Temporal Devservice. */ + // @WithDefault("sha-053ea8f") @WithDefault("latest") String version(); diff --git a/extension/deployment/src/main/java/io/quarkiverse/temporal/deployment/devui/TemporalDevserviceProcessor.java b/extension/deployment/src/main/java/io/quarkiverse/temporal/deployment/devui/TemporalDevserviceProcessor.java index c706399..3b0272d 100644 --- a/extension/deployment/src/main/java/io/quarkiverse/temporal/deployment/devui/TemporalDevserviceProcessor.java +++ b/extension/deployment/src/main/java/io/quarkiverse/temporal/deployment/devui/TemporalDevserviceProcessor.java @@ -4,13 +4,22 @@ import org.testcontainers.utility.DockerImageName; +import io.quarkiverse.temporal.deployment.TemporalProcessor; +import io.quarkiverse.temporal.devui.TemporalUiProxy; import io.quarkus.deployment.IsNormal; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.annotations.BuildSteps; +import io.quarkus.deployment.annotations.ExecutionTime; +import io.quarkus.deployment.annotations.Record; import io.quarkus.deployment.builditem.DevServicesResultBuildItem; +import io.quarkus.deployment.builditem.LaunchModeBuildItem; import io.quarkus.deployment.dev.devservices.GlobalDevServicesConfig; import io.quarkus.devservices.common.ContainerLocator; +import io.quarkus.vertx.core.deployment.CoreVertxBuildItem; +import io.quarkus.vertx.http.deployment.NonApplicationRootPathBuildItem; +import io.quarkus.vertx.http.deployment.RouteBuildItem; +import io.quarkus.vertx.http.runtime.management.ManagementInterfaceBuildTimeConfig; @BuildSteps(onlyIfNot = IsNormal.class, onlyIf = { GlobalDevServicesConfig.Enabled.class }) public class TemporalDevserviceProcessor { @@ -21,21 +30,53 @@ public class TemporalDevserviceProcessor { private static final ContainerLocator containerLocator = new ContainerLocator(DEV_SERVICE_LABEL, SERVER_EXPOSED_PORT); @BuildStep - public void build(TemporalDevserviceConfig config, BuildProducer devServiceProducer) { + void build( + TemporalDevserviceConfig config, + BuildProducer devServiceProducer, + ManagementInterfaceBuildTimeConfig managementInterfaceBuildTimeConfig, + LaunchModeBuildItem launchMode, + NonApplicationRootPathBuildItem nonApplicationRootPathBuildItem) { if (Boolean.FALSE.equals(config.enabled())) { return; } + var path = nonApplicationRootPathBuildItem.resolveManagementPath( + TemporalProcessor.FEATURE, + managementInterfaceBuildTimeConfig, + launchMode); + var imageStr = config.image() + ":" + config.version(); var image = DockerImageName.parse(imageStr) .asCompatibleSubstituteFor(imageStr); - var serverContainer = new TemporalContainer(image, config); + var serverContainer = new TemporalContainer(image, config, path, DEV_SERVICE_LABEL); serverContainer.start(); var serverPort = serverContainer.getMappedPort(SERVER_EXPOSED_PORT); devServiceProducer.produce(new DevServicesResultBuildItem("temporal", serverContainer.getContainerId(), Map.of( - "quarkus.temporal.connection.target", "localhost:" + serverPort))); + "quarkus.temporal.connection.target", "localhost:" + serverPort, + "quarkus.temporal.ui.url", serverContainer.getUiUrl(), + "quarkus.temporal.ui.port", serverContainer.getMappedPort(UI_EXPOSED_PORT).toString()))); + } + + @BuildStep + @Record(ExecutionTime.RUNTIME_INIT) + void registerProxy( + TemporalDevserviceConfig config, + TemporalUiProxy proxy, + BuildProducer routes, + NonApplicationRootPathBuildItem frameworkRoot, + CoreVertxBuildItem coreVertxBuildItem) { + if (Boolean.FALSE.equals(config.enabled())) { + return; + } + + routes.produce(frameworkRoot.routeBuilder() + .management() + .route(TemporalProcessor.FEATURE + "/*") + .displayOnNotFoundPage("Portal UI not found") + .handler(proxy.handler(coreVertxBuildItem.getVertx())) + .build()); } } diff --git a/extension/deployment/src/main/java/io/quarkiverse/temporal/deployment/devui/TemporalUiConfig.java b/extension/deployment/src/main/java/io/quarkiverse/temporal/deployment/devui/TemporalUiConfig.java new file mode 100644 index 0000000..4d37941 --- /dev/null +++ b/extension/deployment/src/main/java/io/quarkiverse/temporal/deployment/devui/TemporalUiConfig.java @@ -0,0 +1,18 @@ +package io.quarkiverse.temporal.deployment.devui; + +import java.util.Optional; + +import io.quarkus.runtime.annotations.ConfigPhase; +import io.quarkus.runtime.annotations.ConfigRoot; +import io.smallrye.config.ConfigMapping; + +@ConfigMapping(prefix = "quarkus.temporal.ui") +@ConfigRoot(phase = ConfigPhase.BUILD_TIME) +public interface TemporalUiConfig { + + /** + * The url of the Temporal UI. + */ + Optional url(); + +} diff --git a/extension/runtime/pom.xml b/extension/runtime/pom.xml index 4be20a9..2df28ed 100644 --- a/extension/runtime/pom.xml +++ b/extension/runtime/pom.xml @@ -54,6 +54,14 @@ io.opentelemetry opentelemetry-opentracing-shim + + io.quarkus + quarkus-vertx-http + + + io.vertx + vertx-web-client + diff --git a/extension/runtime/src/main/java/io/quarkiverse/temporal/devui/TemporalUiProxy.java b/extension/runtime/src/main/java/io/quarkiverse/temporal/devui/TemporalUiProxy.java new file mode 100644 index 0000000..d43ce95 --- /dev/null +++ b/extension/runtime/src/main/java/io/quarkiverse/temporal/devui/TemporalUiProxy.java @@ -0,0 +1,63 @@ +package io.quarkiverse.temporal.devui; + +import java.util.function.Supplier; + +import org.eclipse.microprofile.config.ConfigProvider; +import org.jboss.logging.Logger; + +import io.quarkus.runtime.annotations.Recorder; +import io.vertx.core.Handler; +import io.vertx.core.Vertx; +import io.vertx.core.buffer.Buffer; +import io.vertx.ext.web.RoutingContext; +import io.vertx.ext.web.client.HttpRequest; +import io.vertx.ext.web.client.WebClient; + +@Recorder +public class TemporalUiProxy { + + private static final Logger log = Logger.getLogger(TemporalUiProxy.class); + + public Handler handler(Supplier vertx) { + final var portOptional = ConfigProvider.getConfig().getOptionalValue("quarkus.temporal.ui.port", Integer.class); + final var client = WebClient.create(vertx.get()); + + return new Handler() { + @Override + public void handle(RoutingContext event) { + if (!portOptional.isPresent()) { + event.response().setStatusCode(404).end(); + return; + } + + final Integer port = portOptional.get(); + final HttpRequest r = client.request(event.request().method(), port, "localhost", + event.request().uri()); + + // copy all headers + event.request().headers().forEach(h -> r.putHeader(h.getKey(), h.getValue())); + + if ("websocket".equals(event.request().getHeader("upgrade"))) { + // handle WebSocket request + event.request().toWebSocket().onComplete(ws -> { + if (ws.succeeded()) { + event.request().resume(); + ws.result().handler(buff -> { + event.response().write(buff); + }); + } else { + log.error("WebSocket failed", ws.cause()); + } + }); + } else { + // handle normal request + r.sendBuffer(event.body().buffer()).andThen(resp -> { + event.response().setStatusCode(resp.result().statusCode()); + resp.result().headers().forEach(h -> event.response().putHeader(h.getKey(), h.getValue())); + event.response().end(resp.result().bodyAsBuffer()); + }); + } + } + }; + } +}