1
- import * as crypto from 'crypto' ;
2
-
3
1
import { IBucket } from '@aws-cdk/aws-s3' ;
4
2
import { CfnElement , Fn , Resource , Stack } from '@aws-cdk/core' ;
5
3
import { OperatingSystemType } from './machine-image' ;
@@ -63,7 +61,7 @@ export interface ExecuteFileOptions {
63
61
/**
64
62
* Instance User Data
65
63
*/
66
- export abstract class UserData implements IMultipartUserDataPartProducer {
64
+ export abstract class UserData {
67
65
/**
68
66
* Create a userdata object for Linux hosts
69
67
*/
@@ -110,13 +108,6 @@ export abstract class UserData implements IMultipartUserDataPartProducer {
110
108
*/
111
109
public abstract render ( ) : string ;
112
110
113
- /**
114
- * Render the user data as a part for `MultipartUserData`. Not all subclasses supports this.
115
- */
116
- public renderAsMimePart ( _renderOpts ?: MultipartRenderOptions ) : MutlipartUserDataPart {
117
- throw new Error ( 'This class does not support rendering as MIME part' ) ;
118
- }
119
-
120
111
/**
121
112
* Adds commands to download a file from S3
122
113
*
@@ -160,15 +151,6 @@ class LinuxUserData extends UserData {
160
151
return [ shebang , ...( this . renderOnExitLines ( ) ) , ...this . lines ] . join ( '\n' ) ;
161
152
}
162
153
163
- public renderAsMimePart ( renderOpts ?: MultipartRenderOptions ) : MutlipartUserDataPart {
164
- const contentType = renderOpts ?. contentType || 'text/x-shellscript' ;
165
- return new MutlipartUserDataPart ( {
166
- body : Fn . base64 ( this . render ( ) ) , // Wrap into base64, to support UTF-8 encoding (actually decoding)
167
- contentType : `${ contentType } ; charset="utf-8"` ,
168
- transferEncoding : 'base64' ,
169
- } ) ;
170
- }
171
-
172
154
public addS3DownloadCommand ( params : S3DownloadOptions ) : string {
173
155
const s3Path = `s3://${ params . bucket . bucketName } /${ params . bucketKey } ` ;
174
156
const localPath = ( params . localFile && params . localFile . length !== 0 ) ? params . localFile : `/tmp/${ params . bucketKey } ` ;
@@ -296,81 +278,169 @@ class CustomUserData extends UserData {
296
278
}
297
279
298
280
/**
299
- * Options when creating `MutlipartUserDataPart`.
281
+ * Suggested content types, however any value is allowed.
282
+ */
283
+ export type MultipartContentType = 'text/x-shellscript; charset="utf-8"' | 'text/cloud-boothook; charset="utf-8"' | string ;
284
+
285
+ /**
286
+ * Options when creating `MultipartUserDataPart`.
300
287
*/
301
- export interface MutlipartUserDataPartOptions {
288
+ export interface MultipartUserDataPartOptions {
289
+
290
+ /**
291
+ * `Content-Type` header of this part.
292
+ *
293
+ * For Linux shell scripts use `text/x-shellscript`
294
+ */
295
+ readonly contentType : MultipartContentType ;
296
+
297
+ /**
298
+ * `Content-Transfer-Encoding` header specifying part encoding.
299
+ *
300
+ * @default undefined - don't add this header
301
+ */
302
+ readonly transferEncoding ?: string ;
303
+ }
304
+
305
+ /**
306
+ * Options when creating `MultipartUserDataPart`.
307
+ */
308
+ export interface MultipartUserDataPartOptionsWithBody extends MultipartUserDataPartOptions {
302
309
/**
303
310
* The body of message.
304
311
*
305
312
* @default undefined - body will not be added to part
306
313
*/
307
314
readonly body ?: string ,
315
+ }
308
316
317
+ /**
318
+ * Options when creating `MultipartUserDataPartWrapper`.
319
+ */
320
+ export interface MultipartUserDataPartWrapperOptions {
309
321
/**
310
322
* `Content-Type` header of this part.
311
323
*
312
- * For Linux shell scripts use `text/x-shellscript`
324
+ * For Linux shell scripts typically it's `text/x-shellscript`.
325
+ *
326
+ * @default 'text/x-shellscript; charset="utf-8"'
327
+ */
328
+ readonly contentType ?: MultipartContentType ;
329
+ }
330
+
331
+ /**
332
+ * Interface representing part of `MultipartUserData` user data.
333
+ */
334
+ export interface IMultipart {
335
+ /**
336
+ * The body of this MIME part.
337
+ */
338
+ readonly body : string | undefined ;
339
+
340
+ /**
341
+ * `Content-Type` header of this part.
313
342
*/
314
343
readonly contentType : string ;
315
344
316
345
/**
317
- * `Content-Transfer-Encoding` header specifing part encoding.
346
+ * `Content-Transfer-Encoding` header specifying part encoding.
318
347
*
319
348
* @default undefined - don't add this header
320
349
*/
321
350
readonly transferEncoding ?: string ;
322
351
}
323
352
324
353
/**
325
- * The raw part of multip-part user data, which can be added to {@link MultipartUserData}.
354
+ * The base class for all classes which can be used as {@link MultipartUserData}.
326
355
*/
327
- export class MutlipartUserDataPart implements IMultipartUserDataPartProducer {
356
+ export abstract class MultipartUserDataPart implements IMultipart {
357
+
358
+ /**
359
+ * Constructs the new `MultipartUserDataPart` wrapping existing `UserData`. Modification to `UserData` are reflected
360
+ * in subsequent renders of the part.
361
+ *
362
+ * For more information about content types see `MultipartUserDataPartOptionsWithBody`
363
+ */
364
+ public static fromUserData ( userData : UserData , opts ?: MultipartUserDataPartWrapperOptions ) : MultipartUserDataPart {
365
+ opts = opts || { } ;
366
+ return new MultipartUserDataPartWrapper ( userData , opts ) ;
367
+ }
368
+
369
+ /**
370
+ * Constructs the raw `MultipartUserDataPart` using specified body, content type and transfer encoding.
371
+ *
372
+ * When transfer encoding is specified (typically as Base64), it's caller responsibility to convert body to
373
+ * Base64 either by wrapping with `Fn.base64` or by converting it by other converters.
374
+ */
375
+ public static fromRawBody ( opts : MultipartUserDataPartOptionsWithBody ) : MultipartUserDataPart {
376
+ return new MultipartUserDataPartRaw ( opts ) ;
377
+ }
378
+
379
+ protected static readonly DEFAULT_CONTENT_TYPE = 'text/x-shellscript; charset="utf-8"' ;
380
+
328
381
/** The body of this MIME part. */
329
- public readonly body ? : string ;
382
+ public abstract get body ( ) : string | undefined ;
330
383
331
384
/** `Content-Type` header of this part */
332
385
public readonly contentType : string ;
333
386
334
387
/**
335
- * `Content-Transfer-Encoding` header specifing part encoding.
388
+ * `Content-Transfer-Encoding` header specifying part encoding.
336
389
*
337
390
* @default undefined - don't add this header
338
391
*/
339
- readonly transferEncoding ?: string ;
392
+ public readonly transferEncoding ?: string ;
340
393
341
- public constructor ( props : MutlipartUserDataPartOptions ) {
342
- this . body = props . body ;
394
+ public constructor ( props : MultipartUserDataPartOptions ) {
343
395
this . contentType = props . contentType ;
344
396
this . transferEncoding = props . transferEncoding ;
345
397
}
398
+ }
346
399
347
- renderAsMimePart ( _renderOpts ?: MultipartRenderOptions ) : MutlipartUserDataPart {
348
- return this ;
400
+ /**
401
+ * The raw part of multi-part user data, which can be added to {@link MultipartUserData}.
402
+ */
403
+ class MultipartUserDataPartRaw extends MultipartUserDataPart {
404
+ private _body : string | undefined ;
405
+
406
+ public constructor ( props : MultipartUserDataPartOptionsWithBody ) {
407
+ super ( props ) ;
408
+ this . _body = props . body ;
409
+ }
410
+
411
+ public get body ( ) : string | undefined {
412
+ return this . _body ;
349
413
}
350
414
}
351
415
352
416
/**
353
- * Render options for parts of multipart user data .
417
+ * Wrapper for `UserData` .
354
418
*/
355
- export interface MultipartRenderOptions {
356
- /**
357
- * Can be used to override default content type (without charset part) used when producing
358
- * part by `IMultipartUserDataPartProducer`.
359
- *
360
- * @default undefined - leave content type unchanged
361
- */
362
- readonly contentType ?: string ;
419
+ class MultipartUserDataPartWrapper extends MultipartUserDataPart {
420
+ public constructor ( public readonly userData : UserData , opts : MultipartUserDataPartWrapperOptions ) {
421
+ super ( {
422
+ contentType : opts . contentType || MultipartUserDataPart . DEFAULT_CONTENT_TYPE ,
423
+ // Force Base64 in case userData will contain UTF-8 characters
424
+ transferEncoding : 'base64' ,
425
+ } ) ;
426
+ }
427
+
428
+ public get body ( ) : string {
429
+ // Wrap rendered user data with Base64 function, in case data contains tokens
430
+ return Fn . base64 ( this . userData . render ( ) ) ;
431
+ }
363
432
}
364
433
365
434
/**
366
- * Class implementing this interface can produce `MutlipartUserDataPart` and can be added
367
- * to `MultipartUserData`.
435
+ * Options for creating {@link MultipartUserData}
368
436
*/
369
- export interface IMultipartUserDataPartProducer {
437
+ export interface MultipartUserDataOptions {
370
438
/**
371
- * Creats the `MutlipartUserDataPart.
439
+ * The string used to separate parts in multipart user data archive (it's like MIME boundary).
440
+ *
441
+ * This string should contain [a-zA-Z0-9] characters only, and should not be present in any part, or in text content of archive.
372
442
*/
373
- renderAsMimePart ( renderOpts ?: MultipartRenderOptions ) : MutlipartUserDataPart ;
443
+ readonly partsSeparator : string ;
374
444
}
375
445
376
446
/**
@@ -383,41 +453,47 @@ export interface IMultipartUserDataPartProducer {
383
453
export class MultipartUserData extends UserData {
384
454
private static readonly USE_PART_ERROR = 'MultipartUserData does not support this operation. Please add part using addPart.' ;
385
455
386
- private parts : IMultipartUserDataPartProducer [ ] = [ ] ;
456
+ private parts : IMultipart [ ] = [ ] ;
457
+
458
+ private opts : MultipartUserDataOptions ;
459
+
460
+ constructor ( opts : MultipartUserDataOptions ) {
461
+ super ( ) ;
387
462
463
+ this . opts = {
464
+ ...opts ,
465
+ } ;
466
+ }
388
467
/**
389
- * Adds a class which can producer `MutlipartUserDataPart`. I. e. `UserData.forLinux()`.
468
+ * Adds existing `UserData`. Modification to `UserData` are reflected in subsequent renders of the part.
469
+ *
470
+ * For more information about content types see `MultipartUserDataPartOptionsWithBody`
390
471
*/
391
- public addPart ( producer : IMultipartUserDataPartProducer ) : this {
392
- this . parts . push ( producer ) ;
472
+ public addUserDataPart ( userData : UserData , opts ?: MultipartUserDataPartWrapperOptions ) : this {
473
+ this . parts . push ( MultipartUserDataPart . fromUserData ( userData , opts ) ) ;
393
474
394
475
return this ;
395
476
}
396
477
397
- public render ( ) : string {
398
- const renderedParts : MutlipartUserDataPart [ ] = this . parts . map ( producer => producer . renderAsMimePart ( ) ) ;
399
-
400
- // Hash the message content, it will be used as boundry. The boundry should be
401
- // so much unique not to be in message text, and stable so the text of archive will
402
- // not be changed only due to change of boundry (may cause redeploys of resources)
403
- const hash = crypto . createHash ( 'sha256' ) ;
404
- renderedParts . forEach ( part => {
405
- hash
406
- . update ( part . contentType )
407
- . update ( part . body || 'empty-body' )
408
- . update ( part . transferEncoding || '' ) ;
409
- } ) ;
410
- hash . update ( 'salt-boundary-rado' ) ;
478
+ /**
479
+ * Adds the 'raw' part using provided options.
480
+ */
481
+ public addPart ( opts : MultipartUserDataPartOptionsWithBody ) : this {
482
+ this . parts . push ( MultipartUserDataPart . fromRawBody ( opts ) ) ;
411
483
412
- const boundary = '-' + hash . digest ( 'base64' ) + '-' ;
484
+ return this ;
485
+ }
486
+
487
+ public render ( ) : string {
488
+ const boundary = this . opts . partsSeparator ;
413
489
414
490
// Now build final MIME archive - there are few changes from MIME message which are accepted by cloud-init:
415
- // - MIME RFC uses CRLF to separarte lines - cloud-init is fine with LF \n only
491
+ // - MIME RFC uses CRLF to separate lines - cloud-init is fine with LF \n only
416
492
var resultArchive = `Content-Type: multipart/mixed; boundary="${ boundary } "\n` ;
417
493
resultArchive = resultArchive + 'MIME-Version: 1.0\n' ;
418
494
419
- // Add parts - each part starts with boundry
420
- renderedParts . forEach ( part => {
495
+ // Add parts - each part starts with boundary
496
+ this . parts . forEach ( part => {
421
497
resultArchive = resultArchive + '\n--' + boundary + '\n' + 'Content-Type: ' + part . contentType + '\n' ;
422
498
423
499
if ( part . transferEncoding != null ) {
@@ -429,7 +505,7 @@ export class MultipartUserData extends UserData {
429
505
}
430
506
} ) ;
431
507
432
- // Add closing boundry
508
+ // Add closing boundary
433
509
resultArchive = resultArchive + `\n--${ boundary } --\n` ;
434
510
435
511
return resultArchive ;
0 commit comments