Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit ac3fb6d

Browse files
committedAug 27, 2024
proxy for Temporal UI
1 parent 5a5084b commit ac3fb6d

File tree

10 files changed

+208
-13
lines changed

10 files changed

+208
-13
lines changed
 

‎docs/modules/ROOT/pages/includes/quarkus-temporal.adoc

+2-2
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ ifndef::add-copy-button-to-env-var[]
2424
Environment variable: `+++QUARKUS_TEMPORAL_ENABLE_MOCK+++`
2525
endif::add-copy-button-to-env-var[]
2626
--|boolean
27-
|`true`
27+
|`false`
2828

2929

3030
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]`
@@ -840,7 +840,7 @@ Environment variable: `+++QUARKUS_TEMPORAL_WORKFLOW_RETRIES_INITIAL_INTERVAL+++`
840840
endif::add-copy-button-to-env-var[]
841841
--|link:https://docs.oracle.com/javase/8/docs/api/java/time/Duration.html[Duration]
842842
link:#duration-note-anchor-{summaryTableId}[icon:question-circle[title=More information about the Duration format]]
843-
|`1S`
843+
|`1s`
844844

845845

846846
a| [[quarkus-temporal_quarkus-temporal-workflow-retries-backoff-coefficient]]`link:#quarkus-temporal_quarkus-temporal-workflow-retries-backoff-coefficient[quarkus.temporal.workflow.retries.backoff-coefficient]`

‎extension/deployment/pom.xml

+4
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@
2525
<groupId>io.quarkus</groupId>
2626
<artifactId>quarkus-devservices-deployment</artifactId>
2727
</dependency>
28+
<dependency>
29+
<groupId>io.quarkus</groupId>
30+
<artifactId>quarkus-vertx-http-deployment</artifactId>
31+
</dependency>
2832
<dependency>
2933
<groupId>org.testcontainers</groupId>
3034
<artifactId>testcontainers</artifactId>

