From d7371f0f836ce8b0e08df1673672f904bde1cfe1 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Wed, 9 Jan 2019 09:17:47 +0100 Subject: [PATCH] feat(cdk): transparently use constructs from another stack It is now no longer necessary to use `export()` and `import()` when sharing constructs between two `Stacks` inside the same CDK app. Instead, objects defined in one stack can be used directly in another stack. The CDK will detect when an attribute (such as an ARN, ID or URL) of such an object is used in a different stack, and will automatically create the required `Export` in the producing stack and insert the corresponding `Fn::ImportValue` in the consuming stack. BREAKING CHANGE: if you are using `export()` and `import()` to share constructs between stacks, you can stop doing that, instead of `FooImportProps` accept an `IFoo` directly on the consuming stack, and use that object as usual. `ArnUtils.fromComponents()` and `ArnUtils.parse()` have been moved onto `Stack`. All CloudFormation pseudo-parameter (such as `AWS::AccountId` etc) are now also accessible via `Stack`, as `stack.accountId` etc. `resolve()` has been moved to `this.node.resolve()`. `CloudFormationJSON.stringify()` has been moved to `this.node.stringifyJson()`. `validate()` now should be `protected`. Fixes #1324. --- design/aws-guidelines.md | 2 +- .../advanced-usage/index.ts | 17 +- .../lib/pipeline-deploy-stack-action.ts | 20 +- .../test/test.pipeline-deploy-stack-action.ts | 2 +- packages/@aws-cdk/assert/lib/expect.ts | 5 +- .../assets-docker/lib/adopted-repository.ts | 4 +- .../@aws-cdk/aws-apigateway/lib/deployment.ts | 15 +- .../aws-apigateway/lib/integrations/aws.ts | 22 +- .../aws-apigateway/lib/integrations/lambda.ts | 1 + .../@aws-cdk/aws-apigateway/lib/method.ts | 3 +- .../@aws-cdk/aws-apigateway/lib/restapi.ts | 24 +- packages/@aws-cdk/aws-apigateway/lib/stage.ts | 4 +- .../test/integ.restapi.books.expected.json | 4 +- .../test/integ.restapi.defaults.expected.json | 4 +- .../test/integ.restapi.expected.json | 4 +- .../aws-apigateway/test/test.deployment.ts | 2 +- .../aws-apigateway/test/test.method.ts | 4 +- .../aws-apigateway/test/test.restapi.ts | 26 +- .../lib/pipeline-actions.ts | 30 +- .../test/test.pipeline-actions.ts | 59 +- packages/@aws-cdk/aws-cloudtrail/lib/index.ts | 4 +- .../aws-cloudtrail/test/test.cloudtrail.ts | 6 +- .../@aws-cdk/aws-cloudwatch/lib/dashboard.ts | 4 +- packages/@aws-cdk/aws-cloudwatch/lib/graph.ts | 8 +- .../aws-cloudwatch/test/test.graphs.ts | 18 +- .../@aws-cdk/aws-codebuild/lib/project.ts | 40 +- .../aws-codebuild/test/test.codebuild.ts | 2 +- .../@aws-cdk/aws-codecommit/lib/repository.ts | 5 +- .../aws-codedeploy/lib/application.ts | 8 +- .../aws-codedeploy/lib/deployment-config.ts | 24 +- .../aws-codedeploy/lib/deployment-group.ts | 22 +- .../aws-codedeploy/lib/pipeline-action.ts | 2 +- .../aws-codepipeline-api/lib/action.ts | 16 +- .../aws-codepipeline-api/lib/artifact.ts | 6 +- .../@aws-cdk/aws-codepipeline/lib/pipeline.ts | 32 +- .../@aws-cdk/aws-codepipeline/lib/stage.ts | 8 +- ...eg.pipeline-cfn-cross-region.expected.json | 6 +- .../test/test.general-validation.ts | 6 +- .../aws-codepipeline/test/test.pipeline.ts | 6 +- packages/@aws-cdk/aws-dynamodb/lib/table.ts | 4 +- .../aws-dynamodb/test/test.dynamodb.ts | 4 +- packages/@aws-cdk/aws-ec2/lib/vpc-ref.ts | 15 +- packages/@aws-cdk/aws-ec2/lib/vpc.ts | 1 + packages/@aws-cdk/aws-ec2/test/test.vpc.ts | 10 +- .../@aws-cdk/aws-ecr/lib/repository-ref.ts | 8 +- packages/@aws-cdk/aws-ecr/lib/repository.ts | 2 +- .../@aws-cdk/aws-ecr/test/test.repository.ts | 26 +- .../@aws-cdk/aws-ecs/lib/base/base-service.ts | 2 +- .../aws-ecs/lib/base/task-definition.ts | 40 +- .../@aws-cdk/aws-ecs/lib/ec2/ec2-service.ts | 2 +- .../aws-ecs/lib/log-drivers/aws-log-driver.ts | 3 +- .../test/ec2/test.ec2-task-definition.ts | 2 +- .../lib/alb/application-listener-rule.ts | 22 +- .../lib/alb/application-listener.ts | 22 +- .../lib/shared/base-listener.ts | 2 +- .../test/alb/test.listener.ts | 2 +- .../test/alb/test.load-balancer.ts | 2 +- packages/@aws-cdk/aws-events/lib/rule.ts | 2 +- .../@aws-cdk/aws-events/test/test.rule.ts | 6 +- .../@aws-cdk/aws-iam/lib/managed-policy.ts | 4 +- .../@aws-cdk/aws-iam/lib/policy-document.ts | 39 +- packages/@aws-cdk/aws-iam/lib/policy.ts | 2 +- packages/@aws-cdk/aws-iam/lib/util.ts | 6 +- .../aws-iam/test/test.managed-policy.ts | 5 +- .../aws-iam/test/test.policy-document.ts | 53 +- packages/@aws-cdk/aws-iam/test/test.role.ts | 8 +- packages/@aws-cdk/aws-kinesis/lib/stream.ts | 5 +- packages/@aws-cdk/aws-kms/lib/key.ts | 4 +- packages/@aws-cdk/aws-kms/test/integ.key.ts | 4 +- .../@aws-cdk/aws-lambda/lib/lambda-ref.ts | 8 +- packages/@aws-cdk/aws-lambda/lib/lambda.ts | 4 +- .../@aws-cdk/aws-lambda/test/test.alias.ts | 10 +- .../@aws-cdk/aws-lambda/test/test.lambda.ts | 20 +- .../aws-logs/lib/cross-account-destination.ts | 4 +- packages/@aws-cdk/aws-logs/lib/log-group.ts | 2 +- .../aws-rds/lib/cluster-parameter-group.ts | 2 +- .../@aws-cdk/aws-rds/test/test.cluster.ts | 4 +- .../@aws-cdk/aws-route53/lib/hosted-zone.ts | 2 +- .../test/test.hosted-zone-provider.ts | 2 +- .../@aws-cdk/aws-route53/test/test.route53.ts | 6 +- packages/@aws-cdk/aws-s3/lib/bucket.ts | 7 +- .../notifications-resource-handler.ts | 2 +- packages/@aws-cdk/aws-s3/lib/util.ts | 10 +- packages/@aws-cdk/aws-s3/test/test.bucket.ts | 14 +- packages/@aws-cdk/aws-s3/test/test.util.ts | 30 +- .../test/test.secret-string.ts | 4 +- packages/@aws-cdk/aws-sns/test/test.sns.ts | 13 +- packages/@aws-cdk/aws-sqs/test/test.sqs.ts | 10 +- .../test/test.parameter-store-string.ts | 4 +- .../aws-stepfunctions/lib/state-machine.ts | 5 +- .../aws-stepfunctions/lib/states/parallel.ts | 20 +- .../aws-stepfunctions/test/test.activity.ts | 4 +- .../test/test.state-machine-resources.ts | 4 +- .../test/test.states-language.ts | 2 +- packages/@aws-cdk/cdk/lib/app.ts | 12 +- .../@aws-cdk/cdk/lib/cloudformation/arn.ts | 373 ++++++------- .../cdk/lib/cloudformation/cfn-tokens.ts | 112 ++++ .../lib/cloudformation/cloudformation-json.ts | 18 +- .../cloudformation/cloudformation-token.ts | 106 ---- .../cdk/lib/cloudformation/condition.ts | 9 +- .../@aws-cdk/cdk/lib/cloudformation/fn.ts | 62 ++- .../cdk/lib/cloudformation/include.ts | 2 +- .../cdk/lib/cloudformation/instrinsics.ts | 50 ++ .../cdk/lib/cloudformation/logical-id.ts | 2 +- .../cdk/lib/cloudformation/mapping.ts | 2 +- .../@aws-cdk/cdk/lib/cloudformation/output.ts | 39 +- .../cdk/lib/cloudformation/parameter.ts | 2 +- .../@aws-cdk/cdk/lib/cloudformation/pseudo.ts | 102 ++-- .../cdk/lib/cloudformation/resource.ts | 18 +- .../@aws-cdk/cdk/lib/cloudformation/rule.ts | 4 +- .../cdk/lib/cloudformation/stack-element.ts | 172 ++++++ .../@aws-cdk/cdk/lib/cloudformation/stack.ts | 339 +++++++----- packages/@aws-cdk/cdk/lib/core/construct.ts | 145 ++++- packages/@aws-cdk/cdk/lib/core/tag-manager.ts | 4 +- packages/@aws-cdk/cdk/lib/core/tokens.ts | 520 ------------------ .../cdk/lib/core/tokens/cfn-concat.ts | 22 + .../@aws-cdk/cdk/lib/core/tokens/encoding.ts | 288 ++++++++++ .../@aws-cdk/cdk/lib/core/tokens/index.ts | 4 + .../@aws-cdk/cdk/lib/core/tokens/options.ts | 56 ++ .../@aws-cdk/cdk/lib/core/tokens/resolve.ts | 145 +++++ .../@aws-cdk/cdk/lib/core/tokens/token.ts | 133 +++++ .../cdk/lib/core/tokens/unresolved.ts | 18 + packages/@aws-cdk/cdk/lib/core/util.ts | 14 +- packages/@aws-cdk/cdk/lib/index.ts | 3 +- packages/@aws-cdk/cdk/lib/runtime.ts | 7 +- packages/@aws-cdk/cdk/lib/util/uniqueid.ts | 4 +- .../cdk/test/cloudformation/test.arn.ts | 77 ++- .../test.cloudformation-json.ts | 60 +- .../cloudformation/test.dynamic-reference.ts | 4 +- .../cdk/test/cloudformation/test.fn.ts | 46 +- .../cdk/test/cloudformation/test.output.ts | 6 +- .../cdk/test/cloudformation/test.parameter.ts | 6 +- .../cdk/test/cloudformation/test.resource.ts | 4 +- .../cdk/test/cloudformation/test.secret.ts | 9 +- .../cdk/test/cloudformation/test.stack.ts | 180 +++++- .../@aws-cdk/cdk/test/core/test.construct.ts | 8 +- .../cdk/test/core/test.tag-manager.ts | 41 +- .../@aws-cdk/cdk/test/core/test.tokens.ts | 44 +- packages/@aws-cdk/cdk/test/core/test.util.ts | 48 +- packages/@aws-cdk/cdk/test/test.app.ts | 2 +- packages/@aws-cdk/cdk/test/test.context.ts | 4 +- packages/@aws-cdk/cx-api/lib/cxapi.ts | 5 + packages/@aws-cdk/runtime-values/lib/rtv.ts | 17 +- .../runtime-values/test/integ.rtv.lambda.ts | 2 +- .../@aws-cdk/runtime-values/test/test.rtv.ts | 4 +- packages/aws-cdk/bin/cdk.ts | 14 +- packages/aws-cdk/integ-tests/app/app.js | 16 + packages/aws-cdk/integ-tests/common.bash | 2 +- .../aws-cdk/integ-tests/test-cdk-order.sh | 15 + packages/aws-cdk/lib/api/cxapp/stacks.ts | 13 +- .../lib/api/util/string-manipulation.ts | 7 + packages/aws-cdk/lib/api/util/toposort.ts | 44 ++ scripts/generate-aggregate-tsconfig.sh | 10 +- tools/cdk-integ-tools/bin/cdk-integ.ts | 3 +- tools/cfn2ts/lib/codegen.ts | 2 +- 155 files changed, 2767 insertions(+), 1688 deletions(-) create mode 100644 packages/@aws-cdk/cdk/lib/cloudformation/cfn-tokens.ts delete mode 100644 packages/@aws-cdk/cdk/lib/cloudformation/cloudformation-token.ts create mode 100644 packages/@aws-cdk/cdk/lib/cloudformation/instrinsics.ts create mode 100644 packages/@aws-cdk/cdk/lib/cloudformation/stack-element.ts delete mode 100644 packages/@aws-cdk/cdk/lib/core/tokens.ts create mode 100644 packages/@aws-cdk/cdk/lib/core/tokens/cfn-concat.ts create mode 100644 packages/@aws-cdk/cdk/lib/core/tokens/encoding.ts create mode 100644 packages/@aws-cdk/cdk/lib/core/tokens/index.ts create mode 100644 packages/@aws-cdk/cdk/lib/core/tokens/options.ts create mode 100644 packages/@aws-cdk/cdk/lib/core/tokens/resolve.ts create mode 100644 packages/@aws-cdk/cdk/lib/core/tokens/token.ts create mode 100644 packages/@aws-cdk/cdk/lib/core/tokens/unresolved.ts create mode 100755 packages/aws-cdk/integ-tests/test-cdk-order.sh create mode 100644 packages/aws-cdk/lib/api/util/string-manipulation.ts create mode 100644 packages/aws-cdk/lib/api/util/toposort.ts diff --git a/design/aws-guidelines.md b/design/aws-guidelines.md index e03a689de5a38..879dcee75cf20 100644 --- a/design/aws-guidelines.md +++ b/design/aws-guidelines.md @@ -204,7 +204,7 @@ properties that allow the user to specify an external resource identity, usually by providing one or more resource attributes such as ARN, physical name, etc. The import interface should have the minimum required properties, that is: if it -is possible to parse the resource name from the ARN (using `cdk.ArnUtils.parse`), +is possible to parse the resource name from the ARN (using `cdk.Stack.parseArn`), then only the ARN should be required. In cases where it is not possible to parse the ARN (e.g. if it is a token and the resource name might have use "/" characters), both the ARN and the name should be optional and diff --git a/examples/cdk-examples-typescript/advanced-usage/index.ts b/examples/cdk-examples-typescript/advanced-usage/index.ts index 20fb650ebbe2c..0bd33a422e47e 100644 --- a/examples/cdk-examples-typescript/advanced-usage/index.ts +++ b/examples/cdk-examples-typescript/advanced-usage/index.ts @@ -157,7 +157,7 @@ class CloudFormationExample extends cdk.Stack { // outputs are constructs the synthesize into the template's "Outputs" section new cdk.Output(this, 'Output', { description: 'This is an output of the template', - value: `${new cdk.AwsAccountId()}/${param.ref}` + value: `${this.accountId}/${param.ref}` }); // stack.templateOptions can be used to specify template-level options @@ -166,14 +166,13 @@ class CloudFormationExample extends cdk.Stack { // all CloudFormation's pseudo-parameters are supported via the `cdk.AwsXxx` classes PseudoParameters: [ - new cdk.AwsAccountId(), - new cdk.AwsDomainSuffix(), - new cdk.AwsNotificationARNs(), - new cdk.AwsNoValue(), - new cdk.AwsPartition(), - new cdk.AwsRegion(), - new cdk.AwsStackId(), - new cdk.AwsStackName(), + this.accountId, + this.urlSuffix, + this.notificationArns, + this.partition, + this.region, + this.stackId, + this.stackName, ], // all CloudFormation's intrinsic functions are supported via the `cdk.Fn.xxx` static methods. diff --git a/packages/@aws-cdk/app-delivery/lib/pipeline-deploy-stack-action.ts b/packages/@aws-cdk/app-delivery/lib/pipeline-deploy-stack-action.ts index e784850965d7a..4b9873f6b48b2 100644 --- a/packages/@aws-cdk/app-delivery/lib/pipeline-deploy-stack-action.ts +++ b/packages/@aws-cdk/app-delivery/lib/pipeline-deploy-stack-action.ts @@ -140,16 +140,6 @@ export class PipelineDeployStackAction extends cdk.Construct { }); } - public validate(): string[] { - const result = super.validate(); - const assets = this.stack.node.metadata.filter(md => md.type === cxapi.ASSET_METADATA); - if (assets.length > 0) { - // FIXME: Implement the necessary actions to publish assets - result.push(`Cannot deploy the stack ${this.stack.name} because it references ${assets.length} asset(s)`); - } - return result; - } - /** * Add policy statements to the role deploying the stack. * @@ -162,6 +152,16 @@ export class PipelineDeployStackAction extends cdk.Construct { public addToRolePolicy(statement: iam.PolicyStatement) { this.role.addToPolicy(statement); } + + protected validate(): string[] { + const result = super.validate(); + const assets = this.stack.node.metadata.filter(md => md.type === cxapi.ASSET_METADATA); + if (assets.length > 0) { + // FIXME: Implement the necessary actions to publish assets + result.push(`Cannot deploy the stack ${this.stack.name} because it references ${assets.length} asset(s)`); + } + return result; + } } function cfnCapabilities(adminPermissions: boolean, capabilities?: cfn.CloudFormationCapabilities): cfn.CloudFormationCapabilities { diff --git a/packages/@aws-cdk/app-delivery/test/test.pipeline-deploy-stack-action.ts b/packages/@aws-cdk/app-delivery/test/test.pipeline-deploy-stack-action.ts index 3a73933d135e9..25f41261ec849 100644 --- a/packages/@aws-cdk/app-delivery/test/test.pipeline-deploy-stack-action.ts +++ b/packages/@aws-cdk/app-delivery/test/test.pipeline-deploy-stack-action.ts @@ -274,7 +274,7 @@ export = nodeunit.testCase({ for (let i = 0 ; i < assetCount ; i++) { deployedStack.node.addMetadata(cxapi.ASSET_METADATA, {}); } - test.deepEqual(action.validate(), + test.deepEqual(action.node.validateTree().map(x => x.message), [`Cannot deploy the stack DeployedStack because it references ${assetCount} asset(s)`]); } ) diff --git a/packages/@aws-cdk/assert/lib/expect.ts b/packages/@aws-cdk/assert/lib/expect.ts index a98a18abdc389..13112fbf4b6cd 100644 --- a/packages/@aws-cdk/assert/lib/expect.ts +++ b/packages/@aws-cdk/assert/lib/expect.ts @@ -9,6 +9,9 @@ export function expect(stack: api.SynthesizedStack | cdk.Stack, skipValidation = if (isStackClassInstance(stack)) { if (!skipValidation) { + // Do a prepare-and-validate run over the given stack + stack.node.prepareTree(); + const errors = stack.node.validateTree(); if (errors.length > 0) { throw new Error(`Stack validation failed:\n${errors.map(e => `${e.message} at: ${e.source.node.scope}`).join('\n')}`); @@ -34,4 +37,4 @@ export function expect(stack: api.SynthesizedStack | cdk.Stack, skipValidation = function isStackClassInstance(x: api.SynthesizedStack | cdk.Stack): x is cdk.Stack { return 'toCloudFormation' in x; -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/assets-docker/lib/adopted-repository.ts b/packages/@aws-cdk/assets-docker/lib/adopted-repository.ts index e8948eb436c0a..c8f15a7fc8dbe 100644 --- a/packages/@aws-cdk/assets-docker/lib/adopted-repository.ts +++ b/packages/@aws-cdk/assets-docker/lib/adopted-repository.ts @@ -42,7 +42,7 @@ export class AdoptedRepository extends ecr.RepositoryBase { }); fn.addToRolePolicy(new iam.PolicyStatement() - .addResource(ecr.Repository.arnForLocalRepository(props.repositoryName)) + .addResource(ecr.Repository.arnForLocalRepository(props.repositoryName, this)) .addActions( 'ecr:GetRepositoryPolicy', 'ecr:SetRepositoryPolicy', @@ -67,7 +67,7 @@ export class AdoptedRepository extends ecr.RepositoryBase { // this this repository is "local" to the stack (in the same region/account) // we can render it's ARN from it's name. - this.repositoryArn = ecr.Repository.arnForLocalRepository(this.repositoryName); + this.repositoryArn = ecr.Repository.arnForLocalRepository(this.repositoryName, this); } /** diff --git a/packages/@aws-cdk/aws-apigateway/lib/deployment.ts b/packages/@aws-cdk/aws-apigateway/lib/deployment.ts index 2e7b45c73533c..4aef0dd03306a 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/deployment.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/deployment.ts @@ -107,6 +107,7 @@ class LatestDeploymentResource extends CfnDeployment { private originalLogicalId?: string; private lazyLogicalIdRequired: boolean; private lazyLogicalId?: string; + private logicalIdToken: cdk.Token; private hashComponents = new Array(); constructor(scope: cdk.Construct, id: string, props: CfnDeploymentProps) { @@ -114,6 +115,8 @@ class LatestDeploymentResource extends CfnDeployment { // from this point, don't allow accessing logical ID before synthesis this.lazyLogicalIdRequired = true; + + this.logicalIdToken = new cdk.Token(() => this.lazyLogicalId); } /** @@ -124,11 +127,7 @@ class LatestDeploymentResource extends CfnDeployment { return this.originalLogicalId!; } - if (!this.lazyLogicalId) { - throw new Error('This resource has a lazy logical ID which is calculated just before synthesis. Use a cdk.Token to evaluate'); - } - - return this.lazyLogicalId; + return this.logicalIdToken.toString(); } /** @@ -170,7 +169,7 @@ class LatestDeploymentResource extends CfnDeployment { * Hooks into synthesis to calculate a logical ID that hashes all the components * add via `addToLogicalId`. */ - public validate() { + protected prepare() { // if hash components were added to the deployment, we use them to calculate // a logical ID for the deployment resource. if (this.hashComponents.length === 0) { @@ -178,12 +177,10 @@ class LatestDeploymentResource extends CfnDeployment { } else { const md5 = crypto.createHash('md5'); this.hashComponents - .map(c => cdk.resolve(c)) + .map(c => this.node.resolve(c)) .forEach(c => md5.update(JSON.stringify(c))); this.lazyLogicalId = this.originalLogicalId + md5.digest("hex"); } - - return []; } } diff --git a/packages/@aws-cdk/aws-apigateway/lib/integrations/aws.ts b/packages/@aws-cdk/aws-apigateway/lib/integrations/aws.ts index 70d13b641c2f0..341bbc88b40fb 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/integrations/aws.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/integrations/aws.ts @@ -1,5 +1,6 @@ import cdk = require('@aws-cdk/cdk'); import { Integration, IntegrationOptions, IntegrationType } from '../integration'; +import { Method } from '../method'; import { parseAwsApiCall } from '../util'; export interface AwsIntegrationProps { @@ -61,6 +62,8 @@ export interface AwsIntegrationProps { * technology. */ export class AwsIntegration extends Integration { + private scope?: cdk.IConstruct; + constructor(props: AwsIntegrationProps) { const backend = props.subdomain ? `${props.subdomain}.${props.service}` : props.service; const type = props.proxy ? IntegrationType.AwsProxy : IntegrationType.Aws; @@ -68,14 +71,21 @@ export class AwsIntegration extends Integration { super({ type, integrationHttpMethod: 'POST', - uri: cdk.ArnUtils.fromComponents({ - service: 'apigateway', - account: backend, - resource: apiType, - sep: '/', - resourceName: apiValue, + uri: new cdk.Token(() => { + if (!this.scope) { throw new Error('AwsIntegration must be used in API'); } + return cdk.Stack.find(this.scope).formatArn({ + service: 'apigateway', + account: backend, + resource: apiType, + sep: '/', + resourceName: apiValue, + }); }), options: props.options, }); } + + public bind(method: Method) { + this.scope = method; + } } diff --git a/packages/@aws-cdk/aws-apigateway/lib/integrations/lambda.ts b/packages/@aws-cdk/aws-apigateway/lib/integrations/lambda.ts index 1970b71923455..10ba40547523d 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/integrations/lambda.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/integrations/lambda.ts @@ -52,6 +52,7 @@ export class LambdaIntegration extends AwsIntegration { } public bind(method: Method) { + super.bind(method); const principal = new iam.ServicePrincipal('apigateway.amazonaws.com'); const desc = `${method.httpMethod}.${method.resource.resourcePath.replace(/\//g, '.')}`; diff --git a/packages/@aws-cdk/aws-apigateway/lib/method.ts b/packages/@aws-cdk/aws-apigateway/lib/method.ts index 08dc1841ee4ff..3822c56ee85e3 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/method.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/method.ts @@ -158,7 +158,8 @@ export class Method extends cdk.Construct { credentials = options.credentialsRole.roleArn; } else if (options.credentialsPassthrough) { // arn:aws:iam::*:user/* - credentials = cdk.ArnUtils.fromComponents({ service: 'iam', region: '', account: '*', resource: 'user', sep: '/', resourceName: '*' }); + // tslint:disable-next-line:max-line-length + credentials = cdk.Stack.find(this).formatArn({ service: 'iam', region: '', account: '*', resource: 'user', sep: '/', resourceName: '*' }); } return { diff --git a/packages/@aws-cdk/aws-apigateway/lib/restapi.ts b/packages/@aws-cdk/aws-apigateway/lib/restapi.ts index b4cdda1805f1a..ea6ed72213356 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/restapi.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/restapi.ts @@ -301,7 +301,7 @@ export class RestApi extends cdk.Construct implements cdk.IDependable, IRestApi method = '*'; } - return cdk.ArnUtils.fromComponents({ + return cdk.Stack.find(this).formatArn({ service: 'execute-api', resource: this.restApiId, sep: '/', @@ -309,10 +309,18 @@ export class RestApi extends cdk.Construct implements cdk.IDependable, IRestApi }); } + /** + * Internal API used by `Method` to keep an inventory of methods at the API + * level for validation purposes. + */ + public _attachMethod(method: Method) { + this.methods.push(method); + } + /** * Performs validation of the REST API. */ - public validate() { + protected validate() { if (this.methods.length === 0) { return [ `The REST API doesn't contain any methods` ]; } @@ -320,14 +328,6 @@ export class RestApi extends cdk.Construct implements cdk.IDependable, IRestApi return []; } - /** - * Internal API used by `Method` to keep an inventory of methods at the API - * level for validation purposes. - */ - public _attachMethod(method: Method) { - this.methods.push(method); - } - private configureDeployment(props: RestApiProps) { const deploy = props.deploy === undefined ? true : props.deploy; if (deploy) { @@ -358,7 +358,7 @@ export class RestApi extends cdk.Construct implements cdk.IDependable, IRestApi private configureCloudWatchRole(apiResource: CfnRestApi) { const role = new iam.Role(this, 'CloudWatchRole', { assumedBy: new iam.ServicePrincipal('apigateway.amazonaws.com'), - managedPolicyArns: [ cdk.ArnUtils.fromComponents({ + managedPolicyArns: [ cdk.Stack.find(this).formatArn({ service: 'iam', region: '', account: 'aws', @@ -405,8 +405,6 @@ export enum EndpointType { Private = 'PRIVATE' } -export class RestApiUrl extends cdk.CloudFormationToken { } - class ImportedRestApi extends cdk.Construct implements IRestApi { public restApiId: string; diff --git a/packages/@aws-cdk/aws-apigateway/lib/stage.ts b/packages/@aws-cdk/aws-apigateway/lib/stage.ts index 8878217162c3a..45bad2898c1eb 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/stage.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/stage.ts @@ -1,4 +1,5 @@ import cdk = require('@aws-cdk/cdk'); +import { Stack } from '@aws-cdk/cdk'; import { CfnStage } from './apigateway.generated'; import { Deployment } from './deployment'; import { IRestApi } from './restapi'; @@ -180,7 +181,8 @@ export class Stage extends cdk.Construct implements cdk.IDependable { if (!path.startsWith('/')) { throw new Error(`Path must begin with "/": ${path}`); } - return `https://${this.restApi.restApiId}.execute-api.${new cdk.AwsRegion()}.amazonaws.com/${this.stageName}${path}`; + const stack = Stack.find(this); + return `https://${this.restApi.restApiId}.execute-api.${stack.region}.${stack.urlSuffix}/${this.stageName}${path}`; } private renderMethodSettings(props: StageProps): CfnStage.MethodSettingProperty[] | undefined { diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.books.expected.json b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.books.expected.json index e76050bfba5a0..acdcd09664d43 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.books.expected.json +++ b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.books.expected.json @@ -836,7 +836,9 @@ { "Ref": "AWS::Region" }, - ".amazonaws.com/", + ".", + { "Ref": "AWS::URLSuffix" }, + "/", { "Ref": "booksapiDeploymentStageprod55D8E03E" }, diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.defaults.expected.json b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.defaults.expected.json index aac6799e40c09..888fa29a306be 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.defaults.expected.json +++ b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.defaults.expected.json @@ -109,7 +109,9 @@ { "Ref": "AWS::Region" }, - ".amazonaws.com/", + ".", + { "Ref": "AWS::URLSuffix" }, + "/", { "Ref": "myapiDeploymentStageprod298F01AF" }, diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.expected.json b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.expected.json index 5d15ff5c7b253..3af25751a5649 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.expected.json +++ b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.expected.json @@ -601,7 +601,9 @@ { "Ref": "AWS::Region" }, - ".amazonaws.com/", + ".", + { "Ref": "AWS::URLSuffix" }, + "/", { "Ref": "myapiDeploymentStagebeta96434BEB" }, diff --git a/packages/@aws-cdk/aws-apigateway/test/test.deployment.ts b/packages/@aws-cdk/aws-apigateway/test/test.deployment.ts index f9b927af1bd2a..aaec885005146 100644 --- a/packages/@aws-cdk/aws-apigateway/test/test.deployment.ts +++ b/packages/@aws-cdk/aws-apigateway/test/test.deployment.ts @@ -152,7 +152,7 @@ export = { test.done(); function synthesize() { - stack.node.validateTree(); + stack.node.prepareTree(); return stack.toCloudFormation(); } }, diff --git a/packages/@aws-cdk/aws-apigateway/test/test.method.ts b/packages/@aws-cdk/aws-apigateway/test/test.method.ts index 2f2d94f59d55d..2a377a139f661 100644 --- a/packages/@aws-cdk/aws-apigateway/test/test.method.ts +++ b/packages/@aws-cdk/aws-apigateway/test/test.method.ts @@ -123,7 +123,7 @@ export = { }); // THEN - test.deepEqual(cdk.resolve(method.methodArn), { + test.deepEqual(method.node.resolve(method.methodArn), { "Fn::Join": [ "", [ @@ -157,7 +157,7 @@ export = { }); // THEN - test.deepEqual(cdk.resolve(method.testMethodArn), { + test.deepEqual(method.node.resolve(method.testMethodArn), { "Fn::Join": [ "", [ diff --git a/packages/@aws-cdk/aws-apigateway/test/test.restapi.ts b/packages/@aws-cdk/aws-apigateway/test/test.restapi.ts index a0917ec673025..0810e8938c67b 100644 --- a/packages/@aws-cdk/aws-apigateway/test/test.restapi.ts +++ b/packages/@aws-cdk/aws-apigateway/test/test.restapi.ts @@ -125,7 +125,9 @@ export = { { Ref: "AWS::Region" }, - ".amazonaws.com/", + ".", + { Ref: "AWS::URLSuffix" }, + "/", { Ref: "myapiDeploymentStageprod298F01AF" }, @@ -386,13 +388,13 @@ export = { const exported = api.export(); // THEN - stack.node.validateTree(); + stack.node.prepareTree(); test.deepEqual(stack.toCloudFormation().Outputs.MyRestApiRestApiIdB93C5C2D, { Value: { Ref: 'MyRestApi2D1F47A9' }, Export: { Name: 'MyRestApiRestApiIdB93C5C2D' } }); - test.deepEqual(cdk.resolve(imported.restApiId), 'api-rxt4498f'); - test.deepEqual(cdk.resolve(exported), { restApiId: { 'Fn::ImportValue': 'MyRestApiRestApiIdB93C5C2D' } }); + test.deepEqual(imported.node.resolve(imported.restApiId), 'api-rxt4498f'); + test.deepEqual(imported.node.resolve(exported), { restApiId: { 'Fn::ImportValue': 'MyRestApiRestApiIdB93C5C2D' } }); test.done(); }, @@ -403,22 +405,26 @@ export = { api.root.addMethod('GET'); // THEN - test.deepEqual(cdk.resolve(api.url), { 'Fn::Join': + test.deepEqual(api.node.resolve(api.url), { 'Fn::Join': [ '', [ 'https://', { Ref: 'apiC8550315' }, '.execute-api.', { Ref: 'AWS::Region' }, - '.amazonaws.com/', + ".", + { Ref: "AWS::URLSuffix" }, + "/", { Ref: 'apiDeploymentStageprod896C8101' }, '/' ] ] }); - test.deepEqual(cdk.resolve(api.urlForPath('/foo/bar')), { 'Fn::Join': + test.deepEqual(api.node.resolve(api.urlForPath('/foo/bar')), { 'Fn::Join': [ '', [ 'https://', { Ref: 'apiC8550315' }, '.execute-api.', { Ref: 'AWS::Region' }, - '.amazonaws.com/', + ".", + { Ref: "AWS::URLSuffix" }, + "/", { Ref: 'apiDeploymentStageprod896C8101' }, '/foo/bar' ] ] }); test.done(); @@ -457,7 +463,7 @@ export = { const arn = api.executeApiArn('method', '/path', 'stage'); // THEN - test.deepEqual(cdk.resolve(arn), { 'Fn::Join': + test.deepEqual(api.node.resolve(arn), { 'Fn::Join': [ '', [ 'arn:', { Ref: 'AWS::Partition' }, @@ -490,7 +496,7 @@ export = { const method = api.root.addMethod('ANY'); // THEN - test.deepEqual(cdk.resolve(method.methodArn), { 'Fn::Join': + test.deepEqual(api.node.resolve(method.methodArn), { 'Fn::Join': [ '', [ 'arn:', { Ref: 'AWS::Partition' }, diff --git a/packages/@aws-cdk/aws-cloudformation/lib/pipeline-actions.ts b/packages/@aws-cdk/aws-cloudformation/lib/pipeline-actions.ts index f38df0a6c4d22..1c3ee7be6767f 100644 --- a/packages/@aws-cdk/aws-cloudformation/lib/pipeline-actions.ts +++ b/packages/@aws-cdk/aws-cloudformation/lib/pipeline-actions.ts @@ -203,7 +203,7 @@ export abstract class PipelineCloudFormationDeployAction extends PipelineCloudFo // None evaluates to empty string which is falsey and results in undefined Capabilities: (capabilities && capabilities.toString()) || undefined, RoleArn: new cdk.Token(() => this.role.roleArn), - ParameterOverrides: cdk.CloudFormationJSON.stringify(props.parameterOverrides), + ParameterOverrides: new cdk.Token(() => this.node.stringifyJson(props.parameterOverrides)), TemplateConfiguration: props.templateConfiguration ? props.templateConfiguration.location : undefined, StackName: props.stackName, }); @@ -410,7 +410,7 @@ class SingletonPolicy extends cdk.Construct { this.statementFor({ actions: ['cloudformation:ExecuteChangeSet'], conditions: { StringEquals: { 'cloudformation:ChangeSetName': props.changeSetName } }, - }).addResource(stackArnFromProps(props)); + }).addResource(this.stackArnFromProps(props)); } public grantCreateReplaceChangeSet(props: { stackName: string, changeSetName: string, region?: string }): void { @@ -422,7 +422,7 @@ class SingletonPolicy extends cdk.Construct { 'cloudformation:DescribeStacks', ], conditions: { StringEqualsIfExists: { 'cloudformation:ChangeSetName': props.changeSetName } }, - }).addResource(stackArnFromProps(props)); + }).addResource(this.stackArnFromProps(props)); } public grantCreateUpdateStack(props: { stackName: string, replaceOnFailure?: boolean, region?: string }): void { @@ -438,7 +438,7 @@ class SingletonPolicy extends cdk.Construct { if (props.replaceOnFailure) { actions.push('cloudformation:DeleteStack'); } - this.statementFor({ actions }).addResource(stackArnFromProps(props)); + this.statementFor({ actions }).addResource(this.stackArnFromProps(props)); } public grantDeleteStack(props: { stackName: string, region?: string }): void { @@ -447,7 +447,7 @@ class SingletonPolicy extends cdk.Construct { 'cloudformation:DescribeStack*', 'cloudformation:DeleteStack', ] - }).addResource(stackArnFromProps(props)); + }).addResource(this.stackArnFromProps(props)); } public grantPassRole(role: iam.IRole): void { @@ -485,6 +485,15 @@ class SingletonPolicy extends cdk.Construct { } } } + + private stackArnFromProps(props: { stackName: string, region?: string }): string { + return cdk.Stack.find(this).formatArn({ + region: props.region, + service: 'cloudformation', + resource: 'stack', + resourceName: `${props.stackName}/*` + }); + } } interface StatementTemplate { @@ -492,13 +501,4 @@ interface StatementTemplate { conditions?: StatementCondition; } -type StatementCondition = { [op: string]: { [attribute: string]: string } }; - -function stackArnFromProps(props: { stackName: string, region?: string }): string { - return cdk.ArnUtils.fromComponents({ - region: props.region, - service: 'cloudformation', - resource: 'stack', - resourceName: `${props.stackName}/*` - }); -} +type StatementCondition = { [op: string]: { [attribute: string]: string } }; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cloudformation/test/test.pipeline-actions.ts b/packages/@aws-cdk/aws-cloudformation/test/test.pipeline-actions.ts index 001799de59a13..2e512336c0ab5 100644 --- a/packages/@aws-cdk/aws-cloudformation/test/test.pipeline-actions.ts +++ b/packages/@aws-cdk/aws-cloudformation/test/test.pipeline-actions.ts @@ -11,7 +11,7 @@ export = nodeunit.testCase({ 'works'(test: nodeunit.Test) { const stack = new cdk.Stack(); const pipelineRole = new RoleDouble(stack, 'PipelineRole'); - const stage = new StageDouble({ pipeline: new PipelineDouble({ role: pipelineRole }) }); + const stage = new StageDouble({ pipeline: new PipelineDouble(stack, 'Pipeline', { role: pipelineRole }) }); const artifact = new cpapi.Artifact(stack as any, 'TestArtifact'); const action = new cloudformation.PipelineCreateReplaceChangeSetAction(stack, 'Action', { stage, @@ -23,7 +23,7 @@ export = nodeunit.testCase({ _assertPermissionGranted(test, pipelineRole.statements, 'iam:PassRole', action.role.roleArn); - const stackArn = _stackArn('MyStack'); + const stackArn = _stackArn('MyStack', stack); const changeSetCondition = { StringEqualsIfExists: { 'cloudformation:ChangeSetName': 'MyChangeSet' } }; _assertPermissionGranted(test, pipelineRole.statements, 'cloudformation:DescribeStacks', stackArn, changeSetCondition); _assertPermissionGranted(test, pipelineRole.statements, 'cloudformation:DescribeChangeSet', stackArn, changeSetCondition); @@ -45,7 +45,7 @@ export = nodeunit.testCase({ 'uses a single permission statement if the same ChangeSet name is used'(test: nodeunit.Test) { const stack = new cdk.Stack(); const pipelineRole = new RoleDouble(stack, 'PipelineRole'); - const stage = new StageDouble({ pipeline: new PipelineDouble({ role: pipelineRole }) }); + const stage = new StageDouble({ pipeline: new PipelineDouble(stack, 'Pipeline', { role: pipelineRole }) }); const artifact = new cpapi.Artifact(stack as any, 'TestArtifact'); new cloudformation.PipelineCreateReplaceChangeSetAction(stack, 'ActionA', { stage, @@ -64,7 +64,7 @@ export = nodeunit.testCase({ }); test.deepEqual( - cdk.resolve(pipelineRole.statements), + stack.node.resolve(pipelineRole.statements), [ { Action: 'iam:PassRole', @@ -101,14 +101,14 @@ export = nodeunit.testCase({ 'works'(test: nodeunit.Test) { const stack = new cdk.Stack(); const pipelineRole = new RoleDouble(stack, 'PipelineRole'); - const stage = new StageDouble({ pipeline: new PipelineDouble({ role: pipelineRole }) }); + const stage = new StageDouble({ pipeline: new PipelineDouble(stack, 'Pipeline', { role: pipelineRole }) }); new cloudformation.PipelineExecuteChangeSetAction(stack, 'Action', { stage, changeSetName: 'MyChangeSet', stackName: 'MyStack', }); - const stackArn = _stackArn('MyStack'); + const stackArn = _stackArn('MyStack', stack); _assertPermissionGranted(test, pipelineRole.statements, 'cloudformation:ExecuteChangeSet', stackArn, { StringEquals: { 'cloudformation:ChangeSetName': 'MyChangeSet' } }); @@ -124,7 +124,7 @@ export = nodeunit.testCase({ 'uses a single permission statement if the same ChangeSet name is used'(test: nodeunit.Test) { const stack = new cdk.Stack(); const pipelineRole = new RoleDouble(stack, 'PipelineRole'); - const stage = new StageDouble({ pipeline: new PipelineDouble({ role: pipelineRole }) }); + const stage = new StageDouble({ pipeline: new PipelineDouble(stack, 'Pipeline', { role: pipelineRole }) }); new cloudformation.PipelineExecuteChangeSetAction(stack, 'ActionA', { stage, changeSetName: 'MyChangeSet', @@ -138,7 +138,7 @@ export = nodeunit.testCase({ }); test.deepEqual( - cdk.resolve(pipelineRole.statements), + stack.node.resolve(pipelineRole.statements), [ { Action: 'cloudformation:ExecuteChangeSet', @@ -162,13 +162,13 @@ export = nodeunit.testCase({ const stack = new cdk.Stack(); const pipelineRole = new RoleDouble(stack, 'PipelineRole'); const action = new cloudformation.PipelineCreateUpdateStackAction(stack, 'Action', { - stage: new StageDouble({ pipeline: new PipelineDouble({ role: pipelineRole }) }), + stage: new StageDouble({ pipeline: new PipelineDouble(stack, 'Pipeline', { role: pipelineRole }) }), templatePath: new cpapi.Artifact(stack as any, 'TestArtifact').atPath('some/file'), stackName: 'MyStack', adminPermissions: false, replaceOnFailure: true, }); - const stackArn = _stackArn('MyStack'); + const stackArn = _stackArn('MyStack', stack); _assertPermissionGranted(test, pipelineRole.statements, 'cloudformation:DescribeStack*', stackArn); _assertPermissionGranted(test, pipelineRole.statements, 'cloudformation:CreateStack', stackArn); @@ -184,11 +184,11 @@ export = nodeunit.testCase({ const stack = new cdk.Stack(); const pipelineRole = new RoleDouble(stack, 'PipelineRole'); const action = new cloudformation.PipelineDeleteStackAction(stack, 'Action', { - stage: new StageDouble({ pipeline: new PipelineDouble({ role: pipelineRole }) }), + stage: new StageDouble({ pipeline: new PipelineDouble(stack, 'Pipeline', { role: pipelineRole }) }), adminPermissions: false, stackName: 'MyStack', }); - const stackArn = _stackArn('MyStack'); + const stackArn = _stackArn('MyStack', stack); _assertPermissionGranted(test, pipelineRole.statements, 'cloudformation:DescribeStack*', stackArn); _assertPermissionGranted(test, pipelineRole.statements, 'cloudformation:DeleteStack', stackArn); @@ -213,10 +213,10 @@ function _assertActionMatches(test: nodeunit.Test, category: string, configuration?: { [key: string]: any }) { const configurationStr = configuration - ? `configuration including ${JSON.stringify(cdk.resolve(configuration), null, 2)}` + ? `configuration including ${JSON.stringify(resolve(configuration), null, 2)}` : ''; const actionsStr = JSON.stringify(actions.map(a => - ({ owner: a.owner, provider: a.provider, category: a.category, configuration: cdk.resolve(a.configuration) }) + ({ owner: a.owner, provider: a.provider, category: a.category, configuration: resolve(a.configuration) }) ), null, 2); test.ok(_hasAction(actions, owner, provider, category, configuration), `Expected to find an action with owner ${owner}, provider ${provider}, category ${category}${configurationStr}, but found ${actionsStr}`); @@ -230,7 +230,7 @@ function _hasAction(actions: cpapi.Action[], owner: string, provider: string, ca if (configuration && !action.configuration) { continue; } if (configuration) { for (const key of Object.keys(configuration)) { - if (!_.isEqual(cdk.resolve(action.configuration[key]), cdk.resolve(configuration[key]))) { + if (!_.isEqual(resolve(action.configuration[key]), resolve(configuration[key]))) { continue; } } @@ -242,12 +242,12 @@ function _hasAction(actions: cpapi.Action[], owner: string, provider: string, ca function _assertPermissionGranted(test: nodeunit.Test, statements: iam.PolicyStatement[], action: string, resource: string, conditions?: any) { const conditionStr = conditions - ? ` with condition(s) ${JSON.stringify(cdk.resolve(conditions))}` + ? ` with condition(s) ${JSON.stringify(resolve(conditions))}` : ''; - const resolvedStatements = cdk.resolve(statements); + const resolvedStatements = resolve(statements); const statementsStr = JSON.stringify(resolvedStatements, null, 2); test.ok(_grantsPermission(resolvedStatements, action, resource, conditions), - `Expected to find a statement granting ${action} on ${JSON.stringify(cdk.resolve(resource))}${conditionStr}, found:\n${statementsStr}`); + `Expected to find a statement granting ${action} on ${JSON.stringify(resolve(resource))}${conditionStr}, found:\n${statementsStr}`); } function _grantsPermission(statements: PolicyStatementJson[], action: string, resource: string, conditions?: any) { @@ -261,8 +261,8 @@ function _grantsPermission(statements: PolicyStatementJson[], action: string, re } function _isOrContains(entity: string | string[], value: string): boolean { - const resolvedValue = cdk.resolve(value); - const resolvedEntity = cdk.resolve(entity); + const resolvedValue = resolve(value); + const resolvedEntity = resolve(entity); if (_.isEqual(resolvedEntity, resolvedValue)) { return true; } if (!Array.isArray(resolvedEntity)) { return false; } for (const tested of entity) { @@ -271,26 +271,23 @@ function _isOrContains(entity: string | string[], value: string): boolean { return false; } -function _stackArn(stackName: string): string { - return cdk.ArnUtils.fromComponents({ +function _stackArn(stackName: string, scope: cdk.IConstruct): string { + return cdk.Stack.find(scope).formatArn({ service: 'cloudformation', resource: 'stack', resourceName: `${stackName}/*`, }); } -class PipelineDouble implements cpapi.IPipeline { +class PipelineDouble extends cdk.Construct implements cpapi.IPipeline { public readonly pipelineName: string; public readonly pipelineArn: string; public readonly role: iam.Role; - public get node(): cdk.ConstructNode { - throw new Error('this is not a real construct'); - } - - constructor({ pipelineName, role }: { pipelineName?: string, role: iam.Role }) { + constructor(scope: cdk.Construct, id: string, { pipelineName, role }: { pipelineName?: string, role: iam.Role }) { + super(scope, id); this.pipelineName = pipelineName || 'TestPipeline'; - this.pipelineArn = cdk.ArnUtils.fromComponents({ service: 'codepipeline', resource: 'pipeline', resourceName: this.pipelineName }); + this.pipelineArn = cdk.Stack.find(this).formatArn({ service: 'codepipeline', resource: 'pipeline', resourceName: this.pipelineName }); this.role = role; } @@ -352,3 +349,7 @@ class RoleDouble extends iam.Role { this.statements.push(statement); } } + +function resolve(x: any): any { + return new cdk.Stack().node.resolve(x); +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cloudtrail/lib/index.ts b/packages/@aws-cdk/aws-cloudtrail/lib/index.ts index fc8f08883cd13..4b0da728f7e57 100644 --- a/packages/@aws-cdk/aws-cloudtrail/lib/index.ts +++ b/packages/@aws-cdk/aws-cloudtrail/lib/index.ts @@ -132,13 +132,15 @@ export class CloudTrail extends cdk.Construct { const s3bucket = new s3.Bucket(this, 'S3', {encryption: s3.BucketEncryption.Unencrypted}); const cloudTrailPrincipal = "cloudtrail.amazonaws.com"; + const stack = cdk.Stack.find(this); + s3bucket.addToResourcePolicy(new iam.PolicyStatement() .addResource(s3bucket.bucketArn) .addActions('s3:GetBucketAcl') .addServicePrincipal(cloudTrailPrincipal)); s3bucket.addToResourcePolicy(new iam.PolicyStatement() - .addResource(s3bucket.arnForObjects(`AWSLogs/${new cdk.AwsAccountId()}/*`)) + .addResource(s3bucket.arnForObjects(`AWSLogs/${stack.accountId}/*`)) .addActions("s3:PutObject") .addServicePrincipal(cloudTrailPrincipal) .setCondition("StringEquals", {'s3:x-amz-acl': "bucket-owner-full-control"})); diff --git a/packages/@aws-cdk/aws-cloudtrail/test/test.cloudtrail.ts b/packages/@aws-cdk/aws-cloudtrail/test/test.cloudtrail.ts index 59783aee5764f..ae6066ec43ff4 100644 --- a/packages/@aws-cdk/aws-cloudtrail/test/test.cloudtrail.ts +++ b/packages/@aws-cdk/aws-cloudtrail/test/test.cloudtrail.ts @@ -40,11 +40,7 @@ const ExpectedBucketPolicyProperties = { "Arn" ] }, - "/AWSLogs/", - { - Ref: "AWS::AccountId" - }, - "/*" + "/AWSLogs/123456789012/*", ] ] } diff --git a/packages/@aws-cdk/aws-cloudwatch/lib/dashboard.ts b/packages/@aws-cdk/aws-cloudwatch/lib/dashboard.ts index db5681eb0d693..3974af3f5caf2 100644 --- a/packages/@aws-cdk/aws-cloudwatch/lib/dashboard.ts +++ b/packages/@aws-cdk/aws-cloudwatch/lib/dashboard.ts @@ -1,4 +1,4 @@ -import { CloudFormationJSON, Construct, Stack, Token } from "@aws-cdk/cdk"; +import { Construct, Stack, Token } from "@aws-cdk/cdk"; import { CfnDashboard } from './cloudwatch.generated'; import { Column, Row } from "./layout"; import { IWidget } from "./widget"; @@ -33,7 +33,7 @@ export class Dashboard extends Construct { dashboardBody: new Token(() => { const column = new Column(...this.rows); column.position(0, 0); - return CloudFormationJSON.stringify({ widgets: column.toJson() }); + return this.node.stringifyJson({ widgets: column.toJson() }); }).toString() }); } diff --git a/packages/@aws-cdk/aws-cloudwatch/lib/graph.ts b/packages/@aws-cdk/aws-cloudwatch/lib/graph.ts index 27dc9c4cb44b4..55b4808b25bd1 100644 --- a/packages/@aws-cdk/aws-cloudwatch/lib/graph.ts +++ b/packages/@aws-cdk/aws-cloudwatch/lib/graph.ts @@ -1,4 +1,4 @@ -import { AwsRegion } from "@aws-cdk/cdk"; +import cdk = require('@aws-cdk/cdk'); import { Alarm } from "./alarm"; import { Metric } from "./metric"; import { parseStatistic } from './util.statistic'; @@ -73,7 +73,7 @@ export class AlarmWidget extends ConcreteWidget { properties: { view: 'timeSeries', title: this.props.title, - region: this.props.region || new AwsRegion(), + region: this.props.region || new cdk.Aws().region, annotations: { alarms: [this.props.alarm.alarmArn] }, @@ -150,7 +150,7 @@ export class GraphWidget extends ConcreteWidget { properties: { view: 'timeSeries', title: this.props.title, - region: this.props.region || new AwsRegion(), + region: this.props.region || new cdk.Aws().region, metrics: (this.props.left || []).map(m => metricJson(m, 'left')).concat( (this.props.right || []).map(m => metricJson(m, 'right'))), annotations: { @@ -197,7 +197,7 @@ export class SingleValueWidget extends ConcreteWidget { properties: { view: 'singleValue', title: this.props.title, - region: this.props.region || new AwsRegion(), + region: this.props.region || new cdk.Aws().region, metrics: this.props.metrics.map(m => metricJson(m, 'left')) } }]; diff --git a/packages/@aws-cdk/aws-cloudwatch/test/test.graphs.ts b/packages/@aws-cdk/aws-cloudwatch/test/test.graphs.ts index 3b70ad7ca60df..8a3073ffd0750 100644 --- a/packages/@aws-cdk/aws-cloudwatch/test/test.graphs.ts +++ b/packages/@aws-cdk/aws-cloudwatch/test/test.graphs.ts @@ -1,10 +1,11 @@ -import { resolve, Stack } from '@aws-cdk/cdk'; +import { Stack } from '@aws-cdk/cdk'; import { Test } from 'nodeunit'; import { AlarmWidget, GraphWidget, Metric, Shading, SingleValueWidget } from '../lib'; export = { 'add metrics to graphs on either axis'(test: Test) { // WHEN + const stack = new Stack(); const widget = new GraphWidget({ title: 'My fancy graph', left: [ @@ -16,7 +17,7 @@ export = { }); // THEN - test.deepEqual(resolve(widget.toJson()), [{ + test.deepEqual(stack.node.resolve(widget.toJson()), [{ type: 'metric', width: 6, height: 6, @@ -38,12 +39,13 @@ export = { 'label and color are respected in constructor'(test: Test) { // WHEN + const stack = new Stack(); const widget = new GraphWidget({ left: [new Metric({ namespace: 'CDK', metricName: 'Test', label: 'MyMetric', color: '000000' }) ], }); // THEN - test.deepEqual(resolve(widget.toJson()), [{ + test.deepEqual(stack.node.resolve(widget.toJson()), [{ type: 'metric', width: 6, height: 6, @@ -63,6 +65,7 @@ export = { 'singlevalue widget'(test: Test) { // GIVEN + const stack = new Stack(); const metric = new Metric({ namespace: 'CDK', metricName: 'Test' }); // WHEN @@ -71,7 +74,7 @@ export = { }); // THEN - test.deepEqual(resolve(widget.toJson()), [{ + test.deepEqual(stack.node.resolve(widget.toJson()), [{ type: 'metric', width: 6, height: 3, @@ -102,7 +105,7 @@ export = { }); // THEN - test.deepEqual(resolve(widget.toJson()), [{ + test.deepEqual(stack.node.resolve(widget.toJson()), [{ type: 'metric', width: 6, height: 6, @@ -121,6 +124,7 @@ export = { 'add annotations to graph'(test: Test) { // WHEN + const stack = new Stack(); const widget = new GraphWidget({ title: 'My fancy graph', left: [ @@ -135,7 +139,7 @@ export = { }); // THEN - test.deepEqual(resolve(widget.toJson()), [{ + test.deepEqual(stack.node.resolve(widget.toJson()), [{ type: 'metric', width: 6, height: 6, @@ -178,7 +182,7 @@ export = { }); // THEN - test.deepEqual(resolve(widget.toJson()), [{ + test.deepEqual(stack.node.resolve(widget.toJson()), [{ type: 'metric', width: 6, height: 6, diff --git a/packages/@aws-cdk/aws-codebuild/lib/project.ts b/packages/@aws-cdk/aws-codebuild/lib/project.ts index 42defa04578b7..2ea98fa5e0c15 100644 --- a/packages/@aws-cdk/aws-codebuild/lib/project.ts +++ b/packages/@aws-cdk/aws-codebuild/lib/project.ts @@ -436,7 +436,7 @@ class ImportedProject extends ProjectBase { constructor(scope: cdk.Construct, id: string, private readonly props: ProjectImportProps) { super(scope, id); - this.projectArn = cdk.ArnUtils.fromComponents({ + this.projectArn = cdk.Stack.find(this).formatArn({ service: 'codebuild', resource: 'project', resourceName: props.projectName, @@ -705,24 +705,6 @@ export class Project extends ProjectBase { this.addToRolePolicy(this.createLoggingPermission()); } - /** - * @override - */ - public validate(): string[] { - const ret = new Array(); - if (this.source.type === SourceType.CodePipeline) { - if (this._secondarySources.length > 0) { - ret.push('A Project with a CodePipeline Source cannot have secondary sources. ' + - "Use the CodeBuild Pipeline Actions' `additionalInputArtifacts` property instead"); - } - if (this._secondaryArtifacts.length > 0) { - ret.push('A Project with a CodePipeline Source cannot have secondary artifacts. ' + - "Use the CodeBuild Pipeline Actions' `additionalOutputArtifactNames` property instead"); - } - } - return ret; - } - /** * Export this Project. Allows referencing this Project in a different CDK Stack. */ @@ -770,8 +752,26 @@ export class Project extends ProjectBase { this._secondaryArtifacts.push(secondaryArtifact); } + /** + * @override + */ + protected validate(): string[] { + const ret = new Array(); + if (this.source.type === SourceType.CodePipeline) { + if (this._secondarySources.length > 0) { + ret.push('A Project with a CodePipeline Source cannot have secondary sources. ' + + "Use the CodeBuild Pipeline Actions' `additionalInputArtifacts` property instead"); + } + if (this._secondaryArtifacts.length > 0) { + ret.push('A Project with a CodePipeline Source cannot have secondary artifacts. ' + + "Use the CodeBuild Pipeline Actions' `additionalOutputArtifactNames` property instead"); + } + } + return ret; + } + private createLoggingPermission() { - const logGroupArn = cdk.ArnUtils.fromComponents({ + const logGroupArn = cdk.Stack.find(this).formatArn({ service: 'logs', resource: 'log-group', sep: ':', diff --git a/packages/@aws-cdk/aws-codebuild/test/test.codebuild.ts b/packages/@aws-cdk/aws-codebuild/test/test.codebuild.ts index 187b72cbcacb7..f4a58d9d7d40e 100644 --- a/packages/@aws-cdk/aws-codebuild/test/test.codebuild.ts +++ b/packages/@aws-cdk/aws-codebuild/test/test.codebuild.ts @@ -857,7 +857,7 @@ export = { environment: { environmentVariables: { FOO: { value: '1234' }, - BAR: { value: `111${new cdk.CloudFormationToken({ twotwotwo: '222' })}`, type: codebuild.BuildEnvironmentVariableType.ParameterStore } + BAR: { value: `111${new cdk.Token({ twotwotwo: '222' })}`, type: codebuild.BuildEnvironmentVariableType.ParameterStore } } }, environmentVariables: { diff --git a/packages/@aws-cdk/aws-codecommit/lib/repository.ts b/packages/@aws-cdk/aws-codecommit/lib/repository.ts index 086990af2bd25..80bb733212c59 100644 --- a/packages/@aws-cdk/aws-codecommit/lib/repository.ts +++ b/packages/@aws-cdk/aws-codecommit/lib/repository.ts @@ -244,7 +244,7 @@ class ImportedRepository extends RepositoryBase { constructor(scope: cdk.Construct, id: string, private readonly props: RepositoryImportProps) { super(scope, id); - this.repositoryArn = cdk.ArnUtils.fromComponents({ + this.repositoryArn = cdk.Stack.find(this).formatArn({ service: 'codecommit', resource: props.repositoryName, }); @@ -264,7 +264,8 @@ class ImportedRepository extends RepositoryBase { } private repositoryCloneUrl(protocol: 'https' | 'ssh'): string { - return `${protocol}://git-codecommit.${new cdk.AwsRegion()}.${new cdk.AwsURLSuffix()}/v1/repos/${this.repositoryName}`; + const stack = cdk.Stack.find(this); + return `${protocol}://git-codecommit.${stack.region}.${stack.urlSuffix}/v1/repos/${this.repositoryName}`; } } diff --git a/packages/@aws-cdk/aws-codedeploy/lib/application.ts b/packages/@aws-cdk/aws-codedeploy/lib/application.ts index 3c8bfeee3e806..e2aeb6ce4087b 100644 --- a/packages/@aws-cdk/aws-codedeploy/lib/application.ts +++ b/packages/@aws-cdk/aws-codedeploy/lib/application.ts @@ -41,7 +41,7 @@ class ImportedServerApplication extends cdk.Construct implements IServerApplicat super(scope, id); this.applicationName = props.applicationName; - this.applicationArn = applicationName2Arn(this.applicationName); + this.applicationArn = applicationNameToArn(this.applicationName, this); } public export() { @@ -90,7 +90,7 @@ export class ServerApplication extends cdk.Construct implements IServerApplicati }); this.applicationName = resource.ref; - this.applicationArn = applicationName2Arn(this.applicationName); + this.applicationArn = applicationNameToArn(this.applicationName, this); } public export(): ServerApplicationImportProps { @@ -100,8 +100,8 @@ export class ServerApplication extends cdk.Construct implements IServerApplicati } } -function applicationName2Arn(applicationName: string): string { - return cdk.ArnUtils.fromComponents({ +function applicationNameToArn(applicationName: string, scope: cdk.IConstruct): string { + return cdk.Stack.find(scope).formatArn({ service: 'codedeploy', resource: 'application', resourceName: applicationName, diff --git a/packages/@aws-cdk/aws-codedeploy/lib/deployment-config.ts b/packages/@aws-cdk/aws-codedeploy/lib/deployment-config.ts index 6783ecb4114f8..dfca704574f25 100644 --- a/packages/@aws-cdk/aws-codedeploy/lib/deployment-config.ts +++ b/packages/@aws-cdk/aws-codedeploy/lib/deployment-config.ts @@ -10,7 +10,7 @@ import { CfnDeploymentConfig } from './codedeploy.generated'; */ export interface IServerDeploymentConfig { readonly deploymentConfigName: string; - readonly deploymentConfigArn: string; + deploymentConfigArn(scope: cdk.IConstruct): string; export(): ServerDeploymentConfigImportProps; } @@ -30,13 +30,15 @@ export interface ServerDeploymentConfigImportProps { class ImportedServerDeploymentConfig extends cdk.Construct implements IServerDeploymentConfig { public readonly deploymentConfigName: string; - public readonly deploymentConfigArn: string; constructor(scope: cdk.Construct, id: string, private readonly props: ServerDeploymentConfigImportProps) { super(scope, id); this.deploymentConfigName = props.deploymentConfigName; - this.deploymentConfigArn = arnForDeploymentConfigName(this.deploymentConfigName); + } + + public deploymentConfigArn(scope: cdk.IConstruct): string { + return arnForDeploymentConfigName(this.deploymentConfigName, scope); } public export() { @@ -46,11 +48,13 @@ class ImportedServerDeploymentConfig extends cdk.Construct implements IServerDep class DefaultServerDeploymentConfig implements IServerDeploymentConfig { public readonly deploymentConfigName: string; - public readonly deploymentConfigArn: string; constructor(deploymentConfigName: string) { this.deploymentConfigName = deploymentConfigName; - this.deploymentConfigArn = arnForDeploymentConfigName(this.deploymentConfigName); + } + + public deploymentConfigArn(scope: cdk.IConstruct): string { + return arnForDeploymentConfigName(this.deploymentConfigName, scope); } public export(): ServerDeploymentConfigImportProps { @@ -110,7 +114,6 @@ export class ServerDeploymentConfig extends cdk.Construct implements IServerDepl } public readonly deploymentConfigName: string; - public readonly deploymentConfigArn: string; constructor(scope: cdk.Construct, id: string, props: ServerDeploymentConfigProps) { super(scope, id); @@ -121,7 +124,10 @@ export class ServerDeploymentConfig extends cdk.Construct implements IServerDepl }); this.deploymentConfigName = resource.ref.toString(); - this.deploymentConfigArn = arnForDeploymentConfigName(this.deploymentConfigName); + } + + public deploymentConfigArn(scope: cdk.IConstruct): string { + return arnForDeploymentConfigName(this.deploymentConfigName, scope); } public export(): ServerDeploymentConfigImportProps { @@ -150,8 +156,8 @@ export class ServerDeploymentConfig extends cdk.Construct implements IServerDepl } } -function arnForDeploymentConfigName(name: string): string { - return cdk.ArnUtils.fromComponents({ +function arnForDeploymentConfigName(name: string, scope: cdk.IConstruct): string { + return cdk.Stack.find(scope).formatArn({ service: 'codedeploy', resource: 'deploymentconfig', resourceName: name, diff --git a/packages/@aws-cdk/aws-codedeploy/lib/deployment-group.ts b/packages/@aws-cdk/aws-codedeploy/lib/deployment-group.ts index e24c68cacaebb..083cb853653ad 100644 --- a/packages/@aws-cdk/aws-codedeploy/lib/deployment-group.ts +++ b/packages/@aws-cdk/aws-codedeploy/lib/deployment-group.ts @@ -104,8 +104,8 @@ class ImportedServerDeploymentGroup extends ServerDeploymentGroupBase { this.application = props.application; this.deploymentGroupName = props.deploymentGroupName; - this.deploymentGroupArn = deploymentGroupName2Arn(props.application.applicationName, - props.deploymentGroupName); + this.deploymentGroupArn = deploymentGroupNameToArn(props.application.applicationName, + props.deploymentGroupName, this); } public export() { @@ -310,9 +310,9 @@ export class ServerDeploymentGroup extends ServerDeploymentGroupBase { this._autoScalingGroups = props.autoScalingGroups || []; this.installAgent = props.installAgent === undefined ? true : props.installAgent; - const region = new cdk.AwsRegion().toString(); + const stack = cdk.Stack.find(this); this.codeDeployBucket = s3.Bucket.import(this, 'CodeDeployBucket', { - bucketName: `aws-codedeploy-${region}`, + bucketName: `aws-codedeploy-${stack.region}`, }); for (const asg of this._autoScalingGroups) { this.addCodeDeployAgentInstallUserData(asg); @@ -343,8 +343,8 @@ export class ServerDeploymentGroup extends ServerDeploymentGroupBase { }); this.deploymentGroupName = resource.deploymentGroupName; - this.deploymentGroupArn = deploymentGroupName2Arn(this.application.applicationName, - this.deploymentGroupName); + this.deploymentGroupArn = deploymentGroupNameToArn(this.application.applicationName, + this.deploymentGroupName, this); } public export(): ServerDeploymentGroupImportProps { @@ -387,7 +387,7 @@ export class ServerDeploymentGroup extends ServerDeploymentGroupBase { this.codeDeployBucket.grantRead(asg.role, 'latest/*'); - const region = (new cdk.AwsRegion()).toString(); + const stack = cdk.Stack.find(this); switch (asg.osType) { case ec2.OperatingSystemType.Linux: asg.addUserData( @@ -405,7 +405,7 @@ export class ServerDeploymentGroup extends ServerDeploymentGroupBase { '$PKG_CMD install -y awscli', 'TMP_DIR=`mktemp -d`', 'cd $TMP_DIR', - `aws s3 cp s3://aws-codedeploy-${region}/latest/install . --region ${region}`, + `aws s3 cp s3://aws-codedeploy-${stack.region}/latest/install . --region ${stack.region}`, 'chmod +x ./install', './install auto', 'rm -fr $TMP_DIR', @@ -414,7 +414,7 @@ export class ServerDeploymentGroup extends ServerDeploymentGroupBase { case ec2.OperatingSystemType.Windows: asg.addUserData( 'Set-Variable -Name TEMPDIR -Value (New-TemporaryFile).DirectoryName', - `aws s3 cp s3://aws-codedeploy-${region}/latest/codedeploy-agent.msi $TEMPDIR\\codedeploy-agent.msi`, + `aws s3 cp s3://aws-codedeploy-${stack.region}/latest/codedeploy-agent.msi $TEMPDIR\\codedeploy-agent.msi`, '$TEMPDIR\\codedeploy-agent.msi /quiet /l c:\\temp\\host-agent-install-log.txt', ); break; @@ -560,8 +560,8 @@ export class ServerDeploymentGroup extends ServerDeploymentGroupBase { } } -function deploymentGroupName2Arn(applicationName: string, deploymentGroupName: string): string { - return cdk.ArnUtils.fromComponents({ +function deploymentGroupNameToArn(applicationName: string, deploymentGroupName: string, scope: cdk.IConstruct): string { + return cdk.Stack.find(scope).formatArn({ service: 'codedeploy', resource: 'deploymentgroup', resourceName: `${applicationName}/${deploymentGroupName}`, diff --git a/packages/@aws-cdk/aws-codedeploy/lib/pipeline-action.ts b/packages/@aws-cdk/aws-codedeploy/lib/pipeline-action.ts index 8461eb85a6734..27bd3485d3ba0 100644 --- a/packages/@aws-cdk/aws-codedeploy/lib/pipeline-action.ts +++ b/packages/@aws-cdk/aws-codedeploy/lib/pipeline-action.ts @@ -60,7 +60,7 @@ export class PipelineDeployAction extends codepipeline.DeployAction { )); props.stage.pipeline.role.addToPolicy(new iam.PolicyStatement() - .addResource(props.deploymentGroup.deploymentConfig.deploymentConfigArn) + .addResource(props.deploymentGroup.deploymentConfig.deploymentConfigArn(this)) .addActions( 'codedeploy:GetDeploymentConfig', )); diff --git a/packages/@aws-cdk/aws-codepipeline-api/lib/action.ts b/packages/@aws-cdk/aws-codepipeline-api/lib/action.ts index 5d6fdacf9fba8..0e1bbf2344590 100644 --- a/packages/@aws-cdk/aws-codepipeline-api/lib/action.ts +++ b/packages/@aws-cdk/aws-codepipeline-api/lib/action.ts @@ -239,14 +239,6 @@ export abstract class Action extends cdk.Construct { this.stage._internal._attachAction(this); } - public validate(): string[] { - return validation.validateArtifactBounds('input', this._actionInputArtifacts, this.artifactBounds.minInputs, - this.artifactBounds.maxInputs, this.category, this.provider) - .concat(validation.validateArtifactBounds('output', this._actionOutputArtifacts, this.artifactBounds.minOutputs, - this.artifactBounds.maxOutputs, this.category, this.provider) - ); - } - public onStateChange(name: string, target?: events.IEventRuleTarget, options?: events.EventRuleProps) { const rule = new events.EventRule(this, name, options); rule.addTarget(target); @@ -270,6 +262,14 @@ export abstract class Action extends cdk.Construct { return this._actionOutputArtifacts.slice(); } + protected validate(): string[] { + return validation.validateArtifactBounds('input', this._actionInputArtifacts, this.artifactBounds.minInputs, + this.artifactBounds.maxInputs, this.category, this.provider) + .concat(validation.validateArtifactBounds('output', this._actionOutputArtifacts, this.artifactBounds.minOutputs, + this.artifactBounds.maxOutputs, this.category, this.provider) + ); + } + protected addOutputArtifact(name: string = this.stage._internal._generateOutputArtifactName(this)): Artifact { const artifact = new Artifact(this, name); this._actionOutputArtifacts.push(artifact); diff --git a/packages/@aws-cdk/aws-codepipeline-api/lib/artifact.ts b/packages/@aws-cdk/aws-codepipeline-api/lib/artifact.ts index 4269b38b0fa6d..b236622d33f62 100644 --- a/packages/@aws-cdk/aws-codepipeline-api/lib/artifact.ts +++ b/packages/@aws-cdk/aws-codepipeline-api/lib/artifact.ts @@ -1,4 +1,4 @@ -import { CloudFormationToken, Construct } from "@aws-cdk/cdk"; +import { Construct, Token } from "@aws-cdk/cdk"; import { Action } from "./action"; /** @@ -72,9 +72,9 @@ export class ArtifactPath { } function artifactAttribute(artifact: Artifact, attributeName: string) { - return new CloudFormationToken(() => ({ 'Fn::GetArtifactAtt': [artifact.name, attributeName] })).toString(); + return new Token(() => ({ 'Fn::GetArtifactAtt': [artifact.name, attributeName] })).toString(); } function artifactGetParam(artifact: Artifact, jsonFile: string, keyName: string) { - return new CloudFormationToken(() => ({ 'Fn::GetParam': [artifact.name, jsonFile, keyName] })).toString(); + return new Token(() => ({ 'Fn::GetParam': [artifact.name, jsonFile, keyName] })).toString(); } diff --git a/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts b/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts index 81437ec3eeb0a..7c29de60f231a 100644 --- a/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts +++ b/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts @@ -127,7 +127,7 @@ export class Pipeline extends cdk.Construct implements cpapi.IPipeline { this.artifactStores = {}; // Does not expose a Fn::GetAtt for the ARN so we'll have to make it ourselves - this.pipelineArn = cdk.ArnUtils.fromComponents({ + this.pipelineArn = cdk.Stack.find(this).formatArn({ service: 'codepipeline', resource: this.pipelineName }); @@ -212,21 +212,6 @@ export class Pipeline extends cdk.Construct implements cpapi.IPipeline { return rule; } - /** - * Validate the pipeline structure - * - * Validation happens according to the rules documented at - * - * https://docs.aws.amazon.com/codepipeline/latest/userguide/reference-pipeline-structure.html#pipeline-requirements - * @override - */ - public validate(): string[] { - return [ - ...this.validateHasStages(), - ...this.validateSourceActionLocations() - ]; - } - /** * Get the number of Stages in this Pipeline. */ @@ -254,6 +239,21 @@ export class Pipeline extends cdk.Construct implements cpapi.IPipeline { return ret; } + /** + * Validate the pipeline structure + * + * Validation happens according to the rules documented at + * + * https://docs.aws.amazon.com/codepipeline/latest/userguide/reference-pipeline-structure.html#pipeline-requirements + * @override + */ + protected validate(): string[] { + return [ + ...this.validateHasStages(), + ...this.validateSourceActionLocations() + ]; + } + /** * Adds a Stage to this Pipeline. * This is an internal operation - diff --git a/packages/@aws-cdk/aws-codepipeline/lib/stage.ts b/packages/@aws-cdk/aws-codepipeline/lib/stage.ts index ee51f5c5fa33b..1a1eb46e6e4ea 100644 --- a/packages/@aws-cdk/aws-codepipeline/lib/stage.ts +++ b/packages/@aws-cdk/aws-codepipeline/lib/stage.ts @@ -105,10 +105,6 @@ export class Stage extends cdk.Construct implements cpapi.IStage, cpapi.IInterna return this._actions.slice(); } - public validate(): string[] { - return this.validateHasActions(); - } - public render(): CfnPipeline.StageDeclarationProperty { return { name: this.node.id, @@ -149,6 +145,10 @@ export class Stage extends cdk.Construct implements cpapi.IStage, cpapi.IInterna return (this.pipeline as any)._findInputArtifact(this, action); } + protected validate(): string[] { + return this.validateHasActions(); + } + private renderAction(action: cpapi.Action): CfnPipeline.ActionDeclarationProperty { return { name: action.node.id, diff --git a/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-cfn-cross-region.expected.json b/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-cfn-cross-region.expected.json index f2f25bf9fcd04..99f7e7886620d 100644 --- a/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-cfn-cross-region.expected.json +++ b/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-cfn-cross-region.expected.json @@ -122,11 +122,7 @@ { "Ref": "AWS::Partition" }, - ":cloudformation:us-west-2:", - { - "Ref": "AWS::AccountId" - }, - ":stack/aws-cdk-codepipeline-cross-region-deploy-stack/*" + ":cloudformation:us-west-2:12345678:stack/aws-cdk-codepipeline-cross-region-deploy-stack/*" ] ] } diff --git a/packages/@aws-cdk/aws-codepipeline/test/test.general-validation.ts b/packages/@aws-cdk/aws-codepipeline/test/test.general-validation.ts index f10c842b09e7e..a152b39a76579 100644 --- a/packages/@aws-cdk/aws-codepipeline/test/test.general-validation.ts +++ b/packages/@aws-cdk/aws-codepipeline/test/test.general-validation.ts @@ -37,7 +37,7 @@ export = { 'should fail if Stage has no Actions'(test: Test) { const stage = stageForTesting(); - test.deepEqual(stage.validate().length, 1); + test.deepEqual(stage.node.validateTree().length, 1); test.done(); } @@ -48,7 +48,7 @@ export = { const stack = new cdk.Stack(); const pipeline = new Pipeline(stack, 'Pipeline'); - test.deepEqual(pipeline.validate().length, 1); + test.deepEqual(pipeline.node.validateTree().length, 1); test.done(); }, @@ -73,7 +73,7 @@ export = { bucketKey: 'key', }); - test.deepEqual(pipeline.validate().length, 1); + test.deepEqual(pipeline.node.validateTree().length, 1); test.done(); } diff --git a/packages/@aws-cdk/aws-codepipeline/test/test.pipeline.ts b/packages/@aws-cdk/aws-codepipeline/test/test.pipeline.ts index 5a29a47764a2f..de1b69b01c5be 100644 --- a/packages/@aws-cdk/aws-codepipeline/test/test.pipeline.ts +++ b/packages/@aws-cdk/aws-codepipeline/test/test.pipeline.ts @@ -38,7 +38,7 @@ export = { }); test.notDeepEqual(stack.toCloudFormation(), {}); - test.deepEqual([], pipeline.validate()); + test.deepEqual([], pipeline.node.validateTree()); test.done(); }, @@ -127,7 +127,7 @@ export = { ] })); - test.deepEqual([], p.validate()); + test.deepEqual([], p.node.validateTree()); test.done(); }, @@ -211,7 +211,7 @@ export = { ] })); - test.deepEqual([], pipeline.validate()); + test.deepEqual([], pipeline.node.validateTree()); test.done(); }, diff --git a/packages/@aws-cdk/aws-dynamodb/lib/table.ts b/packages/@aws-cdk/aws-dynamodb/lib/table.ts index 7f3d8e1685344..cd8ebef7be464 100644 --- a/packages/@aws-cdk/aws-dynamodb/lib/table.ts +++ b/packages/@aws-cdk/aws-dynamodb/lib/table.ts @@ -474,7 +474,7 @@ export class Table extends Construct { * * @returns an array of validation error message */ - public validate(): string[] { + protected validate(): string[] { const errors = new Array(); if (!this.tablePartitionKey) { @@ -614,7 +614,7 @@ export class Table extends Construct { private makeScalingRole(): iam.IRole { // Use a Service Linked Role. return iam.Role.import(this, 'ScalingRole', { - roleArn: cdk.ArnUtils.fromComponents({ + roleArn: cdk.Stack.find(this).formatArn({ // https://docs.aws.amazon.com/autoscaling/application/userguide/application-auto-scaling-service-linked-roles.html service: 'iam', resource: 'role/aws-service-role/dynamodb.application-autoscaling.amazonaws.com', diff --git a/packages/@aws-cdk/aws-dynamodb/test/test.dynamodb.ts b/packages/@aws-cdk/aws-dynamodb/test/test.dynamodb.ts index b5e5ab4c32e4b..c19eaeaf6521b 100644 --- a/packages/@aws-cdk/aws-dynamodb/test/test.dynamodb.ts +++ b/packages/@aws-cdk/aws-dynamodb/test/test.dynamodb.ts @@ -1175,10 +1175,10 @@ export = { sortKey: LSI_SORT_KEY }); - const errors = table.validate(); + const errors = table.node.validateTree(); test.strictEqual(1, errors.length); - test.strictEqual('a sort key of the table must be specified to add local secondary indexes', errors[0]); + test.strictEqual('a sort key of the table must be specified to add local secondary indexes', errors[0].message); test.done(); }, diff --git a/packages/@aws-cdk/aws-ec2/lib/vpc-ref.ts b/packages/@aws-cdk/aws-ec2/lib/vpc-ref.ts index f92f0f6447aea..e7c696de6bd26 100644 --- a/packages/@aws-cdk/aws-ec2/lib/vpc-ref.ts +++ b/packages/@aws-cdk/aws-ec2/lib/vpc-ref.ts @@ -1,4 +1,4 @@ -import { Construct, IConstruct, IDependable } from "@aws-cdk/cdk"; +import { Construct, IConstruct, IDependable, Stack } from "@aws-cdk/cdk"; import { subnetName } from './util'; export interface IVpcSubnet extends IConstruct, IDependable { @@ -44,6 +44,11 @@ export interface IVpcNetwork extends IConstruct, IDependable { */ readonly availabilityZones: string[]; + /** + * Region where this VPC is located + */ + readonly vpcRegion: string; + /** * Take a dependency on internet connectivity having been added to this VPC * @@ -240,6 +245,14 @@ export abstract class VpcNetworkBase extends Construct implements IVpcNetwork { public internetDependency(): IDependable { return new DependencyList(this.internetDependencies); } + + /** + * The region where this VPC is defined + */ + public get vpcRegion(): string { + return Stack.find(this).region; + } + } /** diff --git a/packages/@aws-cdk/aws-ec2/lib/vpc.ts b/packages/@aws-cdk/aws-ec2/lib/vpc.ts index 6c25430b6cca5..875d22ba24483 100644 --- a/packages/@aws-cdk/aws-ec2/lib/vpc.ts +++ b/packages/@aws-cdk/aws-ec2/lib/vpc.ts @@ -5,6 +5,7 @@ import { NetworkBuilder } from './network-util'; import { DEFAULT_SUBNET_NAME, ExportSubnetGroup, ImportSubnetGroup, subnetId } from './util'; import { VpcNetworkProvider, VpcNetworkProviderProps } from './vpc-network-provider'; import { IVpcNetwork, IVpcSubnet, SubnetType, VpcNetworkBase, VpcNetworkImportProps, VpcPlacementStrategy, VpcSubnetImportProps } from './vpc-ref'; + /** * Name tag constant */ diff --git a/packages/@aws-cdk/aws-ec2/test/test.vpc.ts b/packages/@aws-cdk/aws-ec2/test/test.vpc.ts index dcc3ecd5d3827..afa4c91f45177 100644 --- a/packages/@aws-cdk/aws-ec2/test/test.vpc.ts +++ b/packages/@aws-cdk/aws-ec2/test/test.vpc.ts @@ -1,5 +1,5 @@ import { countResources, expect, haveResource, haveResourceLike, isSuperObject } from '@aws-cdk/assert'; -import { AvailabilityZoneProvider, Construct, resolve, Stack, Tags } from '@aws-cdk/cdk'; +import { AvailabilityZoneProvider, Construct, Stack, Tags } from '@aws-cdk/cdk'; import { Test } from 'nodeunit'; import { DefaultInstanceTenancy, IVpcNetwork, SubnetType, VpcNetwork } from '../lib'; @@ -10,7 +10,7 @@ export = { "vpc.vpcId returns a token to the VPC ID"(test: Test) { const stack = getTestStack(); const vpc = new VpcNetwork(stack, 'TheVPC'); - test.deepEqual(resolve(vpc.vpcId), {Ref: 'TheVPC92636AB0' } ); + test.deepEqual(stack.node.resolve(vpc.vpcId), {Ref: 'TheVPC92636AB0' } ); test.done(); }, @@ -68,7 +68,7 @@ export = { const zones = new AvailabilityZoneProvider(stack).availabilityZones.length; test.equal(vpc.publicSubnets.length, zones); test.equal(vpc.privateSubnets.length, zones); - test.deepEqual(resolve(vpc.vpcId), { Ref: 'TheVPC92636AB0' }); + test.deepEqual(stack.node.resolve(vpc.vpcId), { Ref: 'TheVPC92636AB0' }); test.done(); }, @@ -442,7 +442,7 @@ export = { }); // THEN - test.deepEqual(resolve(vpc2.vpcId), { + test.deepEqual(vpc2.node.resolve(vpc2.vpcId), { 'Fn::ImportValue': 'TestStack:TheVPCVpcIdD346CDBA' }); @@ -461,7 +461,7 @@ export = { }); // THEN - test.deepEqual(resolve(imported.vpcId), { + test.deepEqual(imported.node.resolve(imported.vpcId), { 'Fn::ImportValue': 'TestStack:TheVPCVpcIdD346CDBA' }); diff --git a/packages/@aws-cdk/aws-ecr/lib/repository-ref.ts b/packages/@aws-cdk/aws-ecr/lib/repository-ref.ts index f85be5d0db917..ad7e8870f7ac1 100644 --- a/packages/@aws-cdk/aws-ecr/lib/repository-ref.ts +++ b/packages/@aws-cdk/aws-ecr/lib/repository-ref.ts @@ -122,8 +122,8 @@ export abstract class RepositoryBase extends cdk.Construct implements IRepositor * Returns an ECR ARN for a repository that resides in the same account/region * as the current stack. */ - public static arnForLocalRepository(repositoryName: string): string { - return cdk.ArnUtils.fromComponents({ + public static arnForLocalRepository(repositoryName: string, scope: cdk.IConstruct): string { + return cdk.Stack.find(scope).formatArn({ service: 'ecr', resource: 'repository', resourceName: repositoryName @@ -164,7 +164,7 @@ export abstract class RepositoryBase extends cdk.Construct implements IRepositor */ public repositoryUriForTag(tag?: string): string { const tagSuffix = tag ? `:${tag}` : ''; - const parts = cdk.ArnUtils.parse(this.repositoryArn); + const parts = cdk.Stack.find(this).parseArn(this.repositoryArn); return `${parts.account}.dkr.ecr.${parts.region}.amazonaws.com/${this.repositoryName}${tagSuffix}`; } @@ -265,7 +265,7 @@ class ImportedRepository extends RepositoryBase { 'which also implies that the repository resides in the same region/account as this stack'); } - this.repositoryArn = RepositoryBase.arnForLocalRepository(props.repositoryName); + this.repositoryArn = RepositoryBase.arnForLocalRepository(props.repositoryName, this); } if (props.repositoryName) { diff --git a/packages/@aws-cdk/aws-ecr/lib/repository.ts b/packages/@aws-cdk/aws-ecr/lib/repository.ts index 98ea1f4697752..c7346889b8177 100644 --- a/packages/@aws-cdk/aws-ecr/lib/repository.ts +++ b/packages/@aws-cdk/aws-ecr/lib/repository.ts @@ -126,7 +126,7 @@ export class Repository extends RepositoryBase { if (this.lifecycleRules.length === 0 && !this.registryId) { return undefined; } if (this.lifecycleRules.length > 0) { - lifecyclePolicyText = JSON.stringify(cdk.resolve({ + lifecyclePolicyText = JSON.stringify(this.node.resolve({ rules: this.orderedLifecycleRules().map(renderLifecycleRule), })); } diff --git a/packages/@aws-cdk/aws-ecr/test/test.repository.ts b/packages/@aws-cdk/aws-ecr/test/test.repository.ts index e3bfa4ffcc657..0869fbb59fe0e 100644 --- a/packages/@aws-cdk/aws-ecr/test/test.repository.ts +++ b/packages/@aws-cdk/aws-ecr/test/test.repository.ts @@ -137,7 +137,7 @@ export = { // THEN const arnSplit = { 'Fn::Split': [ ':', { 'Fn::GetAtt': [ 'Repo02AC86CF', 'Arn' ] } ] }; - test.deepEqual(cdk.resolve(uri), { 'Fn::Join': [ '', [ + test.deepEqual(repo.node.resolve(uri), { 'Fn::Join': [ '', [ { 'Fn::Select': [ 4, arnSplit ] }, '.dkr.ecr.', { 'Fn::Select': [ 3, arnSplit ] }, @@ -159,11 +159,11 @@ export = { const repo2 = ecr.Repository.import(stack2, 'Repo', repo1.export()); // THEN - test.deepEqual(cdk.resolve(repo2.repositoryArn), { + test.deepEqual(repo2.node.resolve(repo2.repositoryArn), { 'Fn::ImportValue': 'RepoRepositoryArn7F2901C9' }); - test.deepEqual(cdk.resolve(repo2.repositoryName), { + test.deepEqual(repo2.node.resolve(repo2.repositoryName), { 'Fn::ImportValue': 'RepoRepositoryName58A7E467' }); @@ -182,9 +182,9 @@ export = { const exportImport = repo2.export(); // THEN - test.deepEqual(cdk.resolve(repo2.repositoryArn), 'arn:aws:ecr:us-east-1:585695036304:repository/foo/bar/foo/fooo'); - test.deepEqual(cdk.resolve(repo2.repositoryName), 'foo/bar/foo/fooo'); - test.deepEqual(cdk.resolve(exportImport), { repositoryArn: 'arn:aws:ecr:us-east-1:585695036304:repository/foo/bar/foo/fooo' }); + test.deepEqual(repo2.node.resolve(repo2.repositoryArn), 'arn:aws:ecr:us-east-1:585695036304:repository/foo/bar/foo/fooo'); + test.deepEqual(repo2.node.resolve(repo2.repositoryName), 'foo/bar/foo/fooo'); + test.deepEqual(repo2.node.resolve(exportImport), { repositoryArn: 'arn:aws:ecr:us-east-1:585695036304:repository/foo/bar/foo/fooo' }); test.done(); }, @@ -212,8 +212,8 @@ export = { }); // THEN - test.deepEqual(cdk.resolve(repo.repositoryArn), { 'Fn::GetAtt': [ 'Boom', 'Arn' ] }); - test.deepEqual(cdk.resolve(repo.repositoryName), { 'Fn::GetAtt': [ 'Boom', 'Name' ] }); + test.deepEqual(repo.node.resolve(repo.repositoryArn), { 'Fn::GetAtt': [ 'Boom', 'Arn' ] }); + test.deepEqual(repo.node.resolve(repo.repositoryName), { 'Fn::GetAtt': [ 'Boom', 'Name' ] }); test.done(); }, @@ -227,7 +227,7 @@ export = { }); // THEN - test.deepEqual(cdk.resolve(repo.repositoryArn), { + test.deepEqual(repo.node.resolve(repo.repositoryArn), { 'Fn::Join': [ '', [ 'arn:', { Ref: 'AWS::Partition' }, @@ -238,7 +238,7 @@ export = { ':repository/my-repo' ] ] }); - test.deepEqual(cdk.resolve(repo.repositoryName), 'my-repo'); + test.deepEqual(repo.node.resolve(repo.repositoryName), 'my-repo'); test.done(); }, @@ -249,13 +249,13 @@ export = { // WHEN const repo = ecr.Repository.import(stack, 'Repo', { - repositoryArn: ecr.Repository.arnForLocalRepository(repoName), + repositoryArn: ecr.Repository.arnForLocalRepository(repoName, stack), repositoryName: repoName }); // THEN - test.deepEqual(cdk.resolve(repo.repositoryName), { 'Fn::GetAtt': [ 'Boom', 'Name' ] }); - test.deepEqual(cdk.resolve(repo.repositoryArn), { + test.deepEqual(repo.node.resolve(repo.repositoryName), { 'Fn::GetAtt': [ 'Boom', 'Name' ] }); + test.deepEqual(repo.node.resolve(repo.repositoryArn), { 'Fn::Join': [ '', [ 'arn:', { Ref: 'AWS::Partition' }, diff --git a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts index 9ccfe00335f60..cc3e5cefd8e48 100644 --- a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts @@ -228,7 +228,7 @@ export abstract class BaseService extends cdk.Construct private makeAutoScalingRole(): iam.IRole { // Use a Service Linked Role. return iam.Role.import(this, 'ScalingRole', { - roleArn: cdk.ArnUtils.fromComponents({ + roleArn: cdk.Stack.find(this).formatArn({ service: 'iam', resource: 'role/aws-service-role/ecs.application-autoscaling.amazonaws.com', resourceName: 'AWSServiceRoleForApplicationAutoScaling_ECSService', diff --git a/packages/@aws-cdk/aws-ecs/lib/base/task-definition.ts b/packages/@aws-cdk/aws-ecs/lib/base/task-definition.ts index 4c197604b76d2..f63fe555c3def 100644 --- a/packages/@aws-cdk/aws-ecs/lib/base/task-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/base/task-definition.ts @@ -242,25 +242,6 @@ export class TaskDefinition extends cdk.Construct { this.volumes.push(volume); } - /** - * Validate this task definition - */ - public validate(): string[] { - const ret = super.validate(); - - if (isEc2Compatible(this.compatibility)) { - // EC2 mode validations - - // Container sizes - for (const container of this.containers) { - if (!container.memoryLimitSpecified) { - ret.push(`ECS Container ${container.node.id} must have at least one of 'memoryLimitMiB' or 'memoryReservationMiB' specified`); - } - } - } - return ret; - } - /** * Constrain where tasks can be placed */ @@ -294,6 +275,25 @@ export class TaskDefinition extends cdk.Construct { return this.executionRole; } + /** + * Validate this task definition + */ + protected validate(): string[] { + const ret = super.validate(); + + if (isEc2Compatible(this.compatibility)) { + // EC2 mode validations + + // Container sizes + for (const container of this.containers) { + if (!container.memoryLimitSpecified) { + ret.push(`ECS Container ${container.node.id} must have at least one of 'memoryLimitMiB' or 'memoryReservationMiB' specified`); + } + } + } + return ret; + } + /** * Render the placement constraints */ @@ -423,4 +423,4 @@ export interface ITaskDefinitionExtension { * Apply the extension to the given TaskDefinition */ extend(taskDefinition: TaskDefinition): void; -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-service.ts b/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-service.ts index 80e85bf387778..80628ad98ccb6 100644 --- a/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-service.ts @@ -231,7 +231,7 @@ export class Ec2Service extends BaseService implements elb.ILoadBalancerTarget { /** * Validate this Ec2Service */ - public validate(): string[] { + protected validate(): string[] { const ret = super.validate(); if (!this.cluster.hasEc2Capacity) { ret.push('Cluster for this service needs Ec2 capacity. Call addXxxCapacity() on the cluster.'); diff --git a/packages/@aws-cdk/aws-ecs/lib/log-drivers/aws-log-driver.ts b/packages/@aws-cdk/aws-ecs/lib/log-drivers/aws-log-driver.ts index b97b4f9616714..0614626947f7f 100644 --- a/packages/@aws-cdk/aws-ecs/lib/log-drivers/aws-log-driver.ts +++ b/packages/@aws-cdk/aws-ecs/lib/log-drivers/aws-log-driver.ts @@ -73,12 +73,13 @@ export class AwsLogDriver extends LogDriver { * Return the log driver CloudFormation JSON */ public renderLogDriver(): CfnTaskDefinition.LogConfigurationProperty { + const stack = cdk.Stack.find(this); return { logDriver: 'awslogs', options: removeEmpty({ 'awslogs-group': this.logGroup.logGroupName, 'awslogs-stream-prefix': this.props.streamPrefix, - 'awslogs-region': `${new cdk.AwsRegion()}`, + 'awslogs-region': stack.region, 'awslogs-datetime-format': this.props.datetimeFormat, 'awslogs-multiline-pattern': this.props.multilinePattern, }), diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/test.ec2-task-definition.ts b/packages/@aws-cdk/aws-ecs/test/ec2/test.ec2-task-definition.ts index 04042794afbc2..feee1b254f7b0 100644 --- a/packages/@aws-cdk/aws-ecs/test/ec2/test.ec2-task-definition.ts +++ b/packages/@aws-cdk/aws-ecs/test/ec2/test.ec2-task-definition.ts @@ -232,7 +232,7 @@ export = { // THEN expect(stack).to(haveResourceLike("AWS::ECS::TaskDefinition", { - TaskRoleArn: cdk.resolve(taskDefinition.taskRole.roleArn) + TaskRoleArn: stack.node.resolve(taskDefinition.taskRole.roleArn) })); test.done(); diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener-rule.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener-rule.ts index cbe634e20b6c6..b6857f0fdbfa0 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener-rule.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener-rule.ts @@ -109,16 +109,6 @@ export class ApplicationListenerRule extends cdk.Construct implements cdk.IDepen this.conditions[field] = values; } - /** - * Validate the rule - */ - public validate() { - if (this.actions.length === 0) { - return ['Listener rule needs at least one action']; - } - return []; - } - /** * Add a TargetGroup to load balance to */ @@ -130,6 +120,16 @@ export class ApplicationListenerRule extends cdk.Construct implements cdk.IDepen targetGroup.registerListener(this.listener, this); } + /** + * Validate the rule + */ + protected validate() { + if (this.actions.length === 0) { + return ['Listener rule needs at least one action']; + } + return []; + } + /** * Render the conditions for this rule */ @@ -142,4 +142,4 @@ export class ApplicationListenerRule extends cdk.Construct implements cdk.IDepen } return ret; } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts index 60c2033f1e153..eb32e0264f6b1 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts @@ -224,17 +224,6 @@ export class ApplicationListener extends BaseListener implements IApplicationLis this.connections.allowTo(connectable, portRange, 'Load balancer to target'); } - /** - * Validate this listener. - */ - public validate(): string[] { - const errors = super.validate(); - if (this.protocol === ApplicationProtocol.Https && this.certificateArns.length === 0) { - errors.push('HTTPS Listener needs at least one certificate (call addCertificateArns)'); - } - return errors; - } - /** * Export this listener */ @@ -246,6 +235,17 @@ export class ApplicationListener extends BaseListener implements IApplicationLis }; } + /** + * Validate this listener. + */ + protected validate(): string[] { + const errors = super.validate(); + if (this.protocol === ApplicationProtocol.Https && this.certificateArns.length === 0) { + errors.push('HTTPS Listener needs at least one certificate (call addCertificateArns)'); + } + return errors; + } + /** * Add a default TargetGroup */ diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-listener.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-listener.ts index a4043438529b8..44b8299d5cb9b 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-listener.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-listener.ts @@ -25,7 +25,7 @@ export abstract class BaseListener extends cdk.Construct implements cdk.IDependa /** * Validate this listener */ - public validate(): string[] { + protected validate(): string[] { if (this.defaultActions.length === 0) { return ['Listener needs at least one default target group (call addTargetGroups)']; } diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/test.listener.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/test.listener.ts index 2bc6f79866622..2311081a79f3a 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/test.listener.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/test.listener.ts @@ -412,7 +412,7 @@ export = { test.equal('AWS/ApplicationELB', metric.namespace); const loadBalancerArn = { Ref: "LBSomeListenerCA01F1A0" }; - test.deepEqual(cdk.resolve(metric.dimensions), { + test.deepEqual(lb.node.resolve(metric.dimensions), { TargetGroup: { 'Fn::GetAtt': [ 'TargetGroup3D7CD9B8', 'TargetGroupFullName' ] }, LoadBalancer: { 'Fn::Join': [ '', diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/test.load-balancer.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/test.load-balancer.ts index 7f804951972b4..6a566df599ec3 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/test.load-balancer.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/test.load-balancer.ts @@ -180,7 +180,7 @@ export = { for (const metric of metrics) { test.equal('AWS/ApplicationELB', metric.namespace); - test.deepEqual(cdk.resolve(metric.dimensions), { + test.deepEqual(stack.node.resolve(metric.dimensions), { LoadBalancer: { 'Fn::GetAtt': ['LB8A12904C', 'LoadBalancerFullName'] } }); } diff --git a/packages/@aws-cdk/aws-events/lib/rule.ts b/packages/@aws-cdk/aws-events/lib/rule.ts index e7ff424d4f582..56b58e73c96c5 100644 --- a/packages/@aws-cdk/aws-events/lib/rule.ts +++ b/packages/@aws-cdk/aws-events/lib/rule.ts @@ -199,7 +199,7 @@ export class EventRule extends Construct implements IEventRule { mergeEventPattern(this.eventPattern, eventPattern); } - public validate() { + protected validate() { if (Object.keys(this.eventPattern).length === 0 && !this.scheduleExpression) { return [ `Either 'eventPattern' or 'scheduleExpression' must be defined` ]; } diff --git a/packages/@aws-cdk/aws-events/test/test.rule.ts b/packages/@aws-cdk/aws-events/test/test.rule.ts index 3e0001c455d9d..870194bde5ead 100644 --- a/packages/@aws-cdk/aws-events/test/test.rule.ts +++ b/packages/@aws-cdk/aws-events/test/test.rule.ts @@ -1,6 +1,6 @@ import { expect, haveResource } from '@aws-cdk/assert'; import cdk = require('@aws-cdk/cdk'); -import { resolve, Stack } from '@aws-cdk/cdk'; +import { Stack } from '@aws-cdk/cdk'; import { Test } from 'nodeunit'; import { IEventRuleTarget } from '../lib'; import { EventRule } from '../lib/rule'; @@ -329,7 +329,7 @@ export = { const rule = new EventRule(stack, 'EventRule'); rule.addTarget(t1); - test.deepEqual(resolve(receivedRuleArn), resolve(rule.ruleArn)); + test.deepEqual(stack.node.resolve(receivedRuleArn), stack.node.resolve(rule.ruleArn)); test.deepEqual(receivedRuleId, rule.node.uniqueId); test.done(); }, @@ -347,7 +347,7 @@ export = { }); // THEN - test.deepEqual(cdk.resolve(exportedRule), { eventRuleArn: { 'Fn::ImportValue': 'MyRuleRuleArnDB13ADB1' } }); + test.deepEqual(stack.node.resolve(exportedRule), { eventRuleArn: { 'Fn::ImportValue': 'MyRuleRuleArnDB13ADB1' } }); test.deepEqual(importedRule.ruleArn, 'arn:of:rule'); test.done(); diff --git a/packages/@aws-cdk/aws-iam/lib/managed-policy.ts b/packages/@aws-cdk/aws-iam/lib/managed-policy.ts index 64d78f4c2fe2e..531080b80ac4b 100644 --- a/packages/@aws-cdk/aws-iam/lib/managed-policy.ts +++ b/packages/@aws-cdk/aws-iam/lib/managed-policy.ts @@ -10,7 +10,7 @@ import cdk = require('@aws-cdk/cdk'); * prefix when constructing this object. */ export class AwsManagedPolicy { - constructor(private readonly managedPolicyName: string) { + constructor(private readonly managedPolicyName: string, private readonly scope: cdk.IConstruct) { } /** @@ -18,7 +18,7 @@ export class AwsManagedPolicy { */ public get policyArn(): string { // the arn is in the form of - arn:aws:iam::aws:policy/ - return cdk.ArnUtils.fromComponents({ + return cdk.Stack.find(this.scope).formatArn({ service: "iam", region: "", // no region for managed policy account: "aws", // the account for a managed policy is 'aws' diff --git a/packages/@aws-cdk/aws-iam/lib/policy-document.ts b/packages/@aws-cdk/aws-iam/lib/policy-document.ts index 5022a6f7445ca..3a30676b18a59 100644 --- a/packages/@aws-cdk/aws-iam/lib/policy-document.ts +++ b/packages/@aws-cdk/aws-iam/lib/policy-document.ts @@ -1,6 +1,6 @@ -import { AwsAccountId, AwsPartition, Token } from '@aws-cdk/cdk'; +import cdk = require('@aws-cdk/cdk'); -export class PolicyDocument extends Token { +export class PolicyDocument extends cdk.Token { private statements = new Array(); /** @@ -12,7 +12,7 @@ export class PolicyDocument extends Token { super(); } - public resolve(): any { + public resolve(_context: cdk.ResolveContext): any { if (this.isEmpty) { return undefined; } @@ -82,7 +82,7 @@ export class ArnPrincipal extends PolicyPrincipal { export class AccountPrincipal extends ArnPrincipal { constructor(public readonly accountId: any) { - super(`arn:${new AwsPartition()}:iam::${accountId}:root`); + super(new StackDependentToken(stack => `arn:${stack.partition}:iam::${accountId}:root`).toString()); } } @@ -137,7 +137,7 @@ export class FederatedPrincipal extends PolicyPrincipal { export class AccountRootPrincipal extends AccountPrincipal { constructor() { - super(new AwsAccountId()); + super(new StackDependentToken(stack => stack.accountId).toString()); } } @@ -201,7 +201,7 @@ export class CompositePrincipal extends PolicyPrincipal { /** * Represents a statement in an IAM policy document. */ -export class PolicyStatement extends Token { +export class PolicyStatement extends cdk.Token { private action = new Array(); private principal: { [key: string]: any[] } = {}; private resource = new Array(); @@ -250,14 +250,14 @@ export class PolicyStatement extends Token { return this.addPrincipal(new ArnPrincipal(arn)); } - public addArnPrincipal(arn: string): this { - return this.addAwsPrincipal(arn); - } - public addAwsAccountPrincipal(accountId: string): this { return this.addPrincipal(new AccountPrincipal(accountId)); } + public addArnPrincipal(arn: string): this { + return this.addAwsPrincipal(arn); + } + public addServicePrincipal(service: string): this { return this.addPrincipal(new ServicePrincipal(service)); } @@ -363,7 +363,7 @@ export class PolicyStatement extends Token { } public limitToAccount(accountId: string): PolicyStatement { - return this.addCondition('StringEquals', new Token(() => { + return this.addCondition('StringEquals', new cdk.Token(() => { return { 'sts:ExternalId': accountId }; })); } @@ -371,8 +371,7 @@ export class PolicyStatement extends Token { // // Serialization // - - public resolve(): any { + public resolve(_context: cdk.ResolveContext): any { return this.toJson(); } @@ -450,3 +449,17 @@ function mergePrincipal(target: { [key: string]: string[] }, source: { [key: str return target; } + +/** + * A lazy token that requires an instance of Stack to evaluate + */ +class StackDependentToken extends cdk.Token { + constructor(private readonly fn: (stack: cdk.Stack) => any) { + super(); + } + + public resolve(context: cdk.ResolveContext) { + const stack = cdk.Stack.find(context.scope); + return this.fn(stack); + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-iam/lib/policy.ts b/packages/@aws-cdk/aws-iam/lib/policy.ts index d3a5104a02b9e..6c5208a7b9dd9 100644 --- a/packages/@aws-cdk/aws-iam/lib/policy.ts +++ b/packages/@aws-cdk/aws-iam/lib/policy.ts @@ -171,7 +171,7 @@ export class Policy extends Construct implements IDependable { group.attachInlinePolicy(this); } - public validate(): string[] { + protected validate(): string[] { const result = new Array(); // validate that the policy document is not empty diff --git a/packages/@aws-cdk/aws-iam/lib/util.ts b/packages/@aws-cdk/aws-iam/lib/util.ts index 9acb4a71747bc..aeb0f2c39652f 100644 --- a/packages/@aws-cdk/aws-iam/lib/util.ts +++ b/packages/@aws-cdk/aws-iam/lib/util.ts @@ -1,10 +1,10 @@ -import { CloudFormationToken } from '@aws-cdk/cdk'; +import { Token } from '@aws-cdk/cdk'; import { Policy } from './policy'; const MAX_POLICY_NAME_LEN = 128; -export function undefinedIfEmpty(f: () => T[]): CloudFormationToken { - return new CloudFormationToken(() => { +export function undefinedIfEmpty(f: () => T[]): Token { + return new Token(() => { const array = f(); return (array && array.length > 0) ? array : undefined; }); diff --git a/packages/@aws-cdk/aws-iam/test/test.managed-policy.ts b/packages/@aws-cdk/aws-iam/test/test.managed-policy.ts index 49989eecc870a..142a239febaee 100644 --- a/packages/@aws-cdk/aws-iam/test/test.managed-policy.ts +++ b/packages/@aws-cdk/aws-iam/test/test.managed-policy.ts @@ -4,9 +4,10 @@ import { AwsManagedPolicy } from '../lib'; export = { 'simple managed policy'(test: Test) { - const mp = new AwsManagedPolicy("service-role/SomePolicy"); + const stack = new cdk.Stack(); + const mp = new AwsManagedPolicy("service-role/SomePolicy", stack); - test.deepEqual(cdk.resolve(mp.policyArn), { + test.deepEqual(stack.node.resolve(mp.policyArn), { "Fn::Join": ['', [ 'arn:', { Ref: 'AWS::Partition' }, diff --git a/packages/@aws-cdk/aws-iam/test/test.policy-document.ts b/packages/@aws-cdk/aws-iam/test/test.policy-document.ts index 7cb0e3997aa94..97eeef42f95aa 100644 --- a/packages/@aws-cdk/aws-iam/test/test.policy-document.ts +++ b/packages/@aws-cdk/aws-iam/test/test.policy-document.ts @@ -1,10 +1,12 @@ -import { resolve, Token } from '@aws-cdk/cdk'; +import { Stack, Token } from '@aws-cdk/cdk'; import { Test } from 'nodeunit'; import { Anyone, AnyPrincipal, CanonicalUserPrincipal, PolicyDocument, PolicyPrincipal, PolicyStatement } from '../lib'; import { ArnPrincipal, CompositePrincipal, FederatedPrincipal, PrincipalPolicyFragment, ServicePrincipal } from '../lib'; export = { 'the Permission class is a programming model for iam'(test: Test) { + const stack = new Stack(); + const p = new PolicyStatement(); p.addAction('sqs:SendMessage'); p.addActions('dynamodb:CreateTable', 'dynamodb:DeleteTable'); @@ -15,7 +17,7 @@ export = { p.addAwsAccountPrincipal(`my${new Token({ account: 'account' })}name`); p.limitToAccount('12221121221'); - test.deepEqual(resolve(p), { Action: + test.deepEqual(stack.node.resolve(p), { Action: [ 'sqs:SendMessage', 'dynamodb:CreateTable', 'dynamodb:DeleteTable' ], @@ -36,6 +38,7 @@ export = { }, 'the PolicyDocument class is a dom for iam policy documents'(test: Test) { + const stack = new Stack(); const doc = new PolicyDocument(); const p1 = new PolicyStatement(); p1.addAction('sqs:SendMessage'); @@ -48,7 +51,7 @@ export = { doc.addStatement(p1); doc.addStatement(p2); - test.deepEqual(resolve(doc), { + test.deepEqual(stack.node.resolve(doc), { Version: '2012-10-17', Statement: [ { Effect: 'Allow', Action: 'sqs:SendMessage', Resource: '*' }, @@ -58,6 +61,7 @@ export = { }, 'A PolicyDocument can be initialized with an existing policy, which is merged upon serialization'(test: Test) { + const stack = new Stack(); const base = { Version: 'Foo', Something: 123, @@ -69,7 +73,7 @@ export = { const doc = new PolicyDocument(base); doc.addStatement(new PolicyStatement().addResource('resource').addAction('action')); - test.deepEqual(resolve(doc), { Version: 'Foo', + test.deepEqual(stack.node.resolve(doc), { Version: 'Foo', Something: 123, Statement: [ { Statement1: 1 }, @@ -79,8 +83,9 @@ export = { }, 'Permission allows specifying multiple actions upon construction'(test: Test) { + const stack = new Stack(); const perm = new PolicyStatement().addResource('MyResource').addActions('Action1', 'Action2', 'Action3'); - test.deepEqual(resolve(perm), { + test.deepEqual(stack.node.resolve(perm), { Effect: 'Allow', Action: [ 'Action1', 'Action2', 'Action3' ], Resource: 'MyResource' }); @@ -88,16 +93,18 @@ export = { }, 'PolicyDoc resolves to undefined if there are no permissions'(test: Test) { + const stack = new Stack(); const p = new PolicyDocument(); - test.deepEqual(resolve(p), undefined); + test.deepEqual(stack.node.resolve(p), undefined); test.done(); }, 'canonicalUserPrincipal adds a principal to a policy with the passed canonical user id'(test: Test) { + const stack = new Stack(); const p = new PolicyStatement(); const canoncialUser = "averysuperduperlongstringfor"; p.addPrincipal(new CanonicalUserPrincipal(canoncialUser)); - test.deepEqual(resolve(p), { + test.deepEqual(stack.node.resolve(p), { Effect: "Allow", Principal: { CanonicalUser: canoncialUser @@ -107,9 +114,11 @@ export = { }, 'addAccountRootPrincipal adds a principal with the current account root'(test: Test) { + const stack = new Stack(); + const p = new PolicyStatement(); p.addAccountRootPrincipal(); - test.deepEqual(resolve(p), { + test.deepEqual(stack.node.resolve(p), { Effect: "Allow", Principal: { AWS: { @@ -130,9 +139,10 @@ export = { }, 'addFederatedPrincipal adds a Federated principal with the passed value'(test: Test) { + const stack = new Stack(); const p = new PolicyStatement(); p.addFederatedPrincipal("com.amazon.cognito", { StringEquals: { key: 'value' }}); - test.deepEqual(resolve(p), { + test.deepEqual(stack.node.resolve(p), { Effect: "Allow", Principal: { Federated: "com.amazon.cognito" @@ -145,10 +155,12 @@ export = { }, 'addAwsAccountPrincipal can be used multiple times'(test: Test) { + const stack = new Stack(); + const p = new PolicyStatement(); p.addAwsAccountPrincipal('1234'); p.addAwsAccountPrincipal('5678'); - test.deepEqual(resolve(p), { + test.deepEqual(stack.node.resolve(p), { Effect: 'Allow', Principal: { AWS: [ @@ -208,13 +220,14 @@ export = { }, 'the { AWS: "*" } principal is represented as `Anyone` or `AnyPrincipal`'(test: Test) { + const stack = new Stack(); const p = new PolicyDocument(); p.addStatement(new PolicyStatement().addPrincipal(new Anyone())); p.addStatement(new PolicyStatement().addPrincipal(new AnyPrincipal())); p.addStatement(new PolicyStatement().addAnyPrincipal()); - test.deepEqual(resolve(p), { + test.deepEqual(stack.node.resolve(p), { Statement: [ { Effect: 'Allow', Principal: '*' }, { Effect: 'Allow', Principal: '*' }, @@ -226,13 +239,14 @@ export = { }, 'addAwsPrincipal/addArnPrincipal are the aliases'(test: Test) { + const stack = new Stack(); const p = new PolicyDocument(); p.addStatement(new PolicyStatement().addAwsPrincipal('111222-A')); p.addStatement(new PolicyStatement().addArnPrincipal('111222-B')); p.addStatement(new PolicyStatement().addPrincipal(new ArnPrincipal('111222-C'))); - test.deepEqual(resolve(p), { + test.deepEqual(stack.node.resolve(p), { Statement: [ { Effect: 'Allow', Principal: { AWS: '111222-A' } }, { Effect: 'Allow', Principal: { AWS: '111222-B' } }, @@ -245,12 +259,13 @@ export = { }, 'addCanonicalUserPrincipal can be used to add cannonical user principals'(test: Test) { + const stack = new Stack(); const p = new PolicyDocument(); p.addStatement(new PolicyStatement().addCanonicalUserPrincipal('cannonical-user-1')); p.addStatement(new PolicyStatement().addPrincipal(new CanonicalUserPrincipal('cannonical-user-2'))); - test.deepEqual(resolve(p), { + test.deepEqual(stack.node.resolve(p), { Statement: [ { Effect: 'Allow', Principal: { CanonicalUser: 'cannonical-user-1' } }, { Effect: 'Allow', Principal: { CanonicalUser: 'cannonical-user-2' } } @@ -262,13 +277,14 @@ export = { }, 'addPrincipal correctly merges array in'(test: Test) { + const stack = new Stack(); const arrayPrincipal: PolicyPrincipal = { assumeRoleAction: 'sts:AssumeRole', policyFragment: () => new PrincipalPolicyFragment({ AWS: ['foo', 'bar'] }), }; const s = new PolicyStatement().addAccountRootPrincipal() .addPrincipal(arrayPrincipal); - test.deepEqual(resolve(s), { + test.deepEqual(stack.node.resolve(s), { Effect: 'Allow', Principal: { AWS: [ @@ -282,13 +298,14 @@ export = { // https://github.com/awslabs/aws-cdk/issues/1201 'policy statements with multiple principal types can be created using multiple addPrincipal calls'(test: Test) { + const stack = new Stack(); const s = new PolicyStatement() .addAwsPrincipal('349494949494') .addServicePrincipal('ec2.amazonaws.com') .addResource('resource') .addAction('action'); - test.deepEqual(resolve(s), { + test.deepEqual(stack.node.resolve(s), { Action: 'action', Effect: 'Allow', Principal: { AWS: '349494949494', Service: 'ec2.amazonaws.com' }, @@ -301,9 +318,10 @@ export = { 'CompositePrincipal can be used to represent a principal that has multiple types': { 'with a single principal'(test: Test) { + const stack = new Stack(); const p = new CompositePrincipal(new ArnPrincipal('i:am:an:arn')); const statement = new PolicyStatement().addPrincipal(p); - test.deepEqual(resolve(statement), { Effect: 'Allow', Principal: { AWS: 'i:am:an:arn' } }); + test.deepEqual(stack.node.resolve(statement), { Effect: 'Allow', Principal: { AWS: 'i:am:an:arn' } }); test.done(); }, @@ -316,6 +334,7 @@ export = { }, 'principals and conditions are a big nice merge'(test: Test) { + const stack = new Stack(); // add via ctor const p = new CompositePrincipal( new ArnPrincipal('i:am:an:arn'), @@ -333,7 +352,7 @@ export = { statement.addAwsPrincipal('aws-principal-3'); statement.addCondition('cond2', { boom: 123 }); - test.deepEqual(resolve(statement), { + test.deepEqual(stack.node.resolve(statement), { Condition: { cond2: { boom: 123 } }, diff --git a/packages/@aws-cdk/aws-iam/test/test.role.ts b/packages/@aws-cdk/aws-iam/test/test.role.ts index b78deac2bb514..54ea02a6a3121 100644 --- a/packages/@aws-cdk/aws-iam/test/test.role.ts +++ b/packages/@aws-cdk/aws-iam/test/test.role.ts @@ -1,5 +1,5 @@ import { expect, haveResource } from '@aws-cdk/assert'; -import { resolve, Resource, Stack } from '@aws-cdk/cdk'; +import { Resource, Stack } from '@aws-cdk/cdk'; import { Test } from 'nodeunit'; import { ArnPrincipal, CompositePrincipal, FederatedPrincipal, PolicyStatement, Role, ServicePrincipal } from '../lib'; @@ -249,13 +249,13 @@ export = { const importedRole = Role.import(stack, 'ImportedRole', exportedRole); // THEN - test.deepEqual(resolve(exportedRole), { + test.deepEqual(stack.node.resolve(exportedRole), { roleArn: { 'Fn::ImportValue': 'MyRoleRoleArn3388B7E2' }, roleId: { 'Fn::ImportValue': 'MyRoleRoleIdF7B258D8' } }); - test.deepEqual(resolve(importedRole.roleArn), { 'Fn::ImportValue': 'MyRoleRoleArn3388B7E2' }); - test.deepEqual(resolve(importedRole.roleId), { 'Fn::ImportValue': 'MyRoleRoleIdF7B258D8' }); + test.deepEqual(stack.node.resolve(importedRole.roleArn), { 'Fn::ImportValue': 'MyRoleRoleArn3388B7E2' }); + test.deepEqual(stack.node.resolve(importedRole.roleId), { 'Fn::ImportValue': 'MyRoleRoleIdF7B258D8' }); test.done(); } }; diff --git a/packages/@aws-cdk/aws-kinesis/lib/stream.ts b/packages/@aws-cdk/aws-kinesis/lib/stream.ts index 4cdda2e39a634..0b569ccf07227 100644 --- a/packages/@aws-cdk/aws-kinesis/lib/stream.ts +++ b/packages/@aws-cdk/aws-kinesis/lib/stream.ts @@ -197,9 +197,10 @@ export abstract class StreamBase extends cdk.Construct implements IStream { public logSubscriptionDestination(sourceLogGroup: logs.ILogGroup): logs.LogSubscriptionDestination { // Following example from https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/SubscriptionFilters.html#DestinationKinesisExample if (!this.cloudWatchLogsRole) { + const stack = cdk.Stack.find(this); // Create a role to be assumed by CWL that can write to this stream and pass itself. this.cloudWatchLogsRole = new iam.Role(this, 'CloudWatchLogsCanPutRecords', { - assumedBy: new iam.ServicePrincipal(`logs.${new cdk.AwsRegion()}.amazonaws.com`) + assumedBy: new iam.ServicePrincipal(`logs.${stack.region}.amazonaws.com`) }); this.cloudWatchLogsRole.addToPolicy(new iam.PolicyStatement().addAction('kinesis:PutRecord').addResource(this.streamArn)); this.cloudWatchLogsRole.addToPolicy(new iam.PolicyStatement().addAction('iam:PassRole').addResource(this.cloudWatchLogsRole.roleArn)); @@ -428,7 +429,7 @@ class ImportedStream extends StreamBase { this.streamArn = props.streamArn; // Get the name from the ARN - this.streamName = cdk.ArnUtils.parse(props.streamArn).resourceName!; + this.streamName = cdk.Stack.find(this).parseArn(props.streamArn).resourceName!; if (props.encryptionKey) { // TODO: import "scope" should be changed to "this" diff --git a/packages/@aws-cdk/aws-kms/lib/key.ts b/packages/@aws-cdk/aws-kms/lib/key.ts index 42f40d6803641..7b259bfb8fc25 100644 --- a/packages/@aws-cdk/aws-kms/lib/key.ts +++ b/packages/@aws-cdk/aws-kms/lib/key.ts @@ -1,5 +1,5 @@ import { PolicyDocument, PolicyStatement } from '@aws-cdk/aws-iam'; -import { Construct, DeletionPolicy, IConstruct, Output, resolve, TagManager, Tags } from '@aws-cdk/cdk'; +import { Construct, DeletionPolicy, IConstruct, Output, TagManager, Tags } from '@aws-cdk/cdk'; import { EncryptionKeyAlias } from './alias'; import { CfnKey } from './kms.generated'; @@ -68,7 +68,7 @@ export abstract class EncryptionKeyBase extends Construct { public addToResourcePolicy(statement: PolicyStatement, allowNoOp = true) { if (!this.policy) { if (allowNoOp) { return; } - throw new Error(`Unable to add statement to IAM resource policy for KMS key: ${JSON.stringify(resolve(this.keyArn))}`); + throw new Error(`Unable to add statement to IAM resource policy for KMS key: ${JSON.stringify(this.node.resolve(this.keyArn))}`); } this.policy.addStatement(statement); diff --git a/packages/@aws-cdk/aws-kms/test/integ.key.ts b/packages/@aws-cdk/aws-kms/test/integ.key.ts index f682b1d19fe6e..370a696fbaab0 100644 --- a/packages/@aws-cdk/aws-kms/test/integ.key.ts +++ b/packages/@aws-cdk/aws-kms/test/integ.key.ts @@ -1,5 +1,5 @@ import { PolicyStatement } from '@aws-cdk/aws-iam'; -import { App, AwsAccountId, Stack } from '@aws-cdk/cdk'; +import { App, Stack } from '@aws-cdk/cdk'; import { EncryptionKey } from '../lib'; const app = new App(); @@ -11,7 +11,7 @@ const key = new EncryptionKey(stack, 'MyKey'); key.addToResourcePolicy(new PolicyStatement() .addAllResources() .addAction('kms:encrypt') - .addAwsPrincipal(new AwsAccountId().toString())); + .addAwsPrincipal(stack.accountId)); key.addAlias('alias/bar'); diff --git a/packages/@aws-cdk/aws-lambda/lib/lambda-ref.ts b/packages/@aws-cdk/aws-lambda/lib/lambda-ref.ts index ae954a9db205c..480b6af15c6d9 100644 --- a/packages/@aws-cdk/aws-lambda/lib/lambda-ref.ts +++ b/packages/@aws-cdk/aws-lambda/lib/lambda-ref.ts @@ -327,12 +327,13 @@ export abstract class FunctionBase extends cdk.Construct implements IFunction { const arn = sourceLogGroup.logGroupArn; if (this.logSubscriptionDestinationPolicyAddedFor.indexOf(arn) === -1) { + const stack = cdk.Stack.find(this); // NOTE: the use of {AWS::Region} limits this to the same region, which shouldn't really be an issue, // since the Lambda must be in the same region as the SubscriptionFilter anyway. // // (Wildcards in principals are unfortunately not supported. this.addPermission('InvokedByCloudWatchLogs', { - principal: new iam.ServicePrincipal(`logs.${new cdk.AwsRegion()}.amazonaws.com`), + principal: new iam.ServicePrincipal(`logs.${stack.region}.amazonaws.com`), sourceArn: arn }); this.logSubscriptionDestinationPolicyAddedFor.push(arn); @@ -351,9 +352,10 @@ export abstract class FunctionBase extends cdk.Construct implements IFunction { */ public asBucketNotificationDestination(bucketArn: string, bucketId: string): s3n.BucketNotificationDestinationProps { const permissionId = `AllowBucketNotificationsFrom${bucketId}`; + const stack = cdk.Stack.find(this); if (!this.node.tryFindChild(permissionId)) { this.addPermission(permissionId, { - sourceAccount: new cdk.AwsAccountId().toString(), + sourceAccount: stack.accountId, principal: new iam.ServicePrincipal('s3.amazonaws.com'), sourceArn: bucketArn, }); @@ -414,7 +416,7 @@ export abstract class FunctionBase extends cdk.Construct implements IFunction { return (principal as iam.ServicePrincipal).service; } - throw new Error(`Invalid principal type for Lambda permission statement: ${JSON.stringify(cdk.resolve(principal))}. ` + + throw new Error(`Invalid principal type for Lambda permission statement: ${JSON.stringify(this.node.resolve(principal))}. ` + 'Supported: AccountPrincipal, ServicePrincipal'); } } diff --git a/packages/@aws-cdk/aws-lambda/lib/lambda.ts b/packages/@aws-cdk/aws-lambda/lib/lambda.ts index 22536616043b9..a1eaaa906fa8c 100644 --- a/packages/@aws-cdk/aws-lambda/lib/lambda.ts +++ b/packages/@aws-cdk/aws-lambda/lib/lambda.ts @@ -311,11 +311,11 @@ export class Function extends FunctionBase { const managedPolicyArns = new Array(); // the arn is in the form of - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - managedPolicyArns.push(new iam.AwsManagedPolicy("service-role/AWSLambdaBasicExecutionRole").policyArn); + managedPolicyArns.push(new iam.AwsManagedPolicy("service-role/AWSLambdaBasicExecutionRole", this).policyArn); if (props.vpc) { // Policy that will have ENI creation permissions - managedPolicyArns.push(new iam.AwsManagedPolicy("service-role/AWSLambdaVPCAccessExecutionRole").policyArn); + managedPolicyArns.push(new iam.AwsManagedPolicy("service-role/AWSLambdaVPCAccessExecutionRole", this).policyArn); } this.role = props.role || new iam.Role(this, 'ServiceRole', { diff --git a/packages/@aws-cdk/aws-lambda/test/test.alias.ts b/packages/@aws-cdk/aws-lambda/test/test.alias.ts index 3a0316c6a8450..cd2b30baeeaa9 100644 --- a/packages/@aws-cdk/aws-lambda/test/test.alias.ts +++ b/packages/@aws-cdk/aws-lambda/test/test.alias.ts @@ -1,6 +1,6 @@ import { beASupersetOfTemplate, expect, haveResource } from '@aws-cdk/assert'; import { AccountPrincipal } from '@aws-cdk/aws-iam'; -import { resolve, Stack } from '@aws-cdk/cdk'; +import { Stack } from '@aws-cdk/cdk'; import { Test } from 'nodeunit'; import lambda = require('../lib'); @@ -31,7 +31,7 @@ export = { Type: "AWS::Lambda::Alias", Properties: { FunctionName: { Ref: "MyLambdaCCE802FB" }, - FunctionVersion: resolve(version.functionVersion), + FunctionVersion: stack.node.resolve(version.functionVersion), Name: "prod" } } @@ -59,11 +59,11 @@ export = { }); expect(stack).to(haveResource('AWS::Lambda::Alias', { - FunctionVersion: resolve(version1.functionVersion), + FunctionVersion: stack.node.resolve(version1.functionVersion), RoutingConfig: { AdditionalVersionWeights: [ { - FunctionVersion: resolve(version2.functionVersion), + FunctionVersion: stack.node.resolve(version2.functionVersion), FunctionWeight: 0.1 } ] @@ -123,7 +123,7 @@ export = { // THEN expect(stack).to(haveResource('AWS::Lambda::Permission', { - FunctionName: resolve(fn.functionName), + FunctionName: stack.node.resolve(fn.functionName), Principal: "123456" })); diff --git a/packages/@aws-cdk/aws-lambda/test/test.lambda.ts b/packages/@aws-cdk/aws-lambda/test/test.lambda.ts index e1f8e4526013b..c74dde7129541 100644 --- a/packages/@aws-cdk/aws-lambda/test/test.lambda.ts +++ b/packages/@aws-cdk/aws-lambda/test/test.lambda.ts @@ -1,4 +1,4 @@ -import { countResources, expect, haveResource, ResourcePart } from '@aws-cdk/assert'; +import { countResources, expect, haveResource, MatchStyle, ResourcePart } from '@aws-cdk/assert'; import events = require('@aws-cdk/aws-events'); import iam = require('@aws-cdk/aws-iam'); import sqs = require('@aws-cdk/aws-sqs'); @@ -118,7 +118,7 @@ export = { fn.addPermission('S3Permission', { action: 'lambda:*', principal: new iam.ServicePrincipal('s3.amazonaws.com'), - sourceAccount: new cdk.AwsAccountId().toString(), + sourceAccount: stack.accountId, sourceArn: 'arn:aws:s3:::my_bucket' }); @@ -621,9 +621,7 @@ export = { 'default function with SQS DLQ when client provides Queue to be used as DLQ'(test: Test) { const stack = new cdk.Stack(); - const dlqStack = new cdk.Stack(); - - const dlQueue = new sqs.Queue(dlqStack, 'DeadLetterQueue', { + const dlQueue = new sqs.Queue(stack, 'DeadLetterQueue', { queueName: 'MyLambda_DLQ', retentionPeriodSec: 1209600 }); @@ -725,16 +723,14 @@ export = { } } } - ); + , MatchStyle.SUPERSET); test.done(); }, 'default function with SQS DLQ when client provides Queue to be used as DLQ and deadLetterQueueEnabled set to true'(test: Test) { const stack = new cdk.Stack(); - const dlqStack = new cdk.Stack(); - - const dlQueue = new sqs.Queue(dlqStack, 'DeadLetterQueue', { + const dlQueue = new sqs.Queue(stack, 'DeadLetterQueue', { queueName: 'MyLambda_DLQ', retentionPeriodSec: 1209600 }); @@ -837,16 +833,14 @@ export = { } } } - ); + , MatchStyle.SUPERSET); test.done(); }, 'error when default function with SQS DLQ when client provides Queue to be used as DLQ and deadLetterQueueEnabled set to false'(test: Test) { const stack = new cdk.Stack(); - const dlqStack = new cdk.Stack(); - - const dlQueue = new sqs.Queue(dlqStack, 'DeadLetterQueue', { + const dlQueue = new sqs.Queue(stack, 'DeadLetterQueue', { queueName: 'MyLambda_DLQ', retentionPeriodSec: 1209600 }); diff --git a/packages/@aws-cdk/aws-logs/lib/cross-account-destination.ts b/packages/@aws-cdk/aws-logs/lib/cross-account-destination.ts index 4507f01808926..dccec000fa360 100644 --- a/packages/@aws-cdk/aws-logs/lib/cross-account-destination.ts +++ b/packages/@aws-cdk/aws-logs/lib/cross-account-destination.ts @@ -94,7 +94,7 @@ export class CrossAccountDestination extends cdk.Construct implements ILogSubscr /** * Return a stringified JSON version of the PolicyDocument */ - private lazyStringifiedPolicyDocument() { - return new cdk.Token(() => this.policyDocument.isEmpty ? '' : cdk.CloudFormationJSON.stringify(cdk.resolve(this.policyDocument))).toString(); + private lazyStringifiedPolicyDocument(): string { + return new cdk.Token(() => this.policyDocument.isEmpty ? '' : this.node.stringifyJson(this.policyDocument)).toString(); } } diff --git a/packages/@aws-cdk/aws-logs/lib/log-group.ts b/packages/@aws-cdk/aws-logs/lib/log-group.ts index dec077a84d1b3..4f548fd295bdc 100644 --- a/packages/@aws-cdk/aws-logs/lib/log-group.ts +++ b/packages/@aws-cdk/aws-logs/lib/log-group.ts @@ -295,7 +295,7 @@ class ImportedLogGroup extends LogGroupBase { super(scope, id); this.logGroupArn = props.logGroupArn; - this.logGroupName = cdk.ArnUtils.resourceNameComponent(props.logGroupArn, ':'); + this.logGroupName = cdk.Stack.find(this).parseArn(props.logGroupArn, ':').resourceName!; } /** diff --git a/packages/@aws-cdk/aws-rds/lib/cluster-parameter-group.ts b/packages/@aws-cdk/aws-rds/lib/cluster-parameter-group.ts index 59d0c1afb2441..10a8e89c06524 100644 --- a/packages/@aws-cdk/aws-rds/lib/cluster-parameter-group.ts +++ b/packages/@aws-cdk/aws-rds/lib/cluster-parameter-group.ts @@ -105,7 +105,7 @@ export class ClusterParameterGroup extends cdk.Construct implements IClusterPara /** * Validate this construct */ - public validate(): string[] { + protected validate(): string[] { if (Object.keys(this.parameters).length === 0) { return ['At least one parameter required, call setParameter().']; } diff --git a/packages/@aws-cdk/aws-rds/test/test.cluster.ts b/packages/@aws-cdk/aws-rds/test/test.cluster.ts index 841a6b2776d04..9ec072c6aa298 100644 --- a/packages/@aws-cdk/aws-rds/test/test.cluster.ts +++ b/packages/@aws-cdk/aws-rds/test/test.cluster.ts @@ -136,8 +136,8 @@ export = { const imported = ClusterParameterGroup.import(stack, 'ImportParams', exported); // THEN - test.deepEqual(cdk.resolve(exported), { parameterGroupName: { 'Fn::ImportValue': 'ParamsParameterGroupNameA6B808D7' } }); - test.deepEqual(cdk.resolve(imported.parameterGroupName), { 'Fn::ImportValue': 'ParamsParameterGroupNameA6B808D7' }); + test.deepEqual(stack.node.resolve(exported), { parameterGroupName: { 'Fn::ImportValue': 'ParamsParameterGroupNameA6B808D7' } }); + test.deepEqual(stack.node.resolve(imported.parameterGroupName), { 'Fn::ImportValue': 'ParamsParameterGroupNameA6B808D7' }); test.done(); } }; diff --git a/packages/@aws-cdk/aws-route53/lib/hosted-zone.ts b/packages/@aws-cdk/aws-route53/lib/hosted-zone.ts index 944296916741b..4b6c621845fa5 100644 --- a/packages/@aws-cdk/aws-route53/lib/hosted-zone.ts +++ b/packages/@aws-cdk/aws-route53/lib/hosted-zone.ts @@ -92,7 +92,7 @@ export class HostedZone extends cdk.Construct implements IHostedZone { * @param vpc the other VPC to add. */ public addVpc(vpc: ec2.IVpcNetwork) { - this.vpcs.push({ vpcId: vpc.vpcId, vpcRegion: new cdk.AwsRegion().toString() }); + this.vpcs.push({ vpcId: vpc.vpcId, vpcRegion: vpc.vpcRegion }); } } diff --git a/packages/@aws-cdk/aws-route53/test/test.hosted-zone-provider.ts b/packages/@aws-cdk/aws-route53/test/test.hosted-zone-provider.ts index c22c49cbedd79..e7dd3d2d09b06 100644 --- a/packages/@aws-cdk/aws-route53/test/test.hosted-zone-provider.ts +++ b/packages/@aws-cdk/aws-route53/test/test.hosted-zone-provider.ts @@ -33,7 +33,7 @@ export = { // WHEN const provider = new HostedZoneProvider(stack, filter); - const zoneProps = cdk.resolve(provider.findHostedZone()); + const zoneProps = stack.node.resolve(provider.findHostedZone()); const zoneRef = provider.findAndImport(stack, 'MyZoneProvider'); // THEN diff --git a/packages/@aws-cdk/aws-route53/test/test.route53.ts b/packages/@aws-cdk/aws-route53/test/test.route53.ts index 87898bcf30603..27a26cc19ce6e 100644 --- a/packages/@aws-cdk/aws-route53/test/test.route53.ts +++ b/packages/@aws-cdk/aws-route53/test/test.route53.ts @@ -33,7 +33,7 @@ export = { Name: "test.private.", VPCs: [{ VPCId: { Ref: 'VPCB9E5F0B4' }, - VPCRegion: { Ref: 'AWS::Region' } + VPCRegion: 'bermuda-triangle' }] } } @@ -55,11 +55,11 @@ export = { Name: "test.private.", VPCs: [{ VPCId: { Ref: 'VPC17DE2CF87' }, - VPCRegion: { Ref: 'AWS::Region' } + VPCRegion: 'bermuda-triangle' }, { VPCId: { Ref: 'VPC2C1F0E711' }, - VPCRegion: { Ref: 'AWS::Region' } + VPCRegion: 'bermuda-triangle' }] } } diff --git a/packages/@aws-cdk/aws-s3/lib/bucket.ts b/packages/@aws-cdk/aws-s3/lib/bucket.ts index ec53b1ee88ea9..f8f4f2cf1952d 100644 --- a/packages/@aws-cdk/aws-s3/lib/bucket.ts +++ b/packages/@aws-cdk/aws-s3/lib/bucket.ts @@ -310,7 +310,8 @@ export abstract class BucketBase extends cdk.Construct implements IBucket { * @returns an ObjectS3Url token */ public urlForObject(key?: string): string { - const components = [ `https://s3.${new cdk.AwsRegion()}.${new cdk.AwsURLSuffix()}/${this.bucketName}` ]; + const stack = cdk.Stack.find(this); + const components = [ `https://s3.${stack.region}.${stack.urlSuffix}/${this.bucketName}` ]; if (key) { // trim prepending '/' if (typeof key === 'string' && key.startsWith('/')) { @@ -963,12 +964,12 @@ class ImportedBucket extends BucketBase { constructor(scope: cdk.Construct, id: string, private readonly props: BucketImportProps) { super(scope, id); - const bucketName = parseBucketName(props); + const bucketName = parseBucketName(this, props); if (!bucketName) { throw new Error('Bucket name is required'); } - this.bucketArn = parseBucketArn(props); + this.bucketArn = parseBucketArn(this, props); this.bucketName = bucketName; this.domainName = props.bucketDomainName || this.generateDomainName(); this.autoCreatePolicy = false; diff --git a/packages/@aws-cdk/aws-s3/lib/notifications-resource/notifications-resource-handler.ts b/packages/@aws-cdk/aws-s3/lib/notifications-resource/notifications-resource-handler.ts index a3c04c426b26a..29ac33224c938 100644 --- a/packages/@aws-cdk/aws-s3/lib/notifications-resource/notifications-resource-handler.ts +++ b/packages/@aws-cdk/aws-s3/lib/notifications-resource/notifications-resource-handler.ts @@ -50,7 +50,7 @@ export class NotificationsResourceHandler extends cdk.Construct { const role = new iam.Role(this, 'Role', { assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'), managedPolicyArns: [ - cdk.ArnUtils.fromComponents({ + cdk.Stack.find(this).formatArn({ service: 'iam', region: '', // no region for managed policy account: 'aws', // the account for a managed policy is 'aws' diff --git a/packages/@aws-cdk/aws-s3/lib/util.ts b/packages/@aws-cdk/aws-s3/lib/util.ts index a6c9861829a15..e241db77cd5a0 100644 --- a/packages/@aws-cdk/aws-s3/lib/util.ts +++ b/packages/@aws-cdk/aws-s3/lib/util.ts @@ -1,7 +1,7 @@ import cdk = require('@aws-cdk/cdk'); import { BucketImportProps } from './bucket'; -export function parseBucketArn(props: BucketImportProps): string { +export function parseBucketArn(construct: cdk.IConstruct, props: BucketImportProps): string { // if we have an explicit bucket ARN, use it. if (props.bucketArn) { @@ -9,7 +9,7 @@ export function parseBucketArn(props: BucketImportProps): string { } if (props.bucketName) { - return cdk.ArnUtils.fromComponents({ + return cdk.Stack.find(construct).formatArn({ // S3 Bucket names are globally unique in a partition, // and so their ARNs have empty region and account components region: '', @@ -22,7 +22,7 @@ export function parseBucketArn(props: BucketImportProps): string { throw new Error('Cannot determine bucket ARN. At least `bucketArn` or `bucketName` is needed'); } -export function parseBucketName(props: BucketImportProps): string | undefined { +export function parseBucketName(construct: cdk.IConstruct, props: BucketImportProps): string | undefined { // if we have an explicit bucket name, use it. if (props.bucketName) { @@ -32,9 +32,9 @@ export function parseBucketName(props: BucketImportProps): string | undefined { // if we have a string arn, we can extract the bucket name from it. if (props.bucketArn) { - const resolved = cdk.resolve(props.bucketArn); + const resolved = construct.node.resolve(props.bucketArn); if (typeof(resolved) === 'string') { - const components = cdk.ArnUtils.parse(resolved); + const components = cdk.Stack.find(construct).parseArn(resolved); if (components.service !== 's3') { throw new Error('Invalid ARN. Expecting "s3" service:' + resolved); } diff --git a/packages/@aws-cdk/aws-s3/test/test.bucket.ts b/packages/@aws-cdk/aws-s3/test/test.bucket.ts index 1c1acab595baf..cd368c0232005 100644 --- a/packages/@aws-cdk/aws-s3/test/test.bucket.ts +++ b/packages/@aws-cdk/aws-s3/test/test.bucket.ts @@ -246,7 +246,7 @@ export = { const x = new iam.PolicyStatement().addResource(bucket.bucketArn).addAction('s3:ListBucket'); - test.deepEqual(cdk.resolve(x), { + test.deepEqual(bucket.node.resolve(x), { Action: 's3:ListBucket', Effect: 'Allow', Resource: { 'Fn::GetAtt': [ 'MyBucketF68F3FF0', 'Arn' ] } @@ -262,7 +262,7 @@ export = { const p = new iam.PolicyStatement().addResource(bucket.arnForObjects('hello/world')).addAction('s3:GetObject'); - test.deepEqual(cdk.resolve(p), { + test.deepEqual(bucket.node.resolve(p), { Action: 's3:GetObject', Effect: 'Allow', Resource: { @@ -288,7 +288,7 @@ export = { const resource = bucket.arnForObjects('home/', team.groupName, '/', user.userName, '/*'); const p = new iam.PolicyStatement().addResource(resource).addAction('s3:GetObject'); - test.deepEqual(cdk.resolve(p), { + test.deepEqual(bucket.node.resolve(p), { Action: 's3:GetObject', Effect: 'Allow', Resource: { @@ -331,7 +331,7 @@ export = { const stack = new cdk.Stack(undefined, 'MyStack'); const bucket = new s3.Bucket(stack, 'MyBucket'); const bucketRef = bucket.export(); - test.deepEqual(cdk.resolve(bucketRef), { + test.deepEqual(bucket.node.resolve(bucketRef), { bucketArn: { 'Fn::ImportValue': 'MyStack:MyBucketBucketArnE260558C' }, bucketName: { 'Fn::ImportValue': 'MyStack:MyBucketBucketName8A027014' }, bucketDomainName: { 'Fn::ImportValue': 'MyStack:MyBucketDomainNameF76B9A7A' } @@ -343,7 +343,7 @@ export = { const stack = new cdk.Stack(undefined, 'MyStack'); const bucket = new s3.Bucket(stack, 'MyBucket', { encryption: s3.BucketEncryption.Kms }); const bucketRef = bucket.export(); - test.deepEqual(cdk.resolve(bucketRef), { + test.deepEqual(bucket.node.resolve(bucketRef), { bucketArn: { 'Fn::ImportValue': 'MyStack:MyBucketBucketArnE260558C' }, bucketName: { 'Fn::ImportValue': 'MyStack:MyBucketBucketName8A027014' }, bucketDomainName: { 'Fn::ImportValue': 'MyStack:MyBucketDomainNameF76B9A7A' } @@ -363,14 +363,14 @@ export = { const p = new iam.PolicyStatement().addResource(bucket.bucketArn).addAction('s3:ListBucket'); // it is possible to obtain a permission statement for a ref - test.deepEqual(cdk.resolve(p), { + test.deepEqual(bucket.node.resolve(p), { Action: 's3:ListBucket', Effect: 'Allow', Resource: 'arn:aws:s3:::my-bucket' }); test.deepEqual(bucket.bucketArn, bucketArn); - test.deepEqual(cdk.resolve(bucket.bucketName), 'my-bucket'); + test.deepEqual(bucket.node.resolve(bucket.bucketName), 'my-bucket'); test.deepEqual(stack.toCloudFormation(), {}, 'the ref is not a real resource'); test.done(); diff --git a/packages/@aws-cdk/aws-s3/test/test.util.ts b/packages/@aws-cdk/aws-s3/test/test.util.ts index 2f33e12c81a02..11b87854e3551 100644 --- a/packages/@aws-cdk/aws-s3/test/test.util.ts +++ b/packages/@aws-cdk/aws-s3/test/test.util.ts @@ -1,19 +1,20 @@ import cdk = require('@aws-cdk/cdk'); -import { CloudFormationToken } from '@aws-cdk/cdk'; import { Test } from 'nodeunit'; import { parseBucketArn, parseBucketName } from '../lib/util'; export = { parseBucketArn: { 'explicit arn'(test: Test) { + const stack = new cdk.Stack(); const bucketArn = 'my:bucket:arn'; - test.deepEqual(parseBucketArn({ bucketArn }), bucketArn); + test.deepEqual(parseBucketArn(stack, { bucketArn }), bucketArn); test.done(); }, 'produce arn from bucket name'(test: Test) { + const stack = new cdk.Stack(); const bucketName = 'hello'; - test.deepEqual(cdk.resolve(parseBucketArn({ bucketName })), { 'Fn::Join': + test.deepEqual(stack.node.resolve(parseBucketArn(stack, { bucketName })), { 'Fn::Join': [ '', [ 'arn:', { Ref: 'AWS::Partition' }, @@ -22,7 +23,8 @@ export = { }, 'fails if neither arn nor name are provided'(test: Test) { - test.throws(() => parseBucketArn({}), /Cannot determine bucket ARN. At least `bucketArn` or `bucketName` is needed/); + const stack = new cdk.Stack(); + test.throws(() => parseBucketArn(stack, {}), /Cannot determine bucket ARN. At least `bucketArn` or `bucketName` is needed/); test.done(); } }, @@ -30,38 +32,44 @@ export = { parseBucketName: { 'explicit name'(test: Test) { + const stack = new cdk.Stack(); const bucketName = 'foo'; - test.deepEqual(cdk.resolve(parseBucketName({ bucketName })), 'foo'); + test.deepEqual(stack.node.resolve(parseBucketName(stack, { bucketName })), 'foo'); test.done(); }, 'extract bucket name from string arn'(test: Test) { + const stack = new cdk.Stack(); const bucketArn = 'arn:aws:s3:::my-bucket'; - test.deepEqual(cdk.resolve(parseBucketName({ bucketArn })), 'my-bucket'); + test.deepEqual(stack.node.resolve(parseBucketName(stack, { bucketArn })), 'my-bucket'); test.done(); }, 'undefined if cannot extract name from a non-string arn'(test: Test) { - const bucketArn = `arn:aws:s3:::${new CloudFormationToken({ Ref: 'my-bucket' })}`; - test.deepEqual(cdk.resolve(parseBucketName({ bucketArn })), undefined); + const stack = new cdk.Stack(); + const bucketArn = `arn:aws:s3:::${new cdk.Token({ Ref: 'my-bucket' })}`; + test.deepEqual(stack.node.resolve(parseBucketName(stack, { bucketArn })), undefined); test.done(); }, 'fails if arn uses a non "s3" service'(test: Test) { + const stack = new cdk.Stack(); const bucketArn = 'arn:aws:xx:::my-bucket'; - test.throws(() => parseBucketName({ bucketArn }), /Invalid ARN/); + test.throws(() => parseBucketName(stack, { bucketArn }), /Invalid ARN/); test.done(); }, 'fails if ARN has invalid format'(test: Test) { + const stack = new cdk.Stack(); const bucketArn = 'invalid-arn'; - test.throws(() => parseBucketName({ bucketArn }), /ARNs must have at least 6 components/); + test.throws(() => parseBucketName(stack, { bucketArn }), /ARNs must have at least 6 components/); test.done(); }, 'fails if ARN has path'(test: Test) { + const stack = new cdk.Stack(); const bucketArn = 'arn:aws:s3:::my-bucket/path'; - test.throws(() => parseBucketName({ bucketArn }), /Bucket ARN must not contain a path/); + test.throws(() => parseBucketName(stack, { bucketArn }), /Bucket ARN must not contain a path/); test.done(); } }, diff --git a/packages/@aws-cdk/aws-secretsmanager/test/test.secret-string.ts b/packages/@aws-cdk/aws-secretsmanager/test/test.secret-string.ts index 56883bd0f7837..2a0eb8f0afcfd 100644 --- a/packages/@aws-cdk/aws-secretsmanager/test/test.secret-string.ts +++ b/packages/@aws-cdk/aws-secretsmanager/test/test.secret-string.ts @@ -13,7 +13,7 @@ export = { }); // THEN - test.equal(cdk.resolve(ref.value), '{{resolve:secretsmanager:SomeSecret:SecretString:::}}'); + test.equal(ref.node.resolve(ref.value), '{{resolve:secretsmanager:SomeSecret:SecretString:::}}'); test.done(); }, @@ -28,7 +28,7 @@ export = { }); // THEN - test.equal(cdk.resolve(ref.jsonFieldValue('subkey')), '{{resolve:secretsmanager:SomeSecret:SecretString:subkey::}}'); + test.equal(ref.node.resolve(ref.jsonFieldValue('subkey')), '{{resolve:secretsmanager:SomeSecret:SecretString:subkey::}}'); test.done(); }, diff --git a/packages/@aws-cdk/aws-sns/test/test.sns.ts b/packages/@aws-cdk/aws-sns/test/test.sns.ts index ad7f6bdbc5a27..7d9b21872bd2a 100644 --- a/packages/@aws-cdk/aws-sns/test/test.sns.ts +++ b/packages/@aws-cdk/aws-sns/test/test.sns.ts @@ -5,7 +5,6 @@ import lambda = require('@aws-cdk/aws-lambda'); import s3n = require('@aws-cdk/aws-s3-notifications'); import sqs = require('@aws-cdk/aws-sqs'); import cdk = require('@aws-cdk/cdk'); -import { resolve } from '@aws-cdk/cdk'; import { Test } from 'nodeunit'; import sns = require('../lib'); @@ -552,7 +551,7 @@ export = { { "Action": "sns:Publish", "Effect": "Allow", - "Resource": cdk.resolve(topic.topicArn) + "Resource": stack.node.resolve(topic.topicArn) } ], } @@ -692,11 +691,11 @@ export = { "Action": "sqs:SendMessage", "Condition": { "ArnEquals": { - "aws:SourceArn": resolve(imported.topicArn) + "aws:SourceArn": stack2.node.resolve(imported.topicArn) } }, "Principal": { "Service": "sns.amazonaws.com" }, - "Resource": resolve(queue.queueArn), + "Resource": stack2.node.resolve(queue.queueArn), "Effect": "Allow", } ], @@ -715,7 +714,7 @@ export = { const bucketId = 'bucketId'; const dest1 = topic.asBucketNotificationDestination(bucketArn, bucketId); - test.deepEqual(resolve(dest1.arn), resolve(topic.topicArn)); + test.deepEqual(stack.node.resolve(dest1.arn), stack.node.resolve(topic.topicArn)); test.deepEqual(dest1.type, s3n.BucketNotificationDestinationType.Topic); const dep: cdk.Construct = dest1.dependencies![0] as any; @@ -723,12 +722,12 @@ export = { // calling again on the same bucket yields is idempotent const dest2 = topic.asBucketNotificationDestination(bucketArn, bucketId); - test.deepEqual(resolve(dest2.arn), resolve(topic.topicArn)); + test.deepEqual(stack.node.resolve(dest2.arn), stack.node.resolve(topic.topicArn)); test.deepEqual(dest2.type, s3n.BucketNotificationDestinationType.Topic); // another bucket will be added to the topic policy const dest3 = topic.asBucketNotificationDestination('bucket2', 'bucket2'); - test.deepEqual(resolve(dest3.arn), resolve(topic.topicArn)); + test.deepEqual(stack.node.resolve(dest3.arn), stack.node.resolve(topic.topicArn)); test.deepEqual(dest3.type, s3n.BucketNotificationDestinationType.Topic); expect(stack).toMatch({ diff --git a/packages/@aws-cdk/aws-sqs/test/test.sqs.ts b/packages/@aws-cdk/aws-sqs/test/test.sqs.ts index 37e9faff6a5ff..fb2bfcc28883a 100644 --- a/packages/@aws-cdk/aws-sqs/test/test.sqs.ts +++ b/packages/@aws-cdk/aws-sqs/test/test.sqs.ts @@ -2,7 +2,7 @@ import { expect, haveResource } from '@aws-cdk/assert'; import iam = require('@aws-cdk/aws-iam'); import kms = require('@aws-cdk/aws-kms'); import s3 = require('@aws-cdk/aws-s3'); -import { resolve, Stack } from '@aws-cdk/cdk'; +import { Stack } from '@aws-cdk/cdk'; import { Test } from 'nodeunit'; import sqs = require('../lib'); import { Queue } from '../lib'; @@ -103,8 +103,8 @@ export = { // THEN // "import" returns an IQueue bound to `Fn::ImportValue`s. - test.deepEqual(resolve(imports.queueArn), { 'Fn::ImportValue': 'QueueQueueArn8CF496D5' }); - test.deepEqual(resolve(imports.queueUrl), { 'Fn::ImportValue': 'QueueQueueUrlC30FF916' }); + test.deepEqual(stack.node.resolve(imports.queueArn), { 'Fn::ImportValue': 'QueueQueueArn8CF496D5' }); + test.deepEqual(stack.node.resolve(imports.queueUrl), { 'Fn::ImportValue': 'QueueQueueUrlC30FF916' }); // the exporting stack has Outputs for QueueARN and QueueURL const outputs = stack.toCloudFormation().Outputs; @@ -246,7 +246,7 @@ export = { const exportCustom = customKey.export(); - test.deepEqual(resolve(exportCustom), { + test.deepEqual(stack.node.resolve(exportCustom), { queueArn: { 'Fn::ImportValue': 'QueueWithCustomKeyQueueArnD326BB9B' }, queueUrl: { 'Fn::ImportValue': 'QueueWithCustomKeyQueueUrlF07DDC70' }, keyArn: { 'Fn::ImportValue': 'QueueWithCustomKeyKeyArn537F6E42' } @@ -294,7 +294,7 @@ export = { const exportManaged = managedKey.export(); - test.deepEqual(resolve(exportManaged), { + test.deepEqual(stack.node.resolve(exportManaged), { queueArn: { 'Fn::ImportValue': 'QueueWithManagedKeyQueueArn8798A14E' }, queueUrl: { 'Fn::ImportValue': 'QueueWithManagedKeyQueueUrlD735C981' }, keyArn: { 'Fn::ImportValue': 'QueueWithManagedKeyKeyArn9C42A85D' } diff --git a/packages/@aws-cdk/aws-ssm/test/test.parameter-store-string.ts b/packages/@aws-cdk/aws-ssm/test/test.parameter-store-string.ts index 897f8cfc96cfe..5eaa03e8974ce 100644 --- a/packages/@aws-cdk/aws-ssm/test/test.parameter-store-string.ts +++ b/packages/@aws-cdk/aws-ssm/test/test.parameter-store-string.ts @@ -14,7 +14,7 @@ export = { }); // THEN - test.equal(cdk.resolve(ref.value), '{{resolve:ssm:/some/key:123}}'); + test.equal(ref.node.resolve(ref.value), '{{resolve:ssm:/some/key:123}}'); test.done(); }, @@ -30,7 +30,7 @@ export = { }); // THEN - test.equal(cdk.resolve(ref.value), '{{resolve:ssm-secure:/some/key:123}}'); + test.equal(ref.node.resolve(ref.value), '{{resolve:ssm-secure:/some/key:123}}'); test.done(); }, diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/state-machine.ts b/packages/@aws-cdk/aws-stepfunctions/lib/state-machine.ts index 696f5a04c5ddb..cba9fdbd0fb35 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/state-machine.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/state-machine.ts @@ -71,8 +71,9 @@ export class StateMachine extends cdk.Construct implements IStateMachine { constructor(scope: cdk.Construct, id: string, props: StateMachineProps) { super(scope, id); + const stack = cdk.Stack.find(this); this.role = props.role || new iam.Role(this, 'Role', { - assumedBy: new iam.ServicePrincipal(`states.${new cdk.AwsRegion()}.amazonaws.com`), + assumedBy: new iam.ServicePrincipal(`states.${stack.region}.amazonaws.com`), }); const graph = new StateGraph(props.definition.startState, `State Machine ${id} definition`); @@ -81,7 +82,7 @@ export class StateMachine extends cdk.Construct implements IStateMachine { const resource = new CfnStateMachine(this, 'Resource', { stateMachineName: props.stateMachineName, roleArn: this.role.roleArn, - definitionString: cdk.CloudFormationJSON.stringify(graph.toGraphJson()), + definitionString: this.node.stringifyJson(graph.toGraphJson()), }); for (const statement of graph.policyStatements) { diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/states/parallel.ts b/packages/@aws-cdk/aws-stepfunctions/lib/states/parallel.ts index 1833dbf5988b5..22bd2fe03eca4 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/states/parallel.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/states/parallel.ts @@ -104,16 +104,6 @@ export class Parallel extends State implements INextable { return this; } - /** - * Validate this state - */ - public validate(): string[] { - if (this.branches.length === 0) { - return ['Parallel must have at least one branch']; - } - return []; - } - /** * Return the Amazon States Language object for this state */ @@ -128,4 +118,14 @@ export class Parallel extends State implements INextable { ...this.renderBranches(), }; } + + /** + * Validate this state + */ + protected validate(): string[] { + if (this.branches.length === 0) { + return ['Parallel must have at least one branch']; + } + return []; + } } diff --git a/packages/@aws-cdk/aws-stepfunctions/test/test.activity.ts b/packages/@aws-cdk/aws-stepfunctions/test/test.activity.ts index 96f1921d90d6c..d4d15c8dec872 100644 --- a/packages/@aws-cdk/aws-stepfunctions/test/test.activity.ts +++ b/packages/@aws-cdk/aws-stepfunctions/test/test.activity.ts @@ -60,13 +60,13 @@ export = { namespace: 'AWS/States', dimensions: { ActivityArn: { Ref: 'Activity04690B0A' }}, }; - test.deepEqual(cdk.resolve(activity.metricRunTime()), { + test.deepEqual(stack.node.resolve(activity.metricRunTime()), { ...sharedMetric, metricName: 'ActivityRunTime', statistic: 'Average' }); - test.deepEqual(cdk.resolve(activity.metricFailed()), { + test.deepEqual(stack.node.resolve(activity.metricFailed()), { ...sharedMetric, metricName: 'ActivitiesFailed', statistic: 'Sum' diff --git a/packages/@aws-cdk/aws-stepfunctions/test/test.state-machine-resources.ts b/packages/@aws-cdk/aws-stepfunctions/test/test.state-machine-resources.ts index b3adf4581b955..d717524180690 100644 --- a/packages/@aws-cdk/aws-stepfunctions/test/test.state-machine-resources.ts +++ b/packages/@aws-cdk/aws-stepfunctions/test/test.state-machine-resources.ts @@ -79,13 +79,13 @@ export = { namespace: 'AWS/States', dimensions: { ResourceArn: 'resource' }, }; - test.deepEqual(cdk.resolve(task.metricRunTime()), { + test.deepEqual(stack.node.resolve(task.metricRunTime()), { ...sharedMetric, metricName: 'FakeResourceRunTime', statistic: 'Average' }); - test.deepEqual(cdk.resolve(task.metricFailed()), { + test.deepEqual(stack.node.resolve(task.metricFailed()), { ...sharedMetric, metricName: 'FakeResourcesFailed', statistic: 'Sum' diff --git a/packages/@aws-cdk/aws-stepfunctions/test/test.states-language.ts b/packages/@aws-cdk/aws-stepfunctions/test/test.states-language.ts index fd30ca17a1d59..5b15006d36395 100644 --- a/packages/@aws-cdk/aws-stepfunctions/test/test.states-language.ts +++ b/packages/@aws-cdk/aws-stepfunctions/test/test.states-language.ts @@ -729,5 +729,5 @@ class FakeResource implements stepfunctions.IStepFunctionsTaskResource { } function render(sm: stepfunctions.IChainable) { - return cdk.resolve(new stepfunctions.StateGraph(sm.startState, 'Test Graph').toGraphJson()); + return new cdk.Stack().node.resolve(new stepfunctions.StateGraph(sm.startState, 'Test Graph').toGraphJson()); } diff --git a/packages/@aws-cdk/cdk/lib/app.ts b/packages/@aws-cdk/cdk/lib/app.ts index 7cf7fbecc478f..dc38817aafba8 100644 --- a/packages/@aws-cdk/cdk/lib/app.ts +++ b/packages/@aws-cdk/cdk/lib/app.ts @@ -3,7 +3,6 @@ import fs = require('fs'); import path = require('path'); import { Stack } from './cloudformation/stack'; import { IConstruct, MetadataEntry, PATH_SEP, Root } from './core/construct'; -import { resolve } from './core/tokens'; /** * Represents a CDK program. @@ -59,6 +58,8 @@ export class App extends Root { public synthesizeStack(stackName: string): cxapi.SynthesizedStack { const stack = this.getStack(stackName); + this.node.prepareTree(); + // first, validate this stack and stop if there are errors. const errors = stack.node.validateTree(); if (errors.length > 0) { @@ -81,7 +82,8 @@ export class App extends Root { environment, missing, template: stack.toCloudFormation(), - metadata: this.collectMetadata(stack) + metadata: this.collectMetadata(stack), + dependsOn: noEmptyArray(stack.dependencies().map(s => s.node.id)), }; } @@ -114,7 +116,7 @@ export class App extends Root { function visit(node: IConstruct) { if (node.node.metadata.length > 0) { // Make the path absolute - output[PATH_SEP + node.node.path] = node.node.metadata.map(md => resolve(md) as MetadataEntry); + output[PATH_SEP + node.node.path] = node.node.metadata.map(md => node.node.resolve(md) as MetadataEntry); } for (const child of node.node.children) { @@ -226,4 +228,8 @@ function getJsiiAgentVersion() { } return jsiiAgent; +} + +function noEmptyArray(xs: T[]): T[] | undefined { + return xs.length > 0 ? xs : undefined; } \ No newline at end of file diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/arn.ts b/packages/@aws-cdk/cdk/lib/cloudformation/arn.ts index 52e2fb3a563a3..e6b86166c3a01 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/arn.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/arn.ts @@ -1,239 +1,216 @@ -import { AwsAccountId, AwsPartition, AwsRegion } from '..'; import { Fn } from '../cloudformation/fn'; import { unresolved } from '../core/tokens'; +import { Stack } from './stack'; /** - * An Amazon Resource Name (ARN). - * http://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html + * Creates an ARN from components. + * + * If `partition`, `region` or `account` are not specified, the stack's + * partition, region and account will be used. + * + * If any component is the empty string, an empty string will be inserted + * into the generated ARN at the location that component corresponds to. + * + * The ARN will be formatted as follows: + * + * arn:{partition}:{service}:{region}:{account}:{resource}{sep}}{resource-name} + * + * The required ARN pieces that are omitted will be taken from the stack that + * the 'scope' is attached to. If all ARN pieces are supplied, the supplied scope + * can be 'undefined'. */ -export class ArnUtils { - /** - * Creates an ARN from components. - * - * If `partition`, `region` or `account` are not specified, the stack's - * partition, region and account will be used. - * - * If any component is the empty string, an empty string will be inserted - * into the generated ARN at the location that component corresponds to. - * - * The ARN will be formatted as follows: - * - * arn:{partition}:{service}:{region}:{account}:{resource}{sep}}{resource-name} - * - */ - public static fromComponents(components: ArnComponents): string { - const partition = components.partition == null - ? new AwsPartition() - : components.partition; - const region = components.region == null - ? new AwsRegion() - : components.region; - const account = components.account == null - ? new AwsAccountId() - : components.account; - - const values = [ 'arn', ':', partition, ':', components.service, ':', region, ':', account, ':', components.resource ]; - - const sep = components.sep || '/'; - if (sep !== '/' && sep !== ':') { - throw new Error('resourcePathSep may only be ":" or "/"'); - } +export function arnFromComponents(components: ArnComponents, stack: Stack): string { + const partition = components.partition !== undefined ? components.partition : stack.partition; + const region = components.region !== undefined ? components.region : stack.region; + const account = components.account !== undefined ? components.account : stack.accountId; - if (components.resourceName != null) { - values.push(sep); - values.push(components.resourceName); - } + const values = [ 'arn', ':', partition, ':', components.service, ':', region, ':', account, ':', components.resource ]; - return values.join(''); + const sep = components.sep || '/'; + if (sep !== '/' && sep !== ':') { + throw new Error('resourcePathSep may only be ":" or "/"'); } - /** - * Given an ARN, parses it and returns components. - * - * If the ARN is a concrete string, it will be parsed and validated. The - * separator (`sep`) will be set to '/' if the 6th component includes a '/', - * in which case, `resource` will be set to the value before the '/' and - * `resourceName` will be the rest. In case there is no '/', `resource` will - * be set to the 6th components and `resourceName` will be set to the rest - * of the string. - * - * If the ARN includes tokens (or is a token), the ARN cannot be validated, - * since we don't have the actual value yet at the time of this function - * call. You will have to know the separator and the type of ARN. The - * resulting `ArnComponents` object will contain tokens for the - * subexpressions of the ARN, not string literals. In this case this - * function cannot properly parse the complete final resourceName (path) out - * of ARNs that use '/' to both separate the 'resource' from the - * 'resourceName' AND to subdivide the resourceName further. For example, in - * S3 ARNs: - * - * arn:aws:s3:::my_corporate_bucket/path/to/exampleobject.png - * - * After parsing the resourceName will not contain - * 'path/to/exampleobject.png' but simply 'path'. This is a limitation - * because there is no slicing functionality in CloudFormation templates. - * - * @param sep The separator used to separate resource from resourceName - * @param hasName Whether there is a name component in the ARN at all. For - * example, SNS Topics ARNs have the 'resource' component contain the topic - * name, and no 'resourceName' component. - * - * @returns an ArnComponents object which allows access to the various - * components of the ARN. - * - * @returns an ArnComponents object which allows access to the various - * components of the ARN. - */ - public static parse(arn: string, sepIfToken: string = '/', hasName: boolean = true): ArnComponents { - if (unresolved(arn)) { - return ArnUtils.parseToken(arn, sepIfToken, hasName); - } + if (components.resourceName != null) { + values.push(sep); + values.push(components.resourceName); + } - const components = arn.split(':') as Array; + return values.join(''); +} - if (components.length < 6) { - throw new Error('ARNs must have at least 6 components: ' + arn); - } +/** + * Given an ARN, parses it and returns components. + * + * If the ARN is a concrete string, it will be parsed and validated. The + * separator (`sep`) will be set to '/' if the 6th component includes a '/', + * in which case, `resource` will be set to the value before the '/' and + * `resourceName` will be the rest. In case there is no '/', `resource` will + * be set to the 6th components and `resourceName` will be set to the rest + * of the string. + * + * If the ARN includes tokens (or is a token), the ARN cannot be validated, + * since we don't have the actual value yet at the time of this function + * call. You will have to know the separator and the type of ARN. The + * resulting `ArnComponents` object will contain tokens for the + * subexpressions of the ARN, not string literals. In this case this + * function cannot properly parse the complete final resourceName (path) out + * of ARNs that use '/' to both separate the 'resource' from the + * 'resourceName' AND to subdivide the resourceName further. For example, in + * S3 ARNs: + * + * arn:aws:s3:::my_corporate_bucket/path/to/exampleobject.png + * + * After parsing the resourceName will not contain + * 'path/to/exampleobject.png' but simply 'path'. This is a limitation + * because there is no slicing functionality in CloudFormation templates. + * + * @param sep The separator used to separate resource from resourceName + * @param hasName Whether there is a name component in the ARN at all. For + * example, SNS Topics ARNs have the 'resource' component contain the topic + * name, and no 'resourceName' component. + * + * @returns an ArnComponents object which allows access to the various + * components of the ARN. + * + * @returns an ArnComponents object which allows access to the various + * components of the ARN. + */ +export function parseArn(arn: string, sepIfToken: string = '/', hasName: boolean = true): ArnComponents { + if (unresolved(arn)) { + return parseToken(arn, sepIfToken, hasName); + } - const [ arnPrefix, partition, service, region, account, sixth, ...rest ] = components; + const components = arn.split(':') as Array; - if (arnPrefix !== 'arn') { - throw new Error('ARNs must start with "arn:": ' + arn); - } + if (components.length < 6) { + throw new Error('ARNs must have at least 6 components: ' + arn); + } - if (!service) { - throw new Error('The `service` component (3rd component) is required: ' + arn); - } + const [ arnPrefix, partition, service, region, account, sixth, ...rest ] = components; - if (!sixth) { - throw new Error('The `resource` component (6th component) is required: ' + arn); - } + if (arnPrefix !== 'arn') { + throw new Error('ARNs must start with "arn:": ' + arn); + } - let resource: string; - let resourceName: string | undefined; - let sep: string | undefined; + if (!service) { + throw new Error('The `service` component (3rd component) is required: ' + arn); + } - let sepIndex = sixth.indexOf('/'); - if (sepIndex !== -1) { - sep = '/'; - } else if (rest.length > 0) { - sep = ':'; - sepIndex = -1; - } + if (!sixth) { + throw new Error('The `resource` component (6th component) is required: ' + arn); + } - if (sepIndex !== -1) { - resource = sixth.substr(0, sepIndex); - resourceName = sixth.substr(sepIndex + 1); - } else { - resource = sixth; - } + let resource: string; + let resourceName: string | undefined; + let sep: string | undefined; - if (rest.length > 0) { - if (!resourceName) { - resourceName = ''; - } else { - resourceName += ':'; - } + let sepIndex = sixth.indexOf('/'); + if (sepIndex !== -1) { + sep = '/'; + } else if (rest.length > 0) { + sep = ':'; + sepIndex = -1; + } - resourceName += rest.join(':'); - } + if (sepIndex !== -1) { + resource = sixth.substr(0, sepIndex); + resourceName = sixth.substr(sepIndex + 1); + } else { + resource = sixth; + } - const result: ArnComponents = { service, resource }; - if (partition) { - result.partition = partition; + if (rest.length > 0) { + if (!resourceName) { + resourceName = ''; + } else { + resourceName += ':'; } - if (region) { - result.region = region; - } + resourceName += rest.join(':'); + } - if (account) { - result.account = account; - } + const result: ArnComponents = { service, resource }; + if (partition) { + result.partition = partition; + } - if (resourceName) { - result.resourceName = resourceName; - } + if (region) { + result.region = region; + } - if (sep) { - result.sep = sep; - } + if (account) { + result.account = account; + } - return result; + if (resourceName) { + result.resourceName = resourceName; } - /** - * Given a Token evaluating to ARN, parses it and returns components. - * - * The ARN cannot be validated, since we don't have the actual value yet - * at the time of this function call. You will have to know the separator - * and the type of ARN. - * - * The resulting `ArnComponents` object will contain tokens for the - * subexpressions of the ARN, not string literals. - * - * WARNING: this function cannot properly parse the complete final - * resourceName (path) out of ARNs that use '/' to both separate the - * 'resource' from the 'resourceName' AND to subdivide the resourceName - * further. For example, in S3 ARNs: - * - * arn:aws:s3:::my_corporate_bucket/path/to/exampleobject.png - * - * After parsing the resourceName will not contain 'path/to/exampleobject.png' - * but simply 'path'. This is a limitation because there is no slicing - * functionality in CloudFormation templates. - * - * @param arnToken The input token that contains an ARN - * @param sep The separator used to separate resource from resourceName - * @param hasName Whether there is a name component in the ARN at all. - * For example, SNS Topics ARNs have the 'resource' component contain the - * topic name, and no 'resourceName' component. - * @returns an ArnComponents object which allows access to the various - * components of the ARN. - */ - public static parseToken(arnToken: string, sep: string = '/', hasName: boolean = true): ArnComponents { - // Arn ARN looks like: - // arn:partition:service:region:account-id:resource - // arn:partition:service:region:account-id:resourcetype/resource - // arn:partition:service:region:account-id:resourcetype:resource + if (sep) { + result.sep = sep; + } - // We need the 'hasName' argument because {Fn::Select}ing a nonexistent field - // throws an error. + return result; +} - const components = Fn.split(':', arnToken); +/** + * Given a Token evaluating to ARN, parses it and returns components. + * + * The ARN cannot be validated, since we don't have the actual value yet + * at the time of this function call. You will have to know the separator + * and the type of ARN. + * + * The resulting `ArnComponents` object will contain tokens for the + * subexpressions of the ARN, not string literals. + * + * WARNING: this function cannot properly parse the complete final + * resourceName (path) out of ARNs that use '/' to both separate the + * 'resource' from the 'resourceName' AND to subdivide the resourceName + * further. For example, in S3 ARNs: + * + * arn:aws:s3:::my_corporate_bucket/path/to/exampleobject.png + * + * After parsing the resourceName will not contain 'path/to/exampleobject.png' + * but simply 'path'. This is a limitation because there is no slicing + * functionality in CloudFormation templates. + * + * @param arnToken The input token that contains an ARN + * @param sep The separator used to separate resource from resourceName + * @param hasName Whether there is a name component in the ARN at all. + * For example, SNS Topics ARNs have the 'resource' component contain the + * topic name, and no 'resourceName' component. + * @returns an ArnComponents object which allows access to the various + * components of the ARN. + */ +function parseToken(arnToken: string, sep: string = '/', hasName: boolean = true): ArnComponents { + // Arn ARN looks like: + // arn:partition:service:region:account-id:resource + // arn:partition:service:region:account-id:resourcetype/resource + // arn:partition:service:region:account-id:resourcetype:resource - const partition = Fn.select(1, components).toString(); - const service = Fn.select(2, components).toString(); - const region = Fn.select(3, components).toString(); - const account = Fn.select(4, components).toString(); + // We need the 'hasName' argument because {Fn::Select}ing a nonexistent field + // throws an error. - if (sep === ':') { - const resource = Fn.select(5, components).toString(); - const resourceName = hasName ? Fn.select(6, components).toString() : undefined; + const components = Fn.split(':', arnToken); - return { partition, service, region, account, resource, resourceName, sep }; - } else { - const lastComponents = Fn.split(sep, Fn.select(5, components)); + const partition = Fn.select(1, components).toString(); + const service = Fn.select(2, components).toString(); + const region = Fn.select(3, components).toString(); + const account = Fn.select(4, components).toString(); - const resource = Fn.select(0, lastComponents).toString(); - const resourceName = hasName ? Fn.select(1, lastComponents).toString() : undefined; + if (sep === ':') { + const resource = Fn.select(5, components).toString(); + const resourceName = hasName ? Fn.select(6, components).toString() : undefined; - return { partition, service, region, account, resource, resourceName, sep }; - } - } + return { partition, service, region, account, resource, resourceName, sep }; + } else { + const lastComponents = Fn.split(sep, Fn.select(5, components)); - /** - * Return a Token that represents the resource component of the ARN - */ - public static resourceComponent(arn: string, sep: string = '/'): string { - return ArnUtils.parseToken(arn, sep).resource; - } + const resource = Fn.select(0, lastComponents).toString(); + const resourceName = hasName ? Fn.select(1, lastComponents).toString() : undefined; - /** - * Return a Token that represents the resource Name component of the ARN - */ - public static resourceNameComponent(arn: string, sep: string = '/'): string { - return ArnUtils.parseToken(arn, sep, true).resourceName!; + return { partition, service, region, account, resource, resourceName, sep }; } } diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/cfn-tokens.ts b/packages/@aws-cdk/cdk/lib/cloudformation/cfn-tokens.ts new file mode 100644 index 0000000000000..44e184bbbc8ee --- /dev/null +++ b/packages/@aws-cdk/cdk/lib/cloudformation/cfn-tokens.ts @@ -0,0 +1,112 @@ +import { ResolveContext, Token } from "../core/tokens"; + +/** + * A Token that represents a CloudFormation reference to another resource + * + * If these references are used in a different stack from where they are + * defined, appropriate CloudFormation `Export`s and `Fn::ImportValue`s will be + * synthesized automatically instead of the regular CloudFormation references. + * + * Additionally, the dependency between the stacks will be recorded, and the toolkit + * will make sure to deploy producing stack before the consuming stack. + * + * This magic happens in the prepare() phase, where consuming stacks will call + * `consumeFromStack` on these Tokens and if they happen to be exported by a different + * Stack, we'll register the dependency. + */ +export class CfnReference extends Token { + /** + * Check whether this is actually a CfnReference + */ + public static isInstance(x: Token): x is CfnReference { + return (x as any).consumeFromStack !== undefined; + } + + public readonly isReference?: boolean; + + /** + * What stack this Token is pointing to + */ + private readonly producingStack?: Stack; + + /** + * The Tokens that should be returned for each consuming stack (as decided by the producing Stack) + */ + private readonly replacementTokens: Map; + + constructor(value: any, displayName?: string, scope?: Construct) { + if (typeof(value) === 'function') { + throw new Error('CfnReference can only hold CloudFormation intrinsics (not a function)'); + } + super(value, displayName); + this.replacementTokens = new Map(); + this.isReference = true; + + if (scope !== undefined) { + this.producingStack = Stack.find(scope); + } + } + + public resolve(context: ResolveContext): any { + // If we have a special token for this consuming stack, resolve that. Otherwise resolve as if + // we are in the same stack. + const token = this.replacementTokens.get(Stack.find(context.scope)); + if (token) { + return token.resolve(context); + } else { + return super.resolve(context); + } + } + + /** + * Register a stack this references is being consumed from. + */ + public consumeFromStack(consumingStack: Stack) { + if (this.producingStack && this.producingStack !== consumingStack && !this.replacementTokens.has(consumingStack)) { + // We're trying to resolve a cross-stack reference + consumingStack.addDependency(this.producingStack); + this.replacementTokens.set(consumingStack, this.exportValue(this, consumingStack)); + } + } + + /** + * Export a Token value for use in another stack + * + * Works by mutating the producing stack in-place. + */ + private exportValue(tokenValue: Token, consumingStack: Stack): Token { + const producingStack = this.producingStack!; + + if (producingStack.env.account !== consumingStack.env.account || producingStack.env.region !== consumingStack.env.region) { + throw new Error('Can only reference cross stacks in the same region and account.'); + } + + // Ensure a singleton "Exports" scoping Construct + // This mostly exists to trigger LogicalID munging, which would be + // disabled if we parented constructs directly under Stack. + // Also it nicely prevents likely construct name clashes + + const exportsName = 'Exports'; + let stackExports = producingStack.node.tryFindChild(exportsName) as Construct; + if (stackExports === undefined) { + stackExports = new Construct(producingStack, exportsName); + } + + // Ensure a singleton Output for this value + const resolved = producingStack.node.resolve(tokenValue); + const id = 'Output' + JSON.stringify(resolved); + let output = stackExports.node.tryFindChild(id) as Output; + if (!output) { + output = new Output(stackExports, id, { value: tokenValue }); + } + + // We want to return an actual FnImportValue Token here, but Fn.importValue() returns a 'string', + // so construct one in-place. + return new Token({ 'Fn::ImportValue': output.export }); + } + +} + +import { Construct } from "../core/construct"; +import { Output } from "./output"; +import { Stack } from "./stack"; diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/cloudformation-json.ts b/packages/@aws-cdk/cdk/lib/cloudformation/cloudformation-json.ts index 670a974a1b224..62d5333397f49 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/cloudformation-json.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/cloudformation-json.ts @@ -1,5 +1,7 @@ -import { resolve, Token } from "../core/tokens"; -import { CloudFormationToken, isIntrinsic } from "./cloudformation-token"; +import { IConstruct } from "../core/construct"; +import { Token } from "../core/tokens"; +import { resolve } from "../core/tokens/resolve"; +import { isIntrinsic } from "./instrinsics"; /** * Class for JSON routines that are framework-aware @@ -14,8 +16,11 @@ export class CloudFormationJSON { * * All Tokens substituted in this way must return strings, or the evaluation * in CloudFormation will fail. + * + * @param obj The object to stringify + * @param context The Construct from which to resolve any Tokens found in the object */ - public static stringify(obj: any): string { + public static stringify(obj: any, context: IConstruct): string { return new Token(() => { // Resolve inner value first so that if they evaluate to literals, we // maintain the type (and discard 'undefined's). @@ -26,7 +31,10 @@ export class CloudFormationJSON { // deep-escapes any strings inside the intrinsic, so that if literal // strings are used in {Fn::Join} or something, they will end up // escaped in the final JSON output. - const resolved = resolve(obj); + const resolved = resolve(obj, { + scope: context, + prefix: [] + }); // We can just directly return this value, since resolve() will be called // on our return value anyway. @@ -65,7 +73,7 @@ export class CloudFormationJSON { /** * Token that also stringifies in the toJSON() operation. */ -class IntrinsicToken extends CloudFormationToken { +class IntrinsicToken extends Token { /** * Special handler that gets called when JSON.stringify() is used. */ diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/cloudformation-token.ts b/packages/@aws-cdk/cdk/lib/cloudformation/cloudformation-token.ts deleted file mode 100644 index 467de44968370..0000000000000 --- a/packages/@aws-cdk/cdk/lib/cloudformation/cloudformation-token.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { resolve, Token, unresolved } from "../core/tokens"; - -/** - * Base class for CloudFormation built-ins - */ -export class CloudFormationToken extends Token { - public concat(left: any | undefined, right: any | undefined): Token { - const parts = new Array(); - if (left !== undefined) { parts.push(left); } - parts.push(resolve(this)); - if (right !== undefined) { parts.push(right); } - return new FnJoin('', parts); - } -} - -/** - * Return whether the given value represents a CloudFormation intrinsic - */ -export function isIntrinsic(x: any) { - if (Array.isArray(x) || x === null || typeof x !== 'object') { return false; } - - const keys = Object.keys(x); - if (keys.length !== 1) { return false; } - - return keys[0] === 'Ref' || keys[0].startsWith('Fn::'); -} - -/** - * The intrinsic function ``Fn::Join`` appends a set of values into a single value, separated by - * the specified delimiter. If a delimiter is the empty string, the set of values are concatenated - * with no delimiter. - */ -export class FnJoin extends CloudFormationToken { - private readonly delimiter: string; - private readonly listOfValues: any[]; - // Cache for the result of resolveValues() - since it otherwise would be computed several times - private _resolvedValues?: any[]; - private canOptimize: boolean; - - /** - * Creates an ``Fn::Join`` function. - * @param delimiter The value you want to occur between fragments. The delimiter will occur between fragments only. - * It will not terminate the final value. - * @param listOfValues The list of values you want combined. - */ - constructor(delimiter: string, listOfValues: any[]) { - if (listOfValues.length === 0) { - throw new Error(`FnJoin requires at least one value to be provided`); - } - // Passing the values as a token, optimization requires resolving stringified tokens, we should be deferred until - // this token is itself being resolved. - super({ 'Fn::Join': [ delimiter, new Token(() => this.resolveValues()) ] }); - this.delimiter = delimiter; - this.listOfValues = listOfValues; - this.canOptimize = true; - } - - public resolve(): any { - const resolved = this.resolveValues(); - if (this.canOptimize && resolved.length === 1) { - return resolved[0]; - } - return super.resolve(); - } - - /** - * Optimization: if an Fn::Join is nested in another one and they share the same delimiter, then flatten it up. Also, - * if two concatenated elements are literal strings (not tokens), then pre-concatenate them with the delimiter, to - * generate shorter output. - */ - private resolveValues() { - if (this._resolvedValues) { return this._resolvedValues; } - - if (unresolved(this.listOfValues)) { - // This is a list token, don't resolve and also don't optimize. - this.canOptimize = false; - return this._resolvedValues = this.listOfValues; - } - - const resolvedValues = [...this.listOfValues.map(e => resolve(e))]; - let i = 0; - while (i < resolvedValues.length) { - const el = resolvedValues[i]; - if (isFnJoinIntrinsicWithSameDelimiter.call(this, el)) { - resolvedValues.splice(i, 1, ...el['Fn::Join'][1]); - } else if (i > 0 && isPlainString(resolvedValues[i - 1]) && isPlainString(resolvedValues[i])) { - resolvedValues[i - 1] += this.delimiter + resolvedValues[i]; - resolvedValues.splice(i, 1); - } else { - i += 1; - } - } - - return this._resolvedValues = resolvedValues; - - function isFnJoinIntrinsicWithSameDelimiter(this: FnJoin, obj: any): boolean { - return isIntrinsic(obj) - && Object.keys(obj)[0] === 'Fn::Join' - && obj['Fn::Join'][0] === this.delimiter; - } - - function isPlainString(obj: any): boolean { - return typeof obj === 'string' && !unresolved(obj); - } - } -} diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/condition.ts b/packages/@aws-cdk/cdk/lib/cloudformation/condition.ts index 465836420c200..95f4aab398b1d 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/condition.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/condition.ts @@ -1,5 +1,6 @@ import { Construct } from '../core/construct'; -import { Referenceable } from './stack'; +import { ResolveContext } from '../core/tokens'; +import { Referenceable } from './stack-element'; export interface ConditionProps { expression?: IConditionExpression; @@ -39,7 +40,7 @@ export class Condition extends Referenceable implements IConditionExpression { /** * Synthesizes the condition. */ - public resolve(): any { + public resolve(_context: ResolveContext): any { return { Condition: this.logicalId }; } } @@ -72,5 +73,5 @@ export interface IConditionExpression { /** * Returns a JSON node that represents this condition expression */ - resolve(): any; -} \ No newline at end of file + resolve(context: ResolveContext): any; +} diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/fn.ts b/packages/@aws-cdk/cdk/lib/cloudformation/fn.ts index 5ee86ce3b7d81..0a28c09873ee8 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/fn.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/fn.ts @@ -1,5 +1,7 @@ -import { CloudFormationToken, FnJoin } from './cloudformation-token'; +import { ResolveContext, Token, unresolved } from '../core/tokens'; +import { resolve } from '../core/tokens/resolve'; import { IConditionExpression } from './condition'; +import { minimalCloudFormationJoin } from './instrinsics'; // tslint:disable:max-line-length @@ -19,7 +21,7 @@ export class Fn { * attributes available for that resource type. * @returns a CloudFormationToken object */ - public static getAtt(logicalNameOfResource: string, attributeName: string): CloudFormationToken { + public static getAtt(logicalNameOfResource: string, attributeName: string): Token { return new FnGetAtt(logicalNameOfResource, attributeName); } @@ -285,7 +287,7 @@ export class Fn { /** * Base class for tokens that represent CloudFormation intrinsic functions. */ -class FnBase extends CloudFormationToken { +class FnBase extends Token { constructor(name: string, value: any) { super({ [name]: value }); } @@ -442,7 +444,7 @@ class FnCidr extends FnBase { } } -class FnConditionBase extends CloudFormationToken implements IConditionExpression { +class FnConditionBase extends Token implements IConditionExpression { constructor(type: string, value: any) { super({ [type]: value }); } @@ -607,3 +609,55 @@ class FnValueOfAll extends FnBase { super('Fn::ValueOfAll', [ parameterType, attribute ]); } } + +/** + * The intrinsic function ``Fn::Join`` appends a set of values into a single value, separated by + * the specified delimiter. If a delimiter is the empty string, the set of values are concatenated + * with no delimiter. + */ +class FnJoin extends Token { + private readonly delimiter: string; + private readonly listOfValues: any[]; + // Cache for the result of resolveValues() - since it otherwise would be computed several times + private _resolvedValues?: any[]; + + /** + * Creates an ``Fn::Join`` function. + * @param delimiter The value you want to occur between fragments. The delimiter will occur between fragments only. + * It will not terminate the final value. + * @param listOfValues The list of values you want combined. + */ + constructor(delimiter: string, listOfValues: any[]) { + if (listOfValues.length === 0) { + throw new Error(`FnJoin requires at least one value to be provided`); + } + super(); + + this.delimiter = delimiter; + this.listOfValues = listOfValues; + } + + public resolve(context: ResolveContext): any { + if (unresolved(this.listOfValues)) { + // This is a list token, don't try to do smart things with it. + return { 'Fn::Join': [ this.delimiter, this.listOfValues ] }; + } + const resolved = this.resolveValues(context); + if (resolved.length === 1) { + return resolved[0]; + } + return { 'Fn::Join': [ this.delimiter, resolved ] }; + } + + /** + * Optimization: if an Fn::Join is nested in another one and they share the same delimiter, then flatten it up. Also, + * if two concatenated elements are literal strings (not tokens), then pre-concatenate them with the delimiter, to + * generate shorter output. + */ + private resolveValues(context: ResolveContext) { + if (this._resolvedValues) { return this._resolvedValues; } + + const resolvedValues = this.listOfValues.map(e => resolve(e, context)); + return this._resolvedValues = minimalCloudFormationJoin(this.delimiter, resolvedValues); + } +} diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/include.ts b/packages/@aws-cdk/cdk/lib/cloudformation/include.ts index ba307cd58b475..a938fe87a58ba 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/include.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/include.ts @@ -1,5 +1,5 @@ import { Construct } from '../core/construct'; -import { StackElement } from './stack'; +import { StackElement } from './stack-element'; export interface IncludeProps { /** diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/instrinsics.ts b/packages/@aws-cdk/cdk/lib/cloudformation/instrinsics.ts new file mode 100644 index 0000000000000..af2094cc5894d --- /dev/null +++ b/packages/@aws-cdk/cdk/lib/cloudformation/instrinsics.ts @@ -0,0 +1,50 @@ +/** + * Do an intelligent CloudFormation join on the given values, producing a minimal expression + */ +export function minimalCloudFormationJoin(delimiter: string, values: any[]): any[] { + let i = 0; + while (i < values.length) { + const el = values[i]; + if (isSplicableFnJoinInstrinsic(el)) { + values.splice(i, 1, ...el['Fn::Join'][1]); + } else if (i > 0 && isPlainString(values[i - 1]) && isPlainString(values[i])) { + values[i - 1] += delimiter + values[i]; + values.splice(i, 1); + } else { + i += 1; + } + } + + return values; + + function isPlainString(obj: any): boolean { + return typeof obj === 'string' && !unresolved(obj); + } + + function isSplicableFnJoinInstrinsic(obj: any): boolean { + return isIntrinsic(obj) + && Object.keys(obj)[0] === 'Fn::Join' + && obj['Fn::Join'][0] === delimiter; + } +} + +/** + * Return whether the given value represents a CloudFormation intrinsic + */ +export function isIntrinsic(x: any) { + if (Array.isArray(x) || x === null || typeof x !== 'object') { return false; } + + const keys = Object.keys(x); + if (keys.length !== 1) { return false; } + + return keys[0] === 'Ref' || keys[0].startsWith('Fn::'); +} + +/** + * Return whether this is an intrinsic that could potentially (or definitely) evaluate to a list + */ +export function canEvaluateToList(x: any) { + return isIntrinsic(x) && ['Ref', 'Fn::GetAtt', 'Fn::GetAZs', 'Fn::Split', 'Fn::FindInMap', 'Fn::ImportValue'].includes(Object.keys(x)[0]); +} + +import { unresolved } from "../core/tokens/unresolved"; diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/logical-id.ts b/packages/@aws-cdk/cdk/lib/cloudformation/logical-id.ts index 54d3465cd2dba..5f858f774efd8 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/logical-id.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/logical-id.ts @@ -1,5 +1,5 @@ import { makeUniqueId } from '../util/uniqueid'; -import { StackElement } from './stack'; +import { StackElement } from './stack-element'; const PATH_SEP = '/'; diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/mapping.ts b/packages/@aws-cdk/cdk/lib/cloudformation/mapping.ts index 65f4025bdd2a3..5446b6977f49b 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/mapping.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/mapping.ts @@ -1,6 +1,6 @@ import { Construct } from '../core/construct'; import { Fn } from './fn'; -import { Referenceable } from './stack'; +import { Referenceable } from './stack-element'; export interface MappingProps { mapping?: { [k1: string]: { [k2: string]: any } }; diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/output.ts b/packages/@aws-cdk/cdk/lib/cloudformation/output.ts index bacb463f17dc1..3ac13d52a34c4 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/output.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/output.ts @@ -1,7 +1,5 @@ import { Construct } from '../core/construct'; -import { Condition } from './condition'; -import { Fn } from './fn'; -import { Stack, StackElement } from './stack'; +import { StackElement } from './stack-element'; export interface OutputProps { /** @@ -51,13 +49,6 @@ export class Output extends StackElement { */ public readonly description?: string; - /** - * The value of the property returned by the aws cloudformation describe-stacks command. - * The value of an output can include literals, parameter references, pseudo-parameters, - * a mapping value, or intrinsic functions. - */ - public readonly value?: any; - /** * The name of the resource output to be exported for a cross-stack reference. * By default, the logical ID of the Output element is used as it's export name. @@ -71,6 +62,8 @@ export class Output extends StackElement { */ public readonly condition?: Condition; + private _value?: any; + /** * Creates an Output value for this stack. * @param parent The parent construct. @@ -80,7 +73,7 @@ export class Output extends StackElement { super(scope, id); this.description = props.description; - this.value = props.value; + this._value = props.value; this.condition = props.condition; if (props.export) { @@ -90,12 +83,21 @@ export class Output extends StackElement { this.export = props.export; } else if (!props.disableExport) { // prefix export name with stack name since exports are global within account + region. - const stackName = Stack.find(this).node.id; + const stackName = require('./stack').Stack.find(this).node.id; this.export = stackName ? stackName + ':' : ''; this.export += this.logicalId; } } + /** + * The value of the property returned by the aws cloudformation describe-stacks command. + * The value of an output can include literals, parameter references, pseudo-parameters, + * a mapping value, or intrinsic functions. + */ + public get value(): any { + return this._value; + } + /** * Returns an FnImportValue bound to this export name. */ @@ -103,7 +105,7 @@ export class Output extends StackElement { if (!this.export) { throw new Error('Cannot create an ImportValue without an export name'); } - return Fn.importValue(this.export); + return fn().importValue(this.export); } public toCloudFormation(): object { @@ -206,7 +208,7 @@ export class StringListOutput extends Construct { condition: props.condition, disableExport: props.disableExport, export: props.export, - value: Fn.join(this.separator, props.values) + value: fn().join(this.separator, props.values) }); } @@ -218,9 +220,16 @@ export class StringListOutput extends Construct { const ret = []; for (let i = 0; i < this.length; i++) { - ret.push(Fn.select(i, Fn.split(this.separator, combined))); + ret.push(fn().select(i, fn().split(this.separator, combined))); } return ret; } } + +function fn() { + // Lazy loading of "Fn" module to break dependency cycles on startup + return require('./fn').Fn; +} + +import { Condition } from './condition'; \ No newline at end of file diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/parameter.ts b/packages/@aws-cdk/cdk/lib/cloudformation/parameter.ts index 0a22b6779cf74..6931baa629760 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/parameter.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/parameter.ts @@ -1,6 +1,6 @@ import { Construct } from '../core/construct'; import { Token } from '../core/tokens'; -import { Ref, Referenceable } from './stack'; +import { Ref, Referenceable } from './stack-element'; export interface ParameterProps { /** diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/pseudo.ts b/packages/@aws-cdk/cdk/lib/cloudformation/pseudo.ts index e576320655ec2..e85ac027ad082 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/pseudo.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/pseudo.ts @@ -1,61 +1,101 @@ -import { CloudFormationToken } from './cloudformation-token'; +import { Construct } from '../core/construct'; +import { Token } from '../core/tokens'; +import { CfnReference } from './cfn-tokens'; -export class PseudoParameter extends CloudFormationToken { - constructor(name: string) { - super({ Ref: name }, name); +/** + * Accessor for pseudo parameters + * + * Since pseudo parameters need to be anchored to a stack somewhere in the + * construct tree, this class takes an scope parameter; the pseudo parameter + * values can be obtained as properties from an scoped object. + */ +export class Aws { + constructor(private readonly scope?: Construct) { } -} -export class AwsAccountId extends PseudoParameter { - constructor() { - super('AWS::AccountId'); + public get accountId(): string { + return new AwsAccountId(this.scope).toString(); + } + + public get urlSuffix(): string { + return new AwsURLSuffix(this.scope).toString(); + } + + public get notificationArns(): string[] { + return new AwsNotificationARNs(this.scope).toList(); + } + + public get partition(): string { + return new AwsPartition(this.scope).toString(); + } + + public get region(): string { + return new AwsRegion(this.scope).toString(); + } + + public get stackId(): string { + return new AwsStackId(this.scope).toString(); + } + + public get stackName(): string { + return new AwsStackName(this.scope).toString(); + } + + public get noValue(): string { + return new AwsNoValue().toString(); } } -export class AwsDomainSuffix extends PseudoParameter { - constructor() { - super('AWS::DomainSuffix'); +class PseudoParameter extends CfnReference { + constructor(name: string, scope: Construct | undefined) { + super({ Ref: name }, name, scope); } } -export class AwsURLSuffix extends PseudoParameter { - constructor() { - super('AWS::URLSuffix'); +class AwsAccountId extends PseudoParameter { + constructor(scope: Construct | undefined) { + super('AWS::AccountId', scope); } } -export class AwsNotificationARNs extends PseudoParameter { - constructor() { - super('AWS::NotificationARNs'); +class AwsURLSuffix extends PseudoParameter { + constructor(scope: Construct | undefined) { + super('AWS::URLSuffix', scope); } } -export class AwsNoValue extends PseudoParameter { - constructor() { - super('AWS::NoValue'); +class AwsNotificationARNs extends PseudoParameter { + constructor(scope: Construct | undefined) { + super('AWS::NotificationARNs', scope); } } -export class AwsPartition extends PseudoParameter { +export class AwsNoValue extends Token { constructor() { - super('AWS::Partition'); + super({ Ref: 'AWS::NoValue' }); } } -export class AwsRegion extends PseudoParameter { - constructor() { - super('AWS::Region'); +class AwsPartition extends PseudoParameter { + constructor(scope: Construct | undefined) { + super('AWS::Partition', scope); } } -export class AwsStackId extends PseudoParameter { - constructor() { - super('AWS::StackId'); +class AwsRegion extends PseudoParameter { + constructor(scope: Construct | undefined) { + super('AWS::Region', scope); } } -export class AwsStackName extends PseudoParameter { - constructor() { - super('AWS::StackName'); +class AwsStackId extends PseudoParameter { + constructor(scope: Construct | undefined) { + super('AWS::StackId', scope); } } + +class AwsStackName extends PseudoParameter { + constructor(scope: Construct | undefined) { + super('AWS::StackName', scope); + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/resource.ts b/packages/@aws-cdk/cdk/lib/cloudformation/resource.ts index 92cdd25eeb99e..a2a4243dde2ac 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/resource.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/resource.ts @@ -1,10 +1,10 @@ import cxapi = require('@aws-cdk/cx-api'); import { Construct } from '../core/construct'; import { capitalizePropertyNames, ignoreEmpty } from '../core/util'; -import { CloudFormationToken } from './cloudformation-token'; +import { CfnReference } from './cfn-tokens'; import { Condition } from './condition'; import { CreationPolicy, DeletionPolicy, UpdatePolicy } from './resource-policy'; -import { IDependable, Referenceable, StackElement } from './stack'; +import { IDependable, Referenceable, StackElement } from './stack-element'; export interface ResourceProps { /** @@ -105,7 +105,7 @@ export class Resource extends Referenceable { * @param attributeName The name of the attribute. */ public getAtt(attributeName: string) { - return new CloudFormationToken({ 'Fn::GetAtt': [this.logicalId, attributeName] }, `${this.logicalId}.${attributeName}`); + return new CfnReference({ 'Fn::GetAtt': [this.logicalId, attributeName] }, `${this.logicalId}.${attributeName}`, this); } /** @@ -187,12 +187,12 @@ export class Resource extends Referenceable { Resources: { [this.logicalId]: deepMerge({ Type: this.resourceType, - Properties: ignoreEmpty(properties), - DependsOn: ignoreEmpty(this.renderDependsOn()), - CreationPolicy: capitalizePropertyNames(this.options.creationPolicy), - UpdatePolicy: capitalizePropertyNames(this.options.updatePolicy), - DeletionPolicy: capitalizePropertyNames(this.options.deletionPolicy), - Metadata: ignoreEmpty(this.options.metadata), + Properties: ignoreEmpty(this, properties), + DependsOn: ignoreEmpty(this, this.renderDependsOn()), + CreationPolicy: capitalizePropertyNames(this, this.options.creationPolicy), + UpdatePolicy: capitalizePropertyNames(this, this.options.updatePolicy), + DeletionPolicy: capitalizePropertyNames(this, this.options.deletionPolicy), + Metadata: ignoreEmpty(this, this.options.metadata), Condition: this.options.condition && this.options.condition.logicalId }, this.rawOverrides) } diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/rule.ts b/packages/@aws-cdk/cdk/lib/cloudformation/rule.ts index ecfbbbf200867..1b7b0fe5caac9 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/rule.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/rule.ts @@ -1,7 +1,7 @@ import { Construct } from '../core/construct'; import { capitalizePropertyNames } from '../core/util'; import { IConditionExpression } from './condition'; -import { Referenceable } from './stack'; +import { Referenceable } from './stack-element'; /** * A rule can include a RuleCondition property and must include an Assertions property. @@ -97,7 +97,7 @@ export class Rule extends Referenceable { Rules: { [this.logicalId]: { RuleCondition: this.ruleCondition, - Assertions: capitalizePropertyNames(this.assertions) + Assertions: capitalizePropertyNames(this, this.assertions) } } }; diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/stack-element.ts b/packages/@aws-cdk/cdk/lib/cloudformation/stack-element.ts new file mode 100644 index 0000000000000..e07cdae7a0825 --- /dev/null +++ b/packages/@aws-cdk/cdk/lib/cloudformation/stack-element.ts @@ -0,0 +1,172 @@ +import { Construct, IConstruct, PATH_SEP } from "../core/construct"; + +const LOGICAL_ID_MD = 'aws:cdk:logicalId'; + +/** + * Represents a construct that can be "depended on" via `addDependency`. + */ +export interface IDependable { + /** + * Returns the set of all stack elements (resources, parameters, conditions) + * that should be added when a resource "depends on" this construct. + */ + readonly dependencyElements: IDependable[]; +} + +/** + * An element of a CloudFormation stack. + */ +export abstract class StackElement extends Construct implements IDependable { + /** + * Returns `true` if a construct is a stack element (i.e. part of the + * synthesized cloudformation template). + * + * Uses duck-typing instead of `instanceof` to allow stack elements from different + * versions of this library to be included in the same stack. + * + * @returns The construct as a stack element or undefined if it is not a stack element. + */ + public static _asStackElement(construct: IConstruct): StackElement | undefined { + if ('logicalId' in construct && 'toCloudFormation' in construct) { + return construct as StackElement; + } else { + return undefined; + } + } + + /** + * The logical ID for this CloudFormation stack element + */ + public readonly logicalId: string; + + /** + * The stack this Construct has been made a part of + */ + protected stack: Stack; + + /** + * Creates an entity and binds it to a tree. + * Note that the root of the tree must be a Stack object (not just any Root). + * + * @param parent The parent construct + * @param props Construct properties + */ + constructor(scope: Construct, id: string) { + super(scope, id); + const s = Stack.find(this); + if (!s) { + throw new Error('The tree root must be derived from "Stack"'); + } + this.stack = s; + + this.node.addMetadata(LOGICAL_ID_MD, new (require("../core/tokens/token").Token)(() => this.logicalId), this.constructor); + + this.logicalId = this.stack.logicalIds.getLogicalId(this); + } + + /** + * @returns the stack trace of the point where this Resource was created from, sourced + * from the +metadata+ entry typed +aws:cdk:logicalId+, and with the bottom-most + * node +internal+ entries filtered. + */ + public get creationStackTrace(): string[] { + return filterStackTrace(this.node.metadata.find(md => md.type === LOGICAL_ID_MD)!.trace); + + function filterStackTrace(stack: string[]): string[] { + const result = Array.of(...stack); + while (result.length > 0 && shouldFilter(result[result.length - 1])) { + result.pop(); + } + // It's weird if we filtered everything, so return the whole stack... + return result.length === 0 ? stack : result; + } + + function shouldFilter(str: string): boolean { + return str.match(/[^(]+\(internal\/.*/) !== null; + } + } + + /** + * Return the path with respect to the stack + */ + public get stackPath(): string { + return this.node.ancestors(this.stack).map(c => c.node.id).join(PATH_SEP); + } + + public get dependencyElements(): IDependable[] { + return [ this ]; + } + + /** + * Returns the CloudFormation 'snippet' for this entity. The snippet will only be merged + * at the root level to ensure there are no identity conflicts. + * + * For example, a Resource class will return something like: + * { + * Resources: { + * [this.logicalId]: { + * Type: this.resourceType, + * Properties: this.props, + * Condition: this.condition + * } + * } + * } + */ + public abstract toCloudFormation(): object; + + /** + * Automatically detect references in this StackElement + */ + protected prepare() { + try { + // Note: it might be that the properties of the CFN object aren't valid. + // This will usually be preventatively caught in a construct's validate() + // and turned into a nicely descriptive error, but we're running prepare() + // before validate(). Swallow errors that occur because the CFN layer + // doesn't validate completely. + // + // This does make the assumption that the error will not be rectified, + // but the error will be thrown later on anyway. If the error doesn't + // get thrown down the line, we may miss references. + this.node.recordReference(...findTokens(this.toCloudFormation(), { + scope: this, + prefix: [] + })); + } catch (e) { + if (e.type !== 'CfnSynthesisError') { throw e; } + } + } +} + +import { CfnReference } from "./cfn-tokens"; + +/** + * A generic, untyped reference to a Stack Element + */ +export class Ref extends CfnReference { + constructor(element: StackElement) { + super({ Ref: element.logicalId }, `${element.logicalId}.Ref`, element); + } +} + +import { findTokens } from "../core/tokens/resolve"; +import { Stack } from "./stack"; + +/** + * Base class for referenceable CloudFormation constructs which are not Resources + * + * These constructs are things like Conditions and Parameters, can be + * referenced by taking the `.ref` attribute. + * + * Resource constructs do not inherit from Referenceable because they have their + * own, more specific types returned from the .ref attribute. Also, some + * resources aren't referenceable at all (such as BucketPolicies or GatewayAttachments). + */ +export abstract class Referenceable extends StackElement { + /** + * Returns a token to a CloudFormation { Ref } that references this entity based on it's logical ID. + */ + public get ref(): string { + return new Ref(this).toString(); + } +} diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/stack.ts b/packages/@aws-cdk/cdk/lib/cloudformation/stack.ts index f7a3af4b62b05..d096a75c4c446 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/stack.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/stack.ts @@ -1,9 +1,8 @@ import cxapi = require('@aws-cdk/cx-api'); import { App } from '../app'; -import { Construct, IConstruct, PATH_SEP } from '../core/construct'; -import { resolve, Token } from '../core/tokens'; +import { Construct, IConstruct } from '../core/construct'; import { Environment } from '../environment'; -import { CloudFormationToken } from './cloudformation-token'; +import { CfnReference } from './cfn-tokens'; import { HashedAddressingScheme, IAddressingScheme, LogicalIDs } from './logical-id'; import { Resource } from './resource'; @@ -30,17 +29,17 @@ export interface StackProps { export class Stack extends Construct { /** * Traverses the tree and looks up for the Stack root. - * @param node A construct in the tree + * @param scope A construct in the tree * @returns The Stack object (throws if the node is not part of a Stack-rooted tree) */ - public static find(node: Construct): Stack { - let curr: IConstruct | undefined = node; + public static find(scope: IConstruct): Stack { + let curr: IConstruct | undefined = scope; while (curr != null && !Stack.isStack(curr)) { curr = curr.node.scope; } if (curr == null) { - throw new Error(`Cannot find a Stack parent for '${node.toString()}'`); + throw new Error(`Cannot find a Stack parent for '${scope.toString()}'`); } return curr; } @@ -96,11 +95,16 @@ export class Stack extends Construct { */ public readonly name: string; - /** + /* * Used to determine if this construct is a stack. */ protected readonly _isStack = true; + /** + * Other stacks this stack depends on + */ + private readonly stackDependencies = new Set(); + /** * Creates a new stack. * @@ -108,7 +112,7 @@ export class Stack extends Construct { * @param name The name of the CloudFormation stack. Defaults to "Stack". * @param props Stack properties. */ - public constructor(scope?: App, name?: string, props?: StackProps) { + public constructor(scope?: App, name?: string, private readonly props?: StackProps) { // For unit test convenience parents are optional, so bypass the type check when calling the parent. super(scope!, name!); @@ -164,7 +168,7 @@ export class Stack extends Construct { } // resolve all tokens and remove all empties - const ret = resolve(template) || { }; + const ret = this.node.resolve(template) || {}; this.logicalIds.assertAllRenamesApplied(); @@ -235,159 +239,227 @@ export class Stack extends Construct { } /** - * Applied defaults to environment attributes. + * Add a dependency between this stack and another stack */ - private parseEnvironment(props?: StackProps) { - // start with `env`. - const env: Environment = (props && props.env) || { }; + public addDependency(stack: Stack) { + if (stack.dependsOnStack(this)) { + // tslint:disable-next-line:max-line-length + throw new Error(`Stack '${this.name}' already depends on stack '${stack.name}'. Adding this dependency would create a cyclic reference.`); + } + this.stackDependencies.add(stack); + } - // if account is not specified, attempt to read from context. - if (!env.account) { - env.account = this.node.getContext(cxapi.DEFAULT_ACCOUNT_CONTEXT_KEY); + /** + * Return the stacks this stack depends on + */ + public dependencies(): Stack[] { + return Array.from(this.stackDependencies.values()); + } + + /** + * The account in which this stack is defined + * + * Either returns the literal account for this stack if it was specified + * literally upon Stack construction, or a symbolic value that will evaluate + * to the correct account at deployment time. + */ + public get accountId(): string { + if (this.props && this.props.env && this.props.env.account) { + return this.props.env.account; } + return new Aws(this).accountId; + } - // if region is not specified, attempt to read from context. - if (!env.region) { - env.region = this.node.getContext(cxapi.DEFAULT_REGION_CONTEXT_KEY); + /** + * The region in which this stack is defined + * + * Either returns the literal region for this stack if it was specified + * literally upon Stack construction, or a symbolic value that will evaluate + * to the correct region at deployment time. + */ + public get region(): string { + if (this.props && this.props.env && this.props.env.region) { + return this.props.env.region; } + return new Aws(this).region; + } - return env; + /** + * The partition in which this stack is defined + */ + public get partition(): string { + return new Aws(this).partition; } -} -function merge(template: any, part: any) { - for (const section of Object.keys(part)) { - const src = part[section]; + /** + * The Amazon domain suffix for the region in which this stack is defined + */ + public get urlSuffix(): string { + return new Aws(this).urlSuffix; + } - // create top-level section if it doesn't exist - let dest = template[section]; - if (!dest) { - template[section] = dest = src; - } else { - // add all entities from source section to destination section - for (const id of Object.keys(src)) { - if (id in dest) { - throw new Error(`section '${section}' already contains '${id}'`); - } - dest[id] = src[id]; - } - } + /** + * The ID of the stack + * + * @example After resolving, looks like arn:aws:cloudformation:us-west-2:123456789012:stack/teststack/51af3dc0-da77-11e4-872e-1234567db123 + */ + public get stackId(): string { + return new Aws(this).stackId; } -} -const LOGICAL_ID_MD = 'aws:cdk:logicalId'; + /** + * The name of the stack currently being deployed + * + * Only available at deployment time. + */ + public get stackName(): string { + return new Aws(this).stackName; + } -/** - * Represents a construct that can be "depended on" via `addDependency`. - */ -export interface IDependable { /** - * Returns the set of all stack elements (resources, parameters, conditions) - * that should be added when a resource "depends on" this construct. + * Returns the list of notification Amazon Resource Names (ARNs) for the current stack. */ - readonly dependencyElements: IDependable[]; -} + public get notificationArns(): string[] { + return new Aws(this).notificationArns; + } -/** - * An element of a CloudFormation stack. - */ -export abstract class StackElement extends Construct implements IDependable { /** - * Returns `true` if a construct is a stack element (i.e. part of the - * synthesized cloudformation template). + * Creates an ARN from components. + * + * If `partition`, `region` or `account` are not specified, the stack's + * partition, region and account will be used. + * + * If any component is the empty string, an empty string will be inserted + * into the generated ARN at the location that component corresponds to. + * + * The ARN will be formatted as follows: * - * Uses duck-typing instead of `instanceof` to allow stack elements from different - * versions of this library to be included in the same stack. + * arn:{partition}:{service}:{region}:{account}:{resource}{sep}}{resource-name} * - * @returns The construct as a stack element or undefined if it is not a stack element. + * The required ARN pieces that are omitted will be taken from the stack that + * the 'scope' is attached to. If all ARN pieces are supplied, the supplied scope + * can be 'undefined'. */ - public static _asStackElement(construct: IConstruct): StackElement | undefined { - if ('logicalId' in construct && 'toCloudFormation' in construct) { - return construct as StackElement; - } else { - return undefined; - } + public formatArn(components: ArnComponents): string { + return arnFromComponents(components, this); } /** - * The logical ID for this CloudFormation stack element + * Given an ARN, parses it and returns components. + * + * If the ARN is a concrete string, it will be parsed and validated. The + * separator (`sep`) will be set to '/' if the 6th component includes a '/', + * in which case, `resource` will be set to the value before the '/' and + * `resourceName` will be the rest. In case there is no '/', `resource` will + * be set to the 6th components and `resourceName` will be set to the rest + * of the string. + * + * If the ARN includes tokens (or is a token), the ARN cannot be validated, + * since we don't have the actual value yet at the time of this function + * call. You will have to know the separator and the type of ARN. The + * resulting `ArnComponents` object will contain tokens for the + * subexpressions of the ARN, not string literals. In this case this + * function cannot properly parse the complete final resourceName (path) out + * of ARNs that use '/' to both separate the 'resource' from the + * 'resourceName' AND to subdivide the resourceName further. For example, in + * S3 ARNs: + * + * arn:aws:s3:::my_corporate_bucket/path/to/exampleobject.png + * + * After parsing the resourceName will not contain + * 'path/to/exampleobject.png' but simply 'path'. This is a limitation + * because there is no slicing functionality in CloudFormation templates. + * + * @param sep The separator used to separate resource from resourceName + * @param hasName Whether there is a name component in the ARN at all. For + * example, SNS Topics ARNs have the 'resource' component contain the topic + * name, and no 'resourceName' component. + * + * @returns an ArnComponents object which allows access to the various + * components of the ARN. + * + * @returns an ArnComponents object which allows access to the various + * components of the ARN. */ - public readonly logicalId: string; + public parseArn(arn: string, sepIfToken: string = '/', hasName: boolean = true): ArnComponents { + return parseArn(arn, sepIfToken, hasName); + } /** - * The stack this Construct has been made a part of + * Validate stack name + * + * CloudFormation stack names can include dashes in addition to the regular identifier + * character classes, and we don't allow one of the magic markers. */ - protected stack: Stack; + protected _validateId(name: string) { + if (name && !Stack.VALID_STACK_NAME_REGEX.test(name)) { + throw new Error(`Stack name must match the regular expression: ${Stack.VALID_STACK_NAME_REGEX.toString()}, got '${name}'`); + } + } /** - * Creates an entity and binds it to a tree. - * Note that the root of the tree must be a Stack object (not just any Root). + * Prepare stack * - * @param parent The parent construct - * @param props Construct properties - */ - constructor(scope: Construct, id: string) { - super(scope, id); - const s = Stack.find(this); - if (!s) { - throw new Error('The tree root must be derived from "Stack"'); + * Find all CloudFormation references and tell them we're consuming them. + */ + protected prepare() { + for (const ref of this.node.findReferences()) { + if (CfnReference.isInstance(ref)) { + ref.consumeFromStack(this); + } } - this.stack = s; - - this.node.addMetadata(LOGICAL_ID_MD, new Token(() => this.logicalId), this.constructor); - - this.logicalId = this.stack.logicalIds.getLogicalId(this); } /** - * @returns the stack trace of the point where this Resource was created from, sourced - * from the +metadata+ entry typed +aws:cdk:logicalId+, and with the bottom-most - * node +internal+ entries filtered. + * Applied defaults to environment attributes. */ - public get creationStackTrace(): string[] { - return filterStackTrace(this.node.metadata.find(md => md.type === LOGICAL_ID_MD)!.trace); + private parseEnvironment(props?: StackProps) { + // start with `env`. + const env: Environment = (props && props.env) || { }; - function filterStackTrace(stack: string[]): string[] { - const result = Array.of(...stack); - while (result.length > 0 && shouldFilter(result[result.length - 1])) { - result.pop(); - } - // It's weird if we filtered everything, so return the whole stack... - return result.length === 0 ? stack : result; + // if account is not specified, attempt to read from context. + if (!env.account) { + env.account = this.node.getContext(cxapi.DEFAULT_ACCOUNT_CONTEXT_KEY); } - function shouldFilter(str: string): boolean { - return str.match(/[^(]+\(internal\/.*/) !== null; + // if region is not specified, attempt to read from context. + if (!env.region) { + env.region = this.node.getContext(cxapi.DEFAULT_REGION_CONTEXT_KEY); } + + return env; } /** - * Return the path with respect to the stack + * Check whether this stack has a (transitive) dependency on another stack */ - public get stackPath(): string { - return this.node.ancestors(this.stack).map(c => c.node.id).join(PATH_SEP); + private dependsOnStack(other: Stack) { + if (this === other) { return true; } + for (const dep of this.stackDependencies) { + if (dep.dependsOnStack(other)) { return true; } + } + return false; } +} - public get dependencyElements(): IDependable[] { - return [ this ]; - } +function merge(template: any, part: any) { + for (const section of Object.keys(part)) { + const src = part[section]; - /** - * Returns the CloudFormation 'snippet' for this entity. The snippet will only be merged - * at the root level to ensure there are no identity conflicts. - * - * For example, a Resource class will return something like: - * { - * Resources: { - * [this.logicalId]: { - * Type: this.resourceType, - * Properties: this.props, - * Condition: this.condition - * } - * } - * } - */ - public abstract toCloudFormation(): object; + // create top-level section if it doesn't exist + let dest = template[section]; + if (!dest) { + template[section] = dest = src; + } else { + // add all entities from source section to destination section + for (const id of Object.keys(src)) { + if (id in dest) { + throw new Error(`section '${section}' already contains '${id}'`); + } + dest[id] = src[id]; + } + } + } } /** @@ -416,25 +488,6 @@ export interface TemplateOptions { metadata?: { [key: string]: any }; } -/** - * Base class for referenceable CloudFormation constructs which are not Resources - * - * These constructs are things like Conditions and Parameters, can be - * referenced by taking the `.ref` attribute. - * - * Resource constructs do not inherit from Referenceable because they have their - * own, more specific types returned from the .ref attribute. Also, some - * resources aren't referenceable at all (such as BucketPolicies or GatewayAttachments). - */ -export abstract class Referenceable extends StackElement { - /** - * Returns a token to a CloudFormation { Ref } that references this entity based on it's logical ID. - */ - public get ref(): string { - return new Ref(this).toString(); - } -} - /** * Collect all StackElements from a construct * @@ -455,11 +508,7 @@ function stackElements(node: IConstruct, into: StackElement[] = []): StackElemen return into; } -/** - * A generic, untyped reference to a Stack Element - */ -export class Ref extends CloudFormationToken { - constructor(element: StackElement) { - super({ Ref: element.logicalId }, `${element.logicalId}.Ref`); - } -} +// These imports have to be at the end to prevent circular imports +import { ArnComponents, arnFromComponents, parseArn } from './arn'; +import { Aws } from './pseudo'; +import { StackElement } from './stack-element'; \ No newline at end of file diff --git a/packages/@aws-cdk/cdk/lib/core/construct.ts b/packages/@aws-cdk/cdk/lib/core/construct.ts index 96b6453b35602..9551618c24b46 100644 --- a/packages/@aws-cdk/cdk/lib/core/construct.ts +++ b/packages/@aws-cdk/cdk/lib/core/construct.ts @@ -1,6 +1,8 @@ import cxapi = require('@aws-cdk/cx-api'); +import { CloudFormationJSON } from '../cloudformation/cloudformation-json'; import { makeUniqueId } from '../util/uniqueid'; -import { unresolved } from './tokens'; +import { Token, unresolved } from './tokens'; +import { resolve } from './tokens/resolve'; export const PATH_SEP = '/'; /** @@ -35,6 +37,7 @@ export class ConstructNode { private readonly _children: { [name: string]: IConstruct } = { }; private readonly context: { [key: string]: any } = { }; private readonly _metadata = new Array(); + private readonly references = new Set(); /** * If this is set to 'true'. addChild() calls for this construct and any child @@ -150,7 +153,23 @@ export class ConstructNode { * All direct children of this construct. */ public get children() { - return Object.keys(this._children).map(k => this._children[k]); + return Object.values(this._children); + } + + /** + * Return this construct and all of its children in the given order + */ + public findAll(order: ConstructOrder = ConstructOrder.DepthFirst): IConstruct[] { + const ret = new Array(); + const queue: IConstruct[] = [this.host]; + + while (queue.length > 0) { + const next = order === ConstructOrder.BreadthFirst ? queue.splice(0, 1)[0] : queue.pop()!; + ret.push(next); + queue.push(...next.node.children); + } + + return ret; } /** @@ -263,10 +282,23 @@ export class ConstructNode { errors = errors.concat(child.node.validateTree()); } - const localErrors: string[] = this.host.validate(); + const localErrors: string[] = (this.host as any).validate(); return errors.concat(localErrors.map(msg => new ValidationError(this.host, msg))); } + /** + * Run 'prepare()' on all constructs in the tree + */ + public prepareTree() { + const constructs = this.host.node.findAll(ConstructOrder.BreadthFirst); + // Use .reverse() to achieve post-order traversal + for (const construct of constructs.reverse()) { + if (Construct.isInstance(construct)) { + (construct as any).prepare(); + } + } + } + /** * Return the ancestors (including self) of this Construct up until and excluding the indicated component * @@ -350,15 +382,6 @@ export class ConstructNode { this._locked = false; } - /** - * Return the path of components up to but excluding the root - */ - private rootPath(): IConstruct[] { - const ancestors = this.ancestors(); - ancestors.shift(); - return ancestors; - } - /** * Returns true if this construct or the scopes in which it is defined are * locked. @@ -375,6 +398,64 @@ export class ConstructNode { return false; } + /** + * Resolve a tokenized value in the context of the current Construct + */ + public resolve(obj: any): any { + return resolve(obj, { + scope: this.host, + prefix: [] + }); + } + + /** + * Convert an object, potentially containing tokens, to a JSON string + */ + public stringifyJson(obj: any): string { + return CloudFormationJSON.stringify(obj, this.host).toString(); + } + + /** + * Record a reference originating from this construct node + */ + public recordReference(...refs: Token[]) { + for (const ref of refs) { + if (ref.isReference) { + this.references.add(ref); + } + } + } + + /** + * Return all references of the given type originating from this node or any of its children + */ + public findReferences(): Token[] { + const ret = new Set(); + + function recurse(node: ConstructNode) { + for (const ref of node.references) { + ret.add(ref); + } + + for (const child of node.children) { + recurse(child.node); + } + } + + recurse(this); + + return Array.from(ret); + } + + /** + * Return the path of components up to but excluding the root + */ + private rootPath(): IConstruct[] { + const ancestors = this.ancestors(); + ancestors.shift(); + return ancestors; + } + /** * If the construct ID contains a path separator, it is replaced by double dash (`--`). */ @@ -391,6 +472,13 @@ export class ConstructNode { * another construct. */ export class Construct implements IConstruct { + /** + * Return whether the given object is a Construct + */ + public static isInstance(x: IConstruct): x is Construct { + return (x as any).prepare !== undefined && (x as any).validate !== undefined; + } + /** * Construct node. */ @@ -417,14 +505,30 @@ export class Construct implements IConstruct { } /** + * Validate the current construct. + * * This method can be implemented by derived constructs in order to perform * validation logic. It is called on all constructs before synthesis. * * @returns An array of validation error messages, or an empty array if there the construct is valid. */ - public validate(): string[] { + protected validate(): string[] { return []; } + + /** + * Perform final modifications before synthesis + * + * This method can be implemented by derived constructs in order to perform + * final changes before synthesis. prepare() will be called after child + * constructs have been prepared. + * + * This is an advanced framework feature. Only use this if you + * understand the implications. + */ + protected prepare(): void { + // Intentionally left blank + } } /** @@ -479,3 +583,18 @@ function createStackTrace(below: Function): string[] { } return object.stack.split('\n').slice(1).map(s => s.replace(/^\s*at\s+/, '')); } + +/** + * In what order to return constructs + */ +export enum ConstructOrder { + /** + * Breadth first + */ + BreadthFirst, + + /** + * Depth first + */ + DepthFirst +} diff --git a/packages/@aws-cdk/cdk/lib/core/tag-manager.ts b/packages/@aws-cdk/cdk/lib/core/tag-manager.ts index 8ee0ab75b07e3..7f5f6c57d3814 100644 --- a/packages/@aws-cdk/cdk/lib/core/tag-manager.ts +++ b/packages/@aws-cdk/cdk/lib/core/tag-manager.ts @@ -1,5 +1,5 @@ import { Construct, IConstruct } from './construct'; -import { Token } from './tokens'; +import { ResolveContext, Token } from './tokens'; /** * ITaggable indicates a entity manages tags via the `tags` property @@ -171,7 +171,7 @@ export class TagManager extends Token { /** * Converts the `tags` to a Token for use in lazy evaluation */ - public resolve(): any { + public resolve(_context: ResolveContext): any { // need this for scoping const blockedTags = this.blockedTags; function filterTags(_tags: FullTags, filter: TagProps = {}): Tags { diff --git a/packages/@aws-cdk/cdk/lib/core/tokens.ts b/packages/@aws-cdk/cdk/lib/core/tokens.ts deleted file mode 100644 index 8b5b4b99a3dde..0000000000000 --- a/packages/@aws-cdk/cdk/lib/core/tokens.ts +++ /dev/null @@ -1,520 +0,0 @@ -import { Construct } from "./construct"; - -/** - * If objects has a function property by this name, they will be considered tokens, and this - * function will be called to resolve the value for this object. - */ -export const RESOLVE_METHOD = 'resolve'; - -/** - * Represents a special or lazily-evaluated value. - * - * Can be used to delay evaluation of a certain value in case, for example, - * that it requires some context or late-bound data. Can also be used to - * mark values that need special processing at document rendering time. - * - * Tokens can be embedded into strings while retaining their original - * semantics. - */ -export class Token { - private tokenStringification?: string; - private tokenListification?: string[]; - - /** - * Creates a token that resolves to `value`. - * - * If value is a function, the function is evaluated upon resolution and - * the value it returns will be used as the token's value. - * - * displayName is used to represent the Token when it's embedded into a string; it - * will look something like this: - * - * "embedded in a larger string is ${Token[DISPLAY_NAME.123]}" - * - * This value is used as a hint to humans what the meaning of the Token is, - * and does not have any effect on the evaluation. - * - * Must contain only alphanumeric and simple separator characters (_.:-). - * - * @param valueOrFunction What this token will evaluate to, literal or function. - * @param displayName A human-readable display hint for this Token - */ - constructor(private readonly valueOrFunction?: any, private readonly displayName?: string) { - } - - /** - * @returns The resolved value for this token. - */ - public resolve(): any { - let value = this.valueOrFunction; - if (typeof(value) === 'function') { - value = value(); - } - - return value; - } - - /** - * Return a reversible string representation of this token - * - * If the Token is initialized with a literal, the stringified value of the - * literal is returned. Otherwise, a special quoted string representation - * of the Token is returned that can be embedded into other strings. - * - * Strings with quoted Tokens in them can be restored back into - * complex values with the Tokens restored by calling `resolve()` - * on the string. - */ - public toString(): string { - const valueType = typeof this.valueOrFunction; - // Optimization: if we can immediately resolve this, don't bother - // registering a Token. - if (valueType === 'string' || valueType === 'number' || valueType === 'boolean') { - return this.valueOrFunction.toString(); - } - - if (this.tokenStringification === undefined) { - this.tokenStringification = TOKEN_MAP.registerString(this, this.displayName); - } - return this.tokenStringification; - } - - /** - * Turn this Token into JSON - * - * This gets called by JSON.stringify(). We want to prohibit this, because - * it's not possible to do this properly, so we just throw an error here. - */ - public toJSON(): any { - // tslint:disable-next-line:max-line-length - throw new Error('JSON.stringify() cannot be applied to structure with a Token in it. Use a document-specific stringification method instead.'); - } - - /** - * Return a string list representation of this token - * - * Call this if the Token intrinsically evaluates to a list of strings. - * If so, you can represent the Token in a similar way in the type - * system. - * - * Note that even though the Token is represented as a list of strings, you - * still cannot do any operations on it such as concatenation, indexing, - * or taking its length. The only useful operations you can do to these lists - * is constructing a `FnJoin` or a `FnSelect` on it. - */ - public toList(): string[] { - const valueType = typeof this.valueOrFunction; - if (valueType === 'string' || valueType === 'number' || valueType === 'boolean') { - throw new Error('Got a literal Token value; cannot be encoded as a list.'); - } - - if (this.tokenListification === undefined) { - this.tokenListification = TOKEN_MAP.registerList(this, this.displayName); - } - return this.tokenListification; - } - - /** - * Return a concated version of this Token in a string context - * - * The default implementation of this combines strings, but specialized - * implements of Token can return a more appropriate value. - */ - public concat(left: any | undefined, right: any | undefined): Token { - const parts = [left, resolve(this), right].filter(x => x !== undefined); - return new Token(parts.map(x => `${x}`).join('')); - } -} - -/** - * Returns true if obj is a token (i.e. has the resolve() method or is a string - * that includes token markers), or it's a listifictaion of a Token string. - * - * @param obj The object to test. - */ -export function unresolved(obj: any): boolean { - if (typeof(obj) === 'string') { - return TOKEN_MAP.createStringTokenString(obj).test(); - } else if (Array.isArray(obj) && obj.length === 1) { - return isListToken(obj[0]); - } else { - return typeof(obj[RESOLVE_METHOD]) === 'function'; - } -} - -/** - * Resolves an object by evaluating all tokens and removing any undefined or empty objects or arrays. - * Values can only be primitives, arrays or tokens. Other objects (i.e. with methods) will be rejected. - * - * @param obj The object to resolve. - * @param prefix Prefix key path components for diagnostics. - */ -export function resolve(obj: any, prefix?: string[]): any { - const path = prefix || [ ]; - const pathName = '/' + path.join('/'); - - // protect against cyclic references by limiting depth. - if (path.length > 200) { - throw new Error('Unable to resolve object tree with circular reference. Path: ' + pathName); - } - - // - // undefined - // - - if (typeof(obj) === 'undefined') { - return undefined; - } - - // - // null - // - - if (obj === null) { - return null; - } - - // - // functions - not supported (only tokens are supported) - // - - if (typeof(obj) === 'function') { - throw new Error(`Trying to resolve a non-data object. Only token are supported for lazy evaluation. Path: ${pathName}. Object: ${obj}`); - } - - // - // string - potentially replace all stringified Tokens - // - if (typeof(obj) === 'string') { - return TOKEN_MAP.resolveStringTokens(obj as string); - } - - // - // primitives - as-is - // - - if (typeof(obj) !== 'object' || obj instanceof Date) { - return obj; - } - - // - // arrays - resolve all values, remove undefined and remove empty arrays - // - - if (Array.isArray(obj)) { - if (containsListToken(obj)) { - return TOKEN_MAP.resolveListTokens(obj); - } - - const arr = obj - .map((x, i) => resolve(x, path.concat(i.toString()))) - .filter(x => typeof(x) !== 'undefined'); - - return arr; - } - - // - // tokens - invoke 'resolve' and continue to resolve recursively - // - - if (unresolved(obj)) { - const value = obj[RESOLVE_METHOD](); - return resolve(value, path); - } - - // - // objects - deep-resolve all values - // - - // Must not be a Construct at this point, otherwise you probably made a type - // mistake somewhere and resolve will get into an infinite loop recursing into - // child.parent <---> parent.children - if (obj instanceof Construct) { - throw new Error('Trying to resolve() a Construct at ' + pathName); - } - - const result: any = { }; - for (const key of Object.keys(obj)) { - const resolvedKey = resolve(key); - if (typeof(resolvedKey) !== 'string') { - throw new Error(`The key "${key}" has been resolved to ${JSON.stringify(resolvedKey)} but must be resolvable to a string`); - } - - const value = resolve(obj[key], path.concat(key)); - - // skip undefined - if (typeof(value) === 'undefined') { - continue; - } - - result[resolvedKey] = value; - } - - return result; -} - -function isListToken(x: any) { - return typeof(x) === 'string' && TOKEN_MAP.createListTokenString(x).test(); -} - -function containsListToken(xs: any[]) { - return xs.some(isListToken); -} - -/** - * Central place where we keep a mapping from Tokens to their String representation - * - * The string representation is used to embed token into strings, - * and stored to be able to - * - * All instances of TokenStringMap share the same storage, so that this process - * works even when different copies of the library are loaded. - */ -class TokenMap { - private readonly tokenMap: {[key: string]: Token}; - - constructor() { - const glob = global as any; - this.tokenMap = glob.__cdkTokenMap = glob.__cdkTokenMap || {}; - } - - /** - * Generate a unique string for this Token, returning a key - * - * Every call for the same Token will produce a new unique string, no - * attempt is made to deduplicate. Token objects should cache the - * value themselves, if required. - * - * The token can choose (part of) its own representation string with a - * hint. This may be used to produce aesthetically pleasing and - * recognizable token representations for humans. - */ - public registerString(token: Token, representationHint?: string): string { - const key = this.register(token, representationHint); - return `${BEGIN_STRING_TOKEN_MARKER}${key}${END_TOKEN_MARKER}`; - } - - /** - * Generate a unique string for this Token, returning a key - */ - public registerList(token: Token, representationHint?: string): string[] { - const key = this.register(token, representationHint); - return [`${BEGIN_LIST_TOKEN_MARKER}${key}${END_TOKEN_MARKER}`]; - } - - /** - * Returns a `TokenString` for this string. - */ - public createStringTokenString(s: string) { - return new TokenString(s, BEGIN_STRING_TOKEN_MARKER, `[${VALID_KEY_CHARS}]+`, END_TOKEN_MARKER); - } - - /** - * Returns a `TokenString` for this string. - */ - public createListTokenString(s: string) { - return new TokenString(s, BEGIN_LIST_TOKEN_MARKER, `[${VALID_KEY_CHARS}]+`, END_TOKEN_MARKER); - } - - /** - * Replace any Token markers in this string with their resolved values - */ - public resolveStringTokens(s: string): any { - const str = this.createStringTokenString(s); - const fragments = str.split(this.lookupToken.bind(this)); - return fragments.join(); - } - - public resolveListTokens(xs: string[]): any { - // Must be a singleton list token, because concatenation is not allowed. - if (xs.length !== 1) { - throw new Error(`Cannot add elements to list token, got: ${xs}`); - } - - const str = this.createListTokenString(xs[0]); - const fragments = str.split(this.lookupToken.bind(this)); - if (fragments.length !== 1) { - throw new Error(`Cannot concatenate strings in a tokenized string array, got: ${xs[0]}`); - } - return fragments.values()[0]; - } - - /** - * Find a Token by key - */ - public lookupToken(key: string): Token { - if (!(key in this.tokenMap)) { - throw new Error(`Unrecognized token key: ${key}`); - } - - return this.tokenMap[key]; - } - - private register(token: Token, representationHint?: string): string { - const counter = Object.keys(this.tokenMap).length; - const representation = representationHint || `TOKEN`; - - const key = `${representation}.${counter}`; - if (new RegExp(`[^${VALID_KEY_CHARS}]`).exec(key)) { - throw new Error(`Invalid characters in token representation: ${key}`); - } - - this.tokenMap[key] = token; - return key; - } -} - -const BEGIN_STRING_TOKEN_MARKER = '${Token['; -const BEGIN_LIST_TOKEN_MARKER = '#{Token['; -const END_TOKEN_MARKER = ']}'; -const VALID_KEY_CHARS = 'a-zA-Z0-9:._-'; - -/** - * Singleton instance of the token string map - */ -const TOKEN_MAP = new TokenMap(); - -/** - * Interface that Token joiners implement - */ -export interface ITokenJoiner { - /** - * The name of the joiner. - * - * Must be unique per joiner: this value will be used to assert that there - * is exactly only type of joiner in a join operation. - */ - id: string; - - /** - * Return the language intrinsic that will combine the strings in the given engine - */ - join(fragments: any[]): any; -} - -/** - * A string with markers in it that can be resolved to external values - */ -class TokenString { - private pattern: string; - - constructor( - private readonly str: string, - private readonly beginMarker: string, - private readonly idPattern: string, - private readonly endMarker: string) { - this.pattern = `${regexQuote(this.beginMarker)}(${this.idPattern})${regexQuote(this.endMarker)}`; - } - - /** - * Split string on markers, substituting markers with Tokens - */ - public split(lookup: (id: string) => Token): TokenStringFragments { - const re = new RegExp(this.pattern, 'g'); - const ret = new TokenStringFragments(); - - let rest = 0; - let m = re.exec(this.str); - while (m) { - if (m.index > rest) { - ret.addString(this.str.substring(rest, m.index)); - } - - ret.addToken(lookup(m[1])); - - rest = re.lastIndex; - m = re.exec(this.str); - } - - if (rest < this.str.length) { - ret.addString(this.str.substring(rest)); - } - - return ret; - } - - /** - * Indicates if this string includes tokens. - */ - public test(): boolean { - const re = new RegExp(this.pattern, 'g'); - return re.test(this.str); - } -} - -/** - * Result of the split of a string with Tokens - * - * Either a literal part of the string, or an unresolved Token. - */ -type StringFragment = { type: 'string'; str: string }; -type TokenFragment = { type: 'token'; token: Token }; -type Fragment = StringFragment | TokenFragment; - -/** - * Fragments of a string with markers - */ -class TokenStringFragments { - private readonly fragments = new Array(); - - public get length() { - return this.fragments.length; - } - - public values(): any[] { - return this.fragments.map(f => f.type === 'token' ? resolve(f.token) : f.str); - } - - public addString(str: string) { - this.fragments.push({ type: 'string', str }); - } - - public addToken(token: Token) { - this.fragments.push({ type: 'token', token }); - } - - /** - * Combine the resolved string fragments using the Tokens to join. - * - * Resolves the result. - */ - public join(): any { - if (this.fragments.length === 0) { return ''; } - if (this.fragments.length === 1) { return resolveFragment(this.fragments[0]); } - - const first = this.fragments[0]; - - let i; - let token: Token; - - if (first.type === 'token') { - token = first.token; - i = 1; - } else { - // We never have two strings in a row - token = (this.fragments[1] as TokenFragment).token.concat(first.str, undefined); - i = 2; - } - - while (i < this.fragments.length) { - token = token.concat(undefined, resolveFragment(this.fragments[i])); - i++; - } - - return resolve(token); - } -} - -/** - * Resolve the value from a single fragment - */ -function resolveFragment(fragment: Fragment): any { - return fragment.type === 'string' ? fragment.str : resolve(fragment.token); -} - -/** - * Quote a string for use in a regex - */ -function regexQuote(s: string) { - return s.replace(/[.?*+^$[\]\\(){}|-]/g, "\\$&"); -} diff --git a/packages/@aws-cdk/cdk/lib/core/tokens/cfn-concat.ts b/packages/@aws-cdk/cdk/lib/core/tokens/cfn-concat.ts new file mode 100644 index 0000000000000..f454521bce255 --- /dev/null +++ b/packages/@aws-cdk/cdk/lib/core/tokens/cfn-concat.ts @@ -0,0 +1,22 @@ +/** + * Produce a CloudFormation expression to concat two arbitrary expressions when resolving + */ +export function cloudFormationConcat(left: any | undefined, right: any | undefined): any { + if (left === undefined && right === undefined) { return ''; } + + const parts = new Array(); + if (left !== undefined) { parts.push(left); } + if (right !== undefined) { parts.push(right); } + + // Some case analysis to produce minimal expressions + if (parts.length === 1) { return parts[0]; } + if (parts.length === 2 && typeof parts[0] === 'string' && typeof parts[1] === 'string') { + return parts[0] + parts[1]; + } + + // Otherwise return a Join intrinsic (already in the target document language to avoid taking + // circular dependencies on FnJoin & friends) + return { 'Fn::Join': ['', minimalCloudFormationJoin('', parts)] }; +} + +import { minimalCloudFormationJoin } from "../../cloudformation/instrinsics"; \ No newline at end of file diff --git a/packages/@aws-cdk/cdk/lib/core/tokens/encoding.ts b/packages/@aws-cdk/cdk/lib/core/tokens/encoding.ts new file mode 100644 index 0000000000000..301698214dbb9 --- /dev/null +++ b/packages/@aws-cdk/cdk/lib/core/tokens/encoding.ts @@ -0,0 +1,288 @@ +import { resolve } from "./resolve"; +import { ResolveContext, Token } from "./token"; +import { unresolved } from "./unresolved"; + +// Encoding Tokens into native types; should not be exported + +/** + * Central place where we keep a mapping from Tokens to their String representation + * + * The string representation is used to embed token into strings, + * and stored to be able to + * + * All instances of TokenStringMap share the same storage, so that this process + * works even when different copies of the library are loaded. + */ +export class TokenMap { + private readonly tokenMap: {[key: string]: Token} = {}; + + /** + * Generate a unique string for this Token, returning a key + * + * Every call for the same Token will produce a new unique string, no + * attempt is made to deduplicate. Token objects should cache the + * value themselves, if required. + * + * The token can choose (part of) its own representation string with a + * hint. This may be used to produce aesthetically pleasing and + * recognizable token representations for humans. + */ + public registerString(token: Token, representationHint?: string): string { + const key = this.register(token, representationHint); + return `${BEGIN_STRING_TOKEN_MARKER}${key}${END_TOKEN_MARKER}`; + } + + /** + * Generate a unique string for this Token, returning a key + */ + public registerList(token: Token, representationHint?: string): string[] { + const key = this.register(token, representationHint); + return [`${BEGIN_LIST_TOKEN_MARKER}${key}${END_TOKEN_MARKER}`]; + } + + /** + * Returns a `TokenString` for this string. + */ + public createStringTokenString(s: string) { + return new TokenString(s, BEGIN_STRING_TOKEN_MARKER, `[${VALID_KEY_CHARS}]+`, END_TOKEN_MARKER); + } + + /** + * Returns a `TokenString` for this string. + */ + public createListTokenString(s: string) { + return new TokenString(s, BEGIN_LIST_TOKEN_MARKER, `[${VALID_KEY_CHARS}]+`, END_TOKEN_MARKER); + } + + /** + * Replace any Token markers in this string with their resolved values + */ + public resolveStringTokens(s: string, context: ResolveContext): any { + const str = this.createStringTokenString(s); + const fragments = str.split(this.lookupToken.bind(this)); + // require() here to break cyclic dependencies + const ret = fragments.mapUnresolved(x => resolve(x, context)).join(require('./cfn-concat').cloudFormationConcat); + if (unresolved(ret)) { + return resolve(ret, context); + } + return ret; + } + + public resolveListTokens(xs: string[], context: ResolveContext): any { + // Must be a singleton list token, because concatenation is not allowed. + if (xs.length !== 1) { + throw new Error(`Cannot add elements to list token, got: ${xs}`); + } + + const str = this.createListTokenString(xs[0]); + const fragments = str.split(this.lookupToken.bind(this)); + if (fragments.length !== 1) { + throw new Error(`Cannot concatenate strings in a tokenized string array, got: ${xs[0]}`); + } + return fragments.mapUnresolved(x => resolve(x, context)).values[0]; + } + + /** + * Find a Token by key + */ + public lookupToken(key: string): Token { + if (!(key in this.tokenMap)) { + throw new Error(`Unrecognized token key: ${key}`); + } + + return this.tokenMap[key]; + } + + private register(token: Token, representationHint?: string): string { + const counter = Object.keys(this.tokenMap).length; + const representation = representationHint || `TOKEN`; + + const key = `${representation}.${counter}`; + if (new RegExp(`[^${VALID_KEY_CHARS}]`).exec(key)) { + throw new Error(`Invalid characters in token representation: ${key}`); + } + + this.tokenMap[key] = token; + return key; + } +} + +const BEGIN_STRING_TOKEN_MARKER = '${Token['; +const BEGIN_LIST_TOKEN_MARKER = '#{Token['; +const END_TOKEN_MARKER = ']}'; +const VALID_KEY_CHARS = 'a-zA-Z0-9:._-'; + +/** + * Interface that Token joiners implement + */ +export interface ITokenJoiner { + /** + * The name of the joiner. + * + * Must be unique per joiner: this value will be used to assert that there + * is exactly only type of joiner in a join operation. + */ + id: string; + + /** + * Return the language intrinsic that will combine the strings in the given engine + */ + join(fragments: any[]): any; +} + +/** + * A string with markers in it that can be resolved to external values + */ +class TokenString { + private pattern: string; + + constructor( + private readonly str: string, + private readonly beginMarker: string, + private readonly idPattern: string, + private readonly endMarker: string) { + this.pattern = `${regexQuote(this.beginMarker)}(${this.idPattern})${regexQuote(this.endMarker)}`; + } + + /** + * Split string on markers, substituting markers with Tokens + */ + public split(lookup: (id: string) => Token): TokenizedStringFragments { + const re = new RegExp(this.pattern, 'g'); + const ret = new TokenizedStringFragments(); + + let rest = 0; + let m = re.exec(this.str); + while (m) { + if (m.index > rest) { + ret.addLiteral(this.str.substring(rest, m.index)); + } + + ret.addUnresolved(lookup(m[1])); + + rest = re.lastIndex; + m = re.exec(this.str); + } + + if (rest < this.str.length) { + ret.addLiteral(this.str.substring(rest)); + } + + return ret; + } + + /** + * Indicates if this string includes tokens. + */ + public test(): boolean { + const re = new RegExp(this.pattern, 'g'); + return re.test(this.str); + } +} + +/** + * Result of the split of a string with Tokens + * + * Either a literal part of the string, or an unresolved Token. + */ +type LiteralFragment = { type: 'literal'; lit: any; }; +type UnresolvedFragment = { type: 'unresolved'; token: any; }; +type Fragment = LiteralFragment | UnresolvedFragment; + +/** + * Fragments of a string with markers + */ +class TokenizedStringFragments { + private readonly fragments = new Array(); + + public get length() { + return this.fragments.length; + } + + public get values(): any[] { + return this.fragments.map(f => f.type === 'unresolved' ? f.token : f.lit); + } + + public addLiteral(lit: any) { + this.fragments.push({ type: 'literal', lit }); + } + + public addUnresolved(token: Token) { + this.fragments.push({ type: 'unresolved', token }); + } + + public mapUnresolved(fn: (t: any) => any): TokenizedStringFragments { + const ret = new TokenizedStringFragments(); + + for (const f of this.fragments) { + switch (f.type) { + case 'literal': + ret.addLiteral(f.lit); + break; + case 'unresolved': + const mappedToken = fn(f.token); + + if (unresolved(mappedToken)) { + ret.addUnresolved(mappedToken); + } else { + ret.addLiteral(mappedToken); + } + break; + } + } + + return ret; + } + + /** + * Combine the resolved string fragments using the Tokens to join. + * + * Resolves the result. + */ + public join(concat: ConcatFunc): any { + if (this.fragments.length === 0) { return concat(undefined, undefined); } + + const values = this.fragments.map(fragmentValue); + + while (values.length > 1) { + const prefix = values.splice(0, 2); + values.splice(0, 0, concat(prefix[0], prefix[1])); + } + + return values[0]; + } +} + +/** + * Resolve the value from a single fragment + */ +function fragmentValue(fragment: Fragment): any { + return fragment.type === 'literal' ? fragment.lit : fragment.token; +} + +/** + * Quote a string for use in a regex + */ +function regexQuote(s: string) { + return s.replace(/[.?*+^$[\]\\(){}|-]/g, "\\$&"); +} + +/** + * Function used to concatenate symbols in the target document language + */ +export type ConcatFunc = (left: any | undefined, right: any | undefined) => any; + +const glob = global as any; + +/** + * Singleton instance of the token string map + */ +export const TOKEN_MAP: TokenMap = glob.__cdkTokenMap = glob.__cdkTokenMap || new TokenMap(); + +export function isListToken(x: any) { + return typeof(x) === 'string' && TOKEN_MAP.createListTokenString(x).test(); +} + +export function containsListToken(xs: any[]) { + return xs.some(isListToken); +} \ No newline at end of file diff --git a/packages/@aws-cdk/cdk/lib/core/tokens/index.ts b/packages/@aws-cdk/cdk/lib/core/tokens/index.ts new file mode 100644 index 0000000000000..af82e0f1c238c --- /dev/null +++ b/packages/@aws-cdk/cdk/lib/core/tokens/index.ts @@ -0,0 +1,4 @@ +// This exports the modules that should be publicly available (not all of them) + +export * from './token'; +export * from './unresolved'; \ No newline at end of file diff --git a/packages/@aws-cdk/cdk/lib/core/tokens/options.ts b/packages/@aws-cdk/cdk/lib/core/tokens/options.ts new file mode 100644 index 0000000000000..8fb5bc90eee16 --- /dev/null +++ b/packages/@aws-cdk/cdk/lib/core/tokens/options.ts @@ -0,0 +1,56 @@ +import { Token } from "./token"; + +/** + * Function used to preprocess Tokens before resolving + */ +export type CollectFunc = (token: Token) => void; + +/** + * Global options for resolve() + * + * Because there are many independent calls to resolve(), some losing context, + * we cannot simply pass through options at each individual call. Instead, + * we configure global context at the stack synthesis level. + */ +export class ResolveConfiguration { + private readonly options = new Array(); + + public push(options: ResolveOptions): IOptionsContext { + this.options.push(options); + + return { + pop: () => { + if (this.options.length === 0 || this.options[this.options.length - 1] !== options) { + throw new Error('ResolveConfiguration push/pop mismatch'); + } + this.options.pop(); + } + }; + } + + public get collect(): CollectFunc | undefined { + for (let i = this.options.length - 1; i >= 0; i--) { + const ret = this.options[i].collect; + if (ret !== undefined) { return ret; } + } + return undefined; + } +} + +interface IOptionsContext { + pop(): void; +} + +interface ResolveOptions { + /** + * What function to use to preprocess Tokens before resolving them + */ + collect?: CollectFunc; +} + +const glob = global as any; + +/** + * Singleton instance of resolver options + */ +export const RESOLVE_OPTIONS: ResolveConfiguration = glob.__cdkResolveOptions = glob.__cdkResolveOptions || new ResolveConfiguration(); diff --git a/packages/@aws-cdk/cdk/lib/core/tokens/resolve.ts b/packages/@aws-cdk/cdk/lib/core/tokens/resolve.ts new file mode 100644 index 0000000000000..9fec0fc3c12ac --- /dev/null +++ b/packages/@aws-cdk/cdk/lib/core/tokens/resolve.ts @@ -0,0 +1,145 @@ +import { containsListToken, TOKEN_MAP } from "./encoding"; +import { RESOLVE_OPTIONS } from "./options"; +import { RESOLVE_METHOD, ResolveContext, Token } from "./token"; +import { unresolved } from "./unresolved"; + +// This file should not be exported to consumers, resolving should happen through Construct.resolve() + +/** + * Resolves an object by evaluating all tokens and removing any undefined or empty objects or arrays. + * Values can only be primitives, arrays or tokens. Other objects (i.e. with methods) will be rejected. + * + * @param obj The object to resolve. + * @param prefix Prefix key path components for diagnostics. + */ +export function resolve(obj: any, context: ResolveContext): any { + const pathName = '/' + context.prefix.join('/'); + + // protect against cyclic references by limiting depth. + if (context.prefix.length > 200) { + throw new Error('Unable to resolve object tree with circular reference. Path: ' + pathName); + } + + // + // undefined + // + + if (typeof(obj) === 'undefined') { + return undefined; + } + + // + // null + // + + if (obj === null) { + return null; + } + + // + // functions - not supported (only tokens are supported) + // + + if (typeof(obj) === 'function') { + throw new Error(`Trying to resolve a non-data object. Only token are supported for lazy evaluation. Path: ${pathName}. Object: ${obj}`); + } + + // + // string - potentially replace all stringified Tokens + // + if (typeof(obj) === 'string') { + return TOKEN_MAP.resolveStringTokens(obj, context); + } + + // + // primitives - as-is + // + + if (typeof(obj) !== 'object' || obj instanceof Date) { + return obj; + } + + // + // arrays - resolve all values, remove undefined and remove empty arrays + // + + if (Array.isArray(obj)) { + if (containsListToken(obj)) { + return TOKEN_MAP.resolveListTokens(obj, context); + } + + const arr = obj + .map((x, i) => resolve(x, { ...context, prefix: context.prefix.concat(i.toString()) })) + .filter(x => typeof(x) !== 'undefined'); + + return arr; + } + + // + // tokens - invoke 'resolve' and continue to resolve recursively + // + + if (unresolved(obj)) { + const collect = RESOLVE_OPTIONS.collect; + if (collect) { collect(obj); } + const value = obj[RESOLVE_METHOD](context); + return resolve(value, context); + } + + // + // objects - deep-resolve all values + // + + // Must not be a Construct at this point, otherwise you probably made a typo + // mistake somewhere and resolve will get into an infinite loop recursing into + // child.parent <---> parent.children + if (isConstruct(obj)) { + throw new Error('Trying to resolve() a Construct at ' + pathName); + } + + const result: any = { }; + for (const key of Object.keys(obj)) { + const resolvedKey = resolve(key, context); + if (typeof(resolvedKey) !== 'string') { + throw new Error(`The key "${key}" has been resolved to ${JSON.stringify(resolvedKey)} but must be resolvable to a string`); + } + + const value = resolve(obj[key], {...context, prefix: context.prefix.concat(key) }); + + // skip undefined + if (typeof(value) === 'undefined') { + continue; + } + + result[resolvedKey] = value; + } + + return result; +} + +/** + * Find all Tokens that are used in the given structure + */ +export function findTokens(obj: any, context: ResolveContext): Token[] { + const ret = new Array(); + + const options = RESOLVE_OPTIONS.push({ collect: ret.push.bind(ret) }); + try { + // resolve() for side effect of calling 'preProcess', which adds to the + resolve(obj, context); + } finally { + options.pop(); + } + + return ret; +} + +/** + * Determine whether an object is a Construct + * + * Not in 'construct.ts' because that would lead to a dependency cycle via 'uniqueid.ts', + * and this is a best-effort protection against a common programming mistake anyway. + */ +function isConstruct(x: any): boolean { + return x._children !== undefined && x._metadata !== undefined; +} \ No newline at end of file diff --git a/packages/@aws-cdk/cdk/lib/core/tokens/token.ts b/packages/@aws-cdk/cdk/lib/core/tokens/token.ts new file mode 100644 index 0000000000000..35924a457f717 --- /dev/null +++ b/packages/@aws-cdk/cdk/lib/core/tokens/token.ts @@ -0,0 +1,133 @@ +import { IConstruct } from "../construct"; +import { TOKEN_MAP } from "./encoding"; + +/** + * If objects has a function property by this name, they will be considered tokens, and this + * function will be called to resolve the value for this object. + */ +export const RESOLVE_METHOD = 'resolve'; + +/** + * Represents a special or lazily-evaluated value. + * + * Can be used to delay evaluation of a certain value in case, for example, + * that it requires some context or late-bound data. Can also be used to + * mark values that need special processing at document rendering time. + * + * Tokens can be embedded into strings while retaining their original + * semantics. + */ +export class Token { + /** + * Indicate whether this Token represent a "reference" + * + * The Construct tree can be queried for the Reference Tokens that + * are used in it. + */ + public readonly isReference?: boolean; + + private tokenStringification?: string; + private tokenListification?: string[]; + + /** + * Creates a token that resolves to `value`. + * + * If value is a function, the function is evaluated upon resolution and + * the value it returns will be used as the token's value. + * + * displayName is used to represent the Token when it's embedded into a string; it + * will look something like this: + * + * "embedded in a larger string is ${Token[DISPLAY_NAME.123]}" + * + * This value is used as a hint to humans what the meaning of the Token is, + * and does not have any effect on the evaluation. + * + * Must contain only alphanumeric and simple separator characters (_.:-). + * + * @param valueOrFunction What this token will evaluate to, literal or function. + * @param displayName A human-readable display hint for this Token + */ + constructor(private readonly valueOrFunction?: any, private readonly displayName?: string) { + } + + /** + * @returns The resolved value for this token. + */ + public resolve(_context: ResolveContext): any { + let value = this.valueOrFunction; + if (typeof(value) === 'function') { + value = value(); + } + + return value; + } + + /** + * Return a reversible string representation of this token + * + * If the Token is initialized with a literal, the stringified value of the + * literal is returned. Otherwise, a special quoted string representation + * of the Token is returned that can be embedded into other strings. + * + * Strings with quoted Tokens in them can be restored back into + * complex values with the Tokens restored by calling `resolve()` + * on the string. + */ + public toString(): string { + const valueType = typeof this.valueOrFunction; + // Optimization: if we can immediately resolve this, don't bother + // registering a Token. + if (valueType === 'string' || valueType === 'number' || valueType === 'boolean') { + return this.valueOrFunction.toString(); + } + + if (this.tokenStringification === undefined) { + this.tokenStringification = TOKEN_MAP.registerString(this, this.displayName); + } + return this.tokenStringification; + } + + /** + * Turn this Token into JSON + * + * This gets called by JSON.stringify(). We want to prohibit this, because + * it's not possible to do this properly, so we just throw an error here. + */ + public toJSON(): any { + // tslint:disable-next-line:max-line-length + throw new Error('JSON.stringify() cannot be applied to structure with a Token in it. Use this.node.stringifyJson() instead.'); + } + + /** + * Return a string list representation of this token + * + * Call this if the Token intrinsically evaluates to a list of strings. + * If so, you can represent the Token in a similar way in the type + * system. + * + * Note that even though the Token is represented as a list of strings, you + * still cannot do any operations on it such as concatenation, indexing, + * or taking its length. The only useful operations you can do to these lists + * is constructing a `FnJoin` or a `FnSelect` on it. + */ + public toList(): string[] { + const valueType = typeof this.valueOrFunction; + if (valueType === 'string' || valueType === 'number' || valueType === 'boolean') { + throw new Error('Got a literal Token value; only intrinsics can ever evaluate to lists.'); + } + + if (this.tokenListification === undefined) { + this.tokenListification = TOKEN_MAP.registerList(this, this.displayName); + } + return this.tokenListification; + } +} + +/** + * Current resolution context for tokens + */ +export interface ResolveContext { + scope: IConstruct; + prefix: string[]; +} \ No newline at end of file diff --git a/packages/@aws-cdk/cdk/lib/core/tokens/unresolved.ts b/packages/@aws-cdk/cdk/lib/core/tokens/unresolved.ts new file mode 100644 index 0000000000000..1262f6f86c0fd --- /dev/null +++ b/packages/@aws-cdk/cdk/lib/core/tokens/unresolved.ts @@ -0,0 +1,18 @@ +import { isListToken, TOKEN_MAP } from "./encoding"; +import { RESOLVE_METHOD } from "./token"; + +/** + * Returns true if obj is a token (i.e. has the resolve() method or is a string + * that includes token markers), or it's a listifictaion of a Token string. + * + * @param obj The object to test. + */ +export function unresolved(obj: any): boolean { + if (typeof(obj) === 'string') { + return TOKEN_MAP.createStringTokenString(obj).test(); + } else if (Array.isArray(obj) && obj.length === 1) { + return isListToken(obj[0]); + } else { + return obj && typeof(obj[RESOLVE_METHOD]) === 'function'; + } +} diff --git a/packages/@aws-cdk/cdk/lib/core/util.ts b/packages/@aws-cdk/cdk/lib/core/util.ts index d1be7700e7c26..e083ddc5fc271 100644 --- a/packages/@aws-cdk/cdk/lib/core/util.ts +++ b/packages/@aws-cdk/cdk/lib/core/util.ts @@ -1,18 +1,18 @@ -import { resolve } from './tokens'; +import { IConstruct } from "./construct"; /** * Given an object, converts all keys to PascalCase given they are currently in camel case. * @param obj The object. */ -export function capitalizePropertyNames(obj: any): any { - obj = resolve(obj); +export function capitalizePropertyNames(construct: IConstruct, obj: any): any { + obj = construct.node.resolve(obj); if (typeof(obj) !== 'object') { return obj; } if (Array.isArray(obj)) { - return obj.map(x => capitalizePropertyNames(x)); + return obj.map(x => capitalizePropertyNames(construct, x)); } const newObj: any = { }; @@ -21,7 +21,7 @@ export function capitalizePropertyNames(obj: any): any { const first = key.charAt(0).toUpperCase(); const newKey = first + key.slice(1); - newObj[newKey] = capitalizePropertyNames(value); + newObj[newKey] = capitalizePropertyNames(construct, value); } return newObj; @@ -30,8 +30,8 @@ export function capitalizePropertyNames(obj: any): any { /** * Turns empty arrays/objects to undefined (after evaluating tokens). */ -export function ignoreEmpty(o: any): any { - o = resolve(o); // first resolve tokens, in case they evaluate to 'undefined'. +export function ignoreEmpty(construct: IConstruct, o: any): any { + o = construct.node.resolve(o); // first resolve tokens, in case they evaluate to 'undefined'. // undefined/null if (o == null) { diff --git a/packages/@aws-cdk/cdk/lib/index.ts b/packages/@aws-cdk/cdk/lib/index.ts index e04b06a8812d8..72e4e00592b2d 100644 --- a/packages/@aws-cdk/cdk/lib/index.ts +++ b/packages/@aws-cdk/cdk/lib/index.ts @@ -3,7 +3,7 @@ export * from './core/tokens'; export * from './core/tag-manager'; export * from './cloudformation/cloudformation-json'; -export * from './cloudformation/cloudformation-token'; +export * from './cloudformation/cfn-tokens'; export * from './cloudformation/condition'; export * from './cloudformation/fn'; export * from './cloudformation/include'; @@ -16,6 +16,7 @@ export * from './cloudformation/resource'; export * from './cloudformation/resource-policy'; export * from './cloudformation/rule'; export * from './cloudformation/stack'; +export * from './cloudformation/stack-element'; export * from './cloudformation/dynamic-reference'; export * from './cloudformation/tag'; export * from './cloudformation/removal-policy'; diff --git a/packages/@aws-cdk/cdk/lib/runtime.ts b/packages/@aws-cdk/cdk/lib/runtime.ts index 7087a5cee19ad..e8fb8ba893064 100644 --- a/packages/@aws-cdk/cdk/lib/runtime.ts +++ b/packages/@aws-cdk/cdk/lib/runtime.ts @@ -138,7 +138,7 @@ export class ValidationResult { let message = this.errorTree(); // The first letter will be lowercase, so uppercase it for a nicer error message message = message.substr(0, 1).toUpperCase() + message.substr(1); - throw new TypeError(message); + throw new CfnSynthesisError(message); } } @@ -384,3 +384,8 @@ function isCloudFormationIntrinsic(x: any) { return keys[0] === 'Ref' || keys[0].substr(0, 4) === 'Fn::'; } + +// Cannot be public because JSII gets confused about es5.d.ts +class CfnSynthesisError extends Error { + public readonly type = 'CfnSynthesisError'; +} \ No newline at end of file diff --git a/packages/@aws-cdk/cdk/lib/util/uniqueid.ts b/packages/@aws-cdk/cdk/lib/util/uniqueid.ts index b9a9e34eb97fe..6f58d44bd7471 100644 --- a/packages/@aws-cdk/cdk/lib/util/uniqueid.ts +++ b/packages/@aws-cdk/cdk/lib/util/uniqueid.ts @@ -1,6 +1,5 @@ // tslint:disable-next-line:no-var-requires import crypto = require('crypto'); -import { unresolved } from '../core/tokens'; /** * Resources with this ID are hidden from humans @@ -36,7 +35,8 @@ export function makeUniqueId(components: string[]) { throw new Error('Unable to calculate a unique id for an empty set of components'); } - const unresolvedTokens = components.filter(c => unresolved(c)); + // Lazy require in order to break a module dependency cycle + const unresolvedTokens = components.filter(c => require('../core/tokens').unresolved(c)); if (unresolvedTokens.length > 0) { throw new Error(`ID components may not include unresolved tokens: ${unresolvedTokens.join(',')}`); } diff --git a/packages/@aws-cdk/cdk/test/cloudformation/test.arn.ts b/packages/@aws-cdk/cdk/test/cloudformation/test.arn.ts index 18b90b460db69..cbb3c3caa96de 100644 --- a/packages/@aws-cdk/cdk/test/cloudformation/test.arn.ts +++ b/packages/@aws-cdk/cdk/test/cloudformation/test.arn.ts @@ -1,20 +1,26 @@ import { Test } from 'nodeunit'; -import { ArnComponents, ArnUtils, AwsAccountId, AwsPartition, AwsRegion, resolve, Token } from '../../lib'; +import { ArnComponents, Aws, Stack, Token } from '../../lib'; export = { 'create from components with defaults'(test: Test) { - const arn = ArnUtils.fromComponents({ + const stack = new Stack(); + + const arn = stack.formatArn({ service: 'sqs', resource: 'myqueuename' }); - test.deepEqual(resolve(arn), - resolve(`arn:${new AwsPartition()}:sqs:${new AwsRegion()}:${new AwsAccountId()}:myqueuename`)); + const pseudo = new Aws(stack); + + test.deepEqual(stack.node.resolve(arn), + stack.node.resolve(`arn:${pseudo.partition}:sqs:${pseudo.region}:${pseudo.accountId}:myqueuename`)); test.done(); }, 'create from components with specific values for the various components'(test: Test) { - const arn = ArnUtils.fromComponents({ + const stack = new Stack(); + + const arn = stack.formatArn({ service: 'dynamodb', resource: 'table', account: '123456789012', @@ -23,13 +29,15 @@ export = { resourceName: 'mytable/stream/label' }); - test.deepEqual(resolve(arn), + test.deepEqual(stack.node.resolve(arn), 'arn:aws-cn:dynamodb:us-east-1:123456789012:table/mytable/stream/label'); test.done(); }, 'allow empty string in components'(test: Test) { - const arn = ArnUtils.fromComponents({ + const stack = new Stack(); + + const arn = stack.formatArn({ service: 's3', resource: 'my-bucket', account: '', @@ -37,27 +45,33 @@ export = { partition: 'aws-cn', }); - test.deepEqual(resolve(arn), + test.deepEqual(stack.node.resolve(arn), 'arn:aws-cn:s3:::my-bucket'); test.done(); }, 'resourcePathSep can be set to ":" instead of the default "/"'(test: Test) { - const arn = ArnUtils.fromComponents({ + const stack = new Stack(); + + const arn = stack.formatArn({ service: 'codedeploy', resource: 'application', sep: ':', resourceName: 'WordPress_App' }); - test.deepEqual(resolve(arn), - resolve(`arn:${new AwsPartition()}:codedeploy:${new AwsRegion()}:${new AwsAccountId()}:application:WordPress_App`)); + const pseudo = new Aws(stack); + + test.deepEqual(stack.node.resolve(arn), + stack.node.resolve(`arn:${pseudo.partition}:codedeploy:${pseudo.region}:${pseudo.accountId}:application:WordPress_App`)); test.done(); }, 'fails if resourcePathSep is neither ":" nor "/"'(test: Test) { - test.throws(() => ArnUtils.fromComponents({ + const stack = new Stack(); + + test.throws(() => stack.formatArn({ service: 'foo', resource: 'bar', sep: 'x' })); @@ -68,27 +82,32 @@ export = { 'fails': { 'if doesn\'t start with "arn:"'(test: Test) { - test.throws(() => ArnUtils.parse("barn:foo:x:a:1:2"), /ARNs must start with "arn:": barn:foo/); + const stack = new Stack(); + test.throws(() => stack.parseArn("barn:foo:x:a:1:2"), /ARNs must start with "arn:": barn:foo/); test.done(); }, 'if the ARN doesnt have enough components'(test: Test) { - test.throws(() => ArnUtils.parse('arn:is:too:short'), /ARNs must have at least 6 components: arn:is:too:short/); + const stack = new Stack(); + test.throws(() => stack.parseArn('arn:is:too:short'), /ARNs must have at least 6 components: arn:is:too:short/); test.done(); }, 'if "service" is not specified'(test: Test) { - test.throws(() => ArnUtils.parse('arn:aws::4:5:6'), /The `service` component \(3rd component\) is required/); + const stack = new Stack(); + test.throws(() => stack.parseArn('arn:aws::4:5:6'), /The `service` component \(3rd component\) is required/); test.done(); }, 'if "resource" is not specified'(test: Test) { - test.throws(() => ArnUtils.parse('arn:aws:service:::'), /The `resource` component \(6th component\) is required/); + const stack = new Stack(); + test.throws(() => stack.parseArn('arn:aws:service:::'), /The `resource` component \(6th component\) is required/); test.done(); } }, 'various successful parses'(test: Test) { + const stack = new Stack(); const tests: { [arn: string]: ArnComponents } = { 'arn:aws:a4b:region:accountid:resourcetype/resource': { partition: 'aws', @@ -130,37 +149,39 @@ export = { Object.keys(tests).forEach(arn => { const expected = tests[arn]; - test.deepEqual(ArnUtils.parse(arn), expected, arn); + test.deepEqual(stack.parseArn(arn), expected, arn); }); test.done(); }, 'a Token with : separator'(test: Test) { + const stack = new Stack(); const theToken = { Ref: 'SomeParameter' }; - const parsed = ArnUtils.parseToken(new Token(() => theToken).toString(), ':'); - - test.deepEqual(resolve(parsed.partition), { 'Fn::Select': [ 1, { 'Fn::Split': [ ':', theToken ]} ]}); - test.deepEqual(resolve(parsed.service), { 'Fn::Select': [ 2, { 'Fn::Split': [ ':', theToken ]} ]}); - test.deepEqual(resolve(parsed.region), { 'Fn::Select': [ 3, { 'Fn::Split': [ ':', theToken ]} ]}); - test.deepEqual(resolve(parsed.account), { 'Fn::Select': [ 4, { 'Fn::Split': [ ':', theToken ]} ]}); - test.deepEqual(resolve(parsed.resource), { 'Fn::Select': [ 5, { 'Fn::Split': [ ':', theToken ]} ]}); - test.deepEqual(resolve(parsed.resourceName), { 'Fn::Select': [ 6, { 'Fn::Split': [ ':', theToken ]} ]}); + const parsed = stack.parseArn(new Token(() => theToken).toString(), ':'); + + test.deepEqual(stack.node.resolve(parsed.partition), { 'Fn::Select': [ 1, { 'Fn::Split': [ ':', theToken ]} ]}); + test.deepEqual(stack.node.resolve(parsed.service), { 'Fn::Select': [ 2, { 'Fn::Split': [ ':', theToken ]} ]}); + test.deepEqual(stack.node.resolve(parsed.region), { 'Fn::Select': [ 3, { 'Fn::Split': [ ':', theToken ]} ]}); + test.deepEqual(stack.node.resolve(parsed.account), { 'Fn::Select': [ 4, { 'Fn::Split': [ ':', theToken ]} ]}); + test.deepEqual(stack.node.resolve(parsed.resource), { 'Fn::Select': [ 5, { 'Fn::Split': [ ':', theToken ]} ]}); + test.deepEqual(stack.node.resolve(parsed.resourceName), { 'Fn::Select': [ 6, { 'Fn::Split': [ ':', theToken ]} ]}); test.equal(parsed.sep, ':'); test.done(); }, 'a Token with / separator'(test: Test) { + const stack = new Stack(); const theToken = { Ref: 'SomeParameter' }; - const parsed = ArnUtils.parseToken(new Token(() => theToken).toString()); + const parsed = stack.parseArn(new Token(() => theToken).toString()); test.equal(parsed.sep, '/'); // tslint:disable-next-line:max-line-length - test.deepEqual(resolve(parsed.resource), { 'Fn::Select': [ 0, { 'Fn::Split': [ '/', { 'Fn::Select': [ 5, { 'Fn::Split': [ ':', theToken ]} ]} ]} ]}); + test.deepEqual(stack.node.resolve(parsed.resource), { 'Fn::Select': [ 0, { 'Fn::Split': [ '/', { 'Fn::Select': [ 5, { 'Fn::Split': [ ':', theToken ]} ]} ]} ]}); // tslint:disable-next-line:max-line-length - test.deepEqual(resolve(parsed.resourceName), { 'Fn::Select': [ 1, { 'Fn::Split': [ '/', { 'Fn::Select': [ 5, { 'Fn::Split': [ ':', theToken ]} ]} ]} ]}); + test.deepEqual(stack.node.resolve(parsed.resourceName), { 'Fn::Select': [ 1, { 'Fn::Split': [ '/', { 'Fn::Select': [ 5, { 'Fn::Split': [ ':', theToken ]} ]} ]} ]}); test.done(); } diff --git a/packages/@aws-cdk/cdk/test/cloudformation/test.cloudformation-json.ts b/packages/@aws-cdk/cdk/test/cloudformation/test.cloudformation-json.ts index ecc52f95aea52..440e782af6ac6 100644 --- a/packages/@aws-cdk/cdk/test/cloudformation/test.cloudformation-json.ts +++ b/packages/@aws-cdk/cdk/test/cloudformation/test.cloudformation-json.ts @@ -1,5 +1,5 @@ import { Test } from 'nodeunit'; -import { CloudFormationJSON, CloudFormationToken, Fn, resolve, Token } from '../../lib'; +import { CloudFormationJSON, Fn, Stack, Token } from '../../lib'; import { evaluateCFN } from './evaluate-cfn'; export = { @@ -16,12 +16,14 @@ export = { }, 'string tokens can be JSONified and JSONification can be reversed'(test: Test) { + const stack = new Stack(); + for (const token of tokensThatResolveTo('woof woof')) { // GIVEN const fido = { name: 'Fido', speaks: token }; // WHEN - const resolved = resolve(CloudFormationJSON.stringify(fido)); + const resolved = stack.node.resolve(CloudFormationJSON.stringify(fido, stack)); // THEN test.deepEqual(evaluateCFN(resolved), '{"name":"Fido","speaks":"woof woof"}'); @@ -31,12 +33,14 @@ export = { }, 'string tokens can be embedded while being JSONified'(test: Test) { + const stack = new Stack(); + for (const token of tokensThatResolveTo('woof woof')) { // GIVEN const fido = { name: 'Fido', speaks: `deep ${token}` }; // WHEN - const resolved = resolve(CloudFormationJSON.stringify(fido)); + const resolved = stack.node.resolve(CloudFormationJSON.stringify(fido, stack)); // THEN test.deepEqual(evaluateCFN(resolved), '{"name":"Fido","speaks":"deep woof woof"}'); @@ -47,25 +51,27 @@ export = { 'integer Tokens behave correctly in stringification and JSONification'(test: Test) { // GIVEN + const stack = new Stack(); const num = new Token(() => 1); const embedded = `the number is ${num}`; // WHEN - test.equal(evaluateCFN(resolve(embedded)), "the number is 1"); - test.equal(evaluateCFN(resolve(CloudFormationJSON.stringify({ embedded }))), "{\"embedded\":\"the number is 1\"}"); - test.equal(evaluateCFN(resolve(CloudFormationJSON.stringify({ num }))), "{\"num\":1}"); + test.equal(evaluateCFN(stack.node.resolve(embedded)), "the number is 1"); + test.equal(evaluateCFN(stack.node.resolve(CloudFormationJSON.stringify({ embedded }, stack))), "{\"embedded\":\"the number is 1\"}"); + test.equal(evaluateCFN(stack.node.resolve(CloudFormationJSON.stringify({ num }, stack))), "{\"num\":1}"); test.done(); }, 'tokens in strings survive additional TokenJSON.stringification()'(test: Test) { // GIVEN + const stack = new Stack(); for (const token of tokensThatResolveTo('pong!')) { // WHEN - const stringified = CloudFormationJSON.stringify(`ping? ${token}`); + const stringified = CloudFormationJSON.stringify(`ping? ${token}`, stack); // THEN - test.equal(evaluateCFN(resolve(stringified)), '"ping? pong!"'); + test.equal(evaluateCFN(stack.node.resolve(stringified)), '"ping? pong!"'); } test.done(); @@ -73,10 +79,11 @@ export = { 'intrinsic Tokens embed correctly in JSONification'(test: Test) { // GIVEN - const bucketName = new CloudFormationToken({ Ref: 'MyBucket' }); + const stack = new Stack(); + const bucketName = new Token({ Ref: 'MyBucket' }); // WHEN - const resolved = resolve(CloudFormationJSON.stringify({ theBucket: bucketName })); + const resolved = stack.node.resolve(CloudFormationJSON.stringify({ theBucket: bucketName }, stack)); // THEN const context = {MyBucket: 'TheName'}; @@ -86,14 +93,15 @@ export = { }, 'embedded string literals in intrinsics are escaped when calling TokenJSON.stringify()'(test: Test) { - // WHEN + // GIVEN + const stack = new Stack(); const token = Fn.join('', [ 'Hello', 'This\nIs', 'Very "cool"' ]); // WHEN - const resolved = resolve(CloudFormationJSON.stringify({ + const resolved = stack.node.resolve(CloudFormationJSON.stringify({ literal: 'I can also "contain" quotes', token - })); + }, stack)); // THEN const expected = '{"literal":"I can also \\"contain\\" quotes","token":"HelloThis\\nIsVery \\"cool\\""}'; @@ -104,11 +112,12 @@ export = { 'Tokens in Tokens are handled correctly'(test: Test) { // GIVEN - const bucketName = new CloudFormationToken({ Ref: 'MyBucket' }); + const stack = new Stack(); + const bucketName = new Token({ Ref: 'MyBucket' }); const combinedName = Fn.join('', [ 'The bucket name is ', bucketName.toString() ]); // WHEN - const resolved = resolve(CloudFormationJSON.stringify({ theBucket: combinedName })); + const resolved = stack.node.resolve(CloudFormationJSON.stringify({ theBucket: combinedName }, stack)); // THEN const context = {MyBucket: 'TheName'}; @@ -119,12 +128,13 @@ export = { 'Doubly nested strings evaluate correctly in JSON context'(test: Test) { // WHEN + const stack = new Stack(); const fidoSays = new Token(() => 'woof'); // WHEN - const resolved = resolve(CloudFormationJSON.stringify({ + const resolved = stack.node.resolve(CloudFormationJSON.stringify({ information: `Did you know that Fido says: ${fidoSays}` - })); + }, stack)); // THEN test.deepEqual(evaluateCFN(resolved), '{"information":"Did you know that Fido says: woof"}'); @@ -133,13 +143,14 @@ export = { }, 'Doubly nested intrinsics evaluate correctly in JSON context'(test: Test) { - // WHEN - const fidoSays = new CloudFormationToken(() => ({ Ref: 'Something' })); + // GIVEN + const stack = new Stack(); + const fidoSays = new Token(() => ({ Ref: 'Something' })); // WHEN - const resolved = resolve(CloudFormationJSON.stringify({ + const resolved = stack.node.resolve(CloudFormationJSON.stringify({ information: `Did you know that Fido says: ${fidoSays}` - })); + }, stack)); // THEN const context = {Something: 'woof woof'}; @@ -149,13 +160,14 @@ export = { }, 'Quoted strings in embedded JSON context are escaped'(test: Test) { - // WHEN + // GIVEN + const stack = new Stack(); const fidoSays = new Token(() => '"woof"'); // WHEN - const resolved = resolve(CloudFormationJSON.stringify({ + const resolved = stack.node.resolve(CloudFormationJSON.stringify({ information: `Did you know that Fido says: ${fidoSays}` - })); + }, stack)); // THEN test.deepEqual(evaluateCFN(resolved), '{"information":"Did you know that Fido says: \\"woof\\""}'); diff --git a/packages/@aws-cdk/cdk/test/cloudformation/test.dynamic-reference.ts b/packages/@aws-cdk/cdk/test/cloudformation/test.dynamic-reference.ts index 4dc2f2767f40d..37b3751b3c167 100644 --- a/packages/@aws-cdk/cdk/test/cloudformation/test.dynamic-reference.ts +++ b/packages/@aws-cdk/cdk/test/cloudformation/test.dynamic-reference.ts @@ -1,5 +1,5 @@ import { Test } from 'nodeunit'; -import { DynamicReference, DynamicReferenceService, resolve, Stack } from '../../lib'; +import { DynamicReference, DynamicReferenceService, Stack } from '../../lib'; export = { 'can create dynamic references with service and key with colons'(test: Test) { @@ -13,7 +13,7 @@ export = { }); // THEN - test.equal(resolve(ref.value), '{{resolve:ssm:a:b:c}}'); + test.equal(stack.node.resolve(ref.value), '{{resolve:ssm:a:b:c}}'); test.done(); }, diff --git a/packages/@aws-cdk/cdk/test/cloudformation/test.fn.ts b/packages/@aws-cdk/cdk/test/cloudformation/test.fn.ts index afb247fbebd57..adb82677c5248 100644 --- a/packages/@aws-cdk/cdk/test/cloudformation/test.fn.ts +++ b/packages/@aws-cdk/cdk/test/cloudformation/test.fn.ts @@ -1,9 +1,7 @@ import fc = require('fast-check'); import _ = require('lodash'); import nodeunit = require('nodeunit'); -import { CloudFormationToken } from '../../lib'; -import { Fn } from '../../lib/cloudformation/fn'; -import { resolve } from '../../lib/core/tokens'; +import { Fn, Stack, Token } from '../../lib'; function asyncTest(cb: (test: nodeunit.Test) => Promise): (test: nodeunit.Test) => void { return async (test: nodeunit.Test) => { @@ -31,36 +29,59 @@ export = nodeunit.testCase({ test.throws(() => Fn.join('.', [])); test.done(); }, + 'collapse nested FnJoins even if they contain tokens'(test: nodeunit.Test) { + const stack = new Stack(); + + const obj = Fn.join('', [ + 'a', + Fn.join('', [Fn.getAtt('a', 'bc').toString(), 'c']), + 'd' + ]); + + test.deepEqual(stack.node.resolve(obj), { 'Fn::Join': [ "", + [ + "a", + { 'Fn::GetAtt': ['a', 'bc'] }, + 'cd', + ] + ]}); + + test.done(); + }, 'resolves to the value if only one value is joined': asyncTest(async () => { + const stack = new Stack(); await fc.assert( fc.property( fc.string(), anyValue, - (delimiter, value) => _.isEqual(resolve(Fn.join(delimiter, [value])), value) + (delimiter, value) => _.isEqual(stack.node.resolve(Fn.join(delimiter, [value])), value) ), { verbose: true } ); }), 'pre-concatenates string literals': asyncTest(async () => { + const stack = new Stack(); await fc.assert( fc.property( fc.string(), fc.array(nonEmptyString, 1, 15), - (delimiter, values) => resolve(Fn.join(delimiter, values)) === values.join(delimiter) + (delimiter, values) => stack.node.resolve(Fn.join(delimiter, values)) === values.join(delimiter) ), { verbose: true } ); }), 'pre-concatenates around tokens': asyncTest(async () => { + const stack = new Stack(); await fc.assert( fc.property( fc.string(), fc.array(nonEmptyString, 1, 3), tokenish, fc.array(nonEmptyString, 1, 3), (delimiter, prefix, obj, suffix) => - _.isEqual(resolve(Fn.join(delimiter, [...prefix, stringToken(obj), ...suffix])), + _.isEqual(stack.node.resolve(Fn.join(delimiter, [...prefix, stringToken(obj), ...suffix])), { 'Fn::Join': [delimiter, [prefix.join(delimiter), obj, suffix.join(delimiter)]] }) ), { verbose: true, seed: 1539874645005, path: "0:0:0:0:0:0:0:0:0" } ); }), 'flattens joins nested under joins with same delimiter': asyncTest(async () => { + const stack = new Stack(); await fc.assert( fc.property( fc.string(), fc.array(anyValue), @@ -68,13 +89,14 @@ export = nodeunit.testCase({ fc.array(anyValue), (delimiter, prefix, nested, suffix) => // Gonna test - _.isEqual(resolve(Fn.join(delimiter, [...prefix, Fn.join(delimiter, nested), ...suffix])), - resolve(Fn.join(delimiter, [...prefix, ...nested, ...suffix]))) + _.isEqual(stack.node.resolve(Fn.join(delimiter, [...prefix, Fn.join(delimiter, nested), ...suffix])), + stack.node.resolve(Fn.join(delimiter, [...prefix, ...nested, ...suffix]))) ), { verbose: true } ); }), 'does not flatten joins nested under joins with different delimiter': asyncTest(async () => { + const stack = new Stack(); await fc.assert( fc.property( fc.string(), fc.string(), @@ -84,7 +106,7 @@ export = nodeunit.testCase({ (delimiter1, delimiter2, prefix, nested, suffix) => { fc.pre(delimiter1 !== delimiter2); const join = Fn.join(delimiter1, [...prefix, Fn.join(delimiter2, stringListToken(nested)), ...suffix]); - const resolved = resolve(join); + const resolved = stack.node.resolve(join); return resolved['Fn::Join'][1].find((e: any) => typeof e === 'object' && ('Fn::Join' in e) && e['Fn::Join'][0] === delimiter2) != null; @@ -97,8 +119,8 @@ export = nodeunit.testCase({ }); function stringListToken(o: any): string[] { - return new CloudFormationToken(o).toList(); + return new Token(o).toList(); } function stringToken(o: any): string { - return new CloudFormationToken(o).toString(); -} \ No newline at end of file + return new Token(o).toString(); +} diff --git a/packages/@aws-cdk/cdk/test/cloudformation/test.output.ts b/packages/@aws-cdk/cdk/test/cloudformation/test.output.ts index 3e9de87245942..b6ee26217d2d7 100644 --- a/packages/@aws-cdk/cdk/test/cloudformation/test.output.ts +++ b/packages/@aws-cdk/cdk/test/cloudformation/test.output.ts @@ -1,5 +1,5 @@ import { Test } from 'nodeunit'; -import { Construct, Output, Ref, resolve, Resource, Stack } from '../../lib'; +import { Construct, Output, Ref, Resource, Stack } from '../../lib'; export = { 'outputs can be added to the stack'(test: Test) { @@ -63,7 +63,7 @@ export = { 'makeImportValue can be used to create an Fn::ImportValue from an output'(test: Test) { const stack = new Stack(undefined, 'MyStack'); const output = new Output(stack, 'MyOutput'); - test.deepEqual(resolve(output.makeImportValue()), { 'Fn::ImportValue': 'MyStack:MyOutput' }); + test.deepEqual(stack.node.resolve(output.makeImportValue()), { 'Fn::ImportValue': 'MyStack:MyOutput' }); test.done(); } -}; +}; \ No newline at end of file diff --git a/packages/@aws-cdk/cdk/test/cloudformation/test.parameter.ts b/packages/@aws-cdk/cdk/test/cloudformation/test.parameter.ts index d866af8099a8a..2a5526c4255f2 100644 --- a/packages/@aws-cdk/cdk/test/cloudformation/test.parameter.ts +++ b/packages/@aws-cdk/cdk/test/cloudformation/test.parameter.ts @@ -1,5 +1,5 @@ import { Test } from 'nodeunit'; -import { Construct, Parameter, resolve, Resource, Stack } from '../../lib'; +import { Construct, Parameter, Resource, Stack } from '../../lib'; export = { 'parameters can be used and referenced using param.ref'(test: Test) { @@ -32,7 +32,7 @@ export = { const stack = new Stack(); const param = new Parameter(stack, 'MyParam', { type: 'String' }); - test.deepEqual(resolve(param), { Ref: 'MyParam' }); + test.deepEqual(stack.node.resolve(param), { Ref: 'MyParam' }); test.done(); } -}; +}; \ No newline at end of file diff --git a/packages/@aws-cdk/cdk/test/cloudformation/test.resource.ts b/packages/@aws-cdk/cdk/test/cloudformation/test.resource.ts index 9debe742f70c5..3b0ee595aa35a 100644 --- a/packages/@aws-cdk/cdk/test/cloudformation/test.resource.ts +++ b/packages/@aws-cdk/cdk/test/cloudformation/test.resource.ts @@ -2,7 +2,7 @@ import cxapi = require('@aws-cdk/cx-api'); import { Test } from 'nodeunit'; import { applyRemovalPolicy, Condition, Construct, DeletionPolicy, Fn, HashedAddressingScheme, IDependable, - RemovalPolicy, resolve, Resource, Root, Stack } from '../../lib'; + RemovalPolicy, Resource, Root, Stack } from '../../lib'; export = { 'all resources derive from Resource, which derives from Entity'(test: Test) { @@ -359,7 +359,7 @@ export = { const stack = new Stack(); const r = new Resource(stack, 'MyResource', { type: 'R' }); - test.deepEqual(resolve(r.ref), { Ref: 'MyResource' }); + test.deepEqual(stack.node.resolve(r.ref), { Ref: 'MyResource' }); test.done(); }, diff --git a/packages/@aws-cdk/cdk/test/cloudformation/test.secret.ts b/packages/@aws-cdk/cdk/test/cloudformation/test.secret.ts index 33422251adaa9..37b4dbd2bc805 100644 --- a/packages/@aws-cdk/cdk/test/cloudformation/test.secret.ts +++ b/packages/@aws-cdk/cdk/test/cloudformation/test.secret.ts @@ -1,13 +1,14 @@ import { Test } from 'nodeunit'; -import { resolve, Secret, SecretParameter, Stack } from '../../lib'; +import { Secret, SecretParameter, Stack } from '../../lib'; export = { 'Secret is merely a token'(test: Test) { + const stack = new Stack(); const foo = new Secret('Foo'); const bar = new Secret(() => 'Bar'); - test.deepEqual(resolve(foo), 'Foo'); - test.deepEqual(resolve(bar), 'Bar'); + test.deepEqual(stack.node.resolve(foo), 'Foo'); + test.deepEqual(stack.node.resolve(bar), 'Bar'); test.done(); }, @@ -43,7 +44,7 @@ export = { NoEcho: true } } }); // value resolves to a "Ref" - test.deepEqual(resolve(mySecret.value), { Ref: 'MySecretParameterBB81DE58' }); + test.deepEqual(stack.node.resolve(mySecret.value), { Ref: 'MySecretParameterBB81DE58' }); test.done(); } diff --git a/packages/@aws-cdk/cdk/test/cloudformation/test.stack.ts b/packages/@aws-cdk/cdk/test/cloudformation/test.stack.ts index 1561dfea92e0a..880068e92d589 100644 --- a/packages/@aws-cdk/cdk/test/cloudformation/test.stack.ts +++ b/packages/@aws-cdk/cdk/test/cloudformation/test.stack.ts @@ -1,5 +1,6 @@ +import cxapi = require('@aws-cdk/cx-api'); import { Test } from 'nodeunit'; -import { App, Condition, Construct, Include, Output, Parameter, Resource, Root, Stack, Token } from '../../lib'; +import { App, Aws, Condition, Construct, Include, Output, Parameter, Resource, Root, Stack, Token } from '../../lib'; export = { 'a stack can be serialized into a CloudFormation template, initially it\'s empty'(test: Test) { @@ -172,20 +173,175 @@ export = { test.done(); }, - 'Can\'t add children during synthesis'(test: Test) { - const stack = new Stack(); + 'Pseudo values attached to one stack can be referenced in another stack'(test: Test) { + // GIVEN + const app = new App(); + const stack1 = new Stack(app, 'Stack1'); + const account1 = new Aws(stack1).accountId; + const stack2 = new Stack(app, 'Stack2'); + + // WHEN - used in another stack + new Parameter(stack2, 'SomeParameter', { type: 'String', default: account1 }); + + // THEN + // Need to do this manually now, since we're in testing mode. In a normal CDK app, + // this happens as part of app.run(). + app.node.prepareTree(); + + test.deepEqual(stack1.toCloudFormation(), { + Outputs: { + ExportsOutputRefAWSAccountIdAD568057: { + Value: { Ref: 'AWS::AccountId' }, + Export: { Name: 'Stack1:ExportsOutputRefAWSAccountIdAD568057' } + } + } + }); + + test.deepEqual(stack2.toCloudFormation(), { + Parameters: { + SomeParameter: { + Type: 'String', + Default: { 'Fn::ImportValue': 'Stack1:ExportsOutputRefAWSAccountIdAD568057' } + } + } + }); + + test.done(); + }, + + 'cross-stack references in lazy tokens work'(test: Test) { + // GIVEN + const app = new App(); + const stack1 = new Stack(app, 'Stack1'); + const account1 = new Aws(stack1).accountId; + const stack2 = new Stack(app, 'Stack2'); + + // WHEN - used in another stack + new Parameter(stack2, 'SomeParameter', { type: 'String', default: new Token(() => account1) }); + + app.node.prepareTree(); + + // THEN + test.deepEqual(stack1.toCloudFormation(), { + Outputs: { + ExportsOutputRefAWSAccountIdAD568057: { + Value: { Ref: 'AWS::AccountId' }, + Export: { Name: 'Stack1:ExportsOutputRefAWSAccountIdAD568057' } + } + } + }); + + test.deepEqual(stack2.toCloudFormation(), { + Parameters: { + SomeParameter: { + Type: 'String', + Default: { 'Fn::ImportValue': 'Stack1:ExportsOutputRefAWSAccountIdAD568057' } + } + } + }); + + test.done(); + }, + + 'cross-stack references in strings work'(test: Test) { + // GIVEN + const app = new App(); + const stack1 = new Stack(app, 'Stack1'); + const account1 = new Aws(stack1).accountId; + const stack2 = new Stack(app, 'Stack2'); + + // WHEN - used in another stack + new Parameter(stack2, 'SomeParameter', { type: 'String', default: `TheAccountIs${account1}` }); + + app.node.prepareTree(); + + // THEN + test.deepEqual(stack2.toCloudFormation(), { + Parameters: { + SomeParameter: { + Type: 'String', + Default: { 'Fn::Join': [ '', [ 'TheAccountIs', { 'Fn::ImportValue': 'Stack1:ExportsOutputRefAWSAccountIdAD568057' } ]] } + } + } + }); + + test.done(); + }, + + 'cannot create cyclic reference between stacks'(test: Test) { + // GIVEN + const app = new App(); + const stack1 = new Stack(app, 'Stack1'); + const account1 = new Aws(stack1).accountId; + const stack2 = new Stack(app, 'Stack2'); + const account2 = new Aws(stack2).accountId; + + // WHEN + new Parameter(stack2, 'SomeParameter', { type: 'String', default: account1 }); + new Parameter(stack1, 'SomeParameter', { type: 'String', default: account2 }); + + test.throws(() => { + app.node.prepareTree(); + }, /Adding this dependency would create a cyclic reference/); + + test.done(); + }, + + 'stacks know about their dependencies'(test: Test) { + // GIVEN + const app = new App(); + const stack1 = new Stack(app, 'Stack1'); + const account1 = new Aws(stack1).accountId; + const stack2 = new Stack(app, 'Stack2'); + + // WHEN + new Parameter(stack2, 'SomeParameter', { type: 'String', default: account1 }); + + app.node.prepareTree(); + + // THEN + test.deepEqual(stack2.dependencies().map(s => s.node.id), ['Stack1']); + + test.done(); + }, + + 'cannot create references to stacks in other regions/accounts'(test: Test) { + // GIVEN + const app = new App(); + const stack1 = new Stack(app, 'Stack1', { env: { account: '123456789012', region: 'es-norst-1' }}); + const account1 = new Aws(stack1).accountId; + const stack2 = new Stack(app, 'Stack2', { env: { account: '123456789012', region: 'es-norst-2' }}); + + // WHEN + new Parameter(stack2, 'SomeParameter', { type: 'String', default: account1 }); + + test.throws(() => { + app.node.prepareTree(); + }, /Can only reference cross stacks in the same region and account/); + + test.done(); + }, + + 'stack with region supplied via props returns literal value'(test: Test) { + // GIVEN + const app = new App(); + const stack = new Stack(app, 'Stack1', { env: { account: '123456789012', region: 'es-norst-1' }}); + + // THEN + test.equal(stack.node.resolve(stack.region), 'es-norst-1'); + + test.done(); + }, - // add a construct with a token that when resolved adds a child. this - // means that this child is going to be added during synthesis and this - // is a no-no. - new Resource(stack, 'Resource', { type: 'T', properties: { - foo: new Token(() => new Construct(stack, 'Foo')) - }}); + 'stack with region supplied via context returns symbolic value'(test: Test) { + // GIVEN + const app = new App(); - test.throws(() => stack.toCloudFormation(), /Cannot add children during synthesis/); + app.node.setContext(cxapi.DEFAULT_REGION_CONTEXT_KEY, 'es-norst-1'); + const stack = new Stack(app, 'Stack1'); - // okay to add after synthesis - new Construct(stack, 'C1'); + // THEN + test.deepEqual(stack.node.resolve(stack.region), { Ref: 'AWS::Region' }); test.done(); }, diff --git a/packages/@aws-cdk/cdk/test/core/test.construct.ts b/packages/@aws-cdk/cdk/test/core/test.construct.ts index aa70c643d8e51..dc786b63b1288 100644 --- a/packages/@aws-cdk/cdk/test/core/test.construct.ts +++ b/packages/@aws-cdk/cdk/test/core/test.construct.ts @@ -307,13 +307,13 @@ export = { 'construct.validate() can be implemented to perform validation, construct.validateTree() will return all errors from the subtree (DFS)'(test: Test) { class MyConstruct extends Construct { - public validate() { + protected validate() { return [ 'my-error1', 'my-error2' ]; } } class YourConstruct extends Construct { - public validate() { + protected validate() { return [ 'your-error1' ]; } } @@ -325,7 +325,7 @@ export = { new YourConstruct(this, 'YourConstruct'); } - public validate() { + protected validate() { return [ 'their-error' ]; } } @@ -338,7 +338,7 @@ export = { new TheirConstruct(this, 'TheirConstruct'); } - public validate() { + protected validate() { return [ 'stack-error' ]; } } diff --git a/packages/@aws-cdk/cdk/test/core/test.tag-manager.ts b/packages/@aws-cdk/cdk/test/core/test.tag-manager.ts index f78d98f233786..71af1326f6b7b 100644 --- a/packages/@aws-cdk/cdk/test/core/test.tag-manager.ts +++ b/packages/@aws-cdk/cdk/test/core/test.tag-manager.ts @@ -1,6 +1,5 @@ import { Test } from 'nodeunit'; -import { Construct, Root } from '../../lib/core/construct'; -import { ITaggable, TagManager } from '../../lib/core/tag-manager'; +import { Construct, ITaggable, Root, TagManager } from '../../lib'; class ChildTagger extends Construct implements ITaggable { public readonly tags: TagManager; @@ -32,10 +31,10 @@ export = { const tagArray = [tag]; for (const construct of [ctagger, ctagger1]) { - test.deepEqual(construct.tags.resolve(), tagArray); + test.deepEqual(root.node.resolve(construct.tags), tagArray); } - test.deepEqual(ctagger2.tags.resolve(), undefined); + test.deepEqual(root.node.resolve(ctagger2.tags), undefined); test.done(); }, 'setTag with propagate false tags do not propagate'(test: Test) { @@ -51,10 +50,10 @@ export = { ctagger.tags.setTag(tag.key, tag.value, {propagate: false}); for (const construct of [ctagger1, ctagger2]) { - test.deepEqual(construct.tags.resolve(), undefined); + test.deepEqual(root.node.resolve(construct.tags), undefined); } - test.deepEqual(ctagger.tags.resolve()[0].key, 'Name'); - test.deepEqual(ctagger.tags.resolve()[0].value, 'TheCakeIsALie'); + test.deepEqual(root.node.resolve(ctagger.tags)[0].key, 'Name'); + test.deepEqual(root.node.resolve(ctagger.tags)[0].value, 'TheCakeIsALie'); test.done(); }, 'setTag with overwrite false does not overwrite a tag'(test: Test) { @@ -62,7 +61,7 @@ export = { const ctagger = new ChildTagger(root, 'one'); ctagger.tags.setTag('Env', 'Dev'); ctagger.tags.setTag('Env', 'Prod', {overwrite: false}); - const result = ctagger.tags.resolve(); + const result = root.node.resolve(ctagger.tags); test.deepEqual(result, [{key: 'Env', value: 'Dev'}]); test.done(); }, @@ -72,8 +71,8 @@ export = { const ctagger1 = new ChildTagger(ctagger, 'two'); ctagger.tags.setTag('Parent', 'Is always right'); ctagger1.tags.setTag('Parent', 'Is wrong', {sticky: false}); - const parent = ctagger.tags.resolve(); - const child = ctagger1.tags.resolve(); + const parent = root.node.resolve(ctagger.tags); + const child = root.node.resolve(ctagger1.tags); test.deepEqual(parent, child); test.done(); @@ -86,7 +85,7 @@ export = { const ctagger2 = new ChildTagger(cNoTag, 'four'); const tag = {key: 'Name', value: 'TheCakeIsALie'}; ctagger.tags.setTag(tag.key, tag.value, {propagate: true}); - test.deepEqual(ctagger2.tags.resolve(), [tag]); + test.deepEqual(root.node.resolve(ctagger2.tags), [tag]); test.done(); }, 'a tag can be removed and added back'(test: Test) { @@ -94,11 +93,11 @@ export = { const ctagger = new ChildTagger(root, 'one'); const tag = {key: 'Name', value: 'TheCakeIsALie'}; ctagger.tags.setTag(tag.key, tag.value, {propagate: true}); - test.deepEqual(ctagger.tags.resolve(), [tag]); + test.deepEqual(root.node.resolve(ctagger.tags), [tag]); ctagger.tags.removeTag(tag.key); - test.deepEqual(ctagger.tags.resolve(), undefined); + test.deepEqual(root.node.resolve(ctagger.tags), undefined); ctagger.tags.setTag(tag.key, tag.value, {propagate: true}); - test.deepEqual(ctagger.tags.resolve(), [tag]); + test.deepEqual(root.node.resolve(ctagger.tags), [tag]); test.done(); }, 'removeTag removes a tag by key'(test: Test) { @@ -115,7 +114,7 @@ export = { ctagger.tags.removeTag('Name'); for (const construct of [ctagger, ctagger1, ctagger2]) { - test.deepEqual(construct.tags.resolve(), undefined); + test.deepEqual(root.node.resolve(construct.tags), undefined); } test.done(); }, @@ -125,9 +124,9 @@ export = { const ctagger1 = new ChildTagger(ctagger, 'two'); ctagger.tags.setTag('Env', 'Dev'); ctagger1.tags.removeTag('Env', {blockPropagate: true}); - const result = ctagger.tags.resolve(); + const result = root.node.resolve(ctagger.tags); test.deepEqual(result, [{key: 'Env', value: 'Dev'}]); - test.deepEqual(ctagger1.tags.resolve(), undefined); + test.deepEqual(root.node.resolve(ctagger1.tags), undefined); test.done(); }, 'children can override parent propagated tags'(test: Test) { @@ -139,8 +138,8 @@ export = { ctagger.tags.setTag(tag2.key, tag2.value); ctagger.tags.setTag(tag.key, tag.value); ctagChild.tags.setTag(tag2.key, tag2.value); - const parentTags = ctagger.tags.resolve(); - const childTags = ctagChild.tags.resolve(); + const parentTags = root.node.resolve(ctagger.tags); + const childTags = root.node.resolve(ctagChild.tags); test.deepEqual(parentTags, [tag]); test.deepEqual(childTags, [tag2]); test.done(); @@ -167,11 +166,11 @@ export = { const cAll = ctagger.tags; const cProp = ctagChild.tags; - for (const tag of cAll.resolve()) { + for (const tag of root.node.resolve(cAll)) { const expectedTag = allTags.filter( (t) => (t.key === tag.key)); test.deepEqual(expectedTag[0].value, tag.value); } - for (const tag of cProp.resolve()) { + for (const tag of root.node.resolve(cProp)) { const expectedTag = tagsProp.filter( (t) => (t.key === tag.key)); test.deepEqual(expectedTag[0].value, tag.value); } diff --git a/packages/@aws-cdk/cdk/test/core/test.tokens.ts b/packages/@aws-cdk/cdk/test/core/test.tokens.ts index 341730aa91bce..dfe4a54498b0b 100644 --- a/packages/@aws-cdk/cdk/test/core/test.tokens.ts +++ b/packages/@aws-cdk/cdk/test/core/test.tokens.ts @@ -1,5 +1,5 @@ import { Test } from 'nodeunit'; -import { CloudFormationToken, Fn, resolve, Token, unresolved } from '../../lib'; +import { Fn, Root, Token, unresolved } from '../../lib'; import { evaluateCFN } from '../cloudformation/evaluate-cfn'; export = { @@ -159,7 +159,7 @@ export = { 'Tokens stringification and reversing of CloudFormation Tokens is implemented using Fn::Join'(test: Test) { // GIVEN - const token = new CloudFormationToken(() => ({ woof: 'woof' })); + const token = new Token(() => ({ woof: 'woof' })); // WHEN const stringified = `The dog says: ${token}`; @@ -259,7 +259,7 @@ export = { 'fails if token in a hash key resolves to a non-string'(test: Test) { // GIVEN - const token = new CloudFormationToken({ Ref: 'Other' }); + const token = new Token({ Ref: 'Other' }); // WHEN const s = { @@ -274,7 +274,7 @@ export = { 'list encoding': { 'can encode Token to string and resolve the encoding'(test: Test) { // GIVEN - const token = new CloudFormationToken({ Ref: 'Other' }); + const token = new Token({ Ref: 'Other' }); // WHEN const struct = { @@ -291,7 +291,7 @@ export = { 'cannot add to encoded list'(test: Test) { // GIVEN - const token = new CloudFormationToken({ Ref: 'Other' }); + const token = new Token({ Ref: 'Other' }); // WHEN const encoded: string[] = token.toList(); @@ -307,7 +307,7 @@ export = { 'cannot add to strings in encoded list'(test: Test) { // GIVEN - const token = new CloudFormationToken({ Ref: 'Other' }); + const token = new Token({ Ref: 'Other' }); // WHEN const encoded: string[] = token.toList(); @@ -323,7 +323,7 @@ export = { 'can pass encoded lists to FnSelect'(test: Test) { // GIVEN - const encoded: string[] = new CloudFormationToken({ Ref: 'Other' }).toList(); + const encoded: string[] = new Token({ Ref: 'Other' }).toList(); // WHEN const struct = Fn.select(1, encoded); @@ -338,7 +338,7 @@ export = { 'can pass encoded lists to FnJoin'(test: Test) { // GIVEN - const encoded: string[] = new CloudFormationToken({ Ref: 'Other' }).toList(); + const encoded: string[] = new Token({ Ref: 'Other' }).toList(); // WHEN const struct = Fn.join('/', encoded); @@ -348,6 +348,21 @@ export = { 'Fn::Join': ['/', { Ref: 'Other'}] }); + test.done(); + }, + + 'can pass encoded lists to FnJoin, even if join is stringified'(test: Test) { + // GIVEN + const encoded: string[] = new Token({ Ref: 'Other' }).toList(); + + // WHEN + const struct = Fn.join('/', encoded).toString(); + + // THEN + test.deepEqual(resolve(struct), { + 'Fn::Join': ['/', { Ref: 'Other'}] + }); + test.done(); } } @@ -401,8 +416,8 @@ function literalTokensThatResolveTo(value: any): Token[] { */ function cloudFormationTokensThatResolveTo(value: any): Token[] { return [ - new CloudFormationToken(value), - new CloudFormationToken(() => value) + new Token(value), + new Token(() => value) ]; } @@ -412,3 +427,12 @@ function cloudFormationTokensThatResolveTo(value: any): Token[] { function tokensThatResolveTo(value: string): Token[] { return literalTokensThatResolveTo(value).concat(cloudFormationTokensThatResolveTo(value)); } + +/** + * Wrapper for resolve that creates an throwaway Construct to call it on + * + * So I don't have to change all call sites in this file. + */ +function resolve(x: any) { + return new Root().node.resolve(x); +} \ No newline at end of file diff --git a/packages/@aws-cdk/cdk/test/core/test.util.ts b/packages/@aws-cdk/cdk/test/core/test.util.ts index 2a7cb94ec87c5..694e3ae86f456 100644 --- a/packages/@aws-cdk/cdk/test/core/test.util.ts +++ b/packages/@aws-cdk/cdk/test/core/test.util.ts @@ -1,25 +1,27 @@ import { Test } from 'nodeunit'; +import { Root } from '../../lib'; import { capitalizePropertyNames, ignoreEmpty } from '../../lib/core/util'; export = { 'capitalizeResourceProperties capitalizes all keys of an object (recursively) from camelCase to PascalCase'(test: Test) { + const c = new Root(); - test.equal(capitalizePropertyNames(undefined), undefined); - test.equal(capitalizePropertyNames(12), 12); - test.equal(capitalizePropertyNames('hello'), 'hello'); - test.deepEqual(capitalizePropertyNames([ 'hello', 88 ]), [ 'hello', 88 ]); - test.deepEqual(capitalizePropertyNames( + test.equal(capitalizePropertyNames(c, undefined), undefined); + test.equal(capitalizePropertyNames(c, 12), 12); + test.equal(capitalizePropertyNames(c, 'hello'), 'hello'); + test.deepEqual(capitalizePropertyNames(c, [ 'hello', 88 ]), [ 'hello', 88 ]); + test.deepEqual(capitalizePropertyNames(c, { Hello: 'world', hey: 'dude' }), { Hello: 'world', Hey: 'dude' }); - test.deepEqual(capitalizePropertyNames( + test.deepEqual(capitalizePropertyNames(c, [ 1, 2, { three: 3 }]), [ 1, 2, { Three: 3 }]); - test.deepEqual(capitalizePropertyNames( + test.deepEqual(capitalizePropertyNames(c, { Hello: 'world', recursive: { foo: 123, there: { another: [ 'hello', { world: 123 } ]} } }), { Hello: 'world', Recursive: { Foo: 123, There: { Another: [ 'hello', { World: 123 } ]} } }); // make sure tokens are resolved and result is also capitalized - test.deepEqual(capitalizePropertyNames( + test.deepEqual(capitalizePropertyNames(c, { hello: { resolve: () => ({ foo: 'bar' }) }, world: new SomeToken() }), { Hello: { Foo: 'bar' }, World: 100 }); @@ -29,38 +31,44 @@ export = { 'ignoreEmpty': { '[]'(test: Test) { - test.strictEqual(ignoreEmpty([]), undefined); + const c = new Root(); + test.strictEqual(ignoreEmpty(c, []), undefined); test.done(); }, '{}'(test: Test) { - test.strictEqual(ignoreEmpty({}), undefined); + const c = new Root(); + test.strictEqual(ignoreEmpty(c, {}), undefined); test.done(); }, 'undefined/null'(test: Test) { - test.strictEqual(ignoreEmpty(undefined), undefined); - test.strictEqual(ignoreEmpty(null), null); + const c = new Root(); + test.strictEqual(ignoreEmpty(c, undefined), undefined); + test.strictEqual(ignoreEmpty(c, null), null); test.done(); }, 'primitives'(test: Test) { - test.strictEqual(ignoreEmpty(12), 12); - test.strictEqual(ignoreEmpty("12"), "12"); + const c = new Root(); + test.strictEqual(ignoreEmpty(c, 12), 12); + test.strictEqual(ignoreEmpty(c, "12"), "12"); test.done(); }, 'non-empty arrays/objects'(test: Test) { - test.deepEqual(ignoreEmpty([ 1, 2, 3, undefined ]), [ 1, 2, 3 ]); // undefined array values is cleaned up by "resolve" - test.deepEqual(ignoreEmpty({ o: 1, b: 2, j: 3 }), { o: 1, b: 2, j: 3 }); + const c = new Root(); + test.deepEqual(ignoreEmpty(c, [ 1, 2, 3, undefined ]), [ 1, 2, 3 ]); // undefined array values is cleaned up by "resolve" + test.deepEqual(ignoreEmpty(c, { o: 1, b: 2, j: 3 }), { o: 1, b: 2, j: 3 }); test.done(); }, 'resolve first'(test: Test) { - test.deepEqual(ignoreEmpty({ xoo: { resolve: () => 123 }}), { xoo: 123 }); - test.strictEqual(ignoreEmpty({ xoo: { resolve: () => undefined }}), undefined); - test.deepEqual(ignoreEmpty({ xoo: { resolve: () => [ ] }}), { xoo: [] }); - test.deepEqual(ignoreEmpty({ xoo: { resolve: () => [ undefined, undefined ] }}), { xoo: [] }); + const c = new Root(); + test.deepEqual(ignoreEmpty(c, { xoo: { resolve: () => 123 }}), { xoo: 123 }); + test.strictEqual(ignoreEmpty(c, { xoo: { resolve: () => undefined }}), undefined); + test.deepEqual(ignoreEmpty(c, { xoo: { resolve: () => [ ] }}), { xoo: [] }); + test.deepEqual(ignoreEmpty(c, { xoo: { resolve: () => [ undefined, undefined ] }}), { xoo: [] }); test.done(); } } diff --git a/packages/@aws-cdk/cdk/test/test.app.ts b/packages/@aws-cdk/cdk/test/test.app.ts index 8c55bc8cc4d45..b0e5edff50db5 100644 --- a/packages/@aws-cdk/cdk/test/test.app.ts +++ b/packages/@aws-cdk/cdk/test/test.app.ts @@ -195,7 +195,7 @@ export = { 'app.synthesizeStack(stack) performs validation first (app.validateAll()) and if there are errors, it returns the errors'(test: Test) { class Child extends Construct { - public validate() { + protected validate() { return [ `Error from ${this.node.id}` ]; } } diff --git a/packages/@aws-cdk/cdk/test/test.context.ts b/packages/@aws-cdk/cdk/test/test.context.ts index 373c5e0dfcea4..0752b7fa1c2f1 100644 --- a/packages/@aws-cdk/cdk/test/test.context.ts +++ b/packages/@aws-cdk/cdk/test/test.context.ts @@ -1,7 +1,7 @@ import cxapi = require('@aws-cdk/cx-api'); import { Test } from 'nodeunit'; import { App, AvailabilityZoneProvider, Construct, ContextProvider, - MetadataEntry, resolve, SSMParameterProvider, Stack } from '../lib'; + MetadataEntry, SSMParameterProvider, Stack } from '../lib'; export = { 'AvailabilityZoneProvider returns a list with dummy values if the context is not available'(test: Test) { @@ -86,7 +86,7 @@ export = { stack.node.setContext(key, 'abc'); const ssmp = new SSMParameterProvider(stack, {parameterName: 'test'}); - const azs = resolve(ssmp.parameterValue()); + const azs = stack.node.resolve(ssmp.parameterValue()); test.deepEqual(azs, 'abc'); test.done(); diff --git a/packages/@aws-cdk/cx-api/lib/cxapi.ts b/packages/@aws-cdk/cx-api/lib/cxapi.ts index 83c0fa6f4828a..867fd4a1af323 100644 --- a/packages/@aws-cdk/cx-api/lib/cxapi.ts +++ b/packages/@aws-cdk/cx-api/lib/cxapi.ts @@ -56,6 +56,11 @@ export interface SynthesizedStack { missing?: { [key: string]: MissingContext }; metadata: StackMetadata; template: any; + + /** + * Other stacks this stack depends on + */ + dependsOn?: string[]; } /** diff --git a/packages/@aws-cdk/runtime-values/lib/rtv.ts b/packages/@aws-cdk/runtime-values/lib/rtv.ts index 53be77ca90d30..3cf7222e78c0b 100644 --- a/packages/@aws-cdk/runtime-values/lib/rtv.ts +++ b/packages/@aws-cdk/runtime-values/lib/rtv.ts @@ -27,11 +27,6 @@ export class RuntimeValue extends cdk.Construct { */ public static readonly ENV_NAME = 'RTV_STACK_NAME'; - /** - * The value to assign to the `RTV_STACK_NAME` environment variable. - */ - public static readonly ENV_VALUE = new cdk.AwsStackName(); - /** * IAM actions needed to read a value from an SSM parameter. */ @@ -41,6 +36,11 @@ export class RuntimeValue extends cdk.Construct { 'ssm:GetParameter' ]; + /** + * The value to assign to the `RTV_STACK_NAME` environment variable. + */ + public readonly envValue: string; + /** * The name of the runtime parameter. */ @@ -54,7 +54,10 @@ export class RuntimeValue extends cdk.Construct { constructor(scope: cdk.Construct, id: string, props: RuntimeValueProps) { super(scope, id); - this.parameterName = `/rtv/${new cdk.AwsStackName()}/${props.package}/${id}`; + const stack = cdk.Stack.find(this); + + this.parameterName = `/rtv/${stack.stackName}/${props.package}/${id}`; + this.envValue = stack.stackName; new ssm.CfnParameter(this, 'Parameter', { name: this.parameterName, @@ -62,7 +65,7 @@ export class RuntimeValue extends cdk.Construct { value: props.value, }); - this.parameterArn = cdk.ArnUtils.fromComponents({ + this.parameterArn = cdk.Stack.find(this).formatArn({ service: 'ssm', resource: 'parameter', resourceName: this.parameterName diff --git a/packages/@aws-cdk/runtime-values/test/integ.rtv.lambda.ts b/packages/@aws-cdk/runtime-values/test/integ.rtv.lambda.ts index ab9432cb88353..81e8b7d4d48f6 100644 --- a/packages/@aws-cdk/runtime-values/test/integ.rtv.lambda.ts +++ b/packages/@aws-cdk/runtime-values/test/integ.rtv.lambda.ts @@ -31,7 +31,7 @@ class TestStack extends cdk.Stack { // adds the `RTV_STACK_NAME` to the environment of the lambda function // and the fleet (via user-data) - fn.addEnvironment(RuntimeValue.ENV_NAME, RuntimeValue.ENV_VALUE); + fn.addEnvironment(RuntimeValue.ENV_NAME, queueUrlRtv.envValue); } } diff --git a/packages/@aws-cdk/runtime-values/test/test.rtv.ts b/packages/@aws-cdk/runtime-values/test/test.rtv.ts index 4cd4f6c6a95ab..5bb331617dd87 100644 --- a/packages/@aws-cdk/runtime-values/test/test.rtv.ts +++ b/packages/@aws-cdk/runtime-values/test/test.rtv.ts @@ -23,6 +23,8 @@ class RuntimeValueTest extends cdk.Construct { constructor(scope: cdk.Construct, id: string) { super(scope, id); + const stack = cdk.Stack.find(this); + const queue = new sqs.CfnQueue(this, 'Queue', {}); const role = new iam.Role(this, 'Role', { @@ -42,7 +44,7 @@ class RuntimeValueTest extends cdk.Construct { role: role.roleArn, environment: { variables: { - [RuntimeValue.ENV_NAME]: RuntimeValue.ENV_VALUE + [RuntimeValue.ENV_NAME]: stack.stackName, } } }); diff --git a/packages/aws-cdk/bin/cdk.ts b/packages/aws-cdk/bin/cdk.ts index 600a9d86ec1f0..ef82eedbfe31e 100644 --- a/packages/aws-cdk/bin/cdk.ts +++ b/packages/aws-cdk/bin/cdk.ts @@ -10,6 +10,7 @@ import yargs = require('yargs'); import { bootstrapEnvironment, deployStack, destroyStack, loadToolkitInfo, Mode, SDK } from '../lib'; import { environmentsFromDescriptors, globEnvironmentsFromStacks } from '../lib/api/cxapp/environments'; import { AppStacks, listStackNames } from '../lib/api/cxapp/stacks'; +import { leftPad } from '../lib/api/util/string-manipulation'; import { printSecurityDiff, printStackDiff, RequireApproval } from '../lib/diff'; import { availableInitLanguages, cliInit, printAvailableTemplates } from '../lib/init'; import { interactive } from '../lib/interactive'; @@ -53,7 +54,8 @@ async function parseCommandLineArguments() { .option('long', { type: 'boolean', default: false, alias: 'l', desc: 'display environment information for each stack' })) .command([ 'synthesize [STACKS..]', 'synth [STACKS..]' ], 'Synthesizes and prints the CloudFormation template for this stack', yargs => yargs .option('interactive', { type: 'boolean', alias: 'i', desc: 'interactively watch and show template updates' }) - .option('output', { type: 'string', alias: 'o', desc: 'write CloudFormation template for requested stacks to the given directory' })) + .option('output', { type: 'string', alias: 'o', desc: 'write CloudFormation template for requested stacks to the given directory' }) + .option('numbered', { type: 'boolean', alias: 'n', desc: 'Prefix filenames with numbers to indicate deployment ordering' })) .command('bootstrap [ENVIRONMENTS..]', 'Deploys the CDK toolkit stack into an AWS environment') .command('deploy [STACKS..]', 'Deploys the stack(s) named STACKS into your AWS account', yargs => yargs .option('require-approval', { type: 'string', choices: [RequireApproval.Never, RequireApproval.AnyChange, RequireApproval.Broadening], desc: 'what security-sensitive changes need manual approval' })) @@ -172,7 +174,7 @@ async function initCommandLine() { case 'synthesize': case 'synth': - return await cliSynthesize(args.STACKS, args.interactive, args.output, args.json); + return await cliSynthesize(args.STACKS, args.interactive, args.output, args.json, args.numbered); case 'metadata': return await cliMetadata(await findStack(args.STACK)); @@ -239,7 +241,8 @@ async function initCommandLine() { async function cliSynthesize(stackNames: string[], doInteractive: boolean, outputDir: string|undefined, - json: boolean): Promise { + json: boolean, + numbered: boolean): Promise { const stacks = await appStacks.selectStacks(...stackNames); renames.validateSelectedStacks(stacks); @@ -261,11 +264,14 @@ async function initCommandLine() { fs.mkdirpSync(outputDir); + let i = 0; for (const stack of stacks) { const finalName = renames.finalName(stack.name); - const fileName = `${outputDir}/${finalName}.template.${json ? 'json' : 'yaml'}`; + const prefix = numbered ? leftPad(`${i}`, 3, '0') + '.' : ''; + const fileName = `${outputDir}/${prefix}${finalName}.template.${json ? 'json' : 'yaml'}`; highlight(fileName); await fs.writeFile(fileName, toJsonOrYaml(stack.template)); + i++; } return undefined; // Nothing to print diff --git a/packages/aws-cdk/integ-tests/app/app.js b/packages/aws-cdk/integ-tests/app/app.js index 8da2b8aa6507b..c07d5a43551c1 100644 --- a/packages/aws-cdk/integ-tests/app/app.js +++ b/packages/aws-cdk/integ-tests/app/app.js @@ -30,6 +30,20 @@ class IamStack extends cdk.Stack { } } +class ProvidingStack extends cdk.Stack { + constructor(parent, id) { + super(parent, id); + } +} + +class ConsumingStack extends cdk.Stack { + constructor(parent, id, providingStack) { + super(parent, id); + + new cdk.Output(this, 'IConsumedSomething', { value: providingStack.stackName }); + } +} + const app = new cdk.App(); // Deploy all does a wildcard cdk-toolkit-integration-test-* @@ -37,5 +51,7 @@ new MyStack(app, 'cdk-toolkit-integration-test-1'); new YourStack(app, 'cdk-toolkit-integration-test-2'); // Not included in wildcard new IamStack(app, 'cdk-toolkit-integration-iam-test'); +const providing = new ProvidingStack(app, 'cdk-toolkit-integration-order-providing'); +new ConsumingStack(app, 'cdk-toolkit-integration-order-consuming', providing); app.run(); diff --git a/packages/aws-cdk/integ-tests/common.bash b/packages/aws-cdk/integ-tests/common.bash index 97733316dd841..6af9d8bec095c 100644 --- a/packages/aws-cdk/integ-tests/common.bash +++ b/packages/aws-cdk/integ-tests/common.bash @@ -77,7 +77,7 @@ function assert() { echo "| running ${command}" - $command > ${actual} || { + eval "$command" > ${actual} || { fail "command ${command} non-zero exit code" } diff --git a/packages/aws-cdk/integ-tests/test-cdk-order.sh b/packages/aws-cdk/integ-tests/test-cdk-order.sh new file mode 100755 index 0000000000000..9aa46451fbcfa --- /dev/null +++ b/packages/aws-cdk/integ-tests/test-cdk-order.sh @@ -0,0 +1,15 @@ +#!/bin/bash +set -euo pipefail +scriptdir=$(cd $(dirname $0) && pwd) +source ${scriptdir}/common.bash +# ---------------------------------------------------------- + +setup + +# ls order == synthesis order == provider before consumer +assert "cdk list | grep -- -order-" < matched.has(s.name)); } + /** + * Return all stacks in the CX + * + * If the stacks have dependencies between them, they will be returned in + * topologically sorted order. If there are dependencies that are not in the + * set, they will be ignored; it is the user's responsibility that the + * non-selected stacks have already been deployed previously. + */ public async listStacks(): Promise { const response = await this.synthesizeStacks(); - return response.stacks; + return topologicalSort(response.stacks, s => s.name, s => s.dependsOn || []); } /** @@ -196,4 +205,4 @@ export class AppStacks { */ export function listStackNames(stacks: cxapi.SynthesizedStack[]): string { return stacks.map(s => s.name).join(', '); -} +} \ No newline at end of file diff --git a/packages/aws-cdk/lib/api/util/string-manipulation.ts b/packages/aws-cdk/lib/api/util/string-manipulation.ts new file mode 100644 index 0000000000000..aa51679967ba3 --- /dev/null +++ b/packages/aws-cdk/lib/api/util/string-manipulation.ts @@ -0,0 +1,7 @@ +/** + * Pad 's' on the left with 'char' until it is n characters wide + */ +export function leftPad(s: string, n: number, char: string) { + const padding = Math.max(0, n - s.length); + return char.repeat(padding) + s; +} \ No newline at end of file diff --git a/packages/aws-cdk/lib/api/util/toposort.ts b/packages/aws-cdk/lib/api/util/toposort.ts new file mode 100644 index 0000000000000..97dd35ea8bb46 --- /dev/null +++ b/packages/aws-cdk/lib/api/util/toposort.ts @@ -0,0 +1,44 @@ +export type KeyFunc = (x: T) => string; +export type DepFunc = (x: T) => string[]; + +/** + * Return a topological sort of all elements of xs, according to the given dependency functions + * + * Dependencies outside the referenced set are ignored. + * + * Not a stable sort, but in order to keep the order as stable as possible, we'll sort by key + * among elements of equal precedence. + */ +export function topologicalSort(xs: Iterable, keyFn: KeyFunc, depFn: DepFunc): T[] { + const remaining = new Map>(); + for (const element of xs) { + const key = keyFn(element); + remaining.set(key, { key, element, dependencies: depFn(element) }); + } + + const ret = new Array(); + while (remaining.size > 0) { + // All elements with no more deps in the set can be ordered + const selectable = Array.from(remaining.values()).filter(e => e.dependencies.every(d => !remaining.has(d))); + + selectable.sort((a, b) => a.key < b.key ? -1 : b.key < a.key ? 1 : 0); + + for (const selected of selectable) { + ret.push(selected.element); + remaining.delete(selected.key); + } + + // If we didn't make any progress, we got stuck + if (selectable.length === 0) { + throw new Error(`Could not determine ordering between: ${Array.from(remaining.keys()).join(', ')}`); + } + } + + return ret; +} + +interface TopoElement { + key: string; + dependencies: string[]; + element: T; +} \ No newline at end of file diff --git a/scripts/generate-aggregate-tsconfig.sh b/scripts/generate-aggregate-tsconfig.sh index 9b6b4649aae86..724186a28663a 100755 --- a/scripts/generate-aggregate-tsconfig.sh +++ b/scripts/generate-aggregate-tsconfig.sh @@ -8,10 +8,12 @@ echo ' "__comment__": "This file is necessary to make transitive Project Refe echo ' "files": [],' echo ' "references": [' comma=' ' -for package in $(node_modules/.bin/lerna ls -p); do - relpath=${package#"$prefix"} - echo ' '"$comma"'{ "path": "'"$relpath"'" }' - comma=', ' +for package in $(node_modules/.bin/lerna ls -ap); do + if [[ -f ${package}/tsconfig.json ]]; then + relpath=${package#"$prefix"} + echo ' '"$comma"'{ "path": "'"$relpath"'" }' + comma=', ' + fi done echo ' ]' echo '}' diff --git a/tools/cdk-integ-tools/bin/cdk-integ.ts b/tools/cdk-integ-tools/bin/cdk-integ.ts index 898f86996423f..96c39794d5c0f 100644 --- a/tools/cdk-integ-tools/bin/cdk-integ.ts +++ b/tools/cdk-integ-tools/bin/cdk-integ.ts @@ -35,7 +35,8 @@ async function main() { } try { - await test.invoke([ ...args, 'deploy', '--prompt', 'never' ], { verbose: argv.verbose }); // Note: no context, so use default user settings! + // tslint:disable-next-line:max-line-length + await test.invoke([ ...args, 'deploy', '--require-approval', 'never' ], { verbose: argv.verbose }); // Note: no context, so use default user settings! console.error(`Success! Writing out reference synth.`); diff --git a/tools/cfn2ts/lib/codegen.ts b/tools/cfn2ts/lib/codegen.ts index 2e40e6ac24b26..17be51bc8ddce 100644 --- a/tools/cfn2ts/lib/codegen.ts +++ b/tools/cfn2ts/lib/codegen.ts @@ -310,7 +310,7 @@ export default class CodeGenerator { this.code.closeBlock(); this.code.openBlock('protected renderProperties(properties: any): { [key: string]: any } '); - this.code.line(`return ${genspec.cfnMapperName(propsType).fqn}(${CORE}.resolve(properties));`); + this.code.line(`return ${genspec.cfnMapperName(propsType).fqn}(this.node.resolve(properties));`); this.code.closeBlock(); }