initCommands = new ArrayList<>();
+
+ private String[] startConsulCmd = new String[] { "agent", "-dev", "-client", "0.0.0.0" };
+
+ public ConsulContainer(String dockerImageName) {
+ this(DockerImageName.parse(dockerImageName));
+ }
+
+ public ConsulContainer(final DockerImageName dockerImageName) {
+ super(dockerImageName);
+ dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME);
+
+ // Use the status leader endpoint to verify if consul is running.
+ setWaitStrategy(Wait.forHttp("/v1/status/leader").forPort(CONSUL_HTTP_PORT).forStatusCode(200));
+
+ withCreateContainerCmdModifier(cmd -> cmd.getHostConfig().withCapAdd(Capability.IPC_LOCK));
+ withEnv("CONSUL_ADDR", "http://0.0.0.0:" + CONSUL_HTTP_PORT);
+ withCommand(startConsulCmd);
+ withExposedPorts(CONSUL_HTTP_PORT, CONSUL_GRPC_PORT);
+ }
+
+ @Override
+ protected void containerIsStarted(InspectContainerResponse containerInfo) {
+ runConsulCommands();
+ }
+
+ private void runConsulCommands() {
+ if (!initCommands.isEmpty()) {
+ String commands = initCommands
+ .stream()
+ .map(command -> "consul " + command)
+ .collect(Collectors.joining(" && "));
+ try {
+ ExecResult execResult = this.execInContainer(new String[] { "/bin/sh", "-c", commands });
+ if (execResult.getExitCode() != 0) {
+ logger()
+ .error(
+ "Failed to execute these init commands {}. Exit code {}. Stdout {}. Stderr {}",
+ initCommands,
+ execResult.getExitCode(),
+ execResult.getStdout(),
+ execResult.getStderr()
+ );
+ }
+ } catch (IOException | InterruptedException e) {
+ logger()
+ .error(
+ "Failed to execute these init commands {}. Exception message: {}",
+ initCommands,
+ e.getMessage()
+ );
+ }
+ }
+ }
+
+ /**
+ * Run consul commands using the consul cli.
+ *
+ * Useful for enableing more secret engines like:
+ *
+ * .withConsulCommand("secrets enable pki")
+ * .withConsulCommand("secrets enable transit")
+ *
+ * or register specific K/V like:
+ *
+ * .withConsulCommand("kv put config/testing1 value123")
+ *
+ * @param commands The commands to send to the consul cli
+ * @return this
+ */
+ public ConsulContainer withConsulCommand(String... commands) {
+ initCommands.addAll(Arrays.asList(commands));
+ return self();
+ }
+}
diff --git a/modules/consul/src/test/java/org/testcontainers/consul/ConsulContainerTest.java b/modules/consul/src/test/java/org/testcontainers/consul/ConsulContainerTest.java
new file mode 100644
index 00000000000..e29ff7ffbd8
--- /dev/null
+++ b/modules/consul/src/test/java/org/testcontainers/consul/ConsulContainerTest.java
@@ -0,0 +1,78 @@
+package org.testcontainers.consul;
+
+import com.ecwid.consul.v1.ConsulClient;
+import com.ecwid.consul.v1.Response;
+import com.ecwid.consul.v1.kv.model.GetValue;
+import io.restassured.RestAssured;
+import org.assertj.core.api.Assertions;
+import org.hamcrest.CoreMatchers;
+import org.hamcrest.MatcherAssert;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.testcontainers.containers.GenericContainer;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * This test shows the pattern to use the ConsulContainer @ClassRule for a junit test. It also has tests that ensure
+ * the properties were added correctly by reading from Consul with the CLI and over HTTP.
+ */
+public class ConsulContainerTest {
+
+ @ClassRule
+ public static ConsulContainer consulContainer = new ConsulContainer(ConsulTestImages.CONSUL_IMAGE)
+ .withConsulCommand("kv put config/testing1 value123");
+
+ @Test
+ public void readFirstPropertyPathWithCli() throws IOException, InterruptedException {
+ GenericContainer.ExecResult result = consulContainer.execInContainer("consul", "kv", "get", "config/testing1");
+ final String output = result.getStdout().replaceAll("\\r?\\n", "");
+ MatcherAssert.assertThat(output, CoreMatchers.containsString("value123"));
+ }
+
+ @Test
+ public void readFirstSecretPathOverHttpApi() throws InterruptedException {
+ RestAssured
+ .given()
+ .when()
+ .get("http://" + getHostAndPort() + "/v1/kv/config/testing1")
+ .then()
+ .assertThat()
+ .body(
+ "[0].Value",
+ CoreMatchers.equalTo(Base64.getEncoder().encodeToString("value123".getBytes(StandardCharsets.UTF_8)))
+ );
+ }
+
+ @Test
+ public void writeAndReadMultipleValuesUsingClient() {
+ final ConsulClient consulClient = new ConsulClient(
+ consulContainer.getHost(),
+ consulContainer.getFirstMappedPort()
+ );
+
+ final Map properties = new HashMap<>();
+ properties.put("value", "world");
+ properties.put("other_value", "another world");
+
+ // Write operation
+ properties.forEach((key, value) -> {
+ Response writeResponse = consulClient.setKVValue(key, value);
+ Assertions.assertThat(writeResponse.getValue()).isTrue();
+ });
+
+ // Read operation
+ properties.forEach((key, value) -> {
+ Response readResponse = consulClient.getKVValue(key);
+ Assertions.assertThat(readResponse.getValue().getDecodedValue()).isEqualTo(value);
+ });
+ }
+
+ private String getHostAndPort() {
+ return consulContainer.getHost() + ":" + consulContainer.getMappedPort(8500);
+ }
+}
diff --git a/modules/consul/src/test/java/org/testcontainers/consul/ConsulTestImages.java b/modules/consul/src/test/java/org/testcontainers/consul/ConsulTestImages.java
new file mode 100644
index 00000000000..de2b9702066
--- /dev/null
+++ b/modules/consul/src/test/java/org/testcontainers/consul/ConsulTestImages.java
@@ -0,0 +1,7 @@
+package org.testcontainers.consul;
+
+import org.testcontainers.utility.DockerImageName;
+
+public interface ConsulTestImages {
+ DockerImageName CONSUL_IMAGE = DockerImageName.parse("consul:1.10.12");
+}
diff --git a/modules/consul/src/test/resources/logback-test.xml b/modules/consul/src/test/resources/logback-test.xml
new file mode 100644
index 00000000000..535e406fc13
--- /dev/null
+++ b/modules/consul/src/test/resources/logback-test.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+ %d{HH:mm:ss.SSS} %-5level %logger - %msg%n
+
+
+
+
+
+
+
+
+