Skip to content

Commit

Permalink
proxy for Temporal UI
Browse files Browse the repository at this point in the history
  • Loading branch information
ggrebert committed Oct 7, 2024
1 parent ba09cee commit b7275da
Show file tree
Hide file tree
Showing 12 changed files with 216 additions and 30 deletions.
4 changes: 4 additions & 0 deletions extension/deployment/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@
<groupId>io.quarkus</groupId>
<artifactId>quarkus-devservices-deployment</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-vertx-http-deployment</artifactId>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,8 @@ public class TemporalProcessor {

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

private static final String FEATURE = "temporal";
public static final String FEATURE = "temporal";
public static final DotName CONTEXT_PROPAGATOR = DotName.createSimple(ContextPropagator.class);

@BuildStep
FeatureBuildItem feature() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,15 @@ public class TemporalContainer extends GenericContainer<TemporalContainer> {

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";
}

Expand All @@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<CardPageBuildItem> cardPageBuildItemBuildProducer, List<WorkflowBuildItem> workflows,
List<WorkerBuildItem> workers) {
@BuildStep
void createCard(
BuildProducer<CardPageBuildItem> cardPageBuildItemBuildProducer,
List<WorkflowBuildItem> workflows,
List<WorkerBuildItem> workers,
GlobalDevServicesConfig globalDevServicesConfig,
TemporalUiConfig uiConfig,
TemporalDevserviceConfig temporalDevserviceConfig,
ManagementInterfaceBuildTimeConfig managementInterfaceBuildTimeConfig,
LaunchModeBuildItem launchMode,
NonApplicationRootPathBuildItem nonApplicationRootPathBuildItem) {
final CardPageBuildItem card = new CardPageBuildItem();

final PageBuilder<ExternalPageBuilder> versionPage = Page.externalPageBuilder("Version")
Expand Down Expand Up @@ -58,11 +74,47 @@ void createCard(BuildProducer<CardPageBuildItem> 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<String> 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<ExternalPageBuilder> 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");
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,13 @@ public interface TemporalDevserviceConfig {
*/
// @WithDefault("temporalio/auto-setup")
@WithDefault("temporaliotest/auto-setup")

String image();

/**
* The version of the image to use for the Temporal Devservice.
*/
// @WithDefault("sha-053ea8f")
@WithDefault("latest")
String version();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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<DevServicesResultBuildItem> devServiceProducer) {
void build(
TemporalDevserviceConfig config,
BuildProducer<DevServicesResultBuildItem> 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<RouteBuildItem> 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());
}

}
Original file line number Diff line number Diff line change
@@ -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<String> url();

}
8 changes: 8 additions & 0 deletions extension/runtime/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,14 @@
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-opentracing-shim</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-vertx-http</artifactId>
</dependency>
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-web-client</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
Expand Down
Original file line number Diff line number Diff line change
@@ -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<RoutingContext> handler(Supplier<Vertx> vertx) {
final var portOptional = ConfigProvider.getConfig().getOptionalValue("quarkus.temporal.ui.port", Integer.class);
final var client = WebClient.create(vertx.get());

return new Handler<RoutingContext>() {
@Override
public void handle(RoutingContext event) {
if (!portOptional.isPresent()) {
event.response().setStatusCode(404).end();
return;
}

final Integer port = portOptional.get();
final HttpRequest<Buffer> 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());
});
}
}
};
}
}
Loading

0 comments on commit b7275da

Please sign in to comment.