Skip to content

Commit 3126f68

Browse files
committed
feat: Configure connectors with DNS Name.
wip: check for domain changed on connect.
1 parent 59dc310 commit 3126f68

File tree

11 files changed

+686
-311
lines changed

11 files changed

+686
-311
lines changed

.github/workflows/tests.yml

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -154,13 +154,15 @@ jobs:
154154
MYSQL_DB:${{ vars.GOOGLE_CLOUD_PROJECT }}/MYSQL_DB
155155
POSTGRES_CONNECTION_NAME:${{ vars.GOOGLE_CLOUD_PROJECT }}/POSTGRES_CONNECTION_NAME
156156
POSTGRES_USER:${{ vars.GOOGLE_CLOUD_PROJECT }}/POSTGRES_USER
157-
POSTGRES_IAM_USER:${{ vars.GOOGLE_CLOUD_PROJECT }}/POSTGRES_USER_IAM_NODE
157+
POSTGRES_USER_IAM_NODE:${{ vars.GOOGLE_CLOUD_PROJECT }}/POSTGRES_USER_IAM_NODE
158158
POSTGRES_PASS:${{ vars.GOOGLE_CLOUD_PROJECT }}/POSTGRES_PASS
159159
POSTGRES_DB:${{ vars.GOOGLE_CLOUD_PROJECT }}/POSTGRES_DB
160160
POSTGRES_CAS_CONNECTION_NAME:${{ vars.GOOGLE_CLOUD_PROJECT }}/POSTGRES_CAS_CONNECTION_NAME
161161
POSTGRES_CAS_PASS:${{ vars.GOOGLE_CLOUD_PROJECT }}/POSTGRES_CAS_PASS
162162
POSTGRES_CUSTOMER_CAS_CONNECTION_NAME:${{ vars.GOOGLE_CLOUD_PROJECT }}/POSTGRES_CUSTOMER_CAS_CONNECTION_NAME
163163
POSTGRES_CUSTOMER_CAS_PASS:${{ vars.GOOGLE_CLOUD_PROJECT }}/POSTGRES_CUSTOMER_CAS_PASS
164+
POSTGRES_CUSTOMER_CAS_DOMAIN_NAME:${{ vars.GOOGLE_CLOUD_PROJECT }}/POSTGRES_CUSTOMER_CAS_DOMAIN_NAME
165+
POSTGRES_CUSTOMER_CAS_INVALID_DOMAIN_NAME:${{ vars.GOOGLE_CLOUD_PROJECT }}/POSTGRES_CUSTOMER_CAS_INVALID_DOMAIN_NAME
164166
SQLSERVER_CONNECTION_NAME:${{ vars.GOOGLE_CLOUD_PROJECT }}/SQLSERVER_CONNECTION_NAME
165167
SQLSERVER_USER:${{ vars.GOOGLE_CLOUD_PROJECT }}/SQLSERVER_USER
166168
SQLSERVER_PASS:${{ vars.GOOGLE_CLOUD_PROJECT }}/SQLSERVER_PASS
@@ -181,13 +183,15 @@ jobs:
181183
MYSQL_DB: "${{ steps.secrets.outputs.MYSQL_DB }}"
182184
POSTGRES_CONNECTION_NAME: "${{ steps.secrets.outputs.POSTGRES_CONNECTION_NAME }}"
183185
POSTGRES_USER: "${{ steps.secrets.outputs.POSTGRES_USER }}"
184-
POSTGRES_IAM_USER: "${{ steps.secrets.outputs.POSTGRES_IAM_USER }}"
186+
POSTGRES_USER_IAM_NODE: "${{ steps.secrets.outputs.POSTGRES_USER_IAM_NODE }}"
185187
POSTGRES_PASS: "${{ steps.secrets.outputs.POSTGRES_PASS }}"
186188
POSTGRES_DB: "${{ steps.secrets.outputs.POSTGRES_DB }}"
187189
POSTGRES_CAS_CONNECTION_NAME: "${{ steps.secrets.outputs.POSTGRES_CAS_CONNECTION_NAME }}"
188190
POSTGRES_CAS_PASS: "${{ steps.secrets.outputs.POSTGRES_CAS_PASS }}"
189191
POSTGRES_CUSTOMER_CAS_CONNECTION_NAME: "${{ steps.secrets.outputs.POSTGRES_CUSTOMER_CAS_CONNECTION_NAME }}"
190192
POSTGRES_CUSTOMER_CAS_PASS: "${{ steps.secrets.outputs.POSTGRES_CUSTOMER_CAS_PASS }}"
193+
POSTGRES_CUSTOMER_CAS_DOMAIN_NAME: "${{ steps.secrets.outputs.POSTGRES_CUSTOMER_CAS_DOMAIN_NAME }}"
194+
POSTGRES_CUSTOMER_CAS_INVALID_DOMAIN_NAME: "${{ steps.secrets.outputs.POSTGRES_CUSTOMER_CAS_INVALID_DOMAIN_NAME }}"
191195
SQLSERVER_CONNECTION_NAME: "${{ steps.secrets.outputs.SQLSERVER_CONNECTION_NAME }}"
192196
SQLSERVER_USER: "${{ steps.secrets.outputs.SQLSERVER_USER }}"
193197
SQLSERVER_PASS: "${{ steps.secrets.outputs.SQLSERVER_PASS }}"
@@ -275,9 +279,15 @@ jobs:
275279
MYSQL_DB:${{ vars.GOOGLE_CLOUD_PROJECT }}/MYSQL_DB
276280
POSTGRES_CONNECTION_NAME:${{ vars.GOOGLE_CLOUD_PROJECT }}/POSTGRES_CONNECTION_NAME
277281
POSTGRES_USER:${{ vars.GOOGLE_CLOUD_PROJECT }}/POSTGRES_USER
278-
POSTGRES_IAM_USER:${{ vars.GOOGLE_CLOUD_PROJECT }}/POSTGRES_USER_IAM_NODE
282+
POSTGRES_USER_IAM_NODE:${{ vars.GOOGLE_CLOUD_PROJECT }}/POSTGRES_USER_IAM_NODE
279283
POSTGRES_PASS:${{ vars.GOOGLE_CLOUD_PROJECT }}/POSTGRES_PASS
280284
POSTGRES_DB:${{ vars.GOOGLE_CLOUD_PROJECT }}/POSTGRES_DB
285+
POSTGRES_CAS_CONNECTION_NAME:${{ vars.GOOGLE_CLOUD_PROJECT }}/POSTGRES_CAS_CONNECTION_NAME
286+
POSTGRES_CAS_PASS:${{ vars.GOOGLE_CLOUD_PROJECT }}/POSTGRES_CAS_PASS
287+
POSTGRES_CUSTOMER_CAS_CONNECTION_NAME:${{ vars.GOOGLE_CLOUD_PROJECT }}/POSTGRES_CUSTOMER_CAS_CONNECTION_NAME
288+
POSTGRES_CUSTOMER_CAS_PASS:${{ vars.GOOGLE_CLOUD_PROJECT }}/POSTGRES_CUSTOMER_CAS_PASS
289+
POSTGRES_CUSTOMER_CAS_DOMAIN_NAME:${{ vars.GOOGLE_CLOUD_PROJECT }}/POSTGRES_CUSTOMER_CAS_DOMAIN_NAME
290+
POSTGRES_CUSTOMER_CAS_INVALID_DOMAIN_NAME:${{ vars.GOOGLE_CLOUD_PROJECT }}/POSTGRES_CUSTOMER_CAS_INVALID_DOMAIN_NAME
281291
SQLSERVER_CONNECTION_NAME:${{ vars.GOOGLE_CLOUD_PROJECT }}/SQLSERVER_CONNECTION_NAME
282292
SQLSERVER_USER:${{ vars.GOOGLE_CLOUD_PROJECT }}/SQLSERVER_USER
283293
SQLSERVER_PASS:${{ vars.GOOGLE_CLOUD_PROJECT }}/SQLSERVER_PASS
@@ -292,9 +302,15 @@ jobs:
292302
MYSQL_DB: "${{ steps.secrets.outputs.MYSQL_DB }}"
293303
POSTGRES_CONNECTION_NAME: "${{ steps.secrets.outputs.POSTGRES_CONNECTION_NAME }}"
294304
POSTGRES_USER: "${{ steps.secrets.outputs.POSTGRES_USER }}"
295-
POSTGRES_IAM_USER: "${{ steps.secrets.outputs.POSTGRES_IAM_USER }}"
305+
POSTGRES_IAM_USER: "${{ steps.secrets.outputs.POSTGRES_USER_IAM_NODE }}"
296306
POSTGRES_PASS: "${{ steps.secrets.outputs.POSTGRES_PASS }}"
297307
POSTGRES_DB: "${{ steps.secrets.outputs.POSTGRES_DB }}"
308+
POSTGRES_CAS_CONNECTION_NAME: "${{ steps.secrets.outputs.POSTGRES_CAS_CONNECTION_NAME }}"
309+
POSTGRES_CAS_PASS: "${{ steps.secrets.outputs.POSTGRES_CAS_PASS }}"
310+
POSTGRES_CUSTOMER_CAS_CONNECTION_NAME: "${{ steps.secrets.outputs.POSTGRES_CUSTOMER_CAS_CONNECTION_NAME }}"
311+
POSTGRES_CUSTOMER_CAS_PASS: "${{ steps.secrets.outputs.POSTGRES_CUSTOMER_CAS_PASS }}"
312+
POSTGRES_CUSTOMER_CAS_DOMAIN_NAME: "${{ steps.secrets.outputs.POSTGRES_CUSTOMER_CAS_DOMAIN_NAME }}"
313+
POSTGRES_CUSTOMER_CAS_INVALID_DOMAIN_NAME: "${{ steps.secrets.outputs.POSTGRES_CUSTOMER_CAS_INVALID_DOMAIN_NAME }}"
298314
SQLSERVER_CONNECTION_NAME: "${{ steps.secrets.outputs.SQLSERVER_CONNECTION_NAME }}"
299315
SQLSERVER_USER: "${{ steps.secrets.outputs.SQLSERVER_USER }}"
300316
SQLSERVER_PASS: "${{ steps.secrets.outputs.SQLSERVER_PASS }}"

