Skip to content

Commit d8c5f09

Browse files
committed
Added config property to run the Container with a fixed name. Added README and enhanced other documentation.
1 parent 562465e commit d8c5f09

File tree

7 files changed

+232
-36
lines changed

7 files changed

+232
-36
lines changed

README.md

Lines changed: 171 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,171 @@
1-
# Spring Local Elasticsearch
1+
# Spring Local Elasticsearch
2+
3+
## Description
4+
Decorates the Testcontainers Elasticsearch module with enhanced configuration for Spring Boot Applications.
5+
6+
Requires minimal configuration using Spring conventions, but a variety of optional properties are provided to override default behavior by profile, supporting local development in addition to test execution.
7+
8+
## Features
9+
- Configure whether the Testcontainers Elasticsearch module is active or not. Allows you to control its activation by profile.
10+
- Configure the Docker Image to use with the Testcontainers Elasticsearch module. Allows you to match the Elasticsearch version used in local and test environments with the version in production.
11+
- Configure the Testcontainers Elasticsearch module to run with a fixed container name. Useful for local development so that developers can easily find the running container.
12+
- Configure the Testcontainers Elasticsearch module to run with a fixed port. Useful for local development so that developers can connect with consistent, predictable configuration.
13+
- Configure whether to follow the Docker Container's log output. Useful for troubleshooting in some cases.
14+
15+
## Rationale
16+
When developing an Application that uses Elasticsearch in production, an instance of Elasticsearch is needed for testing and local development, and it should be configurable in a way where it will spin up and tear down when the Application starts up and shuts down.
17+
18+
While Testcontainers is designed to provide that spin up/tear down capability to support Integration Tests, this project is designed to support running the Application locally as well, reducing the overhead that would come with maintaining Testcontainers in addition to some other solution that fundamentally does the same thing. There is no need to maintain a local Elasticsearch server nor additional Docker configuration inside or outside the project.
19+
20+
## Requirements
21+
### Java 17
22+
https://adoptium.net/temurin/releases/?version=17
23+
24+
### Docker
25+
https://www.docker.com/products/docker-desktop/ <br>
26+
https://rancherdesktop.io/
27+
28+
NOTE: Be sure to allocate at least 8GB of memory, otherwise the Elasticsearch Container will not start properly.
29+
30+
NOTE: Rancher Desktop may not work correctly if Docker Desktop had been previously installed.
31+
32+
## Elasticsearch 8 or higher
33+
Versions less than version 7 are not supported.
34+
35+
## Transitive Dependencies
36+
- Spring Boot Starter Web 3.2.0
37+
- Spring Boot Configuration Processor 3.2.0
38+
- Spring Boot Starter Data Elasticsearch 3.2.0
39+
- Spring Boot Testcontainers 3.2.0
40+
- Testcontainers Elasticsearch 1.19.3
41+
42+
## Usage
43+
### Configuration as a Test Dependency
44+
While it is possible to declare `spring-local-elasticsearch` as a compile dependency, and control its activation with profiles, it is better practice to declare it as a test dependency.
45+
46+
This means, however, that all configuration for `spring-local-elasticsearch` (for both Integration Tests *and* for running the Application locally) can only reside in your project's test source. For Integration Tests this is common practice, but for running the Application locally it may seem unusual or perhaps difficult to do. However, by implementing the approach surfaced in this [article](https://bsideup.github.io/posts/local_development_with_testcontainers/) by Sergei Egorov, configuring a local profile in your project's test source becomes a simple process that will likely become a preferred practice as well.
47+
48+
### Add Spring Local Elasticsearch
49+
Add the `spring-local-elasticsearch` artifact to your project as a test dependency:
50+
```xml
51+
<dependency>
52+
<groupId>io.github.quinnandrews</groupId>
53+
<artifactId>spring-local-elasticsearch</artifactId>
54+
<version>1.0.0</version>
55+
<scope>test</scope>
56+
</dependency>
57+
```
58+
(NOTE: The `spring-local-elasticsearch` artifact is NOT yet available in Maven Central, but is available from GitHub Packages, which requires additional configuration in your pom.xml file.)
59+
60+
### Configure a Local Profile
61+
Create a properties files in your test resources directory to configuration for a `local` profile.
62+
63+
Configure the `local` profile with a fixed container name, so that developers can quickly and consistently identify the running container.
64+
65+
Configure the `local` profile with a fixed port, so that developers can connect with a consistent and predictable port.
66+
67+
Set other configuration properties as desired, or not at all to use default settings.
68+
69+
application-local.properties:
70+
```properties
71+
spring.local.elasticsearch.container.image=docker.elastic.co/elasticsearch/elasticsearch:8.10.2
72+
spring.local.elasticsearch.container.name=local_elasticsearch
73+
spring.local.elasticsearch.container.port=19200
74+
spring.local.elasticsearch.password=flange_local
75+
```
76+
77+
In the example above, in addition to a fixed container name and port, the `local` profile has the following settings:
78+
- The `docker.elastic.co/elasticsearch/elasticsearch:8.10.2` Docker Image is set to use a more recent version of Elasticsearch than the default (`docker.elastic.co/elasticsearch/elasticsearch:8.7.1`) to match production.
79+
- The `flange_local` password is set to avoid using the default, and to isolate the local environment from the test environment.
80+
81+
### Implement a Spring Boot Application Class to Run the Application with the Local Profile
82+
Add a Spring Boot Application Class named `LocalDevApplication` to your project's test source, preferably in the same package as the Spring Boot Application Class in the main source, to mirror the convention of Test Classes residing in the same package as the Classes they test.
83+
84+
Annotate `LocalDevApplication` with `@EnableLocalElasticsearch` and `@Profile("local")`. The `@EnableLocalElasticsearch` activates configuration of the Testcontainers Elasticsearch module while `@Profile("local")` ensures that configuration declared within the `LocalDevApplication` is only scanned and initialized if the `local` profile is active.
85+
86+
Inside the body of the `main` method, instantiate an instance of `SpringApplication` with the Application Class residing in the main source, to ensure that configuration in the main source is scanned. Then activate the `local` profile programmatically by calling `setAdditionalProfiles`. This will allow you to run `LocalDevApplication` in IntelliJ IDEA by simply right-clicking on the Class in the Project Panel and selecting `Run 'LocalDevApplication'` without having to add the `local` profile to the generated Spring Boot Run Configuration.
87+
88+
LocalDevApplication.java:
89+
```java
90+
@EnableLocalElasticsearch
91+
@Profile("local")
92+
@SpringBootApplication
93+
public class LocalDevApplication {
94+
95+
public static void main(final String[] args) {
96+
final var springApplication = new SpringApplication(Application.class);
97+
springApplication.setAdditionalProfiles("local");
98+
springApplication.run(args);
99+
}
100+
}
101+
```
102+
### Configure a Test Profile
103+
104+
Configure the `test` profile to use a random container name and port by leaving their properties undeclared so that default settings will be used. Random names and ports are best practice for Integration Tests, and means that Integration Tests can be executed while the Application is running locally with the `local` profile.
105+
106+
Set other configuration properties as desired, or not at all to use default settings.
107+
108+
application-test.properties:
109+
```properties
110+
spring.local.elasticsearch.container.image=docker.elastic.co/elasticsearch/elasticsearch:8.10.2
111+
spring.local.elasticsearch.password=flange_test
112+
```
113+
In the example above, the `test` profile has the following settings:
114+
- The `docker.elastic.co/elasticsearch/elasticsearch:8.10.2` Docker Image is set to use a more recent version of Elasticsearch than the default (`docker.elastic.co/elasticsearch/elasticsearch:8.7.1`) to match production.
115+
- The `flange_test` password is set to avoid using the default, and to isolate the test environment from the local environment.
116+
-
117+
NOTE: One can, of course, configure the test profile and local profile to use the same passwords, but using distinct passwords is recommended, since the isolation will lower the risk of potential issues.
118+
119+
#### Implement an Integration Test
120+
121+
Add an Integration Test Class. Annotate with `@EnableLocalElasticsearch`, `@ActiveProfiles("test")` and `@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)`. The `@EnableLocalElasticsearch` activates configuration of the Testcontainers Elasticsearch module. The `@ActiveProfiles("test")` will activate the `test` profile when executed. And the `@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)`.
122+
123+
Write a test. The example below uses the RestAssured framework to call a REST endpoint backed by the Elasticsearch Container.
124+
125+
Example:
126+
127+
```java
128+
@EnableLocalElasticsearch
129+
@ActiveProfiles("test")
130+
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
131+
public class GuitarPedalControllerTest {
132+
133+
@LocalServerPort
134+
private Integer port;
135+
136+
@Test
137+
void getAllGuitarPedals() {
138+
given().port(port)
139+
.when().get("/guitar-pedals")
140+
.then().statusCode(HttpStatus.OK.value())
141+
.and().contentType(ContentType.JSON)
142+
.and().body("size()", equalTo(4))
143+
.and().body("guitarPedalName", hasItems(
144+
"Big Muff Fuzz",
145+
"Deco: Tape Saturation and Double Tracker",
146+
"Soft Focus Reverb",
147+
"Sneak Attack: Attack/Decay and Tremolo"))
148+
.and().body("dateSold", hasItems(null, null, null, "2023-03-21"));
149+
}
150+
}
151+
```
152+
NOTE: It is, of course, possible to declare `@EnableLocalElasticsearch` in a Spring Boot Application Class, named `TestApplication`, for example, so that one does not have to add `@EnableLocalElasticsearch` to every test Class, and that may be appropriate in some cases, but in general it is recommended that each test Class controls the declaration of the resources it needs. After all, some test Classes may need both Elasticsearch and Kafka, for instance, while other test Classes may only need one or the other. In such a case, initializing Elasticsearch and Kafka containers for all test Classes would waste resources and prolong the time it takes for test Classes to run.
153+
154+
## Supported Configuration Properties
155+
**spring.local.elasticsearch.engaged**<br/>
156+
Whether the containerized Elasticsearch server should be configured and started when the Application starts. By default, it is set to `true`. To disengage, set to `false`.
157+
158+
**spring.local.elasticsearch.container.image**<br/>
159+
The Docker Image with the chosen version of Elasticsearch (example: `docker.elastic.co/elasticsearch/elasticsearch:8.10.2`). If undefined, a default will be used (`docker.elastic.co/elasticsearch/elasticsearch:8.7.1`).
160+
161+
**spring.local.elasticsearch.container.name**<br/>
162+
The name to use for the Docker Container when started. If undefined, a random name is used. Random names are preferred for Integration Tests, but when running the Application locally, a fixed name is useful, since it allows developers to find the running container with a consistent, predictable name.
163+
164+
**spring.local.elasticsearch.container.port**<br/>
165+
The port on the Docker Container to map with the Elasticsearch port inside the container. If undefined, a random port is used. Random ports are preferred for Integration Tests, but when running the Application locally, a fixed port is useful, since it allows developers to configure any connecting, external tools or apps with a consistent, predictable port.
166+
167+
**spring.local.elasticsearch.container.log.follow**<br/>
168+
Whether the Application should log the output produced by the container's log. By default, container logs are not followed. Set with `true` to see their output.
169+
170+
**spring.local.elasticsearch.password**<br/>
171+
The password the Application will use to connect with Elasticsearch. If undefined, Testcontainers will use its default (`changeme`). NOTE: The corresponding username is not configurable. It will be `elastic` in all cases.

