Skip to content

Commit 07a4a53

Browse files
committed
refactor: Load connection info asynchronously.
1 parent f24d64b commit 07a4a53

File tree

6 files changed

+448
-123
lines changed

6 files changed

+448
-123
lines changed

src/connector.ts

Lines changed: 107 additions & 89 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 {Server, createServer} 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,8 @@ import {IpAddressTypes} from './ip-addresses';
2222
import {AuthTypes} from './auth-types';
2323
import {SQLAdminFetcher} from './sqladmin-fetcher';
2424
import {CloudSQLConnectorError} from './errors';
25+
import {SocketWrapper} from './socket-wrapper';
26+
import stream from 'node:stream';
2527

2628
// These Socket types are subsets from nodejs definitely typed repo, ref:
2729
// https://github.com/DefinitelyTyped/DefinitelyTyped/blob/ae0fe42ff0e6e820e8ae324acf4f8e944aa1b2b7/types/node/v18/net.d.ts#L437
@@ -42,19 +44,19 @@ export declare interface UnixSocketOptions {
4244
export declare interface ConnectionOptions {
4345
authType?: AuthTypes;
4446
ipType?: IpAddressTypes;
45-
instanceConnectionName: string;
47+
instanceConnectionName?: string;
4648
}
4749

4850
export declare interface SocketConnectionOptions extends ConnectionOptions {
4951
listenOptions: UnixSocketOptions;
5052
}
5153

5254
interface StreamFunction {
53-
(): tls.TLSSocket;
55+
(): stream.Duplex;
5456
}
5557

5658
interface PromisedStreamFunction {
57-
(): Promise<tls.TLSSocket>;
59+
(): Promise<stream.Duplex>;
5860
}
5961

6062
// DriverOptions is the interface describing the object returned by
@@ -86,11 +88,17 @@ class CloudSQLInstanceMap extends Map {
8688
authType: AuthTypes;
8789
instanceConnectionName: string;
8890
sqlAdminFetcher: SQLAdminFetcher;
89-
}): Promise<void> {
91+
}): Promise<CloudSQLInstance> {
9092
// in case an instance to that connection name has already
9193
// been setup there's no need to set it up again
9294
if (this.has(instanceConnectionName)) {
9395
const instance = this.get(instanceConnectionName);
96+
if (!instance) {
97+
throw new CloudSQLConnectorError({
98+
message: `Cannot find info for instance: ${instanceConnectionName}`,
99+
code: 'ENOINSTANCEINFO',
100+
});
101+
}
94102
if (instance.authType && instance.authType !== authType) {
95103
throw new CloudSQLConnectorError({
96104
message:
@@ -100,42 +108,23 @@ class CloudSQLInstanceMap extends Map {
100108
code: 'EMISMATCHAUTHTYPE',
101109
});
102110
}
103-
return;
111+
return instance;
104112
}
113+
105114
const connectionInstance = await CloudSQLInstance.getCloudSQLInstance({
106115
ipType,
107116
authType,
108117
instanceConnectionName,
109118
sqlAdminFetcher: sqlAdminFetcher,
110119
});
111-
this.set(instanceConnectionName, connectionInstance);
112-
}
113-
114-
getInstance({
115-
instanceConnectionName,
116-
authType,
117-
}: {
118-
instanceConnectionName: string;
119-
authType: AuthTypes;
120-
}): CloudSQLInstance {
121-
const connectionInstance = this.get(instanceConnectionName);
122120
if (!connectionInstance) {
123121
throw new CloudSQLConnectorError({
124122
message: `Cannot find info for instance: ${instanceConnectionName}`,
125123
code: 'ENOINSTANCEINFO',
126124
});
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-
});
138125
}
126+
this.set(instanceConnectionName, connectionInstance);
127+
139128
return connectionInstance;
140129
}
141130
}
@@ -158,7 +147,7 @@ export class Connector {
158147
private readonly instances: CloudSQLInstanceMap;
159148
private readonly sqlAdminFetcher: SQLAdminFetcher;
160149
private readonly localProxies: Set<Server>;
161-
private readonly sockets: Set<Socket>;
150+
private readonly sockets: Set<stream.Duplex>;
162151

163152
constructor(opts: ConnectorOptions = {}) {
164153
this.instances = new CloudSQLInstanceMap();
@@ -171,78 +160,107 @@ export class Connector {
171160
this.localProxies = new Set();
172161
this.sockets = new Set();
173162
}
174-
175-
// Connector.getOptions is a method that accepts a Cloud SQL instance
176-
// connection name along with the connection type and returns an object
177-
// that can be used to configure a driver to be used with Cloud SQL. e.g:
178-
//
179-
// const connector = new Connector()
180-
// const opts = await connector.getOptions({
181-
// ipType: 'PUBLIC',
182-
// instanceConnectionName: 'PROJECT:REGION:INSTANCE',
183-
// });
184-
// const pool = new Pool(opts)
185-
// const res = await pool.query('SELECT * FROM pg_catalog.pg_tables;')
186-
async getOptions({
163+
async loadInstance({
187164
authType = AuthTypes.PASSWORD,
188165
ipType = IpAddressTypes.PUBLIC,
189166
instanceConnectionName,
190-
}: ConnectionOptions): Promise<DriverOptions> {
191-
const {instances} = this;
192-
await instances.loadInstance({
167+
}: ConnectionOptions): Promise<CloudSQLInstance> {
168+
if (!instanceConnectionName) {
169+
throw new CloudSQLConnectorError({
170+
code: 'ENOTCONFIGURED',
171+
message: 'Instance connection name missing.',
172+
});
173+
}
174+
175+
const inst = await this.instances.loadInstance({
193176
ipType,
194177
authType,
195178
instanceConnectionName,
196179
sqlAdminFetcher: this.sqlAdminFetcher,
197180
});
198181

182+
return inst;
183+
}
184+
185+
async connect({
186+
authType = AuthTypes.PASSWORD,
187+
ipType = IpAddressTypes.PUBLIC,
188+
instanceConnectionName,
189+
}: ConnectionOptions): Promise<tls.TLSSocket> {
190+
if (!instanceConnectionName) {
191+
throw new CloudSQLConnectorError({
192+
code: 'ENOTCONFIGURED',
193+
message: 'Instance connection name missing.',
194+
});
195+
}
196+
197+
const cloudSqlInstance = await this.loadInstance({
198+
ipType,
199+
authType,
200+
instanceConnectionName,
201+
});
202+
203+
const {
204+
instanceInfo,
205+
ephemeralCert,
206+
host,
207+
port,
208+
privateKey,
209+
serverCaCert,
210+
serverCaMode,
211+
dnsName,
212+
} = cloudSqlInstance;
213+
214+
if (
215+
instanceInfo &&
216+
ephemeralCert &&
217+
host &&
218+
port &&
219+
privateKey &&
220+
serverCaCert
221+
) {
222+
const tlsSocket = getSocket({
223+
instanceInfo,
224+
ephemeralCert,
225+
host,
226+
port,
227+
privateKey,
228+
serverCaCert,
229+
serverCaMode,
230+
dnsName: instanceInfo.domainName || dnsName, // use the configured domain name, or the instance dnsName.
231+
});
232+
tlsSocket.once('error', () => {
233+
cloudSqlInstance.forceRefresh();
234+
});
235+
tlsSocket.once('secureConnect', async () => {
236+
cloudSqlInstance.setEstablishedConnection();
237+
});
238+
return tlsSocket;
239+
}
240+
throw new CloudSQLConnectorError({
241+
message: 'Invalid Cloud SQL Instance info',
242+
code: 'EBADINSTANCEINFO',
243+
});
244+
}
245+
246+
getOptions({
247+
authType = AuthTypes.PASSWORD,
248+
ipType = IpAddressTypes.PUBLIC,
249+
instanceConnectionName,
250+
}: ConnectionOptions): DriverOptions {
251+
// bring 'this' into a closure-scope variable.
252+
//eslint-disable-next-line @typescript-eslint/no-this-alias
253+
const connector = this;
199254
return {
200255
stream() {
201-
const cloudSqlInstance = instances.getInstance({
202-
instanceConnectionName,
203-
authType,
204-
});
205-
const {
206-
instanceInfo,
207-
ephemeralCert,
208-
host,
209-
port,
210-
privateKey,
211-
serverCaCert,
212-
serverCaMode,
213-
dnsName,
214-
} = cloudSqlInstance;
215-
216-
if (
217-
instanceInfo &&
218-
ephemeralCert &&
219-
host &&
220-
port &&
221-
privateKey &&
222-
serverCaCert
223-
) {
224-
const tlsSocket = getSocket({
225-
instanceInfo,
226-
ephemeralCert,
227-
host,
228-
port,
229-
privateKey,
230-
serverCaCert,
231-
serverCaMode,
232-
dnsName: instanceInfo.domainName || dnsName, // use the configured domain name, or the instance dnsName.
233-
});
234-
tlsSocket.once('error', () => {
235-
cloudSqlInstance.forceRefresh();
236-
});
237-
tlsSocket.once('secureConnect', async () => {
238-
cloudSqlInstance.setEstablishedConnection();
239-
});
240-
return tlsSocket;
241-
}
256+
return new SocketWrapper({
257+
connector,
242258

243-
throw new CloudSQLConnectorError({
244-
message: 'Invalid Cloud SQL Instance info',
245-
code: 'EBADINSTANCEINFO',
259+
connectionConfig: {
260+
authType,
261+
ipType,
262+
instanceConnectionName,
263+
},
246264
});
247265
},
248266
};

0 commit comments

Comments
 (0)