@@ -67,57 +67,6 @@ export interface Options extends CreateCloudFilesystem {
67
67
account_id : string ;
68
68
}
69
69
70
- // creates bucket -- a race condition here would result in creating an extra bucket (garbage).
71
- // mutates input
72
- export async function ensureBucketExists (
73
- cloudFilesystem : Partial < CloudFilesystem > ,
74
- ) : Promise < string > {
75
- const { id } = cloudFilesystem ;
76
- if ( ! id ) {
77
- throw Error ( "ensureBucketExists failed -- id must be specified" ) ;
78
- }
79
- if ( cloudFilesystem . bucket ) {
80
- // already created
81
- return cloudFilesystem . bucket ;
82
- }
83
- logger . debug ( "ensureBucketExists: creating for " , id ) ;
84
- try {
85
- // randomized bucket name -- all GCS buckets are in a single global
86
- // namespace, but by using a uuid it's extremely unlikely that
87
- // a bucket name would ever not be avialable; also nobody will
88
- // ever guess a bucket name, which is an extra level of security.
89
- // If there is a conflict, it would be an error and the user
90
- // would just retry creating their bucket (it's much more likely
91
- // to hit a random networking error).
92
- const s = `-${ id } -${ uuid ( ) } ` ;
93
- const bucket = `${ ( await getGoogleCloudPrefix ( ) ) . slice (
94
- 0 ,
95
- 63 - s . length - 1 ,
96
- ) } ${ s } `;
97
- logger . debug ( "ensureBucketExists" , { bucket } ) ;
98
- // in place mutate!
99
- cloudFilesystem . bucket = bucket ;
100
-
101
- // create storage bucket -- for now only support google
102
- // cloud storage, as mentioned above.
103
- await createBucket ( bucket , bucketOptions ( cloudFilesystem ) ) ;
104
- const pool = getPool ( ) ;
105
- await pool . query ( "UPDATE cloud_filesystems SET bucket=$1 WHERE id=$2" , [
106
- bucket ,
107
- id ,
108
- ] ) ;
109
- return bucket ;
110
- } catch ( err ) {
111
- logger . debug ( "ensureBucketExists: error " , err ) ;
112
- const pool = getPool ( ) ;
113
- await pool . query ( "UPDATE cloud_filesystems SET error=$1 WHERE id=$2" , [
114
- `${ err } ` ,
115
- id ,
116
- ] ) ;
117
- throw err ;
118
- }
119
- }
120
-
121
70
// - create service account that has access to storage bucket
122
71
// - mutates input
123
72
// - race condition would create identical service account twice, so not a problem.
@@ -193,6 +142,8 @@ export async function ensureServiceAccountExists(
193
142
}
194
143
}
195
144
145
+ const zeroPad = ( num , places ) => String ( num ) . padStart ( places , "0" ) ;
146
+
196
147
export async function createCloudFilesystem ( opts : Options ) : Promise < number > {
197
148
logger . debug ( "createCloudFilesystem" , opts ) ;
198
149
// copy to avoid mutating
@@ -210,10 +161,6 @@ export async function createCloudFilesystem(opts: Options): Promise<number> {
210
161
assertValidPath ( opts . mountpoint ) ;
211
162
}
212
163
213
- // always set mount to false during creation, so that it doesn't try to mount *while* we're creating the bucket.
214
- const mount_orig = opts . mount ;
215
- opts . mount = false ;
216
-
217
164
if (
218
165
opts [ "block_size" ] < MIN_BLOCK_SIZE ||
219
166
opts [ "block_size" ] > MAX_BLOCK_SIZE
@@ -273,86 +220,76 @@ export async function createCloudFilesystem(opts: Options): Promise<number> {
273
220
// there could be a race condition if user tries to make two cloud filesystems at
274
221
// same time for same project -- one would fail and they get an error due to
275
222
// database uniqueness constraint. That's fine for now.
276
- const project_specific_id = await getAvailabelProjectSpecificId (
223
+ const project_specific_id = await getAvailableProjectSpecificId (
277
224
opts . project_id ,
278
225
) ;
279
226
push ( "project_specific_id" , project_specific_id ) ;
227
+ logger . debug ( "createCloudFilesystem" , { cloudFilesystem } ) ;
280
228
281
229
const query = `INSERT INTO cloud_filesystems(${ fields . join (
282
230
"," ,
283
231
) } ) VALUES(${ dollars . join ( "," ) } ) RETURNING id`;
284
232
const pool = getPool ( ) ;
285
233
const { rows } = await pool . query ( query , params ) ;
286
234
const { id } = rows [ 0 ] ;
287
-
288
- cloudFilesystem . id = id ;
289
- if ( cloudFilesystem . id == null ) {
235
+ if ( id == null ) {
290
236
throw Error ( "bug" ) ;
291
237
}
238
+ cloudFilesystem . id = id ;
292
239
293
- logger . debug ( "createCloudFilesystem: start the purchase" ) ;
294
240
try {
241
+ const bucket = await createRandomBucketName ( id ) ;
242
+ await pool . query ( "UPDATE cloud_filesystems SET bucket=$1 WHERE id=$2" , [
243
+ bucket ,
244
+ id ,
245
+ ] ) ;
246
+
247
+ logger . debug ( "createCloudFilesystem: start the purchase" ) ;
295
248
await createCloudStoragePurchase ( {
296
- cloud_filesystem_id : cloudFilesystem . id ,
249
+ cloud_filesystem_id : id ,
297
250
account_id : opts . account_id ,
298
- project_id : cloudFilesystem . project_id ,
299
- bucket : cloudFilesystem . bucket ,
251
+ project_id : opts . project_id ,
252
+ bucket,
300
253
} ) ;
301
- } catch ( err ) {
302
- logger . debug (
303
- "createCloudFilesystem: ERROR -- failed to create purchase" ,
304
- err ,
305
- ) ;
306
- await pool . query ( "DELETE FROM cloud_filesystems WHERE id=$1" , [ id ] ) ;
307
- throw err ;
308
- }
309
254
310
- // NOTE: no matter what, be sure to create the bucket but NOT the service account because
311
- // creating the bucket twice at once could lead to waste via
312
- // a race condition (e.g., multiple compute servers causing creating in different hubs),
313
- // with multiple bucket names and garbage. However, creating the service
314
- // account is canonical so no worries about the race condition.
315
- // Also, in general we will delete the service account completely when the
316
- // filesystem isn't active, to avoid having too many service accounts and
317
- // role bindings, but we obviously can't just delete the bucket when
318
- // it isn't active!
319
- try {
320
- const bucket = await ensureBucketExists ( cloudFilesystem ) ;
321
- if ( mount_orig ) {
322
- // only now that the bucket exists do we actually mount.
323
- await pool . query ( "UPDATE cloud_filesystems SET mount=TRUE WHERE id=$1" , [
324
- id ,
325
- ] ) ;
326
- }
327
- cloudFilesystem . bucket = bucket ;
255
+ // NOTE: no matter what, be sure to create the bucket but NOT the service account because
256
+ // creating the bucket twice at once could lead to waste via
257
+ // a race condition (e.g., multiple compute servers causing creating in different hubs),
258
+ // with multiple bucket names and garbage. However, creating the service
259
+ // account is canonical so no worries about the race condition.
260
+ // Also, in general we will delete the service account completely when the
261
+ // filesystem isn't active, to avoid having too many service accounts and
262
+ // role bindings, but we obviously can't just delete the bucket when
263
+ // it isn't active!
264
+ await createBucket ( bucket , bucketOptions ( cloudFilesystem ) ) ;
328
265
} catch ( err ) {
329
- await pool . query (
330
- "UPDATE cloud_filesystems SET error=$1,mount=FALSE WHERE id=$2" ,
331
- [ `${ err } ` , id ] ,
332
- ) ;
266
+ logger . debug ( "createCloudFilesystem: failed -- " , err ) ;
267
+ await pool . query ( "DELETE FROM cloud_filesystems WHERE id=$1" , [ id ] ) ;
333
268
throw err ;
334
269
}
335
270
336
271
return id ;
337
272
}
338
273
339
- async function createCloudStoragePurchase ( {
274
+ export async function createCloudStoragePurchase ( {
340
275
cloud_filesystem_id,
341
276
account_id,
342
277
project_id,
343
278
bucket,
279
+ period_start,
344
280
} : {
345
281
cloud_filesystem_id : number ;
346
- account_id ;
347
- project_id ;
348
- bucket ;
282
+ account_id : string ;
283
+ project_id : string ;
284
+ bucket : string ;
285
+ period_start ?: Date ;
349
286
} ) {
350
287
const purchase_id = await createPurchase ( {
351
288
client : null ,
352
289
account_id,
353
290
project_id,
354
291
service : "compute-server-storage" ,
355
- period_start : new Date ( ) ,
292
+ period_start : period_start ?? new Date ( ) ,
356
293
description : {
357
294
type : "compute-server-storage" ,
358
295
cloud : "google-cloud" ,
@@ -420,7 +357,7 @@ function bucketOptions({
420
357
} as CreateBucketRequest ;
421
358
}
422
359
423
- export async function getAvailabelProjectSpecificId ( project_id : string ) {
360
+ export async function getAvailableProjectSpecificId ( project_id : string ) {
424
361
const pool = getPool ( ) ;
425
362
const { rows } = await pool . query (
426
363
"SELECT project_specific_id FROM cloud_filesystems WHERE project_id=$1" ,
@@ -435,3 +372,19 @@ export async function getAvailabelProjectSpecificId(project_id: string) {
435
372
}
436
373
return id ;
437
374
}
375
+
376
+ async function createRandomBucketName ( id : number ) : Promise < string > {
377
+ // randomized bucket name -- all GCS buckets are in a single global
378
+ // namespace, but by using a uuid it's sufficiently unlikely that
379
+ // a bucket name would ever not be available; also nobody will
380
+ // ever guess a bucket name, which is an extra level of security.
381
+ // If there is a conflict, it would be an error and the user
382
+ // would just retry creating their bucket (it's much more likely
383
+ // to hit a random networking error).
384
+ const s = `-${ zeroPad ( id , 8 ) } -${ uuid ( ) } ` ;
385
+ const bucket = `${ ( await getGoogleCloudPrefix ( ) ) . slice (
386
+ 0 ,
387
+ 63 - s . length - 1 ,
388
+ ) } ${ s } `;
389
+ return bucket ;
390
+ }
0 commit comments