‎extension/deployment/src/main/java/io/quarkiverse/temporal/deployment/TemporalProcessor.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ public class TemporalProcessor {
6969

7070
public static final DotName ACTIVITY_INTERFACE = DotName.createSimple(ActivityInterface.class);
7171

72-
private static final String FEATURE = "temporal";
72+
public static final String FEATURE = "temporal";
7373

7474
@BuildStep
7575
FeatureBuildItem feature() {

‎extension/deployment/src/main/java/io/quarkiverse/temporal/deployment/devui/TemporalContainer.java

+10-3
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,15 @@ public class TemporalContainer extends GenericContainer<TemporalContainer> {
1212

1313
private final TemporalDevserviceConfig config;
1414
private String serviceName;
15+
private String label;
16+
private String path;
1517
// private String hostname;
1618

17-
public TemporalContainer(DockerImageName dockerImageName, TemporalDevserviceConfig config) {
19+
public TemporalContainer(DockerImageName dockerImageName, TemporalDevserviceConfig config, String path, String label) {
1820
super(dockerImageName);
1921
this.config = config;
22+
this.label = label;
23+
this.path = path;
2024
this.serviceName = "temporal";
2125
}
2226

@@ -26,16 +30,19 @@ protected void configure() {
2630

2731
withCreateContainerCmdModifier(cmd -> {
2832
cmd.withEntrypoint("/usr/local/bin/temporal");
29-
cmd.withCmd("server", "start-dev", "--ip", "0.0.0.0");
33+
cmd.withCmd("server", "start-dev", "--ip", "0.0.0.0", "--ui-public-path", path);
3034
});
3135

3236
withExposedPorts(SERVER_EXPOSED_PORT, UI_EXPOSED_PORT);
3337

34-
withLabel("quarkus-devservice-temporal", serviceName);
38+
withLabel(label, serviceName);
3539

3640
withReuse(config.reuse());
3741

3842
// hostname = ConfigureUtil.configureSharedNetwork(this, "temporal-" + serviceName);
3943
}
4044

45+
public String getUiUrl() {
46+
return "http://" + getHost() + ":" + getMappedPort(UI_EXPOSED_PORT) + path;
47+
}
4148
}

‎extension/deployment/src/main/java/io/quarkiverse/temporal/deployment/devui/TemporalDevUIProcessor.java

+56-4
Original file line numberDiff line numberDiff line change
@@ -2,28 +2,44 @@
22

33
import java.util.List;
44
import java.util.Objects;
5+
import java.util.Optional;
56
import java.util.stream.Collectors;
67

8+
import io.quarkiverse.temporal.deployment.TemporalProcessor;
79
import io.quarkiverse.temporal.deployment.WorkerBuildItem;
810
import io.quarkiverse.temporal.deployment.WorkflowBuildItem;
911
import io.quarkus.deployment.IsDevelopment;
1012
import io.quarkus.deployment.annotations.BuildProducer;
1113
import io.quarkus.deployment.annotations.BuildStep;
14+
import io.quarkus.deployment.annotations.BuildSteps;
15+
import io.quarkus.deployment.builditem.LaunchModeBuildItem;
16+
import io.quarkus.deployment.dev.devservices.GlobalDevServicesConfig;
1217
import io.quarkus.devui.spi.page.CardPageBuildItem;
1318
import io.quarkus.devui.spi.page.ExternalPageBuilder;
1419
import io.quarkus.devui.spi.page.Page;
1520
import io.quarkus.devui.spi.page.PageBuilder;
1621
import io.quarkus.devui.spi.page.TableDataPageBuilder;
22+
import io.quarkus.vertx.http.deployment.NonApplicationRootPathBuildItem;
23+
import io.quarkus.vertx.http.runtime.management.ManagementInterfaceBuildTimeConfig;
1724
import io.temporal.client.WorkflowClient;
1825

1926
/**
2027
* Dev UI card for displaying important details such Temporal version.
2128
*/
29+
@BuildSteps(onlyIf = IsDevelopment.class)
2230
public class TemporalDevUIProcessor {
2331

24-
@BuildStep(onlyIf = IsDevelopment.class)
25-
void createCard(BuildProducer<CardPageBuildItem> cardPageBuildItemBuildProducer, List<WorkflowBuildItem> workflows,
26-
List<WorkerBuildItem> workers) {
32+
@BuildStep
33+
void createCard(
34+
BuildProducer<CardPageBuildItem> cardPageBuildItemBuildProducer,
35+
List<WorkflowBuildItem> workflows,
36+
List<WorkerBuildItem> workers,
37+
GlobalDevServicesConfig globalDevServicesConfig,
38+
TemporalUiConfig uiConfig,
39+
TemporalDevserviceConfig temporalDevserviceConfig,
40+
ManagementInterfaceBuildTimeConfig managementInterfaceBuildTimeConfig,
41+
LaunchModeBuildItem launchMode,
42+
NonApplicationRootPathBuildItem nonApplicationRootPathBuildItem) {
2743
final CardPageBuildItem card = new CardPageBuildItem();
2844

2945
final PageBuilder<ExternalPageBuilder> versionPage = Page.externalPageBuilder("Version")
@@ -58,11 +74,47 @@ void createCard(BuildProducer<CardPageBuildItem> cardPageBuildItemBuildProducer,
5874
card.addBuildTimeData("workers",
5975
workers.stream().map(WorkerBuildTimeData::new).collect(Collectors.toList()));
6076

77+
uiPage(uiConfig.url(), temporalDevserviceConfig, managementInterfaceBuildTimeConfig, launchMode,
78+
nonApplicationRootPathBuildItem, card);
79+
6180
card.setCustomCard("qwc-temporal-card.js");
6281

6382
cardPageBuildItemBuildProducer.produce(card);
6483
}
6584

85+
private void uiPage(
86+
Optional<String> configPath,
87+
TemporalDevserviceConfig temporalDevserviceConfig,
88+
ManagementInterfaceBuildTimeConfig managementInterfaceBuildTimeConfig,
89+
LaunchModeBuildItem launchMode,
90+
NonApplicationRootPathBuildItem nonApplicationRootPathBuildItem,
91+
CardPageBuildItem card) {
92+
var path = configPath;
93+
94+
// check if the UI url is set in the config or if the devservice is enabled
95+
if (!path.isPresent() && Boolean.TRUE.equals(temporalDevserviceConfig.enabled())) {
96+
var defaultBasePath = nonApplicationRootPathBuildItem.resolveManagementPath(
97+
TemporalProcessor.FEATURE,
98+
managementInterfaceBuildTimeConfig,
99+
launchMode);
100+
101+
path = Optional.of(defaultBasePath);
102+
}
103+
104+
// if the path is not set, we don't have a UI to link to
105+
if (!path.isPresent()) {
106+
return;
107+
}
108+
109+
// add the UI page
110+
final PageBuilder<ExternalPageBuilder> uiPage = Page.externalPageBuilder("UI")
111+
.icon("font-awesome-solid:desktop")
112+
.url(path.get(), path.get())
113+
.isHtmlContent();
114+
115+
card.addPage(uiPage);
116+
}
117+
66118
static class WorkflowBuildTimeData {
67119
WorkflowBuildTimeData(WorkflowBuildItem item) {
68120
this.name = item.workflow.getName().replaceAll("\\B\\w+(\\.[a-z])", "$1");
@@ -88,7 +140,7 @@ static class WorkerBuildTimeData {
88140
this.workflows = item.workflows.stream().map(workflow -> workflow.getName().replaceAll("\\B\\w+(\\.[a-z])", "$1"))
89141
.collect(Collectors.toList());
90142
this.activities = item.activities.stream()
91-
.map(activities -> activities.getName().replaceAll("\\B\\w+(\\.[a-z])", "$1")).collect(Collectors.toList());
143+
.map(activity -> activity.getName().replaceAll("\\B\\w+(\\.[a-z])", "$1")).collect(Collectors.toList());
92144
}
93145

94146
private final String name;

‎extension/deployment/src/main/java/io/quarkiverse/temporal/deployment/devui/TemporalDevserviceConfig.java

+2
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,14 @@ public interface TemporalDevserviceConfig {
1818
/**
1919
* The image to use for the Temporal Devservice.
2020
*/
21+
// @WithDefault("temporaliotest/auto-setup")
2122
@WithDefault("temporalio/auto-setup")
2223
String image();
2324

2425
/**
2526
* The version of the image to use for the Temporal Devservice.
2627
*/
28+
// @WithDefault("sha-053ea8f")
2729
@WithDefault("latest")
2830
String version();
2931

‎extension/deployment/src/main/java/io/quarkiverse/temporal/deployment/devui/TemporalDevserviceProcessor.java

+44-3
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,22 @@
44

55
import org.testcontainers.utility.DockerImageName;
66

7+
import io.quarkiverse.temporal.deployment.TemporalProcessor;
8+
import io.quarkiverse.temporal.devui.TemporalUiProxy;
79
import io.quarkus.deployment.IsNormal;
810
import io.quarkus.deployment.annotations.BuildProducer;
911
import io.quarkus.deployment.annotations.BuildStep;
1012
import io.quarkus.deployment.annotations.BuildSteps;
13+
import io.quarkus.deployment.annotations.ExecutionTime;
14+
import io.quarkus.deployment.annotations.Record;
1115
import io.quarkus.deployment.builditem.DevServicesResultBuildItem;
16+
import io.quarkus.deployment.builditem.LaunchModeBuildItem;
1217
import io.quarkus.deployment.dev.devservices.GlobalDevServicesConfig;
1318
import io.quarkus.devservices.common.ContainerLocator;
19+
import io.quarkus.vertx.core.deployment.CoreVertxBuildItem;
20+
import io.quarkus.vertx.http.deployment.NonApplicationRootPathBuildItem;
21+
import io.quarkus.vertx.http.deployment.RouteBuildItem;
22+
import io.quarkus.vertx.http.runtime.management.ManagementInterfaceBuildTimeConfig;
1423

1524
@BuildSteps(onlyIfNot = IsNormal.class, onlyIf = { GlobalDevServicesConfig.Enabled.class })
1625
public class TemporalDevserviceProcessor {
@@ -21,21 +30,53 @@ public class TemporalDevserviceProcessor {
2130
private static final ContainerLocator containerLocator = new ContainerLocator(DEV_SERVICE_LABEL, SERVER_EXPOSED_PORT);
2231

2332
@BuildStep
24-
public void build(TemporalDevserviceConfig config, BuildProducer<DevServicesResultBuildItem> devServiceProducer) {
33+
void build(
34+
TemporalDevserviceConfig config,
35+
BuildProducer<DevServicesResultBuildItem> devServiceProducer,
36+
ManagementInterfaceBuildTimeConfig managementInterfaceBuildTimeConfig,
37+
LaunchModeBuildItem launchMode,
38+
NonApplicationRootPathBuildItem nonApplicationRootPathBuildItem) {
2539
if (Boolean.FALSE.equals(config.enabled())) {
2640
return;
2741
}
2842

43+
var path = nonApplicationRootPathBuildItem.resolveManagementPath(
44+
TemporalProcessor.FEATURE,
45+
managementInterfaceBuildTimeConfig,
46+
launchMode);
47+
2948
var imageStr = config.image() + ":" + config.version();
3049
var image = DockerImageName.parse(imageStr)
3150
.asCompatibleSubstituteFor(imageStr);
3251

33-
var serverContainer = new TemporalContainer(image, config);
52+
var serverContainer = new TemporalContainer(image, config, path, DEV_SERVICE_LABEL);
3453
serverContainer.start();
3554

3655
var serverPort = serverContainer.getMappedPort(SERVER_EXPOSED_PORT);
3756
devServiceProducer.produce(new DevServicesResultBuildItem("temporal", serverContainer.getContainerId(), Map.of(
38-
"quarkus.temporal.connection.target", "localhost:" + serverPort)));
57+
"quarkus.temporal.connection.target", "localhost:" + serverPort,
58+
"quarkus.temporal.ui.url", serverContainer.getUiUrl(),
59+
"quarkus.temporal.ui.port", serverContainer.getMappedPort(UI_EXPOSED_PORT).toString())));
60+
}
61+
62+
@BuildStep
63+
@Record(ExecutionTime.RUNTIME_INIT)
64+
void registerProxy(
65+
TemporalDevserviceConfig config,
66+
TemporalUiProxy proxy,
67+
BuildProducer<RouteBuildItem> routes,
68+
NonApplicationRootPathBuildItem frameworkRoot,
69+
CoreVertxBuildItem coreVertxBuildItem) {
70+
if (Boolean.FALSE.equals(config.enabled())) {
71+
return;
72+
}
73+
74+
routes.produce(frameworkRoot.routeBuilder()
75+
.management()
76+
.route(TemporalProcessor.FEATURE + "/*")
77+
.displayOnNotFoundPage("Portal UI not found")
78+
.handler(proxy.handler(coreVertxBuildItem.getVertx()))
79+
.build());
3980
}
4081

4182
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package io.quarkiverse.temporal.deployment.devui;
2+
3+
import java.util.Optional;
4+
5+
import io.quarkus.runtime.annotations.ConfigPhase;
6+
import io.quarkus.runtime.annotations.ConfigRoot;
7+
import io.smallrye.config.ConfigMapping;
8+
9+
@ConfigMapping(prefix = "quarkus.temporal.ui")
10+
@ConfigRoot(phase = ConfigPhase.BUILD_TIME)
11+
public interface TemporalUiConfig {
12+
13+
/**
14+
* The url of the Temporal UI.
15+
*/
16+
Optional<String> url();
17+
18+
}

‎extension/runtime/pom.xml

+8
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,14 @@
3737
<groupId>io.grpc</groupId>
3838
<artifactId>grpc-netty-shaded</artifactId>
3939
</dependency>
40+
<dependency>
41+
<groupId>io.quarkus</groupId>
42+
<artifactId>quarkus-vertx-http</artifactId>
43+
</dependency>
44+
<dependency>
45+
<groupId>io.vertx</groupId>
46+
<artifactId>vertx-web-client</artifactId>
47+
</dependency>
4048
</dependencies>
4149
<build>
4250
<plugins>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package io.quarkiverse.temporal.devui;
2+
3+
import java.util.function.Supplier;
4+
5+
import org.eclipse.microprofile.config.ConfigProvider;
6+
import org.jboss.logging.Logger;
7+
8+
import io.quarkus.runtime.annotations.Recorder;
9+
import io.vertx.core.Handler;
10+
import io.vertx.core.Vertx;
11+
import io.vertx.core.buffer.Buffer;
12+
import io.vertx.ext.web.RoutingContext;
13+
import io.vertx.ext.web.client.HttpRequest;
14+
import io.vertx.ext.web.client.WebClient;
15+
16+
@Recorder
17+
public class TemporalUiProxy {
18+
19+
private static final Logger log = Logger.getLogger(TemporalUiProxy.class);
20+
21+
public Handler<RoutingContext> handler(Supplier<Vertx> vertx) {
22+
final var portOptional = ConfigProvider.getConfig().getOptionalValue("quarkus.temporal.ui.port", Integer.class);
23+
final var client = WebClient.create(vertx.get());
24+
25+
return new Handler<RoutingContext>() {
26+
@Override
27+
public void handle(RoutingContext event) {
28+
if (!portOptional.isPresent()) {
29+
event.response().setStatusCode(404).end();
30+
return;
31+
}
32+
33+
final Integer port = portOptional.get();
34+
final HttpRequest<Buffer> r = client.request(event.request().method(), port, "localhost",
35+
event.request().uri());
36+
37+
// copy all headers
38+
event.request().headers().forEach(h -> r.putHeader(h.getKey(), h.getValue()));
39+
40+
if ("websocket".equals(event.request().getHeader("upgrade"))) {
41+
// handle WebSocket request
42+
event.request().toWebSocket().onComplete(ws -> {
43+
if (ws.succeeded()) {
44+
event.request().resume();
45+
ws.result().handler(buff -> {
46+
event.response().write(buff);
47+
});
48+
} else {
49+
log.error("WebSocket failed", ws.cause());
50+
}
51+
});
52+
} else {
53+
// handle normal request
54+
r.sendBuffer(event.body().buffer()).andThen(resp -> {
55+
event.response().setStatusCode(resp.result().statusCode());
56+
resp.result().headers().forEach(h -> event.response().putHeader(h.getKey(), h.getValue()));
57+
event.response().end(resp.result().bodyAsBuffer());
58+
});
59+
}
60+
}
61+
};
62+
}
63+
}

0 commit comments

Comments
 (0)
Please sign in to comment.