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

Introduce EnvironmentPostProcessor for R2DBC support in Cloud Sql #772

Merged
merged 27 commits into from
Jan 24, 2022
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
31d7ed8
include starter and samples for R2DBC; undo renaming of class
mpeddada1 Dec 6, 2021
7711250
improve documentation and cleanup samples
mpeddada1 Dec 7, 2021
92ca251
Build r2dbc url when database name and instance connection name are p…
mpeddada1 Dec 10, 2021
eeacab9
fix README; use webflux in samples
mpeddada1 Dec 11, 2021
37ee37c
improve check for postgreSQL and mySQL driver in post processor; clea…
mpeddada1 Dec 13, 2021
92b23e0
more clean up
mpeddada1 Dec 13, 2021
139fb92
fix comment in application.properties for postgreSQL sample
mpeddada1 Dec 13, 2021
99202b3
remove public modifier from test class
mpeddada1 Dec 13, 2021
dee8706
Combine R2dbc logic into CloudSqlEnvironmentPostProcessor
mpeddada1 Dec 14, 2021
5796bdf
Merge branch 'main' of github.com:GoogleCloudPlatform/spring-cloud-gc…
mpeddada1 Dec 14, 2021
8da6245
remove redundant else if condition
mpeddada1 Dec 14, 2021
2975a77
Revert "Combine R2dbc logic into CloudSqlEnvironmentPostProcessor"
mpeddada1 Dec 15, 2021
2394ad6
Introduce helper to gather properties
mpeddada1 Dec 15, 2021
a428256
remove unused class
mpeddada1 Dec 16, 2021
8bd52bf
check if ConnectionFactory is present in getEnabledDatabaseType(); cl…
mpeddada1 Dec 20, 2021
ac2334b
add more instructions to sample
mpeddada1 Dec 20, 2021
c8497b8
switch order in comment
mpeddada1 Dec 20, 2021
1fdf4c8
add test for extra condition
mpeddada1 Dec 20, 2021
a494df6
use Flux; only 2 constants in enum; use properties from pom.xml for m…
mpeddada1 Dec 28, 2021
c05e025
fix pom.xml
mpeddada1 Dec 28, 2021
1171bec
Merge branch 'main' of github.com:GoogleCloudPlatform/spring-cloud-gc…
mpeddada1 Dec 28, 2021
e9bab44
apply google java formatter
mpeddada1 Jan 21, 2022
29d1540
resolve merge conficts
mpeddada1 Jan 21, 2022
c44ca56
Merge branch 'main' of github.com:GoogleCloudPlatform/spring-cloud-gc…
mpeddada1 Jan 21, 2022
31e7ddf
more google formatting
mpeddada1 Jan 21, 2022
cf7d15c
Add property to enable r2dbc
mpeddada1 Jan 21, 2022
f4b758d
fix test
mpeddada1 Jan 21, 2022
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
25 changes: 25 additions & 0 deletions spring-cloud-gcp-autoconfigure/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,31 @@
<scope>test</scope>
</dependency>

<!--R2DBC : mySQL connector factory -->
<dependency>
<groupId>com.google.cloud.sql</groupId>
<artifactId>cloud-sql-connector-r2dbc-mysql</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>dev.miku</groupId>
<artifactId>r2dbc-mysql</artifactId>
<optional>true</optional>
</dependency>


<!--R2DBC : PostgreSQL connector factory -->
<dependency>
<groupId>com.google.cloud.sql</groupId>
<artifactId>cloud-sql-connector-r2dbc-postgres</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>io.r2dbc</groupId>
<artifactId>r2dbc-postgresql</artifactId>
<optional>true</optional>
</dependency>

<!-- Storage -->
<dependency>
<groupId>com.google.cloud</groupId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,11 @@
import org.springframework.core.env.Environment;
import org.springframework.core.env.MapPropertySource;
import org.springframework.core.io.Resource;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;

