Skip to content

Commit 2ac675e

Browse files
committed
Auto-configure the Postgres application_name when using Docker Compose
1 parent 04c8344 commit 2ac675e

File tree

10 files changed

+212
-41
lines changed

10 files changed

+212
-41
lines changed

spring-boot-project/spring-boot-docker-compose/src/dockerTest/java/org/springframework/boot/docker/compose/service/connection/postgres/PostgresJdbcDockerComposeConnectionDetailsFactoryIntegrationTests.java

+32
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,38 @@ void runWithBitnamiImageCreatesConnectionDetails(JdbcConnectionDetails connectio
5757
assertConnectionDetails(connectionDetails);
5858
}
5959

60+
@DockerComposeTest(composeFile = "postgres-compose-application-name-label.yaml", image = TestImage.POSTGRESQL,
61+
properties = "spring.application.name=my-app")
62+
void runCreatesConnectionDetailsApplicationNameFromComposeFileTakesPrecedenceOverSpringApplicationName(
63+
JdbcConnectionDetails connectionDetails) throws ClassNotFoundException {
64+
assertThat(connectionDetails.getUsername()).isEqualTo("myuser");
65+
assertThat(connectionDetails.getPassword()).isEqualTo("secret");
66+
assertThat(connectionDetails.getJdbcUrl()).startsWith("jdbc:postgresql://")
67+
.endsWith("?ApplicationName=spring-boot");
68+
checkDatabaseAccess(connectionDetails);
69+
}
70+
71+
@DockerComposeTest(composeFile = "postgres-compose-connect-timeout-label.yaml", image = TestImage.POSTGRESQL,
72+
properties = "spring.application.name=my-app")
73+
void runCreatesConnectionDetailsAppendSpringApplicationName(JdbcConnectionDetails connectionDetails)
74+
throws ClassNotFoundException {
75+
assertThat(connectionDetails.getUsername()).isEqualTo("myuser");
76+
assertThat(connectionDetails.getPassword()).isEqualTo("secret");
77+
assertThat(connectionDetails.getJdbcUrl()).startsWith("jdbc:postgresql://")
78+
.endsWith("?connectTimeout=15&ApplicationName=my-app");
79+
checkDatabaseAccess(connectionDetails);
80+
}
81+
82+
@DockerComposeTest(composeFile = "postgres-compose.yaml", image = TestImage.POSTGRESQL,
83+
properties = "spring.application.name=my-app")
84+
void runCreatesConnectionDetailsSpringApplicationName(JdbcConnectionDetails connectionDetails)
85+
throws ClassNotFoundException {
86+
assertThat(connectionDetails.getUsername()).isEqualTo("myuser");
87+
assertThat(connectionDetails.getPassword()).isEqualTo("secret");
88+
assertThat(connectionDetails.getJdbcUrl()).startsWith("jdbc:postgresql://").endsWith("?ApplicationName=my-app");
89+
checkDatabaseAccess(connectionDetails);
90+
}
91+
6092
private void assertConnectionDetails(JdbcConnectionDetails connectionDetails) {
6193
assertThat(connectionDetails.getUsername()).isEqualTo("myuser");
6294
assertThat(connectionDetails.getPassword()).isEqualTo("secret");

spring-boot-project/spring-boot-docker-compose/src/dockerTest/java/org/springframework/boot/docker/compose/service/connection/test/DockerComposeTest.java

+9
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import org.springframework.boot.testsupport.container.DisabledIfDockerUnavailable;
3131
import org.springframework.boot.testsupport.container.TestImage;
3232
import org.springframework.boot.testsupport.process.DisabledIfProcessUnavailable;
33+
import org.springframework.core.env.Environment;
3334

3435
/**
3536
* A {@link Test test} that exercises Spring Boot's Docker Compose support.
@@ -44,6 +45,7 @@
4445
* closed.
4546
*
4647
* @author Andy Wilkinson
48+
* @author Dmytro Nosan
4749
*/
4850
@Test
4951
@Target(ElementType.METHOD)
@@ -70,4 +72,11 @@
7072
*/
7173
TestImage image();
7274

75+
/**
76+
* Properties in form {@literal key=value} that should be added to the Spring
77+
* {@link Environment} before the test runs.
78+
* @return the properties to add
79+
*/
80+
String[] properties() default {};
81+
7382
}

