Skip to content

Commit

Permalink
Auto-configure the Postgres application_name when using Docker Compose
Browse files Browse the repository at this point in the history
  • Loading branch information
nosan committed Sep 27, 2024
1 parent 04c8344 commit 2ac675e
Show file tree
Hide file tree
Showing 10 changed files with 212 additions and 41 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,38 @@ void runWithBitnamiImageCreatesConnectionDetails(JdbcConnectionDetails connectio
assertConnectionDetails(connectionDetails);
}

@DockerComposeTest(composeFile = "postgres-compose-application-name-label.yaml", image = TestImage.POSTGRESQL,
properties = "spring.application.name=my-app")
void runCreatesConnectionDetailsApplicationNameFromComposeFileTakesPrecedenceOverSpringApplicationName(
JdbcConnectionDetails connectionDetails) throws ClassNotFoundException {
assertThat(connectionDetails.getUsername()).isEqualTo("myuser");
assertThat(connectionDetails.getPassword()).isEqualTo("secret");
assertThat(connectionDetails.getJdbcUrl()).startsWith("jdbc:postgresql://")
.endsWith("?ApplicationName=spring-boot");
checkDatabaseAccess(connectionDetails);
}

@DockerComposeTest(composeFile = "postgres-compose-connect-timeout-label.yaml", image = TestImage.POSTGRESQL,
properties = "spring.application.name=my-app")
void runCreatesConnectionDetailsAppendSpringApplicationName(JdbcConnectionDetails connectionDetails)
throws ClassNotFoundException {
assertThat(connectionDetails.getUsername()).isEqualTo("myuser");
assertThat(connectionDetails.getPassword()).isEqualTo("secret");
assertThat(connectionDetails.getJdbcUrl()).startsWith("jdbc:postgresql://")
.endsWith("?connectTimeout=15&ApplicationName=my-app");
checkDatabaseAccess(connectionDetails);
}

@DockerComposeTest(composeFile = "postgres-compose.yaml", image = TestImage.POSTGRESQL,
properties = "spring.application.name=my-app")
void runCreatesConnectionDetailsSpringApplicationName(JdbcConnectionDetails connectionDetails)
throws ClassNotFoundException {
assertThat(connectionDetails.getUsername()).isEqualTo("myuser");
assertThat(connectionDetails.getPassword()).isEqualTo("secret");
assertThat(connectionDetails.getJdbcUrl()).startsWith("jdbc:postgresql://").endsWith("?ApplicationName=my-app");
checkDatabaseAccess(connectionDetails);
}