/**
* Provides Google Cloud SQL instance connectivity through Spring JDBC by providing only a
* Provides Google Cloud SQL instance connectivity through Spring JDBC and R2DBC by providing only a
* database and instance connection name.
*
* @author João André Martins
Expand All @@ -52,8 +53,7 @@
* @author Eddú Meléndez
*/
public class CloudSqlEnvironmentPostProcessor implements EnvironmentPostProcessor {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seeing now how handling both JDBC and R2DBC in one class here is not really reusing that much code, and making it less readable, maybe a separate EventPostProcessor is a better design after all. You can still re-use some code bt introducing a common AbstractCloudSqlEnvironmentPostProcessor that both would extend.

private static final Log LOGGER =
LogFactory.getLog(CloudSqlEnvironmentPostProcessor.class);
private static final Log LOGGER = LogFactory.getLog(CloudSqlEnvironmentPostProcessor.class);

@Override
public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
Expand All @@ -64,55 +64,116 @@ public void postProcessEnvironment(ConfigurableEnvironment environment, SpringAp
}

DatabaseType databaseType = getEnabledDatabaseType(environment);
R2dbcDatabaseType r2dbcDatabaseType = getEnabledR2dbcDatabaseType(environment);
if (databaseType == null && r2dbcDatabaseType == null) {
return;
}

// Bind properties without resolving Secret Manager placeholders
Binder binder = new Binder(ConfigurationPropertySources.get(environment),
new NonSecretsManagerPropertiesPlaceholdersResolver(environment),
null, null, null);

String cloudSqlPropertiesPrefix = GcpCloudSqlProperties.class.getAnnotation(ConfigurationProperties.class)
.value();
GcpCloudSqlProperties sqlProperties = binder
.bind(cloudSqlPropertiesPrefix, GcpCloudSqlProperties.class)
.orElse(new GcpCloudSqlProperties());
GcpProperties gcpProperties = binder
.bind(cloudSqlPropertiesPrefix, GcpProperties.class)
.orElse(new GcpProperties());

if (databaseType != null) {
if (LOGGER.isInfoEnabled()) {
LOGGER.info("post-processing Cloud SQL properties for + " + databaseType.name());
}

// Bind properties without resolving Secret Manager placeholders
Binder binder = new Binder(ConfigurationPropertySources.get(environment),
new NonSecretsManagerPropertiesPlaceholdersResolver(environment),
null, null, null);

String cloudSqlPropertiesPrefix = GcpCloudSqlProperties.class.getAnnotation(ConfigurationProperties.class).value();
GcpCloudSqlProperties sqlProperties = binder
.bind(cloudSqlPropertiesPrefix, GcpCloudSqlProperties.class)
.orElse(new GcpCloudSqlProperties());
GcpProperties gcpProperties = binder
.bind(cloudSqlPropertiesPrefix, GcpProperties.class)
.orElse(new GcpProperties());

CloudSqlJdbcInfoProvider cloudSqlJdbcInfoProvider = new DefaultCloudSqlJdbcInfoProvider(sqlProperties, databaseType);
applyJdbcSettings(environment, sqlProperties, databaseType);
}
else {
if (LOGGER.isInfoEnabled()) {
LOGGER.info("Default " + databaseType.name()
+ " JdbcUrl provider. Connecting to "
+ cloudSqlJdbcInfoProvider.getJdbcUrl() + " with driver "
+ cloudSqlJdbcInfoProvider.getJdbcDriverClass());
LOGGER.info("post-processing Cloud SQL properties for + " + r2dbcDatabaseType.name());
}
applyR2dbcSettings(environment, sqlProperties, r2dbcDatabaseType);
}
setCredentials(sqlProperties, gcpProperties);

// support usage metrics
CoreSocketFactory.setApplicationName("spring-cloud-gcp-sql/"
+ this.getClass().getPackage().getImplementationVersion());
}