spring-boot-project/spring-boot-docker-compose/src/dockerTest/java/org/springframework/boot/docker/compose/service/connection/test/DockerComposeTestExtension.java

+55-31
Original file line numberDiff line numberDiff line change
@@ -46,22 +46,23 @@
4646
* {@link Extension} for {@link DockerComposeTest @DockerComposeTest}.
4747
*
4848
* @author Andy Wilkinson
49+
* @author Dmytro Nosan
4950
*/
5051
class DockerComposeTestExtension implements BeforeTestExecutionCallback, AfterTestExecutionCallback, ParameterResolver {
5152

5253
private static final Namespace NAMESPACE = Namespace.create(DockerComposeTestExtension.class);
5354

54-
private static final String STORE_KEY_COMPOSE_FILE = "compose-file";
55+
private static final String STORE_KEY_DOCKER_COMPOSE_ATTRIBUTES = "docker-compose-attributes";
5556

5657
private static final String STORE_KEY_APPLICATION_CONTEXT = "application-context";
5758

5859
@Override
5960
public void beforeTestExecution(ExtensionContext context) throws Exception {
60-
Path transformedComposeFile = prepareComposeFile(context);
6161
Store store = context.getStore(NAMESPACE);
62-
store.put(STORE_KEY_COMPOSE_FILE, transformedComposeFile);
62+
DockerComposeAttributes attributes = DockerComposeAttributes.of(context);
63+
store.put(STORE_KEY_DOCKER_COMPOSE_ATTRIBUTES, attributes);
6364
try {
64-
SpringApplication application = prepareApplication(transformedComposeFile);
65+
SpringApplication application = prepareApplication(attributes);
6566
store.put(STORE_KEY_APPLICATION_CONTEXT, application.run());
6667
}
6768
catch (Exception ex) {
@@ -70,34 +71,13 @@ public void beforeTestExecution(ExtensionContext context) throws Exception {
7071
}
7172
}
7273

73-
private Path prepareComposeFile(ExtensionContext context) {
74-
DockerComposeTest dockerComposeTest = context.getRequiredTestMethod().getAnnotation(DockerComposeTest.class);
75-
TestImage image = dockerComposeTest.image();
76-
Resource composeResource = new ClassPathResource(dockerComposeTest.composeFile(),
77-
context.getRequiredTestClass());
78-
return transformedComposeFile(composeResource, image);
79-
}
80-
81-
private Path transformedComposeFile(Resource composeFileResource, TestImage image) {
82-
try {
83-
Path composeFile = composeFileResource.getFile().toPath();
84-
Path transformedComposeFile = Files.createTempFile("", "-" + composeFile.getFileName().toString());
85-
String transformedContent = Files.readString(composeFile).replace("{imageName}", image.toString());
86-
Files.writeString(transformedComposeFile, transformedContent);
87-
return transformedComposeFile;
88-
}
89-
catch (IOException ex) {
90-
fail("Error transforming Docker compose file '" + composeFileResource + "': " + ex.getMessage());
91-
}
92-
return null;
93-
}
94-
95-
private SpringApplication prepareApplication(Path transformedComposeFile) {
74+
private SpringApplication prepareApplication(DockerComposeAttributes attributes) {
9675
SpringApplication application = new SpringApplication(Config.class);
9776
Map<String, Object> properties = new LinkedHashMap<>();
9877
properties.put("spring.docker.compose.skip.in-tests", "false");
99-
properties.put("spring.docker.compose.file", transformedComposeFile);
78+
properties.put("spring.docker.compose.file", attributes.composeFile());
10079
properties.put("spring.docker.compose.stop.command", "down");
80+
properties.putAll(attributes.properties());
10181
application.setDefaultProperties(properties);
10282
return application;
10383
}
@@ -119,9 +99,10 @@ private void runShutdownHandlers() {
11999
}
120100

121101
private void deleteComposeFile(Store store) throws IOException {
122-
Path composeFile = store.get(STORE_KEY_COMPOSE_FILE, Path.class);
123-
if (composeFile != null) {
124-
Files.delete(composeFile);
102+
DockerComposeAttributes attributes = store.get(STORE_KEY_DOCKER_COMPOSE_ATTRIBUTES,
103+
DockerComposeAttributes.class);
104+
if (attributes != null) {
105+
Files.delete(attributes.composeFile());
125106
}
126107
}
127108

@@ -145,4 +126,47 @@ static class Config {
145126

146127
}
147128

129+
private record DockerComposeAttributes(Path composeFile, Map<String, String> properties) {
130+
131+
private static DockerComposeAttributes of(ExtensionContext context) {
132+
DockerComposeTest dockerComposeTest = context.getRequiredTestMethod()
133+
.getAnnotation(DockerComposeTest.class);
134+
TestImage image = dockerComposeTest.image();
135+
Resource composeResource = new ClassPathResource(dockerComposeTest.composeFile(),
136+
context.getRequiredTestClass());
137+
Path composeFile = transformedComposeFile(composeResource, image);
138+
Map<String, String> properties = getProperties(dockerComposeTest.properties());
139+
return new DockerComposeAttributes(composeFile, properties);
140+
}
141+
142+
private static Map<String, String> getProperties(String[] properties) {
143+
Map<String, String> result = new LinkedHashMap<>();
144+
for (String property : properties) {
145+
int index = property.indexOf('=');
146+
if (index > 0) {
147+
result.put(property.substring(0, index), property.substring(index + 1));
148+
}
149+
else {
150+
result.put(property, "");
151+
}
152+
}
153+
return result;
154+
}
155+
156+
private static Path transformedComposeFile(Resource composeFileResource, TestImage image) {
157+
try {
158+
Path composeFile = composeFileResource.getFile().toPath();
159+
Path transformedComposeFile = Files.createTempFile("", "-" + composeFile.getFileName().toString());
160+
String transformedContent = Files.readString(composeFile).replace("{imageName}", image.toString());
161+
Files.writeString(transformedComposeFile, transformedContent);
162+
return transformedComposeFile;
163+
}
164+
catch (IOException ex) {
165+
fail("Error transforming Docker compose file '" + composeFileResource + "': " + ex.getMessage());
166+
}
167+
return null;
168+
}
169+
170+
}
171+
148172
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
services:
2+
database:
3+
image: '{imageName}'
4+
ports:
5+
- '5432'
6+
environment:
7+
- 'POSTGRES_USER=myuser'
8+
- 'POSTGRES_DB=mydatabase'
9+
- 'POSTGRES_PASSWORD=secret'
10+
labels:
11+
org.springframework.boot.jdbc.parameters: 'ApplicationName=spring-boot'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
services:
2+
database:
3+
image: '{imageName}'
4+
ports:
5+
- '5432'
6+
environment:
7+
- 'POSTGRES_USER=myuser'
8+
- 'POSTGRES_DB=mydatabase'
9+
- 'POSTGRES_PASSWORD=secret'
10+
labels:
11+
org.springframework.boot.jdbc.parameters: 'connectTimeout=15'

spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/DockerComposeConnectionSource.java

+16-1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package org.springframework.boot.docker.compose.service.connection;
1818

1919
import org.springframework.boot.docker.compose.core.RunningService;
20+
import org.springframework.core.env.Environment;
2021

2122
/**
2223
* Passed to {@link DockerComposeConnectionDetailsFactory} to provide details of the
@@ -25,19 +26,24 @@
2526
* @author Moritz Halbritter
2627
* @author Andy Wilkinson
2728
* @author Phillip Webb
29+
* @author Dmytro Nosan
2830
* @since 3.1.0
2931
* @see DockerComposeConnectionDetailsFactory
3032
*/
3133
public final class DockerComposeConnectionSource {
3234

3335
private final RunningService runningService;
3436

37+
private final Environment environment;
38+
3539
/**
3640
* Create a new {@link DockerComposeConnectionSource} instance.
3741
* @param runningService the running Docker Compose service
42+
* @param environment environment in which the current application is running
3843
*/
39-
DockerComposeConnectionSource(RunningService runningService) {
44+
DockerComposeConnectionSource(RunningService runningService, Environment environment) {
4045
this.runningService = runningService;
46+
this.environment = environment;
4147
}
4248

4349
/**
@@ -48,4 +54,13 @@ public RunningService getRunningService() {
4854
return this.runningService;
4955
}
5056

57+
/**
58+
* Environment in which the current application is running.
59+
* @return the environment
60+
* @since 3.4.0
61+
*/
62+
public Environment getEnvironment() {
63+
return this.environment;
64+
}
65+
5166
}

spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/DockerComposeServiceConnectionsApplicationListener.java

+7-3
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import org.springframework.boot.docker.compose.lifecycle.DockerComposeServicesReadyEvent;
3232
import org.springframework.context.ApplicationContext;
3333
import org.springframework.context.ApplicationListener;
34+
import org.springframework.core.env.Environment;
3435
import org.springframework.util.ClassUtils;
3536
import org.springframework.util.StringUtils;
3637

@@ -41,6 +42,7 @@
4142
* @author Moritz Halbritter
4243
* @author Andy Wilkinson
4344
* @author Phillip Webb
45+
* @author Dmytro Nosan
4446
*/
4547
class DockerComposeServiceConnectionsApplicationListener
4648
implements ApplicationListener<DockerComposeServicesReadyEvent> {
@@ -59,13 +61,15 @@ class DockerComposeServiceConnectionsApplicationListener
5961
public void onApplicationEvent(DockerComposeServicesReadyEvent event) {
6062
ApplicationContext applicationContext = event.getSource();
6163
if (applicationContext instanceof BeanDefinitionRegistry registry) {
62-
registerConnectionDetails(registry, event.getRunningServices());
64+
Environment environment = applicationContext.getEnvironment();
65+
registerConnectionDetails(registry, environment, event.getRunningServices());
6366
}
6467
}
6568

66-
private void registerConnectionDetails(BeanDefinitionRegistry registry, List<RunningService> runningServices) {
69+
private void registerConnectionDetails(BeanDefinitionRegistry registry, Environment environment,
70+
List<RunningService> runningServices) {
6771
for (RunningService runningService : runningServices) {
68-
DockerComposeConnectionSource source = new DockerComposeConnectionSource(runningService);
72+
DockerComposeConnectionSource source = new DockerComposeConnectionSource(runningService, environment);
6973
this.factories.getConnectionDetails(source, false).forEach((connectionDetailsType, connectionDetails) -> {
7074
register(registry, runningService, connectionDetailsType, connectionDetails);
7175
this.factories.getConnectionDetails(connectionDetails, false)

spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/jdbc/JdbcUrlBuilder.java

+16-2
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,16 @@
2626
* @author Moritz Halbritter
2727
* @author Andy Wilkinson
2828
* @author Phillip Webb
29+
* @author Dmytro Nosan
2930
* @since 3.1.0
3031
*/
3132
public class JdbcUrlBuilder {
3233

33-
private static final String PARAMETERS_LABEL = "org.springframework.boot.jdbc.parameters";
34+
/**
35+
* The default label for JDBC URL additional parameters.
36+
* @since 3.4.0
37+
*/
38+
protected static final String PARAMETERS_LABEL = "org.springframework.boot.jdbc.parameters";
3439

3540
private final String driverProtocol;
3641

@@ -93,7 +98,16 @@ protected void appendParameters(StringBuilder url, String parameters) {
9398
url.append("?").append(parameters);
9499
}
95100

96-
private String getParameters(RunningService service) {
101+
/**
102+
* Returns additional parameters that will be added to the JDBC URL if any.
103+
* <p>
104+
* The default implementation gets value from the service labels using the label named
105+
* {@link #PARAMETERS_LABEL}.
106+
* @param service the running service
107+
* @return additional parameters
108+
* @since 3.4.0
109+
*/
110+
protected String getParameters(RunningService service) {
97111
return service.labels().get(PARAMETERS_LABEL);
98112
}
99113

0 commit comments

Comments
 (0)