Skip to content

Commit

Permalink
feat: Add all additional Cloud SQL Java Connector parameters to the s…
Browse files Browse the repository at this point in the history
…pring configuration. (#3286)

This adds JDBC needed for lazy refresh, service account impersonation, universe domains, and alternate 
SQL Admin API endpoints to the JDBC configuration for a Cloud SQL JDBC connection. 

This is related to GoogleCloudPlatform/cloud-sql-jdbc-socket-factory#2065.
  • Loading branch information
hessjcg authored Nov 1, 2024
1 parent 6c1b039 commit f2212d3
Show file tree
Hide file tree
Showing 4 changed files with 193 additions and 2 deletions.
7 changes: 7 additions & 0 deletions docs/src/main/asciidoc/sql.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -54,12 +58,49 @@ public String getJdbcUrl() {
this.properties.getDatabaseName(),
this.properties.getInstanceConnectionName());

// Build additional JDBC url parameters from the configuration.
Map<String, String> 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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit f2212d3

Please sign in to comment.