Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Auto-configure the Postgres application_name when using Docker Compose #42460

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -57,22 +57,43 @@ void runWithBitnamiImageCreatesConnectionDetails(JdbcConnectionDetails connectio
assertConnectionDetails(connectionDetails);
}

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

private void assertConnectionDetails(JdbcConnectionDetails connectionDetails) {
assertThat(connectionDetails.getUsername()).isEqualTo("myuser");
assertThat(connectionDetails.getPassword()).isEqualTo("secret");
assertThat(connectionDetails.getJdbcUrl()).startsWith("jdbc:postgresql://").endsWith("/mydatabase");
}

@SuppressWarnings("unchecked")
private void checkDatabaseAccess(JdbcConnectionDetails connectionDetails) throws ClassNotFoundException {
assertThat(queryForObject(connectionDetails, DatabaseDriver.POSTGRESQL.getValidationQuery(), Integer.class))
.isEqualTo(1);
}

private void checkApplicationName(JdbcConnectionDetails connectionDetails, String applicationName)
throws ClassNotFoundException {
assertThat(queryForObject(connectionDetails, "select current_setting('application_name')", String.class))
.isEqualTo(applicationName);
}

@SuppressWarnings("unchecked")
private <T> T queryForObject(JdbcConnectionDetails connectionDetails, String sql, Class<T> result)
throws ClassNotFoundException {
SimpleDriverDataSource dataSource = new SimpleDriverDataSource();
dataSource.setUrl(connectionDetails.getJdbcUrl());
dataSource.setUsername(connectionDetails.getUsername());
dataSource.setPassword(connectionDetails.getPassword());
dataSource.setDriverClass((Class<? extends Driver>) ClassUtils.forName(connectionDetails.getDriverClassName(),
getClass().getClassLoader()));
JdbcTemplate template = new JdbcTemplate(dataSource);
assertThat(template.queryForObject(DatabaseDriver.POSTGRESQL.getValidationQuery(), Integer.class)).isEqualTo(1);
return new JdbcTemplate(dataSource).queryForObject(sql, result);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

import io.r2dbc.spi.ConnectionFactories;
import io.r2dbc.spi.ConnectionFactoryOptions;
import io.r2dbc.spi.Option;

import org.springframework.boot.autoconfigure.r2dbc.R2dbcConnectionDetails;
import org.springframework.boot.docker.compose.service.connection.test.DockerComposeTest;
Expand Down Expand Up @@ -60,21 +61,42 @@ void runWithBitnamiImageCreatesConnectionDetails(R2dbcConnectionDetails connecti
assertConnectionDetails(connectionDetails);
}

@DockerComposeTest(composeFile = "postgres-application-name-compose.yaml", image = TestImage.POSTGRESQL)
void runCreatesConnectionDetailsApplicationName(R2dbcConnectionDetails connectionDetails) {
assertConnectionDetails(connectionDetails);
ConnectionFactoryOptions options = connectionDetails.getConnectionFactoryOptions();
assertThat(options.getValue(Option.valueOf("applicationName"))).isEqualTo("spring boot");
checkApplicationName(connectionDetails, "spring boot");
}

private void assertConnectionDetails(R2dbcConnectionDetails connectionDetails) {
ConnectionFactoryOptions connectionFactoryOptions = connectionDetails.getConnectionFactoryOptions();
assertThat(connectionFactoryOptions.toString()).contains("database=mydatabase", "driver=postgresql",
"password=REDACTED", "user=myuser");
assertThat(connectionFactoryOptions.getRequiredValue(ConnectionFactoryOptions.PASSWORD)).isEqualTo("secret");
ConnectionFactoryOptions options = connectionDetails.getConnectionFactoryOptions();
assertThat(options.getRequiredValue(ConnectionFactoryOptions.HOST)).isNotNull();
assertThat(options.getRequiredValue(ConnectionFactoryOptions.PORT)).isNotNull();
assertThat(options.getRequiredValue(ConnectionFactoryOptions.DATABASE)).isEqualTo("mydatabase");
assertThat(options.getRequiredValue(ConnectionFactoryOptions.USER)).isEqualTo("myuser");
assertThat(options.getRequiredValue(ConnectionFactoryOptions.PASSWORD)).isEqualTo("secret");
assertThat(options.getRequiredValue(ConnectionFactoryOptions.DRIVER)).isEqualTo("postgresql");
}

private void checkDatabaseAccess(R2dbcConnectionDetails connectionDetails) {
Integer result = queryForObject(connectionDetails, DatabaseDriver.POSTGRESQL.getValidationQuery(),
Integer.class);
assertThat(result).isEqualTo(1);
}

private void checkApplicationName(R2dbcConnectionDetails connectionDetails, String applicationName) {
assertThat(queryForObject(connectionDetails, "select current_setting('application_name')", String.class))
.isEqualTo(applicationName);
}

private <T> T queryForObject(R2dbcConnectionDetails connectionDetails, String sql, Class<T> result) {
ConnectionFactoryOptions connectionFactoryOptions = connectionDetails.getConnectionFactoryOptions();
Object result = DatabaseClient.create(ConnectionFactories.get(connectionFactoryOptions))
.sql(DatabaseDriver.POSTGRESQL.getValidationQuery())
.map((row, metadata) -> row.get(0))
return DatabaseClient.create(ConnectionFactories.get(connectionFactoryOptions))
.sql(sql)
.mapValue(result)
.first()
.block(Duration.ofSeconds(30));
assertThat(result).isEqualTo(1);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
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'
org.springframework.boot.r2dbc.parameters: 'applicationName=spring boot'
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 @@ -32,12 +33,16 @@ 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 +53,13 @@ public RunningService getRunningService() {
return this.runningService;
}

/**
* Environment in which the current application is running.
* @return the environment
* @since 3.5.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 Down Expand Up @@ -59,13 +60,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 @@ -16,11 +16,16 @@

package org.springframework.boot.docker.compose.service.connection.postgres;

import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;

import org.springframework.boot.autoconfigure.jdbc.JdbcConnectionDetails;
import org.springframework.boot.docker.compose.core.RunningService;
import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionDetailsFactory;
import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionSource;
import org.springframework.boot.docker.compose.service.connection.jdbc.JdbcUrlBuilder;
import org.springframework.core.env.Environment;
import org.springframework.util.StringUtils;

/**
* {@link DockerComposeConnectionDetailsFactory} to create {@link JdbcConnectionDetails}
Expand All @@ -42,7 +47,7 @@ protected PostgresJdbcDockerComposeConnectionDetailsFactory() {

@Override
protected JdbcConnectionDetails getDockerComposeConnectionDetails(DockerComposeConnectionSource source) {
return new PostgresJdbcDockerComposeConnectionDetails(source.getRunningService());
return new PostgresJdbcDockerComposeConnectionDetails(source.getRunningService(), source.getEnvironment());
}

/**
Expand All @@ -57,10 +62,11 @@ static class PostgresJdbcDockerComposeConnectionDetails extends DockerComposeCon

private final String jdbcUrl;

PostgresJdbcDockerComposeConnectionDetails(RunningService service) {
PostgresJdbcDockerComposeConnectionDetails(RunningService service, Environment environment) {
super(service);
this.environment = new PostgresEnvironment(service.env());
this.jdbcUrl = jdbcUrlBuilder.build(service, this.environment.getDatabase());
this.jdbcUrl = addApplicationNameIfNecessary(jdbcUrlBuilder.build(service, this.environment.getDatabase()),
environment);
}

@Override
Expand All @@ -78,6 +84,27 @@ public String getJdbcUrl() {
return this.jdbcUrl;
}

private static String addApplicationNameIfNecessary(String jdbcUrl, Environment environment) {
if (jdbcUrl.contains("&ApplicationName=") || jdbcUrl.contains("?ApplicationName=")) {
return jdbcUrl;
}
String applicationName = environment.getProperty("spring.application.name");
if (!StringUtils.hasText(applicationName)) {
return jdbcUrl;
}
StringBuilder jdbcUrlBuilder = new StringBuilder(jdbcUrl);
if (!jdbcUrl.contains("?")) {
jdbcUrlBuilder.append("?");
}
else if (!jdbcUrl.endsWith("&")) {
jdbcUrlBuilder.append("&");
}
return jdbcUrlBuilder.append("ApplicationName")
.append('=')
.append(URLEncoder.encode(applicationName, StandardCharsets.UTF_8))
.toString();
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,15 @@
package org.springframework.boot.docker.compose.service.connection.postgres;

import io.r2dbc.spi.ConnectionFactoryOptions;
import io.r2dbc.spi.Option;

import org.springframework.boot.autoconfigure.r2dbc.R2dbcConnectionDetails;
import org.springframework.boot.docker.compose.core.RunningService;
import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionDetailsFactory;
import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionSource;
import org.springframework.boot.docker.compose.service.connection.r2dbc.ConnectionFactoryOptionsBuilder;
import org.springframework.core.env.Environment;
import org.springframework.util.StringUtils;

/**
* {@link DockerComposeConnectionDetailsFactory} to create {@link R2dbcConnectionDetails}
Expand All @@ -44,7 +47,7 @@ class PostgresR2dbcDockerComposeConnectionDetailsFactory

@Override
protected R2dbcConnectionDetails getDockerComposeConnectionDetails(DockerComposeConnectionSource source) {
return new PostgresDbR2dbcDockerComposeConnectionDetails(source.getRunningService());
return new PostgresDbR2dbcDockerComposeConnectionDetails(source.getRunningService(), source.getEnvironment());
}

/**
Expand All @@ -53,23 +56,43 @@ protected R2dbcConnectionDetails getDockerComposeConnectionDetails(DockerCompose
static class PostgresDbR2dbcDockerComposeConnectionDetails extends DockerComposeConnectionDetails
implements R2dbcConnectionDetails {

private static final Option<String> APPLICATION_NAME = Option.valueOf("applicationName");

private static final ConnectionFactoryOptionsBuilder connectionFactoryOptionsBuilder = new ConnectionFactoryOptionsBuilder(
"postgresql", 5432);

private final ConnectionFactoryOptions connectionFactoryOptions;

PostgresDbR2dbcDockerComposeConnectionDetails(RunningService service) {
PostgresDbR2dbcDockerComposeConnectionDetails(RunningService service, Environment environment) {
super(service);
PostgresEnvironment environment = new PostgresEnvironment(service.env());
this.connectionFactoryOptions = connectionFactoryOptionsBuilder.build(service, environment.getDatabase(),
environment.getUsername(), environment.getPassword());
this.connectionFactoryOptions = getConnectionFactoryOptions(service, environment);
}

@Override
public ConnectionFactoryOptions getConnectionFactoryOptions() {
return this.connectionFactoryOptions;
}

private static ConnectionFactoryOptions getConnectionFactoryOptions(RunningService service,
Environment environment) {
PostgresEnvironment env = new PostgresEnvironment(service.env());
ConnectionFactoryOptions connectionFactoryOptions = connectionFactoryOptionsBuilder.build(service,
env.getDatabase(), env.getUsername(), env.getPassword());
return addApplicationNameIfNecessary(connectionFactoryOptions, environment);
}

private static ConnectionFactoryOptions addApplicationNameIfNecessary(
ConnectionFactoryOptions connectionFactoryOptions, Environment environment) {
if (connectionFactoryOptions.hasOption(APPLICATION_NAME)) {
return connectionFactoryOptions;
}
String applicationName = environment.getProperty("spring.application.name");
if (!StringUtils.hasText(applicationName)) {
return connectionFactoryOptions;
}
return connectionFactoryOptions.mutate().option(APPLICATION_NAME, applicationName).build();
}

}

}
Loading