diff --git a/docs/src/main/asciidoc/sql.adoc b/docs/src/main/asciidoc/sql.adoc index 94edde0983..fe58f1875c 100644 --- a/docs/src/main/asciidoc/sql.adoc +++ b/docs/src/main/asciidoc/sql.adoc @@ -177,6 +177,13 @@ Used to authenticate and authorize new connections to a Google Cloud SQL instanc Used to authenticate and authorize new connections to a Google Cloud SQL instance. | No | Default credentials provided by the Spring Framework on Google Cloud Core Starter | `spring.cloud.gcp.sql.enableIamAuth` | Specifies whether to enable IAM database authentication (PostgreSQL only). | No | `False` +| `spring.cloud.gcp.sql.refreshStrategy` | The strategy used to refresh the Google Cloud SQL authentication tokens. Valid values: `background` - refresh credentials using a background thread, `lazy` - refresh credentials during connection attempts. | No | "background" +| `spring.cloud.gcp.sql.targetPrincipal` | The service account to impersonate when connecting to the database and database admin API. | No | (empty) +| `spring.cloud.gcp.sql.delegates` | A comma-separated list of service accounts delegates. | No | (empty) +| `spring.cloud.gcp.sql.universeDomain` | A universe domain for the TPC environment. | No | "googleapis.com" +| `spring.cloud.gcp.sql.adminRootUrl` | An alternate root url for the Cloud SQL admin API. | No | (empty) +| `spring.cloud.gcp.sql.adminServicePath` | An alternate path to the SQL Admin API endpoint. | No | (empty) +| `spring.cloud.gcp.sql.adminQuotaProject` | A project ID for quota and billing. | No | (empty) |=== === Troubleshooting tips diff --git a/spring-cloud-gcp-autoconfigure/src/main/java/com/google/cloud/spring/autoconfigure/sql/DefaultCloudSqlJdbcInfoProvider.java b/spring-cloud-gcp-autoconfigure/src/main/java/com/google/cloud/spring/autoconfigure/sql/DefaultCloudSqlJdbcInfoProvider.java index 1143b32998..a1234471b2 100644 --- a/spring-cloud-gcp-autoconfigure/src/main/java/com/google/cloud/spring/autoconfigure/sql/DefaultCloudSqlJdbcInfoProvider.java +++ b/spring-cloud-gcp-autoconfigure/src/main/java/com/google/cloud/spring/autoconfigure/sql/DefaultCloudSqlJdbcInfoProvider.java @@ -16,6 +16,10 @@ package com.google.cloud.spring.autoconfigure.sql; +import java.net.URLEncoder; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.stream.Collectors; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -54,12 +58,49 @@ public String getJdbcUrl() { this.properties.getDatabaseName(), this.properties.getInstanceConnectionName()); + // Build additional JDBC url parameters from the configuration. + Map urlParams = new LinkedHashMap<>(); if (StringUtils.hasText(properties.getIpTypes())) { - jdbcUrl += "&ipTypes=" + properties.getIpTypes(); + urlParams.put("ipTypes", properties.getIpTypes()); } if (properties.isEnableIamAuth()) { - jdbcUrl += "&enableIamAuth=true&sslmode=disable"; + urlParams.put("enableIamAuth", "true"); + urlParams.put("sslmode", "disable"); + } + if (StringUtils.hasText(properties.getTargetPrincipal())) { + urlParams.put("cloudSqlTargetPrincipal", properties.getTargetPrincipal()); + } + if (StringUtils.hasText(properties.getDelegates())) { + urlParams.put("cloudSqlDelegates", properties.getDelegates()); + } + if (StringUtils.hasText(properties.getAdminRootUrl())) { + urlParams.put("cloudSqlAdminRootUrl", properties.getAdminRootUrl()); + } + if (StringUtils.hasText(properties.getAdminServicePath())) { + urlParams.put("cloudSqlAdminServicePath", properties.getAdminServicePath()); + } + if (StringUtils.hasText(properties.getAdminQuotaProject())) { + urlParams.put("cloudSqlAdminQuotaProject", properties.getAdminQuotaProject()); + } + if (StringUtils.hasText(properties.getUniverseDomain())) { + urlParams.put("cloudSqlUniverseDomain", properties.getUniverseDomain()); + } + if (StringUtils.hasText(properties.getRefreshStrategy())) { + urlParams.put("cloudSqlRefreshStrategy", properties.getRefreshStrategy()); + } + + // Convert map to a string of url parameters + String urlParamsString = + urlParams.entrySet().stream() + .map( + entry -> + URLEncoder.encode(entry.getKey()) + "=" + URLEncoder.encode(entry.getValue())) + .collect(Collectors.joining("&")); + + // Append url parameters to the JDBC URL. + if (StringUtils.hasText(urlParamsString)) { + jdbcUrl = jdbcUrl + "&" + urlParamsString; } return jdbcUrl; diff --git a/spring-cloud-gcp-autoconfigure/src/main/java/com/google/cloud/spring/autoconfigure/sql/GcpCloudSqlProperties.java b/spring-cloud-gcp-autoconfigure/src/main/java/com/google/cloud/spring/autoconfigure/sql/GcpCloudSqlProperties.java index d3f8a08bad..9301879ce4 100644 --- a/spring-cloud-gcp-autoconfigure/src/main/java/com/google/cloud/spring/autoconfigure/sql/GcpCloudSqlProperties.java +++ b/spring-cloud-gcp-autoconfigure/src/main/java/com/google/cloud/spring/autoconfigure/sql/GcpCloudSqlProperties.java @@ -36,6 +36,44 @@ public class GcpCloudSqlProperties { /** Specifies whether to enable IAM database authentication (PostgreSQL only). */ private boolean enableIamAuth; + /** + * The target principal to use for service account impersonation. Corresponds to + * Cloud SQL Java Connector JDBC property cloudSqlTargetPrincipal + */ + private String targetPrincipal; + + /** + * The chain of delegated service accounts to use for service account impersonation. + * Corresponds to Cloud SQL Java Connector JDBC property cloudSqlDelegates + */ + private String delegates; + + /** + * The alternate admin root url for the Cloud SQL Admin API. + * Corresponds to Cloud SQL Java Connector JDBC property cloudSqlAdminRootUrl. + */ + private String adminRootUrl; + /** + * The alternate service path for the Cloud SQL Admin API + * Corresponds to Cloud SQL Java Connector JDBC property cloudSqlAdminServicePath. + */ + private String adminServicePath; + /** + * The quota project to use for API requests. + * Corresponds to Cloud SQL Java Connector JDBC property cloudSqlAdminQuotaProject + */ + private String adminQuotaProject; + /** + * The universe domain to use for API requests + * Corresponds to Cloud SQL Java Connector JDBC property cloudSqlUniverseDomain + */ + private String universeDomain; + /** + * The refresh strategy to use for API requests + * Corresponds to Cloud SQL Java Connector JDBC property cloudSqlRefreshStrategy + */ + private String refreshStrategy; + public String getDatabaseName() { return this.databaseName; @@ -76,4 +114,60 @@ public boolean isEnableIamAuth() { public void setEnableIamAuth(boolean enableIamAuth) { this.enableIamAuth = enableIamAuth; } + + public String getTargetPrincipal() { + return targetPrincipal; + } + + public void setTargetPrincipal(String targetPrincipal) { + this.targetPrincipal = targetPrincipal; + } + + public String getDelegates() { + return delegates; + } + + public void setDelegates(String delegates) { + this.delegates = delegates; + } + + public String getAdminRootUrl() { + return adminRootUrl; + } + + public void setAdminRootUrl(String adminRootUrl) { + this.adminRootUrl = adminRootUrl; + } + + public String getAdminServicePath() { + return adminServicePath; + } + + public void setAdminServicePath(String adminServicePath) { + this.adminServicePath = adminServicePath; + } + + public String getAdminQuotaProject() { + return adminQuotaProject; + } + + public void setAdminQuotaProject(String adminQuotaProject) { + this.adminQuotaProject = adminQuotaProject; + } + + public String getUniverseDomain() { + return universeDomain; + } + + public void setUniverseDomain(String universeDomain) { + this.universeDomain = universeDomain; + } + + public String getRefreshStrategy() { + return refreshStrategy; + } + + public void setRefreshStrategy(String refreshStrategy) { + this.refreshStrategy = refreshStrategy; + } } diff --git a/spring-cloud-gcp-autoconfigure/src/test/java/com/google/cloud/spring/autoconfigure/sql/CloudSqlEnvironmentPostProcessorTests.java b/spring-cloud-gcp-autoconfigure/src/test/java/com/google/cloud/spring/autoconfigure/sql/CloudSqlEnvironmentPostProcessorTests.java index fa06d2d44a..f3c1300fcc 100644 --- a/spring-cloud-gcp-autoconfigure/src/test/java/com/google/cloud/spring/autoconfigure/sql/CloudSqlEnvironmentPostProcessorTests.java +++ b/spring-cloud-gcp-autoconfigure/src/test/java/com/google/cloud/spring/autoconfigure/sql/CloudSqlEnvironmentPostProcessorTests.java @@ -22,6 +22,7 @@ import com.google.api.gax.core.NoCredentialsProvider; import com.zaxxer.hikari.HikariDataSource; import javax.sql.DataSource; +import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.Test; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.AutoConfigurations; @@ -122,6 +123,54 @@ void testCloudSqlDataSourceWithIgnoredProvidedUrl() { }); } + void testCloudSqlSpringDatasourceWithAllOptions() { + this.contextRunner + .withPropertyValues( + "spring.cloud.gcp.sql.admin-quota-project=someAdminQuotaProjectValue", + "spring.cloud.gcp.sql.admin-root-url=someAdminRootUrlValue", + "spring.cloud.gcp.sql.admin-service-path=someAdminServicePathValue", + "spring.cloud.gcp.sql.database-name=test-database", + "spring.cloud.gcp.sql.delegates=delegate1,delegate2", + "spring.cloud.gcp.sql.enable-iam-auth=true", + "spring.cloud.gcp.sql.instance-connection-name=tubular-bells:singapore:test-instance", + "spring.cloud.gcp.sql.ip-types=PRIVATE", + "spring.cloud.gcp.sql.refresh-strategy=lazy", + "spring.cloud.gcp.sql.target-principal=target-principal", + "spring.cloud.gcp.sql.universe-domain=someUniverseDomainValue", + "spring.datasource.username=foo", + "spring.datasource.password=bar") + .run( + context -> { + HikariDataSource dataSource = (HikariDataSource) context.getBean(DataSource.class); + assertThat(dataSource) + .returns("com.mysql.cj.jdbc.Driver", HikariDataSource::getDriverClassName) + .returns("foo", HikariDataSource::getUsername) + .returns("bar", HikariDataSource::getPassword) + .extracting(HikariDataSource::getJdbcUrl) + .asInstanceOf(InstanceOfAssertFactories.STRING) + .startsWith( + "jdbc:mysql://google/test-database?" + + "socketFactory=com.google.cloud.sql.mysql.SocketFactory") + .satisfies( + jdbcUrl -> + assertThat(jdbcUrl.substring(jdbcUrl.indexOf('&') + 1).split("&")) + .containsExactlyInAnyOrder( + "cloudSqlAdminQuotaProject=someAdminQuotaProjectValue", + "cloudSqlAdminRootUrl=someAdminRootUrlValue", + "cloudSqlAdminServicePath=someAdminServicePathValue", + "cloudSqlDelegates=delegate1%2Cdelegate2", + "cloudSqlInstance=tubular-bells:singapore:test-instance", + "cloudSqlRefreshStrategy=lazy", + "cloudSqlTargetPrincipal=target-principal", + "cloudSqlUniverseDomain=someUniverseDomainValue", + "enableIamAuth=true", + "ipTypes=PRIVATE", + "sslmode=disable")); + assertThat(getSpringDatasourceDriverClassName(context)) + .matches("com.mysql.cj.jdbc.Driver"); + }); + } + @Test void testCloudSqlAppEngineDataSourceDefaultUserNameMySqlTest() { this.contextRunner