Skip to content

Commit d747360

Browse files
authored
chore: Add domain name to the connector config. (#2098)
Add DomainName to connection config, and an InstanceNameResolver function. This will allow the connector to be configured using a domain name referencing the instead of the instance name. This is part of the implementation of #2043.
1 parent aaed143 commit d747360

9 files changed

+285
-14
lines changed

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

+22-4
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import com.google.auth.oauth2.GoogleCredentials;
2020
import com.google.common.base.Objects;
2121
import java.util.List;
22+
import java.util.function.Function;
2223
import java.util.function.Supplier;
2324

2425
/**
@@ -33,6 +34,7 @@ public class ConnectorConfig {
3334
private final String adminRootUrl;
3435
private final String adminServicePath;
3536
private final Supplier<GoogleCredentials> googleCredentialsSupplier;
37+
private final Function<String, String> instanceNameResolver;
3638
private final GoogleCredentials googleCredentials;
3739
private final String googleCredentialsPath;
3840
private final String adminQuotaProject;
@@ -50,7 +52,8 @@ private ConnectorConfig(
5052
String googleCredentialsPath,
5153
String adminQuotaProject,
5254
String universeDomain,
53-
RefreshStrategy refreshStrategy) {
55+
RefreshStrategy refreshStrategy,
56+
Function<String, String> instanceNameResolver) {
5457
this.targetPrincipal = targetPrincipal;
5558
this.delegates = delegates;
5659
this.adminRootUrl = adminRootUrl;
@@ -61,6 +64,7 @@ private ConnectorConfig(
6164
this.adminQuotaProject = adminQuotaProject;
6265
this.universeDomain = universeDomain;
6366
this.refreshStrategy = refreshStrategy;
67+
this.instanceNameResolver = instanceNameResolver;
6468
}
6569

6670
@Override
@@ -81,7 +85,8 @@ public boolean equals(Object o) {
8185
&& Objects.equal(googleCredentialsPath, that.googleCredentialsPath)
8286
&& Objects.equal(adminQuotaProject, that.adminQuotaProject)
8387
&& Objects.equal(universeDomain, that.universeDomain)
84-
&& Objects.equal(refreshStrategy, that.refreshStrategy);
88+
&& Objects.equal(refreshStrategy, that.refreshStrategy)
89+
&& Objects.equal(instanceNameResolver, that.instanceNameResolver);
8590
}
8691

8792
@Override
@@ -96,7 +101,8 @@ public int hashCode() {
96101
googleCredentialsPath,
97102
adminQuotaProject,
98103
universeDomain,
99-
refreshStrategy);
104+
refreshStrategy,
105+
instanceNameResolver);
100106
}
101107

102108
public String getTargetPrincipal() {
@@ -139,6 +145,10 @@ public RefreshStrategy getRefreshStrategy() {
139145
return refreshStrategy;
140146
}
141147

148+
public Function<String, String> getInstanceNameResolver() {
149+
return instanceNameResolver;
150+
}
151+
142152
/** The builder for the ConnectionConfig. */
143153
public static class Builder {
144154

@@ -152,6 +162,7 @@ public static class Builder {
152162
private String adminQuotaProject;
153163
private String universeDomain;
154164
private RefreshStrategy refreshStrategy = RefreshStrategy.BACKGROUND;
165+
private Function<String, String> instanceNameResolver;
155166

156167
/** Chained setter for TargetPrinciple field. */
157168
public Builder withTargetPrincipal(String targetPrincipal) {
@@ -214,6 +225,12 @@ public Builder withRefreshStrategy(RefreshStrategy refreshStrategy) {
214225
return this;
215226
}
216227

228+
/** Chained setter for the InstanceNameResolver field. */
229+
public Builder withInstanceNameResolver(Function<String, String> instanceNameResolver) {
230+
this.instanceNameResolver = instanceNameResolver;
231+
return this;
232+
}
233+
217234
/** Builds a new instance of {@code ConnectionConfig}. */
218235
public ConnectorConfig build() {
219236
// validate only one GoogleCredentials configuration field set
@@ -248,7 +265,8 @@ public ConnectorConfig build() {
248265
googleCredentialsPath,
249266
adminQuotaProject,
250267
universeDomain,
251-
refreshStrategy);
268+
refreshStrategy,
269+
instanceNameResolver);
252270
}
253271
}
254272
}

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

+58-1
Original file line numberDiff line numberDiff line change
@@ -31,28 +31,81 @@ class CloudSqlInstanceName {
3131
// Some legacy project ids are domain-scoped (e.g. "example.com:PROJECT:REGION:INSTANCE")
3232
private static final Pattern CONNECTION_NAME =
3333
Pattern.compile("([^:]+(:[^:]+)?):([^:]+):([^:]+)");
34+
35+
/**
36+
* The domain name pattern in accordance with RFC 1035, RFC 1123 and RFC 2181.
37+
*
38+
* <p>Explanation of the Regex:
39+
*
40+
* <p>^: Matches the beginning of the string.<br>
41+
* (?=.{1,255}$): Positive lookahead assertion to ensure the domain name is between 1 and 255
42+
* characters long.<br>
43+
* (?!-): Negative lookahead assertion to prevent hyphens at the beginning.<br>
44+
* [A-Za-z0-9-]+: Matches one or more alphanumeric characters or hyphens.<br>
45+
* (\\.[A-Za-z0-9-]+)*: Matches zero or more occurrences of a period followed by one or more
46+
* alphanumeric characters or hyphens (for subdomains).<br>
47+
* \\.: Matches a period before the TLD.<br>
48+
* [A-Za-z]{2,}: Matches two or more letters for the TLD.<br>
49+
* $: Matches the end of the string.<br>
50+
*/
51+
private static final Pattern DOMAIN_NAME =
52+
Pattern.compile("^(?=.{1,255}$)(?!-)[A-Za-z0-9-]+(\\.[A-Za-z0-9-]+)*\\.[A-Za-z]{2,}$");
53+
3454
private final String projectId;
3555
private final String regionId;
3656
private final String instanceId;
3757
private final String connectionName;
58+
private final String domainName;
59+
60+
/**
61+
* Validates that a string is a well-formed domain name.
62+
*
63+
* @param domain the domain name to check
64+
* @return true if domain is a well-formed domain name.
65+
*/
66+
public static boolean isValidDomain(String domain) {
67+
Matcher matcher = DOMAIN_NAME.matcher(domain);
68+
return matcher.matches();
69+
}
3870

3971
/**
4072
* Initializes a new CloudSqlInstanceName class.
4173
*
4274
* @param connectionName instance connection name in the format "PROJECT_ID:REGION_ID:INSTANCE_ID"
4375
*/
4476
CloudSqlInstanceName(String connectionName) {
45-
this.connectionName = connectionName;
77+
this(connectionName, null);
78+
}
79+
80+
/**
81+
* Initializes a new CloudSqlInstanceName class containing the domain name.
82+
*
83+
* @param connectionName instance connection name in the format "PROJECT_ID:REGION_ID:INSTANCE_ID"
84+
* @param domainName the domain name used to look up the instance, or null.
85+
*/
86+
CloudSqlInstanceName(String connectionName, String domainName) {
4687
Matcher matcher = CONNECTION_NAME.matcher(connectionName);
4788
checkArgument(
4889
matcher.matches(),
4990
String.format(
5091
"[%s] Cloud SQL connection name is invalid, expected string in the form of"
5192
+ " \"<PROJECT_ID>:<REGION_ID>:<INSTANCE_ID>\".",
5293
connectionName));
94+
this.connectionName = connectionName;
5395
this.projectId = matcher.group(1);
5496
this.regionId = matcher.group(3);
5597
this.instanceId = matcher.group(4);
98+
99+
// Only set this.domainName when it is not empty
100+
if (domainName != null && !domainName.isEmpty()) {
101+
Matcher domainMatcher = DOMAIN_NAME.matcher(domainName);
102+
checkArgument(
103+
domainMatcher.matches(),
104+
String.format("[%s] Domain name is invalid, expected a valid domain name", domainName));
105+
this.domainName = domainName;
106+
} else {
107+
this.domainName = null;
108+
}
56109
}
57110

58111
String getConnectionName() {
@@ -71,6 +124,10 @@ String getInstanceId() {
71124
return instanceId;
72125
}
73126

127+
String getDomainName() {
128+
return domainName;
129+
}
130+
74131
@Override
75132
public String toString() {
76133
return connectionName;

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

+60-1
Original file line numberDiff line numberDiff line change
@@ -63,9 +63,19 @@ public class ConnectionConfig {
6363

6464
private final AuthType authType;
6565
private final String unixSocketPathSuffix;
66+
private final String domainName;
6667

6768
/** Create a new ConnectionConfig from the well known JDBC Connection properties. */
6869
public static ConnectionConfig fromConnectionProperties(Properties props) {
70+
// TODO convert internal uses to fromConnectionProperties(props, domainName)
71+
return fromConnectionProperties(props, null);
72+
}
73+
74+
/**
75+
* Create a new ConnectionConfig from the well known JDBC Connection properties, also setting
76+
* database domain name.
77+
*/
78+
public static ConnectionConfig fromConnectionProperties(Properties props, String domainName) {
6979
final String csqlInstanceName = props.getProperty(ConnectionConfig.CLOUD_SQL_INSTANCE_PROPERTY);
7080
final String namedConnection =
7181
props.getProperty(ConnectionConfig.CLOUD_SQL_NAMED_CONNECTOR_PROPERTY);
@@ -113,6 +123,7 @@ public static ConnectionConfig fromConnectionProperties(Properties props) {
113123
ipTypes,
114124
authType,
115125
unixSocketPathSuffix,
126+
domainName,
116127
new ConnectorConfig.Builder()
117128
.withTargetPrincipal(targetPrincipal)
118129
.withDelegates(delegates)
@@ -162,13 +173,20 @@ public boolean equals(Object o) {
162173
&& Objects.equals(unixSocketPath, config.unixSocketPath)
163174
&& Objects.equals(ipTypes, config.ipTypes)
164175
&& Objects.equals(authType, config.authType)
176+
&& Objects.equals(domainName, config.domainName)
165177
&& Objects.equals(connectorConfig, config.connectorConfig);
166178
}
167179

168180
@Override
169181
public int hashCode() {
170182
return Objects.hash(
171-
cloudSqlInstance, namedConnector, unixSocketPath, ipTypes, authType, connectorConfig);
183+
cloudSqlInstance,
184+
namedConnector,
185+
unixSocketPath,
186+
ipTypes,
187+
authType,
188+
domainName,
189+
connectorConfig);
172190
}
173191

174192
private ConnectionConfig(
@@ -178,6 +196,7 @@ private ConnectionConfig(
178196
List<IpType> ipTypes,
179197
AuthType authType,
180198
String unixSocketPathSuffix,
199+
String domainName,
181200
ConnectorConfig connectorConfig) {
182201
this.cloudSqlInstance = cloudSqlInstance;
183202
this.namedConnector = namedConnector;
@@ -186,6 +205,7 @@ private ConnectionConfig(
186205
this.unixSocketPathSuffix = unixSocketPathSuffix;
187206
this.connectorConfig = connectorConfig;
188207
this.authType = authType;
208+
this.domainName = domainName;
189209
}
190210

191211
/** Creates a new instance of the ConnectionConfig with an updated connectorConfig. */
@@ -197,9 +217,36 @@ public ConnectionConfig withConnectorConfig(ConnectorConfig config) {
197217
ipTypes,
198218
authType,
199219
unixSocketPathSuffix,
220+
domainName,
200221
config);
201222
}
202223

224+
/** Creates a new instance of the ConnectionConfig with an updated cloudSqlInstance. */
225+
public ConnectionConfig withCloudSqlInstance(String newCloudSqlInstance) {
226+
return new ConnectionConfig(
227+
newCloudSqlInstance,
228+
namedConnector,
229+
unixSocketPath,
230+
ipTypes,
231+
authType,
232+
unixSocketPathSuffix,
233+
domainName,
234+
connectorConfig);
235+
}
236+
237+
/** Creates a new instance of the ConnectionConfig with an updated cloudSqlInstance. */
238+
public ConnectionConfig withDomainName(String domainName) {
239+
return new ConnectionConfig(
240+
cloudSqlInstance,
241+
namedConnector,
242+
unixSocketPath,
243+
ipTypes,
244+
authType,
245+
unixSocketPathSuffix,
246+
domainName,
247+
connectorConfig);
248+
}
249+
203250
public String getNamedConnector() {
204251
return namedConnector;
205252
}
@@ -228,6 +275,10 @@ public AuthType getAuthType() {
228275
return authType;
229276
}
230277

278+
public String getDomainName() {
279+
return domainName;
280+
}
281+
231282
/** The builder for the ConnectionConfig. */
232283
public static class Builder {
233284

@@ -238,6 +289,7 @@ public static class Builder {
238289
private String unixSocketPathSuffix;
239290
private ConnectorConfig connectorConfig = new ConnectorConfig.Builder().build();
240291
private AuthType authType = DEFAULT_AUTH_TYPE;
292+
private String domainName;
241293

242294
/** Chained setter for CloudSqlInstance field. */
243295
public Builder withCloudSqlInstance(String cloudSqlInstance) {
@@ -281,6 +333,12 @@ public Builder withIpTypes(List<IpType> ipTypes) {
281333
return this;
282334
}
283335

336+
/** Set domainName. */
337+
public Builder withDomainName(String domainName) {
338+
this.domainName = domainName;
339+
return this;
340+
}
341+
284342
/** Chained setter for UnixSocketPathSuffix field. */
285343
public Builder withUnixSocketPathSuffix(String unixSocketPathSuffix) {
286344
this.unixSocketPathSuffix = unixSocketPathSuffix;
@@ -296,6 +354,7 @@ public ConnectionConfig build() {
296354
ipTypes,
297355
authType,
298356
unixSocketPathSuffix,
357+
domainName,
299358
connectorConfig);
300359
}
301360
}

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

+43-2
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import java.security.KeyPair;
2929
import java.util.concurrent.ConcurrentHashMap;
3030
import java.util.concurrent.ExecutionException;
31+
import java.util.function.Function;
3132
import javax.net.ssl.SSLSocket;
3233
import jnr.unixsocket.UnixSocketAddress;
3334
import jnr.unixsocket.UnixSocketChannel;
@@ -140,9 +141,11 @@ Socket connect(ConnectionConfig config, long timeoutMs) throws IOException {
140141
}
141142
}
142143

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

147150
// If the client certificate has expired (as when the computer goes to
148151
// sleep, and the refresh cycle cannot run), force a refresh immediately.
@@ -154,6 +157,44 @@ ConnectionInfoCache getConnection(ConnectionConfig config) {
154157
return instance;
155158
}
156159

160+
/**
161+
* Updates the ConnectionConfig to ensure that the cloudSqlInstance field is set, resolving the
162+
* domainName using the InstanceNameResolver.
163+
*
164+
* @param config the configuration to resolve.
165+
* @return a ConnectionConfig guaranteed to have the CloudSqlInstance field set.
166+
*/
167+
private ConnectionConfig resolveConnectionName(ConnectionConfig config) {
168+
// If domainName is not set, return the original configuration unmodified.
169+
if (config.getDomainName() == null || config.getDomainName().isEmpty()) {
170+
return config;
171+
}
172+
173+
// If both domainName and cloudSqlInstance are set, ignore the domain name. Return a new
174+
// configuration with domainName set to null.
175+
if (config.getCloudSqlInstance() != null && !config.getCloudSqlInstance().isEmpty()) {
176+
return config.withDomainName(null);
177+
}
178+
179+
// If only domainName is set, resolve the domain name.
180+
try {
181+
final String unresolvedName = config.getDomainName();
182+
final Function<String, String> resolver =
183+
config.getConnectorConfig().getInstanceNameResolver();
184+
if (resolver != null) {
185+
return config.withCloudSqlInstance(resolver.apply(unresolvedName));
186+
} else {
187+
throw new IllegalStateException(
188+
"Can't resolve domain " + unresolvedName + ". ConnectorConfig.resolver is not set.");
189+
}
190+
} catch (IllegalArgumentException e) {
191+
throw new IllegalArgumentException(
192+
String.format(
193+
"Cloud SQL connection name is invalid: \"%s\"", config.getCloudSqlInstance()),
194+
e);
195+
}
196+
}
197+
157198
private ConnectionInfoCache createConnectionInfo(ConnectionConfig config) {
158199
logger.debug(
159200
String.format("[%s] Connection info added to cache.", config.getCloudSqlInstance()));

0 commit comments

Comments
 (0)