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 Oct 6, 2024
1 parent 8ccf77b commit 3a5a70a
Show file tree
Hide file tree
Showing 11 changed files with 555 additions and 23 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
* @author Andy Wilkinson
* @author Phillip Webb
* @author Scott Frederick
* @author Dmytro Nosan
*/
class PostgresJdbcDockerComposeConnectionDetailsFactoryIntegrationTests {

Expand All @@ -57,22 +58,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 All @@ -36,6 +37,7 @@
* @author Andy Wilkinson
* @author Phillip Webb
* @author Scott Frederick
* @author Dmytro Nosan
*/
class PostgresR2dbcDockerComposeConnectionDetailsFactoryIntegrationTests {

Expand All @@ -60,21 +62,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 @@ -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 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 @@ -30,6 +35,7 @@
* @author Andy Wilkinson
* @author Phillip Webb
* @author Scott Frederick
* @author Dmytro Nosan
*/
class PostgresJdbcDockerComposeConnectionDetailsFactory
extends DockerComposeConnectionDetailsFactory<JdbcConnectionDetails> {
Expand All @@ -42,7 +48,7 @@ protected PostgresJdbcDockerComposeConnectionDetailsFactory() {

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

/**
Expand All @@ -53,14 +59,16 @@ static class PostgresJdbcDockerComposeConnectionDetails extends DockerComposeCon

private static final JdbcUrlBuilder jdbcUrlBuilder = new JdbcUrlBuilder("postgresql", 5432);

private static final String APPLICATION_NAME = "ApplicationName";

private final PostgresEnvironment environment;

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 = getJdbcUrl(service, this.environment.getDatabase(), environment);
}

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

private static String getJdbcUrl(RunningService service, String database, Environment environment) {
PostgresJdbcUrl jdbcUrl = new PostgresJdbcUrl(jdbcUrlBuilder.build(service, database));
addApplicationNameIfNecessary(jdbcUrl, environment);
return jdbcUrl.toString();
}

private static void addApplicationNameIfNecessary(PostgresJdbcUrl jdbcUrl, Environment environment) {
String applicationName = environment.getProperty("spring.application.name");
if (StringUtils.hasText(applicationName)) {
jdbcUrl.addParameterIfDoesNotExist(APPLICATION_NAME,
URLEncoder.encode(applicationName, StandardCharsets.UTF_8));
}
}

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/*
* Copyright 2012-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

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

import java.util.StringTokenizer;

import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;

/**
* Utility class to customize Postgres JDBC URL.
*
* @author Dmytro Nosan
*/
class PostgresJdbcUrl {

private final String url;

private final MultiValueMap<String, String> parameters;

/**
* Creates new {@link PostgresJdbcUrl} instance.
* @param jdbcUrl the JDBC URL
*/
PostgresJdbcUrl(String jdbcUrl) {
this.url = getUrl(jdbcUrl);
this.parameters = getParameters(jdbcUrl);
}

/**
* Adds value to the JDBC URL if a given name does not exist.
* @param name the JDBC parameter name
* @param value the JDBC parameter value
*/
void addParameterIfDoesNotExist(String name, String value) {
if (this.parameters.containsKey(name)) {
return;
}
this.parameters.add(name, value);
}

/**
* Build a JDBC URL.
* @return a new JDBC URL
*/
@Override
public String toString() {
StringBuilder jdbcUrlBuilder = new StringBuilder(this.url);
if (this.parameters.isEmpty()) {
return jdbcUrlBuilder.toString();
}
jdbcUrlBuilder.append('?');
this.parameters.forEach((name, values) -> values.forEach((value) -> {
jdbcUrlBuilder.append(name);
if (value != null) {
jdbcUrlBuilder.append('=').append(value);
}
jdbcUrlBuilder.append('&');
}));
jdbcUrlBuilder.deleteCharAt(jdbcUrlBuilder.length() - 1);
return jdbcUrlBuilder.toString();
}

private static String getUrl(String jdbcUrl) {
int index = jdbcUrl.indexOf('?');
return (index != -1) ? jdbcUrl.substring(0, index) : jdbcUrl;
}

private static MultiValueMap<String, String> getParameters(String jdbcUrl) {
MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
int index = jdbcUrl.indexOf('?');
if (index == -1) {
return parameters;
}
StringTokenizer tokenizer = new StringTokenizer(jdbcUrl.substring(index + 1), "&");
while (tokenizer.hasMoreTokens()) {
String token = tokenizer.nextToken();
int pos = token.indexOf('=');
if (pos == -1) {
parameters.add(token, null);
}
else {
parameters.add(token.substring(0, pos), token.substring(pos + 1));
}
}
return parameters;
}

}
Loading

0 comments on commit 3a5a70a

Please sign in to comment.