Adding Dapr Container default wait strategy based on healthz/outbound (#1105)

* Adding Dapr Container default wait strategy based on healthz/outbound

Signed-off-by: Artur Ciocanu <ciocanu@adobe.com>

* Ensure Testcontainers Dapr uses the alpha version

Signed-off-by: Artur Ciocanu <ciocanu@adobe.com>

---------

Signed-off-by: Artur Ciocanu <ciocanu@adobe.com>
Co-authored-by: Artur Ciocanu <ciocanu@adobe.com>
This commit is contained in:
artur-ciocanu 2024-08-26 18:29:20 +03:00 committed by GitHub
parent b808c92320
commit a6923ed75c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 92 additions and 73 deletions

View File

@ -25,6 +25,8 @@
<springboot.version>3.3.1</springboot.version> <springboot.version>3.3.1</springboot.version>
<logback-classic.version>1.4.12</logback-classic.version> <logback-classic.version>1.4.12</logback-classic.version>
<wiremock.version>3.9.1</wiremock.version> <wiremock.version>3.9.1</wiremock.version>
<testcontainers-dapr.version>${dapr.sdk.alpha.version}</testcontainers-dapr.version>
<testcontainers-test.version>1.20.0</testcontainers-test.version>
</properties> </properties>
<dependencyManagement> <dependencyManagement>
@ -214,6 +216,17 @@
<version>4.0.1</version> <version>4.0.1</version>
<scope>compile</scope> <scope>compile</scope>
</dependency> </dependency>
<dependency>
<groupId>io.dapr</groupId>
<artifactId>testcontainers-dapr</artifactId>
<version>${testcontainers-dapr.version}</version>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${testcontainers-test.version}</version>
<scope>test</scope>
</dependency>
</dependencies> </dependencies>
<build> <build>

View File

@ -13,7 +13,7 @@ limitations under the License.
package io.dapr.it.testcontainers; package io.dapr.it.testcontainers;
import com.github.tomakehurst.wiremock.junit.WireMockRule; import com.github.tomakehurst.wiremock.junit5.WireMockTest;
import io.dapr.client.DaprClient; import io.dapr.client.DaprClient;
import io.dapr.client.DaprClientBuilder; import io.dapr.client.DaprClientBuilder;
import io.dapr.client.domain.Metadata; import io.dapr.client.domain.Metadata;
@ -23,10 +23,10 @@ import io.dapr.testcontainers.DaprContainer;
import okhttp3.OkHttpClient; import okhttp3.OkHttpClient;
import okhttp3.Request; import okhttp3.Request;
import okhttp3.Response; import okhttp3.Response;
import org.junit.BeforeClass; import org.junit.jupiter.api.BeforeAll;
import org.junit.ClassRule; import org.junit.jupiter.api.Test;
import org.junit.Test; import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.Testcontainers; import org.testcontainers.junit.jupiter.Testcontainers;
import java.io.IOException; import java.io.IOException;
import java.util.Map; import java.util.Map;
@ -43,11 +43,12 @@ 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.urlEqualTo;
import static com.github.tomakehurst.wiremock.client.WireMock.urlMatching; import static com.github.tomakehurst.wiremock.client.WireMock.urlMatching;
import static com.github.tomakehurst.wiremock.client.WireMock.verify; import static com.github.tomakehurst.wiremock.client.WireMock.verify;
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static java.util.Collections.singletonMap; import static java.util.Collections.singletonMap;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
@Testcontainers
@WireMockTest(httpPort = 8081)
public class DaprContainerTest { public class DaprContainerTest {
// Time-to-live for messages published. // Time-to-live for messages published.
@ -57,10 +58,7 @@ public class DaprContainerTest {
private static final String PUBSUB_NAME = "pubsub"; private static final String PUBSUB_NAME = "pubsub";
private static final String PUBSUB_TOPIC_NAME = "topic"; private static final String PUBSUB_TOPIC_NAME = "topic";
@ClassRule @Container
public static WireMockRule wireMockRule = new WireMockRule(wireMockConfig().port(8081));
@ClassRule
public static DaprContainer daprContainer = new DaprContainer("daprio/daprd") public static DaprContainer daprContainer = new DaprContainer("daprio/daprd")
.withAppName("dapr-app") .withAppName("dapr-app")
.withAppPort(8081) .withAppPort(8081)
@ -69,16 +67,15 @@ public class DaprContainerTest {
/** /**
* Sets the Dapr properties for the test. * Sets the Dapr properties for the test.
*/ */
@BeforeClass @BeforeAll
public static void setDaprProperties() { public static void setDaprProperties() {
configStub(); configStub();
Testcontainers.exposeHostPorts(8081); org.testcontainers.Testcontainers.exposeHostPorts(8081);
System.setProperty("dapr.grpc.port", Integer.toString(daprContainer.getGrpcPort())); System.setProperty("dapr.grpc.port", Integer.toString(daprContainer.getGrpcPort()));
System.setProperty("dapr.http.port", Integer.toString(daprContainer.getHttpPort())); System.setProperty("dapr.http.port", Integer.toString(daprContainer.getHttpPort()));
} }
private static void configStub() { private static void configStub() {
stubFor(any(urlMatching("/dapr/subscribe")) stubFor(any(urlMatching("/dapr/subscribe"))
.willReturn(aResponse().withBody("[]").withStatus(200))); .willReturn(aResponse().withBody("[]").withStatus(200)));
@ -98,19 +95,20 @@ public class DaprContainerTest {
@Test @Test
public void testDaprContainerDefaults() { public void testDaprContainerDefaults() {
assertEquals( assertEquals(
"The pubsub and kvstore component should be configured by default",
2, 2,
daprContainer.getComponents().size()); daprContainer.getComponents().size(),
"The pubsub and kvstore component should be configured by default"
);
assertEquals( assertEquals(
"A subscription should be configured by default if none is provided",
1, 1,
daprContainer.getSubscriptions().size()); daprContainer.getSubscriptions().size(),
"A subscription should be configured by default if none is provided");
} }
@Test @Test
public void testStateStore() throws Exception { public void testStateStore() throws Exception {
try (DaprClient client = (new DaprClientBuilder()).build()) { try (DaprClient client = (new DaprClientBuilder()).build()) {
client.waitForSidecar(5000).block(); client.waitForSidecar(1000).block();
String value = "value"; String value = "value";
// Save state // Save state
@ -127,7 +125,7 @@ public class DaprContainerTest {
public void testPlacement() throws Exception { public void testPlacement() throws Exception {
// Here we are just waiting for Dapr to be ready // Here we are just waiting for Dapr to be ready
try (DaprClient client = (new DaprClientBuilder()).build()) { try (DaprClient client = (new DaprClientBuilder()).build()) {
client.waitForSidecar(5000).block(); client.waitForSidecar(1000).block();
} }
OkHttpClient client = new OkHttpClient.Builder().build(); OkHttpClient client = new OkHttpClient.Builder().build();
@ -143,13 +141,12 @@ public class DaprContainerTest {
throw new IOException("Unexpected response: " + response.code()); throw new IOException("Unexpected response: " + response.code());
} }
} }
} }
@Test @Test
public void testPubSub() throws Exception { public void testPubSub() throws Exception {
try (DaprClient client = (new DaprClientBuilder()).build()) { try (DaprClient client = (new DaprClientBuilder()).build()) {
client.waitForSidecar(5000).block(); client.waitForSidecar(1000).block();
String message = "message content"; String message = "message content";
Map<String, String> metadata = singletonMap(Metadata.TTL_IN_SECONDS, MESSAGE_TTL_IN_SECONDS); Map<String, String> metadata = singletonMap(Metadata.TTL_IN_SECONDS, MESSAGE_TTL_IN_SECONDS);

View File

@ -11,19 +11,23 @@
limitations under the License. limitations under the License.
*/ */
package io.dapr.testcontainers; package io.dapr.it.testcontainers;
import org.junit.Assert; import io.dapr.testcontainers.DaprPlacementContainer;
import org.junit.ClassRule; import org.junit.jupiter.api.Test;
import org.junit.Test; import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import static org.junit.jupiter.api.Assertions.assertEquals;
@Testcontainers
public class DaprPlacementContainerTest { public class DaprPlacementContainerTest {
@ClassRule @Container
public static DaprPlacementContainer placement = new DaprPlacementContainer("daprio/placement"); private static final DaprPlacementContainer PLACEMENT_CONTAINER = new DaprPlacementContainer("daprio/placement");
@Test @Test
public void testDaprPlacementContainerDefaults() { public void testDaprPlacementContainerDefaults() {
Assert.assertEquals("The default port is set", 50005, placement.getPort()); assertEquals(50005, PLACEMENT_CONTAINER.getPort(), "The default port is set");
} }
} }

View File

@ -15,6 +15,11 @@
<packaging>jar</packaging> <packaging>jar</packaging>
<dependencies> <dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency> <dependency>
<groupId>org.yaml</groupId> <groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId> <artifactId>snakeyaml</artifactId>

View File

@ -15,6 +15,8 @@ package io.dapr.testcontainers;
import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.Network; import org.testcontainers.containers.Network;
import org.testcontainers.containers.wait.strategy.Wait;
import org.testcontainers.containers.wait.strategy.WaitStrategy;
import org.testcontainers.images.builder.Transferable; import org.testcontainers.images.builder.Transferable;
import org.testcontainers.utility.DockerImageName; import org.testcontainers.utility.DockerImageName;
import org.yaml.snakeyaml.DumperOptions; import org.yaml.snakeyaml.DumperOptions;
@ -38,6 +40,10 @@ public class DaprContainer extends GenericContainer<DaprContainer> {
private static final int DAPRD_DEFAULT_HTTP_PORT = 3500; private static final int DAPRD_DEFAULT_HTTP_PORT = 3500;
private static final int DAPRD_DEFAULT_GRPC_PORT = 50001; private static final int DAPRD_DEFAULT_GRPC_PORT = 50001;
private static final WaitStrategy WAIT_STRATEGY = Wait.forHttp("/v1.0/healthz/outbound")
.forPort(DAPRD_DEFAULT_HTTP_PORT)
.forStatusCodeMatching(statusCode -> statusCode >= 200 && statusCode <= 399);
private final Set<Component> components = new HashSet<>(); private final Set<Component> components = new HashSet<>();
private final Set<Subscription> subscriptions = new HashSet<>(); private final Set<Subscription> subscriptions = new HashSet<>();
private DaprProtocol protocol = DaprProtocol.HTTP; private DaprProtocol protocol = DaprProtocol.HTTP;
@ -59,23 +65,9 @@ public class DaprContainer extends GenericContainer<DaprContainer> {
public DaprContainer(DockerImageName dockerImageName) { public DaprContainer(DockerImageName dockerImageName) {
super(dockerImageName); super(dockerImageName);
dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME); dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME);
// For susbcriptions the container needs to access the app channel
withAccessToHost(true); 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); withExposedPorts(DAPRD_DEFAULT_HTTP_PORT, DAPRD_DEFAULT_GRPC_PORT);
setWaitStrategy(WAIT_STRATEGY);
}
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);
} }
/** /**
@ -124,8 +116,6 @@ public class DaprContainer extends GenericContainer<DaprContainer> {
return this; return this;
} }
/** /**
* Adds a Dapr component from a YAML file. * Adds a Dapr component from a YAML file.
* @param path Path to the YAML file. * @param path Path to the YAML file.
@ -197,6 +187,7 @@ public class DaprContainer extends GenericContainer<DaprContainer> {
if (!component.getMetadata().isEmpty()) { if (!component.getMetadata().isEmpty()) {
componentSpec.put("metadata", component.getMetadata()); componentSpec.put("metadata", component.getMetadata());
} }
componentProps.put("spec", componentSpec); componentProps.put("spec", componentSpec);
return Collections.unmodifiableMap(componentProps); return Collections.unmodifiableMap(componentProps);
} }
@ -231,6 +222,7 @@ public class DaprContainer extends GenericContainer<DaprContainer> {
if (getNetwork() == null) { if (getNetwork() == null) {
withNetwork(Network.newNetwork()); withNetwork(Network.newNetwork());
} }
if (this.placementContainer == null) { if (this.placementContainer == null) {
this.placementContainer = new DaprPlacementContainer(this.placementDockerImageName) this.placementContainer = new DaprPlacementContainer(this.placementDockerImageName)
.withNetwork(getNetwork()) .withNetwork(getNetwork())
@ -253,10 +245,12 @@ public class DaprContainer extends GenericContainer<DaprContainer> {
cmds.add("--app-channel-address"); cmds.add("--app-channel-address");
cmds.add(appChannelAddress); cmds.add(appChannelAddress);
} }
if (appPort != null) { if (appPort != null) {
cmds.add("--app-port"); cmds.add("--app-port");
cmds.add(Integer.toString(appPort)); cmds.add(Integer.toString(appPort));
} }
cmds.add("--log-level"); cmds.add("--log-level");
cmds.add(daprLogLevel.toString()); cmds.add(daprLogLevel.toString());
cmds.add("-components-path"); cmds.add("-components-path");
@ -338,4 +332,13 @@ public class DaprContainer extends GenericContainer<DaprContainer> {
public int hashCode() { public int hashCode() {
return super.hashCode(); return super.hashCode();
} }
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);
}
} }

View File

@ -17,7 +17,7 @@ public enum DaprProtocol {
HTTP("http"), HTTP("http"),
GRPC("grpc"); GRPC("grpc");
private String name; private final String name;
DaprProtocol(String name) { DaprProtocol(String name) {
this.name = name; this.name = name;

View File

@ -14,10 +14,10 @@ limitations under the License.
package io.dapr.testcontainers; package io.dapr.testcontainers;
public class Subscription { public class Subscription {
private String name; private final String name;
private String pubsubName; private final String pubsubName;
private String topic; private final String topic;
private String route; private final String route;
/** /**
* Creates a new subscription. * Creates a new subscription.

View File

@ -13,8 +13,7 @@ limitations under the License.
package io.dapr.testcontainers; package io.dapr.testcontainers;
import org.junit.Assert; import org.junit.jupiter.api.Test;
import org.junit.Test;
import java.net.URL; import java.net.URL;
import java.nio.file.Path; import java.nio.file.Path;
@ -22,7 +21,9 @@ import java.nio.file.Paths;
import java.util.Collections; import java.util.Collections;
import java.util.Set; import java.util.Set;
import static org.junit.Assert.assertThrows; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
public class DaprComponentTest { public class DaprComponentTest {
@ -39,10 +40,10 @@ public class DaprComponentTest {
.withAppChannelAddress("host.testcontainers.internal"); .withAppChannelAddress("host.testcontainers.internal");
Set<Component> components = dapr.getComponents(); Set<Component> components = dapr.getComponents();
Assert.assertEquals(1, components.size()); assertEquals(1, components.size());
Component kvstore = components.iterator().next(); Component kvstore = components.iterator().next();
Assert.assertEquals(false, kvstore.getMetadata().isEmpty()); assertFalse(kvstore.getMetadata().isEmpty());
String componentYaml = dapr.componentToYaml(kvstore); String componentYaml = dapr.componentToYaml(kvstore);
String expectedComponentYaml = "metadata:\n" + " name: statestore\n" String expectedComponentYaml = "metadata:\n" + " name: statestore\n"
@ -55,25 +56,21 @@ public class DaprComponentTest {
+ " type: state.in-memory\n" + " type: state.in-memory\n"
+ " version: v1\n"; + " version: v1\n";
Assert.assertEquals(expectedComponentYaml, componentYaml); assertEquals(expectedComponentYaml, componentYaml);
} }
@Test @Test
public void containerConfigurationTest() { public void containerConfigurationTest() {
DaprContainer dapr = new DaprContainer("daprio/daprd") DaprContainer dapr = new DaprContainer("daprio/daprd")
.withAppName("dapr-app") .withAppName("dapr-app")
.withAppPort(8081) .withAppPort(8081)
.withDaprLogLevel(DaprLogLevel.DEBUG) .withDaprLogLevel(DaprLogLevel.DEBUG)
.withAppChannelAddress("host.testcontainers.internal"); .withAppChannelAddress("host.testcontainers.internal");
dapr.configure(); dapr.configure();
assertThrows(IllegalStateException.class, () -> { dapr.getHttpEndpoint(); }); assertThrows(IllegalStateException.class, dapr::getHttpEndpoint);
assertThrows(IllegalStateException.class, () -> { dapr.getGrpcPort(); }); assertThrows(IllegalStateException.class, dapr::getGrpcPort);
} }
@Test @Test
@ -85,7 +82,7 @@ public class DaprComponentTest {
.withAppChannelAddress("host.testcontainers.internal"); .withAppChannelAddress("host.testcontainers.internal");
Set<Subscription> subscriptions = dapr.getSubscriptions(); Set<Subscription> subscriptions = dapr.getSubscriptions();
Assert.assertEquals(1, subscriptions.size()); assertEquals(1, subscriptions.size());
String subscriptionYaml = dapr.subscriptionToYaml(subscriptions.iterator().next()); String subscriptionYaml = dapr.subscriptionToYaml(subscriptions.iterator().next());
String expectedSubscriptionYaml = "metadata:\n" + " name: my-subscription\n" String expectedSubscriptionYaml = "metadata:\n" + " name: my-subscription\n"
@ -95,7 +92,7 @@ public class DaprComponentTest {
+ " route: /events\n" + " route: /events\n"
+ " pubsubname: pubsub\n" + " pubsubname: pubsub\n"
+ " topic: topic\n"; + " topic: topic\n";
Assert.assertEquals(expectedSubscriptionYaml, subscriptionYaml); assertEquals(expectedSubscriptionYaml, subscriptionYaml);
} }
@Test @Test
@ -110,9 +107,9 @@ public class DaprComponentTest {
.withAppChannelAddress("host.testcontainers.internal"); .withAppChannelAddress("host.testcontainers.internal");
Set<Component> components = dapr.getComponents(); Set<Component> components = dapr.getComponents();
Assert.assertEquals(1, components.size()); assertEquals(1, components.size());
Component kvstore = components.iterator().next(); Component kvstore = components.iterator().next();
Assert.assertEquals(false, kvstore.getMetadata().isEmpty()); assertFalse(kvstore.getMetadata().isEmpty());
String componentYaml = dapr.componentToYaml(kvstore); String componentYaml = dapr.componentToYaml(kvstore);
String expectedComponentYaml = "metadata:\n" String expectedComponentYaml = "metadata:\n"
@ -136,6 +133,6 @@ public class DaprComponentTest {
+ " type: null\n" + " type: null\n"
+ " version: v1\n"; + " version: v1\n";
Assert.assertEquals(expectedComponentYaml, componentYaml); assertEquals(expectedComponentYaml, componentYaml);
} }
} }