Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,295 changes: 1,065 additions & 230 deletions package-lock.json

Large diffs are not rendered by default.

20 changes: 10 additions & 10 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,20 +63,20 @@
"@sequelize/mssql": "^7.0.0-alpha.29",
"@sequelize/mysql": "^7.0.0-alpha.29",
"@sequelize/postgres": "^7.0.0-alpha.29",
"@types/node": "^24.12.4",
"@types/node": "^24.13.3",
"@types/pg": "^8.20.0",
"@typescript-eslint/eslint-plugin": "^7.18.0",
"eslint-plugin-prettier": "^5.5.5",
"eslint-plugin-prettier": "^5.5.6",
"gts": "^5.3.1",
"knex": "^3.2.10",
"mssql": "^12.5.4",
"mysql2": "^3.22.3",
"nock": "^14.0.15",
"pg": "^8.21.0",
"knex": "^3.3.0",
"mssql": "^12.7.0",
"mysql2": "^3.23.1",
"nock": "^14.0.16",
"pg": "^8.22.0",
"prisma": "^5.22.0",
"tap": "^21.7.4",
"tedious": "^20.0.0",
"typeorm": "^1.0.0",
"typeorm": "^1.1.0",
"typescript": "^5.9.3"
},
"engines": {
Expand All @@ -88,8 +88,8 @@
},
"dependencies": {
"@googleapis/sqladmin": "^37.0.0",
"gaxios": "^7.1.4",
"google-auth-library": "^10.6.2",
"gaxios": "^7.3.0",
"google-auth-library": "^10.9.1",
"p-throttle": "^8.1.0"
}
}
86 changes: 74 additions & 12 deletions src/cloud-sql-instance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import {InstanceConnectionInfo} from './instance-connection-info';
import {
isSameInstance,
resolveInstanceName,
isInstanceDNSName,
parseInstanceConnectionName,
} from './parse-instance-connection-name';
import {resolveARecord} from './dns-lookup';
import {InstanceMetadata} from './sqladmin-fetcher';
Expand Down Expand Up @@ -47,11 +49,12 @@ interface Fetcher {
publicKey: string,
authType: AuthTypes
): Promise<SslCert>;
resolveConnectSettings(region: string, dnsName: string): Promise<string>;
}

