Skip to content

Commit 2a6a42c

Browse files
committed
Add support for Temporal (#2339)
1 parent c3c6c83 commit 2a6a42c

File tree

11 files changed

+354
-0
lines changed

11 files changed

+354
-0
lines changed

embedded-temporal/README.adoc

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
=== embedded-temporal
2+
3+
==== Maven dependency
4+
5+
.pom.xml
6+
[source,xml]
7+
----
8+
<dependency>
9+
<groupId>com.playtika.testcontainers</groupId>
10+
<artifactId>embedded-temporal</artifactId>
11+
<scope>test</scope>
12+
</dependency>
13+
----
14+
15+
==== Consumes (via `bootstrap.properties`)
16+
17+
* `embedded.temporal.enabled` `(true|false, default is true)`
18+
* `embedded.temporal.reuseContainer` `(true|false, default is false)`
19+
* `embedded.temporal.cliVersion` `(default is '1.3.0')`
20+
** Temporal CLI versions on https://github.com/temporalio/cli/releases
21+
22+
==== Produces
23+
24+
* `embedded.temporal.host`
25+
* `embedded.temporal.port`
26+
* `embedded.temporal.networkAlias`
27+
* `embedded.temporal.internalPort`

embedded-temporal/pom.xml

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
5+
<modelVersion>4.0.0</modelVersion>
6+
7+
<parent>
8+
<groupId>com.playtika.testcontainers</groupId>
9+
<artifactId>testcontainers-spring-boot-parent</artifactId>
10+
<version>3.1.11</version>
11+
<relativePath>../testcontainers-spring-boot-parent</relativePath>
12+
</parent>
13+
14+
<artifactId>embedded-temporal</artifactId>
15+
16+
<properties>
17+
<temporal.version>1.28.4</temporal.version>
18+
</properties>
19+
20+
<dependencyManagement>
21+
<dependencies>
22+
<dependency>
23+
<groupId>io.temporal</groupId>
24+
<artifactId>temporal-bom</artifactId>
25+
<version>${temporal.version}</version>
26+
<type>pom</type>
27+
<scope>import</scope>
28+
</dependency>
29+
</dependencies>
30+
</dependencyManagement>
31+
32+
<dependencies>
33+
<dependency>
34+
<groupId>com.playtika.testcontainers</groupId>
35+
<artifactId>testcontainers-common</artifactId>
36+
</dependency>
37+
<dependency>
38+
<groupId>io.temporal</groupId>
39+
<artifactId>temporal-spring-boot-starter</artifactId>
40+
<scope>test</scope>
41+
<exclusions>
42+
<exclusion>
43+
<groupId>org.springframework.boot</groupId>
44+
<artifactId>spring-boot-starter-logging</artifactId>
45+
</exclusion>
46+
</exclusions>
47+
</dependency>
48+
</dependencies>
49+
50+
</project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package com.playtika.testcontainer.temporal;
2+
3+
import com.playtika.testcontainer.common.spring.DockerPresenceBootstrapConfiguration;
4+
import lombok.extern.slf4j.Slf4j;
5+
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
6+
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
7+
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
8+
import org.springframework.boot.context.properties.EnableConfigurationProperties;
9+
import org.springframework.context.annotation.Bean;
10+
import org.springframework.context.annotation.Configuration;
11+
import org.springframework.core.env.ConfigurableEnvironment;
12+
import org.springframework.core.env.MapPropertySource;
13+
import org.testcontainers.containers.GenericContainer;
14+
import org.testcontainers.containers.Network;
15+
import org.testcontainers.containers.wait.strategy.HostPortWaitStrategy;
16+
import org.testcontainers.images.builder.ImageFromDockerfile;
17+
18+
import java.nio.file.Paths;
19+
import java.util.LinkedHashMap;
20+
import java.util.Optional;
21+
22+
import static com.playtika.testcontainer.common.utils.ContainerUtils.configureCommonsAndStart;
23+
import static com.playtika.testcontainer.temporal.TemporalProperties.BEAN_NAME_EMBEDDED_TEMPORAL;
24+
25+
@Slf4j
26+
@Configuration
27+
@ConditionalOnExpression("${embedded.containers.enabled:true}")
28+
@AutoConfigureAfter(DockerPresenceBootstrapConfiguration.class)
29+
@ConditionalOnProperty(
30+
name = "embedded.temporal.enabled",
31+
havingValue = "true",
32+
matchIfMissing = true)
33+
@EnableConfigurationProperties(TemporalProperties.class)
34+
public class EmbeddedTemporalBootstrapConfiguration {
35+
36+
private static final String TEMPORAL_NETWORK_ALIAS = "temporal.testcontainer.docker";
37+
38+
@Bean(value = BEAN_NAME_EMBEDDED_TEMPORAL, destroyMethod = "stop")
39+
public GenericContainer<?> temporal(ConfigurableEnvironment environment,
40+
TemporalProperties properties,
41+
Optional<Network> network) {
42+
43+
GenericContainer<?> container =
44+
new GenericContainer<>(new ImageFromDockerfile(properties.getDefaultDockerImage(), false)
45+
.withDockerfile(Paths.get("src/main/resources/Dockerfile"))
46+
.withBuildArg("TEMPORAL_CLI_VERSION", properties.getCliVersion()))
47+
.withExposedPorts(properties.getPort())
48+
.withNetworkAliases(TEMPORAL_NETWORK_ALIAS)
49+
.waitingFor(new HostPortWaitStrategy());
50+
51+
network.ifPresent(container::withNetwork);
52+
53+
configureCommonsAndStart(container, properties, log);
54+
55+
registerTemporalEnvironment(container, environment, properties);
56+
57+
return container;
58+
}
59+
60+
private void registerTemporalEnvironment(GenericContainer<?> container,
61+
ConfigurableEnvironment environment,
62+
TemporalProperties properties) {
63+
64+
String host = container.getHost();
65+
Integer port = container.getMappedPort(properties.getPort());
66+
67+
LinkedHashMap<String, Object> map = new LinkedHashMap<>();
68+
map.put("embedded.temporal.host", host);
69+
map.put("embedded.temporal.port", port);
70+
map.put("embedded.temporal.networkAlias", TEMPORAL_NETWORK_ALIAS);
71+
map.put("embedded.temporal.internalPort", properties.getPort());
72+
73+
log.info("Started Temporal service. Connection details: {}, URI: http://{}:{}", map, host, port);
74+
75+
MapPropertySource propertySource = new MapPropertySource("embeddedTemporalInfo", map);
76+
environment.getPropertySources().addFirst(propertySource);
77+
}
78+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package com.playtika.testcontainer.temporal;
2+
3+
import com.playtika.testcontainer.common.properties.CommonContainerProperties;
4+
import lombok.Data;
5+
import lombok.EqualsAndHashCode;
6+
import org.springframework.boot.context.properties.ConfigurationProperties;
7+
8+
@Data
9+
@EqualsAndHashCode(callSuper = true)
10+
@ConfigurationProperties("embedded.temporal")
11+
public class TemporalProperties extends CommonContainerProperties {
12+
13+
public static final String BEAN_NAME_EMBEDDED_TEMPORAL = "embeddedTemporal";
14+
15+
private int port = 7233;
16+
private String cliVersion = "1.3.0";
17+
18+
@Override
19+
public String getDefaultDockerImage() {
20+
return "temporalio/dev:" + cliVersion;
21+
}
22+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
#https://community.temporal.io/t/building-a-docker-image-for-temporalite/6140/3
2+
FROM curlimages/curl:8.13.0 as builder
3+
ARG TEMPORAL_CLI_VERSION
4+
5+
RUN curl -sSf https://temporal.download/cli.sh | sh -s -- --version $TEMPORAL_CLI_VERSION --dir /tmp/temporalio
6+
7+
FROM golang:1.23.4-alpine3.21
8+
9+
COPY --from=builder /tmp/temporalio/bin/temporal /bin/temporal
10+
11+
EXPOSE 7233
12+
13+
ENTRYPOINT ["temporal", "server", "start-dev", "--headless", "--ip" , "0.0.0.0"]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
{
2+
"groups": [
3+
],
4+
"properties": [
5+
{
6+
"name": "embedded.temporal.enabled",
7+
"type": "java.lang.Boolean",
8+
"defaultValue": "true"
9+
},
10+
{
11+
"name": "embedded.temporal.cliVersion",
12+
"type": "java.lang.String",
13+
"description": "The Temporal CLI version.",
14+
"defaultValue": "1.3.0"
15+
}
16+
],
17+
"hints": [
18+
{
19+
"name": "embedded.temporal.enabled",
20+
"values": [
21+
{
22+
"value": "true",
23+
"description": "Enables configuration of Temporal on startup."
24+
},
25+
{
26+
"value": "false",
27+
"description": "Disabled configuration of Temporal on startup."
28+
}
29+
]
30+
},
31+
{
32+
"name": "embedded.temporal.cli-version",
33+
"values": [
34+
{
35+
"value": "1.3.0",
36+
"description": "Default Temporal CLI Ref https://github.com/temporalio/cli/releases for further info."
37+
}
38+
]
39+
}
40+
]
41+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
org.springframework.cloud.bootstrap.BootstrapConfiguration=\
2+
com.playtika.testcontainer.temporal.EmbeddedTemporalBootstrapConfiguration
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package com.playtika.testcontainer.temporal;
2+
3+
import org.junit.jupiter.api.Test;
4+
import org.springframework.boot.autoconfigure.AutoConfigurations;
5+
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
6+
import org.testcontainers.containers.Container;
7+
8+
import static org.assertj.core.api.Assertions.assertThat;
9+
10+
class DisableTemporalTest {
11+
12+
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
13+
.withConfiguration(AutoConfigurations.of(EmbeddedTemporalBootstrapConfiguration.class));
14+
15+
@Test
16+
void contextLoads() {
17+
contextRunner
18+
.withPropertyValues(
19+
"embedded.temporal.enabled=false"
20+
)
21+
.run((context) -> assertThat(context)
22+
.hasNotFailed()
23+
.doesNotHaveBean(Container.class));
24+
}
25+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package com.playtika.testcontainer.temporal;
2+
3+
import io.temporal.api.history.v1.HistoryEvent;
4+
import io.temporal.client.WorkflowClient;
5+
import io.temporal.client.WorkflowOptions;
6+
import io.temporal.serviceclient.WorkflowServiceStubs;
7+
import io.temporal.serviceclient.WorkflowServiceStubsOptions;
8+
import io.temporal.workflow.WorkflowInterface;
9+
import io.temporal.workflow.WorkflowMethod;
10+
import org.junit.jupiter.api.Test;
11+
import org.springframework.beans.factory.annotation.Autowired;
12+
import org.springframework.beans.factory.annotation.Value;
13+
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
14+
import org.springframework.boot.test.context.SpringBootTest;
15+
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
16+
import org.springframework.context.annotation.Bean;
17+
import org.springframework.context.annotation.Configuration;
18+
19+
import java.util.UUID;
20+
21+
import static io.temporal.api.enums.v1.EventType.EVENT_TYPE_WORKFLOW_EXECUTION_STARTED;
22+
import static java.util.UUID.randomUUID;
23+
import static org.assertj.core.api.Assertions.assertThat;
24+
25+
@SpringBootTest(
26+
webEnvironment = WebEnvironment.NONE
27+
, classes = EmbeddedTemporalBootstrapConfigurationTest.TestConfiguration.class
28+
)
29+
class EmbeddedTemporalBootstrapConfigurationTest {
30+
31+
@Autowired
32+
WorkflowClient client;
33+
34+
@Test
35+
void workflowExecutionStart() {
36+
UUID workflowId = randomUUID();
37+
Workflow workflow = client.newWorkflowStub(Workflow.class, WorkflowOptions.newBuilder()
38+
.setTaskQueue("task-queue")
39+
.setWorkflowId(workflowId.toString())
40+
.build());
41+
42+
WorkflowClient.start(workflow::execute);
43+
44+
assertThat(client.streamHistory(workflowId.toString()))
45+
.extracting(HistoryEvent::getEventType)
46+
.contains(EVENT_TYPE_WORKFLOW_EXECUTION_STARTED);
47+
}
48+
49+
@EnableAutoConfiguration
50+
@Configuration
51+
static class TestConfiguration {
52+
53+
@Bean
54+
WorkflowClient workflowClient(@Value("${embedded.temporal.host}") String host,
55+
@Value("${embedded.temporal.port}") Integer port) {
56+
57+
WorkflowServiceStubsOptions options = WorkflowServiceStubsOptions.newBuilder()
58+
.setTarget("%s:%s".formatted(host, port))
59+
.build();
60+
61+
WorkflowServiceStubs service = WorkflowServiceStubs.newServiceStubs(options);
62+
63+
return WorkflowClient.newInstance(service);
64+
}
65+
}
66+
67+
@WorkflowInterface
68+
public interface Workflow {
69+
70+
@WorkflowMethod
71+
void execute();
72+
}
73+
74+
public static class TestWorkflow implements Workflow {
75+
76+
@Override
77+
public void execute() {
78+
}
79+
}
80+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<Configuration shutdownHook="disable">
3+
<Appenders>
4+
<Console name="console" target="SYSTEM_OUT">
5+
<PatternLayout pattern="%d{HH:mm:ss.SSS} |%-5p| %c{1}:%L - %m%n"/>
6+
</Console>
7+
</Appenders>
8+
<Loggers>
9+
<Logger name="com.playtika.testcontainer" level="info"/>
10+
<Logger name="org.springframework" level="error"/>
11+
<Root level="error">
12+
<AppenderRef ref="console"/>
13+
</Root>
14+
</Loggers>
15+
</Configuration>

pom.xml

+1
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@
8686
<module>embedded-mailhog</module>
8787
<module>embedded-aerospike-enterprise</module>
8888
<module>embedded-spicedb</module>
89+
<module>embedded-temporal</module>
8990
</modules>
9091

9192
<properties>

0 commit comments

Comments
 (0)