src/cloud-sql-instance.ts

Lines changed: 22 additions & 13 deletions
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,10 @@ 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(options.instanceConnectionName, options.domainName),
61+
});
5862
await instance.refresh();
5963
return instance;
6064
}
@@ -80,17 +84,17 @@ export class CloudSQLInstance {
8084
public dnsName = '';
8185

8286
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;
87+
options,
88+
instanceInfo,
89+
}: {
90+
options: CloudSQLInstanceOptions;
91+
instanceInfo: InstanceConnectionInfo;
92+
}) {
93+
this.instanceInfo = instanceInfo;
94+
this.authType = options.authType || AuthTypes.PASSWORD;
95+
this.ipType = options.ipType || IpAddressTypes.PUBLIC;
96+
this.limitRateInterval = options.limitRateInterval || 30 * 1000; // 30 seconds
97+
this.sqlAdminFetcher = options.sqlAdminFetcher;
9498
}
9599

96100
// p-throttle library has to be initialized in an async scope in order to
@@ -286,6 +290,7 @@ export class CloudSQLInstance {
286290
}
287291

288292
cancelRefresh(): void {
293+
// If refresh has not yet started, then cancel the setTimeout
289294
if (this.scheduledRefreshID) {
290295
clearTimeout(this.scheduledRefreshID);
291296
}
@@ -305,4 +310,8 @@ export class CloudSQLInstance {
305310
this.closed = true;
306311
this.cancelRefresh();
307312
}
313+
314+
isClosed(): boolean {
315+
return this.closed;
316+
}
308317
}

