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 all 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 and driver -->
<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 and driver -->
<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 @@ -27,14 +27,8 @@
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.boot.context.properties.bind.PlaceholdersResolver;
import org.springframework.boot.context.properties.bind.PropertySourcesPlaceholdersResolver;
import org.springframework.boot.context.properties.source.ConfigurationPropertySources;
import org.springframework.boot.env.EnvironmentPostProcessor;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.Environment;
import org.springframework.core.env.MapPropertySource;
import org.springframework.core.io.Resource;
import org.springframework.util.ClassUtils;
Expand Down Expand Up @@ -62,24 +56,8 @@ public void postProcessEnvironment(
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());

PropertiesRetriever propertiesRetriever = new PropertiesRetriever(environment);
GcpCloudSqlProperties sqlProperties = propertiesRetriever.getCloudSqlProperties();
CloudSqlJdbcInfoProvider cloudSqlJdbcInfoProvider =
new DefaultCloudSqlJdbcInfoProvider(sqlProperties, databaseType);
if (LOGGER.isInfoEnabled()) {
Expand Down Expand Up @@ -108,7 +86,7 @@ public void postProcessEnvironment(
.getPropertySources()
.addFirst(new MapPropertySource("CLOUD_SQL_DATA_SOURCE_URL", primaryMap));

setCredentials(sqlProperties, gcpProperties);
setCredentials(sqlProperties, propertiesRetriever.getGcpProperties());

// support usage metrics
CoreSocketFactory.setApplicationName(
Expand Down Expand Up @@ -191,22 +169,4 @@ private void setCredentialsFileProperty(Resource credentialsLocation) {
LOGGER.info("Error reading Cloud SQL credentials file.", ioe);
}
}

private static class NonSecretsManagerPropertiesPlaceholdersResolver
implements PlaceholdersResolver {
private PlaceholdersResolver resolver;

NonSecretsManagerPropertiesPlaceholdersResolver(Environment environment) {
this.resolver = new PropertySourcesPlaceholdersResolver(environment);
}

@Override
public Object resolvePlaceholders(Object value) {
if (value.toString().contains("sm://")) {
return value;
} else {
return resolver.resolvePlaceholders(value);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ public enum DatabaseType {
"jdbc:mysql://google/%s?"
+ "socketFactory=com.google.cloud.sql.mysql.SocketFactory"
+ "&cloudSqlInstance=%s",
"r2dbc:gcp:mysql://%s/%s",
"root"),

/** Postgresql constants. */
Expand All @@ -32,17 +33,25 @@ public enum DatabaseType {
"jdbc:postgresql://google/%s?"
+ "socketFactory=com.google.cloud.sql.postgres.SocketFactory"
+ "&cloudSqlInstance=%s",
"r2dbc:gcp:postgres://%s/%s",
"postgres");

private final String jdbcDriverName;

private final String jdbcUrlTemplate;

private final String r2dbcUrlTemplate;

private final String defaultUsername;

DatabaseType(String jdbcDriverName, String jdbcUrlTemplate, String defaultUsername) {
DatabaseType(
String jdbcDriverName,
String jdbcUrlTemplate,
String r2dbcUrlTemplate,
String defaultUsername) {
this.jdbcDriverName = jdbcDriverName;
this.jdbcUrlTemplate = jdbcUrlTemplate;
this.r2dbcUrlTemplate = r2dbcUrlTemplate;
this.defaultUsername = defaultUsername;
}

Expand All @@ -54,6 +63,10 @@ public String getJdbcUrlTemplate() {
return this.jdbcUrlTemplate;
}

public String getR2dbcUrlTemplate() {
return this.r2dbcUrlTemplate;
}

public String getDefaultUsername() {
return defaultUsername;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*
* 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;

import com.google.cloud.spring.autoconfigure.core.GcpProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.boot.context.properties.bind.PlaceholdersResolver;
import org.springframework.boot.context.properties.bind.PropertySourcesPlaceholdersResolver;
import org.springframework.boot.context.properties.source.ConfigurationPropertySources;
import org.springframework.core.env.Environment;

/**
* Helper class to derive Cloud SQL and GCP properties from the user configuration
* (application.properties, for example).
*/
class PropertiesRetriever {

private Binder binder;

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

GcpCloudSqlProperties getCloudSqlProperties() {
String cloudSqlPropertiesPrefix =
GcpCloudSqlProperties.class.getAnnotation(ConfigurationProperties.class).value();
return this.binder
.bind(cloudSqlPropertiesPrefix, GcpCloudSqlProperties.class)
.orElse(new GcpCloudSqlProperties());
}

GcpProperties getGcpProperties() {
String gcpPropertiesPrefix =
GcpProperties.class.getAnnotation(ConfigurationProperties.class).value();
return this.binder.bind(gcpPropertiesPrefix, GcpProperties.class).orElse(new GcpProperties());
}

private static class NonSecretsManagerPropertiesPlaceholdersResolver
implements PlaceholdersResolver {
private PlaceholdersResolver resolver;

NonSecretsManagerPropertiesPlaceholdersResolver(Environment environment) {
this.resolver = new PropertySourcesPlaceholdersResolver(environment);
}

@Override
public Object resolvePlaceholders(Object value) {
if (value.toString().contains("sm://")) {
return value;
} else {
return resolver.resolvePlaceholders(value);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/*
* 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;

import java.util.HashMap;
import java.util.Map;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.env.EnvironmentPostProcessor;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MapPropertySource;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;

/**
* Builds connection string for Cloud SQL through Spring R2DBC by requiring only a database and
* instance connection name.
*/
public class R2dbcCloudSqlEnvironmentPostProcessor implements EnvironmentPostProcessor {
private static final Log LOGGER = LogFactory.getLog(R2dbcCloudSqlEnvironmentPostProcessor.class);

@Override
public void postProcessEnvironment(
ConfigurableEnvironment environment, SpringApplication application) {
if (environment.getPropertySources().contains("bootstrap")) {
// Do not run in the bootstrap phase as the user configuration is not available yet
return;
}

DatabaseType databaseType = getEnabledDatabaseType(environment);
if (databaseType != null) {
PropertiesRetriever propertiesRetriever = new PropertiesRetriever(environment);
String r2dbcUrl = createUrl(databaseType, propertiesRetriever.getCloudSqlProperties());
if (LOGGER.isInfoEnabled()) {
LOGGER.info(
"Default " + databaseType.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", databaseType.getDefaultUsername());
environment
.getPropertySources()
.addLast(new MapPropertySource("CLOUD_SQL_R2DBC_USERNAME", fallbackMap));

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

String createUrl(DatabaseType 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.getR2dbcUrlTemplate(),
sqlProperties.getInstanceConnectionName(),
sqlProperties.getDatabaseName());
}

/**
* Returns {@link DatabaseType} constant based on whether mySQL or postgreSQL R2DBC driver and
* connector dependencies are present on the classpath. Returns null if Cloud SQL is not enabled
* in Spring Cloud GCP, CredentialFactory is not present or ConnectionFactory (which is used to
* enable Spring R2DBC auto-configuration) is not present.
*
* @param environment environment to post-process
* @return database type
*/
DatabaseType getEnabledDatabaseType(ConfigurableEnvironment environment) {
if (isR2dbcEnabled(environment)
&& isOnClasspath("com.google.cloud.sql.CredentialFactory")
&& isOnClasspath("io.r2dbc.spi.ConnectionFactory")) {
if (isOnClasspath("com.google.cloud.sql.core.GcpConnectionFactoryProviderMysql")
&& isOnClasspath("dev.miku.r2dbc.mysql.MySqlConnectionFactoryProvider")) {
return DatabaseType.MYSQL;
} else if (isOnClasspath("com.google.cloud.sql.core.GcpConnectionFactoryProviderPostgres")
&& isOnClasspath("io.r2dbc.postgresql.PostgresqlConnectionFactoryProvider")) {
return DatabaseType.POSTGRESQL;
}
}
return null;
}

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

private boolean isR2dbcEnabled(ConfigurableEnvironment environment) {
return Boolean.parseBoolean(environment.getProperty("spring.cloud.gcp.sql.enabled", "true"))
&& Boolean.parseBoolean(
environment.getProperty("spring.cloud.gcp.sql.r2dbc.enabled", "true"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,5 @@ com.google.cloud.spring.autoconfigure.secretmanager.GcpSecretManagerBootstrapCon

org.springframework.boot.env.EnvironmentPostProcessor=\
com.google.cloud.spring.autoconfigure.sql.CloudSqlEnvironmentPostProcessor,\
com.google.cloud.spring.autoconfigure.sql.R2dbcCloudSqlEnvironmentPostProcessor,\
com.google.cloud.spring.autoconfigure.secretmanager.GcpSecretManagerEnvironmentPostProcessor
Loading