private void assertConnectionDetails(JdbcConnectionDetails connectionDetails) {
assertThat(connectionDetails.getUsername()).isEqualTo("myuser");
assertThat(connectionDetails.getPassword()).isEqualTo("secret");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import org.springframework.boot.testsupport.container.DisabledIfDockerUnavailable;
import org.springframework.boot.testsupport.container.TestImage;
import org.springframework.boot.testsupport.process.DisabledIfProcessUnavailable;
import org.springframework.core.env.Environment;

/**
* A {@link Test test} that exercises Spring Boot's Docker Compose support.
Expand All @@ -44,6 +45,7 @@
* closed.
*
* @author Andy Wilkinson
* @author Dmytro Nosan
*/
@Test
@Target(ElementType.METHOD)
Expand All @@ -70,4 +72,11 @@
*/
TestImage image();

/**
* Properties in form {@literal key=value} that should be added to the Spring
* {@link Environment} before the test runs.
* @return the properties to add
*/
String[] properties() default {};

}
Original file line number Diff line number Diff line change
Expand Up @@ -46,22 +46,23 @@
* {@link Extension} for {@link DockerComposeTest @DockerComposeTest}.
*
* @author Andy Wilkinson
* @author Dmytro Nosan
*/
class DockerComposeTestExtension implements BeforeTestExecutionCallback, AfterTestExecutionCallback, ParameterResolver {

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

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

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

@Override
public void beforeTestExecution(ExtensionContext context) throws Exception {
Path transformedComposeFile = prepareComposeFile(context);
Store store = context.getStore(NAMESPACE);
store.put(STORE_KEY_COMPOSE_FILE, transformedComposeFile);
DockerComposeAttributes attributes = DockerComposeAttributes.of(context);
store.put(STORE_KEY_DOCKER_COMPOSE_ATTRIBUTES, attributes);
try {
SpringApplication application = prepareApplication(transformedComposeFile);
SpringApplication application = prepareApplication(attributes);
store.put(STORE_KEY_APPLICATION_CONTEXT, application.run());
}
catch (Exception ex) {
Expand All @@ -70,34 +71,13 @@ public void beforeTestExecution(ExtensionContext context) throws Exception {
}
}

private Path prepareComposeFile(ExtensionContext context) {
DockerComposeTest dockerComposeTest = context.getRequiredTestMethod().getAnnotation(DockerComposeTest.class);
TestImage image = dockerComposeTest.image();
Resource composeResource = new ClassPathResource(dockerComposeTest.composeFile(),
context.getRequiredTestClass());
return transformedComposeFile(composeResource, image);
}

private Path transformedComposeFile(Resource composeFileResource, TestImage image) {
try {
Path composeFile = composeFileResource.getFile().toPath();
Path transformedComposeFile = Files.createTempFile("", "-" + composeFile.getFileName().toString());
String transformedContent = Files.readString(composeFile).replace("{imageName}", image.toString());
Files.writeString(transformedComposeFile, transformedContent);
return transformedComposeFile;
}
catch (IOException ex) {
fail("Error transforming Docker compose file '" + composeFileResource + "': " + ex.getMessage());
}
return null;
}

private SpringApplication prepareApplication(Path transformedComposeFile) {
private SpringApplication prepareApplication(DockerComposeAttributes attributes) {
SpringApplication application = new SpringApplication(Config.class);
Map<String, Object> properties = new LinkedHashMap<>();
properties.put("spring.docker.compose.skip.in-tests", "false");
properties.put("spring.docker.compose.file", transformedComposeFile);
properties.put("spring.docker.compose.file", attributes.composeFile());
properties.put("spring.docker.compose.stop.command", "down");
properties.putAll(attributes.properties());
application.setDefaultProperties(properties);
return application;
}
Expand All @@ -119,9 +99,10 @@ private void runShutdownHandlers() {
}

private void deleteComposeFile(Store store) throws IOException {
Path composeFile = store.get(STORE_KEY_COMPOSE_FILE, Path.class);
if (composeFile != null) {
Files.delete(composeFile);
DockerComposeAttributes attributes = store.get(STORE_KEY_DOCKER_COMPOSE_ATTRIBUTES,
DockerComposeAttributes.class);
if (attributes != null) {
Files.delete(attributes.composeFile());
}
}

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

}

private record DockerComposeAttributes(Path composeFile, Map<String, String> properties) {

private static DockerComposeAttributes of(ExtensionContext context) {
DockerComposeTest dockerComposeTest = context.getRequiredTestMethod()
.getAnnotation(DockerComposeTest.class);
TestImage image = dockerComposeTest.image();
Resource composeResource = new ClassPathResource(dockerComposeTest.composeFile(),
context.getRequiredTestClass());
Path composeFile = transformedComposeFile(composeResource, image);
Map<String, String> properties = getProperties(dockerComposeTest.properties());
return new DockerComposeAttributes(composeFile, properties);
}

private static Map<String, String> getProperties(String[] properties) {
Map<String, String> result = new LinkedHashMap<>();
for (String property : properties) {
int index = property.indexOf('=');
if (index > 0) {
result.put(property.substring(0, index), property.substring(index + 1));
}
else {
result.put(property, "");
}
}
return result;
}

private static Path transformedComposeFile(Resource composeFileResource, TestImage image) {
try {
Path composeFile = composeFileResource.getFile().toPath();
Path transformedComposeFile = Files.createTempFile("", "-" + composeFile.getFileName().toString());
String transformedContent = Files.readString(composeFile).replace("{imageName}", image.toString());
Files.writeString(transformedComposeFile, transformedContent);
return transformedComposeFile;
}
catch (IOException ex) {
fail("Error transforming Docker compose file '" + composeFileResource + "': " + ex.getMessage());
}
return null;
}

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
services:
database:
image: '{imageName}'
ports:
- '5432'
environment:
- 'POSTGRES_USER=myuser'
- 'POSTGRES_DB=mydatabase'
- 'POSTGRES_PASSWORD=secret'
labels:
org.springframework.boot.jdbc.parameters: 'ApplicationName=spring-boot'
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
services:
database:
image: '{imageName}'
ports:
- '5432'
environment:
- 'POSTGRES_USER=myuser'
- 'POSTGRES_DB=mydatabase'
- 'POSTGRES_PASSWORD=secret'
labels:
org.springframework.boot.jdbc.parameters: 'connectTimeout=15'
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package org.springframework.boot.docker.compose.service.connection;

import org.springframework.boot.docker.compose.core.RunningService;
import org.springframework.core.env.Environment;

/**
* Passed to {@link DockerComposeConnectionDetailsFactory} to provide details of the
Expand All @@ -25,19 +26,24 @@
* @author Moritz Halbritter
* @author Andy Wilkinson
* @author Phillip Webb
* @author Dmytro Nosan
* @since 3.1.0
* @see DockerComposeConnectionDetailsFactory
*/
public final class DockerComposeConnectionSource {

private final RunningService runningService;

private final Environment environment;

/**
* Create a new {@link DockerComposeConnectionSource} instance.
* @param runningService the running Docker Compose service
* @param environment environment in which the current application is running
*/
DockerComposeConnectionSource(RunningService runningService) {
DockerComposeConnectionSource(RunningService runningService, Environment environment) {
this.runningService = runningService;
this.environment = environment;
}

/**
Expand All @@ -48,4 +54,13 @@ public RunningService getRunningService() {
return this.runningService;
}

/**
* Environment in which the current application is running.
* @return the environment
* @since 3.4.0
*/
public Environment getEnvironment() {
return this.environment;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import org.springframework.boot.docker.compose.lifecycle.DockerComposeServicesReadyEvent;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationListener;
import org.springframework.core.env.Environment;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;

Expand All @@ -41,6 +42,7 @@
* @author Moritz Halbritter
* @author Andy Wilkinson
* @author Phillip Webb
* @author Dmytro Nosan
*/
class DockerComposeServiceConnectionsApplicationListener
implements ApplicationListener<DockerComposeServicesReadyEvent> {
Expand All @@ -59,13 +61,15 @@ class DockerComposeServiceConnectionsApplicationListener
public void onApplicationEvent(DockerComposeServicesReadyEvent event) {
ApplicationContext applicationContext = event.getSource();
if (applicationContext instanceof BeanDefinitionRegistry registry) {
registerConnectionDetails(registry, event.getRunningServices());
Environment environment = applicationContext.getEnvironment();
registerConnectionDetails(registry, environment, event.getRunningServices());
}
}

private void registerConnectionDetails(BeanDefinitionRegistry registry, List<RunningService> runningServices) {
private void registerConnectionDetails(BeanDefinitionRegistry registry, Environment environment,
List<RunningService> runningServices) {
for (RunningService runningService : runningServices) {
DockerComposeConnectionSource source = new DockerComposeConnectionSource(runningService);
DockerComposeConnectionSource source = new DockerComposeConnectionSource(runningService, environment);
this.factories.getConnectionDetails(source, false).forEach((connectionDetailsType, connectionDetails) -> {
register(registry, runningService, connectionDetailsType, connectionDetails);
this.factories.getConnectionDetails(connectionDetails, false)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,16 @@
* @author Moritz Halbritter
* @author Andy Wilkinson
* @author Phillip Webb
* @author Dmytro Nosan
* @since 3.1.0
*/
public class JdbcUrlBuilder {

private static final String PARAMETERS_LABEL = "org.springframework.boot.jdbc.parameters";
/**
* The default label for JDBC URL additional parameters.
* @since 3.4.0
*/
protected static final String PARAMETERS_LABEL = "org.springframework.boot.jdbc.parameters";

private final String driverProtocol;

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

private String getParameters(RunningService service) {
/**
* Returns additional parameters that will be added to the JDBC URL if any.
* <p>
* The default implementation gets value from the service labels using the label named
* {@link #PARAMETERS_LABEL}.
* @param service the running service
* @return additional parameters
* @since 3.4.0
*/
protected String getParameters(RunningService service) {
return service.labels().get(PARAMETERS_LABEL);
}

Expand Down
Loading

0 comments on commit 2ac675e

Please sign in to comment.