12
12
// See the License for the specific language governing permissions and
13
13
// limitations under the License.
14
14
15
- import { createServer , Server , Socket } from 'node:net' ;
15
+ import { createServer , Server } from 'node:net' ;
16
16
import tls from 'node:tls' ;
17
17
import { promisify } from 'node:util' ;
18
18
import { AuthClient , GoogleAuth } from 'google-auth-library' ;
@@ -22,6 +22,10 @@ import {IpAddressTypes} from './ip-addresses';
22
22
import { AuthTypes } from './auth-types' ;
23
23
import { SQLAdminFetcher } from './sqladmin-fetcher' ;
24
24
import { CloudSQLConnectorError } from './errors' ;
25
+ import { SocketWrapper , SocketWrapperOptions } from './socket-wrapper' ;
26
+ import stream from 'node:stream' ;
27
+ import { resolveInstanceName } from './parse-instance-connection-name' ;
28
+ import { InstanceConnectionInfo } from './instance-connection-info' ;
25
29
26
30
// These Socket types are subsets from nodejs definitely typed repo, ref:
27
31
// https://github.com/DefinitelyTyped/DefinitelyTyped/blob/ae0fe42ff0e6e820e8ae324acf4f8e944aa1b2b7/types/node/v18/net.d.ts#L437
@@ -53,11 +57,13 @@ export declare interface SocketConnectionOptions extends ConnectionOptions {
53
57
}
54
58
55
59
interface StreamFunction {
56
- ( ) : tls . TLSSocket ;
60
+ //eslint-disable-next-line @typescript-eslint/no-explicit-any
61
+ ( ...opts : any | undefined ) : stream . Duplex ;
57
62
}
58
63
59
64
interface PromisedStreamFunction {
60
- ( ) : Promise < tls . TLSSocket > ;
65
+ //eslint-disable-next-line @typescript-eslint/no-explicit-any
66
+ ( ...opts : any | undefined ) : Promise < stream . Duplex > ;
61
67
}
62
68
63
69
// DriverOptions is the interface describing the object returned by
@@ -108,33 +114,42 @@ class CloudSQLInstanceMap extends Map<string, CacheEntry> {
108
114
this . sqlAdminFetcher = sqlAdminFetcher ;
109
115
}
110
116
111
- private cacheKey ( opts : ConnectionOptions ) : string {
112
- //TODO: for now, the cache key function must be synchronous.
113
- // When we implement the async connection info from
114
- // https://github.com/GoogleCloudPlatform/cloud-sql-nodejs-connector/pull/426
115
- // then the cache key should contain both the domain name
116
- // and the resolved instance name.
117
- return (
118
- ( opts . instanceConnectionName || opts . domainName ) +
119
- '-' +
120
- opts . authType +
121
- '-' +
122
- opts . ipType
123
- ) ;
117
+ private async cacheKey (
118
+ instanceName : InstanceConnectionInfo ,
119
+ opts : ConnectionOptions
120
+ ) : Promise < string > {
121
+ let key : Array < string > ;
122
+ if ( instanceName . domainName ) {
123
+ key = [ instanceName . domainName ] ;
124
+ } else {
125
+ key = [
126
+ instanceName . projectId ,
127
+ instanceName . regionId ,
128
+ instanceName . instanceId ,
129
+ ] ;
130
+ }
131
+ key . push ( String ( opts . authType ) ) ;
132
+ key . push ( String ( opts . ipType ) ) ;
133
+
134
+ return key . join ( '-' ) ;
124
135
}
125
136
126
- async loadInstance ( opts : ConnectionOptions ) : Promise < void > {
137
+ async loadInstance ( opts : ConnectionOptions ) : Promise < CloudSQLInstance > {
127
138
// in case an instance to that connection name has already
128
139
// been setup there's no need to set it up again
129
- const key = this . cacheKey ( opts ) ;
140
+ const instanceName = await resolveInstanceName (
141
+ opts . instanceConnectionName ,
142
+ opts . domainName
143
+ ) ;
144
+ const key = await this . cacheKey ( instanceName , opts ) ;
130
145
const entry = this . get ( key ) ;
131
146
if ( entry ) {
132
147
if ( entry . isResolved ( ) ) {
133
148
await entry . instance ?. checkDomainChanged ( ) ;
134
149
if ( ! entry . instance ?. isClosed ( ) ) {
135
150
// The instance is open and the domain has not changed.
136
151
// use the cached instance.
137
- return ;
152
+ return entry . promise ;
138
153
}
139
154
} else if ( entry . isError ( ) ) {
140
155
// The instance failed it's initial refresh. Remove it from the
@@ -143,36 +158,28 @@ class CloudSQLInstanceMap extends Map<string, CacheEntry> {
143
158
throw entry . err ;
144
159
} else {
145
160
// The instance initial refresh is in progress.
146
- await entry . promise ;
147
- return ;
161
+ return entry . promise ;
148
162
}
149
163
}
150
164
151
165
// Start the refresh and add a cache entry.
152
- const promise = CloudSQLInstance . getCloudSQLInstance ( {
166
+ const instanceOpts = {
153
167
instanceConnectionName : opts . instanceConnectionName ,
154
168
domainName : opts . domainName ,
155
169
authType : opts . authType || AuthTypes . PASSWORD ,
156
170
ipType : opts . ipType || IpAddressTypes . PUBLIC ,
157
171
limitRateInterval : opts . limitRateInterval || 30 * 1000 , // 30 sec
158
172
sqlAdminFetcher : this . sqlAdminFetcher ,
159
173
checkDomainInterval : opts . checkDomainInterval ,
160
- } ) ;
174
+ } ;
175
+ const promise = CloudSQLInstance . getCloudSQLInstance (
176
+ instanceName ,
177
+ instanceOpts
178
+ ) ;
161
179
this . set ( key , new CacheEntry ( promise ) ) ;
162
180
163
181
// Wait for the cache entry to resolve.
164
- await promise ;
165
- }
166
-
167
- getInstance ( opts : ConnectionOptions ) : CloudSQLInstance {
168
- const connectionInstance = this . get ( this . cacheKey ( opts ) ) ;
169
- if ( ! connectionInstance || ! connectionInstance . instance ) {
170
- throw new CloudSQLConnectorError ( {
171
- message : `Cannot find info for instance: ${ opts . instanceConnectionName } ` ,
172
- code : 'ENOINSTANCEINFO' ,
173
- } ) ;
174
- }
175
- return connectionInstance . instance ;
182
+ return promise ;
176
183
}
177
184
}
178
185
@@ -193,7 +200,7 @@ export class Connector {
193
200
private readonly instances : CloudSQLInstanceMap ;
194
201
private readonly sqlAdminFetcher : SQLAdminFetcher ;
195
202
private readonly localProxies : Set < Server > ;
196
- private readonly sockets : Set < Socket > ;
203
+ private readonly sockets : Set < stream . Duplex > ;
197
204
198
205
constructor ( opts : ConnectorOptions = { } ) {
199
206
this . sqlAdminFetcher = new SQLAdminFetcher ( {
@@ -207,69 +214,95 @@ export class Connector {
207
214
this . sockets = new Set ( ) ;
208
215
}
209
216
210
- // Connector.getOptions is a method that accepts a Cloud SQL instance
211
- // connection name along with the connection type and returns an object
212
- // that can be used to configure a driver to be used with Cloud SQL. e.g:
213
- //
214
- // const connector = new Connector()
215
- // const opts = await connector.getOptions({
216
- // ipType: 'PUBLIC',
217
- // instanceConnectionName: 'PROJECT:REGION:INSTANCE',
218
- // });
219
- // const pool = new Pool(opts)
220
- // const res = await pool.query('SELECT * FROM pg_catalog.pg_tables;')
221
- async getOptions ( opts : ConnectionOptions ) : Promise < DriverOptions > {
222
- const { instances} = this ;
223
- await instances . loadInstance ( opts ) ;
217
+ async connect ( opts : ConnectionOptions ) : Promise < tls . TLSSocket > {
218
+ const cloudSqlInstance = await this . instances . loadInstance ( opts ) ;
219
+
220
+ const {
221
+ instanceInfo,
222
+ ephemeralCert,
223
+ host,
224
+ port,
225
+ privateKey,
226
+ serverCaCert,
227
+ serverCaMode,
228
+ dnsName,
229
+ } = cloudSqlInstance ;
230
+
231
+ if (
232
+ instanceInfo &&
233
+ ephemeralCert &&
234
+ host &&
235
+ port &&
236
+ privateKey &&
237
+ serverCaCert
238
+ ) {
239
+ const tlsSocket = getSocket ( {
240
+ instanceInfo,
241
+ ephemeralCert,
242
+ host,
243
+ port,
244
+ privateKey,
245
+ serverCaCert,
246
+ serverCaMode,
247
+ dnsName : instanceInfo . domainName || dnsName , // use the configured domain name, or the instance dnsName.
248
+ } ) ;
249
+ tlsSocket . once ( 'error' , ( ) => {
250
+ cloudSqlInstance . forceRefresh ( ) ;
251
+ } ) ;
252
+ tlsSocket . once ( 'secureConnect' , async ( ) => {
253
+ cloudSqlInstance . setEstablishedConnection ( ) ;
254
+ } ) ;
255
+ return tlsSocket ;
256
+ }
257
+ throw new CloudSQLConnectorError ( {
258
+ message : 'Invalid Cloud SQL Instance info' ,
259
+ code : 'EBADINSTANCEINFO' ,
260
+ } ) ;
261
+ }
224
262
263
+ getOptions ( {
264
+ authType = AuthTypes . PASSWORD ,
265
+ ipType = IpAddressTypes . PUBLIC ,
266
+ instanceConnectionName,
267
+ } : ConnectionOptions ) : DriverOptions {
268
+ // bring 'this' into a closure-scope variable.
269
+ //eslint-disable-next-line @typescript-eslint/no-this-alias
270
+ const connector = this ;
225
271
return {
226
- stream ( ) {
227
- const cloudSqlInstance = instances . getInstance ( opts ) ;
228
- const {
229
- instanceInfo,
230
- ephemeralCert,
231
- host,
232
- port,
233
- privateKey,
234
- serverCaCert,
235
- serverCaMode,
236
- dnsName,
237
- } = cloudSqlInstance ;
238
-
239
- if (
240
- instanceInfo &&
241
- ephemeralCert &&
242
- host &&
243
- port &&
244
- privateKey &&
245
- serverCaCert
246
- ) {
247
- const tlsSocket = getSocket ( {
248
- instanceInfo,
249
- ephemeralCert,
250
- host,
251
- port,
252
- privateKey,
253
- serverCaCert,
254
- serverCaMode,
255
- dnsName : instanceInfo . domainName || dnsName , // use the configured domain name, or the instance dnsName.
256
- } ) ;
257
- tlsSocket . once ( 'error' , ( ) => {
258
- cloudSqlInstance . forceRefresh ( ) ;
259
- } ) ;
260
- tlsSocket . once ( 'secureConnect' , async ( ) => {
261
- cloudSqlInstance . setEstablishedConnection ( ) ;
262
- } ) ;
263
-
264
- cloudSqlInstance . addSocket ( tlsSocket ) ;
265
-
266
- return tlsSocket ;
272
+ stream ( opts ) {
273
+ let host ;
274
+ let startConnection = false ;
275
+ if ( opts ) {
276
+ if ( opts ?. config ?. host ) {
277
+ // Mysql driver passes the host in the options, and expects
278
+ // this to start the connection.
279
+ host = opts ?. config ?. host ;
280
+ startConnection = true ;
281
+ }
282
+ if ( opts ?. host ) {
283
+ // Sql Server (Tedious) driver passes host in the options
284
+ // this to start the connection.
285
+ host = opts ?. host ;
286
+ startConnection = true ;
287
+ }
288
+ } else {
289
+ // Postgres driver does not pass options.
290
+ // Postgres will call Socket.connect(port,host).
291
+ startConnection = false ;
267
292
}
268
293
269
- throw new CloudSQLConnectorError ( {
270
- message : 'Invalid Cloud SQL Instance info' ,
271
- code : 'EBADINSTANCEINFO' ,
272
- } ) ;
294
+ return new SocketWrapper (
295
+ new SocketWrapperOptions ( {
296
+ connector,
297
+ host,
298
+ startConnection,
299
+ connectionConfig : {
300
+ authType,
301
+ ipType,
302
+ instanceConnectionName,
303
+ } ,
304
+ } )
305
+ ) ;
273
306
} ,
274
307
} ;
275
308
}
@@ -291,8 +324,8 @@ export class Connector {
291
324
instanceConnectionName,
292
325
} ) ;
293
326
return {
294
- async connector ( ) {
295
- return driverOptions . stream ( ) ;
327
+ async connector ( opts ) {
328
+ return driverOptions . stream ( opts ) ;
296
329
} ,
297
330
// note: the connector handles a secured encrypted connection
298
331
// with that in mind, the driver encryption is disabled here
0 commit comments