diff --git a/packages/aws-cdk/lib/cdk-toolkit.ts b/packages/aws-cdk/lib/cdk-toolkit.ts index d85cad9a90214..5a76fafd3adac 100644 --- a/packages/aws-cdk/lib/cdk-toolkit.ts +++ b/packages/aws-cdk/lib/cdk-toolkit.ts @@ -15,6 +15,7 @@ import { HotswapMode } from './api/hotswap/common'; import { findCloudWatchLogGroups } from './api/logs/find-cloudwatch-logs'; import { CloudWatchLogEventMonitor } from './api/logs/logs-monitor'; import { StackActivityProgress } from './api/util/cloudformation/stack-activity-monitor'; +import { generateCdkApp, generateStack, readFromPath, readFromStack, setEnvironment, validateSourceOptions } from './commands/migrate'; import { printSecurityDiff, printStackDiff, RequireApproval } from './diff'; import { ResourceImporter } from './import'; import { data, debug, error, highlight, print, success, warning, withCorkedLogging } from './logging'; @@ -698,6 +699,28 @@ export class CdkToolkit { })); } + /** + * Migrates a CloudFormation stack/template to a CDK app + * @param options Options for CDK app creation + */ + public async migrate(options: MigrateOptions): Promise { + warning('This is an experimental feature. We make no guarantees about the outcome or stability of the functionality.'); + const language = options.language ?? 'typescript'; + + try { + validateSourceOptions(options.fromPath, options.fromStack); + const template = readFromPath(options.fromPath) || + await readFromStack(options.stackName, this.props.sdkProvider, setEnvironment(options.account, options.region)); + const stack = generateStack(template!, options.stackName, language); + success(' ⏳ Generating CDK app for %s...', chalk.blue(options.stackName)); + await generateCdkApp(options.stackName, stack!, language, options.outputPath); + } catch (e) { + error(' ❌ Migrate failed for `%s`: %s', chalk.blue(options.stackName), (e as Error).message); + throw e; + } + + } + private async selectStacksForList(patterns: string[]) { const assembly = await this.assembly(); const stacks = await assembly.selectStacks({ patterns }, { defaultBehavior: DefaultSelection.AllStacks }); @@ -1172,6 +1195,57 @@ export interface DestroyOptions { readonly ci?: boolean; } +export interface MigrateOptions { + /** + * The name assigned to the generated stack. This is also used to get + * the stack from the user's account if `--from-stack` is used. + */ + readonly stackName: string; + + /** + * The target language for the generated the CDK app. + * + * @default typescript + */ + readonly language?: string; + + /** + * The local path of the template used to generate the CDK app. + * + * @default - Local path is not used for the template source. + */ + readonly fromPath?: string; + + /** + * Whether to get the template from an existing CloudFormation stack. + * + * @default false + */ + readonly fromStack?: boolean; + + /** + * The output path at which to create the CDK app. + * + * @default - The current directory + */ + readonly outputPath?: string; + + /** + * The account from which to retrieve the template of the CloudFormation stack. + * + * @default - Uses the account for the credentials in use by the user. + */ + readonly account?: string; + + /** + * The region from which to retrieve the template of the CloudFormation stack. + * + * @default - Uses the default region for the credentials in use by the user. + */ + readonly region?: string; + +} + /** * @returns an array with the tags available in the stack metadata. */ diff --git a/packages/aws-cdk/lib/cli.ts b/packages/aws-cdk/lib/cli.ts index ab397d6b4ea7e..df074eb79894b 100644 --- a/packages/aws-cdk/lib/cli.ts +++ b/packages/aws-cdk/lib/cli.ts @@ -22,7 +22,7 @@ import { CdkToolkit, AssetBuildTime } from '../lib/cdk-toolkit'; import { realHandler as context } from '../lib/commands/context'; import { realHandler as docs } from '../lib/commands/docs'; import { realHandler as doctor } from '../lib/commands/doctor'; -import { MIGRATE_SUPPORTED_LANGUAGES, cliMigrate } from '../lib/commands/migrate'; +import { MIGRATE_SUPPORTED_LANGUAGES } from '../lib/commands/migrate'; import { RequireApproval } from '../lib/diff'; import { availableInitLanguages, cliInit, printAvailableTemplates } from '../lib/init'; import { data, debug, error, print, setLogLevel, setCI } from '../lib/logging'; @@ -274,7 +274,10 @@ async function parseCommandLineArguments(args: string[]) { .command('migrate', false /* hidden from "cdk --help" */, (yargs: Argv) => yargs .option('stack-name', { type: 'string', alias: 'n', desc: 'The name assigned to the stack created in the new project. The name of the app will be based off this name as well.', requiresArg: true }) .option('language', { type: 'string', default: 'typescript', alias: 'l', desc: 'The language to be used for the new project', choices: MIGRATE_SUPPORTED_LANGUAGES }) + .option('account', { type: 'string', alias: 'a' }) + .option('region', { type: 'string' }) .option('from-path', { type: 'string', alias: 'p', desc: 'The path to the CloudFormation template to migrate. Use this for locally stored templates' }) + .option('from-stack', { type: 'boolean', alias: 's', desc: 'USe this flag to retrieve the template for an existing CloudFormation stack' }) .option('output-path', { type: 'string', alias: 'o', desc: 'The output path for the migrated cdk app' }), ) .command('context', 'Manage cached context values', (yargs: Argv) => yargs @@ -659,11 +662,14 @@ export async function exec(args: string[], synthesizer?: Synthesizer): Promise { + const cloudFormation = (await sdkProvider.forEnvironment(environment, Mode.ForReading)).sdk.cloudFormation(); + + return (await cloudFormation.getTemplate({ + StackName: stackName, + }).promise()).TemplateBody; +} + +/** + * Sets the account and region for making CloudFormation calls. + * @param account The account to use + * @param region The region to use + * @returns The environment object + */ +export function setEnvironment(account?: string, region?: string): Environment { + return { account: account ?? UNKNOWN_ACCOUNT, region: region ?? UNKNOWN_REGION, name: 'cdk-migrate-env' }; +} + +/** + * Validates that exactly one source option has been provided. + * @param fromPath The content of the flag `--from-path` + * @param fromStack the content of the flag `--from-stack` + */ +export function validateSourceOptions(fromPath?: string, fromStack?: boolean) { + if (fromPath && fromStack) { + throw new Error('Only one of `--from-path` or `--from-stack` may be provided.'); + } + + if (!fromPath && !fromStack) { + throw new Error('Either `--from-path` or `--from-stack` must be used to provide the source of the CloudFormation template.'); + } } diff --git a/packages/aws-cdk/test/cdk-toolkit.test.ts b/packages/aws-cdk/test/cdk-toolkit.test.ts index 518c1e926b8f7..bb84e0fd6b4a7 100644 --- a/packages/aws-cdk/test/cdk-toolkit.test.ts +++ b/packages/aws-cdk/test/cdk-toolkit.test.ts @@ -54,10 +54,12 @@ jest.mock('../lib/logging', () => ({ })); jest.setTimeout(30_000); +import * as os from 'os'; import * as path from 'path'; import * as cxschema from '@aws-cdk/cloud-assembly-schema'; import { Manifest } from '@aws-cdk/cloud-assembly-schema'; import * as cxapi from '@aws-cdk/cx-api'; +import * as fs from 'fs-extra'; import { instanceMockFrom, MockCloudExecutable, TestStackArtifact } from './util'; import { MockSdkProvider } from './util/mock-sdk'; import { Bootstrapper } from '../lib/api/bootstrap'; @@ -858,6 +860,132 @@ describe('synth', () => { process.env.STACKS_TO_VALIDATE = undefined; }); + describe('migrate', () => { + const testResourcePath = [__dirname, 'commands', 'test-resources']; + const templatePath = [...testResourcePath, 'templates']; + const sqsTemplatePath = path.join(...templatePath, 'sqs-template.json'); + const autoscalingTemplatePath = path.join(...templatePath, 'autoscaling-template.yml'); + const s3TemplatePath = path.join(...templatePath, 's3-template.json'); + + test('migrate fails when neither --from-path or --from-stack are provided', async () => { + const toolkit = defaultToolkitSetup(); + await expect(() => toolkit.migrate({ stackName: 'no-source' })).rejects.toThrowError('Either `--from-path` or `--from-stack` must be used to provide the source of the CloudFormation template.'); + expect(stderrMock.mock.calls[1][0]).toContain(' ❌ Migrate failed for `no-source`: Either `--from-path` or `--from-stack` must be used to provide the source of the CloudFormation template.'); + }); + + test('migrate fails when both --from-path and --from-stack are provided', async () => { + const toolkit = defaultToolkitSetup(); + await expect(() => toolkit.migrate({ + stackName: 'no-source', + fromPath: './here/template.yml', + fromStack: true, + })).rejects.toThrowError('Only one of `--from-path` or `--from-stack` may be provided.'); + expect(stderrMock.mock.calls[1][0]).toContain(' ❌ Migrate failed for `no-source`: Only one of `--from-path` or `--from-stack` may be provided.'); + }); + + test('migrate fails when --from-path is invalid', async () => { + const toolkit = defaultToolkitSetup(); + await expect(() => toolkit.migrate({ + stackName: 'bad-local-source', + fromPath: './here/template.yml', + })).rejects.toThrowError('\'./here/template.yml\' is not a valid path.'); + expect(stderrMock.mock.calls[1][0]).toContain(' ❌ Migrate failed for `bad-local-source`: \'./here/template.yml\' is not a valid path.'); + }); + + test('migrate fails when --from-stack is used and stack does not exist in account', async () => { + const mockSdkProvider = new MockSdkProvider(); + mockSdkProvider.stubCloudFormation({ + getTemplate(_request) { + throw new Error('Stack does not exist in this environment'); + }, + }); + + const mockCloudExecutable = new MockCloudExecutable({ + stacks: [], + }); + + const cdkToolkit = new CdkToolkit({ + cloudExecutable: mockCloudExecutable, + deployments: new Deployments({ sdkProvider: mockSdkProvider }), + sdkProvider: mockSdkProvider, + configuration: mockCloudExecutable.configuration, + }); + + await expect(() => cdkToolkit.migrate({ + stackName: 'bad-cloudformation-source', + fromStack: true, + })).rejects.toThrowError('Stack does not exist in this environment'); + expect(stderrMock.mock.calls[1][0]).toContain(' ❌ Migrate failed for `bad-cloudformation-source`: Stack does not exist in this environment'); + }); + + test('migrate fails when stack cannot be generated', async () => { + const toolkit = defaultToolkitSetup(); + await expect(() => toolkit.migrate({ + stackName: 'cannot-generate-template', + fromPath: path.join(__dirname, 'commands', 'test-resources', 'templates', 'sqs-template.json'), + language: 'rust', + })).rejects.toThrowError('stack generation failed due to error \'unreachable\''); + expect(stderrMock.mock.calls[1][0]).toContain(' ❌ Migrate failed for `cannot-generate-template`: stack generation failed due to error \'unreachable\''); + }); + + cliTest('migrate succeeds for valid template from local path when no lanugage is provided', async (workDir) => { + const toolkit = defaultToolkitSetup(); + await toolkit.migrate({ + stackName: 'SQSTypeScript', + fromPath: sqsTemplatePath, + outputPath: workDir, + }); + + // Packages created for typescript + expect(fs.pathExistsSync(path.join(workDir, 'SQSTypeScript', 'package.json'))).toBeTruthy(); + expect(fs.pathExistsSync(path.join(workDir, 'SQSTypeScript', 'bin', 'sqs_type_script.ts'))).toBeTruthy(); + expect(fs.pathExistsSync(path.join(workDir, 'SQSTypeScript', 'lib', 'sqs_type_script-stack.ts'))).toBeTruthy(); + }); + + cliTest('migrate succeeds for valid template from local path when lanugage is provided', async (workDir) => { + const toolkit = defaultToolkitSetup(); + await toolkit.migrate({ + stackName: 'S3Python', + fromPath: s3TemplatePath, + outputPath: workDir, + language: 'python', + }); + + // Packages created for typescript + expect(fs.pathExistsSync(path.join(workDir, 'S3Python', 'requirements.txt'))).toBeTruthy(); + expect(fs.pathExistsSync(path.join(workDir, 'S3Python', 'app.py'))).toBeTruthy(); + expect(fs.pathExistsSync(path.join(workDir, 'S3Python', 's3_python', 's3_python_stack.py'))).toBeTruthy(); + }); + + cliTest('migrate call is idempotent', async (workDir) => { + const toolkit = defaultToolkitSetup(); + await toolkit.migrate({ + stackName: 'AutoscalingCSharp', + fromPath: autoscalingTemplatePath, + outputPath: workDir, + language: 'csharp', + }); + + // Packages created for typescript + expect(fs.pathExistsSync(path.join(workDir, 'AutoscalingCSharp', 'src', 'AutoscalingCSharp.sln'))).toBeTruthy(); + expect(fs.pathExistsSync(path.join(workDir, 'AutoscalingCSharp', 'src', 'AutoscalingCSharp', 'Program.cs'))).toBeTruthy(); + expect(fs.pathExistsSync(path.join(workDir, 'AutoscalingCSharp', 'src', 'AutoscalingCSharp', 'AutoscalingCSharpStack.cs'))).toBeTruthy(); + + // One more time + await toolkit.migrate({ + stackName: 'AutoscalingCSharp', + fromPath: autoscalingTemplatePath, + outputPath: workDir, + language: 'csharp', + }); + + // Packages created for typescript + expect(fs.pathExistsSync(path.join(workDir, 'AutoscalingCSharp', 'src', 'AutoscalingCSharp.sln'))).toBeTruthy(); + expect(fs.pathExistsSync(path.join(workDir, 'AutoscalingCSharp', 'src', 'AutoscalingCSharp', 'Program.cs'))).toBeTruthy(); + expect(fs.pathExistsSync(path.join(workDir, 'AutoscalingCSharp', 'src', 'AutoscalingCSharp', 'AutoscalingCSharpStack.cs'))).toBeTruthy(); + }); + }); + describe('stack with error and flagged for validation', () => { beforeEach(() => { cloudExecutable = new MockCloudExecutable({ @@ -1098,3 +1226,16 @@ class FakeCloudFormation extends Deployments { } } } + +function cliTest(name: string, handler: (dir: string) => void | Promise): void { + test(name, () => withTempDir(handler)); +} + +async function withTempDir(cb: (dir: string) => void | Promise) { + const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'aws-cdk-test')); + try { + await cb(tmpDir); + } finally { + await fs.remove(tmpDir); + } +} diff --git a/packages/aws-cdk/test/commands/migrate.test.ts b/packages/aws-cdk/test/commands/migrate.test.ts new file mode 100644 index 0000000000000..71e938de5391f --- /dev/null +++ b/packages/aws-cdk/test/commands/migrate.test.ts @@ -0,0 +1,189 @@ +import * as os from 'os'; +import * as path from 'path'; +import * as fs from 'fs-extra'; +import { generateCdkApp, generateStack, readFromPath, readFromStack, setEnvironment, validateSourceOptions } from '../../lib/commands/migrate'; +import { MockSdkProvider, MockedObject, SyncHandlerSubsetOf } from '../util/mock-sdk'; + +describe('Migrate Function Tests', () => { + let sdkProvider: MockSdkProvider; + let getTemplateMock: jest.Mock; + let cfnMocks: MockedObject>; + + const testResourcePath = [__dirname, 'test-resources']; + const templatePath = [...testResourcePath, 'templates']; + const stackPath = [...testResourcePath, 'stacks']; + + const validTemplatePath = path.join(...templatePath, 's3-template.json'); + const validTemplate = readFromPath(validTemplatePath)!; + + beforeEach(() => { + sdkProvider = new MockSdkProvider(); + getTemplateMock = jest.fn(); + cfnMocks = { getTemplate: getTemplateMock }; + sdkProvider.stubCloudFormation(cfnMocks as any); + }); + + test('validateSourceOptions throws if both --from-path and --from-stack is provided', () => { + expect(() => validateSourceOptions('any-value', true)).toThrowError('Only one of `--from-path` or `--from-stack` may be provided.'); + }); + + test('validateSourceOptions throws if neither --from-path or --from-stack is provided', () => { + expect(() => validateSourceOptions(undefined, undefined)).toThrowError('Either `--from-path` or `--from-stack` must be used to provide the source of the CloudFormation template.'); + }); + + test('validateSourceOptions does not throw when only --from-path is supplied', () => { + expect(() => validateSourceOptions('any-value', false)).not.toThrow(); + }); + + test('validateSourceOptions does now throw when only --from-stack is provided', () => { + expect(() => validateSourceOptions(undefined, true)).not.toThrow(); + }); + + test('readFromPath produces a string representation of the template at a given path', () => { + expect(readFromPath(validTemplatePath)).toEqual(fs.readFileSync(validTemplatePath, 'utf8')); + }); + + test('readFromPath returns undefined when template file is not provided', () => { + expect(readFromPath()).toEqual(undefined); + }); + + test('readFromPath throws error when template file does not exist at a given path', () => { + const badTemplatePath = './not-here.json'; + expect(() => readFromPath(badTemplatePath)).toThrowError(`\'${badTemplatePath}\' is not a valid path.`); + }); + + test('readFromStack produces a string representation of the template retrieved from CloudFormation', async () => { + const template = fs.readFileSync(validTemplatePath); + getTemplateMock.mockImplementationOnce(() => ({ + TemplateBody: template, + })); + + expect(await readFromStack('this-one', sdkProvider, { account: 'num', region: 'here', name: 'hello-my-name-is-what...' })).toEqual(template); + }); + + test('readFromStack throws error when no stack exists with the stack name in the account and region', async () => { + getTemplateMock.mockImplementationOnce(() => { throw new Error('No stack. This did not go well.'); }); + await expect(() => readFromStack('that-one', sdkProvider, { account: 'num', region: 'here', name: 'hello-my-name-is-who...' })).rejects.toThrowError('No stack. This did not go well.'); + }); + + test('setEnvironment sets account and region when provided', () => { + expect(setEnvironment('my-account', 'somewhere')).toEqual({ account: 'my-account', region: 'somewhere', name: 'cdk-migrate-env' }); + }); + + test('serEnvironment uses default account and region when not provided', () => { + expect(setEnvironment()).toEqual({ account: 'unknown-account', region: 'unknown-region', name: 'cdk-migrate-env' }); + }); + + test('generateStack generates the expected stack string when called for typescript', () => { + const stack = generateStack(validTemplate, 'GoodTypeScript', 'typescript'); + expect(stack).toEqual(fs.readFileSync(path.join(...stackPath, 's3-stack.ts'), 'utf8')); + }); + + test('generateStack generates the expected stack string when called for python', () => { + const stack = generateStack(validTemplate, 'GoodPython', 'python'); + expect(stack).toEqual(fs.readFileSync(path.join(...stackPath, 's3_stack.py'), 'utf8')); + }); + + test('generateStack generates the expected stack string when called for java', () => { + const stack = generateStack(validTemplate, 'GoodJava', 'java'); + expect(stack).toEqual(fs.readFileSync(path.join(...stackPath, 'S3Stack.java'), 'utf8')); + }); + + test('generateStack generates the expected stack string when called for csharp', () => { + const stack = generateStack(validTemplate, 'GoodCSharp', 'csharp'); + expect(stack).toEqual(fs.readFileSync(path.join(...stackPath, 'S3Stack.cs'), 'utf8')); + }); + + test('generateStack throws error when called for other language', () => { + expect(() => generateStack(validTemplate, 'BadBadBad', 'php')).toThrowError('stack generation failed due to error \'unreachable\''); + }); + + cliTest('generateCdkApp generates the expected cdk app when called for typescript', async (workDir) => { + const stack = generateStack(validTemplate, 'GoodTypeScript', 'typescript'); + await generateCdkApp('GoodTypeScript', stack, 'typescript', workDir); + + // Packages exist in the correct spot + expect(fs.pathExistsSync(path.join(workDir, 'GoodTypeScript', 'package.json'))).toBeTruthy(); + expect(fs.pathExistsSync(path.join(workDir, 'GoodTypeScript', 'bin', 'good_type_script.ts'))).toBeTruthy(); + expect(fs.pathExistsSync(path.join(workDir, 'GoodTypeScript', 'lib', 'good_type_script-stack.ts'))).toBeTruthy(); + + // Replaced stack file is referenced correctly in app file + const app = fs.readFileSync(path.join(workDir, 'GoodTypeScript', 'bin', 'good_type_script.ts'), 'utf8').split('\n'); + expect(app.map(line => line.match('import { GoodTypeScriptStack } from \'../lib/good_type_script-stack\';')).filter(line => line).length).toEqual(1); + expect(app.map(line => line.match(/new GoodTypeScriptStack\(app, \'GoodTypeScript\', \{/)).filter(line => line).length).toEqual(1); + + // Replaced stack file is correctly generated + const replacedStack = fs.readFileSync(path.join(workDir, 'GoodTypeScript', 'lib', 'good_type_script-stack.ts')); + expect(replacedStack).toEqual(fs.readFileSync(path.join(...stackPath, 's3-stack.ts'))); + }); + + cliTest('generateCdkApp generates the expected cdk app when called for python', async (workDir) => { + const stack = generateStack(validTemplate, 'GoodPython', 'python'); + await generateCdkApp('GoodPython', stack, 'python', workDir); + + // Packages exist in the correct spot + expect(fs.pathExistsSync(path.join(workDir, 'GoodPython', 'requirements.txt'))).toBeTruthy(); + expect(fs.pathExistsSync(path.join(workDir, 'GoodPython', 'app.py'))).toBeTruthy(); + expect(fs.pathExistsSync(path.join(workDir, 'GoodPython', 'good_python', 'good_python_stack.py'))).toBeTruthy(); + + // Replaced stack file is referenced correctly in app file + const app = fs.readFileSync(path.join(workDir, 'GoodPython', 'app.py'), 'utf8').split('\n'); + expect(app.map(line => line.match('from good_python.good_python_stack import GoodPythonStack')).filter(line => line).length).toEqual(1); + expect(app.map(line => line.match(/GoodPythonStack\(app, "GoodPython",/)).filter(line => line).length).toEqual(1); + + // Replaced stack file is correctly generated + const replacedStack = fs.readFileSync(path.join(workDir, 'GoodPython', 'good_python', 'good_python_stack.py')); + expect(replacedStack).toEqual(fs.readFileSync(path.join(...stackPath, 's3_stack.py'))); + }); + + cliTest('generateCdkApp generates the expected cdk app when called for java', async (workDir) => { + const stack = generateStack(validTemplate, 'GoodJava', 'java'); + await generateCdkApp('GoodJava', stack, 'java', workDir); + + // Packages exist in the correct spot + expect(fs.pathExistsSync(path.join(workDir, 'GoodJava', 'pom.xml'))).toBeTruthy(); + expect(fs.pathExistsSync(path.join(workDir, 'GoodJava', 'src', 'main', 'java', 'com', 'myorg', 'GoodJavaApp.java'))).toBeTruthy(); + expect(fs.pathExistsSync(path.join(workDir, 'GoodJava', 'src', 'main', 'java', 'com', 'myorg', 'GoodJavaStack.java'))).toBeTruthy(); + + // Replaced stack file is referenced correctly in app file + const app = fs.readFileSync(path.join(workDir, 'GoodJava', 'src', 'main', 'java', 'com', 'myorg', 'GoodJavaApp.java'), 'utf8').split('\n'); + expect(app.map(line => line.match('public class GoodJavaApp \{')).filter(line => line).length).toEqual(1); + expect(app.map(line => line.match(/ new GoodJavaStack\(app, "GoodJava", StackProps.builder()/)).filter(line => line).length).toEqual(1); + + // Replaced stack file is correctly generated + const replacedStack = fs.readFileSync(path.join(workDir, 'GoodJava', 'src', 'main', 'java', 'com', 'myorg', 'GoodJavaStack.java')); + expect(replacedStack).toEqual(fs.readFileSync(path.join(...stackPath, 'S3Stack.java'))); + }); + + cliTest('generateCdkApp generates the expected cdk app when called for csharp', async (workDir) => { + const stack = generateStack(validTemplate, 'GoodCSharp', 'csharp'); + await generateCdkApp('GoodCSharp', stack, 'csharp', workDir); + + // Packages exist in the correct spot + expect(fs.pathExistsSync(path.join(workDir, 'GoodCSharp', 'src', 'GoodCSharp.sln'))).toBeTruthy(); + expect(fs.pathExistsSync(path.join(workDir, 'GoodCSharp', 'src', 'GoodCSharp', 'Program.cs'))).toBeTruthy(); + expect(fs.pathExistsSync(path.join(workDir, 'GoodCSharp', 'src', 'GoodCSharp', 'GoodCSharpStack.cs'))).toBeTruthy(); + + // Replaced stack file is referenced correctly in app file + const app = fs.readFileSync(path.join(workDir, 'GoodCSharp', 'src', 'GoodCSharp', 'Program.cs'), 'utf8').split('\n'); + expect(app.map(line => line.match('namespace GoodCSharp')).filter(line => line).length).toEqual(1); + expect(app.map(line => line.match(/ new GoodCSharpStack\(app, "GoodCSharp", new StackProps/)).filter(line => line).length).toEqual(1); + + // Replaced stack file is correctly generated + const replacedStack = fs.readFileSync(path.join(workDir, 'GoodCSharp', 'src', 'GoodCSharp', 'GoodCSharpStack.cs')); + expect(replacedStack).toEqual(fs.readFileSync(path.join(...stackPath, 'S3Stack.cs'))); + }); +}); + +function cliTest(name: string, handler: (dir: string) => void | Promise): void { + test(name, () => withTempDir(handler)); +} + +async function withTempDir(cb: (dir: string) => void | Promise) { + const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'aws-cdk-test')); + try { + await cb(tmpDir); + } finally { + await fs.remove(tmpDir); + } +} \ No newline at end of file diff --git a/packages/aws-cdk/test/commands/test-resources/stacks/S3Stack.cs b/packages/aws-cdk/test/commands/test-resources/stacks/S3Stack.cs new file mode 100644 index 0000000000000..1b1a8a9b9dcc4 --- /dev/null +++ b/packages/aws-cdk/test/commands/test-resources/stacks/S3Stack.cs @@ -0,0 +1,46 @@ +using Amazon.CDK; +using Amazon.CDK.AWS.S3; +using Constructs; +using System.Collections.Generic; + +namespace Com.Acme.Test.Simple +{ + /// + /// AWS CloudFormation Sample Template S3_Website_Bucket_With_Retain_On_Delete: Sample template showing how to create a publicly accessible S3 bucket configured for website access with a deletion policy of retain on delete. + /// + public class GoodCSharpStack : Stack + { + /// + /// URL for website hosted on S3 + /// + public object WebsiteURL { get; } + + /// + /// Name of S3 bucket to hold website content + /// + public object S3BucketSecureURL { get; } + + public GoodCSharpStack(Construct scope, string id, GoodCSharpStackProps props = null) : base(scope, id, props) + { + + // Resources + var s3Bucket = new CfnBucket(this, "S3Bucket", new CfnBucketProps + { + AccessControl = "PublicRead", + WebsiteConfiguration = new CfnBucket.WebsiteConfigurationProperty + { + IndexDocument = "index.html", + ErrorDocument = "error.html", + }, + }); + + // Outputs + WebsiteURL = s3Bucket.AttrWebsiteURL; + S3BucketSecureURL = string.Join("", new [] + { + "https://", + s3Bucket.AttrDomainName, + }); + } + } +} diff --git a/packages/aws-cdk/test/commands/test-resources/stacks/S3Stack.java b/packages/aws-cdk/test/commands/test-resources/stacks/S3Stack.java new file mode 100644 index 0000000000000..caf47dfcf3dbe --- /dev/null +++ b/packages/aws-cdk/test/commands/test-resources/stacks/S3Stack.java @@ -0,0 +1,59 @@ +package com.acme.test.simple; + +import software.constructs.Construct; + +import java.util.*; +import software.amazon.awscdk.*; +import software.amazon.awscdk.CfnMapping; +import software.amazon.awscdk.CfnTag; +import software.amazon.awscdk.Stack; +import software.amazon.awscdk.StackProps; + +import software.amazon.awscdk.services.s3.*; + +class GoodJavaStack extends Stack { + private Object websiteUrl; + + private Object s3BucketSecureUrl; + + public Object getWebsiteUrl() { + return this.websiteUrl; + } + + public Object getS3BucketSecureUrl() { + return this.s3BucketSecureUrl; + } + + public GoodJavaStack(final Construct scope, final String id) { + super(scope, id, null); + } + + public GoodJavaStack(final Construct scope, final String id, final StackProps props) { + super(scope, id, props); + + CfnBucket s3Bucket = CfnBucket.Builder.create(this, "S3Bucket") + .accessControl("PublicRead") + .websiteConfiguration(CfnBucket.WebsiteConfigurationProperty.builder() + .indexDocument("index.html") + .errorDocument("error.html") + .build()) + .build(); + + s3Bucket.applyRemovalPolicy(RemovalPolicy.RETAIN); + + this.websiteUrl = s3Bucket.getAttrWebsiteUrl(); + CfnOutput.Builder.create(this, "WebsiteURL") + .value(this.websiteUrl.toString()) + .description("URL for website hosted on S3") + .build(); + + this.s3BucketSecureUrl = String.join("", + "https://", + s3Bucket.getAttrDomainName()); + CfnOutput.Builder.create(this, "S3BucketSecureURL") + .value(this.s3BucketSecureUrl.toString()) + .description("Name of S3 bucket to hold website content") + .build(); + + } +} diff --git a/packages/aws-cdk/test/commands/test-resources/stacks/s3-stack.ts b/packages/aws-cdk/test/commands/test-resources/stacks/s3-stack.ts new file mode 100644 index 0000000000000..8172e16a6e0b4 --- /dev/null +++ b/packages/aws-cdk/test/commands/test-resources/stacks/s3-stack.ts @@ -0,0 +1,40 @@ +import * as cdk from 'aws-cdk-lib'; +import * as s3 from 'aws-cdk-lib/aws-s3'; + +export interface GoodTypeScriptStackProps extends cdk.StackProps { +} + +/** + * AWS CloudFormation Sample Template S3_Website_Bucket_With_Retain_On_Delete: Sample template showing how to create a publicly accessible S3 bucket configured for website access with a deletion policy of retain on delete. + */ +export class GoodTypeScriptStack extends cdk.Stack { + /** + * URL for website hosted on S3 + */ + public readonly websiteUrl; + /** + * Name of S3 bucket to hold website content + */ + public readonly s3BucketSecureUrl; + + public constructor(scope: cdk.App, id: string, props: GoodTypeScriptStackProps = {}) { + super(scope, id, props); + + // Resources + const s3Bucket = new s3.CfnBucket(this, 'S3Bucket', { + accessControl: 'PublicRead', + websiteConfiguration: { + indexDocument: 'index.html', + errorDocument: 'error.html', + }, + }); + s3Bucket.cfnOptions.deletionPolicy = cdk.CfnDeletionPolicy.RETAIN; + + // Outputs + this.websiteUrl = s3Bucket.attrWebsiteUrl; + this.s3BucketSecureUrl = [ + 'https://', + s3Bucket.attrDomainName, + ].join(''); + } +} diff --git a/packages/aws-cdk/test/commands/test-resources/stacks/s3_stack.py b/packages/aws-cdk/test/commands/test-resources/stacks/s3_stack.py new file mode 100644 index 0000000000000..327db5465456b --- /dev/null +++ b/packages/aws-cdk/test/commands/test-resources/stacks/s3_stack.py @@ -0,0 +1,47 @@ +from aws_cdk import Stack +import aws_cdk as cdk +import aws_cdk.aws_s3 as s3 +from constructs import Construct + +""" + AWS CloudFormation Sample Template S3_Website_Bucket_With_Retain_On_Delete: Sample template showing how to create a publicly accessible S3 bucket configured for website access with a deletion policy of retain on delete. +""" +class GoodPythonStack(Stack): + """ + URL for website hosted on S3 + """ + global website_u_r_l + """ + Name of S3 bucket to hold website content + """ + global s3_bucket_secure_u_r_l + + def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: + super().__init__(scope, construct_id, **kwargs) + + # Resources + s3Bucket = s3.CfnBucket(self, 'S3Bucket', + access_control = 'PublicRead', + website_configuration = { + 'indexDocument': 'index.html', + 'errorDocument': 'error.html', + }, + ) + s3Bucket.cfnOptions.deletionPolicy = cdk.CfnDeletionPolicy.RETAIN + + # Outputs + self.website_u_r_l = s3Bucket.attrwebsiteUrl + cdk.CfnOutput(self, 'WebsiteURL', + description = 'URL for website hosted on S3', + value = self.website_u_r_l, + ) + self.s3_bucket_secure_u_r_l = [ + 'https://', + s3Bucket.attrdomainName, + ].join('') + cdk.CfnOutput(self, 'S3BucketSecureURL', + description = 'Name of S3 bucket to hold website content', + value = self.s3_bucket_secure_u_r_l, + ) + + diff --git a/packages/aws-cdk/test/commands/test-resources/templates/autoscaling-template.yml b/packages/aws-cdk/test/commands/test-resources/templates/autoscaling-template.yml new file mode 100644 index 0000000000000..3178ee9558678 --- /dev/null +++ b/packages/aws-cdk/test/commands/test-resources/templates/autoscaling-template.yml @@ -0,0 +1,677 @@ +AWSTemplateFormatVersion: 2010-09-09 +Description: >- + AWS CloudFormation Sample Template AutoScalingMultiAZWithNotifications: Create + a multi-az, load balanced and Auto Scaled sample web site running on an Apache + Web Serever. The application is configured to span all Availability Zones in + the region and is Auto-Scaled based on the CPU utilization of the web servers. + Notifications will be sent to the operator email address on scaling events. + The instances are load balanced with a simple health check against the default + web page. +Parameters: + VpcId: + Type: 'AWS::EC2::VPC::Id' + Description: VpcId of your existing Virtual Private Cloud (VPC) + ConstraintDescription: must be the VPC Id of an existing Virtual Private Cloud. + Subnets: + Type: 'List' + Description: The list of SubnetIds in your Virtual Private Cloud (VPC) + ConstraintDescription: >- + must be a list of at least two existing subnets associated with at least + two different availability zones. They should be residing in the selected + Virtual Private Cloud. + InstanceType: + Description: WebServer EC2 instance type + Type: String + Default: t2.small + AllowedValues: + - t1.micro + - t2.nano + - t2.micro + - t2.small + - t2.medium + - t2.large + - m1.small + - m1.medium + - m1.large + - m1.xlarge + - m2.xlarge + - m2.2xlarge + - m2.4xlarge + - m3.medium + - m3.large + - m3.xlarge + - m3.2xlarge + - m4.large + - m4.xlarge + - m4.2xlarge + - m4.4xlarge + - m4.10xlarge + - c1.medium + - c1.xlarge + - c3.large + - c3.xlarge + - c3.2xlarge + - c3.4xlarge + - c3.8xlarge + - c4.large + - c4.xlarge + - c4.2xlarge + - c4.4xlarge + - c4.8xlarge + - g2.2xlarge + - g2.8xlarge + - r3.large + - r3.xlarge + - r3.2xlarge + - r3.4xlarge + - r3.8xlarge + - i2.xlarge + - i2.2xlarge + - i2.4xlarge + - i2.8xlarge + - d2.xlarge + - d2.2xlarge + - d2.4xlarge + - d2.8xlarge + - hi1.4xlarge + - hs1.8xlarge + - cr1.8xlarge + - cc2.8xlarge + - cg1.4xlarge + ConstraintDescription: must be a valid EC2 instance type. + OperatorEMail: + Description: EMail address to notify if there are any scaling operations + Type: String + AllowedPattern: >- + ([a-zA-Z0-9_\-\.]+)@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.)|(([a-zA-Z0-9\-]+\.)+))([a-zA-Z]{2,4}|[0-9]{1,3})(\]?) + ConstraintDescription: must be a valid email address. + KeyName: + Description: The EC2 Key Pair to allow SSH access to the instances + Type: 'AWS::EC2::KeyPair::KeyName' + ConstraintDescription: must be the name of an existing EC2 KeyPair. + SSHLocation: + Description: The IP address range that can be used to SSH to the EC2 instances + Type: String + MinLength: '9' + MaxLength: '18' + Default: 0.0.0.0/0 + AllowedPattern: '(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})/(\d{1,2})' + ConstraintDescription: must be a valid IP CIDR range of the form x.x.x.x/x. +Mappings: + Region2Examples: + ap-east-1: + Examples: 'https://s3-ap-east-1.amazonaws.com/cloudformation-examples-ap-east-1' + ap-northeast-1: + Examples: >- + https://s3-ap-northeast-1.amazonaws.com/cloudformation-examples-ap-northeast-1 + ap-northeast-2: + Examples: >- + https://s3-ap-northeast-2.amazonaws.com/cloudformation-examples-ap-northeast-2 + ap-northeast-3: + Examples: >- + https://s3-ap-northeast-3.amazonaws.com/cloudformation-examples-ap-northeast-3 + ap-south-1: + Examples: 'https://s3-ap-south-1.amazonaws.com/cloudformation-examples-ap-south-1' + ap-southeast-1: + Examples: >- + https://s3-ap-southeast-1.amazonaws.com/cloudformation-examples-ap-southeast-1 + ap-southeast-2: + Examples: >- + https://s3-ap-southeast-2.amazonaws.com/cloudformation-examples-ap-southeast-2 + ca-central-1: + Examples: >- + https://s3-ca-central-1.amazonaws.com/cloudformation-examples-ca-central-1 + cn-north-1: + Examples: >- + https://s3.cn-north-1.amazonaws.com.cn/cloudformation-examples-cn-north-1 + cn-northwest-1: + Examples: >- + https://s3.cn-northwest-1.amazonaws.com.cn/cloudformation-examples-cn-northwest-1 + eu-central-1: + Examples: >- + https://s3-eu-central-1.amazonaws.com/cloudformation-examples-eu-central-1 + eu-north-1: + Examples: 'https://s3-eu-north-1.amazonaws.com/cloudformation-examples-eu-north-1' + eu-west-1: + Examples: 'https://s3-eu-west-1.amazonaws.com/cloudformation-examples-eu-west-1' + eu-west-2: + Examples: 'https://s3-eu-west-2.amazonaws.com/cloudformation-examples-eu-west-2' + eu-west-3: + Examples: 'https://s3-eu-west-3.amazonaws.com/cloudformation-examples-eu-west-3' + me-south-1: + Examples: 'https://s3-me-south-1.amazonaws.com/cloudformation-examples-me-south-1' + sa-east-1: + Examples: 'https://s3-sa-east-1.amazonaws.com/cloudformation-examples-sa-east-1' + us-east-1: + Examples: 'https://s3.amazonaws.com/cloudformation-examples-us-east-1' + us-east-2: + Examples: 'https://s3-us-east-2.amazonaws.com/cloudformation-examples-us-east-2' + us-west-1: + Examples: 'https://s3-us-west-1.amazonaws.com/cloudformation-examples-us-west-1' + us-west-2: + Examples: 'https://s3-us-west-2.amazonaws.com/cloudformation-examples-us-west-2' + AWSInstanceType2Arch: + t1.micro: + Arch: HVM64 + t2.nano: + Arch: HVM64 + t2.micro: + Arch: HVM64 + t2.small: + Arch: HVM64 + t2.medium: + Arch: HVM64 + t2.large: + Arch: HVM64 + m1.small: + Arch: HVM64 + m1.medium: + Arch: HVM64 + m1.large: + Arch: HVM64 + m1.xlarge: + Arch: HVM64 + m2.xlarge: + Arch: HVM64 + m2.2xlarge: + Arch: HVM64 + m2.4xlarge: + Arch: HVM64 + m3.medium: + Arch: HVM64 + m3.large: + Arch: HVM64 + m3.xlarge: + Arch: HVM64 + m3.2xlarge: + Arch: HVM64 + m4.large: + Arch: HVM64 + m4.xlarge: + Arch: HVM64 + m4.2xlarge: + Arch: HVM64 + m4.4xlarge: + Arch: HVM64 + m4.10xlarge: + Arch: HVM64 + c1.medium: + Arch: HVM64 + c1.xlarge: + Arch: HVM64 + c3.large: + Arch: HVM64 + c3.xlarge: + Arch: HVM64 + c3.2xlarge: + Arch: HVM64 + c3.4xlarge: + Arch: HVM64 + c3.8xlarge: + Arch: HVM64 + c4.large: + Arch: HVM64 + c4.xlarge: + Arch: HVM64 + c4.2xlarge: + Arch: HVM64 + c4.4xlarge: + Arch: HVM64 + c4.8xlarge: + Arch: HVM64 + g2.2xlarge: + Arch: HVMG2 + g2.8xlarge: + Arch: HVMG2 + r3.large: + Arch: HVM64 + r3.xlarge: + Arch: HVM64 + r3.2xlarge: + Arch: HVM64 + r3.4xlarge: + Arch: HVM64 + r3.8xlarge: + Arch: HVM64 + i2.xlarge: + Arch: HVM64 + i2.2xlarge: + Arch: HVM64 + i2.4xlarge: + Arch: HVM64 + i2.8xlarge: + Arch: HVM64 + d2.xlarge: + Arch: HVM64 + d2.2xlarge: + Arch: HVM64 + d2.4xlarge: + Arch: HVM64 + d2.8xlarge: + Arch: HVM64 + hi1.4xlarge: + Arch: HVM64 + hs1.8xlarge: + Arch: HVM64 + cr1.8xlarge: + Arch: HVM64 + cc2.8xlarge: + Arch: HVM64 + AWSInstanceType2NATArch: + t1.micro: + Arch: NATHVM64 + t2.nano: + Arch: NATHVM64 + t2.micro: + Arch: NATHVM64 + t2.small: + Arch: NATHVM64 + t2.medium: + Arch: NATHVM64 + t2.large: + Arch: NATHVM64 + m1.small: + Arch: NATHVM64 + m1.medium: + Arch: NATHVM64 + m1.large: + Arch: NATHVM64 + m1.xlarge: + Arch: NATHVM64 + m2.xlarge: + Arch: NATHVM64 + m2.2xlarge: + Arch: NATHVM64 + m2.4xlarge: + Arch: NATHVM64 + m3.medium: + Arch: NATHVM64 + m3.large: + Arch: NATHVM64 + m3.xlarge: + Arch: NATHVM64 + m3.2xlarge: + Arch: NATHVM64 + m4.large: + Arch: NATHVM64 + m4.xlarge: + Arch: NATHVM64 + m4.2xlarge: + Arch: NATHVM64 + m4.4xlarge: + Arch: NATHVM64 + m4.10xlarge: + Arch: NATHVM64 + c1.medium: + Arch: NATHVM64 + c1.xlarge: + Arch: NATHVM64 + c3.large: + Arch: NATHVM64 + c3.xlarge: + Arch: NATHVM64 + c3.2xlarge: + Arch: NATHVM64 + c3.4xlarge: + Arch: NATHVM64 + c3.8xlarge: + Arch: NATHVM64 + c4.large: + Arch: NATHVM64 + c4.xlarge: + Arch: NATHVM64 + c4.2xlarge: + Arch: NATHVM64 + c4.4xlarge: + Arch: NATHVM64 + c4.8xlarge: + Arch: NATHVM64 + g2.2xlarge: + Arch: NATHVMG2 + g2.8xlarge: + Arch: NATHVMG2 + r3.large: + Arch: NATHVM64 + r3.xlarge: + Arch: NATHVM64 + r3.2xlarge: + Arch: NATHVM64 + r3.4xlarge: + Arch: NATHVM64 + r3.8xlarge: + Arch: NATHVM64 + i2.xlarge: + Arch: NATHVM64 + i2.2xlarge: + Arch: NATHVM64 + i2.4xlarge: + Arch: NATHVM64 + i2.8xlarge: + Arch: NATHVM64 + d2.xlarge: + Arch: NATHVM64 + d2.2xlarge: + Arch: NATHVM64 + d2.4xlarge: + Arch: NATHVM64 + d2.8xlarge: + Arch: NATHVM64 + hi1.4xlarge: + Arch: NATHVM64 + hs1.8xlarge: + Arch: NATHVM64 + cr1.8xlarge: + Arch: NATHVM64 + cc2.8xlarge: + Arch: NATHVM64 + AWSRegionArch2AMI: + af-south-1: + HVM64: ami-064cc455f8a1ef504 + HVMG2: NOT_SUPPORTED + ap-east-1: + HVM64: ami-f85b1989 + HVMG2: NOT_SUPPORTED + ap-northeast-1: + HVM64: ami-0b2c2a754d5b4da22 + HVMG2: ami-09d0e0e099ecabba2 + ap-northeast-2: + HVM64: ami-0493ab99920f410fc + HVMG2: NOT_SUPPORTED + ap-northeast-3: + HVM64: ami-01344f6f63a4decc1 + HVMG2: NOT_SUPPORTED + ap-south-1: + HVM64: ami-03cfb5e1fb4fac428 + HVMG2: ami-0244c1d42815af84a + ap-southeast-1: + HVM64: ami-0ba35dc9caf73d1c7 + HVMG2: ami-0e46ce0d6a87dc979 + ap-southeast-2: + HVM64: ami-0ae99b503e8694028 + HVMG2: ami-0c0ab057a101d8ff2 + ca-central-1: + HVM64: ami-0803e21a2ec22f953 + HVMG2: NOT_SUPPORTED + cn-north-1: + HVM64: ami-07a3f215cc90c889c + HVMG2: NOT_SUPPORTED + cn-northwest-1: + HVM64: ami-0a3b3b10f714a0ff4 + HVMG2: NOT_SUPPORTED + eu-central-1: + HVM64: ami-0474863011a7d1541 + HVMG2: ami-0aa1822e3eb913a11 + eu-north-1: + HVM64: ami-0de4b8910494dba0f + HVMG2: ami-32d55b4c + eu-south-1: + HVM64: ami-08427144fe9ebdef6 + HVMG2: NOT_SUPPORTED + eu-west-1: + HVM64: ami-015232c01a82b847b + HVMG2: ami-0d5299b1c6112c3c7 + eu-west-2: + HVM64: ami-0765d48d7e15beb93 + HVMG2: NOT_SUPPORTED + eu-west-3: + HVM64: ami-0caf07637eda19d9c + HVMG2: NOT_SUPPORTED + me-south-1: + HVM64: ami-0744743d80915b497 + HVMG2: NOT_SUPPORTED + sa-east-1: + HVM64: ami-0a52e8a6018e92bb0 + HVMG2: NOT_SUPPORTED + us-east-1: + HVM64: ami-032930428bf1abbff + HVMG2: ami-0aeb704d503081ea6 + us-east-2: + HVM64: ami-027cab9a7bf0155df + HVMG2: NOT_SUPPORTED + us-west-1: + HVM64: ami-088c153f74339f34c + HVMG2: ami-0a7fc72dc0e51aa77 + us-west-2: + HVM64: ami-01fee56b22f308154 + HVMG2: ami-0fe84a5b4563d8f27 +Resources: + NotificationTopic: + Type: 'AWS::SNS::Topic' + Properties: + Subscription: + - Endpoint: !Ref OperatorEMail + Protocol: email + WebServerGroup: + Type: 'AWS::AutoScaling::AutoScalingGroup' + Properties: + VPCZoneIdentifier: !Ref Subnets + LaunchConfigurationName: !Ref LaunchConfig + MinSize: '1' + MaxSize: '3' + TargetGroupARNs: + - !Ref ALBTargetGroup + NotificationConfiguration: + TopicARN: !Ref NotificationTopic + NotificationTypes: + - 'autoscaling:EC2_INSTANCE_LAUNCH' + - 'autoscaling:EC2_INSTANCE_LAUNCH_ERROR' + - 'autoscaling:EC2_INSTANCE_TERMINATE' + - 'autoscaling:EC2_INSTANCE_TERMINATE_ERROR' + CreationPolicy: + ResourceSignal: + Timeout: PT15M + Count: '1' + UpdatePolicy: + AutoScalingRollingUpdate: + MinInstancesInService: '1' + MaxBatchSize: '1' + PauseTime: PT15M + WaitOnResourceSignals: 'true' + LaunchConfig: + Type: 'AWS::AutoScaling::LaunchConfiguration' + Metadata: + Comment: Install a simple application + 'AWS::CloudFormation::Init': + config: + packages: + yum: + httpd: [] + files: + /var/www/html/index.html: + content: !Join + - |+ + + - - AWS CloudFormation Logo + - >- +

Congratulations, you have successfully launched the AWS + CloudFormation sample.

+ mode: '000644' + owner: root + group: root + /etc/cfn/cfn-hup.conf: + content: !Join + - '' + - - | + [main] + - stack= + - !Ref 'AWS::StackId' + - |+ + + - region= + - !Ref 'AWS::Region' + - |+ + + mode: '000400' + owner: root + group: root + /etc/cfn/hooks.d/cfn-auto-reloader.conf: + content: !Join + - '' + - - | + [cfn-auto-reloader-hook] + - | + triggers=post.update + - > + path=Resources.LaunchConfig.Metadata.AWS::CloudFormation::Init + - 'action=/opt/aws/bin/cfn-init -v ' + - ' --stack ' + - !Ref 'AWS::StackName' + - ' --resource LaunchConfig ' + - ' --region ' + - !Ref 'AWS::Region' + - |+ + + - | + runas=root + mode: '000400' + owner: root + group: root + services: + sysvinit: + httpd: + enabled: 'true' + ensureRunning: 'true' + cfn-hup: + enabled: 'true' + ensureRunning: 'true' + files: + - /etc/cfn/cfn-hup.conf + - /etc/cfn/hooks.d/cfn-auto-reloader.conf + Properties: + KeyName: !Ref KeyName + ImageId: !FindInMap + - AWSRegionArch2AMI + - !Ref 'AWS::Region' + - !FindInMap + - AWSInstanceType2Arch + - !Ref InstanceType + - Arch + SecurityGroups: + - !Ref InstanceSecurityGroup + InstanceType: !Ref InstanceType + UserData: !Base64 + 'Fn::Join': + - '' + - - | + #!/bin/bash -xe + - | + yum update -y aws-cfn-bootstrap + - | + yum update -y aws-cli + - '/opt/aws/bin/cfn-init -v ' + - ' --stack ' + - !Ref 'AWS::StackName' + - ' --resource LaunchConfig ' + - ' --region ' + - !Ref 'AWS::Region' + - |+ + + - '/opt/aws/bin/cfn-signal -e $? ' + - ' --stack ' + - !Ref 'AWS::StackName' + - ' --resource WebServerGroup ' + - ' --region ' + - !Ref 'AWS::Region' + - |+ + + WebServerScaleUpPolicy: + Type: 'AWS::AutoScaling::ScalingPolicy' + Properties: + AdjustmentType: ChangeInCapacity + AutoScalingGroupName: !Ref WebServerGroup + Cooldown: '60' + ScalingAdjustment: '1' + WebServerScaleDownPolicy: + Type: 'AWS::AutoScaling::ScalingPolicy' + Properties: + AdjustmentType: ChangeInCapacity + AutoScalingGroupName: !Ref WebServerGroup + Cooldown: '60' + ScalingAdjustment: '-1' + CPUAlarmHigh: + Type: 'AWS::CloudWatch::Alarm' + Properties: + AlarmDescription: Scale-up if CPU > 90% for 10 minutes + MetricName: CPUUtilization + Namespace: AWS/EC2 + Statistic: Average + Period: '300' + EvaluationPeriods: '2' + Threshold: '90' + AlarmActions: + - !Ref WebServerScaleUpPolicy + Dimensions: + - Name: AutoScalingGroupName + Value: !Ref WebServerGroup + ComparisonOperator: GreaterThanThreshold + CPUAlarmLow: + Type: 'AWS::CloudWatch::Alarm' + Properties: + AlarmDescription: Scale-down if CPU < 70% for 10 minutes + MetricName: CPUUtilization + Namespace: AWS/EC2 + Statistic: Average + Period: '300' + EvaluationPeriods: '2' + Threshold: '70' + AlarmActions: + - !Ref WebServerScaleDownPolicy + Dimensions: + - Name: AutoScalingGroupName + Value: !Ref WebServerGroup + ComparisonOperator: LessThanThreshold + ApplicationLoadBalancer: + Type: 'AWS::ElasticLoadBalancingV2::LoadBalancer' + Properties: + Subnets: !Ref Subnets + ALBListener: + Type: 'AWS::ElasticLoadBalancingV2::Listener' + Properties: + DefaultActions: + - Type: forward + TargetGroupArn: !Ref ALBTargetGroup + LoadBalancerArn: !Ref ApplicationLoadBalancer + Port: '80' + Protocol: HTTP + ALBTargetGroup: + Type: 'AWS::ElasticLoadBalancingV2::TargetGroup' + Properties: + HealthCheckIntervalSeconds: 30 + HealthCheckTimeoutSeconds: 5 + HealthyThresholdCount: 3 + Port: 80 + Protocol: HTTP + UnhealthyThresholdCount: 5 + VpcId: !Ref VpcId + InstanceSecurityGroup: + Type: 'AWS::EC2::SecurityGroup' + Properties: + GroupDescription: Enable SSH access and HTTP from the load balancer only + SecurityGroupIngress: + - IpProtocol: tcp + FromPort: '22' + ToPort: '22' + CidrIp: !Ref SSHLocation + - IpProtocol: tcp + FromPort: '80' + ToPort: '80' + SourceSecurityGroupId: !Select + - 0 + - !GetAtt + - ApplicationLoadBalancer + - SecurityGroups + VpcId: !Ref VpcId +Outputs: + URL: + Description: The URL of the website + Value: !Join + - '' + - - 'http://' + - !GetAtt + - ApplicationLoadBalancer + - DNSName \ No newline at end of file diff --git a/packages/aws-cdk/test/commands/test-resources/templates/s3-template.json b/packages/aws-cdk/test/commands/test-resources/templates/s3-template.json new file mode 100644 index 0000000000000..39200f41bda3d --- /dev/null +++ b/packages/aws-cdk/test/commands/test-resources/templates/s3-template.json @@ -0,0 +1,45 @@ +{ + "AWSTemplateFormatVersion": "2010-09-09", + "Description": "AWS CloudFormation Sample Template S3_Website_Bucket_With_Retain_On_Delete: Sample template showing how to create a publicly accessible S3 bucket configured for website access with a deletion policy of retain on delete.", + "Resources": { + "S3Bucket": { + "Type": "AWS::S3::Bucket", + "Properties": { + "AccessControl": "PublicRead", + "WebsiteConfiguration": { + "IndexDocument": "index.html", + "ErrorDocument": "error.html" + } + }, + "DeletionPolicy": "Retain" + } + }, + "Outputs": { + "WebsiteURL": { + "Value": { + "Fn::GetAtt": [ + "S3Bucket", + "WebsiteURL" + ] + }, + "Description": "URL for website hosted on S3" + }, + "S3BucketSecureURL": { + "Value": { + "Fn::Join": [ + "", + [ + "https://", + { + "Fn::GetAtt": [ + "S3Bucket", + "DomainName" + ] + } + ] + ] + }, + "Description": "Name of S3 bucket to hold website content" + } + } +} \ No newline at end of file diff --git a/packages/aws-cdk/test/commands/test-resources/templates/sqs-template.json b/packages/aws-cdk/test/commands/test-resources/templates/sqs-template.json new file mode 100644 index 0000000000000..42767b2a2f808 --- /dev/null +++ b/packages/aws-cdk/test/commands/test-resources/templates/sqs-template.json @@ -0,0 +1,91 @@ +{ + "AWSTemplateFormatVersion": "2010-09-09", + "Description": "AWS CloudFormation Sample Template SQS_With_CloudWatch_Alarms: Sample template showing how to create an SQS queue with AWS CloudWatch alarms on queue depth.", + "Parameters": { + "AlarmEMail": { + "Description": "EMail address to notify if there are any operational issues", + "Type": "String", + "AllowedPattern": "([a-zA-Z0-9_\\-\\.]+)@((\\[[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.)|(([a-zA-Z0-9\\-]+\\.)+))([a-zA-Z]{2,4}|[0-9]{1,3})(\\]?)", + "ConstraintDescription": "must be a valid email address." + } + }, + "Resources": { + "MyQueue": { + "Type": "AWS::SQS::Queue", + "Properties": {} + }, + "AlarmTopic": { + "Type": "AWS::SNS::Topic", + "Properties": { + "Subscription": [ + { + "Endpoint": { + "Ref": "AlarmEMail" + }, + "Protocol": "email" + } + ] + } + }, + "QueueDepthAlarm": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "AlarmDescription": "Alarm if queue depth grows beyond 10 messages", + "Namespace": "AWS/SQS", + "MetricName": "ApproximateNumberOfMessagesVisible", + "Dimensions": [ + { + "Name": "QueueName", + "Value": { + "Fn::GetAtt": [ + "MyQueue", + "QueueName" + ] + } + } + ], + "Statistic": "Sum", + "Period": "300", + "EvaluationPeriods": "1", + "Threshold": "10", + "ComparisonOperator": "GreaterThanThreshold", + "AlarmActions": [ + { + "Ref": "AlarmTopic" + } + ], + "InsufficientDataActions": [ + { + "Ref": "AlarmTopic" + } + ] + } + } + }, + "Outputs": { + "QueueURL": { + "Description": "URL of newly created SQS Queue", + "Value": { + "Ref": "MyQueue" + } + }, + "QueueARN": { + "Description": "ARN of newly created SQS Queue", + "Value": { + "Fn::GetAtt": [ + "MyQueue", + "Arn" + ] + } + }, + "QueueName": { + "Description": "Name newly created SQS Queue", + "Value": { + "Fn::GetAtt": [ + "MyQueue", + "QueueName" + ] + } + } + } +} \ No newline at end of file diff --git a/tools/@aws-cdk/spec2cdk/package.json b/tools/@aws-cdk/spec2cdk/package.json index 348df39ec1a8e..b86e2aa0369eb 100644 --- a/tools/@aws-cdk/spec2cdk/package.json +++ b/tools/@aws-cdk/spec2cdk/package.json @@ -32,8 +32,8 @@ }, "license": "Apache-2.0", "dependencies": { - "@aws-cdk/aws-service-spec": "^0.0.12", - "@aws-cdk/service-spec-types": "^0.0.12", + "@aws-cdk/aws-service-spec": "^0.0.14", + "@aws-cdk/service-spec-types": "^0.0.14", "@cdklabs/tskb": "^0.0.1", "@cdklabs/typewriter": "^0.0.1", "camelcase": "^6", diff --git a/yarn.lock b/yarn.lock index 24db65b99b0f0..b4b48b6df4f67 100644 --- a/yarn.lock +++ b/yarn.lock @@ -55,12 +55,12 @@ resolved "https://registry.npmjs.org/@aws-cdk/asset-node-proxy-agent-v6/-/asset-node-proxy-agent-v6-2.0.1.tgz#6dc9b7cdb22ff622a7176141197962360c33e9ac" integrity sha512-DDt4SLdLOwWCjGtltH4VCST7hpOI5DzieuhGZsBpZ+AgJdSI2GCjklCXm0GCTwJG/SolkL5dtQXyUKgg9luBDg== -"@aws-cdk/aws-service-spec@^0.0.12": - version "0.0.12" - resolved "https://registry.npmjs.org/@aws-cdk/aws-service-spec/-/aws-service-spec-0.0.12.tgz#ce0d1176d261655bf11f4c0b7b1b2779b50d7122" - integrity sha512-APf7RM0lM4iu8s6aP3/bfSUjklrYGi6OGBd4Z5efwDHqebg0rgN7jFucN8i1WGs2oMom+dAsHwZH2RSySUZoOQ== +"@aws-cdk/aws-service-spec@^0.0.14": + version "0.0.14" + resolved "https://registry.npmjs.org/@aws-cdk/aws-service-spec/-/aws-service-spec-0.0.14.tgz#1c1c07c4941dfe4b52ea73ab7e1aecf082559fd9" + integrity sha512-/8UKqt0HyOLyXRVbze1OvFSIQBdvDfpinqvZztAHjL0PXduJI1NRA+KGzWCzx3LMztN9Sgd+229EoKrNnEQNvw== dependencies: - "@aws-cdk/service-spec-types" "^0.0.12" + "@aws-cdk/service-spec-types" "^0.0.14" "@cdklabs/tskb" "^0.0.1" "@aws-cdk/lambda-layer-kubectl-v24@^2.0.242": @@ -68,10 +68,10 @@ resolved "https://registry.npmjs.org/@aws-cdk/lambda-layer-kubectl-v24/-/lambda-layer-kubectl-v24-2.0.242.tgz#4273a5ad7714f933a7eba155eb9280823086db71" integrity sha512-7/wIOo685tmrEe4hh6zqDELhBZh5OQGf3Hd2FU2Vnwy2ZubW8qTmEw5gqJCsCrGKeYDoa1BcVhDRZ/nzjkaqyA== -"@aws-cdk/service-spec-types@^0.0.12": - version "0.0.12" - resolved "https://registry.npmjs.org/@aws-cdk/service-spec-types/-/service-spec-types-0.0.12.tgz#d442f2ecce0f084ffca5ea8a7a3957d9b2c4517e" - integrity sha512-Fp3UgSKCxlOCVzFLCeDmRROT5RYtRKysn9PW9bESCVlisnON7BBml4xCTELpMB/F/MsYQXjlvnpbQGLAnuRhuw== +"@aws-cdk/service-spec-types@^0.0.14": + version "0.0.14" + resolved "https://registry.npmjs.org/@aws-cdk/service-spec-types/-/service-spec-types-0.0.14.tgz#4bc5b2200c9f6077718f71ebe424d6ae3bf8810f" + integrity sha512-mXWxDz0nA/t9JfcJE8aKmzWmULhHZWKbyLxb+Z7mj0nOzILZKNSFkow7cqWYTS/uOMeJVHeNhdu18FcraByS+g== dependencies: "@cdklabs/tskb" "^0.0.1"