-
Notifications
You must be signed in to change notification settings - Fork 4.4k
feat(aws-ec2): signal, download and execute helpers for UserData #6029
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
767064e
d449165
694050d
d0e6595
7ec2893
0092a39
3882da7
71fbdbe
6bfbe1a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,5 @@ | ||
| import { IBucket } from "@aws-cdk/aws-s3"; | ||
| import { CfnElement, Resource, Stack } from "@aws-cdk/core"; | ||
| import { OperatingSystemType } from "./machine-image"; | ||
|
|
||
| /** | ||
|
|
@@ -12,6 +14,50 @@ export interface LinuxUserDataOptions { | |
| readonly shebang?: string; | ||
| } | ||
|
|
||
| /** | ||
| * Options when downloading files from S3 | ||
| */ | ||
| export interface S3DownloadOptions { | ||
|
|
||
| /** | ||
| * Name of the S3 bucket to download from | ||
| */ | ||
| readonly bucket: IBucket; | ||
|
|
||
| /** | ||
| * The key of the file to download | ||
| */ | ||
| readonly bucketKey: string; | ||
|
|
||
| /** | ||
| * The name of the local file. | ||
| * | ||
| * @default Linux - /tmp/bucketKey | ||
| * Windows - %TEMP%/bucketKey | ||
| */ | ||
| readonly localFile?: string; | ||
|
|
||
| } | ||
|
|
||
| /** | ||
| * Options when executing a file. | ||
| */ | ||
| export interface ExecuteFileOptions { | ||
|
|
||
| /** | ||
| * The path to the file. | ||
| */ | ||
| readonly filePath: string; | ||
|
|
||
| /** | ||
| * The arguments to be passed to the file. | ||
| * | ||
| * @default No arguments are passed to the file. | ||
| */ | ||
| readonly arguments?: string; | ||
|
|
||
| } | ||
|
|
||
| /** | ||
| * Instance User Data | ||
| */ | ||
|
|
@@ -51,14 +97,41 @@ export abstract class UserData { | |
| */ | ||
| public abstract addCommands(...commands: string[]): void; | ||
|
|
||
| /** | ||
| * Add one or more commands to the user data that will run when the script exits. | ||
| */ | ||
| public abstract addOnExitCommands(...commands: string[]): void; | ||
|
|
||
| /** | ||
| * Render the UserData for use in a construct | ||
| */ | ||
| public abstract render(): string; | ||
|
|
||
| /** | ||
| * Adds commands to download a file from S3 | ||
| * | ||
| * @returns: The local path that the file will be downloaded to | ||
| */ | ||
| public abstract addS3DownloadCommand(params: S3DownloadOptions): string; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's not completely obvious what this returns, so that might be worth a |
||
|
|
||
| /** | ||
| * Adds commands to execute a file | ||
| */ | ||
| public abstract addExecuteFileCommand( params: ExecuteFileOptions): void; | ||
|
|
||
| /** | ||
| * Adds a command which will send a cfn-signal when the user data script ends | ||
| */ | ||
| public abstract addSignalOnExitCommand( resource: Resource ): void; | ||
|
|
||
| } | ||
|
|
||
| /** | ||
| * Linux Instance User Data | ||
| */ | ||
| class LinuxUserData extends UserData { | ||
| private readonly lines: string[] = []; | ||
| private readonly onExitLines: string[] = []; | ||
|
|
||
| constructor(private readonly props: LinuxUserDataOptions = {}) { | ||
| super(); | ||
|
|
@@ -68,14 +141,54 @@ class LinuxUserData extends UserData { | |
| this.lines.push(...commands); | ||
| } | ||
|
|
||
| public addOnExitCommands(...commands: string[]) { | ||
| this.onExitLines.push(...commands); | ||
| } | ||
|
|
||
| public render(): string { | ||
| const shebang = this.props.shebang !== undefined ? this.props.shebang : '#!/bin/bash'; | ||
| return [shebang, ...this.lines].join('\n'); | ||
| return [shebang, ...(this.renderOnExitLines()), ...this.lines].join('\n'); | ||
| } | ||
|
|
||
| public addS3DownloadCommand(params: S3DownloadOptions): string { | ||
| const s3Path = `s3://${params.bucket.bucketName}/${params.bucketKey}`; | ||
| const localPath = ( params.localFile && params.localFile.length !== 0 ) ? params.localFile : `/tmp/${ params.bucketKey }`; | ||
| this.addCommands( | ||
| `mkdir -p $(dirname '${localPath}')`, | ||
| `aws s3 cp '${s3Path}' '${localPath}'` | ||
| ); | ||
|
|
||
| return localPath; | ||
| } | ||
|
|
||
| public addExecuteFileCommand( params: ExecuteFileOptions): void { | ||
| this.addCommands( | ||
| `set -e`, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The download also needs to execute under Can you change the default shebang to
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Potentially even
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am not sure if we want to actually to modify the shebang like this since I would consider the following:
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That is a fair concern, but by that logic we can never improve any situation. At least I will take this change myself since I also predict it will be a lot of integ test work. |
||
| `chmod +x '${params.filePath}'`, | ||
| `'${params.filePath}' ${params.arguments}` | ||
| ); | ||
| } | ||
|
|
||
| public addSignalOnExitCommand( resource: Resource ): void { | ||
| const stack = Stack.of(resource); | ||
| const resourceID = stack.getLogicalId(resource.node.defaultChild as CfnElement); | ||
| this.addOnExitCommands(`/opt/aws/bin/cfn-signal --stack ${stack.stackName} --resource ${resourceID} --region ${stack.region} -e $exitCode || echo 'Failed to send Cloudformation Signal'`); | ||
| } | ||
|
|
||
| private renderOnExitLines(): string[] { | ||
| if ( this.onExitLines.length > 0 ) { | ||
| return [ 'function exitTrap(){', 'exitCode=$?', ...this.onExitLines, '}', 'trap exitTrap EXIT' ]; | ||
| } | ||
| return []; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Windows Instance User Data | ||
| */ | ||
| class WindowsUserData extends UserData { | ||
| private readonly lines: string[] = []; | ||
| private readonly onExitLines: string[] = []; | ||
|
|
||
| constructor() { | ||
| super(); | ||
|
|
@@ -85,11 +198,53 @@ class WindowsUserData extends UserData { | |
| this.lines.push(...commands); | ||
| } | ||
|
|
||
| public addOnExitCommands(...commands: string[]) { | ||
| this.onExitLines.push(...commands); | ||
| } | ||
|
|
||
| public render(): string { | ||
| return `<powershell>${this.lines.join('\n')}</powershell>`; | ||
| return `<powershell>${ | ||
| [...(this.renderOnExitLines()), | ||
| ...this.lines, | ||
| ...( this.onExitLines.length > 0 ? ['throw "Success"'] : [] ) | ||
| ].join('\n') | ||
| }</powershell>`; | ||
| } | ||
|
|
||
| public addS3DownloadCommand(params: S3DownloadOptions): string { | ||
| const localPath = ( params.localFile && params.localFile.length !== 0 ) ? params.localFile : `C:/temp/${ params.bucketKey }`; | ||
| this.addCommands( | ||
| `mkdir (Split-Path -Path '${localPath}' ) -ea 0`, | ||
| `Read-S3Object -BucketName '${params.bucket.bucketName}' -key '${params.bucketKey}' -file '${localPath}' -ErrorAction Stop` | ||
| ); | ||
| return localPath; | ||
| } | ||
|
|
||
| public addExecuteFileCommand( params: ExecuteFileOptions): void { | ||
| this.addCommands( | ||
| `&'${params.filePath}' ${params.arguments}`, | ||
| `if (!$?) { Write-Error 'Failed to execute the file "${params.filePath}"' -ErrorAction Stop }` | ||
| ); | ||
| } | ||
|
|
||
| public addSignalOnExitCommand( resource: Resource ): void { | ||
| const stack = Stack.of(resource); | ||
| const resourceID = stack.getLogicalId(resource.node.defaultChild as CfnElement); | ||
|
|
||
| this.addOnExitCommands(`cfn-signal --stack ${stack.stackName} --resource ${resourceID} --region ${stack.region} --success ($success.ToString().ToLower())`); | ||
| } | ||
|
|
||
| private renderOnExitLines(): string[] { | ||
| if ( this.onExitLines.length > 0 ) { | ||
| return ['trap {', '$success=($PSItem.Exception.Message -eq "Success")', ...this.onExitLines, 'break', '}']; | ||
| } | ||
| return []; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Custom Instance User Data | ||
| */ | ||
| class CustomUserData extends UserData { | ||
| private readonly lines: string[] = []; | ||
|
|
||
|
|
@@ -101,7 +256,23 @@ class CustomUserData extends UserData { | |
| this.lines.push(...commands); | ||
| } | ||
|
|
||
| public addOnExitCommands(): void { | ||
| throw new Error("CustomUserData does not support addOnExitCommands, use UserData.forLinux() or UserData.forWindows() instead."); | ||
| } | ||
|
|
||
| public render(): string { | ||
| return this.lines.join('\n'); | ||
| } | ||
|
|
||
| public addS3DownloadCommand(): string { | ||
| throw new Error("CustomUserData does not support addS3DownloadCommand, use UserData.forLinux() or UserData.forWindows() instead."); | ||
| } | ||
|
|
||
| public addExecuteFileCommand(): void { | ||
| throw new Error("CustomUserData does not support addExecuteFileCommand, use UserData.forLinux() or UserData.forWindows() instead."); | ||
| } | ||
|
|
||
| public addSignalOnExitCommand(): void { | ||
| throw new Error("CustomUserData does not support addSignalOnExitCommand, use UserData.forLinux() or UserData.forWindows() instead."); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please fix the indentation and other whitespace here to at least be consistent.
Indentation of 2 spaces please, closing curly is not indented.