Initial Testcontainers integration for Dapr (#1085)

* initial testcontainers pr

Signed-off-by: salaboy <Salaboy@gmail.com>

* fixing variable reference

Signed-off-by: salaboy <Salaboy@gmail.com>

* adding equals to spotbug issues

Signed-off-by: salaboy <Salaboy@gmail.com>

* adding http port to run tests

Signed-off-by: salaboy <Salaboy@gmail.com>

* updating pom

Signed-off-by: salaboy <Salaboy@gmail.com>

* fixing style

Signed-off-by: salaboy <Salaboy@gmail.com>

* extracting classes

Signed-off-by: salaboy <Salaboy@gmail.com>

* removing restassured dependency

Signed-off-by: salaboy <Salaboy@gmail.com>

* refactoring IT out to sdk-tests

Signed-off-by: salaboy <Salaboy@gmail.com>

* adding correct wiremock dep version

Signed-off-by: salaboy <Salaboy@gmail.com>

* missing header

Signed-off-by: salaboy <Salaboy@gmail.com>

* fixing spotbugs issue

Signed-off-by: salaboy <Salaboy@gmail.com>

* adding hashcode too

Signed-off-by: salaboy <Salaboy@gmail.com>

* testing configure method

Signed-off-by: salaboy <Salaboy@gmail.com>

* making inmutable collections and maps

Signed-off-by: salaboy <Salaboy@gmail.com>

* checkstyle

Signed-off-by: salaboy <Salaboy@gmail.com>

* removing space

Signed-off-by: salaboy <Salaboy@gmail.com>

* Refactor tracking of alpha artifact version

Signed-off-by: Artur Souza <asouza.pro@gmail.com>
Signed-off-by: salaboy <Salaboy@gmail.com>

* Use
Usage:  docker compose [OPTIONS] COMMAND

Define and run multi-container applications with Docker

Options:
      --all-resources              Include all resources, even those not used by services
      --ansi string                Control when to print ANSI control characters ("never"|"always"|"auto") (default "auto")
      --compatibility              Run compose in backward compatibility mode
      --dry-run                    Execute command in dry run mode
      --env-file stringArray       Specify an alternate environment file
  -f, --file stringArray           Compose configuration files
      --parallel int               Control max parallelism, -1 for unlimited (default -1)
      --profile stringArray        Specify a profile to enable
      --progress string            Set type of progress output (auto, tty, plain, quiet) (default "auto")
      --project-directory string   Specify an alternate working directory
                                   (default: the path of the, first specified, Compose file)
  -p, --project-name string        Project name

Commands:
  attach      Attach local standard input, output, and error streams to a service's running container
  build       Build or rebuild services
  config      Parse, resolve and render compose file in canonical format
  cp          Copy files/folders between a service container and the local filesystem
  create      Creates containers for a service
  down        Stop and remove containers, networks
  events      Receive real time events from containers
  exec        Execute a command in a running container
  images      List images used by the created containers
  kill        Force stop service containers
  logs        View output from containers
  ls          List running compose projects
  pause       Pause services
  port        Print the public port for a port binding
  ps          List containers
  pull        Pull service images
  push        Push service images
  restart     Restart service containers
  rm          Removes stopped service containers
  run         Run a one-off command on a service
  scale       Scale services
  start       Start services
  stats       Display a live stream of container(s) resource usage statistics
  stop        Stop services
  top         Display the running processes
  unpause     Unpause services
  up          Create and start containers
  version     Show the Docker Compose version information
  wait        Block until the first service container stops
  watch       Watch build context for service and rebuild/refresh containers when files are updated

Run 'docker compose COMMAND --help' for more information on a command. instead of docker-compose

Signed-off-by: Artur Souza <asouza.pro@gmail.com>
Signed-off-by: salaboy <Salaboy@gmail.com>

* removing version from docker compose

Signed-off-by: salaboy <Salaboy@gmail.com>

* Update README.md

Signed-off-by: Artur Souza <asouza.pro@gmail.com>

---------

Signed-off-by: salaboy <Salaboy@gmail.com>
Signed-off-by: Artur Souza <asouza.pro@gmail.com>
Co-authored-by: Artur Souza <asouza.pro@gmail.com>
This commit is contained in:
salaboy 2024-08-05 08:30:47 +01:00 committed by GitHub
parent 5618af5ed9
commit e30dc2df28
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 1148 additions and 21 deletions

View File

@ -4,14 +4,23 @@ set -uex
DAPR_JAVA_SDK_VERSION=$1
# The workflows sdk tracks the regular SDK minor and patch versions, just not the major.
# Replaces the workflows SDK major version to 0 until it is stable.
DAPR_JAVA_WORKFLOWS_SDK_VERSION=`echo $DAPR_JAVA_SDK_VERSION | sed 's/^[0-9]*\./0./'`
# Alpha artifacts of the sdk tracks the regular SDK minor and patch versions, just not the major.
# Replaces the SDK major version to 0 for alpha artifacts.
DAPR_JAVA_SDK_ALPHA_VERSION=`echo $DAPR_JAVA_SDK_VERSION | sed 's/^[0-9]*\./0./'`
mvn versions:set -DnewVersion=$DAPR_JAVA_SDK_VERSION
mvn versions:set-property -Dproperty=dapr.sdk.alpha.version -DnewVersion=$DAPR_JAVA_SDK_ALPHA_VERSION
mvn versions:set-property -Dproperty=dapr.sdk.version -DnewVersion=$DAPR_JAVA_SDK_VERSION -f sdk-tests/pom.xml
mvn versions:set-property -Dproperty=dapr.sdk.alpha.version -DnewVersion=$DAPR_JAVA_SDK_ALPHA_VERSION -f sdk-tests/pom.xml
mvn versions:set -DnewVersion=$DAPR_JAVA_WORKFLOWS_SDK_VERSION -f sdk-workflows/pom.xml
mvn versions:set-property -Dproperty=dapr.sdk-workflows.version -DnewVersion=$DAPR_JAVA_WORKFLOWS_SDK_VERSION
###################
# Alpha artifacts #
###################
# sdk-workflows
mvn versions:set -DnewVersion=$DAPR_JAVA_SDK_ALPHA_VERSION -f sdk-workflows/pom.xml
# testcontainers-dapr
mvn versions:set -DnewVersion=$DAPR_JAVA_SDK_ALPHA_VERSION -f testcontainers-dapr/pom.xml
git clean -f

View File

@ -99,7 +99,7 @@ jobs:
./dist/linux_amd64/release/placement &
- name: Spin local environment
run: |
docker-compose -f ./sdk-tests/deploy/local-test.yml up -d mongo kafka
docker compose -f ./sdk-tests/deploy/local-test.yml up -d mongo kafka
docker ps
- name: Install local ToxiProxy to simulate connectivity issues to Dapr sidecar
run: |

View File

@ -237,13 +237,13 @@ Similarly, all of these need to be run for running the ITs either individually o
Run the following commands from the root of the repo to start all the docker containers that the tests depend on.
```bash
docker-compose -f ./sdk-tests/deploy/local-test.yml up -d
docker compose -f ./sdk-tests/deploy/local-test.yml up -d
```
To stop the containers and services, run the following commands.
```bash
docker-compose -f ./sdk-tests/deploy/local-test.yml down
docker compose -f ./sdk-tests/deploy/local-test.yml down
```

View File

@ -113,7 +113,7 @@
<dependency>
<groupId>io.dapr</groupId>
<artifactId>dapr-sdk-workflows</artifactId>
<version>${dapr.sdk-workflows.version}</version>
<version>${dapr.sdk.alpha.version}</version>
</dependency>
<dependency>
<groupId>io.dapr</groupId>

View File

@ -58,13 +58,11 @@ Before getting into the application code, follow these steps in order to set up
<!-- STEP
name: Setup kafka container
expected_stderr_lines:
- 'Creating network "http_default" with the default driver'
sleep: 20
-->
```bash
docker-compose -f ./src/main/java/io/dapr/examples/bindings/http/docker-compose-single-kafka.yml up -d
docker compose -f ./src/main/java/io/dapr/examples/bindings/http/docker-compose-single-kafka.yml up -d
```
<!-- END_STEP -->
@ -248,7 +246,7 @@ name: Cleanup Kafka containers
-->
```bash
docker-compose -f ./src/main/java/io/dapr/examples/bindings/http/docker-compose-single-kafka.yml down
docker compose -f ./src/main/java/io/dapr/examples/bindings/http/docker-compose-single-kafka.yml down
```
<!-- END_STEP -->

View File

@ -1,4 +1,3 @@
version: '3'
services:
zookeeper:
image: confluentinc/cp-zookeeper:7.4.4

View File

@ -44,7 +44,7 @@ sleep: 5
-->
```bash
docker-compose -f ./src/main/java/io/dapr/examples/querystate/docker-compose-single-mongo.yml up -d
docker compose -f ./src/main/java/io/dapr/examples/querystate/docker-compose-single-mongo.yml up -d
```
<!-- END_STEP -->
@ -305,7 +305,7 @@ name: Cleanup MongoDB containers
-->
```bash
docker-compose -f ./src/main/java/io/dapr/examples/querystate/docker-compose-single-mongo.yml down
docker compose -f ./src/main/java/io/dapr/examples/querystate/docker-compose-single-mongo.yml down
```
<!-- END_STEP -->

View File

@ -1,4 +1,3 @@
version: '3'
services:
mongo:
image: mongo

View File

@ -44,7 +44,7 @@ sleep: 5
-->
```bash
docker-compose -f ./src/main/java/io/dapr/examples/state/docker-compose-single-mongo.yml up -d
docker compose -f ./src/main/java/io/dapr/examples/state/docker-compose-single-mongo.yml up -d
```
<!-- END_STEP -->
@ -227,7 +227,7 @@ name: Cleanup MongoDB container
-->
```bash
docker-compose -f ./src/main/java/io/dapr/examples/state/docker-compose-single-mongo.yml down
docker compose -f ./src/main/java/io/dapr/examples/state/docker-compose-single-mongo.yml down
```
<!-- END_STEP -->

View File

@ -1,4 +1,3 @@
version: '3'
services:
mongo:
image: mongo

View File

@ -17,7 +17,7 @@
<grpc.version>1.59.0</grpc.version>
<protobuf.version>3.17.3</protobuf.version>
<dapr.proto.baseurl>https://raw.githubusercontent.com/dapr/dapr/v1.13.0-rc.5/dapr/proto</dapr.proto.baseurl>
<dapr.sdk-workflows.version>0.12.0-SNAPSHOT</dapr.sdk-workflows.version>
<dapr.sdk.alpha.version>0.12.0-SNAPSHOT</dapr.sdk.alpha.version>
<os-maven-plugin.version>1.6.2</os-maven-plugin.version>
<maven-dependency-plugin.version>3.1.1</maven-dependency-plugin.version>
<maven-antrun-plugin.version>1.8</maven-antrun-plugin.version>
@ -38,6 +38,8 @@
<failsafe.version>3.2.2</failsafe.version>
<surefire.version>3.2.2</surefire.version>
<junit-bom.version>5.8.2</junit-bom.version>
<snakeyaml.version>2.0</snakeyaml.version>
<testcontainers.version>1.20.0</testcontainers.version>
</properties>
<distributionManagement>
@ -330,6 +332,8 @@
<module>sdk-workflows</module>
<module>sdk-springboot</module>
<module>examples</module>
<!-- We are following test containers artifact convention on purpose, don't rename -->
<module>testcontainers-dapr</module>
<!-- don't add sdk-tests to the build,
it's only used for CI testing by github action
<module>sdk-tests</module>

View File

@ -16,12 +16,15 @@
<maven.compiler.target>17</maven.compiler.target>
<maven.deploy.skip>true</maven.deploy.skip>
<dapr.sdk.version>1.12.0-SNAPSHOT</dapr.sdk.version>
<dapr.sdk.alpha.version>0.12.0-SNAPSHOT</dapr.sdk.alpha.version>
<protobuf.output.directory>${project.build.directory}/generated-sources</protobuf.output.directory>
<protobuf.input.directory>${project.basedir}/proto</protobuf.input.directory>
<grpc.version>1.59.0</grpc.version>
<protobuf.version>3.17.3</protobuf.version>
<opentelemetry.version>1.39.0</opentelemetry.version>
<spring-boot.version>3.3.1</spring-boot.version>
<logback-classic.version>1.4.12</logback-classic.version>
<wiremock.version>3.9.1</wiremock.version>
</properties>
<dependencyManagement>
@ -130,6 +133,12 @@
<version>${dapr.sdk.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.dapr</groupId>
<artifactId>testcontainers-dapr</artifactId>
<version>${dapr.sdk.alpha.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.dapr</groupId>
<artifactId>dapr-sdk-actors</artifactId>
@ -147,6 +156,18 @@
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.wiremock</groupId>
<artifactId>wiremock-standalone</artifactId>
<version>${wiremock.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>${logback-classic.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>

View File

@ -0,0 +1,162 @@
/*
* Copyright 2024 The Dapr Authors
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
limitations under the License.
*/
package io.dapr.it.testcontainers;
import com.github.tomakehurst.wiremock.junit.WireMockRule;
import io.dapr.client.DaprClient;
import io.dapr.client.DaprClientBuilder;
import io.dapr.client.domain.Metadata;
import io.dapr.client.domain.State;
import io.dapr.testcontainers.DaprContainer;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import org.junit.BeforeClass;
import org.junit.ClassRule;
import org.junit.Test;
import org.testcontainers.Testcontainers;
import java.io.IOException;
import java.util.Map;
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
import static com.github.tomakehurst.wiremock.client.WireMock.any;
import static com.github.tomakehurst.wiremock.client.WireMock.configureFor;
import static com.github.tomakehurst.wiremock.client.WireMock.equalTo;
import static com.github.tomakehurst.wiremock.client.WireMock.get;
import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor;
import static com.github.tomakehurst.wiremock.client.WireMock.post;
import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor;
import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
import static com.github.tomakehurst.wiremock.client.WireMock.urlMatching;
import static com.github.tomakehurst.wiremock.client.WireMock.verify;
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig;
import static java.util.Collections.singletonMap;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
public class DaprContainerTest {
// Time-to-live for messages published.
private static final String MESSAGE_TTL_IN_SECONDS = "1000";
private static final String STATE_STORE_NAME = "kvstore";
private static final String KEY = "my-key";
private static final String PUBSUB_NAME = "pubsub";
private static final String PUBSUB_TOPIC_NAME = "topic";
@ClassRule
public static WireMockRule wireMockRule = new WireMockRule(wireMockConfig().port(8081));
@ClassRule
public static DaprContainer daprContainer = new DaprContainer("daprio/daprd")
.withAppName("dapr-app")
.withAppPort(8081)
.withAppChannelAddress("host.testcontainers.internal");
/**
* Sets the Dapr properties for the test.
*/
@BeforeClass
public static void setDaprProperties() {
configStub();
Testcontainers.exposeHostPorts(8081);
System.setProperty("dapr.grpc.port", Integer.toString(daprContainer.getGrpcPort()));
System.setProperty("dapr.http.port", Integer.toString(daprContainer.getHttpPort()));
}
private static void configStub() {
stubFor(any(urlMatching("/dapr/subscribe"))
.willReturn(aResponse().withBody("[]").withStatus(200)));
stubFor(get(urlMatching("/dapr/config"))
.willReturn(aResponse().withBody("[]").withStatus(200)));
stubFor(any(urlMatching("/([a-z1-9]*)"))
.willReturn(aResponse().withBody("[]").withStatus(200)));
// create a stub
stubFor(post(urlEqualTo("/events"))
.willReturn(aResponse().withBody("event received!").withStatus(200)));
configureFor("localhost", 8081);
}
@Test
public void testDaprContainerDefaults() {
assertEquals(
"The pubsub and kvstore component should be configured by default",
2,
daprContainer.getComponents().size());
assertEquals(
"A subscription should be configured by default if none is provided",
1,
daprContainer.getSubscriptions().size());
}
@Test
public void testStateStore() throws Exception {
try (DaprClient client = (new DaprClientBuilder()).build()) {
client.waitForSidecar(5000).block();
String value = "value";
// Save state
client.saveState(STATE_STORE_NAME, KEY, value).block();
// Get the state back from the state store
State<String> retrievedState = client.getState(STATE_STORE_NAME, KEY, String.class).block();
assertEquals("The value retrieved should be the same as the one stored", value, retrievedState.getValue());
}
}
@Test
public void testPlacement() throws Exception {
// Here we are just waiting for Dapr to be ready
try (DaprClient client = (new DaprClientBuilder()).build()) {
client.waitForSidecar(5000).block();
}
OkHttpClient client = new OkHttpClient.Builder().build();
String url = "http://" + daprContainer.getHost() + ":" + daprContainer.getMappedPort(3500);
Request request = new Request.Builder().url(url + "/v1.0/metadata").build();
try (Response response = client.newCall(request).execute()) {
if (response.isSuccessful()) {
assertTrue(response.body().string().contains("placement: connected"));
} else {
throw new IOException("Unexpected response: " + response.code());
}
}
}
@Test
public void testPubSub() throws Exception {
try (DaprClient client = (new DaprClientBuilder()).build()) {
client.waitForSidecar(5000).block();
String message = "message content";
Map<String, String> metadata = singletonMap(Metadata.TTL_IN_SECONDS, MESSAGE_TTL_IN_SECONDS);
client.publishEvent(PUBSUB_NAME, PUBSUB_TOPIC_NAME, message, metadata).block();
}
verify(getRequestedFor(urlMatching("/dapr/config")));
verify(postRequestedFor(urlEqualTo("/events")).withHeader("Content-Type", equalTo("application/cloudevents+json")));
}
}

111
testcontainers-dapr/pom.xml Normal file
View File

@ -0,0 +1,111 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>io.dapr</groupId>
<artifactId>dapr-sdk-parent</artifactId>
<version>1.12.0-SNAPSHOT</version>
</parent>
<artifactId>testcontainers-dapr</artifactId>
<name>testcontainers-dapr</name>
<description>Testcontainers Dapr Module</description>
<version>0.12.0-SNAPSHOT</version>
<packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
<version>${snakeyaml.version}</version>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<version>${testcontainers.version}</version>
</dependency>
<dependency>
<groupId>io.dapr</groupId>
<artifactId>dapr-sdk</artifactId>
<version>${project.parent.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<version>3.2.1</version>
<executions>
<execution>
<id>attach-sources</id>
<goals>
<goal>jar-no-fork</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>3.2.0</version>
<executions>
<execution>
<id>attach-javadocs</id>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.11</version>
<executions>
<execution>
<id>default-prepare-agent</id>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>report</id>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
<configuration>
<outputDirectory>target/jacoco-report/</outputDirectory>
</configuration>
</execution>
<execution>
<id>check</id>
<goals>
<goal>check</goal>
</goals>
<configuration>
<rules>
<rule>
<element>BUNDLE</element>
<limits>
<limit>
<counter>LINE</counter>
<value>COVEREDRATIO</value>
<minimum>80%</minimum>
</limit>
</limits>
</rule>
</rules>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,83 @@
/*
* Copyright 2024 The Dapr Authors
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
limitations under the License.
*/
package io.dapr.testcontainers;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
/**
* Represents a Dapr component.
*/
public class Component {
private String name;
private String type;
private String version;
private List<MetadataEntry> metadata;
/**
* Creates a new component.
*
* @param name Component name.
* @param type Component type.
* @param version Component version.
* @param metadata Metadata.
*/
public Component(String name, String type, String version, Map<String, String> metadata) {
this.name = name;
this.type = type;
this.version = version;
List<MetadataEntry> entries = new ArrayList<>();
if (!metadata.isEmpty()) {
for (Map.Entry<String, String> entry : metadata.entrySet()) {
entries.add(new MetadataEntry(entry.getKey(), entry.getValue()));
}
}
this.metadata = Collections.unmodifiableList(entries);
}
/**
* Creates a new component.
*
* @param name Component name.
* @param type Component type.
* @param version Component version.
* @param metadataEntries Component metadata entries.
*/
public Component(String name, String type, String version, List<MetadataEntry> metadataEntries) {
this.name = name;
this.type = type;
this.version = version;
metadata = Objects.requireNonNull(metadataEntries);
}
public String getName() {
return name;
}
public String getType() {
return type;
}
public List<MetadataEntry> getMetadata() {
return metadata;
}
public String getVersion() {
return version;
}
}

View File

@ -0,0 +1,341 @@
/*
* Copyright 2024 The Dapr Authors
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
limitations under the License.
*/
package io.dapr.testcontainers;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.Network;
import org.testcontainers.images.builder.Transferable;
import org.testcontainers.utility.DockerImageName;
import org.yaml.snakeyaml.DumperOptions;
import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.nodes.Tag;
import org.yaml.snakeyaml.representer.Representer;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class DaprContainer extends GenericContainer<DaprContainer> {
private static final int DAPRD_DEFAULT_HTTP_PORT = 3500;
private static final int DAPRD_DEFAULT_GRPC_PORT = 50001;
private final Set<Component> components = new HashSet<>();
private final Set<Subscription> subscriptions = new HashSet<>();
private DaprProtocol protocol = DaprProtocol.HTTP;
private String appName;
private Integer appPort = null;
private DaprLogLevel daprLogLevel = DaprLogLevel.INFO;
private String appChannelAddress = "localhost";
private String placementService = "placement";
private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("daprio/daprd");
private static final Yaml yaml = getYamlMapper();
private DaprPlacementContainer placementContainer;
private String placementDockerImageName = "daprio/placement";
private boolean shouldReusePlacement;
/**
* Creates a new Dapr container.
* @param dockerImageName Docker image name.
*/
public DaprContainer(DockerImageName dockerImageName) {
super(dockerImageName);
dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME);
// For susbcriptions the container needs to access the app channel
withAccessToHost(true);
// Here we don't want to wait for the Dapr sidecar to be ready, as the sidecar
// needs to
// connect with the application for susbcriptions
withExposedPorts(DAPRD_DEFAULT_HTTP_PORT, DAPRD_DEFAULT_GRPC_PORT);
}
private static Yaml getYamlMapper() {
DumperOptions options = new DumperOptions();
options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK);
options.setPrettyFlow(true);
Representer representer = new Representer(options);
representer.addClassTag(MetadataEntry.class, Tag.MAP);
return new Yaml(representer);
}
/**
* Creates a new Dapr container.
* @param image Docker image name.
*/
public DaprContainer(String image) {
this(DockerImageName.parse(image));
}
public Set<Component> getComponents() {
return components;
}
public Set<Subscription> getSubscriptions() {
return subscriptions;
}
public DaprContainer withAppPort(Integer port) {
this.appPort = port;
return this;
}
public DaprContainer withPlacementService(String placementService) {
this.placementService = placementService;
return this;
}
public DaprContainer withAppName(String appName) {
this.appName = appName;
return this;
}
public DaprContainer withDaprLogLevel(DaprLogLevel daprLogLevel) {
this.daprLogLevel = daprLogLevel;
return this;
}
public DaprContainer withSubscription(Subscription subscription) {
subscriptions.add(subscription);
return this;
}
public DaprContainer withComponent(Component component) {
components.add(component);
return this;
}
/**
* Adds a Dapr component from a YAML file.
* @param path Path to the YAML file.
* @return This container.
*/
public DaprContainer withComponent(Path path) {
try {
Map<String, Object> component = this.yaml.loadAs(Files.newInputStream(path), Map.class);
String type = (String) component.get("type");
Map<String, Object> metadata = (Map<String, Object>) component.get("metadata");
String name = (String) metadata.get("name");
Map<String, Object> spec = (Map<String, Object>) component.get("spec");
String version = (String) spec.get("version");
List<Map<String, String>> specMetadata =
(List<Map<String, String>>) spec.getOrDefault("metadata", Collections.emptyMap());
ArrayList<MetadataEntry> metadataEntries = new ArrayList<>();
for (Map<String, String> specMetadataItem : specMetadata) {
for (Map.Entry<String, String> metadataItem : specMetadataItem.entrySet()) {
metadataEntries.add(new MetadataEntry(metadataItem.getKey(), metadataItem.getValue()));
}
}
return withComponent(new Component(name, type, version, metadataEntries));
} catch (IOException e) {
logger().warn("Error while reading component from {}", path.toAbsolutePath());
}
return this;
}
public int getHttpPort() {
return getMappedPort(DAPRD_DEFAULT_HTTP_PORT);
}
public String getHttpEndpoint() {
return "http://" + getHost() + ":" + getMappedPort(DAPRD_DEFAULT_HTTP_PORT);
}
public int getGrpcPort() {
return getMappedPort(DAPRD_DEFAULT_GRPC_PORT);
}
public DaprContainer withAppChannelAddress(String appChannelAddress) {
this.appChannelAddress = appChannelAddress;
return this;
}
/**
* Get a map of Dapr component details.
* @param component A Dapr Component.
* @return Map of component details.
*/
public Map<String, Object> componentToMap(Component component) {
Map<String, Object> componentProps = new HashMap<>();
componentProps.put("apiVersion", "dapr.io/v1alpha1");
componentProps.put("kind", "Component");
Map<String, String> componentMetadata = new LinkedHashMap<>();
componentMetadata.put("name", component.getName());
componentProps.put("metadata", componentMetadata);
Map<String, Object> componentSpec = new HashMap<>();
componentSpec.put("type", component.getType());
componentSpec.put("version", component.getVersion());
if (!component.getMetadata().isEmpty()) {
componentSpec.put("metadata", component.getMetadata());
}
componentProps.put("spec", componentSpec);
return Collections.unmodifiableMap(componentProps);
}
/**
* Get a map of Dapr subscription details.
* @param subscription A Dapr Subscription.
* @return Map of subscription details.
*/
public Map<String, Object> subscriptionToMap(Subscription subscription) {
Map<String, Object> subscriptionProps = new HashMap<>();
subscriptionProps.put("apiVersion", "dapr.io/v1alpha1");
subscriptionProps.put("kind", "Subscription");
Map<String, String> subscriptionMetadata = new LinkedHashMap<>();
subscriptionMetadata.put("name", subscription.getName());
subscriptionProps.put("metadata", subscriptionMetadata);
Map<String, Object> subscriptionSpec = new HashMap<>();
subscriptionSpec.put("pubsubname", subscription.getPubsubName());
subscriptionSpec.put("topic", subscription.getTopic());
subscriptionSpec.put("route", subscription.getRoute());
subscriptionProps.put("spec", subscriptionSpec);
return Collections.unmodifiableMap(subscriptionProps);
}
@Override
protected void configure() {
super.configure();
if (getNetwork() == null) {
withNetwork(Network.newNetwork());
}
if (this.placementContainer == null) {
this.placementContainer = new DaprPlacementContainer(this.placementDockerImageName)
.withNetwork(getNetwork())
.withNetworkAliases(placementService)
.withReuse(this.shouldReusePlacement);
this.placementContainer.start();
}
List<String> cmds = new ArrayList<>();
cmds.add("./daprd");
cmds.add("-app-id");
cmds.add(appName);
cmds.add("--dapr-listen-addresses=0.0.0.0");
cmds.add("--app-protocol");
cmds.add(protocol.getName());
cmds.add("-placement-host-address");
cmds.add(placementService + ":50005");
if (appChannelAddress != null && !appChannelAddress.isEmpty()) {
cmds.add("--app-channel-address");
cmds.add(appChannelAddress);
}
if (appPort != null) {
cmds.add("--app-port");
cmds.add(Integer.toString(appPort));
}
cmds.add("--log-level");
cmds.add(daprLogLevel.toString());
cmds.add("-components-path");
cmds.add("/dapr-resources");
withCommand(cmds.toArray(new String[]{}));
if (components.isEmpty()) {
components.add(new Component("kvstore", "state.in-memory", "v1", Collections.emptyMap()));
components.add(new Component("pubsub", "pubsub.in-memory", "v1", Collections.emptyMap()));
}
if (subscriptions.isEmpty() && !components.isEmpty()) {
subscriptions.add(new Subscription("local", "pubsub", "topic", "/events"));
}
for (Component component : components) {
String componentYaml = componentToYaml(component);
withCopyToContainer(Transferable.of(componentYaml), "/dapr-resources/" + component.getName() + ".yaml");
}
for (Subscription subscription : subscriptions) {
String subscriptionYaml = subscriptionToYaml(subscription);
withCopyToContainer(Transferable.of(subscriptionYaml), "/dapr-resources/" + subscription.getName() + ".yaml");
}
}
public String subscriptionToYaml(Subscription subscription) {
Map<String, Object> subscriptionMap = subscriptionToMap(subscription);
return yaml.dumpAsMap(subscriptionMap);
}
public String componentToYaml(Component component) {
Map<String, Object> componentMap = componentToMap(component);
return yaml.dumpAsMap(componentMap);
}
public String getAppName() {
return appName;
}
public Integer getAppPort() {
return appPort;
}
public String getAppChannelAddress() {
return appChannelAddress;
}
public String getPlacementService() {
return placementService;
}
public static DockerImageName getDefaultImageName() {
return DEFAULT_IMAGE_NAME;
}
public DaprContainer withPlacementImage(String placementDockerImageName) {
this.placementDockerImageName = placementDockerImageName;
return this;
}
public DaprContainer withReusablePlacement(boolean reuse) {
this.shouldReusePlacement = reuse;
return this;
}
public DaprContainer withPlacementContainer(DaprPlacementContainer placementContainer) {
this.placementContainer = placementContainer;
return this;
}
// Required by spotbugs plugin
@Override
public boolean equals(Object o) {
return super.equals(o);
}
@Override
public int hashCode() {
return super.hashCode();
}
}

View File

@ -0,0 +1,21 @@
/*
* Copyright 2024 The Dapr Authors
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
limitations under the License.
*/
package io.dapr.testcontainers;
public enum DaprLogLevel {
ERROR,
WARN,
INFO,
DEBUG
}

View File

@ -0,0 +1,75 @@
/*
* Copyright 2024 The Dapr Authors
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
limitations under the License.
*/
package io.dapr.testcontainers;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.utility.DockerImageName;
/**
* Test container for Dapr placement service.
*/
public class DaprPlacementContainer extends GenericContainer<DaprPlacementContainer> {
private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("daprio/placement");
private int placementPort = 50005;
/**
* Creates a new Dapr placement container.
* @param dockerImageName Docker image name.
*/
public DaprPlacementContainer(DockerImageName dockerImageName) {
super(dockerImageName);
dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME);
withExposedPorts(placementPort);
}
/**
* Creates a new Dapr placement container.
* @param image Docker image name.
*/
public DaprPlacementContainer(String image) {
this(DockerImageName.parse(image));
}
@Override
protected void configure() {
super.configure();
withCommand("./placement", "-port", Integer.toString(placementPort));
}
public static DockerImageName getDefaultImageName() {
return DEFAULT_IMAGE_NAME;
}
public DaprPlacementContainer withPort(Integer port) {
this.placementPort = port;
return this;
}
public int getPort() {
return placementPort;
}
// Required by spotbugs plugin
@Override
public boolean equals(Object o) {
return super.equals(o);
}
@Override
public int hashCode() {
return super.hashCode();
}
}

View File

@ -0,0 +1,29 @@
/*
* Copyright 2024 The Dapr Authors
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
limitations under the License.
*/
package io.dapr.testcontainers;
public enum DaprProtocol {
HTTP("http"),
GRPC("grpc");
private String name;
DaprProtocol(String name) {
this.name = name;
}
public String getName() {
return name;
}
}

View File

@ -0,0 +1,40 @@
/*
* Copyright 2024 The Dapr Authors
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
limitations under the License.
*/
package io.dapr.testcontainers;
public class MetadataEntry {
private String name;
private String value;
public MetadataEntry(String name, String value) {
this.name = name;
this.value = value;
}
public String getName() {
return name;
}
public String getValue() {
return value;
}
public void setName(String name) {
this.name = name;
}
public void setValue(String value) {
this.value = value;
}
}

View File

@ -0,0 +1,52 @@
/*
* Copyright 2024 The Dapr Authors
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
limitations under the License.
*/
package io.dapr.testcontainers;
public class Subscription {
private String name;
private String pubsubName;
private String topic;
private String route;
/**
* Creates a new subscription.
*
* @param name Subscription name.
* @param pubsubName PubSub name.
* @param topic Topic name.
* @param route Route.
*/
public Subscription(String name, String pubsubName, String topic, String route) {
this.name = name;
this.pubsubName = pubsubName;
this.topic = topic;
this.route = route;
}
public String getName() {
return name;
}
public String getPubsubName() {
return pubsubName;
}
public String getTopic() {
return topic;
}
public String getRoute() {
return route;
}
}

View File

@ -0,0 +1,141 @@
/*
* Copyright 2024 The Dapr Authors
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
limitations under the License.
*/
package io.dapr.testcontainers;
import org.junit.Assert;
import org.junit.Test;
import java.net.URL;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collections;
import java.util.Set;
import static org.junit.Assert.assertThrows;
public class DaprComponentTest {
@Test
public void componentStateStoreSerializationTest() {
DaprContainer dapr = new DaprContainer("daprio/daprd")
.withAppName("dapr-app")
.withAppPort(8081)
.withComponent(new Component(
"statestore",
"state.in-memory",
"v1",
Collections.singletonMap("actorStateStore", "true")))
.withAppChannelAddress("host.testcontainers.internal");
Set<Component> components = dapr.getComponents();
Assert.assertEquals(1, components.size());
Component kvstore = components.iterator().next();
Assert.assertEquals(false, kvstore.getMetadata().isEmpty());
String componentYaml = dapr.componentToYaml(kvstore);
String expectedComponentYaml = "metadata:\n" + " name: statestore\n"
+ "apiVersion: dapr.io/v1alpha1\n"
+ "kind: Component\n"
+ "spec:\n"
+ " metadata:\n"
+ " - name: actorStateStore\n"
+ " value: 'true'\n"
+ " type: state.in-memory\n"
+ " version: v1\n";
Assert.assertEquals(expectedComponentYaml, componentYaml);
}
@Test
public void containerConfigurationTest() {
DaprContainer dapr = new DaprContainer("daprio/daprd")
.withAppName("dapr-app")
.withAppPort(8081)
.withDaprLogLevel(DaprLogLevel.DEBUG)
.withAppChannelAddress("host.testcontainers.internal");
dapr.configure();
assertThrows(IllegalStateException.class, () -> { dapr.getHttpEndpoint(); });
assertThrows(IllegalStateException.class, () -> { dapr.getGrpcPort(); });
}
@Test
public void subscriptionSerializationTest() {
DaprContainer dapr = new DaprContainer("daprio/daprd")
.withAppName("dapr-app")
.withAppPort(8081)
.withSubscription(new Subscription("my-subscription", "pubsub", "topic", "/events"))
.withAppChannelAddress("host.testcontainers.internal");
Set<Subscription> subscriptions = dapr.getSubscriptions();
Assert.assertEquals(1, subscriptions.size());
String subscriptionYaml = dapr.subscriptionToYaml(subscriptions.iterator().next());
String expectedSubscriptionYaml = "metadata:\n" + " name: my-subscription\n"
+ "apiVersion: dapr.io/v1alpha1\n"
+ "kind: Subscription\n"
+ "spec:\n"
+ " route: /events\n"
+ " pubsubname: pubsub\n"
+ " topic: topic\n";
Assert.assertEquals(expectedSubscriptionYaml, subscriptionYaml);
}
@Test
public void withComponentFromPath() {
URL stateStoreYaml = this.getClass().getClassLoader().getResource("dapr-resources/statestore.yaml");
Path path = Paths.get(stateStoreYaml.getPath());
DaprContainer dapr = new DaprContainer("daprio/daprd")
.withAppName("dapr-app")
.withAppPort(8081)
.withComponent(path)
.withAppChannelAddress("host.testcontainers.internal");
Set<Component> components = dapr.getComponents();
Assert.assertEquals(1, components.size());
Component kvstore = components.iterator().next();
Assert.assertEquals(false, kvstore.getMetadata().isEmpty());
String componentYaml = dapr.componentToYaml(kvstore);
String expectedComponentYaml = "metadata:\n"
+ " name: statestore\n"
+ "apiVersion: dapr.io/v1alpha1\n"
+ "kind: Component\n"
+ "spec:\n"
+ " metadata:\n"
+ " - name: name\n"
+ " value: keyPrefix\n"
+ " - name: value\n"
+ " value: name\n"
+ " - name: name\n"
+ " value: redisHost\n"
+ " - name: value\n"
+ " value: redis:6379\n"
+ " - name: name\n"
+ " value: redisPassword\n"
+ " - name: value\n"
+ " value: ''\n"
+ " type: null\n"
+ " version: v1\n";
Assert.assertEquals(expectedComponentYaml, componentYaml);
}
}

View File

@ -0,0 +1,29 @@
/*
* Copyright 2024 The Dapr Authors
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
limitations under the License.
*/
package io.dapr.testcontainers;
import org.junit.Assert;
import org.junit.ClassRule;
import org.junit.Test;
public class DaprPlacementContainerTest {
@ClassRule
public static DaprPlacementContainer placement = new DaprPlacementContainer("daprio/placement");
@Test
public void testDaprPlacementContainerDefaults() {
Assert.assertEquals("The default port is set", 50005, placement.getPort());
}
}

View File

@ -0,0 +1,14 @@
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: statestore
spec:
type: state.redis
version: v1
metadata:
- name: keyPrefix
value: name
- name: redisHost
value: redis:6379
- name: redisPassword
value: ""