/**
* Sets the DataSource properties such as driver class name and connection url based on
* the Cloud SQL properties specified.
* @param environment environment to post-process
* @param sqlProperties cloud sql properties
* @param databaseType enum containing MySQl and PostgreSQL information (for example, jdbc
* url template and default username)
*/
private void applyJdbcSettings(ConfigurableEnvironment environment, GcpCloudSqlProperties sqlProperties,
DatabaseType databaseType) {
CloudSqlJdbcInfoProvider cloudSqlJdbcInfoProvider = new DefaultCloudSqlJdbcInfoProvider(sqlProperties,
databaseType);
if (LOGGER.isInfoEnabled()) {
LOGGER.info("Default " + databaseType.name()
+ " JdbcUrl provider. Connecting to "
+ cloudSqlJdbcInfoProvider.getJdbcUrl() + " with driver "
+ cloudSqlJdbcInfoProvider.getJdbcDriverClass());
}

// configure default JDBC driver and username as fallback values when not specified
Map<String, Object> fallbackMap = new HashMap<>();
fallbackMap.put("spring.datasource.username", databaseType.getDefaultUsername());
fallbackMap.put("spring.datasource.driver-class-name", cloudSqlJdbcInfoProvider.getJdbcDriverClass());
environment.getPropertySources()
.addLast(new MapPropertySource("CLOUD_SQL_DATA_SOURCE_FALLBACK", fallbackMap));
// Configure default JDBC driver and username as fallback values when not specified
Map<String, Object> fallbackMap = new HashMap<>();
fallbackMap.put("spring.datasource.username", databaseType.getDefaultUsername());
fallbackMap.put("spring.datasource.driver-class-name", cloudSqlJdbcInfoProvider.getJdbcDriverClass());
environment.getPropertySources()
.addLast(new MapPropertySource("CLOUD_SQL_DATA_SOURCE_FALLBACK", fallbackMap));

// always set the spring.datasource.url property in the environment
Map<String, Object> primaryMap = new HashMap<>();
primaryMap.put("spring.datasource.url", cloudSqlJdbcInfoProvider.getJdbcUrl());
environment.getPropertySources()
.addFirst(new MapPropertySource("CLOUD_SQL_DATA_SOURCE_URL", primaryMap));
// Always set the spring.datasource.url property in the environment
Map<String, Object> primaryMap = new HashMap<>();
primaryMap.put("spring.datasource.url", cloudSqlJdbcInfoProvider.getJdbcUrl());
environment.getPropertySources()
.addFirst(new MapPropertySource("CLOUD_SQL_DATA_SOURCE_URL", primaryMap));
}

setCredentials(sqlProperties, gcpProperties);

// support usage metrics
CoreSocketFactory.setApplicationName("spring-cloud-gcp-sql/"
+ this.getClass().getPackage().getImplementationVersion());
/**
* Sets the R2DBC properties such as username and connection url based on the Cloud SQL
* properties specified.
* @param environment environment to post-process
* @param sqlProperties cloud sql properties
* @param r2dbcDatabaseType enum containing MySQl and PostgreSQL information (for example,
* r2dbc url and default username)
*/
private void applyR2dbcSettings(ConfigurableEnvironment environment, GcpCloudSqlProperties sqlProperties,
R2dbcDatabaseType r2dbcDatabaseType) {
String r2dbcUrl = createR2dbcUrl(r2dbcDatabaseType, sqlProperties);
if (LOGGER.isInfoEnabled()) {
LOGGER.info("Default " + r2dbcDatabaseType.name()
+ " R2dbcUrl provider. Connecting to "
+ r2dbcUrl);
}

// Add default username as fallback when not specified
Map<String, Object> fallbackMap = new HashMap<>();
fallbackMap.put("spring.r2dbc.username", r2dbcDatabaseType.getDefaultUsername());
environment.getPropertySources().addLast(new MapPropertySource("CLOUD_SQL_R2DBC_FALLBACK", fallbackMap));

// Always set the spring.r2dbc.url property in the environment
Map<String, Object> primaryMap = new HashMap<>();
primaryMap.put("spring.r2dbc.url", r2dbcUrl);
environment.getPropertySources().addFirst(new MapPropertySource("CLOUD_SQL_R2DBC_URL", primaryMap));
}

