Skip to content

Commit 2a9be38

Browse files
author
Radek Smogura
committed
Replace interface approacg with adaptor
* Remove `IMultipartUserDataPartProducer` * Add `MultipartUserDataPart` & `IMultipart` * Concrete types to represent raw part and UserData wrapper can be created with `MultipartUserDataPart.fromUserData` & `MultipartUserDataPart.fromRawBody` * Removed auto-generation of separator (as with tokens hash codes can differ when tokens are not resolved)
1 parent f37bd20 commit 2a9be38

File tree

4 files changed

+165
-88
lines changed

4 files changed

+165
-88
lines changed

packages/@aws-cdk/aws-ec2/lib/user-data.ts

+145-69
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import * as crypto from 'crypto';
2-
31
import { IBucket } from '@aws-cdk/aws-s3';
42
import { CfnElement, Fn, Resource, Stack } from '@aws-cdk/core';
53
import { OperatingSystemType } from './machine-image';
@@ -63,7 +61,7 @@ export interface ExecuteFileOptions {
6361
/**
6462
* Instance User Data
6563
*/
66-
export abstract class UserData implements IMultipartUserDataPartProducer {
64+
export abstract class UserData {
6765
/**
6866
* Create a userdata object for Linux hosts
6967
*/
@@ -110,13 +108,6 @@ export abstract class UserData implements IMultipartUserDataPartProducer {
110108
*/
111109
public abstract render(): string;
112110

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-
120111
/**
121112
* Adds commands to download a file from S3
122113
*
@@ -160,15 +151,6 @@ class LinuxUserData extends UserData {
160151
return [shebang, ...(this.renderOnExitLines()), ...this.lines].join('\n');
161152
}
162153

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-
172154
public addS3DownloadCommand(params: S3DownloadOptions): string {
173155
const s3Path = `s3://${params.bucket.bucketName}/${params.bucketKey}`;
174156
const localPath = ( params.localFile && params.localFile.length !== 0 ) ? params.localFile : `/tmp/${ params.bucketKey }`;
@@ -296,81 +278,169 @@ class CustomUserData extends UserData {
296278
}
297279

298280
/**
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`.
300287
*/
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 {
302309
/**
303310
* The body of message.
304311
*
305312
* @default undefined - body will not be added to part
306313
*/
307314
readonly body?: string,
315+
}
308316

317+
/**
318+
* Options when creating `MultipartUserDataPartWrapper`.
319+
*/
320+
export interface MultipartUserDataPartWrapperOptions {
309321
/**
310322
* `Content-Type` header of this part.
311323
*
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.
313342
*/
314343
readonly contentType: string;
315344

316345
/**
317-
* `Content-Transfer-Encoding` header specifing part encoding.
346+
* `Content-Transfer-Encoding` header specifying part encoding.
318347
*
319348
* @default undefined - don't add this header
320349
*/
321350
readonly transferEncoding?: string;
322351
}
323352

324353
/**
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}.
326355
*/
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+
328381
/** The body of this MIME part. */
329-
public readonly body?: string;
382+
public abstract get body(): string | undefined;
330383

331384
/** `Content-Type` header of this part */
332385
public readonly contentType: string;
333386

334387
/**
335-
* `Content-Transfer-Encoding` header specifing part encoding.
388+
* `Content-Transfer-Encoding` header specifying part encoding.
336389
*
337390
* @default undefined - don't add this header
338391
*/
339-
readonly transferEncoding?: string;
392+
public readonly transferEncoding?: string;
340393

341-
public constructor(props: MutlipartUserDataPartOptions) {
342-
this.body = props.body;
394+
public constructor(props: MultipartUserDataPartOptions) {
343395
this.contentType = props.contentType;
344396
this.transferEncoding = props.transferEncoding;
345397
}
398+
}
346399

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;
349413
}
350414
}
351415

352416
/**
353-
* Render options for parts of multipart user data.
417+
* Wrapper for `UserData`.
354418
*/
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+
}
363432
}
364433

