Skip to content

Commit 11eec88

Browse files
committed
feat: Automatically configure connections using DNS. Part of #2043.
1 parent e7f0289 commit 11eec88

10 files changed

+277
-35
lines changed

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

+12
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,18 @@ public ConnectionConfig withConnectorConfig(ConnectorConfig config) {
200200
config);
201201
}
202202

203+
/** Creates a new instance of the ConnectionConfig with an updated cloudSqlInstance. */
204+
public ConnectionConfig withCloudSqlInstance(String newCloudSqlInstance) {
205+
return new ConnectionConfig(
206+
newCloudSqlInstance,
207+
namedConnector,
208+
unixSocketPath,
209+
ipTypes,
210+
authType,
211+
unixSocketPathSuffix,
212+
connectorConfig);
213+
}
214+
203215
public String getNamedConnector() {
204216
return namedConnector;
205217
}

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

+18-3
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ class Connector {
4848
private final int serverProxyPort;
4949
private final ConnectorConfig config;
5050

51+
private final InstanceConnectionNameResolver instanceNameResolver;
52+
5153
Connector(
5254
ConnectorConfig config,
5355
ConnectionInfoRepositoryFactory connectionInfoRepositoryFactory,
@@ -56,7 +58,8 @@ class Connector {
5658
ListenableFuture<KeyPair> localKeyPair,
5759
long minRefreshDelayMs,
5860
long refreshTimeoutMs,
59-
int serverProxyPort) {
61+
int serverProxyPort,
62+
InstanceConnectionNameResolver instanceNameResolver) {
6063
this.config = config;
6164

6265
this.adminApi =
@@ -66,6 +69,7 @@ class Connector {
6669
this.localKeyPair = localKeyPair;
6770
this.minRefreshDelayMs = minRefreshDelayMs;
6871
this.serverProxyPort = serverProxyPort;
72+
this.instanceNameResolver = instanceNameResolver;
6973
}
7074

7175
public ConnectorConfig getConfig() {
@@ -139,9 +143,20 @@ Socket connect(ConnectionConfig config, long timeoutMs) throws IOException {
139143
}
140144
}
141145

142-
ConnectionInfoCache getConnection(ConnectionConfig config) {
146+
ConnectionInfoCache getConnection(final ConnectionConfig config) {
147+
CloudSqlInstanceName name = null;
148+
try {
149+
name = instanceNameResolver.resolve(config.getCloudSqlInstance());
150+
} catch (IllegalArgumentException e) {
151+
throw new IllegalArgumentException(
152+
String.format(
153+
"Cloud SQL connection name is invalid: \"%s\"", config.getCloudSqlInstance()),
154+
e);
155+
}
156+
final ConnectionConfig updatedConfig = config.withCloudSqlInstance(name.getConnectionName());
157+
143158
ConnectionInfoCache instance =
144-
instances.computeIfAbsent(config, k -> createConnectionInfo(config));
159+
instances.computeIfAbsent(updatedConfig, k -> createConnectionInfo(updatedConfig));
145160

146161
// If the client certificate has expired (as when the computer goes to
147162
// sleep, and the refresh cycle cannot run), force a refresh immediately.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
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+
* http://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.core;
18+
19+
import java.util.Collection;
20+
import java.util.Objects;
21+
import javax.naming.NameNotFoundException;
22+
import org.slf4j.Logger;
23+
import org.slf4j.LoggerFactory;
24+
25+
/**
26+
* An implementation of InstanceConnectionNameResolver that uses DNS SRV records to resolve an
27+
* instance name from a domain name.
28+
*/
29+
class DnsInstanceConnectionNameResolver implements InstanceConnectionNameResolver {
30+
private static final Logger logger =
31+
LoggerFactory.getLogger(DnsInstanceConnectionNameResolver.class);
32+
33+
private final DnsResolver dnsResolver;
34+
35+
public DnsInstanceConnectionNameResolver(DnsResolver dnsResolver) {
36+
this.dnsResolver = dnsResolver;
37+
}
38+
39+
@Override
40+
public CloudSqlInstanceName resolve(final String name) {
41+
// Attempt to parse the instance name
42+
try {
43+
return new CloudSqlInstanceName(name);
44+
} catch (IllegalArgumentException e) {
45+
// Not a well-formed instance name.
46+
}
47+
48+
// Next, attempt to resolve DNS name.
49+
Collection<DnsSrvRecord> instanceNames;
50+
try {
51+
instanceNames = this.dnsResolver.resolveSrv(name);
52+
} catch (NameNotFoundException ne) {
53+
// No DNS record found. This is not a valid instance name.
54+
throw new IllegalArgumentException(
55+
String.format("Unable to resolve SRV record for \"%s\".", name));
56+
}
57+
58+
// Use the first valid instance name from the list
59+
// or throw an IllegalArgumentException if none of the values can be parsed.
60+
return instanceNames.stream()
61+
.map(
62+
r -> {
63+
String target = r.getTarget();
64+
// Trim trailing '.' from target field
65+
if (target.endsWith(".")) {
66+
target = target.substring(0, target.length() - 1);
67+
}
68+
try {
69+
return new CloudSqlInstanceName(target);
70+
} catch (IllegalArgumentException e) {
71+
logger.info(
72+
"Unable to parse instance name in SRV record for "
73+
+ "domain name \"{}\" with target \"{}\"",
74+
name,
75+
target,
76+
e);
77+
return null;
78+
}
79+
})
80+
.filter(Objects::nonNull)
81+
.findFirst()
82+
.orElseThrow(
83+
() ->
84+
new IllegalArgumentException(
85+
String.format("Unable to parse values of SRV record for \"%s\".", name)));
86+
}
87+
}

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

+2-1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import java.util.Collection;
2020
import javax.naming.NameNotFoundException;
2121

22+
/** Wraps the Java DNS API. */
2223
interface DnsResolver {
23-
Collection<DnsSrvRecord> resolveSrv(String domainName) throws NameNotFoundException;
24+
Collection<DnsSrvRecord> resolveSrv(String name) throws NameNotFoundException;
2425
}

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

+13-8
Original file line numberDiff line numberDiff line change
@@ -20,17 +20,18 @@
2020
import java.util.regex.Matcher;
2121
import java.util.regex.Pattern;
2222

23-
public class DnsSrvRecord {
24-
private static final Pattern RECORD_FORMAT=Pattern.compile("(\\d+) +(\\d+) +(\\d+) +(.*)");
23+
/** This represents the value of an SRV DNS Record. */
24+
class DnsSrvRecord {
25+
private static final Pattern RECORD_FORMAT = Pattern.compile("(\\d+) +(\\d+) +(\\d+) +(.*)");
2526
private final int priority;
2627
private final int weight;
2728
private final int port;
2829
private final String target;
2930

3031
DnsSrvRecord(String record) {
3132
Matcher m = RECORD_FORMAT.matcher(record);
32-
if(! m.find()) {
33-
throw new IllegalArgumentException("Malformed SRV record: "+record);
33+
if (!m.find()) {
34+
throw new IllegalArgumentException("Malformed SRV record: " + record);
3435
}
3536

3637
this.priority = Integer.parseInt(m.group(1));
@@ -57,13 +58,17 @@ public String getTarget() {
5758

5859
@Override
5960
public boolean equals(Object o) {
60-
if (this == o)
61+
if (this == o) {
6162
return true;
62-
if (!(o instanceof DnsSrvRecord))
63+
}
64+
if (!(o instanceof DnsSrvRecord)) {
6365
return false;
66+
}
6467
DnsSrvRecord that = (DnsSrvRecord) o;
65-
return priority == that.priority && weight == that.weight && port == that.port && target.equals(
66-
that.target);
68+
return priority == that.priority
69+
&& weight == that.weight
70+
&& port == that.port
71+
&& target.equals(that.target);
6772
}
6873

6974
@Override
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
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+
* http://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.core;
18+
19+
/** Resolves the Cloud SQL Instance for a particular */
20+
interface InstanceConnectionNameResolver {
21+
22+
/**
23+
* Resolves the CloudSqlInstanceName from a configuration string value.
24+
*
25+
* @param name the configuration string
26+
* @return the CloudSqlInstanceName
27+
* @throws IllegalArgumentException if the name cannot be resolved.
28+
*/
29+
CloudSqlInstanceName resolve(String name);
30+
}

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

+2-1
Original file line numberDiff line numberDiff line change
@@ -332,7 +332,8 @@ private Connector createConnector(ConnectorConfig config) {
332332
localKeyPair,
333333
MIN_REFRESH_DELAY_MS,
334334
connectTimeoutMs,
335-
serverProxyPort);
335+
serverProxyPort,
336+
new DnsInstanceConnectionNameResolver(new JndiDnsResolver()));
336337
}
337338

338339
/** Register the configuration for a named connector. */

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

+10-12
Original file line numberDiff line numberDiff line change
@@ -25,42 +25,41 @@
2525
import javax.naming.directory.Attribute;
2626
import javax.naming.directory.InitialDirContext;
2727

28-
/**
29-
* Implements DnsResolver using the Java JNDI built-in DNS directory.
30-
*/
28+
/** Implements DnsResolver using the Java JNDI built-in DNS directory. */
3129
class JndiDnsResolver implements DnsResolver {
3230
private final String jndiPrefix;
3331

34-
/**
35-
* Creates a resolver using the system DNS settings.
36-
*/
32+
/** Creates a resolver using the system DNS settings. */
3733
JndiDnsResolver() {
3834
this.jndiPrefix = "dns:";
3935
}
4036

4137
/**
4238
* Creates a DNS resolver that uses a specific DNS server.
39+
*
4340
* @param dnsServer the DNS server hostname
4441
* @param port the DNS server port (DNS servers usually use port 53)
4542
*/
4643
JndiDnsResolver(String dnsServer, int port) {
47-
this.jndiPrefix = "dns://"+dnsServer+":"+port+"/";
44+
this.jndiPrefix = "dns://" + dnsServer + ":" + port + "/";
4845
}
4946

5047
/**
5148
* Returns DNS records for a domain name, sorted by priority, then target alphabetically.
49+
*
5250
* @param domainName the domain name to lookup
5351
* @return the list of record
5452
* @throws javax.naming.NameNotFoundException when the domain name did not resolve.
5553
*/
5654
@Override
57-
public Collection<DnsSrvRecord> resolveSrv(String domainName) throws javax.naming.NameNotFoundException{
55+
public Collection<DnsSrvRecord> resolveSrv(String domainName)
56+
throws javax.naming.NameNotFoundException {
5857
try {
5958
// Notice: This is old Java 1.2 style code. It uses the ancient JNDI DNS Provider api.
6059
// See https://docs.oracle.com/javase/7/docs/technotes/guides/jndi/jndi-dns.html
6160
Attribute attr =
6261
new InitialDirContext()
63-
.getAttributes(jndiPrefix+domainName, new String[] {"SRV"})
62+
.getAttributes(jndiPrefix + domainName, new String[] {"SRV"})
6463
.get("SRV");
6564
// attr.getAll() returns a Vector containing strings, one for each record returned by dns.
6665
return Collections.list(attr.getAll()).stream()
@@ -69,9 +68,8 @@ public Collection<DnsSrvRecord> resolveSrv(String domainName) throws javax.namin
6968
.collect(Collectors.toList());
7069
} catch (NameNotFoundException e) {
7170
throw e;
72-
}
73-
catch (NamingException e) {
74-
throw new RuntimeException("Unable to look up domain name "+domainName, e);
71+
} catch (NamingException e) {
72+
throw new RuntimeException("Unable to look up domain name " + domainName, e);
7573
}
7674
}
7775
}

0 commit comments

Comments
 (0)