private DatabaseType getEnabledDatabaseType(ConfigurableEnvironment environment) {
String createR2dbcUrl(R2dbcDatabaseType databaseType, GcpCloudSqlProperties sqlProperties) {
Assert.hasText(sqlProperties.getDatabaseName(), "A database name must be provided.");
Assert.hasText(sqlProperties.getInstanceConnectionName(),
"An instance connection name must be provided in the format <PROJECT_ID>:<REGION>:<INSTANCE_ID>.");

return String.format(databaseType.getUrlTemplate(),
sqlProperties.getInstanceConnectionName(), sqlProperties.getDatabaseName());
}

DatabaseType getEnabledDatabaseType(ConfigurableEnvironment environment) {
if (Boolean.parseBoolean(environment.getProperty("spring.cloud.gcp.sql.enabled", "true"))
&& isOnClasspath("javax.sql.DataSource")
&& isOnClasspath("org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType")
Expand All @@ -129,22 +190,39 @@ && isOnClasspath("org.postgresql.Driver")) {
return null;
}

R2dbcDatabaseType getEnabledR2dbcDatabaseType(ConfigurableEnvironment environment) {
if (Boolean.parseBoolean(environment.getProperty("spring.cloud.gcp.sql.enabled", "true"))
&& isOnClasspath("com.google.cloud.sql.CredentialFactory")) {
if (isOnClasspath("com.google.cloud.sql.core.GcpConnectionFactoryProviderMysql") &&
isOnClasspath("dev.miku.r2dbc.mysql.MySqlConnectionFactoryProvider")) {
return R2dbcDatabaseType.MYSQL;
}
else if (isOnClasspath("com.google.cloud.sql.core.GcpConnectionFactoryProviderPostgres")
&& isOnClasspath("io.r2dbc.postgresql.PostgresqlConnectionFactoryProvider")) {
return R2dbcDatabaseType.POSTGRESQL;
}
}
return null;
}

private boolean isOnClasspath(String className) {
return ClassUtils.isPresent(className, null);
}

/**
* Set credentials to be used by the Google Cloud SQL socket factory.
*
* <p>The only way to pass a {@link CredentialFactory} to the socket factory is by passing a
* <p>
* The only way to pass a {@link CredentialFactory} to the socket factory is by passing a
* class name through a system property. The socket factory creates an instance of
* {@link CredentialFactory} using reflection without any arguments. Because of that, the
* credential location needs to be stored somewhere where the class can read it without
* any context. It could be possible to pass in a Spring context to
* {@link SqlCredentialFactory}, but this is a tricky solution that needs some thinking
* about.
*
* <p>If user didn't specify credentials, the socket factory already does the right thing by
* <p>
* If user didn't specify credentials, the socket factory already does the right thing by
* using the application default credentials by default. So we don't need to do anything.
*/
private void setCredentials(GcpCloudSqlProperties sqlProperties, GcpProperties gcpProperties) {
Expand Down Expand Up @@ -176,7 +254,6 @@ private void setCredentialsEncodedKeyProperty(String encodedKey) {
SqlCredentialFactory.class.getName());
}


private void setCredentialsFileProperty(Resource credentialsLocation) {
try {
// A resource might not be in the filesystem, but the Cloud SQL credential must.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
package com.google.cloud.spring.autoconfigure.sql;

/**
* Enum class containing MySQL and Postgresql constants.
* Enum class for JDBC workflow containing MySQL and Postgresql constants.
*
* @author João André Martins
* @author Chengyuan Zhao
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* Copyright 2021-2022 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 com.google.cloud.spring.autoconfigure.sql;

/**
* Enum class for R2DBC workflow containing MySQL and PostgreSQL constants.
*/
public enum R2dbcDatabaseType {

/**
* MySQL constants.
*/
MYSQL("r2dbc:gcp:mysql://%s/%s", "root"),

/**
* Postgresql constants.
*/
POSTGRESQL("r2dbc:gcp:postgres://%s/%s", "postgres");

private final String urlTemplate;

private final String defaultUsername;

R2dbcDatabaseType(String urlTemplate, String defaultUsername) {
this.urlTemplate = urlTemplate;
this.defaultUsername = defaultUsername;
}

public String getUrlTemplate() {
return this.urlTemplate;
}

public String getDefaultUsername() {
return defaultUsername;
}
}
Loading