365434
/**
366-
* Class implementing this interface can produce `MutlipartUserDataPart` and can be added
367-
* to `MultipartUserData`.
435+
* Options for creating {@link MultipartUserData}
368436
*/
369-
export interface IMultipartUserDataPartProducer {
437+
export interface MultipartUserDataOptions {
370438
/**
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.
372442
*/
373-
renderAsMimePart(renderOpts?: MultipartRenderOptions): MutlipartUserDataPart;
443+
readonly partsSeparator: string;
374444
}
375445

376446
/**
@@ -383,41 +453,47 @@ export interface IMultipartUserDataPartProducer {
383453
export class MultipartUserData extends UserData {
384454
private static readonly USE_PART_ERROR = 'MultipartUserData does not support this operation. Please add part using addPart.';
385455

386-
private parts: IMultipartUserDataPartProducer[] = [];
456+
private parts: IMultipart[] = [];
457+
458+
private opts: MultipartUserDataOptions;
459+
460+
constructor(opts: MultipartUserDataOptions) {
461+
super();
387462

463+
this.opts = {
464+
...opts,
465+
};
466+
}
388467
/**
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`
390471
*/
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));
393474

394475
return this;
395476
}
396477

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));
411483

412-
const boundary = '-' + hash.digest('base64') + '-';
484+
return this;
485+
}
486+
487+
public render(): string {
488+
const boundary = this.opts.partsSeparator;
413489

414490
// 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
416492
var resultArchive = `Content-Type: multipart/mixed; boundary="${boundary}"\n`;
417493
resultArchive = resultArchive + 'MIME-Version: 1.0\n';
418494

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 => {
421497
resultArchive = resultArchive + '\n--' + boundary + '\n' + 'Content-Type: ' + part.contentType + '\n';
422498

423499
if (part.transferEncoding != null) {
@@ -429,7 +505,7 @@ export class MultipartUserData extends UserData {
429505
}
430506
});
431507

432-
// Add closing boundry
508+
// Add closing boundary
433509
resultArchive = resultArchive + `\n--${boundary}--\n`;
434510

435511
return resultArchive;

packages/@aws-cdk/aws-ec2/test/integ.instance-multipart-userdata.expected.json

+5-5
Original file line numberDiff line numberDiff line change
@@ -646,11 +646,11 @@
646646
"Fn::Join": [
647647
"",
648648
[
649-
"Content-Type: multipart/mixed; boundary=\"-uU56KH4CuLBnf9GVhBq+SFx/hFRXS5S9umauYjYeBxQ=-\"\nMIME-Version: 1.0\n\n---uU56KH4CuLBnf9GVhBq+SFx/hFRXS5S9umauYjYeBxQ=-\nContent-Type: text/x-shellscript; charset=\"utf-8\"\nContent-Transfer-Encoding: base64\n\n",
649+
"Content-Type: multipart/mixed; boundary=\"---separator---\"\nMIME-Version: 1.0\n\n-----separator---\nContent-Type: text/x-shellscript; charset=\"utf-8\"\nContent-Transfer-Encoding: base64\n\n",
650650
{
651651
"Fn::Base64": "#!/bin/bash\necho 大らと > /var/tmp/echo1\ncp /var/tmp/echo1 /var/tmp/echo1-copy"
652652
},
653-
"\n---uU56KH4CuLBnf9GVhBq+SFx/hFRXS5S9umauYjYeBxQ=-\nContent-Type: text/x-shellscript; charset=\"utf-8\"\nContent-Transfer-Encoding: base64\n\n",
653+
"\n-----separator---\nContent-Type: text/x-shellscript; charset=\"utf-8\"\nContent-Transfer-Encoding: base64\n\n",
654654
{
655655
"Fn::Base64": {
656656
"Fn::Join": [
@@ -665,15 +665,15 @@
665665
]
666666
}
667667
},
668-
"\n---uU56KH4CuLBnf9GVhBq+SFx/hFRXS5S9umauYjYeBxQ=-\nContent-Type: text/cloud-boothook; charset=\"utf-8\"\nContent-Transfer-Encoding: base64\n\n",
668+
"\n-----separator---\nContent-Type: text/cloud-boothook\nContent-Transfer-Encoding: base64\n\n",
669669
{
670670
"Fn::Base64": "#!/bin/bash\necho \"Boothook2\" > /var/tmp/boothook\ncloud-init-per once docker_options echo 'OPTIONS=\"${OPTIONS} --storage-opt dm.basesize=20G\"' >> /etc/sysconfig/docker"
671671
},
672-
"\n---uU56KH4CuLBnf9GVhBq+SFx/hFRXS5S9umauYjYeBxQ=-\nContent-Type: text/x-shellscript\n\necho \"RawPart\" > /var/tmp/rawPart1\n---uU56KH4CuLBnf9GVhBq+SFx/hFRXS5S9umauYjYeBxQ=-\nContent-Type: text/x-shellscript\n\necho \"RawPart ",
672+
"\n-----separator---\nContent-Type: text/x-shellscript\n\necho \"RawPart\" > /var/tmp/rawPart1\n-----separator---\nContent-Type: text/x-shellscript\n\necho \"RawPart ",
673673
{
674674
"Ref": "VPCB9E5F0B4"
675675
},
676-
"\" > /var/tmp/rawPart2\n---uU56KH4CuLBnf9GVhBq+SFx/hFRXS5S9umauYjYeBxQ=---\n"
676+
"\" > /var/tmp/rawPart2\n-----separator-----\n"
677677
]
678678
]
679679
}

