-
Notifications
You must be signed in to change notification settings - Fork 4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
aws-cdk-toolkit: Initial external release
Co-authored-by: Rico Huijbers <huijbers@amazon.nl> Co-authored-by: Marcadier-Muller, Romain <rmuller@amazon.lu>
- Loading branch information
3 people
committed
May 30, 2018
1 parent
58a45ea
commit 4d7d88b
Showing
31 changed files
with
3,335 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
*.js | ||
*.js.map | ||
*.d.ts | ||
node_modules | ||
dist |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
#!/usr/bin/env node | ||
require('./cdk.js'); |
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
import { CredentialProviderChain } from 'aws-sdk'; | ||
|
||
export enum Mode { | ||
ForReading, | ||
ForWriting | ||
} | ||
|
||
export interface CredentialProviderSource { | ||
name: string; | ||
|
||
/** | ||
* Whether the credential provider is even online | ||
* | ||
* Guaranteed to be called before any of the other functions are called. | ||
*/ | ||
isAvailable(): Promise<boolean>; | ||
|
||
/** | ||
* Whether the credential provider can provide credentials for the given account. | ||
*/ | ||
canProvideCredentials(accountId: string): Promise<boolean>; | ||
|
||
/** | ||
* Construct a credential provider for the given account and the given access mode | ||
* | ||
* Guaranteed to be called only if canProvideCredentails() returned true at some point. | ||
*/ | ||
getProvider(accountId: string, mode: Mode): Promise<CredentialProviderChain>; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
import { App } from 'aws-cdk'; | ||
import { Environment } from 'aws-cdk-cx-api'; | ||
import { deployStack, DeployStackResult } from './deploy-stack'; | ||
import { SDK } from './util/sdk'; | ||
import { ToolkitStack } from './util/toolkit-stack'; | ||
|
||
export async function bootstrapEnvironment(environment: Environment, aws: SDK, toolkitStackName: string): Promise<DeployStackResult> { | ||
const app = new App(); | ||
const stack = new ToolkitStack(app, toolkitStackName, { env: environment }); | ||
const synthesizedStack = await app.synthesizeStack(stack.name); | ||
return await deployStack(synthesizedStack, aws); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,153 @@ | ||
import { App, Stack } from 'aws-cdk'; | ||
import { StackInfo, SynthesizedStack } from 'aws-cdk-cx-api'; | ||
import { cloudformation } from 'aws-cdk-resources'; | ||
import { CloudFormation } from 'aws-sdk'; | ||
import * as colors from 'colors/safe'; | ||
import * as crypto from 'crypto'; | ||
import * as uuid from 'uuid'; | ||
import * as YAML from 'yamljs'; | ||
import { debug, error } from '../logging'; | ||
import { Mode } from './aws-auth/credentials'; | ||
import { ToolkitInfo } from './toolkit-info'; | ||
import { describeStack, stackExists, waitForChangeSet, waitForStack } from './util/cloudformation'; | ||
import { StackActivityMonitor } from './util/cloudformation/stack-activity-monitor'; | ||
import { SDK } from './util/sdk'; | ||
|
||
type TemplateBodyParameter = { | ||
TemplateBody?: string | ||
TemplateURL?: string | ||
}; | ||
|
||
export interface DeployStackResult { | ||
readonly noOp: boolean; | ||
readonly outputs: { [name: string]: string }; | ||
} | ||
|
||
export async function deployStack(stack: SynthesizedStack, | ||
sdk: SDK = new SDK(), | ||
toolkitInfo?: ToolkitInfo, | ||
deployName?: string, | ||
quiet: boolean = false): Promise<DeployStackResult> { | ||
if (!stack.environment) { | ||
throw new Error(`The stack ${stack.name} does not have an environment`); | ||
} | ||
|
||
deployName = deployName || stack.name; | ||
|
||
const executionId = uuid.v4(); | ||
|
||
const cfn = await sdk.cloudFormation(stack.environment, Mode.ForWriting); | ||
const bodyParameter = await makeBodyParameter(stack, sdk, toolkitInfo); | ||
|
||
if (!await stackExists(cfn, deployName)) { | ||
await createEmptyStack(cfn, deployName, quiet); | ||
} else { | ||
debug('Stack named %s already exists, updating it!', deployName); | ||
} | ||
|
||
const changeSetName = `CDK-${executionId}`; | ||
debug('Attempting to create ChangeSet %s on stack %s', changeSetName, deployName); | ||
const changeSet = await cfn.createChangeSet({ | ||
StackName: deployName, | ||
ChangeSetName: changeSetName, | ||
Description: `CDK Changeset for execution ${executionId}`, | ||
TemplateBody: bodyParameter.TemplateBody, | ||
TemplateURL: bodyParameter.TemplateURL, | ||
Capabilities: [ 'CAPABILITY_IAM', 'CAPABILITY_NAMED_IAM' ] | ||
}).promise(); | ||
debug('Initiated creation of changeset: %s; waiting for it to finish creating...', changeSet.Id); | ||
const changeSetDescription = await waitForChangeSet(cfn, deployName, changeSetName); | ||
if (!changeSetDescription || !changeSetDescription.Changes || changeSetDescription.Changes.length === 0) { | ||
debug('No changes are to be performed on %s, assuming success.', deployName); | ||
await cfn.deleteChangeSet({ StackName: deployName, ChangeSetName: changeSetName }).promise(); | ||
return { noOp: true, outputs: await getStackOutputs(cfn, deployName) }; | ||
} | ||
|
||
debug('Initiating execution of changeset %s on stack %s', changeSetName, deployName); | ||
await cfn.executeChangeSet({ StackName: deployName, ChangeSetName: changeSetName }).promise(); | ||
const monitor = quiet ? undefined : new StackActivityMonitor(cfn, deployName, stack.metadata, changeSetDescription.Changes.length).start(); | ||
debug('Execution of changeset %s on stack %s has started; waiting for the update to complete...', changeSetName, deployName); | ||
await waitForStack(cfn, deployName); | ||
if (monitor) { monitor.stop(); } | ||
debug('Stack %s has completed updating', deployName); | ||
return { noOp: false, outputs: await getStackOutputs(cfn, deployName) }; | ||
} | ||
|
||
async function getStackOutputs(cfn: CloudFormation, stackName: string): Promise<{ [name: string]: string }> { | ||
const description = await describeStack(cfn, stackName); | ||
const result: { [name: string]: string } = {}; | ||
if (description && description.Outputs) { | ||
description.Outputs.forEach(output => { | ||
result[output.OutputKey!] = output.OutputValue!; | ||
}); | ||
} | ||
return result; | ||
} | ||
|
||
async function createEmptyStack(cfn: CloudFormation, stackName: string, quiet: boolean): Promise<void> { | ||
debug('Creating new empty stack named %s', stackName); | ||
const app = new App(); | ||
const stack = new Stack(app, stackName); | ||
stack.templateOptions.description = 'This is an empty stack created by AWS CDK during a deployment attempt'; | ||
new cloudformation.WaitConditionHandleResource(stack, 'WaitCondition'); | ||
const template = (await app.synthesizeStack(stackName)).template; | ||
|
||
const response = await cfn.createStack({ StackName: stackName, TemplateBody: JSON.stringify(template, null, 2) }).promise(); | ||
debug('CreateStack response: %j', response); | ||
const monitor = quiet ? undefined : new StackActivityMonitor(cfn, stackName, undefined, 1).start(); | ||
await waitForStack(cfn, stackName); | ||
if (monitor) { monitor.stop(); } | ||
} | ||
|
||
/** | ||
* Prepares the body parameter for +CreateChangeSet+, putting the generated CloudFormation template in the toolkit-provided | ||
* S3 bucket if present, otherwise using in-line template argument. If no +ToolkitInfo+ is provided and the template is | ||
* larger than 50,200 bytes, an +Error+ will be raised. | ||
* | ||
* @param stack the synthesized stack that provides the CloudFormation template | ||
* @param sdk an AWS SDK to use when interacting with S3 | ||
* @param toolkitInfo information about the toolkit stack | ||
*/ | ||
async function makeBodyParameter(stack: SynthesizedStack, sdk: SDK, toolkitInfo?: ToolkitInfo): Promise<TemplateBodyParameter> { | ||
const templateJson = YAML.stringify(stack.template, 16, 4); | ||
if (toolkitInfo) { | ||
const hash = crypto.createHash('sha256').update(templateJson).digest('hex'); | ||
const key = `cdk/${stack.name}/${hash}.yml`; | ||
const s3 = await sdk.s3(stack.environment!, Mode.ForWriting); | ||
await s3.putObject({ | ||
Bucket: toolkitInfo.bucketName, | ||
Key: key, | ||
Body: templateJson, | ||
ContentType: 'application/x-yaml' | ||
}).promise(); | ||
debug('Stored template in S3 at s3://%s/%s', toolkitInfo.bucketName, key); | ||
return { TemplateURL: `https://${toolkitInfo.bucketName}.s3.amazonaws.com/${key}` }; | ||
} else if (templateJson.length > 51_200) { | ||
error('The template for stack %s is %d bytes long, a CDK Toolkit stack is required for deployment of templates larger than 51,200 bytes. ' + | ||
'A CDK Toolkit stack can be created using %s', | ||
stack.name, templateJson.length, colors.blue(`cdk bootstrap '${stack.environment!.name}'`)); | ||
throw new Error(`The template for stack ${stack.name} is larger than 50,200 bytes, and no CDK Toolkit info was provided`); | ||
} else { | ||
return { TemplateBody: templateJson }; | ||
} | ||
} | ||
|
||
export async function destroyStack(stack: StackInfo, sdk: SDK = new SDK(), deployName?: string, quiet: boolean = false) { | ||
if (!stack.environment) { | ||
throw new Error(`The stack ${stack.name} does not have an environment`); | ||
} | ||
|
||
deployName = deployName || stack.name; | ||
const cfn = await sdk.cloudFormation(stack.environment, Mode.ForWriting); | ||
if (!await stackExists(cfn, deployName)) { | ||
return; | ||
} | ||
const monitor = quiet ? undefined : new StackActivityMonitor(cfn, deployName).start(); | ||
await cfn.deleteStack({ StackName: deployName }).promise().catch(e => { throw e; }); | ||
const destroyedStack = await waitForStack(cfn, deployName, false); | ||
if (monitor) { monitor.stop(); } | ||
if (destroyedStack && destroyedStack.StackStatus !== 'DELETE_COMPLETE') { | ||
throw new Error(`Failed to destroy ${deployName} (current state: ${destroyedStack.StackStatus})!`); | ||
} | ||
return; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
import 'source-map-support/register'; | ||
|
||
export * from './aws-auth/credentials'; | ||
export * from './bootstrap-environment'; | ||
export * from './deploy-stack'; | ||
export * from './toolkit-info'; | ||
export * from './util/sdk'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
import { App, Output } from 'aws-cdk'; | ||
import { Environment } from 'aws-cdk-cx-api'; | ||
import { CloudFormation } from 'aws-sdk'; | ||
import * as colors from 'colors/safe'; | ||
import { debug } from '../logging'; | ||
import { Mode } from './aws-auth/credentials'; | ||
import { waitForStack } from './util/cloudformation'; | ||
import { SDK } from './util/sdk'; | ||
import { ToolkitStack } from './util/toolkit-stack'; | ||
|
||
export interface ToolkitInfo { | ||
bucketName: string | ||
bucketEndpoint: string | ||
} | ||
|
||
export async function loadToolkitInfo(environment: Environment, sdk: SDK, stackName: string): Promise<ToolkitInfo | undefined> { | ||
const cfn = await sdk.cloudFormation(environment, Mode.ForReading); | ||
const stack = await waitForStack(cfn, stackName); | ||
if (!stack) { | ||
debug('The environment %s doesn\'t have the CDK toolkit stack (%s) installed. Use %s to setup your environment for use with the toolkit.', | ||
environment.name, stackName, colors.blue(`cdk bootstrap "${environment.name}"`)); | ||
return undefined; | ||
} | ||
const stackModel = new ToolkitStack(new App(), 'ToolkitStack', { env: environment }); | ||
return { | ||
bucketName: getOutputValue(stack, stackModel.bucketNameOutput), | ||
bucketEndpoint: getOutputValue(stack, stackModel.bucketDomainNameOutput) | ||
}; | ||
} | ||
|
||
function getOutputValue(stack: CloudFormation.Stack, output: Output): string { | ||
const name = output.name; | ||
let result: string | undefined; | ||
if (stack.Outputs) { | ||
const found = stack.Outputs.find(o => o.OutputKey === name); | ||
result = found && found.OutputValue; | ||
} | ||
if (result === undefined) { | ||
throw new Error(`The CDK toolkit stack (${stack.StackName}) does not have an output named ${name}. Use 'cdk bootstrap' to correct this.`); | ||
} | ||
return result; | ||
} |
137 changes: 137 additions & 0 deletions
137
packages/aws-cdk-toolkit/lib/api/util/cloudformation.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,137 @@ | ||
import { CloudFormation } from 'aws-sdk'; | ||
import { debug } from '../../logging'; | ||
import { StackStatus } from './cloudformation/stack-status'; | ||
|
||
/** | ||
* Describe a changeset in CloudFormation, regardless of it's current state. | ||
* | ||
* @param cfn a CloudFormation client | ||
* @param stackName the name of the Stack the ChangeSet belongs to | ||
* @param changeSetName the name of the ChangeSet | ||
* | ||
* @returns CloudFormation information about the ChangeSet | ||
*/ | ||
async function describeChangeSet(cfn: CloudFormation, stackName: string, changeSetName: string): Promise<CloudFormation.DescribeChangeSetOutput> { | ||
const response = await cfn.describeChangeSet({ StackName: stackName, ChangeSetName: changeSetName }).promise(); | ||
return response; | ||
} | ||
|
||
/** | ||
* Describes a stack in CloudFormation, regardless of it's current state. | ||
* | ||
* @param cfn a CloudFormation client | ||
* @param stackName the name of the stack to be described | ||
* | ||
* @returns +undefined+ if the stack does not exist or is deleted, and the CloudFormation stack description otherwise | ||
*/ | ||
export async function describeStack(cfn: CloudFormation, stackName: string): Promise<CloudFormation.Stack | undefined> { | ||
try { | ||
const response = await cfn.describeStacks({ StackName: stackName }).promise(); | ||
return response.Stacks && response.Stacks[0]; | ||
} catch (e) { | ||
if (e.code === 'ValidationError' && e.message === `Stack with id ${stackName} does not exist`) { | ||
return undefined; | ||
} | ||
throw e; | ||
} | ||
} | ||
|
||
/** | ||
* Checks whether a stack exists in CloudFormation. | ||
* | ||
* @param cfn a CloudFormation client | ||
* @param stackName the name of the stack to be checked for | ||
* | ||
* @returns +true+ if the stack exists, regardless of it's current state | ||
*/ | ||
export async function stackExists(cfn: CloudFormation, stackName: string): Promise<boolean> { | ||
const description = await describeStack(cfn, stackName); | ||
return description !== undefined; | ||
} | ||
|
||
/** | ||
* Waits for a function to return non-+undefined+ before returning. | ||
* | ||
* @param valueProvider a function that will return a value that is not +undefined+ once the wait should be over | ||
* @param timeout the time to wait between two calls to +valueProvider+ | ||
* | ||
* @returns the value that was returned by +valueProvider+ | ||
*/ | ||
async function waitFor<T>(valueProvider: () => Promise<T | null | undefined>, timeout: number = 5000): Promise<T | undefined> { | ||
while (true) { | ||
const result = await valueProvider(); | ||
if (result === null) { | ||
return undefined; | ||
} else if (result !== undefined) { | ||
return result; | ||
} | ||
await new Promise(cb => setTimeout(cb, timeout)); | ||
} | ||
} | ||
|
||
/** | ||
* Waits for a ChangeSet to be available for triggering a StackUpdate. | ||
* | ||
* @param cfn a CloudFormation client | ||
* @param stackName the name of the Stack that the ChangeSet belongs to | ||
* @param changeSetName the name of the ChangeSet | ||
* | ||
* @returns the CloudFormation description of the ChangeSet | ||
*/ | ||
// tslint:disable-next-line:max-line-length | ||
export async function waitForChangeSet(cfn: CloudFormation, stackName: string, changeSetName: string): Promise<CloudFormation.DescribeChangeSetOutput | undefined> { | ||
debug('Waiting for changeset %s on stack %s to finish creating...', changeSetName, stackName); | ||
return waitFor(async () => { | ||
const description = await describeChangeSet(cfn, stackName, changeSetName); | ||
// The following doesn't use a switch because tsc will not allow fall-through, UNLESS it is allows | ||
// EVERYWHERE that uses this library directly or indirectly, which is undesirable. | ||
if (description.Status === 'CREATE_PENDING' || description.Status === 'CREATE_IN_PROGRESS') { | ||
debug('Changeset %s on stack %s is still creating', changeSetName, stackName); | ||
return undefined; | ||
} else if (description.Status === 'CREATE_COMPLETE') { | ||
return description; | ||
} else if (description.Status === 'FAILED') { | ||
if (description.StatusReason && description.StatusReason.startsWith('The submitted information didn\'t contain changes.')) { | ||
return description; | ||
} | ||
} | ||
// tslint:disable-next-line:max-line-length | ||
throw new Error(`Failed to create ChangeSet ${changeSetName} on ${stackName}: ${description.Status || 'NO_STATUS'}, ${description.StatusReason || 'no reason provided'}`); | ||
}); | ||
} | ||
|
||
/** | ||
* Waits for a CloudFormation stack to stabilize in a complete/available state. | ||
* | ||
* @param cfn a CloudFormation client | ||
* @param stackName the name of the stack to wait for | ||
* @param failOnDeletedStack whether to fail if the awaited stack is deleted. | ||
* | ||
* @returns the CloudFormation description of the stabilized stack | ||
*/ | ||
export async function waitForStack(cfn: CloudFormation, | ||
stackName: string, | ||
failOnDeletedStack: boolean = true): Promise<CloudFormation.Stack | undefined> { | ||
debug('Waiting for stack %s to finish creating or updating...', stackName); | ||
return waitFor(async () => { | ||
const description = await describeStack(cfn, stackName); | ||
if (!description) { | ||
debug('Stack %s does not exist', stackName); | ||
return null; | ||
} | ||
const status = new StackStatus(description.StackStatus); | ||
if (!status.isStable) { | ||
debug('Stack %s is still not stable (%s)', stackName, status.name); | ||
return undefined; | ||
} | ||
if (status.isCreationFailure) { | ||
throw new Error(`The stack named ${stackName} failed creation, it may need to be manually deleted from the AWS console.`); | ||
} else if (!status.isSuccess) { | ||
throw new Error(`The stack named ${stackName} is in a failed state: ${status.name}`); | ||
} else if (status.isDeleted) { | ||
if (failOnDeletedStack) { throw new Error(`The stack named ${stackName} was deleted`); } | ||
return undefined; | ||
} | ||
return description; | ||
}); | ||
} |
Oops, something went wrong.