interface CloudSQLInstanceOptions {
authType: AuthTypes;
instanceConnectionName: string;
instanceConnectionName?: string;
domainName?: string;
ipType: IpAddressTypes;
limitRateInterval?: number;
Expand All @@ -72,7 +75,8 @@ export class CloudSQLInstance {
): Promise<CloudSQLInstance> {
const instanceInfo = await resolveInstanceName(
options.instanceConnectionName,
options.domainName
options.domainName,
options.sqlAdminFetcher
);
const instance = new CloudSQLInstance({
options: options,
Expand Down Expand Up @@ -111,14 +115,34 @@ export class CloudSQLInstance {
instanceInfo,
}: {
options: CloudSQLInstanceOptions;
instanceInfo: InstanceConnectionInfo;
instanceInfo?: InstanceConnectionInfo;
}) {
this.instanceInfo = instanceInfo;
this.authType = options.authType || AuthTypes.PASSWORD;
this.ipType = options.ipType || IpAddressTypes.PUBLIC;
this.limitRateInterval = options.limitRateInterval || 30 * 1000; // 30 seconds
this.sqlAdminFetcher = options.sqlAdminFetcher;
this.failoverPeriod = options.failoverPeriod || 30 * 1000; // 30 seconds

if (instanceInfo) {
this.instanceInfo = instanceInfo;
} else if (options.instanceConnectionName) {
this.instanceInfo = parseInstanceConnectionName(
options.instanceConnectionName
);
} else if (options.domainName) {
this.instanceInfo = {
projectId: '',
regionId: '',
instanceId: '',
domainName: options.domainName,
};
} else {
throw new CloudSQLConnectorError({
message:
'instanceInfo or instanceConnectionName/domainName is required',
code: 'ENOCONNECTIONNAME',
});
}
}

// p-throttle library has to be initialized in an async scope in order to
Expand Down Expand Up @@ -177,13 +201,19 @@ export class CloudSQLInstance {
// Lazy instantiation of the checkDomain interval on the first refresh
// This avoids issues with test cases that instantiate a CloudSqlInstance.
// If failoverPeriod is 0 (or negative) don't check for DNS updates.
const isInstanceDNS = isInstanceDNSName(
this.instanceInfo?.domainName || ''
);
if (
this?.instanceInfo?.domainName &&
!isInstanceDNS &&
!this.checkDomainID &&
this.failoverPeriod > 0
) {
this.checkDomainID = setInterval(() => {
this.checkDomainChanged();
this.checkDomainChanged().catch(() => {
// ignore unhandled rejections
});
}, this.failoverPeriod);
}

Expand Down Expand Up @@ -381,14 +411,46 @@ export class CloudSQLInstance {
return;
}

const newInfo = await resolveInstanceName(
undefined,
this.instanceInfo.domainName
);
if (!isSameInstance(this.instanceInfo, newInfo)) {
// Domain name changed. Close and remove, then create a new map entry.
this.close();
try {
const newInfo = await resolveInstanceName(
undefined,
this.instanceInfo.domainName,
this.sqlAdminFetcher
);
if (!isSameInstance(this.instanceInfo, newInfo)) {
// Domain name changed. Close and remove, then create a new map entry.
this.close();
}
} catch (err) {
if (this.isNonTransient(err)) {
this.close();
}
}
}

private isNonTransient(err: unknown): boolean {
if (err instanceof CloudSQLConnectorError) {
if (
err.code === 'ECNAMELOOPLIMITEXCEEDED' ||
err.code === 'EDNSRESOLVERNOTINITIALIZED' ||
err.code === 'EBADCONNECTIONNAME'
) {
return true;
}
if (err.code === 'ENOSQLADMINRESOLVE') {
for (const nestedErr of err.errors) {
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
const response = (nestedErr as any).response;
if (
response &&
(response.status === 403 || response.status === 404)
) {
return true;
}
}
}
}
return false;
}
addSocket(socket: DestroyableSocket) {
if (!this.instanceInfo.domainName) {
Expand Down
6 changes: 4 additions & 2 deletions src/connector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export declare interface UnixSocketOptions {
export declare interface ConnectionOptions {
authType?: AuthTypes;
ipType?: IpAddressTypes;
instanceConnectionName: string;
instanceConnectionName?: string;
domainName?: string;
failoverPeriod?: number;
limitRateInterval?: number;
Expand Down Expand Up @@ -168,7 +168,9 @@ class CloudSQLInstanceMap extends Map<string, CacheEntry> {
const connectionInstance = this.get(this.cacheKey(opts));
if (!connectionInstance || !connectionInstance.instance) {
throw new CloudSQLConnectorError({
message: `Cannot find info for instance: ${opts.instanceConnectionName}`,
message: `Cannot find info for instance: ${
opts.instanceConnectionName || opts.domainName
}`,
code: 'ENOINSTANCEINFO',
});
}
Expand Down
23 changes: 19 additions & 4 deletions src/dns-lookup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
import dns from 'node:dns';
import {CloudSQLConnectorError} from './errors';

export async function resolveTxtRecord(name: string): Promise<string> {
export async function resolveTxtRecord(name: string): Promise<string[]> {
return new Promise((resolve, reject) => {
dns.resolveTxt(name, (err, addresses) => {
if (err) {
Expand All @@ -41,10 +41,9 @@ export async function resolveTxtRecord(name: string): Promise<string> {

// Each result may be split into multiple strings. Join the strings.
const joinedAddresses = addresses.map(strs => strs.join(''));
// Sort the results alphabetically for consistency,
// Sort the results alphabetically for consistency.
joinedAddresses.sort((a, b) => a.localeCompare(b));
// Return the first result.
resolve(joinedAddresses[0]);
resolve(joinedAddresses);
});
});
}
Expand All @@ -60,3 +59,19 @@ export async function resolveARecord(name: string): Promise<string[]> {
});
});
}

export async function resolveCnameRecord(name: string): Promise<string> {
return new Promise((resolve, reject) => {
dns.resolveCname(name, (err, addresses) => {
if (err) {
reject(err);
return;
}
if (!addresses || addresses.length === 0) {
reject(new Error('No CNAME records found for ' + name));
return;
}
resolve(addresses[0]);
});
});
}
Loading
Loading