Skip to content

Commit e72688b

Browse files
authored
add support for specifying bind mounts and init scripts via @LocalstackDockerProperties (#46)
1 parent be412ee commit e72688b

13 files changed

+217
-33
lines changed

README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,10 @@ You can configure the Docker behaviour using the `@LocalstackDockerProperties` a
7676
| `portEdge` | Port number for the edge service, the main entry point for all API invocations | String | `4566` |
7777
| `portElasticSearch` | Port number for the elasticsearch service | String | `4571` |
7878
| `hostNameResolver` | Used for determining the host name of the machine running the docker containers so that the containers can be addressed. | IHostNameResolver | `localhost` |
79-
| `environmentVariableProvider` | Used for injecting environment variables into the container. | IEnvironmentVariableProvider | Empty Map |
80-
| `useSingleDockerContainer` | Whether a singleton container should be used by all test classes. | boolean | `false` |
79+
| `environmentVariableProvider` | Used for injecting environment variables into the container. | IEnvironmentVariableProvider | Empty Map |
80+
| `bindMountProvider | Used bind mounting files and directories into the container, useful to run init scripts before using the container. | IBindMountProvider | Empty Map |
81+
| initializationToken | Give a regex that will be searched in the logstream of the container, start is complete only when the token is found. Use with bindMountProvider to execute init scripts. | String | Empty String |
82+
| `useSingleDockerContainer` | Whether a singleton container should be used by all test classes. | boolean | `false` |
8183

8284
For more details, please refer to the README of the main LocalStack repo: https://github.com/localstack/localstack
8385

src/main/java/cloud/localstack/Localstack.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ public class Localstack {
2020

2121
public static final String ENV_CONFIG_USE_SSL = "USE_SSL";
2222
public static final String ENV_CONFIG_EDGE_PORT = "EDGE_PORT";
23+
public static final String INIT_SCRIPTS_PATH = "/docker-entrypoint-initaws.d";
24+
public static final String TMP_PATH = "/tmp/localstack";
2325

2426
private static final Logger LOG = Logger.getLogger(Localstack.class.getName());
2527

@@ -71,12 +73,18 @@ public void startup(LocalstackDockerConfiguration dockerConfiguration) {
7173
dockerConfiguration.getPortEdge(),
7274
dockerConfiguration.getPortElasticSearch(),
7375
dockerConfiguration.getEnvironmentVariables(),
74-
dockerConfiguration.getPortMappings()
76+
dockerConfiguration.getPortMappings(),
77+
dockerConfiguration.getBindMounts()
7578
);
7679
loadServiceToPortMap();
7780

7881
LOG.info("Waiting for LocalStack container to be ready...");
7982
localStackContainer.waitForLogToken(READY_TOKEN);
83+
if (dockerConfiguration.getInitializationToken() != null) {
84+
LOG.info("Waiting for LocalStack container to emit your initialization token '"
85+
+ dockerConfiguration.getInitializationToken().toString() + "'...");
86+
localStackContainer.waitForLogToken(dockerConfiguration.getInitializationToken());
87+
}
8088
} catch (Exception t) {
8189
if (t.toString().contains("port is already allocated") && dockerConfiguration.isIgnoreDockerRunErrors()) {
8290
LOG.info("Ignoring port conflict when starting Docker container, due to ignoreDockerRunErrors=true");

src/main/java/cloud/localstack/docker/Container.java

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,12 +51,17 @@ public class Container {
5151
* @param imageName the name of the image defaults to {@value LOCALSTACK_NAME} if null
5252
* @param imageTag the tag of the image to pull, defaults to {@value LOCALSTACK_TAG} if null
5353
* @param environmentVariables map of environment variables to be passed to the docker container
54+
* @param portMappings
55+
* @param bindMounts Docker host to container volume mapping like /host/dir:/container/dir, be aware that the host
56+
* directory must be an absolute path
5457
*/
5558
public static Container createLocalstackContainer(
56-
String externalHostName, boolean pullNewImage, boolean randomizePorts, String imageName, String imageTag, String portEdge,
57-
String portElasticSearch, Map<String, String> environmentVariables, Map<Integer, Integer> portMappings) {
59+
String externalHostName, boolean pullNewImage, boolean randomizePorts, String imageName, String imageTag, String portEdge,
60+
String portElasticSearch, Map<String, String> environmentVariables, Map<Integer, Integer> portMappings,
61+
Map<String, String> bindMounts) {
5862

5963
environmentVariables = environmentVariables == null ? Collections.emptyMap() : environmentVariables;
64+
bindMounts = bindMounts == null ? Collections.emptyMap() : bindMounts;
6065
portMappings = portMappings == null ? Collections.emptyMap() : portMappings;
6166

6267
String imageNameOrDefault = (imageName == null ? LOCALSTACK_NAME : imageName);
@@ -78,7 +83,9 @@ public static Container createLocalstackContainer(
7883
.withEnvironmentVariable(LOCALSTACK_EXTERNAL_HOSTNAME, externalHostName)
7984
.withEnvironmentVariable(ENV_DEBUG, ENV_DEBUG_DEFAULT)
8085
.withEnvironmentVariable(ENV_USE_SSL, Localstack.INSTANCE.useSSL() ? "1" : "0")
81-
.withEnvironmentVariables(environmentVariables);
86+
.withEnvironmentVariables(environmentVariables)
87+
.withBindMountedVolumes(bindMounts);
88+
8289
for (Integer port : portMappings.keySet()) {
8390
runCommand = runCommand.withExposedPorts("" + port, false);
8491
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package cloud.localstack.docker.annotation;
2+
3+
import java.util.Collections;
4+
import java.util.HashMap;
5+
import java.util.Map;
6+
import java.util.function.Supplier;
7+
8+
9+
public interface IBindMountProvider extends Supplier<Map<String, String>> {
10+
11+
class EmptyBindMountProvider implements IBindMountProvider {
12+
13+
@Override
14+
public Map<String, String> get() {
15+
return Collections.emptyMap();
16+
}
17+
}
18+
19+
abstract class BaseBindMountProvider implements IBindMountProvider {
20+
21+
private Map<String, String> mounts = new HashMap<>();
22+
23+
protected BaseBindMountProvider() {
24+
initValues(mounts);
25+
}
26+
27+
protected abstract void initValues(Map<String, String> mounts);
28+
29+
@Override
30+
public final Map<String, String> get() {
31+
return mounts;
32+
}
33+
}
34+
}

src/main/java/cloud/localstack/docker/annotation/LocalstackDockerAnnotationProcessor.java

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import java.util.HashMap;
66
import java.util.Map;
77
import java.util.logging.Logger;
8+
import java.util.regex.Pattern;
89
import java.util.stream.Stream;
910

1011
/**
@@ -21,7 +22,7 @@ public class LocalstackDockerAnnotationProcessor {
2122
public LocalstackDockerConfiguration process(final Class<?> klass) {
2223
return Stream.of(klass.getAnnotations())
2324
.filter(annotation -> annotation instanceof LocalstackDockerProperties)
24-
.map(i -> (LocalstackDockerProperties) i)
25+
.map(LocalstackDockerProperties.class::cast)
2526
.map(this::processDockerPropertiesAnnotation)
2627
.findFirst()
2728
.orElse(LocalstackDockerConfiguration.DEFAULT);
@@ -30,6 +31,8 @@ public LocalstackDockerConfiguration process(final Class<?> klass) {
3031
private LocalstackDockerConfiguration processDockerPropertiesAnnotation(LocalstackDockerProperties properties) {
3132
return LocalstackDockerConfiguration.builder()
3233
.environmentVariables(this.getEnvironments(properties))
34+
.bindMounts(this.getBindMounts(properties))
35+
.initializationToken(StringUtils.isEmpty(properties.initializationToken()) ? null : Pattern.compile(properties.initializationToken()))
3336
.externalHostName(this.getExternalHostName(properties))
3437
.portMappings(this.getCustomPortMappings(properties))
3538
.pullNewImage(properties.pullNewImage())
@@ -75,6 +78,15 @@ private Map<String, String> getEnvironments(final LocalstackDockerProperties pro
7578
return environmentVariables;
7679
}
7780

81+
private Map<String, String> getBindMounts(final LocalstackDockerProperties properties) {
82+
try {
83+
IBindMountProvider environmentProvider = properties.bindMountProvider().newInstance();
84+
return new HashMap<>(environmentProvider.get());
85+
} catch (InstantiationException | IllegalAccessException ex) {
86+
throw new IllegalStateException("Unable to get bind mounts", ex);
87+
}
88+
}
89+
7890
private String getExternalHostName(final LocalstackDockerProperties properties) {
7991
try {
8092
IHostNameResolver hostNameResolver = properties.hostNameResolver().newInstance();

src/main/java/cloud/localstack/docker/annotation/LocalstackDockerConfiguration.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55

66
import java.util.Collections;
77
import java.util.Map;
8+
import java.util.regex.Pattern;
9+
810

911
/**
1012
* Bean to specify the docker configuration.
@@ -41,6 +43,11 @@ public class LocalstackDockerConfiguration {
4143
@Builder.Default
4244
private final Map<Integer, Integer> portMappings = Collections.emptyMap();
4345

46+
@Builder.Default
47+
private final Map<String, String> bindMounts = Collections.emptyMap();
48+
49+
private final Pattern initializationToken;
50+
4451
@Builder.Default
4552
private final boolean useSingleDockerContainer = false;
4653

src/main/java/cloud/localstack/docker/annotation/LocalstackDockerProperties.java

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
package cloud.localstack.docker.annotation;
22

33
import java.lang.annotation.ElementType;
4+
import java.lang.annotation.Inherited;
45
import java.lang.annotation.Retention;
56
import java.lang.annotation.RetentionPolicy;
67
import java.lang.annotation.Target;
7-
import java.lang.annotation.Inherited;
8+
89

910
/**
1011
* An annotation to provide parameters to the main (Docker-based) LocalstackTestRunner
@@ -26,6 +27,18 @@
2627
*/
2728
Class<? extends IEnvironmentVariableProvider> environmentVariableProvider() default DefaultEnvironmentVariableProvider.class;
2829

30+
/**
31+
* Used for injecting directories or files that are bind mounted into the docker container. Implement a class that provides a map
32+
* of host to container directory or file mappings, with host directories/files as keys and container directories/files as values.
33+
* Remember that Docker needs absolute paths for host directories and files.
34+
*/
35+
Class<? extends IBindMountProvider> bindMountProvider() default IBindMountProvider.EmptyBindMountProvider.class;
36+
37+
/**
38+
* A Java Regex that is used to wait for the execution of custom init scripts
39+
*/
40+
String initializationToken() default "";
41+
2942
/**
3043
* Determines if a new image is pulled from the docker repo before the tests are run.
3144
*/
@@ -43,6 +56,7 @@
4356
/**
4457
* Determines which services should be run when the localstack starts. When empty, all the services available get
4558
* up and running.
59+
* @see cloud.localstack.ServiceName
4660
*/
4761
String[] services() default {};
4862

src/main/java/cloud/localstack/docker/command/RunCommand.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,11 @@ public RunCommand withExposedPorts(String portsToExpose, boolean randomize) {
4444
return this;
4545
}
4646

47+
public RunCommand withBindMountedVolumes(Map<String, String> hostToContainerMappings) {
48+
hostToContainerMappings.forEach((host, container) -> addOptions("-v", String.format("%s:%s", host, container)));
49+
return this;
50+
}
51+
4752
public RunCommand withEnvironmentVariable(String name, String value) {
4853
addEnvOption(name, value);
4954
return this;

src/test/java/cloud/localstack/deprecated/PortBindingTest.java

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,16 @@
66
import cloud.localstack.docker.Container;
77
import cloud.localstack.docker.LocalstackDockerExtension;
88
import cloud.localstack.docker.annotation.LocalstackDockerProperties;
9-
9+
import com.amazonaws.services.sqs.AmazonSQS;
10+
import org.junit.Test;
1011
import org.junit.jupiter.api.extension.ExtendWith;
1112
import org.junit.runner.RunWith;
12-
import org.junit.Test;
13-
14-
import static org.junit.Assert.*;
15-
16-
import com.amazonaws.services.sqs.AmazonSQS;
1713

18-
import static cloud.localstack.docker.ContainerTest.*;
14+
import static cloud.localstack.docker.ContainerTest.EXTERNAL_HOST_NAME;
15+
import static cloud.localstack.docker.ContainerTest.pullNewImage;
16+
import static org.junit.Assert.assertEquals;
17+
import static org.junit.Assert.assertNotEquals;
18+
import static org.junit.Assert.assertTrue;
1919

2020
@RunWith(LocalstackTestRunner.class)
2121
@ExtendWith(LocalstackDockerExtension.class)
@@ -35,7 +35,7 @@ public void testAccessPredefinedPort() {
3535
@Test
3636
public void createLocalstackContainerWithRandomPorts() throws Exception {
3737
Container container = Container.createLocalstackContainer(
38-
EXTERNAL_HOST_NAME, pullNewImage, true, null, null, null, null, null, null);
38+
EXTERNAL_HOST_NAME, pullNewImage, true, null, null, null, null, null, null, null);
3939

4040
try {
4141
container.waitForAllPorts(EXTERNAL_HOST_NAME);
@@ -53,7 +53,7 @@ public void createLocalstackContainerWithRandomPorts() throws Exception {
5353
@Test
5454
public void createLocalstackContainerWithStaticPorts() throws Exception {
5555
Container container = Container.createLocalstackContainer(
56-
EXTERNAL_HOST_NAME, pullNewImage, false, null, null, null, null, null, null);
56+
EXTERNAL_HOST_NAME, pullNewImage, false, null, null, null, null, null, null, null);
5757

5858
try {
5959
container.waitForAllPorts(EXTERNAL_HOST_NAME);

src/test/java/cloud/localstack/docker/ContainerTest.java

Lines changed: 42 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
11
package cloud.localstack.docker;
22

33
import cloud.localstack.Localstack;
4-
import cloud.localstack.docker.annotation.LocalstackDockerProperties;
4+
import org.apache.commons.io.FileUtils;
55
import org.junit.Test;
6-
import org.junit.jupiter.api.extension.ExtendWith;
76

7+
import java.io.File;
8+
import java.io.IOException;
9+
import java.io.UncheckedIOException;
10+
import java.nio.charset.StandardCharsets;
811
import java.util.ArrayList;
912
import java.util.Arrays;
13+
import java.util.Collections;
1014
import java.util.HashMap;
15+
import java.util.regex.Pattern;
1116

1217
import static org.junit.Assert.assertEquals;
1318
import static org.junit.Assert.assertNotEquals;
@@ -20,13 +25,14 @@ public class ContainerTest {
2025

2126
public static final boolean pullNewImage = false;
2227

28+
@org.junit.jupiter.api.Test
2329
@Test
2430
public void createLocalstackContainer() throws Exception {
2531

2632
HashMap<String, String> environmentVariables = new HashMap<>();
2733
environmentVariables.put(MY_PROPERTY, MY_VALUE);
2834
Container localStackContainer = Container.createLocalstackContainer(
29-
EXTERNAL_HOST_NAME, pullNewImage, false, null, null, null, null, environmentVariables, null);
35+
EXTERNAL_HOST_NAME, pullNewImage, false, null, null, null, null, environmentVariables, null, null);
3036

3137
try {
3238
localStackContainer.waitForAllPorts(EXTERNAL_HOST_NAME);
@@ -48,12 +54,13 @@ public void createLocalstackContainer() throws Exception {
4854
}
4955
}
5056

57+
@org.junit.jupiter.api.Test
5158
@Test
5259
public void createLocalstackContainerWithFullImage() {
5360

5461
String customImageName = "localstack/localstack-full";
5562
Container localStackContainer = Container.createLocalstackContainer(
56-
EXTERNAL_HOST_NAME, pullNewImage, false, customImageName, null, null, null, null, null);
63+
EXTERNAL_HOST_NAME, pullNewImage, false, customImageName, null, null, null, null, null, null);
5764

5865
try {
5966
localStackContainer.waitForAllPorts(EXTERNAL_HOST_NAME);
@@ -68,23 +75,41 @@ public void createLocalstackContainerWithFullImage() {
6875
}
6976
}
7077

71-
@ExtendWith(LocalstackDockerExtension.class)
72-
@LocalstackDockerProperties(imageName = "localstack/localstack-full")
73-
public static class ContainerTest1 {
74-
@org.junit.jupiter.api.Test
75-
public void imageName() {
76-
String imageName = new DockerExe()
77-
.execute(Arrays.asList("container", "inspect",
78-
Localstack.INSTANCE.getLocalStackContainer().getContainerId(),
79-
"--format", "{{.Config.Image}}"));
80-
assertEquals("localstack/localstack-full", imageName);
78+
@org.junit.jupiter.api.Test
79+
@Test
80+
public void createLocalstackContainerWithScriptMounted() {
81+
82+
Container localStackContainer = Container.createLocalstackContainer(
83+
EXTERNAL_HOST_NAME, pullNewImage, false, null, null, null, null, null, null,
84+
Collections.singletonMap(testFile("echo testmarker"), Localstack.INIT_SCRIPTS_PATH + "/test.sh"));
85+
86+
try {
87+
localStackContainer.waitForAllPorts(EXTERNAL_HOST_NAME);
88+
localStackContainer.waitForLogToken(Pattern.compile("testmarker"));
89+
assertEquals("echo testmarker", localStackContainer.executeCommand(Arrays.asList("cat", Localstack.INIT_SCRIPTS_PATH + "/test.sh")));
90+
91+
}
92+
finally {
93+
localStackContainer.stop();
94+
}
95+
}
96+
97+
static String testFile(String content) {
98+
try {
99+
File testFile = File.createTempFile("localstack", ".sh");
100+
FileUtils.writeStringToFile(testFile, content, StandardCharsets.UTF_8);
101+
testFile.deleteOnExit();
102+
return testFile.getAbsolutePath();
103+
} catch (IOException e) {
104+
throw new UncheckedIOException(e);
81105
}
82106
}
83107

108+
@org.junit.jupiter.api.Test
84109
@Test
85110
public void createLocalstackContainerWithCustomPorts() throws Exception {
86111
Container localStackContainer = Container.createLocalstackContainer(
87-
EXTERNAL_HOST_NAME, pullNewImage, false, null, null, "45660", "45710", null, null);
112+
EXTERNAL_HOST_NAME, pullNewImage, false, null, null, "45660", "45710", null, null, null);
88113

89114
try {
90115
localStackContainer.waitForAllPorts(EXTERNAL_HOST_NAME);
@@ -97,10 +122,11 @@ public void createLocalstackContainerWithCustomPorts() throws Exception {
97122
}
98123
}
99124

125+
@org.junit.jupiter.api.Test
100126
@Test
101127
public void createLocalstackContainerWithRandomPorts() throws Exception {
102128
Container localStackContainer = Container.createLocalstackContainer(
103-
EXTERNAL_HOST_NAME, pullNewImage, false, null, null, ":4566", ":4571", null, null);
129+
EXTERNAL_HOST_NAME, pullNewImage, false, null, null, ":4566", ":4571", null, null, null);
104130

105131
try {
106132
localStackContainer.waitForAllPorts(EXTERNAL_HOST_NAME);

0 commit comments

Comments
 (0)