Skip to content

Commit 6dd50c0

Browse files
committed
Relocate Vault module into main testcontainers-java repository
1 parent 3e765a9 commit 6dd50c0

File tree

8 files changed

+332
-0
lines changed

8 files changed

+332
-0
lines changed

.github/CODEOWNERS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,4 @@
1111
# Contributed modules can have different reviewers
1212

1313
modules/mssqlserver/ @StefanHufschmidt @rnorth @bsideup @kiview
14+
modules/vault/ @mikeoswald @rnorth @bsideup @kiview

modules/vault/AUTHORS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Michael Oswald <michael.oswald@capitalone.com>

modules/vault/LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
The MIT License (MIT)
2+
3+
Copyright (c) 2017 Capital One Services, LLC and other authors
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

modules/vault/README.md

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# TestContainers Vault Module
2+
3+
Testcontainers module for [Vault](https://github.com/hashicorp/vault). Vault is a tool for managing secrets. More information on Vault [here](https://www.vaultproject.io/).
4+
5+
## Usage example
6+
7+
Running Vault in your Junit tests is easily done with an @Rule or @ClassRule such as the following:
8+
9+
```java
10+
public class SomeTest {
11+
12+
@ClassRule
13+
public static VaultContainer vaultContainer = new VaultContainer<>()
14+
.withVaultToken("my-root-token")
15+
.withVaultPort(8200)
16+
.withSecretInVault("secret/testing", "top_secret=password1","db_password=dbpassword1");
17+
18+
@Test
19+
public void someTestMethod() {
20+
//interact with Vault via the container's host, port and Vault token.
21+
22+
//There are many integration clients for Vault so let's just define a general one here:
23+
VaultClient client = new VaultClient(
24+
vaultContainer.getContainerIpAddress(),
25+
vaultContainer.getMappedPort(8200),
26+
"my-root-token");
27+
28+
List<String> secrets = client.readSecret("secret/testing");
29+
30+
}
31+
```
32+
33+
## Why Vault in Junit tests?
34+
35+
With the increasing popularity of Vault and secret management, applications are now needing to source secrets from Vault.
36+
This can prove challenging in the development phase without a running Vault instance readily on hand. This library
37+
aims to solve your apps integration testing with Vault. You can also use it to
38+
test how your application behaves with Vault by writing different test scenarios in Junit.
39+
40+
## Dependency information
41+
42+
### Maven
43+
44+
```
45+
<dependency>
46+
<groupId>org.testcontainers</groupId>
47+
<artifactId>vault</artifactId>
48+
<version>1.4.3</version>
49+
</dependency>
50+
```
51+
52+
### Gradle
53+
54+
```
55+
compile group: 'org.testcontainers', name: 'vault', version: '1.4.3'
56+
```
57+
58+
## License
59+
60+
See [LICENSE](LICENSE).
61+
62+
## Copyright
63+
64+
Copyright (c) 2017 Capital One Services, LLC and other authors.
65+
66+
See [AUTHORS](AUTHORS) for contributors.

modules/vault/build.gradle

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
description = "Testcontainers :: Vault"
2+
3+
dependencies {
4+
compile project(':testcontainers')
5+
6+
testCompile 'io.rest-assured:rest-assured:3.0.0'
7+
}
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
package org.testcontainers.vault;
2+
3+
import com.github.dockerjava.api.command.InspectContainerResponse;
4+
import org.testcontainers.containers.GenericContainer;
5+
import org.testcontainers.containers.traits.LinkableContainer;
6+
7+
import java.io.IOException;
8+
import java.util.ArrayList;
9+
import java.util.Arrays;
10+
import java.util.HashMap;
11+
import java.util.List;
12+
import java.util.Map;
13+
14+
import static com.github.dockerjava.api.model.Capability.IPC_LOCK;
15+
16+
17+
/**
18+
* GenericContainer subclass for Vault specific configuration and features. The main feature is the
19+
* withSecretInVault method, where users can specify which secrets to be pre-loaded into Vault for
20+
* their specific test scenario.
21+
*
22+
* Other helpful features include the withVaultPort, and withVaultToken methods for convenience.
23+
*/
24+
public class VaultContainer<SELF extends VaultContainer<SELF>> extends GenericContainer<SELF>
25+
implements LinkableContainer {
26+
27+
private static final String VAULT_PORT = "8200";
28+
29+
private boolean vaultPortRequested = false;
30+
31+
private Map<String, List<String>> secretsMap = new HashMap<>();
32+
33+
public VaultContainer() {
34+
this("vault:0.7.0");
35+
}
36+
37+
public VaultContainer(String dockerImageName) {
38+
super(dockerImageName);
39+
}
40+
41+
@Override
42+
protected void configure() {
43+
setStartupAttempts(3);
44+
withCreateContainerCmdModifier(cmd -> cmd.withCapAdd(IPC_LOCK));
45+
if(!isVaultPortRequested()){
46+
withEnv("VAULT_ADDR", "http://0.0.0.0:" + VAULT_PORT);
47+
}
48+
}
49+
50+
@Override
51+
protected void containerIsStarted(InspectContainerResponse containerInfo) {
52+
addSecrets();
53+
}
54+
55+
private void addSecrets() {
56+
if(!secretsMap.isEmpty()){
57+
try {
58+
this.execInContainer(buildExecCommand(secretsMap)).getStdout().contains("Success");
59+
}
60+
catch (IOException | InterruptedException e) {
61+
logger().error("Failed to add these secrets {} into Vault via exec command. Exception message: {}", secretsMap, e.getMessage());
62+
}
63+
}
64+
}
65+
66+
private String[] buildExecCommand(Map<String, List<String>> map) {
67+
StringBuilder stringBuilder = new StringBuilder();
68+
map.forEach((path, secrets) -> {
69+
stringBuilder.append(" && vault write " + path);
70+
secrets.forEach(item -> stringBuilder.append(" " + item));
71+
});
72+
return new String[] { "/bin/sh", "-c", stringBuilder.toString().substring(4)};
73+
}
74+
75+
/**
76+
* Sets the Vault root token for the container so application tests can source secrets using the token
77+
*
78+
* @param token the root token value to set for Vault.
79+
* @return this
80+
*/
81+
public SELF withVaultToken(String token) {
82+
withEnv("VAULT_DEV_ROOT_TOKEN_ID", token);
83+
withEnv("VAULT_TOKEN", token);
84+
return self();
85+
}
86+
87+
/**
88+
* Sets the Vault port in the container as well as the port bindings for the host to reach the container over HTTP.
89+
*
90+
* @param port the port number you want to have the Vault container listen on for tests.
91+
* @return this
92+
*/
93+
public SELF withVaultPort(int port){
94+
setVaultPortRequested(true);
95+
String vaultPort = String.valueOf(port);
96+
withEnv("VAULT_ADDR", "http://0.0.0.0:" + VAULT_PORT);
97+
setPortBindings(Arrays.asList(vaultPort + ":" + VAULT_PORT));
98+
return self();
99+
}
100+
101+
/**
102+
* Pre-loads secrets into Vault container. User may specify one or more secrets and all will be added to each path
103+
* that is specified. Thus this can be called more than once for multiple paths to be added to Vault.
104+
*
105+
* The secrets are added to vault directly after the container is up via the
106+
* {@link #addSecrets() addSecrets}, called from {@link #containerIsStarted(InspectContainerResponse) containerIsStarted}
107+
*
108+
* @param path specific Vault path to store specified secrets
109+
* @param firstSecret first secret to add to specifed path
110+
* @param remainingSecrets var args list of secrets to add to specified path
111+
* @return this
112+
*/
113+
public SELF withSecretInVault(String path, String firstSecret, String... remainingSecrets) {
114+
List<String> list = new ArrayList<>();
115+
list.add(firstSecret);
116+
for(String secret : remainingSecrets) {
117+
list.add(secret);
118+
}
119+
if (secretsMap.containsKey(path)) {
120+
list.addAll(list);
121+
}
122+
secretsMap.putIfAbsent(path,list);
123+
return self();
124+
}
125+
126+
private void setVaultPortRequested(boolean vaultPortRequested) {
127+
this.vaultPortRequested = vaultPortRequested;
128+
}
129+
130+
private boolean isVaultPortRequested() {
131+
return vaultPortRequested;
132+
}
133+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package org.testcontainers.vault;
2+
3+
import org.junit.ClassRule;
4+
import org.junit.Test;
5+
import org.testcontainers.containers.GenericContainer;
6+
import org.testcontainers.containers.wait.Wait;
7+
8+
import java.io.IOException;
9+
10+
import static io.restassured.RestAssured.given;
11+
import static org.hamcrest.CoreMatchers.containsString;
12+
import static org.hamcrest.CoreMatchers.equalTo;
13+
import static org.junit.Assert.assertThat;
14+
15+
/**
16+
* This test shows the pattern to use the VaultContainer @ClassRule for a junit test. It also has tests that ensure
17+
* the secrets were added correctly by reading from Vault with the CLI and over HTTP.
18+
*/
19+
public class VaultContainerTest {
20+
21+
private static final int VAULT_PORT = 8201; //using non-default port to show other ports can be passed besides 8200
22+
23+
private static final String VAULT_TOKEN = "my-root-token";
24+
25+
@ClassRule
26+
public static VaultContainer vaultContainer = new VaultContainer<>()
27+
.withVaultToken(VAULT_TOKEN)
28+
.withVaultPort(VAULT_PORT)
29+
.withSecretInVault("secret/testing1", "top_secret=password123")
30+
.withSecretInVault("secret/testing2", "secret_one=password1",
31+
"secret_two=password2", "secret_three=password3", "secret_three=password3",
32+
"secret_four=password4")
33+
.waitingFor(Wait.forHttp("/v1/secret/testing1").forStatusCode(400));
34+
35+
@Test
36+
public void readFirstSecretPathWithCli() throws IOException, InterruptedException {
37+
GenericContainer.ExecResult result = vaultContainer.execInContainer("vault",
38+
"read", "-field=top_secret", "secret/testing1");
39+
assertThat(result.getStdout(), containsString("password123"));
40+
}
41+
42+
@Test
43+
public void readSecondSecretPathWithCli() throws IOException, InterruptedException {
44+
GenericContainer.ExecResult result = vaultContainer.execInContainer("vault",
45+
"read", "secret/testing2");
46+
String output = result.getStdout();
47+
assertThat(output, containsString("password1"));
48+
assertThat(output, containsString("password2"));
49+
assertThat(output, containsString("password3"));
50+
assertThat(output, containsString("password4"));
51+
}
52+
53+
@Test
54+
public void readFirstSecretPathOverHttpApi() throws InterruptedException {
55+
given().
56+
header("X-Vault-Token", VAULT_TOKEN).
57+
when().
58+
get("http://"+getHostAndPort()+"/v1/secret/testing1").
59+
then().
60+
assertThat().body("data.top_secret", equalTo("password123"));
61+
}
62+
63+
@Test
64+
public void readSecondecretPathOverHttpApi() throws InterruptedException {
65+
given().
66+
header("X-Vault-Token", VAULT_TOKEN).
67+
when().
68+
get("http://"+getHostAndPort()+"/v1/secret/testing2").
69+
then().
70+
assertThat().body("data.secret_one", containsString("password1")).
71+
assertThat().body("data.secret_two", containsString("password2")).
72+
assertThat().body("data.secret_three", containsString("password3")).
73+
assertThat().body("data.secret_four", containsString("password4"));
74+
}
75+
76+
private String getHostAndPort(){
77+
return vaultContainer.getContainerIpAddress()+":"+vaultContainer.getMappedPort(8200);
78+
}
79+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<configuration>
2+
3+
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
4+
<!-- encoders are assigned the type
5+
ch.qos.logback.classic.encoder.PatternLayoutEncoder by default -->
6+
<encoder>
7+
<pattern>%d{HH:mm:ss.SSS} %-5level %logger - %msg%n</pattern>
8+
</encoder>
9+
</appender>
10+
11+
<root level="debug">
12+
<appender-ref ref="STDOUT"/>
13+
</root>
14+
15+
<logger name="org.apache.http" level="WARN"/>
16+
<logger name="com.github.dockerjava" level="WARN"/>
17+
<logger name="org.zeroturnaround.exec" level="WARN"/>
18+
<logger name="com.zaxxer.hikari" level="INFO"/>
19+
<logger name="org.rnorth.tcpunixsocketproxy" level="INFO" />
20+
<logger name="io.netty" level="WARN" />
21+
<logger name="org.mongodb" level="INFO" />
22+
<logger name="org.testcontainers.shaded" level="WARN"/>
23+
<logger name="com.zaxxer.hikari" level="INFO"/>
24+
</configuration>

0 commit comments

Comments
 (0)