packages/@aws-cdk/aws-ec2/test/integ.instance-multipart-userdata.ts

+9-7
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ class TestStack extends cdk.Stack {
1111

1212
const vpc = new ec2.Vpc(this, 'VPC');
1313

14-
const multipartUserData = new ec2.MultipartUserData();
14+
const multipartUserData = new ec2.MultipartUserData({
15+
partsSeparator: '---separator---',
16+
});
1517

1618
const userData1 = ec2.UserData.forLinux();
1719
userData1.addCommands('echo 大らと > /var/tmp/echo1');
@@ -20,12 +22,12 @@ class TestStack extends cdk.Stack {
2022
const userData2 = ec2.UserData.forLinux();
2123
userData2.addCommands(`echo 大らと ${vpc.vpcId} > /var/tmp/echo2`);
2224

23-
const rawPart1 = new ec2.MutlipartUserDataPart({
25+
const rawPart1 = ec2.MultipartUserDataPart.fromRawBody({
2426
contentType: 'text/x-shellscript',
2527
body: 'echo "RawPart" > /var/tmp/rawPart1',
2628
});
2729

28-
const rawPart2 = new ec2.MutlipartUserDataPart({
30+
const rawPart2 = ec2.MultipartUserDataPart.fromRawBody({
2931
contentType: 'text/x-shellscript',
3032
body: `echo "RawPart ${vpc.vpcId}" > /var/tmp/rawPart2`,
3133
});
@@ -36,11 +38,11 @@ class TestStack extends cdk.Stack {
3638
'cloud-init-per once docker_options echo \'OPTIONS="${OPTIONS} --storage-opt dm.basesize=20G"\' >> /etc/sysconfig/docker',
3739
);
3840

39-
multipartUserData.addPart(userData1);
40-
multipartUserData.addPart(userData2);
41-
multipartUserData.addPart(bootHook.renderAsMimePart({
41+
multipartUserData.addUserDataPart(userData1);
42+
multipartUserData.addUserDataPart(userData2);
43+
multipartUserData.addUserDataPart(bootHook, {
4244
contentType: 'text/cloud-boothook',
43-
}));
45+
});
4446
multipartUserData.addPart(rawPart1);
4547
multipartUserData.addPart(rawPart2);
4648

0 commit comments

Comments
 (0)