Skip to content

Commit

Permalink
Add Consul module (#4683)
Browse files Browse the repository at this point in the history
Co-authored-by: Kevin Wittek <kiview@users.noreply.github.com>
  • Loading branch information
julb and kiview committed Aug 15, 2022
1 parent 54d7f39 commit 625947f
Show file tree
Hide file tree
Showing 12 changed files with 257 additions and 0 deletions.
1 change: 1 addition & 0 deletions .github/ISSUE_TEMPLATE/bug_report.yaml
Expand Up @@ -18,6 +18,7 @@ body:
- Cassandra
- Clickhouse
- CockroachDB
- Consul
- Couchbase
- DB2
- Dynalite
Expand Down
1 change: 1 addition & 0 deletions .github/ISSUE_TEMPLATE/enhancement.yaml
Expand Up @@ -18,6 +18,7 @@ body:
- Cassandra
- Clickhouse
- CockroachDB
- Consul
- Couchbase
- DB2
- Dynalite
Expand Down
1 change: 1 addition & 0 deletions .github/ISSUE_TEMPLATE/feature.yaml
Expand Up @@ -18,6 +18,7 @@ body:
- Cassandra
- Clickhouse
- CockroachDB
- Consul
- Couchbase
- DB2
- Dynalite
Expand Down
5 changes: 5 additions & 0 deletions .github/dependabot.yml
Expand Up @@ -42,6 +42,11 @@ updates:
schedule:
interval: "monthly"
open-pull-requests-limit: 10
- package-ecosystem: "gradle"
directory: "/modules/consul"
schedule:
interval: "monthly"
open-pull-requests-limit: 10
- package-ecosystem: "gradle"
directory: "/modules/couchbase"
schedule:
Expand Down
2 changes: 2 additions & 0 deletions .github/labeler.yml
Expand Up @@ -15,6 +15,8 @@
- modules/clickhouse/*
"modules/cockroachdb":
- modules/cockroachdb/*
"modules/consul":
- modules/consul/*
"modules/couchbase":
- modules/couchbase/*
"modules/db2":
Expand Down
36 changes: 36 additions & 0 deletions docs/modules/consul.md
@@ -0,0 +1,36 @@
# Hashicorp Consul Module

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/).

## Usage example

<!--codeinclude-->
[Running Consul in your Junit tests](../../modules/consul/src/test/java/org/testcontainers/consul/ConsulContainerTest.java)
<!--/codeinclude-->

## Why Consul in Junit tests?

With the increasing popularity of Consul and config externalization, applications are now needing to source properties from Consul.
This can prove challenging in the development phase without a running Consul instance readily on hand. This library
aims to solve your apps integration testing with Consul. You can also use it to
test how your application behaves with Consul by writing different test scenarios in Junit.

## Adding this module to your project dependencies

Add the following dependency to your `pom.xml`/`build.gradle` file:

```groovy tab='Gradle'
testImplementation "org.testcontainers:consul:{{latest_version}}"
```

```xml tab='Maven'
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>consul</artifactId>
<version>{{latest_version}}</version>
<scope>test</scope>
</dependency>
```

See [AUTHORS](https://raw.githubusercontent.com/testcontainers/testcontainers-java/master/modules/consul/AUTHORS) for contributors.

1 change: 1 addition & 0 deletions mkdocs.yml
Expand Up @@ -66,6 +66,7 @@ nav:
- modules/databases/tidb.md
- modules/databases/trino.md
- modules/azure.md
- modules/consul.md
- modules/docker_compose.md
- modules/elasticsearch.md
- modules/gcloud.md
Expand Down
9 changes: 9 additions & 0 deletions modules/consul/build.gradle
@@ -0,0 +1,9 @@
description = "Testcontainers :: Consul"

dependencies {
api project(':testcontainers')

testImplementation 'com.ecwid.consul:consul-api:1.4.5'
testImplementation 'io.rest-assured:rest-assured:4.4.0'
testImplementation 'org.assertj:assertj-core:3.21.0'
}
@@ -0,0 +1,100 @@
package org.testcontainers.consul;

import com.github.dockerjava.api.command.InspectContainerResponse;
import com.github.dockerjava.api.model.Capability;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.wait.strategy.Wait;
import org.testcontainers.utility.DockerImageName;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

/**
* Testcontainers implementation for Consul.
*/
public class ConsulContainer extends GenericContainer<ConsulContainer> {

private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("consul");

private static final int CONSUL_HTTP_PORT = 8500;

private static final int CONSUL_GRPC_PORT = 8502;

private List<String> 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:
* <pre>
* .withConsulCommand("secrets enable pki")
* .withConsulCommand("secrets enable transit")
* </pre>
* or register specific K/V like:
* <pre>
* .withConsulCommand("kv put config/testing1 value123")
* </pre>
* @param commands The commands to send to the consul cli
* @return this
*/
public ConsulContainer withConsulCommand(String... commands) {
initCommands.addAll(Arrays.asList(commands));
return self();
}
}
@@ -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<String, String> properties = new HashMap<>();
properties.put("value", "world");
properties.put("other_value", "another world");

// Write operation
properties.forEach((key, value) -> {
Response<Boolean> writeResponse = consulClient.setKVValue(key, value);
Assertions.assertThat(writeResponse.getValue()).isTrue();
});

// Read operation
properties.forEach((key, value) -> {
Response<GetValue> readResponse = consulClient.getKVValue(key);
Assertions.assertThat(readResponse.getValue().getDecodedValue()).isEqualTo(value);
});
}

private String getHostAndPort() {
return consulContainer.getHost() + ":" + consulContainer.getMappedPort(8500);
}
}
@@ -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");
}
16 changes: 16 additions & 0 deletions modules/consul/src/test/resources/logback-test.xml
@@ -0,0 +1,16 @@
<configuration>

<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<!-- encoders are assigned the type
ch.qos.logback.classic.encoder.PatternLayoutEncoder by default -->
<encoder>
<pattern>%d{HH:mm:ss.SSS} %-5level %logger - %msg%n</pattern>
</encoder>
</appender>

<root level="INFO">
<appender-ref ref="STDOUT"/>
</root>

<logger name="org.testcontainers" level="DEBUG"/>
</configuration>

0 comments on commit 625947f

Please sign in to comment.