Skip to content

Commit 5dc1049

Browse files
committed
wip: Separate domainName from instanceConnectionName in config.
1 parent ef914ea commit 5dc1049

File tree

7 files changed

+230
-151
lines changed

7 files changed

+230
-151
lines changed

src/cloud-sql-instance.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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;
@@ -56,7 +57,7 @@ export class CloudSQLInstance {
5657
): Promise<CloudSQLInstance> {
5758
const instance = new CloudSQLInstance({
5859
options: options,
59-
instanceInfo: await resolveInstanceName(options.instanceConnectionName),
60+
instanceInfo: await resolveInstanceName(options.instanceConnectionName, options.domainName),
6061
});
6162
await instance.refresh();
6263
return instance;
@@ -307,4 +308,8 @@ export class CloudSQLInstance {
307308
this.closed = true;
308309
this.cancelRefresh();
309310
}
311+
312+
isClosed(): boolean {
313+
return this.closed;
314+
}
310315
}

src/connector.ts

Lines changed: 72 additions & 64 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
}
@@ -161,13 +162,13 @@ export class Connector {
161162
private readonly sockets: Set<Socket>;
162163

163164
constructor(opts: ConnectorOptions = {}) {
164-
this.instances = new CloudSQLInstanceMap();
165165
this.sqlAdminFetcher = new SQLAdminFetcher({
166166
loginAuth: opts.auth,
167167
sqlAdminAPIEndpoint: opts.sqlAdminAPIEndpoint,
168168
universeDomain: opts.universeDomain,
169169
userAgent: opts.userAgent,
170170
});
171+
this.instances = new CloudSQLInstanceMap(this.sqlAdminFetcher);
171172
this.localProxies = new Set();
172173
this.sockets = new Set();
173174
}
@@ -184,22 +185,25 @@ export class Connector {
184185
// const pool = new Pool(opts)
185186
// const res = await pool.query('SELECT * FROM pg_catalog.pg_tables;')
186187
async getOptions({
187-
authType = AuthTypes.PASSWORD,
188-
ipType = IpAddressTypes.PUBLIC,
189-
instanceConnectionName,
190-
}: ConnectionOptions): Promise<DriverOptions> {
188+
authType = AuthTypes.PASSWORD,
189+
ipType = IpAddressTypes.PUBLIC,
190+
instanceConnectionName,
191+
domainName,
192+
}: ConnectionOptions): Promise<DriverOptions> {
191193
const {instances} = this;
192194
await instances.loadInstance({
193195
ipType,
194196
authType,
195197
instanceConnectionName,
196-
sqlAdminFetcher: this.sqlAdminFetcher,
198+
domainName
197199
});
198200

199201
return {
200202
stream() {
201203
const cloudSqlInstance = instances.getInstance({
204+
ipType,
202205
instanceConnectionName,
206+
domainName,
203207
authType,
204208
});
205209
const {
@@ -249,10 +253,10 @@ export class Connector {
249253
}
250254

251255
async getTediousOptions({
252-
authType,
253-
ipType,
254-
instanceConnectionName,
255-
}: ConnectionOptions): Promise<TediousDriverOptions> {
256+
authType,
257+
ipType,
258+
instanceConnectionName,
259+
}: ConnectionOptions): Promise<TediousDriverOptions> {
256260
if (authType === AuthTypes.IAM) {
257261
throw new CloudSQLConnectorError({
258262
message: 'Tedious does not support Auto IAM DB Authentication',
@@ -289,11 +293,11 @@ export class Connector {
289293
// `postgresql://${user}@localhost/${database}?host=${process.cwd()}`;
290294
// const prisma = new PrismaClient({ datasourceUrl });
291295
async startLocalProxy({
292-
authType,
293-
ipType,
294-
instanceConnectionName,
295-
listenOptions,
296-
}: SocketConnectionOptions): Promise<void> {
296+
authType,
297+
ipType,
298+
instanceConnectionName,
299+
listenOptions,
300+
}: SocketConnectionOptions): Promise<void> {
297301
const {stream} = await this.getOptions({
298302
authType,
299303
ipType,
@@ -344,4 +348,8 @@ export class Connector {
344348
socket.destroy();
345349
}
346350
}
351+
352+
isClosed():boolean {
353+
return this.closed;
354+
}
347355
}

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
}

src/parse-instance-connection-name.ts

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,19 +16,27 @@ import {InstanceConnectionInfo} from './instance-connection-info';
1616
import {CloudSQLConnectorError} from './errors';
1717
import {resolveTxtRecord} from './dns-lookup';
1818

19+
export function isSameInstance(a: InstanceConnectionInfo, b: InstanceConnectionInfo): boolean {
20+
return a.instanceId == b.instanceId &&
21+
a.regionId == b.regionId &&
22+
a.projectId == b.projectId &&
23+
a.domainName == b.domainName;
24+
}
25+
1926
export async function resolveInstanceName(
20-
name: string | undefined
27+
instanceConnectionName?: string,
28+
domainName?: string
2129
): Promise<InstanceConnectionInfo> {
22-
if (!name) {
30+
if (!instanceConnectionName && !domainName) {
2331
throw new CloudSQLConnectorError({
2432
message:
25-
'Missing instance connection name, expected: "PROJECT:REGION:INSTANCE"',
33+
'Missing instance connection name, expected: "PROJECT:REGION:INSTANCE" or a valid domain name.',
2634
code: 'ENOCONNECTIONNAME',
2735
});
28-
} else if (isInstanceConnectionName(name)) {
29-
return parseInstanceConnectionName(name);
30-
} else if (isValidDomainName(name)) {
31-
return await resolveDomainName(name);
36+
} else if (instanceConnectionName && isInstanceConnectionName(instanceConnectionName)) {
37+
return parseInstanceConnectionName(instanceConnectionName);
38+
} else if (domainName && isValidDomainName(domainName)) {
39+
return await resolveDomainName(domainName);
3240
} else {
3341
throw new CloudSQLConnectorError({
3442
message:

system-test/pg-connect.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ t.test(
145145
async t => {
146146
const connector = new Connector();
147147
const clientOpts = await connector.getOptions({
148-
instanceConnectionName: String(
148+
domainName: String(
149149
process.env.POSTGRES_CUSTOMER_CAS_DOMAIN_NAME
150150
),
151151
});
@@ -177,7 +177,7 @@ t.test(
177177
async t => {
178178
const connector = new Connector();
179179
const clientOpts = await connector.getOptions({
180-
instanceConnectionName: String(
180+
domainName: String(
181181
process.env.POSTGRES_CUSTOMER_CAS_INVALID_DOMAIN_NAME
182182
),
183183
});

0 commit comments

Comments
 (0)