src/connector.ts

Lines changed: 69 additions & 65 deletions
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';
@@ -22,6 +22,7 @@ import {IpAddressTypes} from './ip-addresses';
2222
import {AuthTypes} from './auth-types';
2323
import {SQLAdminFetcher} from './sqladmin-fetcher';
2424
import {CloudSQLConnectorError} from './errors';
25+
import {isSameInstance, resolveInstanceName} from './parse-instance-connection-name';
2526

2627
// These Socket types are subsets from nodejs definitely typed repo, ref:
2728
// https://github.com/DefinitelyTyped/DefinitelyTyped/blob/ae0fe42ff0e6e820e8ae324acf4f8e944aa1b2b7/types/node/v18/net.d.ts#L437
@@ -43,6 +44,7 @@ export declare interface ConnectionOptions {
4344
authType?: AuthTypes;
4445
ipType?: IpAddressTypes;
4546
instanceConnectionName: string;
47+
domainName?: string;
4648
}
4749

4850
export declare interface SocketConnectionOptions extends ConnectionOptions {
@@ -75,66 +77,65 @@ export declare interface TediousDriverOptions {
7577

7678
// Internal mapping of the CloudSQLInstances that
7779
// 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> {
80+
class CloudSQLInstanceMap extends Map<string,CloudSQLInstance> {
81+
private readonly sqlAdminFetcher: SQLAdminFetcher
82+
83+
constructor(sqlAdminFetcher: SQLAdminFetcher) {
84+
super();
85+
this.sqlAdminFetcher = sqlAdminFetcher;
86+
}
87+
88+
private cacheKey(opts: ConnectionOptions): string {
89+
//TODO: for now, the cache key function must be synchronous.
90+
// When we implement the async connection info from
91+
// https://github.com/GoogleCloudPlatform/cloud-sql-nodejs-connector/pull/426
92+
// then the cache key should contain both the domain name
93+
// and the resolved instance name.
94+
return (opts.instanceConnectionName || opts.domainName)+"-"+
95+
opts.authType+"-"+opts.ipType;
96+
}
97+
98+
async loadInstance(opts: ConnectionOptions): Promise<void> {
9099
// in case an instance to that connection name has already
91100
// 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-
});
101+
if (this.has(this.cacheKey(opts))) {
102+
const instance = this.get(this.cacheKey(opts));
103+
let oldInfo = instance?.instanceInfo
104+
if(oldInfo && oldInfo.domainName){
105+
// configured with domain name
106+
let newInfo = await resolveInstanceName(undefined, oldInfo.domainName);
107+
if(!isSameInstance(oldInfo, newInfo) ) {
108+
// Domain name changed. Close and remove, then create a new map entry.
109+
instance?.close();
110+
this.delete(this.cacheKey(opts))
111+
} else {
112+
// Domain name resolves to the same instance, do nothing.
113+
return
114+
}
115+
} else{
116+
// Configured with instance name. Existing map entry is OK.
117+
return;
102118
}
103-
return;
104119
}
120+
105121
const connectionInstance = await CloudSQLInstance.getCloudSQLInstance({
106-
ipType,
107-
authType,
108-
instanceConnectionName,
109-
sqlAdminFetcher: sqlAdminFetcher,
122+
instanceConnectionName: opts.instanceConnectionName,
123+
domainName: opts.domainName,
124+
authType: opts.authType || AuthTypes.PASSWORD,
125+
ipType: opts.ipType || IpAddressTypes.PUBLIC,
126+
limitRateInterval: 30 * 1000, // 30 sec
127+
sqlAdminFetcher: this.sqlAdminFetcher,
110128
});
111-
this.set(instanceConnectionName, connectionInstance);
129+
this.set(this.cacheKey(opts), connectionInstance);
112130
}
113131

114-
getInstance({
115-
instanceConnectionName,
116-
authType,
117-
}: {
118-
instanceConnectionName: string;
119-
authType: AuthTypes;
120-
}): CloudSQLInstance {
121-
const connectionInstance = this.get(instanceConnectionName);
132+
getInstance(opts: ConnectionOptions): CloudSQLInstance {
133+
const connectionInstance = this.get(this.cacheKey(opts));
122134
if (!connectionInstance) {
123135
throw new CloudSQLConnectorError({
124-
message: `Cannot find info for instance: ${instanceConnectionName}`,
136+
message: `Cannot find info for instance: ${opts.instanceConnectionName}`,
125137
code: 'ENOINSTANCEINFO',
126138
});
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-
});
138139
}
139140
return connectionInstance;
140141
}
@@ -160,13 +161,13 @@ export class Connector {
160161
private readonly sockets: Set<Socket>;
161162

162163
constructor(opts: ConnectorOptions = {}) {
163-
this.instances = new CloudSQLInstanceMap();
164164
this.sqlAdminFetcher = new SQLAdminFetcher({
165165
loginAuth: opts.auth,
166166
sqlAdminAPIEndpoint: opts.sqlAdminAPIEndpoint,
167167
universeDomain: opts.universeDomain,
168168
userAgent: opts.userAgent,
169169
});
170+
this.instances = new CloudSQLInstanceMap(this.sqlAdminFetcher);
170171
this.localProxies = new Set();
171172
this.sockets = new Set();
172173
}
@@ -183,22 +184,25 @@ export class Connector {
183184
// const pool = new Pool(opts)
184185
// const res = await pool.query('SELECT * FROM pg_catalog.pg_tables;')
185186
async getOptions({
186-
authType = AuthTypes.PASSWORD,
187-
ipType = IpAddressTypes.PUBLIC,
188-
instanceConnectionName,
189-
}: ConnectionOptions): Promise<DriverOptions> {
187+
authType = AuthTypes.PASSWORD,
188+
ipType = IpAddressTypes.PUBLIC,
189+
instanceConnectionName,
190+
domainName,
191+
}: ConnectionOptions): Promise<DriverOptions> {
190192
const {instances} = this;
191193
await instances.loadInstance({
192194
ipType,
193195
authType,
194196
instanceConnectionName,
195-
sqlAdminFetcher: this.sqlAdminFetcher,
197+
domainName
196198
});
197199

198200
return {
199201
stream() {
200202
const cloudSqlInstance = instances.getInstance({
203+
ipType,
201204
instanceConnectionName,
205+
domainName,
202206
authType,
203207
});
204208
const {
@@ -228,7 +232,7 @@ export class Connector {
228232
privateKey,
229233
serverCaCert,
230234
serverCaMode,
231-
dnsName,
235+
dnsName: instanceInfo.domainName || dnsName, // use the configured domain name, or the instance dnsName.
232236
});
233237
tlsSocket.once('error', () => {
234238
cloudSqlInstance.forceRefresh();
@@ -248,10 +252,10 @@ export class Connector {
248252
}
249253

250254
async getTediousOptions({
251-
authType,
252-
ipType,
253-
instanceConnectionName,
254-
}: ConnectionOptions): Promise<TediousDriverOptions> {
255+
authType,
256+
ipType,
257+
instanceConnectionName,
258+
}: ConnectionOptions): Promise<TediousDriverOptions> {
255259
if (authType === AuthTypes.IAM) {
256260
throw new CloudSQLConnectorError({
257261
message: 'Tedious does not support Auto IAM DB Authentication',
@@ -288,11 +292,11 @@ export class Connector {
288292
// `postgresql://${user}@localhost/${database}?host=${process.cwd()}`;
289293
// const prisma = new PrismaClient({ datasourceUrl });
290294
async startLocalProxy({
291-
authType,
292-
ipType,
293-
instanceConnectionName,
294-
listenOptions,
295-
}: SocketConnectionOptions): Promise<void> {
295+
authType,
296+
ipType,
297+
instanceConnectionName,
298+
listenOptions,
299+
}: SocketConnectionOptions): Promise<void> {
296300
const {stream} = await this.getOptions({
297301
authType,
298302
ipType,

src/instance-connection-info.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,6 @@ export interface InstanceConnectionInfo {
1717
regionId: string;
1818
instanceId: string;
1919
domainName: string | undefined;
20+
21+
2022
}

0 commit comments

Comments
 (0)