Skip to content

Commit bec7250

Browse files
committed
refactor: Load connection info asynchronously.
wip: make it work with mysql wip: connector test fixes.
1 parent ef914ea commit bec7250

File tree

9 files changed

+727
-128
lines changed

9 files changed

+727
-128
lines changed

src/connector.ts

Lines changed: 136 additions & 92 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, SocketWrapperOptions} 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,21 @@ 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+
//eslint-disable-next-line @typescript-eslint/no-explicit-any
56+
(...opts: any | undefined): stream.Duplex;
5457
}
5558

5659
interface PromisedStreamFunction {
57-
(): Promise<tls.TLSSocket>;
60+
//eslint-disable-next-line @typescript-eslint/no-explicit-any
61+
(...opts: any | undefined): Promise<stream.Duplex>;
5862
}
5963

6064
// DriverOptions is the interface describing the object returned by
@@ -86,11 +90,17 @@ class CloudSQLInstanceMap extends Map {
8690
authType: AuthTypes;
8791
instanceConnectionName: string;
8892
sqlAdminFetcher: SQLAdminFetcher;
89-
}): Promise<void> {
93+
}): Promise<CloudSQLInstance> {
9094
// in case an instance to that connection name has already
9195
// been setup there's no need to set it up again
9296
if (this.has(instanceConnectionName)) {
9397
const instance = this.get(instanceConnectionName);
98+
if (!instance) {
99+
throw new CloudSQLConnectorError({
100+
message: `Cannot find info for instance: ${instanceConnectionName}`,
101+
code: 'ENOINSTANCEINFO',
102+
});
103+
}
94104
if (instance.authType && instance.authType !== authType) {
95105
throw new CloudSQLConnectorError({
96106
message:
@@ -100,42 +110,23 @@ class CloudSQLInstanceMap extends Map {
100110
code: 'EMISMATCHAUTHTYPE',
101111
});
102112
}
103-
return;
113+
return instance;
104114
}
115+
105116
const connectionInstance = await CloudSQLInstance.getCloudSQLInstance({
106117
ipType,
107118
authType,
108119
instanceConnectionName,
109120
sqlAdminFetcher: sqlAdminFetcher,
110121
});
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);
122122
if (!connectionInstance) {
123123
throw new CloudSQLConnectorError({
124124
message: `Cannot find info for instance: ${instanceConnectionName}`,
125125
code: 'ENOINSTANCEINFO',
126126
});
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-
});
138127
}
128+
this.set(instanceConnectionName, connectionInstance);
129+
139130
return connectionInstance;
140131
}
141132
}
@@ -158,7 +149,7 @@ export class Connector {
158149
private readonly instances: CloudSQLInstanceMap;
159150
private readonly sqlAdminFetcher: SQLAdminFetcher;
160151
private readonly localProxies: Set<Server>;
161-
private readonly sockets: Set<Socket>;
152+
private readonly sockets: Set<stream.Duplex>;
162153

163154
constructor(opts: ConnectorOptions = {}) {
164155
this.instances = new CloudSQLInstanceMap();
@@ -171,79 +162,132 @@ export class Connector {
171162
this.localProxies = new Set();
172163
this.sockets = new Set();
173164
}
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({
165+
async loadInstance({
187166
authType = AuthTypes.PASSWORD,
188167
ipType = IpAddressTypes.PUBLIC,
189168
instanceConnectionName,
190-
}: ConnectionOptions): Promise<DriverOptions> {
191-
const {instances} = this;
192-
await instances.loadInstance({
169+
}: ConnectionOptions): Promise<CloudSQLInstance> {
170+
if (!instanceConnectionName) {
171+
throw new CloudSQLConnectorError({
172+
code: 'ENOTCONFIGURED',
173+
message: 'Instance connection name missing.',
174+
});
175+
}
176+
177+
const inst = await this.instances.loadInstance({
193178
ipType,
194179
authType,
195180
instanceConnectionName,
196181
sqlAdminFetcher: this.sqlAdminFetcher,
197182
});
198183

199-
return {
200-
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;
184+
return inst;
185+
}
215186

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;
187+
async connect({
188+
authType = AuthTypes.PASSWORD,
189+
ipType = IpAddressTypes.PUBLIC,
190+
instanceConnectionName,
191+
}: ConnectionOptions): Promise<tls.TLSSocket> {
192+
if (!instanceConnectionName) {
193+
throw new CloudSQLConnectorError({
194+
code: 'ENOTCONFIGURED',
195+
message: 'Instance connection name missing.',
196+
});
197+
}
198+
199+
const cloudSqlInstance = await this.loadInstance({
200+
ipType,
201+
authType,
202+
instanceConnectionName,
203+
});
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+
}
242+
throw new CloudSQLConnectorError({
243+
message: 'Invalid Cloud SQL Instance info',
244+
code: 'EBADINSTANCEINFO',
245+
});
246+
}
247+
248+
getOptions({
249+
authType = AuthTypes.PASSWORD,
250+
ipType = IpAddressTypes.PUBLIC,
251+
instanceConnectionName,
252+
}: ConnectionOptions): DriverOptions {
253+
// bring 'this' into a closure-scope variable.
254+
//eslint-disable-next-line @typescript-eslint/no-this-alias
255+
const connector = this;
256+
return {
257+
stream(opts) {
258+
let host;
259+
let startConnection = false;
260+
if (opts) {
261+
if (opts?.config?.host) {
262+
// Mysql driver passes the host in the options, and expects
263+
// this to start the connection.
264+
host = opts?.config?.host;
265+
startConnection = true;
266+
}
267+
if (opts?.host) {
268+
// Sql Server (Tedious) driver passes host in the options
269+
// this to start the connection.
270+
host = opts?.host;
271+
startConnection = true;
272+
}
273+
} else {
274+
// Postgres driver does not pass options.
275+
// Postgres will call Socket.connect(port,host).
276+
startConnection = false;
241277
}
242278

243-
throw new CloudSQLConnectorError({
244-
message: 'Invalid Cloud SQL Instance info',
245-
code: 'EBADINSTANCEINFO',
246-
});
279+
return new SocketWrapper(
280+
new SocketWrapperOptions({
281+
connector,
282+
host,
283+
startConnection,
284+
connectionConfig: {
285+
authType,
286+
ipType,
287+
instanceConnectionName,
288+
},
289+
})
290+
);
247291
},
248292
};
249293
}
@@ -265,8 +309,8 @@ export class Connector {
265309
instanceConnectionName,
266310
});
267311
return {
268-
async connector() {
269-
return driverOptions.stream();
312+
async connector(opts) {
313+
return driverOptions.stream(opts);
270314
},
271315
// note: the connector handles a secured encrypted connection
272316
// with that in mind, the driver encryption is disabled here

0 commit comments

Comments
 (0)