Skip to content

Commit edb6c91

Browse files
committed
Autoconfigure the Postgres application_name when using Docker Compose
1 parent b82830c commit edb6c91

File tree

12 files changed

+268
-48
lines changed

12 files changed

+268
-48
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/postgres/PostgresR2dbcDockerComposeConnectionDetailsFactoryIntegrationTests.java

+30
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
import io.r2dbc.spi.ConnectionFactories;
2222
import io.r2dbc.spi.ConnectionFactoryOptions;
23+
import io.r2dbc.spi.Option;
2324

2425
import org.springframework.boot.autoconfigure.r2dbc.R2dbcConnectionDetails;
2526
import org.springframework.boot.docker.compose.service.connection.test.DockerComposeTest;
@@ -60,6 +61,35 @@ void runWithBitnamiImageCreatesConnectionDetails(R2dbcConnectionDetails connecti
6061
assertConnectionDetails(connectionDetails);
6162
}
6263

64+
@DockerComposeTest(composeFile = "postgres-compose-application-name-label.yaml", image = TestImage.POSTGRESQL,
65+
properties = "spring.application.name=my-app")
66+
void runCreatesConnectionDetailsApplicationNameFromComposeFileTakesPrecedenceOverSpringApplicationName(
67+
R2dbcConnectionDetails connectionDetails) {
68+
assertConnectionDetails(connectionDetails);
69+
ConnectionFactoryOptions options = connectionDetails.getConnectionFactoryOptions();
70+
assertThat(options.getRequiredValue(Option.valueOf("applicationName"))).isEqualTo("spring-boot");
71+
checkDatabaseAccess(connectionDetails);
72+
}
73+
74+
@DockerComposeTest(composeFile = "postgres-compose-connect-timeout-label.yaml", image = TestImage.POSTGRESQL,
75+
properties = "spring.application.name=my-app")
76+
void runCreatesConnectionDetailsAppendSpringApplicationName(R2dbcConnectionDetails connectionDetails) {
77+
assertConnectionDetails(connectionDetails);
78+
ConnectionFactoryOptions options = connectionDetails.getConnectionFactoryOptions();
79+
assertThat(options.getRequiredValue(Option.valueOf("applicationName"))).isEqualTo("my-app");
80+
assertThat(options.getRequiredValue(ConnectionFactoryOptions.CONNECT_TIMEOUT)).isEqualTo("15");
81+
checkDatabaseAccess(connectionDetails);
82+
}
83+
84+
@DockerComposeTest(composeFile = "postgres-compose.yaml", image = TestImage.POSTGRESQL,
85+
properties = "spring.application.name=my-app")
86+
void runCreatesConnectionDetailsSpringApplicationName(R2dbcConnectionDetails connectionDetails) {
87+
assertConnectionDetails(connectionDetails);
88+
ConnectionFactoryOptions options = connectionDetails.getConnectionFactoryOptions();
89+
assertThat(options.getRequiredValue(Option.valueOf("applicationName"))).isEqualTo("my-app");
90+
checkDatabaseAccess(connectionDetails);
91+
}
92+
6393
private void assertConnectionDetails(R2dbcConnectionDetails connectionDetails) {
6494
ConnectionFactoryOptions connectionFactoryOptions = connectionDetails.getConnectionFactoryOptions();
6595
assertThat(connectionFactoryOptions.toString()).contains("database=mydatabase", "driver=postgresql",

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

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

1919
import java.io.IOException;
20+
import java.io.UncheckedIOException;
2021
import java.nio.file.Files;
2122
import java.nio.file.Path;
2223
import java.util.LinkedHashMap;
@@ -40,28 +41,27 @@
4041
import org.springframework.core.io.ClassPathResource;
4142
import org.springframework.core.io.Resource;
4243

43-
import static org.assertj.core.api.Assertions.fail;
44-
4544
/**
4645
* {@link Extension} for {@link DockerComposeTest @DockerComposeTest}.
4746
*
4847
* @author Andy Wilkinson
48+
* @author Dmytro Nosan
4949
*/
5050
class DockerComposeTestExtension implements BeforeTestExecutionCallback, AfterTestExecutionCallback, ParameterResolver {
5151

5252
private static final Namespace NAMESPACE = Namespace.create(DockerComposeTestExtension.class);
5353

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

5656
private static final String STORE_KEY_APPLICATION_CONTEXT = "application-context";
5757

5858
@Override
5959
public void beforeTestExecution(ExtensionContext context) throws Exception {
60-
Path transformedComposeFile = prepareComposeFile(context);
6160
Store store = context.getStore(NAMESPACE);
62-
store.put(STORE_KEY_COMPOSE_FILE, transformedComposeFile);
61+
DockerComposeAttributes attributes = DockerComposeAttributes.get(context);
62+
store.put(STORE_KEY_DOCKER_COMPOSE_ATTRIBUTES, attributes);
6363
try {
64-
SpringApplication application = prepareApplication(transformedComposeFile);
64+
SpringApplication application = prepareApplication(attributes);
6565
store.put(STORE_KEY_APPLICATION_CONTEXT, application.run());
6666
}
6767
catch (Exception ex) {
@@ -70,34 +70,13 @@ public void beforeTestExecution(ExtensionContext context) throws Exception {
7070
}
7171
}
7272

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) {
73+
private SpringApplication prepareApplication(DockerComposeAttributes attributes) {
9674
SpringApplication application = new SpringApplication(Config.class);
9775
Map<String, Object> properties = new LinkedHashMap<>();
9876
properties.put("spring.docker.compose.skip.in-tests", "false");
99-
properties.put("spring.docker.compose.file", transformedComposeFile);
77+
properties.put("spring.docker.compose.file", attributes.composeFile());
10078
properties.put("spring.docker.compose.stop.command", "down");
79+
properties.putAll(attributes.properties());
10180
application.setDefaultProperties(properties);
10281
return application;
10382
}
@@ -119,9 +98,10 @@ private void runShutdownHandlers() {
11998
}
12099

121100
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);
101+
DockerComposeAttributes attributes = store.get(STORE_KEY_DOCKER_COMPOSE_ATTRIBUTES,
102+
DockerComposeAttributes.class);
103+
if (attributes != null) {
104+
Files.delete(attributes.composeFile());
125105
}
126106
}
127107

@@ -145,4 +125,47 @@ static class Config {
145125

146126
}
147127

128+
private record DockerComposeAttributes(Path composeFile, Map<String, String> properties) {
129+
130+
private static DockerComposeAttributes get(ExtensionContext context) {
131+
DockerComposeTest dockerComposeTest = context.getRequiredTestMethod()
132+
.getAnnotation(DockerComposeTest.class);
133+
TestImage image = dockerComposeTest.image();
134+
Resource composeResource = new ClassPathResource(dockerComposeTest.composeFile(),
135+
context.getRequiredTestClass());
136+
Path composeFile = transformedComposeFile(composeResource, image);
137+
Map<String, String> properties = getProperties(dockerComposeTest.properties());
138+
return new DockerComposeAttributes(composeFile, properties);
139+
}
140+
141+
private static Map<String, String> getProperties(String[] properties) {
142+
Map<String, String> result = new LinkedHashMap<>();
143+
for (String property : properties) {
144+
int index = property.indexOf('=');
145+
if (index > 0) {
146+
result.put(property.substring(0, index), property.substring(index + 1));
147+
}
148+
else {
149+
result.put(property, "");
150+
}
151+
}
152+
return result;
153+
}
154+
155+
private static Path transformedComposeFile(Resource composeFileResource, TestImage image) {
156+
try {
157+
Path composeFile = composeFileResource.getFile().toPath();
158+
Path transformedComposeFile = Files.createTempFile("", "-" + composeFile.getFileName().toString());
159+
String transformedContent = Files.readString(composeFile).replace("{imageName}", image.toString());
160+
Files.writeString(transformedComposeFile, transformedContent);
161+
return transformedComposeFile;
162+
}
163+
catch (IOException ex) {
164+
throw new UncheckedIOException(
165+
"Error transforming Docker compose file '" + composeFileResource + "': " + ex.getMessage(), ex);
166+
}
167+
}
168+
169+
}
170+
148171
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
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'
12+
org.springframework.boot.r2dbc.parameters: 'applicationName=spring-boot'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
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'
12+
org.springframework.boot.r2dbc.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)