Skip to content

Commit b94cc98

Browse files
authored
feat: Configure the connector using a DNS name. (#422)
The connector may be configured to use a DNS name to look up the instance name instead of configuring the connector with the instance name directly. Add a DNS TXT record for the Cloud SQL instance to a private DNS server or a private Google Cloud DNS Zone used by your application. For example: - Record type: TXT - Name: prod-db.mycompany.example.com – This is the domain name used by the application - Value: my-project:region:my-instance – This is the instance connection name Configure the dialer with the cloudsqlconn.WithDNSResolver() option. Open a database connection using the DNS name: ``` const clientOpts = await connector.getOptions({ domainName: "db.example.com", }); ``` Part of #421 See also: GoogleCloudPlatform/cloud-sql-go-connector#843
1 parent de2863e commit b94cc98

File tree

4 files changed

+346
-256
lines changed

4 files changed

+346
-256
lines changed

src/cloud-sql-instance.ts

+25-13
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
import {IpAddressTypes, selectIpAddress} from './ip-addresses';
1616
import {InstanceConnectionInfo} from './instance-connection-info';
17-
import {parseInstanceConnectionName} from './parse-instance-connection-name';
17+
import {resolveInstanceName} from './parse-instance-connection-name';
1818
import {InstanceMetadata} from './sqladmin-fetcher';
1919
import {generateKeys} from './crypto';
2020
import {RSAKeys} from './rsa-keys';
@@ -38,6 +38,7 @@ interface Fetcher {
3838
interface CloudSQLInstanceOptions {
3939
authType: AuthTypes;
4040
instanceConnectionName: string;
41+
domainName?: string;
4142
ipType: IpAddressTypes;
4243
limitRateInterval?: number;
4344
sqlAdminFetcher: Fetcher;
@@ -54,7 +55,13 @@ export class CloudSQLInstance {
5455
static async getCloudSQLInstance(
5556
options: CloudSQLInstanceOptions
5657
): Promise<CloudSQLInstance> {
57-
const instance = new CloudSQLInstance(options);
58+
const instance = new CloudSQLInstance({
59+
options: options,
60+
instanceInfo: await resolveInstanceName(
61+
options.instanceConnectionName,
62+
options.domainName
63+
),
64+
});
5865
await instance.refresh();
5966
return instance;
6067
}
@@ -80,17 +87,17 @@ export class CloudSQLInstance {
8087
public dnsName = '';
8188

8289
constructor({
83-
ipType,
84-
authType,
85-
instanceConnectionName,
86-
sqlAdminFetcher,
87-
limitRateInterval = 30 * 1000, // 30s default
88-
}: CloudSQLInstanceOptions) {
89-
this.authType = authType;
90-
this.instanceInfo = parseInstanceConnectionName(instanceConnectionName);
91-
this.ipType = ipType;
92-
this.limitRateInterval = limitRateInterval;
93-
this.sqlAdminFetcher = sqlAdminFetcher;
90+
options,
91+
instanceInfo,
92+
}: {
93+
options: CloudSQLInstanceOptions;
94+
instanceInfo: InstanceConnectionInfo;
95+
}) {
96+
this.instanceInfo = instanceInfo;
97+
this.authType = options.authType || AuthTypes.PASSWORD;
98+
this.ipType = options.ipType || IpAddressTypes.PUBLIC;
99+
this.limitRateInterval = options.limitRateInterval || 30 * 1000; // 30 seconds
100+
this.sqlAdminFetcher = options.sqlAdminFetcher;
94101
}
95102

96103
// p-throttle library has to be initialized in an async scope in order to
@@ -286,6 +293,7 @@ export class CloudSQLInstance {
286293
}
287294

288295
cancelRefresh(): void {
296+
// If refresh has not yet started, then cancel the setTimeout
289297
if (this.scheduledRefreshID) {
290298
clearTimeout(this.scheduledRefreshID);
291299
}
@@ -305,4 +313,8 @@ export class CloudSQLInstance {
305313
this.closed = true;
306314
this.cancelRefresh();
307315
}
316+
317+
isClosed(): boolean {
318+
return this.closed;
319+
}
308320
}

src/connector.ts

+91-70
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15-
import {Server, Socket, createServer} from 'node:net';
15+
import {createServer, Server, Socket} from 'node:net';
1616
import tls from 'node:tls';
1717
import {promisify} from 'node:util';
1818
import {AuthClient, GoogleAuth} from 'google-auth-library';
@@ -43,6 +43,8 @@ export declare interface ConnectionOptions {
4343
authType?: AuthTypes;
4444
ipType?: IpAddressTypes;
4545
instanceConnectionName: string;
46+
domainName?: string;
47+
limitRateInterval?: number;
4648
}
4749

4850
export declare interface SocketConnectionOptions extends ConnectionOptions {
@@ -72,71 +74,102 @@ export declare interface TediousDriverOptions {
7274
connector: PromisedStreamFunction;
7375
encrypt: boolean;
7476
}
77+
// CacheEntry holds the promise and resolved instance metadata for
78+
// the connector's instances. The instance field will be set when
79+
// the promise resolves.
80+
class CacheEntry {
81+
promise: Promise<CloudSQLInstance>;
82+
instance?: CloudSQLInstance;
83+
err?: Error;
84+
85+
constructor(promise: Promise<CloudSQLInstance>) {
86+
this.promise = promise;
87+
this.promise
88+
.then(inst => (this.instance = inst))
89+
.catch(err => (this.err = err));
90+
}
91+
92+
isResolved(): boolean {
93+
return Boolean(this.instance);
94+
}
95+
isError(): boolean {
96+
return Boolean(this.err);
97+
}
98+
}
7599

76100
// Internal mapping of the CloudSQLInstances that
77101
// adds extra logic to async initialize items.
78-
class CloudSQLInstanceMap extends Map {
79-
async loadInstance({
80-
ipType,
81-
authType,
82-
instanceConnectionName,
83-
sqlAdminFetcher,
84-
}: {
85-
ipType: IpAddressTypes;
86-
authType: AuthTypes;
87-
instanceConnectionName: string;
88-
sqlAdminFetcher: SQLAdminFetcher;
89-
}): Promise<void> {
102+
class CloudSQLInstanceMap extends Map<string, CacheEntry> {
103+
private readonly sqlAdminFetcher: SQLAdminFetcher;
104+
105+
constructor(sqlAdminFetcher: SQLAdminFetcher) {
106+
super();
107+
this.sqlAdminFetcher = sqlAdminFetcher;
108+
}
109+
110+
private cacheKey(opts: ConnectionOptions): string {
111+
//TODO: for now, the cache key function must be synchronous.
112+
// When we implement the async connection info from
113+
// https://github.com/GoogleCloudPlatform/cloud-sql-nodejs-connector/pull/426
114+
// then the cache key should contain both the domain name
115+
// and the resolved instance name.
116+
return (
117+
(opts.instanceConnectionName || opts.domainName) +
118+
'-' +
119+
opts.authType +
120+
'-' +
121+
opts.ipType
122+
);
123+
}
124+
125+
async loadInstance(opts: ConnectionOptions): Promise<void> {
90126
// in case an instance to that connection name has already
91127
// been setup there's no need to set it up again
92-
if (this.has(instanceConnectionName)) {
93-
const instance = this.get(instanceConnectionName);
94-
if (instance.authType && instance.authType !== authType) {
95-
throw new CloudSQLConnectorError({
96-
message:
97-
`getOptions called for instance ${instanceConnectionName} with authType ${authType}, ` +
98-
`but was previously called with authType ${instance.authType}. ` +
99-
'If you require both for your use case, please use a new connector object.',
100-
code: 'EMISMATCHAUTHTYPE',
101-
});
128+
const key = this.cacheKey(opts);
129+
const entry = this.get(key);
130+
if (entry) {
131+
if (entry.isResolved()) {
132+
if (!entry.instance?.isClosed()) {
133+
// The instance is open and the domain has not changed.
134+
// use the cached instance.
135+
return;
136+
}
137+
} else if (entry.isError()) {
138+
// The instance failed it's initial refresh. Remove it from the
139+
// cache and throw the error.
140+
this.delete(key);
141+
throw entry.err;
142+
} else {
143+
// The instance initial refresh is in progress.
144+
await entry.promise;
145+
return;
102146
}
103-
return;
104147
}
105-
const connectionInstance = await CloudSQLInstance.getCloudSQLInstance({
106-
ipType,
107-
authType,
108-
instanceConnectionName,
109-
sqlAdminFetcher: sqlAdminFetcher,
148+
149+
// Start the refresh and add a cache entry.
150+
const promise = CloudSQLInstance.getCloudSQLInstance({
151+
instanceConnectionName: opts.instanceConnectionName,
152+
domainName: opts.domainName,
153+
authType: opts.authType || AuthTypes.PASSWORD,
154+
ipType: opts.ipType || IpAddressTypes.PUBLIC,
155+
limitRateInterval: opts.limitRateInterval || 30 * 1000, // 30 sec
156+
sqlAdminFetcher: this.sqlAdminFetcher,
110157
});
111-
this.set(instanceConnectionName, connectionInstance);
158+
this.set(key, new CacheEntry(promise));
159+
160+
// Wait for the cache entry to resolve.
161+
await promise;
112162
}
113163

114-
getInstance({
115-
instanceConnectionName,
116-
authType,
117-
}: {
118-
instanceConnectionName: string;
119-
authType: AuthTypes;
120-
}): CloudSQLInstance {
121-
const connectionInstance = this.get(instanceConnectionName);
122-
if (!connectionInstance) {
164+
getInstance(opts: ConnectionOptions): CloudSQLInstance {
165+
const connectionInstance = this.get(this.cacheKey(opts));
166+
if (!connectionInstance || !connectionInstance.instance) {
123167
throw new CloudSQLConnectorError({
124-
message: `Cannot find info for instance: ${instanceConnectionName}`,
168+
message: `Cannot find info for instance: ${opts.instanceConnectionName}`,
125169
code: 'ENOINSTANCEINFO',
126170
});
127-
} else if (
128-
connectionInstance.authType &&
129-
connectionInstance.authType !== authType
130-
) {
131-
throw new CloudSQLConnectorError({
132-
message:
133-
`getOptions called for instance ${instanceConnectionName} with authType ${authType}, ` +
134-
`but was previously called with authType ${connectionInstance.authType}. ` +
135-
'If you require both for your use case, please use a new connector object.',
136-
code: 'EMISMATCHAUTHTYPE',
137-
});
138171
}
139-
return connectionInstance;
172+
return connectionInstance.instance;
140173
}
141174
}
142175

@@ -160,13 +193,13 @@ export class Connector {
160193
private readonly sockets: Set<Socket>;
161194

162195
constructor(opts: ConnectorOptions = {}) {
163-
this.instances = new CloudSQLInstanceMap();
164196
this.sqlAdminFetcher = new SQLAdminFetcher({
165197
loginAuth: opts.auth,
166198
sqlAdminAPIEndpoint: opts.sqlAdminAPIEndpoint,
167199
universeDomain: opts.universeDomain,
168200
userAgent: opts.userAgent,
169201
});
202+
this.instances = new CloudSQLInstanceMap(this.sqlAdminFetcher);
170203
this.localProxies = new Set();
171204
this.sockets = new Set();
172205
}
@@ -182,25 +215,13 @@ export class Connector {
182215
// });
183216
// const pool = new Pool(opts)
184217
// const res = await pool.query('SELECT * FROM pg_catalog.pg_tables;')
185-
async getOptions({
186-
authType = AuthTypes.PASSWORD,
187-
ipType = IpAddressTypes.PUBLIC,
188-
instanceConnectionName,
189-
}: ConnectionOptions): Promise<DriverOptions> {
218+
async getOptions(opts: ConnectionOptions): Promise<DriverOptions> {
190219
const {instances} = this;
191-
await instances.loadInstance({
192-
ipType,
193-
authType,
194-
instanceConnectionName,
195-
sqlAdminFetcher: this.sqlAdminFetcher,
196-
});
220+
await instances.loadInstance(opts);
197221

198222
return {
199223
stream() {
200-
const cloudSqlInstance = instances.getInstance({
201-
instanceConnectionName,
202-
authType,
203-
});
224+
const cloudSqlInstance = instances.getInstance(opts);
204225
const {
205226
instanceInfo,
206227
ephemeralCert,
@@ -228,7 +249,7 @@ export class Connector {
228249
privateKey,
229250
serverCaCert,
230251
serverCaMode,
231-
dnsName,
252+
dnsName: instanceInfo.domainName || dnsName, // use the configured domain name, or the instance dnsName.
232253
});
233254
tlsSocket.once('error', () => {
234255
cloudSqlInstance.forceRefresh();
@@ -333,7 +354,7 @@ export class Connector {
333354
// Also clear up any local proxy servers and socket connections.
334355
close(): void {
335356
for (const instance of this.instances.values()) {
336-
instance.close();
357+
instance.promise.then(inst => inst.close());
337358
}
338359
for (const server of this.localProxies) {
339360
server.close();

0 commit comments

Comments
 (0)