src/main/java/io/github/quinnandrews/spring/local/elasticsearch/config/ElasticsearchContainerConfig.java

Lines changed: 41 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
import org.jetbrains.annotations.NotNull;
1111
import org.slf4j.Logger;
1212
import org.slf4j.LoggerFactory;
13-
import org.springframework.beans.factory.annotation.Autowired;
1413
import org.springframework.beans.factory.annotation.Value;
1514
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
1615
import org.springframework.context.annotation.Bean;
@@ -35,7 +34,7 @@
3534
*
3635
* @author Quinn Andrews
3736
*/
38-
@ConditionalOnProperty(name="spring.local.elasticsearch.enabled",
37+
@ConditionalOnProperty(name="spring.local.elasticsearch.engaged",
3938
havingValue="true",
4039
matchIfMissing = true)
4140
@Configuration
@@ -49,28 +48,40 @@ public class ElasticsearchContainerConfig {
4948
private static final Logger logger = LoggerFactory.getLogger(ElasticsearchContainerConfig.class);
5049

5150
private final String containerImage;
51+
private final String containerName;
5252
private final Integer containerPort;
5353
private final Boolean followContainerLog;
5454
private final String password;
5555

56-
@Autowired
56+
/**
57+
* Constructs an instance of this Configuration Class with the given properties.
58+
*
59+
* @param containerImage The Docker Image to use as the Container (optional).
60+
* @param containerName The name to use for the Docker Container when started.
61+
* @param containerPort The port on the Container that should map to PostgreSQL (optional).
62+
* @param followContainerLog Whether to log the output produced by the Container's logs (optional).
63+
* @param password The password for the Elasticsearch 'elastic' user (optional).
64+
*/
5765
public ElasticsearchContainerConfig(@Value("${spring.local.elasticsearch.container.image:#{null}}")
5866
final String containerImage,
67+
@Value("${spring.local.elasticsearch.container.name:#{null}}")
68+
final String containerName,
5969
@Value("${spring.local.elasticsearch.container.port:#{null}}")
6070
final Integer containerPort,
6171
@Value("${spring.local.elasticsearch.container.log.follow:#{false}}")
6272
final Boolean followContainerLog,
6373
@Value("${spring.local.elasticsearch.password:#{null}}")
6474
final String password) {
6575
this.containerImage = containerImage;
76+
this.containerName = containerName;
6677
this.containerPort = containerPort;
6778
this.followContainerLog = followContainerLog;
6879
this.password = password;
6980
}
7081

7182
/**
72-
* Initializes a Testcontainers Bean that runs ElasticsearchL inside a Docker Container
73-
* with the given configuration.
83+
* Returns a Testcontainers Bean that runs Elasticsearch inside a
84+
* Docker Container with the given configuration.
7485
*
7586
* @return ElasticsearchContainer
7687
*/
@@ -81,15 +92,17 @@ public ElasticsearchContainer elasticsearchContainer() {
8192
.orElse(ELASTICSEARCH_DEFAULT_IMAGE))
8293
);
8394
Optional.ofNullable(containerPort).ifPresent(cp ->
84-
container.withCreateContainerCmdModifier(cmd -> cmd.withHostConfig(
85-
new HostConfig().withPortBindings(
86-
new PortBinding(Ports.Binding.bindPort(cp),
87-
new ExposedPort(ELASTICSEARCH_DEFAULT_PORT)),
88-
new PortBinding(Ports.Binding.empty(),
89-
new ExposedPort(9300))
90-
)
91-
))
92-
);
95+
container.withCreateContainerCmdModifier(cmd -> cmd
96+
.withName(containerName)
97+
.withHostConfig(
98+
new HostConfig().withPortBindings(
99+
new PortBinding(
100+
Ports.Binding.bindPort(cp),
101+
new ExposedPort(ELASTICSEARCH_DEFAULT_PORT)),
102+
new PortBinding(
103+
Ports.Binding.empty(),
104+
new ExposedPort(9300))
105+
))));
93106
Optional.ofNullable(password).ifPresent(container::withPassword);
94107
if (followContainerLog) {
95108
container.withLogConsumer(new Slf4jLogConsumer(logger));
@@ -103,26 +116,28 @@ public ElasticsearchContainer elasticsearchContainer() {
103116
104117
Running ElasticsearchContainer for development and testing.
105118
106-
Built with Docker Image: {0}
107-
Host Address URL: {1}
108-
Username: {2}
109-
Password: {3}
110-
111-
Note: The port referenced in the Host Address URL is a port to
112-
access the container. Inside the container Elasticsearch is on
113-
port 9200 as usual.
114-
119+
Container: {0}
120+
Image: {1}
121+
Port Mapping: {2}:{3}
122+
123+
Elasticsearch Host Address URL: {4}
124+
Username: {5}
125+
Password: {6}
126+
115127
|+|+|+|+|+|+|+|+|+|+|+|+|+|+|+|+|+|+|+|+|+|+|+|+|+|+|+|+|+|+|+|+|+|+|+|+|+|+|+|+|+|+|
116128
*************************************************************************************
117129
""",
130+
container.getContainerName(),
118131
container.getDockerImageName(),
132+
String.valueOf(container.getMappedPort(ELASTICSEARCH_DEFAULT_PORT)),
133+
String.valueOf(ELASTICSEARCH_DEFAULT_PORT),
119134
container.getHttpHostAddress(),
120135
ELASTICSEARCH_DEFAULT_USERNAME,
121136
container.getEnvMap().get(ELASTICSEARCH_PASSWORD_ENV_KEY)));
122137
return container;
123138
}
124139

125-
@ConditionalOnProperty(name="spring.local.elasticsearch.enabled",
140+
@ConditionalOnProperty(name="spring.local.elasticsearch.engaged",
126141
havingValue="true",
127142
matchIfMissing = true)
128143
@Configuration
@@ -135,8 +150,8 @@ public ElasticsearchClientConfig(final ElasticsearchContainer elasticsearchConta
135150
}
136151

137152
/**
138-
* Initializes a Spring Bean that instructs how the Elasticsearch RestClient should
139-
* be configured.
153+
* Returns a Spring Bean instructing how the Elasticsearch RestClient
154+
* should be configured.
140155
*
141156
* @return ClientConfiguration
142157
*/

0 commit comments

Comments
 (0)