diff --git a/CHANGELOG.md b/CHANGELOG.md index b868c6a3b3229..5e55e6526b886 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1700,7 +1700,7 @@ Fn.if_(Fn.equals(param.ref, 'True'), 'Encrypted', Pseudo.NO_VALUE) After: ```javascript -new FnIf(new FnEquals(param.ref, 'True'), 'Encrypted', new AwsNoValue()) +new FnIf(Fn.equals(param.ref, 'True'), 'Encrypted', new AwsNoValue()) ``` - CloudFormation template options (`templateFormatVersion`, `description` and `transform`) are now grouped under `Stack.templateOptions` instead of directly under `Stack`. diff --git a/docs/src/cloudformation.rst b/docs/src/cloudformation.rst index de199914b5ae3..70556fd1a0ad4 100644 --- a/docs/src/cloudformation.rst +++ b/docs/src/cloudformation.rst @@ -171,8 +171,8 @@ Intrinsic Functions .. code-block:: js - import cdk = require('@aws-cdk/cdk'); - new cdk.FnJoin(",", [...]) + import { Fn } from'@aws-cdk/cdk'; + Fn.join(",", [...]) .. _pseudo_parameters: diff --git a/examples/cdk-examples-typescript/advanced-usage/index.ts b/examples/cdk-examples-typescript/advanced-usage/index.ts index 5c27436deb4aa..233df2d25be1a 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.FnConcat(new cdk.AwsAccountId(), '/', param.ref) + value: `${new cdk.AwsAccountId()}/${param.ref}` }); // stack.templateOptions can be used to specify template-level options @@ -176,13 +176,13 @@ class CloudFormationExample extends cdk.Stack { new cdk.AwsStackName(), ], - // all CloudFormation's intrinsic functions are supported via the `cdk.FnXxx` classes + // all CloudFormation's intrinsic functions are supported via the `cdk.Fn.xxx` static methods. IntrinsicFunctions: [ - new cdk.FnAnd( - new cdk.FnFindInMap('MyMap', 'K1', 'K2'), - new cdk.FnSub('hello ${world}', { - world: new cdk.FnBase64(param.ref) // resolves to { Ref: } - })) + cdk.Fn.join('', [ + cdk.Fn.findInMap('MyMap', 'K1', 'K2'), + cdk.Fn.sub('hello ${world}', { + world: cdk.Fn.base64(param.ref) // resolves to { Ref: } + }) ]) ], }; } diff --git a/package.json b/package.json index 71f9ea04ba8d7..bce50c9707226 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "pkglint": "tools/pkglint/bin/pkglint -f ." }, "devDependencies": { - "@types/node": "^8.10.38", + "@types/node": "8.10.38", "@types/nodeunit": "^0.0.30", "conventional-changelog-cli": "^2.0.5", "lerna": "^3.3.0", diff --git a/packages/@aws-cdk/assets-docker/lib/image-asset.ts b/packages/@aws-cdk/assets-docker/lib/image-asset.ts index 7b76d91d20560..8226d0635ef24 100644 --- a/packages/@aws-cdk/assets-docker/lib/image-asset.ts +++ b/packages/@aws-cdk/assets-docker/lib/image-asset.ts @@ -61,9 +61,9 @@ export class DockerImageAsset extends cdk.Construct { this.addMetadata(cxapi.ASSET_METADATA, asset); // parse repository name and tag from the parameter (:) - const components = new cdk.FnSplit(':', imageNameParameter.value); - const repositoryName = new cdk.FnSelect(0, components).toString(); - const imageTag = new cdk.FnSelect(1, components).toString(); + const components = cdk.Fn.split(':', imageNameParameter.valueAsString); + const repositoryName = cdk.Fn.select(0, components).toString(); + const imageTag = cdk.Fn.select(1, components).toString(); // Require that repository adoption happens first, so we route the // input ARN into the Custom Resource and then get the URI which we use to diff --git a/packages/@aws-cdk/assets/lib/asset.ts b/packages/@aws-cdk/assets/lib/asset.ts index a075ef5829be0..92af4aeba3588 100644 --- a/packages/@aws-cdk/assets/lib/asset.ts +++ b/packages/@aws-cdk/assets/lib/asset.ts @@ -110,8 +110,8 @@ export class Asset extends cdk.Construct { }); this.s3BucketName = bucketParam.value.toString(); - this.s3Prefix = new cdk.FnSelect(0, new cdk.FnSplit(cxapi.ASSET_PREFIX_SEPARATOR, keyParam.value)).toString(); - const s3Filename = new cdk.FnSelect(1, new cdk.FnSplit(cxapi.ASSET_PREFIX_SEPARATOR, keyParam.value)).toString(); + this.s3Prefix = cdk.Fn.select(0, cdk.Fn.split(cxapi.ASSET_PREFIX_SEPARATOR, keyParam.valueAsString)).toString(); + const s3Filename = cdk.Fn.select(1, cdk.Fn.split(cxapi.ASSET_PREFIX_SEPARATOR, keyParam.valueAsString)).toString(); this.s3ObjectKey = `${this.s3Prefix}${s3Filename}`; this.bucket = s3.BucketRef.import(this, 'AssetBucket', { diff --git a/packages/@aws-cdk/aws-autoscaling/lib/auto-scaling-group.ts b/packages/@aws-cdk/aws-autoscaling/lib/auto-scaling-group.ts index 0ce9a18e26d4f..5a91728d53d70 100644 --- a/packages/@aws-cdk/aws-autoscaling/lib/auto-scaling-group.ts +++ b/packages/@aws-cdk/aws-autoscaling/lib/auto-scaling-group.ts @@ -222,7 +222,7 @@ export class AutoScalingGroup extends cdk.Construct implements IAutoScalingGroup // use delayed evaluation const machineImage = props.machineImage.getImage(this); - const userDataToken = new cdk.Token(() => new cdk.FnBase64((machineImage.os.createUserData(this.userDataLines)))); + const userDataToken = new cdk.Token(() => cdk.Fn.base64((machineImage.os.createUserData(this.userDataLines)))); const securityGroupsToken = new cdk.Token(() => this.securityGroups.map(sg => sg.securityGroupId)); const launchConfig = new CfnLaunchConfiguration(this, 'LaunchConfig', { diff --git a/packages/@aws-cdk/aws-cloudfront/lib/web_distribution.ts b/packages/@aws-cdk/aws-cloudfront/lib/web_distribution.ts index d8f575d67b303..e5b15fddd4721 100644 --- a/packages/@aws-cdk/aws-cloudfront/lib/web_distribution.ts +++ b/packages/@aws-cdk/aws-cloudfront/lib/web_distribution.ts @@ -574,9 +574,7 @@ export class CloudFrontWebDistribution extends cdk.Construct implements route53. if (originConfig.s3OriginSource && originConfig.s3OriginSource.originAccessIdentity) { originProperty.s3OriginConfig = { - originAccessIdentity: new cdk.FnConcat( - "origin-access-identity/cloudfront/", originConfig.s3OriginSource.originAccessIdentity.ref - ), + originAccessIdentity: `origin-access-identity/cloudfront/${originConfig.s3OriginSource.originAccessIdentity.ref}` }; } else if (originConfig.s3OriginSource) { originProperty.s3OriginConfig = {}; diff --git a/packages/@aws-cdk/aws-cloudtrail/lib/index.ts b/packages/@aws-cdk/aws-cloudtrail/lib/index.ts index 8c625b9d6fd91..cdbe5749261cd 100644 --- a/packages/@aws-cdk/aws-cloudtrail/lib/index.ts +++ b/packages/@aws-cdk/aws-cloudtrail/lib/index.ts @@ -138,7 +138,7 @@ export class CloudTrail extends cdk.Construct { .addServicePrincipal(cloudTrailPrincipal)); s3bucket.addToResourcePolicy(new iam.PolicyStatement() - .addResource(s3bucket.arnForObjects(new cdk.FnConcat('AWSLogs/', new cdk.AwsAccountId(), "/*"))) + .addResource(s3bucket.arnForObjects(`AWSLogs/${new cdk.AwsAccountId()}/*`)) .addActions("s3:PutObject") .addServicePrincipal(cloudTrailPrincipal) .setCondition("StringEquals", {'s3:x-amz-acl': "bucket-owner-full-control"})); diff --git a/packages/@aws-cdk/aws-codebuild/lib/project.ts b/packages/@aws-cdk/aws-codebuild/lib/project.ts index 00c12034c7c8b..4ce51ac2412e0 100644 --- a/packages/@aws-cdk/aws-codebuild/lib/project.ts +++ b/packages/@aws-cdk/aws-codebuild/lib/project.ts @@ -495,10 +495,10 @@ export class Project extends ProjectRef { let cache: CfnProject.ProjectCacheProperty | undefined; if (props.cacheBucket) { - const cacheDir = props.cacheDir != null ? props.cacheDir : new cdk.AwsNoValue(); + const cacheDir = props.cacheDir != null ? props.cacheDir : new cdk.AwsNoValue().toString(); cache = { type: 'S3', - location: new cdk.FnJoin('/', [props.cacheBucket.bucketName, cacheDir]), + location: cdk.Fn.join('/', [props.cacheBucket.bucketName, cacheDir]), }; props.cacheBucket.grantReadWrite(this.role); diff --git a/packages/@aws-cdk/aws-codebuild/test/test.codebuild.ts b/packages/@aws-cdk/aws-codebuild/test/test.codebuild.ts index c3c4490c51053..187b72cbcacb7 100644 --- a/packages/@aws-cdk/aws-codebuild/test/test.codebuild.ts +++ b/packages/@aws-cdk/aws-codebuild/test/test.codebuild.ts @@ -855,14 +855,14 @@ export = { new codebuild.Project(stack, 'Project', { source: new codebuild.CodePipelineSource(), environment: { - environmentVariables: { - FOO: { value: '1234' }, - BAR: { value: new cdk.FnConcat('111', { twotwotwo: '222' }), type: codebuild.BuildEnvironmentVariableType.ParameterStore } - } + environmentVariables: { + FOO: { value: '1234' }, + BAR: { value: `111${new cdk.CloudFormationToken({ twotwotwo: '222' })}`, type: codebuild.BuildEnvironmentVariableType.ParameterStore } + } }, environmentVariables: { - GOO: { value: 'ABC' }, - FOO: { value: 'OVERRIDE!' } + GOO: { value: 'ABC' }, + FOO: { value: 'OVERRIDE!' } } }); diff --git a/packages/@aws-cdk/aws-codecommit/lib/repository.ts b/packages/@aws-cdk/aws-codecommit/lib/repository.ts index a49022376e0e3..c28be77131b21 100644 --- a/packages/@aws-cdk/aws-codecommit/lib/repository.ts +++ b/packages/@aws-cdk/aws-codecommit/lib/repository.ts @@ -196,12 +196,7 @@ class ImportedRepositoryRef extends RepositoryRef { } private repositoryCloneUrl(protocol: 'https' | 'ssh'): string { - return new cdk.FnConcat(`${protocol}://git-codecommit.`, - new cdk.AwsRegion(), - '.', - new cdk.AwsURLSuffix(), - '/v1/repos/', - this.repositoryName).toString(); + return `${protocol}://git-codecommit.${new cdk.AwsRegion()}.${new cdk.AwsURLSuffix()}/v1/repos/${this.repositoryName}`; } } diff --git a/packages/@aws-cdk/aws-ecr/test/test.repository.ts b/packages/@aws-cdk/aws-ecr/test/test.repository.ts index e62819e6584cd..a9b58c486b04c 100644 --- a/packages/@aws-cdk/aws-ecr/test/test.repository.ts +++ b/packages/@aws-cdk/aws-ecr/test/test.repository.ts @@ -192,7 +192,7 @@ export = { // WHEN/THEN test.throws(() => ecr.Repository.import(stack, 'Repo', { - repositoryArn: new cdk.FnGetAtt('Boom', 'Boom').toString() + repositoryArn: cdk.Fn.getAtt('Boom', 'Boom').toString() }), /repositoryArn is a late-bound value, and therefore repositoryName is required/); test.done(); @@ -204,8 +204,8 @@ export = { // WHEN const repo = ecr.Repository.import(stack, 'Repo', { - repositoryArn: new cdk.FnGetAtt('Boom', 'Arn').toString(), - repositoryName: new cdk.FnGetAtt('Boom', 'Name').toString() + repositoryArn: cdk.Fn.getAtt('Boom', 'Arn').toString(), + repositoryName: cdk.Fn.getAtt('Boom', 'Name').toString() }); // THEN @@ -242,7 +242,7 @@ export = { 'arnForLocalRepository can be used to render an ARN for a local repository'(test: Test) { // GIVEN const stack = new cdk.Stack(); - const repoName = new cdk.FnGetAtt('Boom', 'Name').toString(); + const repoName = cdk.Fn.getAtt('Boom', 'Name').toString(); // WHEN const repo = ecr.Repository.import(stack, 'Repo', { diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-target-group.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-target-group.ts index 4574d4485f7b4..0a50e3dfe13ee 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-target-group.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-target-group.ts @@ -373,6 +373,6 @@ export interface LoadBalancerTargetProps { * app/my-load-balancer/50dc6c495c0c9188 */ export function loadBalancerNameFromListenerArn(listenerArn: string) { - const arnParts = new cdk.FnSplit('/', listenerArn); - return `${new cdk.FnSelect(1, arnParts)}/${new cdk.FnSelect(2, arnParts)}/${new cdk.FnSelect(3, arnParts)}`; + const arnParts = cdk.Fn.split('/', listenerArn); + return `${cdk.Fn.select(1, arnParts)}/${cdk.Fn.select(2, arnParts)}/${cdk.Fn.select(3, arnParts)}`; } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-events/lib/input-options.ts b/packages/@aws-cdk/aws-events/lib/input-options.ts index a0f58db566ccd..4bf55aa3fe6a8 100644 --- a/packages/@aws-cdk/aws-events/lib/input-options.ts +++ b/packages/@aws-cdk/aws-events/lib/input-options.ts @@ -16,13 +16,13 @@ export interface TargetInputTemplate { * @example * * { - * textTemplate: 'Build started', - * pathsMap: { - * buildid: '$.detail.id' - * } + * textTemplate: 'Build started', + * pathsMap: { + * buildid: '$.detail.id' + * } * } */ - textTemplate?: any; + textTemplate?: string; /** * Input template where you can use the values of the keys from diff --git a/packages/@aws-cdk/aws-events/lib/rule.ts b/packages/@aws-cdk/aws-events/lib/rule.ts index e64171b6402fb..3a2eaf5a0f3a5 100644 --- a/packages/@aws-cdk/aws-events/lib/rule.ts +++ b/packages/@aws-cdk/aws-events/lib/rule.ts @@ -1,4 +1,4 @@ -import { Construct, FnConcat, Token } from '@aws-cdk/cdk'; +import { Construct, Token } from '@aws-cdk/cdk'; import { EventPattern } from './event-pattern'; import { CfnRule } from './events.generated'; import { TargetInputTemplate } from './input-options'; @@ -133,7 +133,7 @@ export class EventRule extends EventRuleRef { } else if (typeof(inputOptions.textTemplate) === 'string') { inputTemplate = JSON.stringify(inputOptions.textTemplate); } else { - inputTemplate = new FnConcat('"', inputOptions.textTemplate, '"'); + inputTemplate = `"${inputOptions.textTemplate}"`; } return { diff --git a/packages/@aws-cdk/aws-events/test/test.rule.ts b/packages/@aws-cdk/aws-events/test/test.rule.ts index 6343e7edfcaf0..72743367c59d0 100644 --- a/packages/@aws-cdk/aws-events/test/test.rule.ts +++ b/packages/@aws-cdk/aws-events/test/test.rule.ts @@ -238,7 +238,7 @@ export = { // tokens are used here (FnConcat), but this is a text template so we // expect it to be wrapped in double quotes automatically for us. rule.addTarget(t1, { - textTemplate: new cdk.FnConcat('a', 'b') + textTemplate: cdk.Fn.join('', [ 'a', 'b' ]).toString() }); // jsonTemplate can be used to format JSON documents with replacements @@ -252,7 +252,7 @@ export = { // tokens can also used for JSON templates, but that means escaping is // the responsibility of the user. rule.addTarget(t4, { - jsonTemplate: new cdk.FnJoin(' ', ['"', 'hello', '\"world\"', '"']), + jsonTemplate: cdk.Fn.join(' ', ['"', 'hello', '\"world\"', '"']), }); expect(stack).toMatch({ 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 a96eee423f7b7..7cb0e3997aa94 100644 --- a/packages/@aws-cdk/aws-iam/test/test.policy-document.ts +++ b/packages/@aws-cdk/aws-iam/test/test.policy-document.ts @@ -1,4 +1,4 @@ -import { FnConcat, resolve } from '@aws-cdk/cdk'; +import { resolve, 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'; @@ -12,7 +12,7 @@ export = { p.addResource('yourQueue'); p.addAllResources(); - p.addAwsAccountPrincipal(new FnConcat('my', { account: 'account' }, 'name').toString()); + p.addAwsAccountPrincipal(`my${new Token({ account: 'account' })}name`); p.limitToAccount('12221121221'); test.deepEqual(resolve(p), { Action: diff --git a/packages/@aws-cdk/aws-kinesis/lib/stream.ts b/packages/@aws-cdk/aws-kinesis/lib/stream.ts index fb0a550872cae..a7261a929c0bf 100644 --- a/packages/@aws-cdk/aws-kinesis/lib/stream.ts +++ b/packages/@aws-cdk/aws-kinesis/lib/stream.ts @@ -170,7 +170,7 @@ export abstract class StreamRef extends cdk.Construct implements logs.ILogSubscr if (!this.cloudWatchLogsRole) { // 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(new cdk.FnConcat('logs.', new cdk.AwsRegion(), '.amazonaws.com').toString()), + assumedBy: new iam.ServicePrincipal(`logs.${new cdk.AwsRegion()}.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)); diff --git a/packages/@aws-cdk/aws-lambda/lib/lambda-ref.ts b/packages/@aws-cdk/aws-lambda/lib/lambda-ref.ts index 58ae13e62af25..2971dc26c76d8 100644 --- a/packages/@aws-cdk/aws-lambda/lib/lambda-ref.ts +++ b/packages/@aws-cdk/aws-lambda/lib/lambda-ref.ts @@ -320,7 +320,7 @@ export abstract class FunctionRef extends cdk.Construct // // (Wildcards in principals are unfortunately not supported. this.addPermission('InvokedByCloudWatchLogs', { - principal: new iam.ServicePrincipal(new cdk.FnConcat('logs.', new cdk.AwsRegion(), '.amazonaws.com').toString()), + principal: new iam.ServicePrincipal(`logs.${new cdk.AwsRegion()}.amazonaws.com`), sourceArn: arn }); this.logSubscriptionDestinationPolicyAddedFor.push(arn); @@ -451,6 +451,6 @@ class LambdaRefImport extends FunctionRef { * @returns `FnSelect(6, FnSplit(':', arn))` */ private extractNameFromArn(arn: string) { - return new cdk.FnSelect(6, new cdk.FnSplit(':', arn)).toString(); + return cdk.Fn.select(6, cdk.Fn.split(':', arn)).toString(); } } diff --git a/packages/@aws-cdk/aws-s3/lib/bucket.ts b/packages/@aws-cdk/aws-s3/lib/bucket.ts index b059f0463ab48..c289052722672 100644 --- a/packages/@aws-cdk/aws-s3/lib/bucket.ts +++ b/packages/@aws-cdk/aws-s3/lib/bucket.ts @@ -165,8 +165,8 @@ export abstract class BucketRef extends cdk.Construct { * bucket is returned. * @returns an ObjectS3Url token */ - public urlForObject(key?: any): string { - const components = [ 'https://', 's3.', new cdk.AwsRegion(), '.', new cdk.AwsURLSuffix(), '/', this.bucketName ]; + public urlForObject(key?: string): string { + const components = [ `https://s3.${new cdk.AwsRegion()}.${new cdk.AwsURLSuffix()}/${this.bucketName}` ]; if (key) { // trim prepending '/' if (typeof key === 'string' && key.startsWith('/')) { @@ -176,7 +176,7 @@ export abstract class BucketRef extends cdk.Construct { components.push(key); } - return new cdk.FnConcat(...components).toString(); + return components.join(''); } /** @@ -188,8 +188,8 @@ export abstract class BucketRef extends cdk.Construct { * arnForObjects('home/', team, '/', user, '/*') * */ - public arnForObjects(...keyPattern: any[]): string { - return new cdk.FnConcat(this.bucketArn, '/', ...keyPattern).toString(); + public arnForObjects(...keyPattern: string[]): string { + return `${this.bucketArn}/${keyPattern.join('')}`; } /** diff --git a/packages/@aws-cdk/aws-s3/test/test.util.ts b/packages/@aws-cdk/aws-s3/test/test.util.ts index 68a2e3ca756e1..2f33e12c81a02 100644 --- a/packages/@aws-cdk/aws-s3/test/test.util.ts +++ b/packages/@aws-cdk/aws-s3/test/test.util.ts @@ -1,4 +1,5 @@ import cdk = require('@aws-cdk/cdk'); +import { CloudFormationToken } from '@aws-cdk/cdk'; import { Test } from 'nodeunit'; import { parseBucketArn, parseBucketName } from '../lib/util'; @@ -41,7 +42,7 @@ export = { }, 'undefined if cannot extract name from a non-string arn'(test: Test) { - const bucketArn = new cdk.FnConcat('arn:aws:s3:::', { Ref: 'my-bucket' }).toString(); + const bucketArn = `arn:aws:s3:::${new CloudFormationToken({ Ref: 'my-bucket' })}`; test.deepEqual(cdk.resolve(parseBucketName({ bucketArn })), undefined); test.done(); }, diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/arn.ts b/packages/@aws-cdk/cdk/lib/cloudformation/arn.ts index 01ce8c7a7b53a..52e2fb3a563a3 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/arn.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/arn.ts @@ -1,7 +1,6 @@ -import { AwsAccountId, AwsPartition, AwsRegion, FnConcat, Token } from '..'; -import { FnSelect, FnSplit } from '../cloudformation/fn'; +import { AwsAccountId, AwsPartition, AwsRegion } from '..'; +import { Fn } from '../cloudformation/fn'; import { unresolved } from '../core/tokens'; -import { CloudFormationToken } from './cloudformation-token'; /** * An Amazon Resource Name (ARN). @@ -45,7 +44,7 @@ export class ArnUtils { values.push(components.resourceName); } - return new FnConcat(...values).toString(); + return values.join(''); } /** @@ -87,7 +86,7 @@ export class ArnUtils { */ public static parse(arn: string, sepIfToken: string = '/', hasName: boolean = true): ArnComponents { if (unresolved(arn)) { - return ArnUtils.parseToken(new CloudFormationToken(arn), sepIfToken, hasName); + return ArnUtils.parseToken(arn, sepIfToken, hasName); } const components = arn.split(':') as Array; @@ -184,7 +183,7 @@ export class ArnUtils { * but simply 'path'. This is a limitation because there is no slicing * functionality in CloudFormation templates. * - * @param arn The input token that contains an ARN + * @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 @@ -192,7 +191,7 @@ export class ArnUtils { * @returns an ArnComponents object which allows access to the various * components of the ARN. */ - public static parseToken(arn: Token, sep: string = '/', hasName: boolean = true): ArnComponents { + 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 @@ -201,23 +200,23 @@ export class ArnUtils { // We need the 'hasName' argument because {Fn::Select}ing a nonexistent field // throws an error. - const components = new FnSplit(':', arn); + const components = Fn.split(':', arnToken); - const partition = new FnSelect(1, components).toString(); - const service = new FnSelect(2, components).toString(); - const region = new FnSelect(3, components).toString(); - const account = new FnSelect(4, components).toString(); + 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(); if (sep === ':') { - const resource = new FnSelect(5, components).toString(); - const resourceName = hasName ? new FnSelect(6, components).toString() : undefined; + const resource = Fn.select(5, components).toString(); + const resourceName = hasName ? Fn.select(6, components).toString() : undefined; return { partition, service, region, account, resource, resourceName, sep }; } else { - const lastComponents = new FnSplit(sep, new FnSelect(5, components)); + const lastComponents = Fn.split(sep, Fn.select(5, components)); - const resource = new FnSelect(0, lastComponents).toString(); - const resourceName = hasName ? new FnSelect(1, lastComponents).toString() : undefined; + const resource = Fn.select(0, lastComponents).toString(); + const resourceName = hasName ? Fn.select(1, lastComponents).toString() : undefined; return { partition, service, region, account, resource, resourceName, sep }; } @@ -227,14 +226,14 @@ export class ArnUtils { * Return a Token that represents the resource component of the ARN */ public static resourceComponent(arn: string, sep: string = '/'): string { - return ArnUtils.parseToken(new Token(arn), sep).resource; + return ArnUtils.parseToken(arn, sep).resource; } /** * Return a Token that represents the resource Name component of the ARN */ public static resourceNameComponent(arn: string, sep: string = '/'): string { - return ArnUtils.parseToken(new Token(arn), sep, true).resourceName!; + return ArnUtils.parseToken(arn, sep, true).resourceName!; } } diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/cloudformation-token.ts b/packages/@aws-cdk/cdk/lib/cloudformation/cloudformation-token.ts index 024414218e3ce..467de44968370 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/cloudformation-token.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/cloudformation-token.ts @@ -1,4 +1,4 @@ -import { resolve, Token } from "../core/tokens"; +import { resolve, Token, unresolved } from "../core/tokens"; /** * Base class for CloudFormation built-ins @@ -9,12 +9,10 @@ export class CloudFormationToken extends Token { if (left !== undefined) { parts.push(left); } parts.push(resolve(this)); if (right !== undefined) { parts.push(right); } - return new FnConcat(...parts); + return new FnJoin('', parts); } } -import { FnConcat } from "./fn"; - /** * Return whether the given value represents a CloudFormation intrinsic */ @@ -26,3 +24,83 @@ export function isIntrinsic(x: any) { 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 e21558225a424..0d78f4191a071 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/condition.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/condition.ts @@ -1,5 +1,5 @@ import { Construct } from '../core/construct'; -import { FnCondition } from './fn'; +import { CloudFormationToken } from './cloudformation-token'; import { Referenceable } from './stack'; export interface ConditionProps { @@ -33,3 +33,28 @@ export class Condition extends Referenceable { }; } } + +/** + * You can use intrinsic functions, such as ``Fn::If``, ``Fn::Equals``, and ``Fn::Not``, to conditionally + * create stack resources. These conditions are evaluated based on input parameters that you + * declare when you create or update a stack. After you define all your conditions, you can + * associate them with resources or resource properties in the Resources and Outputs sections + * of a template. + * + * You define all conditions in the Conditions section of a template except for ``Fn::If`` conditions. + * You can use the ``Fn::If`` condition in the metadata attribute, update policy attribute, and property + * values in the Resources section and Outputs sections of a template. + * + * You might use conditions when you want to reuse a template that can create resources in different + * contexts, such as a test environment versus a production environment. In your template, you can + * add an EnvironmentType input parameter, which accepts either prod or test as inputs. For the + * production environment, you might include Amazon EC2 instances with certain capabilities; + * however, for the test environment, you want to use less capabilities to save costs. With + * conditions, you can define which resources are created and how they're configured for each + * environment type. + */ +export class FnCondition extends CloudFormationToken { + constructor(type: string, value: any) { + super({ [type]: value }); + } +} diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/fn.ts b/packages/@aws-cdk/cdk/lib/cloudformation/fn.ts index 5b9a705e355c1..44b0f440da057 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/fn.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/fn.ts @@ -1,14 +1,293 @@ -import { resolve, Token, unresolved } from '../core/tokens'; -import { CloudFormationToken, isIntrinsic } from './cloudformation-token'; +import { CloudFormationToken, FnJoin } from './cloudformation-token'; +import { FnCondition } from './condition'; + // tslint:disable:max-line-length /** * CloudFormation intrinsic functions. * http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference.html */ -export class Fn extends CloudFormationToken { +export class Fn { + + /** + * The ``Fn::GetAtt`` intrinsic function returns the value of an attribute + * from a resource in the template. + * @param logicalNameOfResource The logical name (also called logical ID) of + * the resource that contains the attribute that you want. + * @param attributeName The name of the resource-specific attribute whose + * value you want. See the resource's reference page for details about the + * attributes available for that resource type. + * @returns a CloudFormationToken object + */ + public static getAtt(logicalNameOfResource: string, attributeName: string): CloudFormationToken { + return new FnGetAtt(logicalNameOfResource, attributeName); + } + + /** + * 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. + * @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. + * @returns a token represented as a string + */ + public static join(delimiter: string, listOfValues: string[]): string { + return new FnJoin(delimiter, listOfValues).toString(); + } + + /** + * To split a string into a list of string values so that you can select an element from the + * resulting string list, use the ``Fn::Split`` intrinsic function. Specify the location of splits + * with a delimiter, such as , (a comma). After you split a string, use the ``Fn::Select`` function + * to pick a specific element. + * @param delimiter A string value that determines where the source string is divided. + * @param source The string value that you want to split. + * @returns a token represented as a string array + */ + public static split(delimiter: string, source: string): string[] { + return new FnSplit(delimiter, source).toList(); + } + + /** + * The intrinsic function ``Fn::Select`` returns a single object from a list of objects by index. + * @param index The index of the object to retrieve. This must be a value from zero to N-1, where N represents the number of elements in the array. + * @param array The list of objects to select from. This list must not be null, nor can it have null entries. + * @returns a token represented as a string + */ + public static select(index: number, array: string[]): string { + return new FnSelect(index, array).toString(); + } + + /** + * The intrinsic function ``Fn::Sub`` substitutes variables in an input string + * with values that you specify. In your templates, you can use this function + * to construct commands or outputs that include values that aren't available + * until you create or update a stack. + * @param body A string with variables that AWS CloudFormation substitutes + * with their associated values at runtime. Write variables as ${MyVarName}. + * Variables can be template parameter names, resource logical IDs, resource + * attributes, or a variable in a key-value map. If you specify only template + * parameter names, resource logical IDs, and resource attributes, don't + * specify a key-value map. + * @param variables The name of a variable that you included in the String + * parameter. The value that AWS CloudFormation substitutes for the associated + * variable name at runtime. + * @returns a token represented as a string + */ + public static sub(body: string, variables?: { [key: string]: string }): string { + return new FnSub(body, variables).toString(); + } + + /** + * The intrinsic function ``Fn::Base64`` returns the Base64 representation of + * the input string. This function is typically used to pass encoded data to + * Amazon EC2 instances by way of the UserData property. + * @param data The string value you want to convert to Base64. + * @returns a token represented as a string + */ + public static base64(data: string): string { + return new FnBase64(data).toString(); + } + + /** + * The intrinsic function ``Fn::Cidr`` returns the specified Cidr address block. + * @param ipBlock The user-specified default Cidr address block. + * @param count The number of subnets' Cidr block wanted. Count can be 1 to 256. + * @param sizeMask The digit covered in the subnet. + * @returns a token represented as a string + */ + public static cidr(ipBlock: string, count: number, sizeMask?: string): string { + return new FnCidr(ipBlock, count, sizeMask).toString(); + } + + /** + * The intrinsic function ``Fn::GetAZs`` returns an array that lists + * Availability Zones for a specified region. Because customers have access to + * different Availability Zones, the intrinsic function ``Fn::GetAZs`` enables + * template authors to write templates that adapt to the calling user's + * access. That way you don't have to hard-code a full list of Availability + * Zones for a specified region. + * @param region The name of the region for which you want to get the + * Availability Zones. You can use the AWS::Region pseudo parameter to specify + * the region in which the stack is created. Specifying an empty string is + * equivalent to specifying AWS::Region. + * @returns a token represented as a string array + */ + public static getAZs(region?: string): string[] { + return new FnGetAZs(region).toList(); + } + + /** + * The intrinsic function ``Fn::ImportValue`` returns the value of an output + * exported by another stack. You typically use this function to create + * cross-stack references. In the following example template snippets, Stack A + * exports VPC security group values and Stack B imports them. + * @param sharedValueToImport The stack output value that you want to import. + * @returns a token represented as a string + */ + public static importValue(sharedValueToImport: string): string { + return new FnImportValue(sharedValueToImport).toString(); + } + + /** + * The intrinsic function ``Fn::FindInMap`` returns the value corresponding to + * keys in a two-level map that is declared in the Mappings section. + * @returns a token represented as a string + */ + public static findInMap(mapName: string, topLevelKey: string, secondLevelKey: string): string { + return new FnFindInMap(mapName, topLevelKey, secondLevelKey).toString(); + } + + /** + * Returns true if all the specified conditions evaluate to true, or returns + * false if any one of the conditions evaluates to false. ``Fn::And`` acts as + * an AND operator. The minimum number of conditions that you can include is + * 2, and the maximum is 10. + * @param conditions conditions to AND + * @returns an FnCondition token + */ + public static conditionAnd(...conditions: FnCondition[]): FnCondition { + return new FnAnd(...conditions); + } + + /** + * Compares if two values are equal. Returns true if the two values are equal + * or false if they aren't. + * @param lhs A value of any type that you want to compare. + * @param rhs A value of any type that you want to compare. + * @returns an FnCondition token + */ + public static conditionEquals(lhs: any, rhs: any): FnCondition { + return new FnEquals(lhs, rhs); + } + + /** + * Returns one value if the specified condition evaluates to true and another + * value if the specified condition evaluates to false. Currently, AWS + * CloudFormation supports the ``Fn::If`` intrinsic function in the metadata + * attribute, update policy attribute, and property values in the Resources + * section and Outputs sections of a template. You can use the AWS::NoValue + * pseudo parameter as a return value to remove the corresponding property. + * @param condition A reference to a condition in the Conditions section. Use + * the condition's name to reference it. + * @param valueIfTrue A value to be returned if the specified condition + * evaluates to true. + * @param valueIfFalse A value to be returned if the specified condition + * evaluates to false. + * @returns an FnCondition token + */ + public static conditionIf(conditionId: string, valueIfTrue: any, valueIfFalse: any): FnCondition { + return new FnIf(conditionId, valueIfTrue, valueIfFalse); + } + + /** + * Returns true for a condition that evaluates to false or returns false for a + * condition that evaluates to true. ``Fn::Not`` acts as a NOT operator. + * @param condition A condition such as ``Fn::Equals`` that evaluates to true + * or false. + * @returns an FnCondition token + */ + public static conditionNot(condition: FnCondition): FnCondition { + return new FnNot(condition); + } + + /** + * Returns true if any one of the specified conditions evaluate to true, or + * returns false if all of the conditions evaluates to false. ``Fn::Or`` acts + * as an OR operator. The minimum number of conditions that you can include is + * 2, and the maximum is 10. + * @param conditions conditions that evaluates to true or false. + * @returns an FnCondition token + */ + public static conditionOr(...conditions: FnCondition[]): FnCondition { + return new FnOr(...conditions); + } + + /** + * Returns true if a specified string matches at least one value in a list of + * strings. + * @param listOfStrings A list of strings, such as "A", "B", "C". + * @param value A string, such as "A", that you want to compare against a list of strings. + * @returns an FnCondition token + */ + public static conditionContains(listOfStrings: string[], value: string): FnCondition { + return new FnContains(listOfStrings, value); + } + + /** + * Returns true if a specified string matches all values in a list. + * @param listOfStrings A list of strings, such as "A", "B", "C". + * @param value A string, such as "A", that you want to compare against a list + * of strings. + * @returns an FnCondition token + */ + public conditionEachMemberEquals(listOfStrings: string[], value: string): FnCondition { + return new FnEachMemberEquals(listOfStrings, value); + } + + /** + * Returns true if each member in a list of strings matches at least one value + * in a second list of strings. + * @param stringsToCheck A list of strings, such as "A", "B", "C". AWS + * CloudFormation checks whether each member in the strings_to_check parameter + * is in the strings_to_match parameter. + * @param stringsToMatch A list of strings, such as "A", "B", "C". Each member + * in the strings_to_match parameter is compared against the members of the + * strings_to_check parameter. + * @returns an FnCondition token + */ + public conditionEachMemberIn(stringsToCheck: string[], stringsToMatch: string): FnCondition { + return new FnEachMemberIn(stringsToCheck, stringsToMatch); + } + + /** + * Returns all values for a specified parameter type. + * @param parameterType An AWS-specific parameter type, such as + * AWS::EC2::SecurityGroup::Id or AWS::EC2::VPC::Id. For more information, see + * Parameters in the AWS CloudFormation User Guide. + * @returns a token represented as a string array + */ + public refAll(parameterType: string): string[] { + return new FnRefAll(parameterType).toList(); + } + + /** + * Returns an attribute value or list of values for a specific parameter and + * attribute. + * @param parameterOrLogicalId The name of a parameter for which you want to + * retrieve attribute values. The parameter must be declared in the Parameters + * section of the template. + * @param attribute The name of an attribute from which you want to retrieve a + * value. + * @returns a token represented as a string + */ + public valueOf(parameterOrLogicalId: string, attribute: string): string { + return new FnValueOf(parameterOrLogicalId, attribute).toString(); + } + + /** + * Returns a list of all attribute values for a given parameter type and + * attribute. + * @param parameterType An AWS-specific parameter type, such as + * AWS::EC2::SecurityGroup::Id or AWS::EC2::VPC::Id. For more information, see + * Parameters in the AWS CloudFormation User Guide. + * @param attribute The name of an attribute from which you want to retrieve a + * value. For more information about attributes, see Supported Attributes. + * @returns a token represented as a string array + */ + public valueOfAll(parameterType: string, attribute: string): string[] { + return new FnValueOfAll(parameterType, attribute).toList(); + } +} + +/** + * Base class for tokens that represent CloudFormation intrinsic functions. + */ +class FnBase extends CloudFormationToken { constructor(name: string, value: any) { - super(() => ({ [name]: value })); + super({ [name]: value }); } } @@ -16,7 +295,7 @@ export class Fn extends CloudFormationToken { * The intrinsic function ``Fn::FindInMap`` returns the value corresponding to keys in a two-level * map that is declared in the Mappings section. */ -export class FnFindInMap extends Fn { +class FnFindInMap extends FnBase { /** * Creates an ``Fn::FindInMap`` function. * @param mapName The logical name of a mapping declared in the Mappings section that contains the keys and values. @@ -31,7 +310,7 @@ export class FnFindInMap extends Fn { /** * The ``Fn::GetAtt`` intrinsic function returns the value of an attribute from a resource in the template. */ -export class FnGetAtt extends Fn { +class FnGetAtt extends FnBase { /** * Creates a ``Fn::GetAtt`` function. * @param logicalNameOfResource The logical name (also called logical ID) of the resource that contains the attribute that you want. @@ -49,7 +328,7 @@ export class FnGetAtt extends Fn { * user's access. That way you don't have to hard-code a full list of Availability Zones for a * specified region. */ -export class FnGetAZs extends Fn { +class FnGetAZs extends FnBase { /** * Creates an ``Fn::GetAZs`` function. * @param region The name of the region for which you want to get the Availability Zones. @@ -67,7 +346,7 @@ export class FnGetAZs extends Fn { * You typically use this function to create cross-stack references. In the following example * template snippets, Stack A exports VPC security group values and Stack B imports them. */ -export class FnImportValue extends Fn { +class FnImportValue extends FnBase { /** * Creates an ``Fn::ImportValue`` function. * @param sharedValueToImport The stack output value that you want to import. @@ -77,103 +356,10 @@ export class FnImportValue extends 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 Fn { - 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); - } - } -} - -/** - * Alias for ``FnJoin('', listOfValues)``. - */ -export class FnConcat extends FnJoin { - /** - * Creates an ``Fn::Join`` function with an empty delimiter. - * @param listOfValues The list of values to concatenate. - */ - constructor(...listOfValues: any[]) { - super('', listOfValues); - } -} - /** * The intrinsic function ``Fn::Select`` returns a single object from a list of objects by index. */ -export class FnSelect extends Fn { +class FnSelect extends FnBase { /** * Creates an ``Fn::Select`` function. * @param index The index of the object to retrieve. This must be a value from zero to N-1, where N represents the number of elements in the array. @@ -190,7 +376,7 @@ export class FnSelect extends Fn { * with a delimiter, such as , (a comma). After you split a string, use the ``Fn::Select`` function * to pick a specific element. */ -export class FnSplit extends Fn { +class FnSplit extends FnBase { /** * Create an ``Fn::Split`` function. * @param delimiter A string value that determines where the source string is divided. @@ -206,7 +392,7 @@ export class FnSplit extends Fn { * you specify. In your templates, you can use this function to construct commands or outputs * that include values that aren't available until you create or update a stack. */ -export class FnSub extends Fn { +class FnSub extends FnBase { /** * Creates an ``Fn::Sub`` function. * @param body A string with variables that AWS CloudFormation substitutes with their @@ -227,7 +413,7 @@ export class FnSub extends Fn { * This function is typically used to pass encoded data to Amazon EC2 instances by way of * the UserData property. */ -export class FnBase64 extends Fn { +class FnBase64 extends FnBase { /** * Creates an ``Fn::Base64`` function. @@ -241,7 +427,7 @@ export class FnBase64 extends Fn { /** * The intrinsic function ``Fn::Cidr`` returns the specified Cidr address block. */ -export class FnCidr extends Fn { +class FnCidr extends FnBase { /** * Creates an ``Fn::Cidr`` function. * @param ipBlock The user-specified default Cidr address block. @@ -256,35 +442,12 @@ export class FnCidr extends Fn { } } -/** - * You can use intrinsic functions, such as ``Fn::If``, ``Fn::Equals``, and ``Fn::Not``, to conditionally - * create stack resources. These conditions are evaluated based on input parameters that you - * declare when you create or update a stack. After you define all your conditions, you can - * associate them with resources or resource properties in the Resources and Outputs sections - * of a template. - * - * You define all conditions in the Conditions section of a template except for ``Fn::If`` conditions. - * You can use the ``Fn::If`` condition in the metadata attribute, update policy attribute, and property - * values in the Resources section and Outputs sections of a template. - * - * You might use conditions when you want to reuse a template that can create resources in different - * contexts, such as a test environment versus a production environment. In your template, you can - * add an EnvironmentType input parameter, which accepts either prod or test as inputs. For the - * production environment, you might include Amazon EC2 instances with certain capabilities; - * however, for the test environment, you want to use less capabilities to save costs. With - * conditions, you can define which resources are created and how they're configured for each - * environment type. - */ -export class FnCondition extends Fn { - -} - /** * Returns true if all the specified conditions evaluate to true, or returns false if any one * of the conditions evaluates to false. ``Fn::And`` acts as an AND operator. The minimum number of * conditions that you can include is 2, and the maximum is 10. */ -export class FnAnd extends FnCondition { +class FnAnd extends FnCondition { constructor(...condition: FnCondition[]) { super('Fn::And', condition); } @@ -294,7 +457,7 @@ export class FnAnd extends FnCondition { * Compares if two values are equal. Returns true if the two values are equal or false * if they aren't. */ -export class FnEquals extends FnCondition { +class FnEquals extends FnCondition { /** * Creates an ``Fn::Equals`` condition function. * @param lhs A value of any type that you want to compare. @@ -312,7 +475,7 @@ export class FnEquals extends FnCondition { * in the Resources section and Outputs sections of a template. You can use the AWS::NoValue * pseudo parameter as a return value to remove the corresponding property. */ -export class FnIf extends FnCondition { +class FnIf extends FnCondition { /** * Creates an ``Fn::If`` condition function. * @param condition A reference to a condition in the Conditions section. Use the condition's name to reference it. @@ -328,7 +491,7 @@ export class FnIf extends FnCondition { * Returns true for a condition that evaluates to false or returns false for a condition that evaluates to true. * ``Fn::Not`` acts as a NOT operator. */ -export class FnNot extends FnCondition { +class FnNot extends FnCondition { /** * Creates an ``Fn::Not`` condition function. * @param condition A condition such as ``Fn::Equals`` that evaluates to true or false. @@ -343,7 +506,7 @@ export class FnNot extends FnCondition { * all of the conditions evaluates to false. ``Fn::Or`` acts as an OR operator. The minimum number * of conditions that you can include is 2, and the maximum is 10. */ -export class FnOr extends FnCondition { +class FnOr extends FnCondition { /** * Creates an ``Fn::Or`` condition function. * @param condition A condition that evaluates to true or false. @@ -356,7 +519,7 @@ export class FnOr extends FnCondition { /** * Returns true if a specified string matches at least one value in a list of strings. */ -export class FnContains extends FnCondition { +class FnContains extends FnCondition { /** * Creates an ``Fn::Contains`` function. * @param listOfStrings A list of strings, such as "A", "B", "C". @@ -370,7 +533,7 @@ export class FnContains extends FnCondition { /** * Returns true if a specified string matches all values in a list. */ -export class FnEachMemberEquals extends FnCondition { +class FnEachMemberEquals extends FnCondition { /** * Creates an ``Fn::EachMemberEquals`` function. * @param listOfStrings A list of strings, such as "A", "B", "C". @@ -385,7 +548,7 @@ export class FnEachMemberEquals extends FnCondition { * Returns true if each member in a list of strings matches at least one value in a second * list of strings. */ -export class FnEachMemberIn extends FnCondition { +class FnEachMemberIn extends FnCondition { /** * Creates an ``Fn::EachMemberIn`` function. * @param stringsToCheck A list of strings, such as "A", "B", "C". AWS CloudFormation checks whether each member in the strings_to_check parameter is in the strings_to_match parameter. @@ -399,7 +562,7 @@ export class FnEachMemberIn extends FnCondition { /** * Returns all values for a specified parameter type. */ -export class FnRefAll extends FnCondition { +class FnRefAll extends FnBase { /** * Creates an ``Fn::RefAll`` function. * @param parameterType An AWS-specific parameter type, such as AWS::EC2::SecurityGroup::Id or @@ -414,7 +577,7 @@ export class FnRefAll extends FnCondition { /** * Returns an attribute value or list of values for a specific parameter and attribute. */ -export class FnValueOf extends FnCondition { +class FnValueOf extends FnBase { /** * Creates an ``Fn::ValueOf`` function. * @param parameterOrLogicalId The name of a parameter for which you want to retrieve attribute values. The parameter must be declared in the Parameters section of the template. @@ -428,7 +591,7 @@ export class FnValueOf extends FnCondition { /** * Returns a list of all attribute values for a given parameter type and attribute. */ -export class FnValueOfAll extends FnCondition { +class FnValueOfAll extends FnBase { /** * Creates an ``Fn::ValueOfAll`` function. * @param parameterType An AWS-specific parameter type, such as AWS::EC2::SecurityGroup::Id or AWS::EC2::VPC::Id. For more information, see Parameters in the AWS CloudFormation User Guide. diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/mapping.ts b/packages/@aws-cdk/cdk/lib/cloudformation/mapping.ts index b77b3af8f1673..8601012b60d47 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/mapping.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/mapping.ts @@ -1,5 +1,5 @@ import { Construct } from '../core/construct'; -import { FnFindInMap } from './fn'; +import { Fn } from './fn'; import { Referenceable } from './stack'; export interface MappingProps { @@ -31,7 +31,7 @@ export class Mapping extends Referenceable { /** * @returns A reference to a value in the map based on the two keys. */ - public findInMap(key1: any, key2: any) { + public findInMap(key1: string, key2: string): string { if (!(key1 in this.mapping)) { throw new Error(`Mapping doesn't contain top-level key '${key1}'`); } @@ -40,7 +40,7 @@ export class Mapping extends Referenceable { throw new Error(`Mapping doesn't contain second-level key '${key2}'`); } - return new FnFindInMap(this.logicalId, key1, key2); + return Fn.findInMap(this.logicalId, key1, key2); } public toCloudFormation(): object { diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/output.ts b/packages/@aws-cdk/cdk/lib/cloudformation/output.ts index fa39cf0990f0c..ecaedb4ef488c 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/output.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/output.ts @@ -1,7 +1,6 @@ import { Construct } from '../core/construct'; -import { Token } from '../core/tokens'; import { Condition } from './condition'; -import { FnImportValue, FnJoin, FnSelect, FnSplit } from './fn'; +import { Fn } from './fn'; import { Stack, StackElement } from './stack'; export interface OutputProps { @@ -104,7 +103,7 @@ export class Output extends StackElement { if (!this.export) { throw new Error('Cannot create an ImportValue without an export name'); } - return new FnImportValue(this.export); + return Fn.importValue(this.export); } public toCloudFormation(): object { @@ -207,19 +206,19 @@ export class StringListOutput extends Construct { condition: props.condition, disableExport: props.disableExport, export: props.export, - value: new FnJoin(this.separator, props.values) + value: Fn.join(this.separator, props.values) }); } /** * Return an array of imported values for this Output */ - public makeImportValues(): Token[] { + public makeImportValues(): string[] { const combined = this.output.makeImportValue(); const ret = []; for (let i = 0; i < this.length; i++) { - ret.push(new FnSelect(i, new FnSplit(this.separator, combined))); + ret.push(Fn.select(i, Fn.split(this.separator, combined))); } return ret; diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/parameter.ts b/packages/@aws-cdk/cdk/lib/cloudformation/parameter.ts index a7ec01fb989d0..9605363188fd3 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/parameter.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/parameter.ts @@ -76,6 +76,16 @@ export class Parameter extends Referenceable { */ public value: Token; + /** + * The parameter value token represented as a string. + */ + public valueAsString: string; + + /** + * The parameter value token represented as a string array. + */ + public valueAsList: string[]; + private properties: ParameterProps; /** @@ -90,6 +100,8 @@ export class Parameter extends Referenceable { super(parent, name); this.properties = props; this.value = new Ref(this); + this.valueAsString = this.value.toString(); + this.valueAsList = this.value.toList(); } public toCloudFormation(): object { diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/rule.ts b/packages/@aws-cdk/cdk/lib/cloudformation/rule.ts index 29a881032809e..039d32381e8ad 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/rule.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/rule.ts @@ -1,6 +1,6 @@ import { Construct } from '../core/construct'; import { capitalizePropertyNames } from '../core/util'; -import { FnCondition } from './fn'; +import { FnCondition } from './condition'; import { Referenceable } from './stack'; /** diff --git a/packages/@aws-cdk/cdk/test/cloudformation/test.arn.ts b/packages/@aws-cdk/cdk/test/cloudformation/test.arn.ts index 3584d20d39766..18b90b460db69 100644 --- a/packages/@aws-cdk/cdk/test/cloudformation/test.arn.ts +++ b/packages/@aws-cdk/cdk/test/cloudformation/test.arn.ts @@ -1,5 +1,5 @@ import { Test } from 'nodeunit'; -import { ArnComponents, ArnUtils, FnConcat, resolve, Token } from '../../lib'; +import { ArnComponents, ArnUtils, AwsAccountId, AwsPartition, AwsRegion, resolve, Token } from '../../lib'; export = { 'create from components with defaults'(test: Test) { @@ -9,17 +9,7 @@ export = { }); test.deepEqual(resolve(arn), - resolve(new FnConcat('arn', - ':', - { Ref: 'AWS::Partition' }, - ':', - 'sqs', - ':', - { Ref: 'AWS::Region' }, - ':', - { Ref: 'AWS::AccountId' }, - ':', - 'myqueuename'))); + resolve(`arn:${new AwsPartition()}:sqs:${new AwsRegion()}:${new AwsAccountId()}:myqueuename`)); test.done(); }, @@ -34,19 +24,7 @@ export = { }); test.deepEqual(resolve(arn), - resolve(new FnConcat('arn', - ':', - 'aws-cn', - ':', - 'dynamodb', - ':', - 'us-east-1', - ':', - '123456789012', - ':', - 'table', - '/', - 'mytable/stream/label'))); + 'arn:aws-cn:dynamodb:us-east-1:123456789012:table/mytable/stream/label'); test.done(); }, @@ -60,17 +38,7 @@ export = { }); test.deepEqual(resolve(arn), - resolve(new FnConcat('arn', - ':', - 'aws-cn', - ':', - 's3', - ':', - '', - ':', - '', - ':', - 'my-bucket'))); + 'arn:aws-cn:s3:::my-bucket'); test.done(); }, @@ -84,19 +52,7 @@ export = { }); test.deepEqual(resolve(arn), - resolve(new FnConcat('arn', - ':', - { Ref: 'AWS::Partition' }, - ':', - 'codedeploy', - ':', - { Ref: 'AWS::Region' }, - ':', - { Ref: 'AWS::AccountId' }, - ':', - 'application', - ':', - 'WordPress_App'))); + resolve(`arn:${new AwsPartition()}:codedeploy:${new AwsRegion()}:${new AwsAccountId()}:application:WordPress_App`)); test.done(); }, @@ -182,7 +138,7 @@ export = { 'a Token with : separator'(test: Test) { const theToken = { Ref: 'SomeParameter' }; - const parsed = ArnUtils.parseToken(new Token(() => theToken), ':'); + 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 ]} ]}); @@ -197,7 +153,7 @@ export = { 'a Token with / separator'(test: Test) { const theToken = { Ref: 'SomeParameter' }; - const parsed = ArnUtils.parseToken(new Token(() => theToken)); + const parsed = ArnUtils.parseToken(new Token(() => theToken).toString()); test.equal(parsed.sep, '/'); 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 1ddfd6269dacb..ecc52f95aea52 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, FnConcat, resolve, Token } from '../../lib'; +import { CloudFormationJSON, CloudFormationToken, Fn, resolve, Token } from '../../lib'; import { evaluateCFN } from './evaluate-cfn'; export = { @@ -87,7 +87,7 @@ export = { 'embedded string literals in intrinsics are escaped when calling TokenJSON.stringify()'(test: Test) { // WHEN - const token = new FnConcat('Hello', 'This\nIs', 'Very "cool"'); + const token = Fn.join('', [ 'Hello', 'This\nIs', 'Very "cool"' ]); // WHEN const resolved = resolve(CloudFormationJSON.stringify({ @@ -105,7 +105,7 @@ export = { 'Tokens in Tokens are handled correctly'(test: Test) { // GIVEN const bucketName = new CloudFormationToken({ Ref: 'MyBucket' }); - const combinedName = new FnConcat('The bucket name is ', bucketName); + const combinedName = Fn.join('', [ 'The bucket name is ', bucketName.toString() ]); // WHEN const resolved = resolve(CloudFormationJSON.stringify({ theBucket: combinedName })); diff --git a/packages/@aws-cdk/cdk/test/cloudformation/test.fn.ts b/packages/@aws-cdk/cdk/test/cloudformation/test.fn.ts index 509834e7f2e72..afb247fbebd57 100644 --- a/packages/@aws-cdk/cdk/test/cloudformation/test.fn.ts +++ b/packages/@aws-cdk/cdk/test/cloudformation/test.fn.ts @@ -1,7 +1,8 @@ import fc = require('fast-check'); import _ = require('lodash'); import nodeunit = require('nodeunit'); -import fn = require('../../lib/cloudformation/fn'); +import { CloudFormationToken } from '../../lib'; +import { Fn } from '../../lib/cloudformation/fn'; import { resolve } from '../../lib/core/tokens'; function asyncTest(cb: (test: nodeunit.Test) => Promise): (test: nodeunit.Test) => void { @@ -27,14 +28,14 @@ const anyValue = fc.oneof(nonEmptyString, tokenish); export = nodeunit.testCase({ FnJoin: { 'rejects empty list of arguments to join'(test: nodeunit.Test) { - test.throws(() => new fn.FnJoin('.', [])); + test.throws(() => Fn.join('.', [])); test.done(); }, 'resolves to the value if only one value is joined': asyncTest(async () => { await fc.assert( fc.property( fc.string(), anyValue, - (delimiter, value) => _.isEqual(resolve(new fn.FnJoin(delimiter, [value])), value) + (delimiter, value) => _.isEqual(resolve(Fn.join(delimiter, [value])), value) ), { verbose: true } ); @@ -43,7 +44,7 @@ export = nodeunit.testCase({ await fc.assert( fc.property( fc.string(), fc.array(nonEmptyString, 1, 15), - (delimiter, values) => resolve(new fn.FnJoin(delimiter, values)) === values.join(delimiter) + (delimiter, values) => resolve(Fn.join(delimiter, values)) === values.join(delimiter) ), { verbose: true } ); @@ -53,7 +54,7 @@ export = nodeunit.testCase({ fc.property( fc.string(), fc.array(nonEmptyString, 1, 3), tokenish, fc.array(nonEmptyString, 1, 3), (delimiter, prefix, obj, suffix) => - _.isEqual(resolve(new fn.FnJoin(delimiter, [...prefix, obj, ...suffix])), + _.isEqual(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" } @@ -67,8 +68,8 @@ export = nodeunit.testCase({ fc.array(anyValue), (delimiter, prefix, nested, suffix) => // Gonna test - _.isEqual(resolve(new fn.FnJoin(delimiter, [...prefix, new fn.FnJoin(delimiter, nested), ...suffix])), - resolve(new fn.FnJoin(delimiter, [...prefix, ...nested, ...suffix]))) + _.isEqual(resolve(Fn.join(delimiter, [...prefix, Fn.join(delimiter, nested), ...suffix])), + resolve(Fn.join(delimiter, [...prefix, ...nested, ...suffix]))) ), { verbose: true } ); @@ -80,9 +81,9 @@ export = nodeunit.testCase({ fc.array(anyValue, 1, 3), fc.array(tokenish, 2, 3), fc.array(anyValue, 3), - (delimiter1, delimiter2, prefix, nested, suffix) => { + (delimiter1, delimiter2, prefix, nested, suffix) => { fc.pre(delimiter1 !== delimiter2); - const join = new fn.FnJoin(delimiter1, [...prefix, new fn.FnJoin(delimiter2, nested), ...suffix]); + const join = Fn.join(delimiter1, [...prefix, Fn.join(delimiter2, stringListToken(nested)), ...suffix]); const resolved = resolve(join); return resolved['Fn::Join'][1].find((e: any) => typeof e === 'object' && ('Fn::Join' in e) @@ -94,3 +95,10 @@ export = nodeunit.testCase({ }), }, }); + +function stringListToken(o: any): string[] { + return new CloudFormationToken(o).toList(); +} +function stringToken(o: any): string { + return new CloudFormationToken(o).toString(); +} \ 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 9b961a4de0cd4..c4feb2ee6d714 100644 --- a/packages/@aws-cdk/cdk/test/cloudformation/test.resource.ts +++ b/packages/@aws-cdk/cdk/test/cloudformation/test.resource.ts @@ -1,7 +1,7 @@ import cxapi = require('@aws-cdk/cx-api'); import { Test } from 'nodeunit'; import { applyRemovalPolicy, Condition, Construct, DeletionPolicy, - FnEquals, FnNot, HashedAddressingScheme, IDependable, + Fn, HashedAddressingScheme, IDependable, RemovalPolicy, resolve, Resource, Root, Stack } from '../../lib'; export = { @@ -154,7 +154,7 @@ export = { 'conditions can be attached to a resource'(test: Test) { const stack = new Stack(); const r1 = new Resource(stack, 'Resource', { type: 'Type' }); - const cond = new Condition(stack, 'MyCondition', { expression: new FnNot(new FnEquals('a', 'b')) }); + const cond = new Condition(stack, 'MyCondition', { expression: Fn.conditionNot(Fn.conditionEquals('a', 'b')) }); r1.options.condition = cond; test.deepEqual(stack.toCloudFormation(), { diff --git a/packages/@aws-cdk/cdk/test/cloudformation/test.rule.ts b/packages/@aws-cdk/cdk/test/cloudformation/test.rule.ts index 9ca9acd6458ee..c4576dae703f2 100644 --- a/packages/@aws-cdk/cdk/test/cloudformation/test.rule.ts +++ b/packages/@aws-cdk/cdk/test/cloudformation/test.rule.ts @@ -1,13 +1,13 @@ import { Test } from 'nodeunit'; -import { FnAnd, FnContains, FnEquals, FnNot, Rule, Stack } from '../../lib'; +import { Fn, Rule, Stack } from '../../lib'; export = { 'Rule can be used to create rules'(test: Test) { const stack = new Stack(); const rule = new Rule(stack, 'MyRule'); - rule.addAssertion(new FnEquals('lhs', 'rhs'), 'lhs equals rhs'); - rule.addAssertion(new FnNot(new FnAnd(new FnContains([ 'hello', 'world' ], "world"))), 'some assertion'); + rule.addAssertion(Fn.conditionEquals('lhs', 'rhs'), 'lhs equals rhs'); + rule.addAssertion(Fn.conditionNot(Fn.conditionAnd(Fn.conditionContains([ 'hello', 'world' ], "world"))), 'some assertion'); test.deepEqual(stack.toCloudFormation(), { Rules: { diff --git a/packages/@aws-cdk/cdk/test/core/test.tokens.ts b/packages/@aws-cdk/cdk/test/core/test.tokens.ts index badab37cd1b47..341730aa91bce 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, FnJoin, FnSelect, resolve, Token, unresolved } from '../../lib'; +import { CloudFormationToken, Fn, resolve, Token, unresolved } from '../../lib'; import { evaluateCFN } from '../cloudformation/evaluate-cfn'; export = { @@ -326,7 +326,7 @@ export = { const encoded: string[] = new CloudFormationToken({ Ref: 'Other' }).toList(); // WHEN - const struct = new FnSelect(1, encoded); + const struct = Fn.select(1, encoded); // THEN test.deepEqual(resolve(struct), { @@ -341,7 +341,7 @@ export = { const encoded: string[] = new CloudFormationToken({ Ref: 'Other' }).toList(); // WHEN - const struct = new FnJoin('/', encoded); + const struct = Fn.join('/', encoded); // THEN test.deepEqual(resolve(struct), { diff --git a/packages/@aws-cdk/cfnspec/build-tools/create-missing-libraries.ts b/packages/@aws-cdk/cfnspec/build-tools/create-missing-libraries.ts index 158e4d2b815cf..5d33a4a404369 100644 --- a/packages/@aws-cdk/cfnspec/build-tools/create-missing-libraries.ts +++ b/packages/@aws-cdk/cfnspec/build-tools/create-missing-libraries.ts @@ -224,7 +224,7 @@ main().catch(e => { }); async function exec(command: string) { - const child = child_process.spawn(command, { + const child = child_process.spawn(command, [], { stdio: [ 'ignore', 'inherit', 'inherit' ], shell: true });