Skip to content

Commit 936e1c1

Browse files
committed
chore: Use domain name from JDBC URL for Postgres and Mysql, Part of #2043.
When connecting to a Cloud SQL database using a domain name like `db.example.com`, you may now set the domain name in the JDBC URL. For example, you can use a URL like "jdbc:mysql://db.example.com/my-schema?socketFactory=socketFactory=com.google.cloud.sql.mysql.SocketFactory" The socket factory will detect "db.example.com" and look up the TXT record to resolve the instance name. See #2043 for the whole feature definition.
1 parent a5f924b commit 936e1c1

File tree

13 files changed

+301
-19
lines changed

13 files changed

+301
-19
lines changed

core/src/main/java/com/google/cloud/sql/ConnectorConfig.java

+5-4
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ public class ConnectorConfig {
3434
private final String adminRootUrl;
3535
private final String adminServicePath;
3636
private final Supplier<GoogleCredentials> googleCredentialsSupplier;
37-
private final Function<String,String> instanceNameResolver;
37+
private final Function<String, String> instanceNameResolver;
3838
private final GoogleCredentials googleCredentials;
3939
private final String googleCredentialsPath;
4040
private final String adminQuotaProject;
@@ -53,7 +53,7 @@ private ConnectorConfig(
5353
String adminQuotaProject,
5454
String universeDomain,
5555
RefreshStrategy refreshStrategy,
56-
Function<String,String> instanceNameResolver) {
56+
Function<String, String> instanceNameResolver) {
5757
this.targetPrincipal = targetPrincipal;
5858
this.delegates = delegates;
5959
this.adminRootUrl = adminRootUrl;
@@ -162,7 +162,7 @@ public static class Builder {
162162
private String adminQuotaProject;
163163
private String universeDomain;
164164
private RefreshStrategy refreshStrategy = RefreshStrategy.BACKGROUND;
165-
private Function<String,String> instanceNameResolver;
165+
private Function<String, String> instanceNameResolver;
166166

167167
public Builder withTargetPrincipal(String targetPrincipal) {
168168
this.targetPrincipal = targetPrincipal;
@@ -214,7 +214,8 @@ public Builder withRefreshStrategy(RefreshStrategy refreshStrategy) {
214214
this.refreshStrategy = refreshStrategy;
215215
return this;
216216
}
217-
public Builder withInstanceNameResolver(Function<String,String> instanceNameResolver) {
217+
218+
public Builder withInstanceNameResolver(Function<String, String> instanceNameResolver) {
218219
this.instanceNameResolver = instanceNameResolver;
219220
return this;
220221
}

core/src/main/java/com/google/cloud/sql/InstanceNameResolver.java

-5
This file was deleted.

core/src/main/java/com/google/cloud/sql/core/ConnectionConfig.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -327,7 +327,8 @@ public Builder withIpTypes(List<IpType> ipTypes) {
327327
this.ipTypes = ipTypes;
328328
return this;
329329
}
330-
/** Set domainName as. */
330+
331+
/** Set domainName. */
331332
public Builder withDomainName(String domainName) {
332333
this.domainName = domainName;
333334
return this;

core/src/main/java/com/google/cloud/sql/core/Connector.java

+28-3
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
import com.google.cloud.sql.ConnectorConfig;
2020
import com.google.cloud.sql.CredentialFactory;
2121
import com.google.cloud.sql.RefreshStrategy;
22-
import com.google.common.base.Strings;
2322
import com.google.common.util.concurrent.ListenableFuture;
2423
import com.google.common.util.concurrent.ListeningScheduledExecutorService;
2524
import java.io.File;
@@ -141,9 +140,11 @@ Socket connect(ConnectionConfig config, long timeoutMs) throws IOException {
141140
}
142141
}
143142

144-
ConnectionInfoCache getConnection(ConnectionConfig config) {
143+
ConnectionInfoCache getConnection(final ConnectionConfig config) {
144+
final ConnectionConfig updatedConfig = resolveConnectionName(config);
145+
145146
ConnectionInfoCache instance =
146-
instances.computeIfAbsent(config, k -> createConnectionInfo(config));
147+
instances.computeIfAbsent(updatedConfig, k -> createConnectionInfo(updatedConfig));
147148

148149
// If the client certificate has expired (as when the computer goes to
149150
// sleep, and the refresh cycle cannot run), force a refresh immediately.
@@ -155,6 +156,30 @@ ConnectionInfoCache getConnection(ConnectionConfig config) {
155156
return instance;
156157
}
157158

159+
private ConnectionConfig resolveConnectionName(ConnectionConfig config) {
160+
// If domainName is not set, return the original configuration unmodified.
161+
if (config.getDomainName() == null || config.getDomainName().length() == 0) {
162+
return config;
163+
}
164+
165+
// Resolve the domain name.
166+
String unresolvedName = config.getDomainName();
167+
try {
168+
Function<String, String> resolver = config.getConnectorConfig().getInstanceNameResolver();
169+
if (resolver != null) {
170+
return config.withCloudSqlInstance(resolver.apply(unresolvedName));
171+
} else {
172+
throw new IllegalStateException(
173+
"Can't resolve domain " + unresolvedName + ". ConnectorConfig.resolver is not set.");
174+
}
175+
} catch (IllegalArgumentException e) {
176+
throw new IllegalArgumentException(
177+
String.format(
178+
"Cloud SQL connection name is invalid: \"%s\"", config.getCloudSqlInstance()),
179+
e);
180+
}
181+
}
182+
158183
private ConnectionInfoCache createConnectionInfo(ConnectionConfig config) {
159184
logger.debug(
160185
String.format("[%s] Connection info added to cache.", config.getCloudSqlInstance()));

core/src/test/java/com/google/cloud/sql/ConnectorConfigTest.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -426,7 +426,8 @@ public void testHashCode() {
426426
wantGoogleCredentialsPath,
427427
wantAdminQuotaProject,
428428
null, // universeDomain
429-
wantRefreshStrategy // refreshStrategy
429+
wantRefreshStrategy, // refreshStrategy
430+
null // instanceNameResolver
430431
));
431432
}
432433
}

core/src/test/java/com/google/cloud/sql/core/ConnectorTest.java

+44
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,50 @@ public void create_successfulPrivateConnection() throws IOException, Interrupted
129129
assertThat(readLine(socket)).isEqualTo(SERVER_MESSAGE);
130130
}
131131

132+
@Test
133+
public void create_successfulPublicConnectionWithDomainName()
134+
throws IOException, InterruptedException {
135+
FakeSslServer sslServer = new FakeSslServer();
136+
ConnectionConfig config =
137+
new ConnectionConfig.Builder()
138+
.withDomainName("db.example.com")
139+
.withIpTypes("PRIMARY")
140+
.withConnectorConfig(
141+
new ConnectorConfig.Builder()
142+
.withInstanceNameResolver((domainName) -> "myProject:myRegion:myInstance")
143+
.build())
144+
.build();
145+
146+
int port = sslServer.start(PUBLIC_IP);
147+
148+
Connector connector = newConnector(config.getConnectorConfig(), port);
149+
150+
Socket socket = connector.connect(config, TEST_MAX_REFRESH_MS);
151+
152+
assertThat(readLine(socket)).isEqualTo(SERVER_MESSAGE);
153+
}
154+
155+
@Test
156+
public void create_throwsErrorForDomainNameWithNoResolver()
157+
throws IOException, InterruptedException {
158+
// The server TLS certificate matches myProject:myRegion:myInstance
159+
FakeSslServer sslServer = new FakeSslServer();
160+
ConnectionConfig config =
161+
new ConnectionConfig.Builder()
162+
.withDomainName("db.example.com")
163+
.withIpTypes("PRIMARY")
164+
.build();
165+
166+
int port = sslServer.start(PUBLIC_IP);
167+
168+
Connector connector = newConnector(config.getConnectorConfig(), port);
169+
IllegalStateException ex =
170+
assertThrows(
171+
IllegalStateException.class, () -> connector.connect(config, TEST_MAX_REFRESH_MS));
172+
173+
assertThat(ex).hasMessageThat().contains("ConnectorConfig.resolver is not set");
174+
}
175+
132176
@Test
133177
public void create_successfulPublicConnection() throws IOException, InterruptedException {
134178
FakeSslServer sslServer = new FakeSslServer();

jdbc/mariadb/src/main/java/com/google/cloud/sql/mariadb/SocketFactory.java

+3-1
Original file line numberDiff line numberDiff line change
@@ -37,20 +37,22 @@ public class SocketFactory extends ConfigurableSocketFactory {
3737
}
3838

3939
private Configuration conf;
40+
private String host;
4041

4142
public SocketFactory() {}
4243

4344
@Override
4445
public void setConfiguration(Configuration conf, String host) {
4546
// Ignore the hostname
4647
this.conf = conf;
48+
this.host = host;
4749
}
4850

4951
@Override
5052
public Socket createSocket() throws IOException {
5153
try {
5254
return InternalConnectorRegistry.getInstance()
53-
.connect(ConnectionConfig.fromConnectionProperties(conf.nonMappedOptions()));
55+
.connect(ConnectionConfig.fromConnectionProperties(conf.nonMappedOptions(), host));
5456
} catch (InterruptedException e) {
5557
throw new RuntimeException(e);
5658
}

jdbc/mysql-j-8/src/main/java/com/google/cloud/sql/mysql/SocketFactory.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ public <T extends Closeable> T connect(
6060
T socket =
6161
(T)
6262
InternalConnectorRegistry.getInstance()
63-
.connect(ConnectionConfig.fromConnectionProperties(props));
63+
.connect(ConnectionConfig.fromConnectionProperties(props, host));
6464
return socket;
6565
}
6666

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/*
2+
* Copyright 2024 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.cloud.sql.mysql;
18+
19+
import static com.google.common.truth.Truth.assertThat;
20+
import static com.google.common.truth.Truth.assertWithMessage;
21+
22+
import com.google.cloud.sql.ConnectorConfig;
23+
import com.google.cloud.sql.ConnectorRegistry;
24+
import com.google.common.collect.ImmutableList;
25+
import com.zaxxer.hikari.HikariConfig;
26+
import com.zaxxer.hikari.HikariDataSource;
27+
import java.sql.*;
28+
import java.util.ArrayList;
29+
import java.util.List;
30+
import java.util.Properties;
31+
import java.util.concurrent.TimeUnit;
32+
import org.junit.Before;
33+
import org.junit.BeforeClass;
34+
import org.junit.Rule;
35+
import org.junit.Test;
36+
import org.junit.rules.Timeout;
37+
import org.junit.runner.RunWith;
38+
import org.junit.runners.JUnit4;
39+
40+
@RunWith(JUnit4.class)
41+
public class JdbcMysqlJ8DomainNameIntegrationTests {
42+
43+
private static final String CONNECTION_NAME = System.getenv("MYSQL_CONNECTION_NAME");
44+
private static final String DB_NAME = System.getenv("MYSQL_DB");
45+
private static final String DB_USER = System.getenv("MYSQL_USER");
46+
private static final String DB_PASSWORD = System.getenv("MYSQL_PASS");
47+
private static final ImmutableList<String> requiredEnvVars =
48+
ImmutableList.of("MYSQL_USER", "MYSQL_PASS", "MYSQL_DB", "MYSQL_CONNECTION_NAME");
49+
@Rule public Timeout globalTimeout = new Timeout(80, TimeUnit.SECONDS);
50+
private HikariDataSource connectionPool;
51+
52+
@BeforeClass
53+
public static void checkEnvVars() {
54+
// Check that required env vars are set
55+
requiredEnvVars.forEach(
56+
(varName) ->
57+
assertWithMessage(
58+
String.format(
59+
"Environment variable '%s' must be set to perform these tests.", varName))
60+
.that(System.getenv(varName))
61+
.isNotEmpty());
62+
}
63+
64+
@Before
65+
public void setUpPool() throws SQLException {
66+
// Set up URL parameters
67+
String jdbcURL = String.format("jdbc:mysql://db.example.com/%s", DB_NAME);
68+
Properties connProps = new Properties();
69+
connProps.setProperty("user", DB_USER);
70+
connProps.setProperty("password", DB_PASSWORD);
71+
connProps.setProperty("socketFactory", "com.google.cloud.sql.mysql.SocketFactory");
72+
73+
// Register a resolver that resolves `db.example.com` to the connection name
74+
connProps.setProperty("cloudSqlNamedConnector", "resolver-test");
75+
ConnectorRegistry.register(
76+
"resolver-test",
77+
new ConnectorConfig.Builder()
78+
.withInstanceNameResolver((n) -> "db.example.com".equals(n) ? CONNECTION_NAME : null)
79+
.build());
80+
81+
// Initialize connection pool
82+
HikariConfig config = new HikariConfig();
83+
config.setJdbcUrl(jdbcURL);
84+
config.setDataSourceProperties(connProps);
85+
config.setConnectionTimeout(10000); // 10s
86+
87+
this.connectionPool = new HikariDataSource(config);
88+
}
89+
90+
@Test
91+
public void pooledConnectionTest() throws SQLException {
92+
93+
List<Timestamp> rows = new ArrayList<>();
94+
try (Connection conn = connectionPool.getConnection()) {
95+
try (PreparedStatement selectStmt = conn.prepareStatement("SELECT NOW() as TS")) {
96+
ResultSet rs = selectStmt.executeQuery();
97+
while (rs.next()) {
98+
rows.add(rs.getTimestamp("TS"));
99+
}
100+
}
101+
}
102+
assertThat(rows.size()).isEqualTo(1);
103+
}
104+
}

jdbc/mysql-j-8/src/test/java/com/google/cloud/sql/mysql/JdbcMysqlJ8IntegrationTests.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ public static void checkEnvVars() {
6262
@Before
6363
public void setUpPool() throws SQLException {
6464
// Set up URL parameters
65-
String jdbcURL = String.format("jdbc:mysql:///%s", DB_NAME);
65+
String jdbcURL = String.format("jdbc:mysql://db.example.com/%s", DB_NAME);
6666
Properties connProps = new Properties();
6767
connProps.setProperty("user", DB_USER);
6868
connProps.setProperty("password", DB_PASSWORD);

jdbc/postgres/src/main/java/com/google/cloud/sql/postgres/SocketFactory.java

+6-1
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ public class SocketFactory extends javax.net.SocketFactory {
3838
private static final String DEPRECATED_SOCKET_ARG = "SocketFactoryArg";
3939
private static final String POSTGRES_SUFFIX = "/.s.PGSQL.5432";
4040

41+
/** The connection property containing the hostname from the JDBC url. */
42+
private static final String POSTGRES_HOST_PROP = "PGHOST";
43+
4144
private final Properties props;
4245

4346
static {
@@ -78,7 +81,9 @@ private static Properties createDefaultProperties(String instanceName) {
7881
public Socket createSocket() throws IOException {
7982
try {
8083
return InternalConnectorRegistry.getInstance()
81-
.connect(ConnectionConfig.fromConnectionProperties(props));
84+
.connect(
85+
ConnectionConfig.fromConnectionProperties(
86+
props, props.getProperty(POSTGRES_HOST_PROP)));
8287
} catch (InterruptedException e) {
8388
throw new RuntimeException(e);
8489
}

0 commit comments

Comments
 (0)