diff --git a/packages/@aws-cdk/assertions/README.md b/packages/@aws-cdk/assertions/README.md index 3d1275d2b4a3e..5370b1d094c8e 100644 --- a/packages/@aws-cdk/assertions/README.md +++ b/packages/@aws-cdk/assertions/README.md @@ -251,7 +251,7 @@ This matcher can be combined with any of the other matchers. // The following will NOT throw an assertion error template.hasResourceProperties('Foo::Bar', { Fred: { - Wobble: [ Match.anyValue(), "Flip" ], + Wobble: [ Match.anyValue(), Match.anyValue() ], }, }); @@ -400,7 +400,7 @@ template.hasResourceProperties('Foo::Bar', { ## Capturing Values -This matcher APIs documented above allow capturing values in the matching entry +The matcher APIs documented above allow capturing values in the matching entry (Resource, Output, Mapping, etc.). The following code captures a string from a matching resource. @@ -492,3 +492,74 @@ fredCapture.asString(); // returns "Flob" fredCapture.next(); // returns true fredCapture.asString(); // returns "Quib" ``` + +## Asserting Annotations + +In addition to template matching, we provide an API for annotation matching. +[Annotations](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.Annotations.html) +can be added via the [Aspects](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.Aspects.html) +API. You can learn more about Aspects [here](https://docs.aws.amazon.com/cdk/v2/guide/aspects.html). + +Say you have a `MyAspect` and a `MyStack` that uses `MyAspect`: + +```ts nofixture +import * as cdk from '@aws-cdk/core'; +import { Construct, IConstruct } from 'constructs'; + +class MyAspect implements cdk.IAspect { + public visit(node: IConstruct): void { + if (node instanceof cdk.CfnResource && node.cfnResourceType === 'Foo::Bar') { + this.error(node, 'we do not want a Foo::Bar resource'); + } + } + + protected error(node: IConstruct, message: string): void { + cdk.Annotations.of(node).addError(message); + } +} + +class MyStack extends cdk.Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + + const stack = new cdk.Stack(); + new cdk.CfnResource(stack, 'Foo', { + type: 'Foo::Bar', + properties: { + Fred: 'Thud', + }, + }); + cdk.Aspects.of(stack).add(new MyAspect()); + } +} +``` + +We can then assert that the stack contains the expected Error: + +```ts +// import { Annotations } from '@aws-cdk/assertions'; + +Annotations.fromStack(stack).hasError( + '/Default/Foo', + 'we do not want a Foo::Bar resource', +); +``` + +Here are the available APIs for `Annotations`: + +- `hasError()` and `findError()` +- `hasWarning()` and `findWarning()` +- `hasInfo()` and `findInfo()` + +The corresponding `findXxx()` API is complementary to the `hasXxx()` API, except instead +of asserting its presence, it returns the set of matching messages. + +In addition, this suite of APIs is compatable with `Matchers` for more fine-grained control. +For example, the following assertion works as well: + +```ts +Annotations.fromStack(stack).hasError( + '/Default/Foo', + Match.stringLikeRegexp('.*Foo::Bar.*'), +); +``` diff --git a/packages/@aws-cdk/assertions/lib/annotations.ts b/packages/@aws-cdk/assertions/lib/annotations.ts new file mode 100644 index 0000000000000..c656b15d6bab8 --- /dev/null +++ b/packages/@aws-cdk/assertions/lib/annotations.ts @@ -0,0 +1,129 @@ +import { Stack, Stage } from '@aws-cdk/core'; +import { SynthesisMessage } from '@aws-cdk/cx-api'; +import { Messages } from './private/message'; +import { findMessage, hasMessage } from './private/messages'; + +/** + * Suite of assertions that can be run on a CDK Stack. + * Focused on asserting annotations. + */ +export class Annotations { + /** + * Base your assertions on the messages returned by a synthesized CDK `Stack`. + * @param stack the CDK Stack to run assertions on + */ + public static fromStack(stack: Stack): Annotations { + return new Annotations(toMessages(stack)); + } + + private readonly _messages: Messages; + + private constructor(messages: SynthesisMessage[]) { + this._messages = convertArrayToMessagesType(messages); + } + + /** + * Assert that an error with the given message exists in the synthesized CDK `Stack`. + * + * @param constructPath the construct path to the error. Provide `'*'` to match all errors in the template. + * @param message the error message as should be expected. This should be a string or Matcher object. + */ + public hasError(constructPath: string, message: any): void { + const matchError = hasMessage(this._messages, constructPath, constructMessage('error', message)); + if (matchError) { + throw new Error(matchError); + } + } + + /** + * Get the set of matching errors of a given construct path and message. + * + * @param constructPath the construct path to the error. Provide `'*'` to match all errors in the template. + * @param message the error message as should be expected. This should be a string or Matcher object. + */ + public findError(constructPath: string, message: any): SynthesisMessage[] { + return convertMessagesTypeToArray(findMessage(this._messages, constructPath, constructMessage('error', message)) as Messages); + } + + /** + * Assert that an warning with the given message exists in the synthesized CDK `Stack`. + * + * @param constructPath the construct path to the warning. Provide `'*'` to match all warnings in the template. + * @param message the warning message as should be expected. This should be a string or Matcher object. + */ + public hasWarning(constructPath: string, message: any): void { + const matchError = hasMessage(this._messages, constructPath, constructMessage('warning', message)); + if (matchError) { + throw new Error(matchError); + } + } + + /** + * Get the set of matching warning of a given construct path and message. + * + * @param constructPath the construct path to the warning. Provide `'*'` to match all warnings in the template. + * @param message the warning message as should be expected. This should be a string or Matcher object. + */ + public findWarning(constructPath: string, message: any): SynthesisMessage[] { + return convertMessagesTypeToArray(findMessage(this._messages, constructPath, constructMessage('warning', message)) as Messages); + } + + /** + * Assert that an info with the given message exists in the synthesized CDK `Stack`. + * + * @param constructPath the construct path to the info. Provide `'*'` to match all info in the template. + * @param message the info message as should be expected. This should be a string or Matcher object. + */ + public hasInfo(constructPath: string, message: any): void { + const matchError = hasMessage(this._messages, constructPath, constructMessage('info', message)); + if (matchError) { + throw new Error(matchError); + } + } + + /** + * Get the set of matching infos of a given construct path and message. + * + * @param constructPath the construct path to the info. Provide `'*'` to match all infos in the template. + * @param message the info message as should be expected. This should be a string or Matcher object. + */ + public findInfo(constructPath: string, message: any): SynthesisMessage[] { + return convertMessagesTypeToArray(findMessage(this._messages, constructPath, constructMessage('info', message)) as Messages); + } +} + +function constructMessage(type: 'info' | 'warning' | 'error', message: any): {[key:string]: any } { + return { + level: type, + entry: { + data: message, + }, + }; +} + +function convertArrayToMessagesType(messages: SynthesisMessage[]): Messages { + return messages.reduce((obj, item) => { + return { + ...obj, + [item.id]: item, + }; + }, {}) as Messages; +} + +function convertMessagesTypeToArray(messages: Messages): SynthesisMessage[] { + return Object.values(messages) as SynthesisMessage[]; +} + +function toMessages(stack: Stack): any { + const root = stack.node.root; + if (!Stage.isStage(root)) { + throw new Error('unexpected: all stacks must be part of a Stage or an App'); + } + + // to support incremental assertions (i.e. "expect(stack).toNotContainSomething(); doSomething(); expect(stack).toContainSomthing()") + const force = true; + + const assembly = root.synth({ force }); + + return assembly.getStackArtifact(stack.artifactId).messages; +} diff --git a/packages/@aws-cdk/assertions/lib/index.ts b/packages/@aws-cdk/assertions/lib/index.ts index 492fad1227af3..eccbfac38637f 100644 --- a/packages/@aws-cdk/assertions/lib/index.ts +++ b/packages/@aws-cdk/assertions/lib/index.ts @@ -1,4 +1,5 @@ export * from './capture'; export * from './template'; export * from './match'; -export * from './matcher'; \ No newline at end of file +export * from './matcher'; +export * from './annotations'; \ No newline at end of file diff --git a/packages/@aws-cdk/assertions/lib/private/message.ts b/packages/@aws-cdk/assertions/lib/private/message.ts new file mode 100644 index 0000000000000..9657a5d90ad99 --- /dev/null +++ b/packages/@aws-cdk/assertions/lib/private/message.ts @@ -0,0 +1,5 @@ +import { SynthesisMessage } from '@aws-cdk/cx-api'; + +export type Messages = { + [logicalId: string]: SynthesisMessage; +} diff --git a/packages/@aws-cdk/assertions/lib/private/messages.ts b/packages/@aws-cdk/assertions/lib/private/messages.ts new file mode 100644 index 0000000000000..75c6fe3ae50b1 --- /dev/null +++ b/packages/@aws-cdk/assertions/lib/private/messages.ts @@ -0,0 +1,41 @@ +import { MatchResult } from '../matcher'; +import { Messages } from './message'; +import { filterLogicalId, formatFailure, matchSection } from './section'; + +export function findMessage(messages: Messages, logicalId: string, props: any = {}): { [key: string]: { [key: string]: any } } { + const section: { [key: string]: {} } = messages; + const result = matchSection(filterLogicalId(section, logicalId), props); + + if (!result.match) { + return {}; + } + + return result.matches; +} + +export function hasMessage(messages: Messages, logicalId: string, props: any): string | void { + const section: { [key: string]: {} } = messages; + const result = matchSection(filterLogicalId(section, logicalId), props); + + if (result.match) { + return; + } + + if (result.closestResult === undefined) { + return 'No messages found in the stack'; + } + + return [ + `Stack has ${result.analyzedCount} messages, but none match as expected.`, + formatFailure(formatMessage(result.closestResult)), + ].join('\n'); +} + +// We redact the stack trace by default because it is unnecessarily long and unintelligible. +// If there is a use case for rendering the trace, we can add it later. +function formatMessage(match: MatchResult, renderTrace: boolean = false): MatchResult { + if (!renderTrace) { + match.target.entry.trace = 'redacted'; + } + return match; +} diff --git a/packages/@aws-cdk/assertions/lib/template.ts b/packages/@aws-cdk/assertions/lib/template.ts index 8875a91d0ac9c..ec23538eaf4aa 100644 --- a/packages/@aws-cdk/assertions/lib/template.ts +++ b/packages/@aws-cdk/assertions/lib/template.ts @@ -129,7 +129,8 @@ export class Template { * @param logicalId the name of the parameter. Provide `'*'` to match all parameters in the template. * @param props by default, matches all Parameters in the template. * When a literal object is provided, performs a partial match via `Match.objectLike()`. - * Use the `Match` APIs to configure a different behaviour. */ + * Use the `Match` APIs to configure a different behaviour. + */ public findParameters(logicalId: string, props: any = {}): { [key: string]: { [key: string]: any } } { return findParameters(this.template, logicalId, props); } diff --git a/packages/@aws-cdk/assertions/rosetta/default.ts-fixture b/packages/@aws-cdk/assertions/rosetta/default.ts-fixture index fa862da5c609a..32d751f8c38fe 100644 --- a/packages/@aws-cdk/assertions/rosetta/default.ts-fixture +++ b/packages/@aws-cdk/assertions/rosetta/default.ts-fixture @@ -1,7 +1,7 @@ // Fixture with packages imported, but nothing else import { Construct } from 'constructs'; -import { Stack } from '@aws-cdk/core'; -import { Capture, Match, Template } from '@aws-cdk/assertions'; +import { Aspects, CfnResource, Stack } from '@aws-cdk/core'; +import { Annotations, Capture, Match, Template } from '@aws-cdk/assertions'; class Fixture extends Stack { constructor(scope: Construct, id: string) { diff --git a/packages/@aws-cdk/assertions/test/annotations.ts b/packages/@aws-cdk/assertions/test/annotations.ts new file mode 100644 index 0000000000000..8275e52d39dff --- /dev/null +++ b/packages/@aws-cdk/assertions/test/annotations.ts @@ -0,0 +1,150 @@ +import { Annotations, Aspects, CfnResource, IAspect, Stack } from '@aws-cdk/core'; +import { IConstruct } from 'constructs'; +import { Annotations as _Annotations, Match } from '../lib'; + +describe('Messages', () => { + let stack: Stack; + let annotations: _Annotations; + beforeAll(() => { + stack = new Stack(); + new CfnResource(stack, 'Foo', { + type: 'Foo::Bar', + properties: { + Fred: 'Thud', + }, + }); + + new CfnResource(stack, 'Bar', { + type: 'Foo::Bar', + properties: { + Baz: 'Qux', + }, + }); + + new CfnResource(stack, 'Fred', { + type: 'Baz::Qux', + properties: { + Foo: 'Bar', + }, + }); + + new CfnResource(stack, 'Qux', { + type: 'Fred::Thud', + properties: { + Fred: 'Bar', + }, + }); + + Aspects.of(stack).add(new MyAspect()); + annotations = _Annotations.fromStack(stack); + }); + + describe('hasError', () => { + test('match', () => { + annotations.hasError('/Default/Foo', 'this is an error'); + }); + + test('no match', () => { + expect(() => annotations.hasError('/Default/Fred', Match.anyValue())) + .toThrowError(/Stack has 1 messages, but none match as expected./); + }); + }); + + describe('findError', () => { + test('match', () => { + const result = annotations.findError('*', Match.anyValue()); + expect(Object.keys(result).length).toEqual(2); + }); + + test('no match', () => { + const result = annotations.findError('*', 'no message looks like this'); + expect(Object.keys(result).length).toEqual(0); + }); + }); + + describe('hasWarning', () => { + test('match', () => { + annotations.hasWarning('/Default/Fred', 'this is a warning'); + }); + + test('no match', () => { + expect(() => annotations.hasWarning('/Default/Foo', Match.anyValue())).toThrowError(/Stack has 1 messages, but none match as expected./); + }); + }); + + describe('findWarning', () => { + test('match', () => { + const result = annotations.findWarning('*', Match.anyValue()); + expect(Object.keys(result).length).toEqual(1); + }); + + test('no match', () => { + const result = annotations.findWarning('*', 'no message looks like this'); + expect(Object.keys(result).length).toEqual(0); + }); + }); + + describe('hasInfo', () => { + test('match', () => { + annotations.hasInfo('/Default/Qux', 'this is an info'); + }); + + test('no match', () => { + expect(() => annotations.hasInfo('/Default/Qux', 'this info is incorrect')).toThrowError(/Stack has 1 messages, but none match as expected./); + }); + }); + + describe('findInfo', () => { + test('match', () => { + const result = annotations.findInfo('/Default/Qux', 'this is an info'); + expect(Object.keys(result).length).toEqual(1); + }); + + test('no match', () => { + const result = annotations.findInfo('*', 'no message looks like this'); + expect(Object.keys(result).length).toEqual(0); + }); + }); + + describe('with matchers', () => { + test('anyValue', () => { + const result = annotations.findError('*', Match.anyValue()); + expect(Object.keys(result).length).toEqual(2); + }); + + test('not', () => { + expect(() => annotations.hasError('/Default/Foo', Match.not('this is an error'))) + .toThrowError(/Found unexpected match: "this is an error" at \/entry\/data/); + }); + + test('stringLikeRegEx', () => { + annotations.hasError('/Default/Foo', Match.stringLikeRegexp('.*error')); + }); + }); +}); + +class MyAspect implements IAspect { + public visit(node: IConstruct): void { + if (node instanceof CfnResource) { + if (node.cfnResourceType === 'Foo::Bar') { + this.error(node, 'this is an error'); + } else if (node.cfnResourceType === 'Baz::Qux') { + this.warn(node, 'this is a warning'); + } else { + this.info(node, 'this is an info'); + } + } + }; + + protected warn(node: IConstruct, message: string): void { + Annotations.of(node).addWarning(message); + } + + protected error(node: IConstruct, message: string): void { + Annotations.of(node).addError(message); + } + + protected info(node: IConstruct, message: string): void { + Annotations.of(node).addInfo(message); + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-acmpca/README.md b/packages/@aws-cdk/aws-acmpca/README.md index 66f15be5be271..d6f2c754f808e 100644 --- a/packages/@aws-cdk/aws-acmpca/README.md +++ b/packages/@aws-cdk/aws-acmpca/README.md @@ -14,7 +14,7 @@ This module is part of the [AWS Cloud Development Kit](https://github.com/aws/aws-cdk) project. -```ts +```ts nofixture import * as acmpca from '@aws-cdk/aws-acmpca'; ``` @@ -62,6 +62,8 @@ If you need to pass the higher-level `ICertificateAuthority` somewhere, you can get it from the lower-level `CfnCertificateAuthority` using the same `fromCertificateAuthorityArn` method: ```ts +declare const cfnCertificateAuthority: acmpca.CfnCertificateAuthority; + const certificateAuthority = acmpca.CertificateAuthority.fromCertificateAuthorityArn(this, 'CertificateAuthority', cfnCertificateAuthority.attrArn); ``` diff --git a/packages/@aws-cdk/aws-acmpca/package.json b/packages/@aws-cdk/aws-acmpca/package.json index 360332a20d54e..a4a7a79a1e8e6 100644 --- a/packages/@aws-cdk/aws-acmpca/package.json +++ b/packages/@aws-cdk/aws-acmpca/package.json @@ -28,7 +28,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", diff --git a/packages/@aws-cdk/aws-acmpca/rosetta/default.ts-fixture b/packages/@aws-cdk/aws-acmpca/rosetta/default.ts-fixture new file mode 100644 index 0000000000000..16526213b2c62 --- /dev/null +++ b/packages/@aws-cdk/aws-acmpca/rosetta/default.ts-fixture @@ -0,0 +1,11 @@ +// Fixture with packages imported, but nothing else +import { Stack } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import * as acmpca from '@aws-cdk/aws-acmpca'; + +class Fixture extends Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + /// here + } +} diff --git a/packages/@aws-cdk/aws-apigateway/package.json b/packages/@aws-cdk/aws-apigateway/package.json index dcab23ccc050e..cff64a652688b 100644 --- a/packages/@aws-cdk/aws-apigateway/package.json +++ b/packages/@aws-cdk/aws-apigateway/package.json @@ -79,7 +79,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@aws-cdk/assert-internal": "0.0.0", + "@aws-cdk/assertions": "0.0.0", "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cdk-integ-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-apigateway/test/access-log.test.ts b/packages/@aws-cdk/aws-apigateway/test/access-log.test.ts index 6509cc24b614b..3262207317c38 100644 --- a/packages/@aws-cdk/aws-apigateway/test/access-log.test.ts +++ b/packages/@aws-cdk/aws-apigateway/test/access-log.test.ts @@ -1,4 +1,3 @@ -import '@aws-cdk/assert-internal/jest'; import * as apigateway from '../lib'; describe('access log', () => { diff --git a/packages/@aws-cdk/aws-apigateway/test/api-definition.test.ts b/packages/@aws-cdk/aws-apigateway/test/api-definition.test.ts index c4af056038451..a74b3664dae26 100644 --- a/packages/@aws-cdk/aws-apigateway/test/api-definition.test.ts +++ b/packages/@aws-cdk/aws-apigateway/test/api-definition.test.ts @@ -1,6 +1,5 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as path from 'path'; -import { ResourcePart } from '@aws-cdk/assert-internal'; import * as s3 from '@aws-cdk/aws-s3'; import * as cdk from '@aws-cdk/core'; import * as cxapi from '@aws-cdk/cx-api'; @@ -84,12 +83,12 @@ describe('api definition', () => { apiDefinition: assetApiDefinition, }); - expect(stack).toHaveResource('AWS::ApiGateway::RestApi', { + Template.fromStack(stack).hasResource('AWS::ApiGateway::RestApi', { Metadata: { 'aws:asset:path': 'asset.68497ac876de4e963fc8f7b5f1b28844c18ecc95e3f7c6e9e0bf250e03c037fb.yaml', 'aws:asset:property': 'BodyS3Location', }, - }, ResourcePart.CompleteDefinition); + }); }); }); diff --git a/packages/@aws-cdk/aws-apigateway/test/api-key.test.ts b/packages/@aws-cdk/aws-apigateway/test/api-key.test.ts index 3008568ce2de4..46620128f7977 100644 --- a/packages/@aws-cdk/aws-apigateway/test/api-key.test.ts +++ b/packages/@aws-cdk/aws-apigateway/test/api-key.test.ts @@ -1,5 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; -import { ResourcePart } from '@aws-cdk/assert-internal'; +import { Match, Template } from '@aws-cdk/assertions'; import * as iam from '@aws-cdk/aws-iam'; import * as cdk from '@aws-cdk/core'; import * as apigateway from '../lib'; @@ -13,8 +12,9 @@ describe('api key', () => { new apigateway.ApiKey(stack, 'my-api-key'); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::ApiKey', undefined, ResourcePart.CompleteDefinition); - // should have an api key with no props defined. + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::ApiKey', { + Enabled: true, + }); }); @@ -29,7 +29,7 @@ describe('api key', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::ApiKey', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::ApiKey', { Enabled: false, Value: 'arandomstringwithmorethantwentycharacters', }); @@ -53,7 +53,7 @@ describe('api key', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::ApiKey', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::ApiKey', { CustomerId: 'test-customer', StageKeys: [ { @@ -76,7 +76,7 @@ describe('api key', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::ApiKey', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::ApiKey', { Description: 'The most secret api key', }); }); @@ -97,7 +97,7 @@ describe('api key', () => { usagePlan.addApiKey(importedKey); // THEN - expect(stack).toHaveResourceLike('AWS::ApiGateway::UsagePlanKey', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::UsagePlanKey', { KeyId: 'KeyIdabc', KeyType: 'API_KEY', UsagePlanId: { @@ -125,7 +125,7 @@ describe('api key', () => { apiKey.grantRead(user); // THEN - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -176,7 +176,7 @@ describe('api key', () => { apiKey.grantWrite(user); // THEN - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -232,7 +232,7 @@ describe('api key', () => { apiKey.grantReadWrite(user); // THEN - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -282,11 +282,11 @@ describe('api key', () => { // THEN // should have an api key with no props defined. - expect(stack).toHaveResource('AWS::ApiGateway::ApiKey', undefined, ResourcePart.CompleteDefinition); + Template.fromStack(stack).hasResource('AWS::ApiGateway::ApiKey', Match.anyValue()); // should not have a usage plan. - expect(stack).not.toHaveResource('AWS::ApiGateway::UsagePlan'); + Template.fromStack(stack).resourceCountIs('AWS::ApiGateway::UsagePlan', 0); // should not have a usage plan key. - expect(stack).not.toHaveResource('AWS::ApiGateway::UsagePlanKey'); + Template.fromStack(stack).resourceCountIs('AWS::ApiGateway::UsagePlanKey', 0); }); test('only api key is created when rate limiting properties are not provided', () => { @@ -306,7 +306,7 @@ describe('api key', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::ApiKey', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::ApiKey', { CustomerId: 'test-customer', StageKeys: [ { @@ -316,9 +316,9 @@ describe('api key', () => { ], }); // should not have a usage plan. - expect(stack).not.toHaveResource('AWS::ApiGateway::UsagePlan'); + Template.fromStack(stack).resourceCountIs('AWS::ApiGateway::UsagePlan', 0); // should not have a usage plan key. - expect(stack).not.toHaveResource('AWS::ApiGateway::UsagePlanKey'); + Template.fromStack(stack).resourceCountIs('AWS::ApiGateway::UsagePlanKey', 0); }); test('api key and usage plan are created and linked when rate limiting properties are provided', () => { @@ -343,7 +343,7 @@ describe('api key', () => { // THEN // should have an api key - expect(stack).toHaveResource('AWS::ApiGateway::ApiKey', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::ApiKey', { CustomerId: 'test-customer', StageKeys: [ { @@ -353,14 +353,14 @@ describe('api key', () => { ], }); // should have a usage plan with specified quota. - expect(stack).toHaveResource('AWS::ApiGateway::UsagePlan', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::UsagePlan', { Quota: { Limit: 10000, Period: 'MONTH', }, - }, ResourcePart.Properties); + }); // should have a usage plan key linking the api key and usage plan - expect(stack).toHaveResource('AWS::ApiGateway::UsagePlanKey', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::UsagePlanKey', { KeyId: { Ref: 'testapikey998028B6', }, @@ -368,7 +368,7 @@ describe('api key', () => { UsagePlanId: { Ref: 'testapikeyUsagePlanResource66DB63D6', }, - }, ResourcePart.Properties); + }); }); }); }); diff --git a/packages/@aws-cdk/aws-apigateway/test/authorizers/cognito.test.ts b/packages/@aws-cdk/aws-apigateway/test/authorizers/cognito.test.ts index 1956c69dbb149..906f772a8505b 100644 --- a/packages/@aws-cdk/aws-apigateway/test/authorizers/cognito.test.ts +++ b/packages/@aws-cdk/aws-apigateway/test/authorizers/cognito.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as cognito from '@aws-cdk/aws-cognito'; import { Duration, Stack } from '@aws-cdk/core'; import { AuthorizationType, CognitoUserPoolsAuthorizer, RestApi } from '../../lib'; @@ -21,7 +21,7 @@ describe('Cognito Authorizer', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::Authorizer', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Authorizer', { Type: 'COGNITO_USER_POOLS', RestApiId: stack.resolve(restApi.restApiId), IdentitySource: 'method.request.header.Authorization', @@ -52,7 +52,7 @@ describe('Cognito Authorizer', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::Authorizer', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Authorizer', { Type: 'COGNITO_USER_POOLS', Name: 'myauthorizer', RestApiId: stack.resolve(restApi.restApiId), diff --git a/packages/@aws-cdk/aws-apigateway/test/authorizers/lambda.test.ts b/packages/@aws-cdk/aws-apigateway/test/authorizers/lambda.test.ts index 6728e0204ed37..a4eea0f56892d 100644 --- a/packages/@aws-cdk/aws-apigateway/test/authorizers/lambda.test.ts +++ b/packages/@aws-cdk/aws-apigateway/test/authorizers/lambda.test.ts @@ -1,5 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; -import { ResourcePart } from '@aws-cdk/assert-internal'; +import { Match, Template } from '@aws-cdk/assertions'; import * as iam from '@aws-cdk/aws-iam'; import * as lambda from '@aws-cdk/aws-lambda'; import { Duration, Stack } from '@aws-cdk/core'; @@ -25,7 +24,7 @@ describe('lambda authorizer', () => { authorizationType: AuthorizationType.CUSTOM, }); - expect(stack).toHaveResource('AWS::ApiGateway::Authorizer', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Authorizer', { Type: 'TOKEN', RestApiId: stack.resolve(restApi.restApiId), IdentitySource: 'method.request.header.Authorization', @@ -71,7 +70,7 @@ describe('lambda authorizer', () => { }, }); - expect(stack).toHaveResource('AWS::Lambda::Permission', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Permission', { Action: 'lambda:InvokeFunction', Principal: 'apigateway.amazonaws.com', }); @@ -100,7 +99,7 @@ describe('lambda authorizer', () => { authorizationType: AuthorizationType.CUSTOM, }); - expect(stack).toHaveResource('AWS::ApiGateway::Authorizer', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Authorizer', { Type: 'REQUEST', RestApiId: stack.resolve(restApi.restApiId), AuthorizerUri: { @@ -145,7 +144,7 @@ describe('lambda authorizer', () => { }, }); - expect(stack).toHaveResource('AWS::Lambda::Permission', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Permission', { Action: 'lambda:InvokeFunction', Principal: 'apigateway.amazonaws.com', }); @@ -194,7 +193,7 @@ describe('lambda authorizer', () => { authorizationType: AuthorizationType.CUSTOM, }); - expect(stack).toHaveResource('AWS::ApiGateway::Authorizer', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Authorizer', { Type: 'TOKEN', RestApiId: stack.resolve(restApi.restApiId), IdentitySource: 'method.request.header.whoami', @@ -266,7 +265,7 @@ describe('lambda authorizer', () => { authorizationType: AuthorizationType.CUSTOM, }); - expect(stack).toHaveResource('AWS::ApiGateway::Authorizer', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Authorizer', { Type: 'REQUEST', RestApiId: stack.resolve(restApi.restApiId), IdentitySource: 'method.request.header.whoami', @@ -340,7 +339,7 @@ describe('lambda authorizer', () => { authorizationType: AuthorizationType.CUSTOM, }); - expect(stack).toHaveResource('AWS::ApiGateway::Authorizer', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Authorizer', { Type: 'TOKEN', RestApiId: stack.resolve(restApi.restApiId), AuthorizerUri: { @@ -385,9 +384,9 @@ describe('lambda authorizer', () => { }, }); - expect(stack).toHaveResource('AWS::IAM::Role'); + Template.fromStack(stack).hasResource('AWS::IAM::Role', Match.anyValue()); - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { Roles: [ stack.resolve(role.roleName), ], @@ -400,9 +399,9 @@ describe('lambda authorizer', () => { }, ], }, - }, ResourcePart.Properties); + }); - expect(stack).not.toHaveResource('AWS::Lambda::Permission'); + Template.fromStack(stack).resourceCountIs('AWS::Lambda::Permission', 0); }); test('request authorizer with assume role', () => { @@ -432,7 +431,7 @@ describe('lambda authorizer', () => { authorizationType: AuthorizationType.CUSTOM, }); - expect(stack).toHaveResource('AWS::ApiGateway::Authorizer', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Authorizer', { Type: 'REQUEST', RestApiId: stack.resolve(restApi.restApiId), AuthorizerUri: { @@ -477,9 +476,9 @@ describe('lambda authorizer', () => { }, }); - expect(stack).toHaveResource('AWS::IAM::Role'); + Template.fromStack(stack).hasResource('AWS::IAM::Role', Match.anyValue()); - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { Roles: [ stack.resolve(role.roleName), ], @@ -492,9 +491,9 @@ describe('lambda authorizer', () => { }, ], }, - }, ResourcePart.Properties); + }); - expect(stack).not.toHaveResource('AWS::Lambda::Permission'); + Template.fromStack(stack).resourceCountIs('AWS::Lambda::Permission', 0); }); test('token authorizer throws when not attached to a rest api', () => { diff --git a/packages/@aws-cdk/aws-apigateway/test/base-path-mapping.test.ts b/packages/@aws-cdk/aws-apigateway/test/base-path-mapping.test.ts index 6e57ce68d1394..a886b3c957dad 100644 --- a/packages/@aws-cdk/aws-apigateway/test/base-path-mapping.test.ts +++ b/packages/@aws-cdk/aws-apigateway/test/base-path-mapping.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as acm from '@aws-cdk/aws-certificatemanager'; import * as cdk from '@aws-cdk/core'; import * as apigw from '../lib'; @@ -22,7 +22,7 @@ describe('BasePathMapping', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::BasePathMapping', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::BasePathMapping', { DomainName: { Ref: 'MyDomainE4943FBC' }, RestApiId: { Ref: 'MyApi49610EDF' }, }); @@ -47,7 +47,7 @@ describe('BasePathMapping', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::ApiGateway::BasePathMapping', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::BasePathMapping', { BasePath: 'My_B45E-P4th', }); }); @@ -100,7 +100,7 @@ describe('BasePathMapping', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::ApiGateway::BasePathMapping', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::BasePathMapping', { Stage: { Ref: 'MyStage572B0482' }, }); }); diff --git a/packages/@aws-cdk/aws-apigateway/test/cors.test.ts b/packages/@aws-cdk/aws-apigateway/test/cors.test.ts index 42879b562df18..581b8af3e6c53 100644 --- a/packages/@aws-cdk/aws-apigateway/test/cors.test.ts +++ b/packages/@aws-cdk/aws-apigateway/test/cors.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as lambda from '@aws-cdk/aws-lambda'; import { Duration, Stack } from '@aws-cdk/core'; import * as apigw from '../lib'; @@ -16,7 +16,7 @@ describe('cors', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { HttpMethod: 'OPTIONS', ResourceId: { Ref: 'apiMyResourceD5CDB490' }, Integration: { @@ -63,7 +63,7 @@ describe('cors', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { HttpMethod: 'OPTIONS', ResourceId: { Ref: 'apiMyResourceD5CDB490' }, Integration: { @@ -112,7 +112,7 @@ describe('cors', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { HttpMethod: 'OPTIONS', ResourceId: { Ref: 'apiMyResourceD5CDB490' }, Integration: { @@ -159,7 +159,7 @@ describe('cors', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { HttpMethod: 'OPTIONS', ResourceId: { Ref: 'apiMyResourceD5CDB490' }, Integration: { @@ -219,7 +219,7 @@ describe('cors', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { HttpMethod: 'OPTIONS', ResourceId: { Ref: 'apiMyResourceD5CDB490' }, Integration: { @@ -277,7 +277,7 @@ describe('cors', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { HttpMethod: 'OPTIONS', ResourceId: { Ref: 'apiMyResourceD5CDB490' }, Integration: { @@ -327,7 +327,7 @@ describe('cors', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { HttpMethod: 'OPTIONS', ResourceId: { Ref: 'apiMyResourceD5CDB490' }, Integration: { @@ -376,7 +376,7 @@ describe('cors', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { HttpMethod: 'OPTIONS', ResourceId: { Ref: 'apiMyResourceD5CDB490' }, Integration: { @@ -439,7 +439,7 @@ describe('cors', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { HttpMethod: 'OPTIONS', ResourceId: { Ref: 'apiMyResourceD5CDB490' }, Integration: { @@ -489,12 +489,12 @@ describe('cors', () => { resource.addResource('MyChildResource'); // THEN - expect(stack).toCountResources('AWS::ApiGateway::Method', 2); // on both resources - expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Template.fromStack(stack).resourceCountIs('AWS::ApiGateway::Method', 2); // on both resources + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { HttpMethod: 'OPTIONS', ResourceId: { Ref: 'apiMyResourceD5CDB490' }, }); - expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { HttpMethod: 'OPTIONS', ResourceId: { Ref: 'apiMyResourceMyChildResource2DC010C5' }, }); @@ -515,15 +515,15 @@ describe('cors', () => { child1.addResource('child2'); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { HttpMethod: 'OPTIONS', ResourceId: { 'Fn::GetAtt': ['apiC8550315', 'RootResourceId'] }, }); - expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { HttpMethod: 'OPTIONS', ResourceId: { Ref: 'apichild1841A5840' }, }); - expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { HttpMethod: 'OPTIONS', ResourceId: { Ref: 'apichild1child26A9A7C47' }, }); @@ -548,7 +548,7 @@ describe('cors', () => { }); // THENB - expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { ResourceId: { Ref: 'apiAllowAll2F5BC564', }, @@ -579,7 +579,7 @@ describe('cors', () => { }, ], }); - expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { ResourceId: { Ref: 'apiAllowSpecific77DD8AF1', }, @@ -646,8 +646,8 @@ describe('cors', () => { }); // THEN - expect(stack).toCountResources('AWS::ApiGateway::Method', 4); // two ANY and two OPTIONS resources - expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Template.fromStack(stack).resourceCountIs('AWS::ApiGateway::Method', 4); // two ANY and two OPTIONS resources + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { HttpMethod: 'OPTIONS', ResourceId: { 'Fn::GetAtt': [ @@ -656,7 +656,7 @@ describe('cors', () => { ], }, }); - expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { HttpMethod: 'OPTIONS', ResourceId: { Ref: 'lambdarestapiproxyE3AE07E3', @@ -676,7 +676,7 @@ describe('cors', () => { api.root.addProxy(); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { HttpMethod: 'OPTIONS', }); }); diff --git a/packages/@aws-cdk/aws-apigateway/test/deployment.test.ts b/packages/@aws-cdk/aws-apigateway/test/deployment.test.ts index 3f431bfb29f2a..ef537a848c070 100644 --- a/packages/@aws-cdk/aws-apigateway/test/deployment.test.ts +++ b/packages/@aws-cdk/aws-apigateway/test/deployment.test.ts @@ -1,6 +1,5 @@ import * as path from 'path'; -import '@aws-cdk/assert-internal/jest'; -import { ResourcePart, SynthUtils } from '@aws-cdk/assert-internal'; +import { Template } from '@aws-cdk/assertions'; import * as lambda from '@aws-cdk/aws-lambda'; import { CfnResource, Lazy, Stack } from '@aws-cdk/core'; import * as apigateway from '../lib'; @@ -16,7 +15,7 @@ describe('deployment', () => { new apigateway.Deployment(stack, 'deployment', { api }); // THEN - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ Resources: { apiGETECF0BD67: { Type: 'AWS::ApiGateway::Method', @@ -68,7 +67,7 @@ describe('deployment', () => { new apigateway.Deployment(stack, 'deployment', { api, retainDeployments: true }); // THEN - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ Resources: { apiGETECF0BD67: { Type: 'AWS::ApiGateway::Method', @@ -122,39 +121,54 @@ describe('deployment', () => { new apigateway.Deployment(stack, 'deployment', { api, description: 'this is my deployment' }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::Deployment', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Deployment', { Description: 'this is my deployment', }); }); - test('logical ID of the deployment resource is salted', () => { - // GIVEN - const stack = new Stack(); - const api = new apigateway.RestApi(stack, 'api', { deploy: false, cloudWatchRole: false }); - const deployment = new apigateway.Deployment(stack, 'deployment', { api }); - api.root.addMethod('GET'); + describe('logical ID of the deployment resource is salted', () => { + test('before salting', () => { + // GIVEN + const stack = new Stack(); + const api = new apigateway.RestApi(stack, 'api', { deploy: false, cloudWatchRole: false }); + new apigateway.Deployment(stack, 'deployment', { api }); + api.root.addMethod('GET'); + + const resources = Template.fromStack(stack).findResources('AWS::ApiGateway::Deployment'); + expect(resources.deployment33381975bba46c5132329b81e7befcbbba5a0e75).toBeDefined(); + }); + + test('after salting with a resolved value', () => { + const stack = new Stack(); + const api = new apigateway.RestApi(stack, 'api', { deploy: false, cloudWatchRole: false }); + const deployment = new apigateway.Deployment(stack, 'deployment', { api }); + api.root.addMethod('GET'); - const resources = synthesize().Resources; - expect(resources.deployment33381975bba46c5132329b81e7befcbbba5a0e75).toBeDefined(); + // adding some salt + deployment.addToLogicalId({ foo: 123 }); // add some data to the logical ID - // adding some salt - deployment.addToLogicalId({ foo: 123 }); // add some data to the logical ID + // the logical ID changed + const template = Template.fromStack(stack).findResources('AWS::ApiGateway::Deployment'); + expect(template.deployment33381975bba46c5132329b81e7befcbbba5a0e75).toBeUndefined(); + expect(template.deployment333819758aa4cdb9d204502b959c4903f4d5d29f).toBeDefined(); + }); - // the logical ID changed - const template = synthesize(); - expect(template.Resources.deployment33381975bba46c5132329b81e7befcbbba5a0e75).toBeUndefined(); - expect(template.Resources.deployment333819758aa4cdb9d204502b959c4903f4d5d29f).toBeDefined(); + test('after salting with a resolved value and a token', () => { + const stack = new Stack(); + const api = new apigateway.RestApi(stack, 'api', { deploy: false, cloudWatchRole: false }); + const deployment = new apigateway.Deployment(stack, 'deployment', { api }); + api.root.addMethod('GET'); - // tokens supported, and are resolved upon synthesis - const value = 'hello hello'; - deployment.addToLogicalId({ foo: Lazy.string({ produce: () => value }) }); + // adding some salt + deployment.addToLogicalId({ foo: 123 }); // add some data to the logical ID - const template2 = synthesize(); - expect(template2.Resources.deployment333819758d91bed959c6bd6268ba84f6d33e888e).toBeDefined(); + // tokens supported, and are resolved upon synthesis + const value = 'hello hello'; + deployment.addToLogicalId({ foo: Lazy.string({ produce: () => value }) }); - function synthesize() { - return SynthUtils.synthesize(stack).template; - } + const template = Template.fromStack(stack).findResources('AWS::ApiGateway::Deployment'); + expect(template.deployment333819758d91bed959c6bd6268ba84f6d33e888e).toBeDefined(); + }); }); test('"addDependency" can be used to add a resource as a dependency', () => { @@ -169,12 +183,12 @@ describe('deployment', () => { // WHEN deployment.node.addDependency(dep); - expect(stack).toHaveResource('AWS::ApiGateway::Deployment', { + Template.fromStack(stack).hasResource('AWS::ApiGateway::Deployment', { DependsOn: [ 'apiGETECF0BD67', 'MyResource', ], - }, ResourcePart.CompleteDefinition); + }); }); @@ -205,10 +219,10 @@ describe('deployment', () => { api2.root.addMethod('GET'); // THEN - expect(stack1).toHaveResource('AWS::ApiGateway::Stage', { + Template.fromStack(stack1).hasResourceProperties('AWS::ApiGateway::Stage', { DeploymentId: { Ref: 'myapiDeploymentB7EF8EB74c5295c27fa87ff13f4d04e13f67662d' }, }); - expect(stack2).toHaveResource('AWS::ApiGateway::Stage', { + Template.fromStack(stack2).hasResourceProperties('AWS::ApiGateway::Stage', { DeploymentId: { Ref: 'myapiDeploymentB7EF8EB7b50d305057ba109c118e4aafd4509355' }, }); @@ -233,12 +247,12 @@ describe('deployment', () => { const resource = restapi.root.addResource('myresource'); resource.addMethod('GET'); - expect(stack).toHaveResource('AWS::ApiGateway::Deployment', { + Template.fromStack(stack).hasResource('AWS::ApiGateway::Deployment', { DependsOn: [ 'myapiGET9B7CD29E', 'myapimyresourceGET732851A5', 'myapiPOST23417BD2', ], - }, ResourcePart.CompleteDefinition); + }); }); }); diff --git a/packages/@aws-cdk/aws-apigateway/test/domains.test.ts b/packages/@aws-cdk/aws-apigateway/test/domains.test.ts index 31c553ad9c973..7e054c83d7102 100644 --- a/packages/@aws-cdk/aws-apigateway/test/domains.test.ts +++ b/packages/@aws-cdk/aws-apigateway/test/domains.test.ts @@ -1,5 +1,4 @@ -import { ABSENT } from '@aws-cdk/assert-internal'; -import '@aws-cdk/assert-internal/jest'; +import { Match, Template } from '@aws-cdk/assertions'; import * as acm from '@aws-cdk/aws-certificatemanager'; import { Bucket } from '@aws-cdk/aws-s3'; import { Stack } from '@aws-cdk/core'; @@ -27,13 +26,13 @@ describe('domains', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::DomainName', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::DomainName', { 'DomainName': 'example.com', 'EndpointConfiguration': { 'Types': ['REGIONAL'] }, 'RegionalCertificateArn': { 'Ref': 'Cert5C9FAEC1' }, }); - expect(stack).toHaveResource('AWS::ApiGateway::DomainName', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::DomainName', { 'DomainName': 'example.com', 'EndpointConfiguration': { 'Types': ['EDGE'] }, 'CertificateArn': { 'Ref': 'Cert5C9FAEC1' }, @@ -57,7 +56,7 @@ describe('domains', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::DomainName', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::DomainName', { 'DomainName': 'example.com', 'EndpointConfiguration': { 'Types': ['REGIONAL'] }, 'RegionalCertificateArn': { 'Ref': 'Cert5C9FAEC1' }, @@ -88,25 +87,25 @@ describe('domains', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::DomainName', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::DomainName', { 'DomainName': 'old.example.com', 'EndpointConfiguration': { 'Types': ['REGIONAL'] }, 'RegionalCertificateArn': { 'Ref': 'Cert5C9FAEC1' }, 'SecurityPolicy': 'TLS_1_0', }); - expect(stack).toHaveResource('AWS::ApiGateway::DomainName', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::DomainName', { 'DomainName': 'new.example.com', 'EndpointConfiguration': { 'Types': ['REGIONAL'] }, 'RegionalCertificateArn': { 'Ref': 'Cert5C9FAEC1' }, 'SecurityPolicy': 'TLS_1_2', }); - expect(stack).toHaveResource('AWS::ApiGateway::DomainName', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::DomainName', { 'DomainName': 'default.example.com', 'EndpointConfiguration': { 'Types': ['REGIONAL'] }, 'RegionalCertificateArn': { 'Ref': 'Cert5C9FAEC1' }, - 'SecurityPolicy': ABSENT, + 'SecurityPolicy': Match.absent(), }); }); @@ -125,7 +124,7 @@ describe('domains', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::BasePathMapping', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::BasePathMapping', { 'DomainName': { 'Ref': 'Domain66AC69E0', }, @@ -156,7 +155,7 @@ describe('domains', () => { domain.addBasePathMapping(api2, { basePath: 'api2' }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::BasePathMapping', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::BasePathMapping', { 'DomainName': { 'Ref': 'mydomain592C948B', }, @@ -169,7 +168,7 @@ describe('domains', () => { }, }); - expect(stack).toHaveResource('AWS::ApiGateway::BasePathMapping', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::BasePathMapping', { 'DomainName': { 'Ref': 'mydomain592C948B', }, @@ -197,7 +196,7 @@ describe('domains', () => { api.root.addMethod('GET'); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::DomainName', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::DomainName', { 'DomainName': 'my.domain.com', 'EndpointConfiguration': { 'Types': [ @@ -208,7 +207,7 @@ describe('domains', () => { 'Ref': 'cert56CA94EB', }, }); - expect(stack).toHaveResource('AWS::ApiGateway::BasePathMapping', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::BasePathMapping', { 'DomainName': { 'Ref': 'apiCustomDomain64773C4F', }, @@ -235,7 +234,7 @@ describe('domains', () => { api.addDomainName('domainId', { domainName, certificate }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::DomainName', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::DomainName', { 'DomainName': domainName, 'EndpointConfiguration': { 'Types': [ @@ -246,7 +245,7 @@ describe('domains', () => { 'Ref': 'cert56CA94EB', }, }); - expect(stack).toHaveResource('AWS::ApiGateway::BasePathMapping', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::BasePathMapping', { 'DomainName': { 'Ref': 'apidomainId102F8DAA', }, @@ -274,7 +273,7 @@ describe('domains', () => { api.addDomainName('domainId', { domainName, certificate, basePath }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::BasePathMapping', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::BasePathMapping', { 'BasePath': 'users', 'RestApiId': { 'Ref': 'apiC8550315', @@ -300,13 +299,13 @@ describe('domains', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::BasePathMapping', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::BasePathMapping', { 'BasePath': 'users', 'RestApiId': { 'Ref': 'apiC8550315', }, }); - expect(stack).toHaveResource('AWS::ApiGateway::BasePathMapping', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::BasePathMapping', { 'BasePath': 'books', 'RestApiId': { 'Ref': 'apiC8550315', @@ -361,7 +360,7 @@ describe('domains', () => { expect(api.domainName).toEqual(domainName1); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::DomainName', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::DomainName', { 'DomainName': 'my.domain.com', 'EndpointConfiguration': { 'Types': [ @@ -372,7 +371,7 @@ describe('domains', () => { 'Ref': 'cert56CA94EB', }, }); - expect(stack).toHaveResource('AWS::ApiGateway::DomainName', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::DomainName', { 'DomainName': 'your.domain.com', 'EndpointConfiguration': { 'Types': [ @@ -383,7 +382,7 @@ describe('domains', () => { 'Ref': 'cert56CA94EB', }, }); - expect(stack).toHaveResource('AWS::ApiGateway::DomainName', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::DomainName', { 'DomainName': 'our.domain.com', 'EndpointConfiguration': { 'Types': [ @@ -394,7 +393,7 @@ describe('domains', () => { 'Ref': 'cert56CA94EB', }, }); - expect(stack).toHaveResource('AWS::ApiGateway::BasePathMapping', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::BasePathMapping', { 'DomainName': { 'Ref': 'apidomainId102F8DAA', }, @@ -433,7 +432,7 @@ describe('domains', () => { domain.addBasePathMapping(api2, { basePath: 'api2' }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::BasePathMapping', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::BasePathMapping', { 'DomainName': { 'Ref': 'mydomain592C948B', }, @@ -444,7 +443,7 @@ describe('domains', () => { 'Stage': stack.resolve(testStage.stageName), }); - expect(stack).toHaveResource('AWS::ApiGateway::BasePathMapping', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::BasePathMapping', { 'DomainName': { 'Ref': 'mydomain592C948B', }, @@ -471,7 +470,7 @@ describe('domains', () => { certificate: acm.Certificate.fromCertificateArn(stack, 'cert', 'arn:aws:acm:us-east-1:1111111:certificate/11-3336f1-44483d-adc7-9cd375c5169d'), }); - expect(stack).toHaveResource('AWS::ApiGateway::DomainName', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::DomainName', { 'DomainName': 'example.com', 'EndpointConfiguration': { 'Types': ['REGIONAL'] }, 'RegionalCertificateArn': 'arn:aws:acm:us-east-1:1111111:certificate/11-3336f1-44483d-adc7-9cd375c5169d', @@ -492,7 +491,7 @@ describe('domains', () => { version: 'version', }, }); - expect(stack).toHaveResource('AWS::ApiGateway::DomainName', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::DomainName', { 'DomainName': 'example.com', 'EndpointConfiguration': { 'Types': ['REGIONAL'] }, 'RegionalCertificateArn': 'arn:aws:acm:us-east-1:1111111:certificate/11-3336f1-44483d-adc7-9cd375c5169d', @@ -512,7 +511,7 @@ describe('domains', () => { }).root.addMethod('GET'); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::BasePathMapping', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::BasePathMapping', { 'DomainName': { 'Ref': 'restApiWithStageCustomDomainC4749625', }, @@ -543,7 +542,7 @@ describe('domains', () => { }).root.addMethod('GET'); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::BasePathMapping', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::BasePathMapping', { 'DomainName': { 'Ref': 'specRestApiWithStageCustomDomain8A36A5C9', }, diff --git a/packages/@aws-cdk/aws-apigateway/test/gateway-response.test.ts b/packages/@aws-cdk/aws-apigateway/test/gateway-response.test.ts index 7a02ca17b6ca0..80093aba04de1 100644 --- a/packages/@aws-cdk/aws-apigateway/test/gateway-response.test.ts +++ b/packages/@aws-cdk/aws-apigateway/test/gateway-response.test.ts @@ -1,5 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; -import { ABSENT } from '@aws-cdk/assert-internal'; +import { Match, Template } from '@aws-cdk/assertions'; import { Stack } from '@aws-cdk/core'; import { ResponseType, RestApi } from '../lib'; @@ -20,12 +19,12 @@ describe('gateway response', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::GatewayResponse', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::GatewayResponse', { ResponseType: 'ACCESS_DENIED', RestApiId: stack.resolve(api.restApiId), - StatusCode: ABSENT, - ResponseParameters: ABSENT, - ResponseTemplates: ABSENT, + StatusCode: Match.absent(), + ResponseParameters: Match.absent(), + ResponseTemplates: Match.absent(), }); }); @@ -50,7 +49,7 @@ describe('gateway response', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::GatewayResponse', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::GatewayResponse', { ResponseType: 'AUTHORIZER_FAILURE', RestApiId: stack.resolve(api.restApiId), StatusCode: '500', @@ -58,7 +57,7 @@ describe('gateway response', () => { 'gatewayresponse.header.Access-Control-Allow-Origin': 'test.com', 'gatewayresponse.header.test-key': 'test-value', }, - ResponseTemplates: ABSENT, + ResponseTemplates: Match.absent(), }); }); @@ -82,11 +81,11 @@ describe('gateway response', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::GatewayResponse', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::GatewayResponse', { ResponseType: 'AUTHORIZER_FAILURE', RestApiId: stack.resolve(api.restApiId), StatusCode: '500', - ResponseParameters: ABSENT, + ResponseParameters: Match.absent(), ResponseTemplates: { 'application/json': '{ "message": $context.error.messageString, "statusCode": "488" }', }, diff --git a/packages/@aws-cdk/aws-apigateway/test/http.test.ts b/packages/@aws-cdk/aws-apigateway/test/http.test.ts index 15714ca988087..4d687da1546b7 100644 --- a/packages/@aws-cdk/aws-apigateway/test/http.test.ts +++ b/packages/@aws-cdk/aws-apigateway/test/http.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as cdk from '@aws-cdk/core'; import * as apigateway from '../lib'; @@ -14,7 +14,7 @@ describe('http integration', () => { api.root.addMethod('GET', integ); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { Integration: { IntegrationHttpMethod: 'GET', Type: 'HTTP_PROXY', @@ -40,7 +40,7 @@ describe('http integration', () => { api.root.addMethod('GET', integ); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { Integration: { CacheNamespace: 'hey', IntegrationHttpMethod: 'POST', diff --git a/packages/@aws-cdk/aws-apigateway/test/integration.test.ts b/packages/@aws-cdk/aws-apigateway/test/integration.test.ts index f89a1fdf042ed..0ae46986bcc93 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integration.test.ts +++ b/packages/@aws-cdk/aws-apigateway/test/integration.test.ts @@ -1,5 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; -import { ABSENT } from '@aws-cdk/assert-internal'; +import { Match, Template } from '@aws-cdk/assertions'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as elbv2 from '@aws-cdk/aws-elasticloadbalancingv2'; import * as iam from '@aws-cdk/aws-iam'; @@ -52,7 +51,7 @@ describe('integration', () => { }); api.root.addMethod('GET', integration); - expect(stack).toHaveResourceLike('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { Integration: { Uri: { 'Fn::Join': [ @@ -133,7 +132,7 @@ describe('integration', () => { })).toThrow(/cannot set 'vpcLink' where 'connectionType' is INTERNET/); }); - test('connectionType is ABSENT when vpcLink is not specified', () => { + test('connectionType is Match.absent() when vpcLink is not specified', () => { // GIVEN const stack = new cdk.Stack(); const api = new apigw.RestApi(stack, 'restapi'); @@ -146,10 +145,10 @@ describe('integration', () => { api.root.addMethod('ANY', integration); // THEN - expect(stack).toHaveResourceLike('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { HttpMethod: 'ANY', Integration: { - ConnectionType: ABSENT, + ConnectionType: Match.absent(), }, }); }); @@ -177,7 +176,7 @@ describe('integration', () => { api.root.addMethod('ANY', integration); // THEN - expect(stack).toHaveResourceLike('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { HttpMethod: 'ANY', Integration: { ConnectionType: 'VPC_LINK', @@ -221,7 +220,7 @@ describe('integration', () => { api.root.addMethod('ANY', integration); // THEN - expect(stack).toHaveResourceLike('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { HttpMethod: 'ANY', Integration: { TimeoutInMillis: 1000, diff --git a/packages/@aws-cdk/aws-apigateway/test/integrations/lambda.test.ts b/packages/@aws-cdk/aws-apigateway/test/integrations/lambda.test.ts index 7c5b9e60e85d6..187e15e2b09cf 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integrations/lambda.test.ts +++ b/packages/@aws-cdk/aws-apigateway/test/integrations/lambda.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Match, Template } from '@aws-cdk/assertions'; import * as lambda from '@aws-cdk/aws-lambda'; import * as cdk from '@aws-cdk/core'; import * as apigateway from '../../lib'; @@ -19,7 +19,7 @@ describe('lambda', () => { api.root.addMethod('GET', integ); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { Integration: { IntegrationHttpMethod: 'POST', Type: 'AWS_PROXY', @@ -66,7 +66,7 @@ describe('lambda', () => { api.root.addMethod('GET', integ); // THEN - expect(stack).toHaveResource('AWS::Lambda::Permission', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Permission', { SourceArn: { 'Fn::Join': [ '', @@ -78,7 +78,7 @@ describe('lambda', () => { }, }); - expect(stack).not.toHaveResource('AWS::Lambda::Permission', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Permission', Match.not({ SourceArn: { 'Fn::Join': [ '', @@ -95,7 +95,7 @@ describe('lambda', () => { ], ], }, - }); + })); }); test('"allowTestInvoke" set to true allows calling the API from the test UI', () => { @@ -114,7 +114,7 @@ describe('lambda', () => { api.root.addMethod('GET', integ); // THEN - expect(stack).toHaveResource('AWS::Lambda::Permission', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Permission', { SourceArn: { 'Fn::Join': [ '', @@ -150,7 +150,7 @@ describe('lambda', () => { api.root.addMethod('GET', integ); // THEN - expect(stack).toHaveResourceLike('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { Integration: { Type: 'AWS', }, @@ -171,7 +171,7 @@ describe('lambda', () => { api.root.addMethod('ANY', target); - expect(stack).toHaveResource('AWS::Lambda::Permission', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Permission', { SourceArn: { 'Fn::Join': [ '', @@ -190,7 +190,7 @@ describe('lambda', () => { }, }); - expect(stack).toHaveResource('AWS::Lambda::Permission', { + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Permission', { SourceArn: { 'Fn::Join': [ '', @@ -235,7 +235,7 @@ describe('lambda', () => { api.root.addMethod('ANY', new apigateway.LambdaIntegration(handler)); - expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { RestApiId: 'imported-rest-api-id', ResourceId: 'imported-root-resource-id', HttpMethod: 'ANY', diff --git a/packages/@aws-cdk/aws-apigateway/test/integrations/stepfunctions.test.ts b/packages/@aws-cdk/aws-apigateway/test/integrations/stepfunctions.test.ts index c803fa974f7f6..830ca2a8e2000 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integrations/stepfunctions.test.ts +++ b/packages/@aws-cdk/aws-apigateway/test/integrations/stepfunctions.test.ts @@ -1,5 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; -import { stringLike, anything } from '@aws-cdk/assert-internal'; +import { Match, Template } from '@aws-cdk/assertions'; import * as sfn from '@aws-cdk/aws-stepfunctions'; import { StateMachine, StateMachineType } from '@aws-cdk/aws-stepfunctions'; import * as cdk from '@aws-cdk/core'; @@ -16,7 +15,7 @@ describe('StepFunctionsIntegration', () => { api.root.addMethod('GET', integ); //THEN - expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { ResourceId: { 'Fn::GetAtt': [ 'myrestapiBAC2BF45', @@ -74,16 +73,16 @@ describe('StepFunctionsIntegration', () => { const integ = apigw.StepFunctionsIntegration.startExecution(stateMachine); api.root.addMethod('GET', integ); - expect(stack).toHaveResourceLike('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { Integration: { RequestTemplates: { 'application/json': { 'Fn::Join': [ '', [ - stringLike('*includeHeaders = false*'), + Match.stringLikeRegexp('includeHeaders = false'), { Ref: 'StateMachine2E01A3A5' }, - anything(), + Match.anyValue(), ], ], }, @@ -102,16 +101,16 @@ describe('StepFunctionsIntegration', () => { }); api.root.addMethod('GET', integ); - expect(stack).toHaveResourceLike('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { Integration: { RequestTemplates: { 'application/json': { 'Fn::Join': [ '', [ - stringLike('*#set($includeHeaders = true)*'), + Match.stringLikeRegexp('#set\\(\\$includeHeaders = true\\)'), { Ref: 'StateMachine2E01A3A5' }, - anything(), + Match.anyValue(), ], ], }, @@ -128,16 +127,16 @@ describe('StepFunctionsIntegration', () => { const integ = apigw.StepFunctionsIntegration.startExecution(stateMachine); api.root.addMethod('GET', integ); - expect(stack).toHaveResourceLike('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { Integration: { RequestTemplates: { 'application/json': { 'Fn::Join': [ '', [ - stringLike('*#set($includeQueryString = true)*'), + Match.stringLikeRegexp('#set\\(\\$includeQueryString = true\\)'), { Ref: 'StateMachine2E01A3A5' }, - anything(), + Match.anyValue(), ], ], }, @@ -145,16 +144,16 @@ describe('StepFunctionsIntegration', () => { }, }); - expect(stack).toHaveResourceLike('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { Integration: { RequestTemplates: { 'application/json': { 'Fn::Join': [ '', [ - stringLike('*#set($includePath = true)*'), + Match.stringLikeRegexp('#set\\(\\$includePath = true\\)'), { Ref: 'StateMachine2E01A3A5' }, - anything(), + Match.anyValue(), ], ], }, @@ -174,16 +173,16 @@ describe('StepFunctionsIntegration', () => { }); api.root.addMethod('GET', integ); - expect(stack).toHaveResourceLike('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { Integration: { RequestTemplates: { 'application/json': { 'Fn::Join': [ '', [ - stringLike('*#set($includeQueryString = false)*'), + Match.stringLikeRegexp('#set\\(\\$includeQueryString = false\\)'), { Ref: 'StateMachine2E01A3A5' }, - anything(), + Match.anyValue(), ], ], }, @@ -191,16 +190,16 @@ describe('StepFunctionsIntegration', () => { }, }); - expect(stack).toHaveResourceLike('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { Integration: { RequestTemplates: { 'application/json': { 'Fn::Join': [ '', [ - stringLike('*#set($includePath = false)*'), + Match.stringLikeRegexp('#set\\(\\$includePath = false\\)'), { Ref: 'StateMachine2E01A3A5' }, - anything(), + Match.anyValue(), ], ], }, @@ -217,16 +216,16 @@ describe('StepFunctionsIntegration', () => { const integ = apigw.StepFunctionsIntegration.startExecution(stateMachine, {}); api.root.addMethod('GET', integ); - expect(stack).toHaveResourceLike('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { Integration: { RequestTemplates: { 'application/json': { 'Fn::Join': [ '', [ - anything(), + Match.anyValue(), { Ref: 'StateMachine2E01A3A5' }, - stringLike('*#set($requestContext = \"\")*'), + Match.stringLikeRegexp('#set\\(\\$requestContext = \"\"\\)'), ], ], }, @@ -247,16 +246,16 @@ describe('StepFunctionsIntegration', () => { }); api.root.addMethod('GET', integ); - expect(stack).toHaveResourceLike('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { Integration: { RequestTemplates: { 'application/json': { 'Fn::Join': [ '', [ - anything(), + Match.anyValue(), { Ref: 'StateMachine2E01A3A5' }, - stringLike('*#set($requestContext = \"{@@accountId@@:@@$context.identity.accountId@@}\"*'), + Match.stringLikeRegexp('#set\\(\\$requestContext = \"{@@accountId@@:@@\\$context.identity.accountId@@}\"'), ], ], }, @@ -283,7 +282,7 @@ describe('StepFunctionsIntegration', () => { api.root.addMethod('ANY', apigw.StepFunctionsIntegration.startExecution(stateMachine)); - expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { ResourceId: 'imported-root-resource-id', RestApiId: 'imported-rest-api-id', }); diff --git a/packages/@aws-cdk/aws-apigateway/test/lambda-api.test.ts b/packages/@aws-cdk/aws-apigateway/test/lambda-api.test.ts index 7e412c549e897..314225d3afe34 100644 --- a/packages/@aws-cdk/aws-apigateway/test/lambda-api.test.ts +++ b/packages/@aws-cdk/aws-apigateway/test/lambda-api.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Match, Template } from '@aws-cdk/assertions'; import * as lambda from '@aws-cdk/aws-lambda'; import * as cdk from '@aws-cdk/core'; import * as apigw from '../lib'; @@ -23,11 +23,11 @@ describe('lambda api', () => { }).toThrow(); // THEN -- template proxies everything - expect(stack).toHaveResource('AWS::ApiGateway::Resource', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Resource', { PathPart: '{proxy+}', }); - expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { HttpMethod: 'ANY', ResourceId: { Ref: 'lambdarestapiproxyE3AE07E3', @@ -91,11 +91,11 @@ describe('lambda api', () => { }).toThrow(); // THEN -- template proxies everything - expect(stack).toHaveResource('AWS::ApiGateway::Resource', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Resource', { PathPart: '{proxy+}', }); - expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { HttpMethod: 'ANY', ResourceId: { Ref: 'lambdarestapiproxyE3AE07E3', @@ -149,20 +149,20 @@ describe('lambda api', () => { tasks.addMethod('POST'); // THEN - expect(stack).not.toHaveResource('AWS::ApiGateway::Resource', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Resource', Match.not({ PathPart: '{proxy+}', - }); + })); - expect(stack).toHaveResource('AWS::ApiGateway::Resource', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Resource', { PathPart: 'tasks', }); - expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { HttpMethod: 'GET', ResourceId: { Ref: 'lambdarestapitasks224418C8' }, }); - expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { HttpMethod: 'POST', ResourceId: { Ref: 'lambdarestapitasks224418C8' }, }); @@ -209,7 +209,7 @@ describe('lambda api', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { HttpMethod: 'OPTIONS', ResourceId: { Ref: 'lambdarestapiproxyE3AE07E3' }, Integration: { diff --git a/packages/@aws-cdk/aws-apigateway/test/method.test.ts b/packages/@aws-cdk/aws-apigateway/test/method.test.ts index f1037e550e23a..de41b0dfe363f 100644 --- a/packages/@aws-cdk/aws-apigateway/test/method.test.ts +++ b/packages/@aws-cdk/aws-apigateway/test/method.test.ts @@ -1,5 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; -import { ABSENT } from '@aws-cdk/assert-internal'; +import { Match, Template } from '@aws-cdk/assertions'; import * as iam from '@aws-cdk/aws-iam'; import * as lambda from '@aws-cdk/aws-lambda'; import { testDeprecated } from '@aws-cdk/cdk-build-tools'; @@ -24,7 +23,7 @@ describe('method', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { HttpMethod: 'POST', AuthorizationType: 'NONE', Integration: { @@ -51,7 +50,7 @@ describe('method', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { ApiKeyRequired: true, OperationName: 'MyOperation', }); @@ -72,7 +71,7 @@ describe('method', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { Integration: { IntegrationHttpMethod: 'POST', Type: 'AWS', @@ -104,7 +103,7 @@ describe('method', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { Integration: { IntegrationHttpMethod: 'POST', Type: 'AWS', @@ -133,7 +132,7 @@ describe('method', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { Integration: { IntegrationHttpMethod: 'GET', }, @@ -159,7 +158,7 @@ describe('method', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { Integration: { Type: 'HTTP_PROXY', Uri: 'https://amazon.com', @@ -325,7 +324,7 @@ describe('method', () => { })); // THEN - expect(stack).toHaveResourceLike('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { Integration: { Credentials: { 'Fn::GetAtt': ['MyRoleF48FFE04', 'Arn'] }, }, @@ -347,7 +346,7 @@ describe('method', () => { })); // THEN - expect(stack).toHaveResourceLike('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { Integration: { Credentials: { 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':iam::*:user/*']] }, }, @@ -386,7 +385,7 @@ describe('method', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { HttpMethod: 'GET', MethodResponses: [{ StatusCode: '200', @@ -435,7 +434,7 @@ describe('method', () => { })); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { Integration: { IntegrationHttpMethod: 'POST', IntegrationResponses: [ @@ -467,9 +466,9 @@ describe('method', () => { api.root.addMethod('PUT'); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::Method', { HttpMethod: 'POST' }); - expect(stack).toHaveResource('AWS::ApiGateway::Method', { HttpMethod: 'GET' }); - expect(stack).toHaveResource('AWS::ApiGateway::Method', { HttpMethod: 'PUT' }); + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { HttpMethod: 'POST' }); + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { HttpMethod: 'GET' }); + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { HttpMethod: 'PUT' }); }); @@ -499,7 +498,7 @@ describe('method', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { HttpMethod: 'GET', RequestModels: { 'application/json': { Ref: stack.getLogicalId(model.node.findChild('Resource') as cdk.CfnElement) }, @@ -549,7 +548,7 @@ describe('method', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { HttpMethod: 'GET', MethodResponses: [{ StatusCode: '200', @@ -593,10 +592,10 @@ describe('method', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { RequestValidatorId: { Ref: stack.getLogicalId(validator.node.findChild('Resource') as cdk.CfnElement) }, }); - expect(stack).toHaveResource('AWS::ApiGateway::RequestValidator', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::RequestValidator', { RestApiId: { Ref: stack.getLogicalId(api.node.findChild('Resource') as cdk.CfnElement) }, ValidateRequestBody: true, ValidateRequestParameters: false, @@ -626,7 +625,7 @@ describe('method', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { OperationName: 'defaultRequestParameters', RequestParameters: { 'method.request.path.proxy': true, @@ -644,7 +643,7 @@ describe('method', () => { authorizer: DUMMY_AUTHORIZER, }); - expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { HttpMethod: 'ANY', AuthorizationType: 'CUSTOM', AuthorizerId: DUMMY_AUTHORIZER.authorizerId, @@ -674,7 +673,7 @@ describe('method', () => { }); restApi.root.addMethod('ANY'); - expect(stack).toHaveResource('AWS::ApiGateway::Authorizer', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Authorizer', { Name: 'myauthorizer1', Type: 'TOKEN', RestApiId: stack.resolve(restApi.restApiId), @@ -732,7 +731,7 @@ describe('method', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { ApiKeyRequired: true, AuthorizationScopes: ['AuthScope1', 'AuthScope2'], }); @@ -761,7 +760,7 @@ describe('method', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { OperationName: 'defaultAuthScopes', AuthorizationScopes: ['DefaultAuth'], }); @@ -791,7 +790,7 @@ describe('method', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { ApiKeyRequired: true, AuthorizationScopes: ['MethodAuthScope'], }); @@ -817,9 +816,9 @@ describe('method', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { OperationName: 'authScopesAbsent', - AuthorizationScopes: ABSENT, + AuthorizationScopes: Match.absent(), }); @@ -844,7 +843,7 @@ describe('method', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::RequestValidator', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::RequestValidator', { RestApiId: stack.resolve(api.restApiId), ValidateRequestBody: true, ValidateRequestParameters: false, @@ -866,8 +865,8 @@ describe('method', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::Method', { - RequestValidatorId: ABSENT, + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { + RequestValidatorId: Match.absent(), }); diff --git a/packages/@aws-cdk/aws-apigateway/test/model.test.ts b/packages/@aws-cdk/aws-apigateway/test/model.test.ts index 62602ef353959..4318f3c5b0093 100644 --- a/packages/@aws-cdk/aws-apigateway/test/model.test.ts +++ b/packages/@aws-cdk/aws-apigateway/test/model.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as cdk from '@aws-cdk/core'; import * as apigw from '../lib'; @@ -24,7 +24,7 @@ describe('model', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::Model', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Model', { RestApiId: { Ref: stack.getLogicalId(api.node.findChild('Resource') as cdk.CfnElement) }, Schema: { $schema: 'http://json-schema.org/draft-04/schema#', @@ -57,7 +57,7 @@ describe('model', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::Model', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Model', { RestApiId: { Ref: stack.getLogicalId(api.node.findChild('Resource') as cdk.CfnElement) }, Schema: { $schema: 'http://json-schema.org/draft-04/schema#', diff --git a/packages/@aws-cdk/aws-apigateway/test/requestvalidator.test.ts b/packages/@aws-cdk/aws-apigateway/test/requestvalidator.test.ts index ec29752b47b6a..061c7853d3392 100644 --- a/packages/@aws-cdk/aws-apigateway/test/requestvalidator.test.ts +++ b/packages/@aws-cdk/aws-apigateway/test/requestvalidator.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as cdk from '@aws-cdk/core'; import * as apigateway from '../lib'; @@ -20,7 +20,7 @@ describe('request validator', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::RequestValidator', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::RequestValidator', { RestApiId: { Ref: stack.getLogicalId(api.node.findChild('Resource') as cdk.CfnElement) }, ValidateRequestBody: true, ValidateRequestParameters: false, @@ -45,7 +45,7 @@ describe('request validator', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::RequestValidator', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::RequestValidator', { RestApiId: { Ref: stack.getLogicalId(api.node.findChild('Resource') as cdk.CfnElement) }, Name: 'my-model', ValidateRequestBody: false, diff --git a/packages/@aws-cdk/aws-apigateway/test/resource.test.ts b/packages/@aws-cdk/aws-apigateway/test/resource.test.ts index b118c328073cb..3c97221b56339 100644 --- a/packages/@aws-cdk/aws-apigateway/test/resource.test.ts +++ b/packages/@aws-cdk/aws-apigateway/test/resource.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Match, Template } from '@aws-cdk/assertions'; import { Stack } from '@aws-cdk/core'; import * as apigw from '../lib'; @@ -16,7 +16,7 @@ describe('resource', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::Resource', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Resource', { 'ParentId': { 'Fn::GetAtt': [ 'apiC8550315', @@ -29,7 +29,7 @@ describe('resource', () => { }, }); - expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { 'HttpMethod': 'ANY', 'ResourceId': { 'Ref': 'proxy3A1DA9C7', @@ -60,9 +60,9 @@ describe('resource', () => { proxy.addMethod('GET'); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::Resource'); - expect(stack).toHaveResource('AWS::ApiGateway::Method', { 'HttpMethod': 'GET' }); - expect(stack).not.toHaveResource('AWS::ApiGateway::Method', { 'HttpMethod': 'ANY' }); + Template.fromStack(stack).resourceCountIs('AWS::ApiGateway::Resource', 1); + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { 'HttpMethod': 'GET' }); + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', Match.not({ 'HttpMethod': 'ANY' })); }); @@ -78,7 +78,7 @@ describe('resource', () => { const v2 = api.root.addResource('v2'); v2.addProxy(); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ 'Resources': { 'apiC8550315': { 'Type': 'AWS::ApiGateway::RestApi', @@ -154,7 +154,7 @@ describe('resource', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { HttpMethod: 'DELETE', ResourceId: { Ref: 'apiproxy4EA44110' }, Integration: { @@ -164,7 +164,7 @@ describe('resource', () => { OperationName: 'DeleteMe', }); - expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { HttpMethod: 'DELETE', ResourceId: { 'Fn::GetAtt': ['apiC8550315', 'RootResourceId'] }, Integration: { @@ -251,7 +251,7 @@ describe('resource', () => { imported.addMethod('GET'); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { HttpMethod: 'GET', ResourceId: resourceId, }); diff --git a/packages/@aws-cdk/aws-apigateway/test/restapi.test.ts b/packages/@aws-cdk/aws-apigateway/test/restapi.test.ts index 7a9f117afe126..f873c6c643f5d 100644 --- a/packages/@aws-cdk/aws-apigateway/test/restapi.test.ts +++ b/packages/@aws-cdk/aws-apigateway/test/restapi.test.ts @@ -1,5 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; -import { ResourcePart, SynthUtils } from '@aws-cdk/assert-internal'; +import { Template } from '@aws-cdk/assertions'; import { GatewayVpcEndpoint } from '@aws-cdk/aws-ec2'; import { testDeprecated } from '@aws-cdk/cdk-build-tools'; import { App, CfnElement, CfnResource, Stack } from '@aws-cdk/core'; @@ -15,7 +14,7 @@ describe('restapi', () => { api.root.addMethod('GET'); // must have at least one method or an API definition // THEN - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ Resources: { myapi4C7BF186: { Type: 'AWS::ApiGateway::RestApi', @@ -132,7 +131,7 @@ describe('restapi', () => { api.root.addMethod('GET'); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::RestApi', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::RestApi', { Name: 'restapi', }); }); @@ -168,17 +167,17 @@ describe('restapi', () => { foo.addResource('{hello}'); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::Resource', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Resource', { PathPart: 'foo', ParentId: { 'Fn::GetAtt': ['restapiC5611D27', 'RootResourceId'] }, }); - expect(stack).toHaveResource('AWS::ApiGateway::Resource', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Resource', { PathPart: 'bar', ParentId: { 'Fn::GetAtt': ['restapiC5611D27', 'RootResourceId'] }, }); - expect(stack).toHaveResource('AWS::ApiGateway::Resource', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Resource', { PathPart: '{hello}', ParentId: { Ref: 'restapifooF697E056' }, }); @@ -198,7 +197,7 @@ describe('restapi', () => { proxy.addMethod('ANY'); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::Resource', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Resource', { PathPart: '{proxy+}', ParentId: { 'Fn::GetAtt': ['restapiC5611D27', 'RootResourceId'] }, }); @@ -216,7 +215,7 @@ describe('restapi', () => { r1.addMethod('POST'); // THEN - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ Resources: { restapiC5611D27: { Type: 'AWS::ApiGateway::RestApi', @@ -330,8 +329,8 @@ describe('restapi', () => { api.root.addMethod('GET'); // THEN - expect(stack).toHaveResource('AWS::IAM::Role'); - expect(stack).toHaveResource('AWS::ApiGateway::Account'); + Template.fromStack(stack).resourceCountIs('AWS::IAM::Role', 1); + Template.fromStack(stack).resourceCountIs('AWS::ApiGateway::Account', 1); }); test('"url" and "urlForPath" return the URL endpoints of the deployed API', () => { @@ -462,7 +461,7 @@ describe('restapi', () => { api.root.addMethod('GET'); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::RestApi', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::RestApi', { EndpointConfiguration: { Types: [ 'EDGE', @@ -486,7 +485,7 @@ describe('restapi', () => { api.root.addMethod('GET'); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::RestApi', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::RestApi', { EndpointConfiguration: { Types: ['EDGE', 'PRIVATE'], }, @@ -511,7 +510,7 @@ describe('restapi', () => { api.root.addMethod('GET'); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::RestApi', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::RestApi', { EndpointConfiguration: { Types: [ 'EDGE', @@ -551,7 +550,7 @@ describe('restapi', () => { api.root.addMethod('GET'); - expect(stack).toHaveResource('AWS::ApiGateway::RestApi', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::RestApi', { CloneFrom: 'foobar', Name: 'api', }); @@ -568,7 +567,7 @@ describe('restapi', () => { resource.node.addDependency(api); // THEN - expect(stack).toHaveResource('My::Resource', { + Template.fromStack(stack).hasResource('My::Resource', { DependsOn: [ 'myapiAccountC3A4750C', 'myapiCloudWatchRoleEB425128', @@ -577,7 +576,7 @@ describe('restapi', () => { 'myapiDeploymentStageprod329F21FF', 'myapi162F20B8', ], - }, ResourcePart.CompleteDefinition); + }); }); test('defaultIntegration and defaultMethodOptions can be used at any level', () => { @@ -624,7 +623,7 @@ describe('restapi', () => { // THEN // CASE #1 - expect(stack).toHaveResourceLike('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { HttpMethod: 'GET', ResourceId: { 'Fn::GetAtt': ['myapi162F20B8', 'RootResourceId'] }, Integration: { Type: 'AWS' }, @@ -633,7 +632,7 @@ describe('restapi', () => { }); // CASE #2 - expect(stack).toHaveResourceLike('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { HttpMethod: 'POST', ResourceId: { Ref: 'myapichildA0A65412' }, Integration: { Type: 'AWS' }, @@ -642,7 +641,7 @@ describe('restapi', () => { }); // CASE #3 - expect(stack).toHaveResourceLike('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { HttpMethod: 'DELETE', Integration: { Type: 'MOCK' }, AuthorizerId: 'AUTHID2', @@ -650,7 +649,7 @@ describe('restapi', () => { }); // CASE #4 - expect(stack).toHaveResourceLike('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { HttpMethod: 'PUT', Integration: { Type: 'AWS' }, AuthorizerId: 'AUTHID2', @@ -671,7 +670,7 @@ describe('restapi', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::ApiKey', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::ApiKey', { Enabled: true, Name: 'myApiKey1', StageKeys: [ @@ -701,7 +700,7 @@ describe('restapi', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::Model', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Model', { RestApiId: { Ref: stack.getLogicalId(api.node.findChild('Resource') as CfnElement) }, Schema: { $schema: 'http://json-schema.org/draft-04/schema#', @@ -731,14 +730,14 @@ describe('restapi', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::RequestValidator', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::RequestValidator', { RestApiId: { Ref: stack.getLogicalId(api.node.findChild('Resource') as CfnElement) }, Name: 'Parameters', ValidateRequestBody: false, ValidateRequestParameters: true, }); - expect(stack).toHaveResource('AWS::ApiGateway::RequestValidator', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::RequestValidator', { RestApiId: { Ref: stack.getLogicalId(api.node.findChild('Resource') as CfnElement) }, Name: 'Body', ValidateRequestBody: true, @@ -755,7 +754,8 @@ describe('restapi', () => { api.root.addMethod('GET'); // THEN - expect(SynthUtils.toCloudFormation(stack).Outputs).toEqual({ + const outputs = Template.fromStack(stack).findOutputs('myapiEndpoint8EB17201'); + expect(outputs).toEqual({ myapiEndpoint8EB17201: { Value: { 'Fn::Join': [ @@ -787,7 +787,8 @@ describe('restapi', () => { api.root.addMethod('GET'); // THEN - expect(SynthUtils.toCloudFormation(stack).Outputs).toEqual({ + const outputs = Template.fromStack(stack).findOutputs('myapiEndpoint8EB17201'); + expect(outputs).toEqual({ myapiEndpoint8EB17201: { Value: { 'Fn::Join': [ @@ -864,11 +865,11 @@ describe('restapi', () => { resource.addMethod('GET'); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::Resource', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Resource', { PathPart: 'pets', ParentId: stack.resolve(imported.restApiRootResourceId), }); - expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { HttpMethod: 'GET', ResourceId: stack.resolve(resource.resourceId), }); @@ -888,11 +889,11 @@ describe('restapi', () => { resource.addMethod('GET'); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::Resource', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Resource', { PathPart: 'pets', ParentId: stack.resolve(api.restApiRootResourceId), }); - expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { HttpMethod: 'GET', ResourceId: stack.resolve(resource.resourceId), }); @@ -911,7 +912,7 @@ describe('restapi', () => { api.root.addMethod('GET'); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::RestApi', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::RestApi', { EndpointConfiguration: { Types: [ 'EDGE', @@ -936,7 +937,7 @@ describe('restapi', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::ApiKey', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::ApiKey', { Enabled: true, Name: 'myApiKey1', StageKeys: [ @@ -1081,7 +1082,7 @@ describe('restapi', () => { api.root.addMethod('GET'); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::RestApi', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::RestApi', { DisableExecuteApiEndpoint: true, }); }); diff --git a/packages/@aws-cdk/aws-apigateway/test/stage.test.ts b/packages/@aws-cdk/aws-apigateway/test/stage.test.ts index a9b030ebba07d..76d07c6e2b4b3 100644 --- a/packages/@aws-cdk/aws-apigateway/test/stage.test.ts +++ b/packages/@aws-cdk/aws-apigateway/test/stage.test.ts @@ -1,5 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; -import { ResourcePart } from '@aws-cdk/assert-internal'; +import { Template } from '@aws-cdk/assertions'; import * as logs from '@aws-cdk/aws-logs'; import * as cdk from '@aws-cdk/core'; import * as apigateway from '../lib'; @@ -16,7 +15,7 @@ describe('stage', () => { new apigateway.Stage(stack, 'my-stage', { deployment }); // THEN - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ Resources: { testapiD6451F70: { Type: 'AWS::ApiGateway::RestApi', @@ -80,9 +79,9 @@ describe('stage', () => { // WHEN new apigateway.Stage(stack, 'my-stage', { deployment }); - expect(stack).toHaveResourceLike('AWS::ApiGateway::Stage', { + Template.fromStack(stack).hasResource('AWS::ApiGateway::Stage', { DependsOn: ['testapiAccount9B907665'], - }, ResourcePart.CompleteDefinition); + }); }); test('common method settings can be set at the stage level', () => { @@ -100,7 +99,7 @@ describe('stage', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::Stage', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Stage', { MethodSettings: [ { DataTraceEnabled: false, @@ -165,7 +164,7 @@ describe('stage', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::Stage', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Stage', { MethodSettings: [ { DataTraceEnabled: false, @@ -198,7 +197,7 @@ describe('stage', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::Stage', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Stage', { CacheClusterEnabled: true, CacheClusterSize: '0.5', }); @@ -218,7 +217,7 @@ describe('stage', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::Stage', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Stage', { CacheClusterEnabled: true, CacheClusterSize: '0.5', }); @@ -253,7 +252,7 @@ describe('stage', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::Stage', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Stage', { CacheClusterEnabled: true, CacheClusterSize: '0.5', MethodSettings: [ @@ -298,7 +297,7 @@ describe('stage', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::Stage', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Stage', { AccessLogSetting: { DestinationArn: { 'Fn::GetAtt': [ @@ -329,7 +328,7 @@ describe('stage', () => { }); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::Stage', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Stage', { AccessLogSetting: { DestinationArn: { 'Fn::GetAtt': [ diff --git a/packages/@aws-cdk/aws-apigateway/test/stepfunctions-api.test.ts b/packages/@aws-cdk/aws-apigateway/test/stepfunctions-api.test.ts index 30a1769b8965b..51426a4ffe6c3 100644 --- a/packages/@aws-cdk/aws-apigateway/test/stepfunctions-api.test.ts +++ b/packages/@aws-cdk/aws-apigateway/test/stepfunctions-api.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as sfn from '@aws-cdk/aws-stepfunctions'; import { StateMachine } from '@aws-cdk/aws-stepfunctions'; import * as cdk from '@aws-cdk/core'; @@ -17,7 +17,7 @@ describe('Step Functions api', () => { }).toThrow(); //THEN - expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { HttpMethod: 'ANY', MethodResponses: getMethodResponse(), AuthorizationType: 'NONE', diff --git a/packages/@aws-cdk/aws-apigateway/test/usage-plan.test.ts b/packages/@aws-cdk/aws-apigateway/test/usage-plan.test.ts index 195e17a0b7fbf..b31e3c90b256b 100644 --- a/packages/@aws-cdk/aws-apigateway/test/usage-plan.test.ts +++ b/packages/@aws-cdk/aws-apigateway/test/usage-plan.test.ts @@ -1,5 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; -import { ResourcePart } from '@aws-cdk/assert-internal'; +import { Template } from '@aws-cdk/assertions'; import * as cdk from '@aws-cdk/core'; import * as cxapi from '@aws-cdk/cx-api'; import { testFutureBehavior } from '@aws-cdk/cdk-build-tools/lib/feature-flag'; @@ -22,10 +21,10 @@ describe('usage plan', () => { }); // THEN - expect(stack).toHaveResource(RESOURCE_TYPE, { + Template.fromStack(stack).hasResourceProperties(RESOURCE_TYPE, { UsagePlanName: usagePlanName, Description: usagePlanDescription, - }, ResourcePart.Properties); + }); }); test('usage plan with throttling limits', () => { @@ -57,7 +56,7 @@ describe('usage plan', () => { }); // THEN - expect(stack).toHaveResource(RESOURCE_TYPE, { + Template.fromStack(stack).hasResourceProperties(RESOURCE_TYPE, { UsagePlanName: usagePlanName, Description: usagePlanDescription, ApiStages: [ @@ -76,7 +75,7 @@ describe('usage plan', () => { }, }, ], - }, ResourcePart.Properties); + }); }); test('usage plan with blocked methods', () => { @@ -108,7 +107,7 @@ describe('usage plan', () => { }); // THEN - expect(stack).toHaveResource(RESOURCE_TYPE, { + Template.fromStack(stack).hasResourceProperties(RESOURCE_TYPE, { UsagePlanName: usagePlanName, Description: usagePlanDescription, ApiStages: [ @@ -127,7 +126,7 @@ describe('usage plan', () => { }, }, ], - }, ResourcePart.Properties); + }); }); test('usage plan with quota limits', () => { @@ -143,12 +142,12 @@ describe('usage plan', () => { }); // THEN - expect(stack).toHaveResource(RESOURCE_TYPE, { + Template.fromStack(stack).hasResourceProperties(RESOURCE_TYPE, { Quota: { Limit: 10000, Period: 'MONTH', }, - }, ResourcePart.Properties); + }); }); describe('UsagePlanKey', () => { @@ -165,7 +164,7 @@ describe('usage plan', () => { usagePlan.addApiKey(apiKey); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::UsagePlanKey', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::UsagePlanKey', { KeyId: { Ref: 'myapikey1B052F70', }, @@ -173,7 +172,7 @@ describe('usage plan', () => { UsagePlanId: { Ref: 'myusageplan23AA1E32', }, - }, ResourcePart.Properties); + }); }); @@ -187,13 +186,13 @@ describe('usage plan', () => { usagePlan.addApiKey(apiKey); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::UsagePlanKey', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::UsagePlanKey', { KeyId: { Ref: 'myapikey1B052F70', }, KeyType: 'API_KEY', UsagePlanId: 'imported-id', - }, ResourcePart.Properties); + }); }); test('multiple keys', () => { @@ -212,22 +211,22 @@ describe('usage plan', () => { usagePlan.addApiKey(apiKey2); // THEN - expect(stack).toHaveResource('AWS::ApiGateway::ApiKey', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::ApiKey', { Name: 'my-api-key-1', - }, ResourcePart.Properties); - expect(stack).toHaveResource('AWS::ApiGateway::ApiKey', { + }); + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::ApiKey', { Name: 'my-api-key-2', - }, ResourcePart.Properties); - expect(stack).toHaveResource('AWS::ApiGateway::UsagePlanKey', { + }); + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::UsagePlanKey', { KeyId: { Ref: 'myapikey11F723FC7', }, - }, ResourcePart.Properties); - expect(stack).toHaveResource('AWS::ApiGateway::UsagePlanKey', { + }); + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::UsagePlanKey', { KeyId: { Ref: 'myapikey2ABDEF012', }, - }, ResourcePart.Properties); + }); }); test('overrideLogicalId', () => { diff --git a/packages/@aws-cdk/aws-apigateway/test/util.test.ts b/packages/@aws-cdk/aws-apigateway/test/util.test.ts index 07c8a2cef379d..30861de05dad1 100644 --- a/packages/@aws-cdk/aws-apigateway/test/util.test.ts +++ b/packages/@aws-cdk/aws-apigateway/test/util.test.ts @@ -1,4 +1,3 @@ -import '@aws-cdk/assert-internal/jest'; import { JsonSchema, JsonSchemaType } from '../lib'; import { JsonSchemaMapper, parseAwsApiCall, parseMethodOptionsPath } from '../lib/util'; diff --git a/packages/@aws-cdk/aws-apigateway/test/vpc-link.test.ts b/packages/@aws-cdk/aws-apigateway/test/vpc-link.test.ts index ba2b82464141d..b41e1a4d19471 100644 --- a/packages/@aws-cdk/aws-apigateway/test/vpc-link.test.ts +++ b/packages/@aws-cdk/aws-apigateway/test/vpc-link.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as elbv2 from '@aws-cdk/aws-elasticloadbalancingv2'; import * as cdk from '@aws-cdk/core'; @@ -20,7 +20,7 @@ describe('vpc link', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::ApiGateway::VpcLink', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::VpcLink', { Name: 'MyLink', TargetArns: [{ Ref: 'NLB55158F82' }], }); @@ -43,7 +43,7 @@ describe('vpc link', () => { link.addTargets(nlb3); // THEN - expect(stack).toHaveResourceLike('AWS::ApiGateway::VpcLink', { + Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::VpcLink', { Name: 'VpcLink', TargetArns: [ { Ref: 'NLB03D178991' }, @@ -62,7 +62,7 @@ describe('vpc link', () => { apigateway.VpcLink.fromVpcLinkId(stack, 'ImportedVpcLink', 'vpclink-id'); // THEN - expect(stack).not.toHaveResource('AWS::ApiGateway::VpcLink'); + Template.fromStack(stack).resourceCountIs('AWS::ApiGateway::VpcLink', 0); }); test('validation error if vpc link is created and no targets are added', () => { diff --git a/packages/@aws-cdk/aws-apprunner/package.json b/packages/@aws-cdk/aws-apprunner/package.json index a9c982a6cd9b0..0627cfbcc38fa 100644 --- a/packages/@aws-cdk/aws-apprunner/package.json +++ b/packages/@aws-cdk/aws-apprunner/package.json @@ -83,7 +83,6 @@ }, "license": "Apache-2.0", "devDependencies": { - "@aws-cdk/assert-internal": "0.0.0", "@aws-cdk/assertions": "0.0.0", "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cdk-integ-tools": "0.0.0", diff --git a/packages/@aws-cdk/aws-autoscaling-hooktargets/package.json b/packages/@aws-cdk/aws-autoscaling-hooktargets/package.json index 89be3fc475a6b..d0e520c926fe7 100644 --- a/packages/@aws-cdk/aws-autoscaling-hooktargets/package.json +++ b/packages/@aws-cdk/aws-autoscaling-hooktargets/package.json @@ -64,7 +64,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@aws-cdk/assert-internal": "0.0.0", + "@aws-cdk/assertions": "0.0.0", "@aws-cdk/aws-ec2": "0.0.0", "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cdk-integ-tools": "0.0.0", diff --git a/packages/@aws-cdk/aws-autoscaling-hooktargets/test/hooks.test.ts b/packages/@aws-cdk/aws-autoscaling-hooktargets/test/hooks.test.ts index 4615c45913b44..413142423f7b5 100644 --- a/packages/@aws-cdk/aws-autoscaling-hooktargets/test/hooks.test.ts +++ b/packages/@aws-cdk/aws-autoscaling-hooktargets/test/hooks.test.ts @@ -1,5 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; -import { arrayWith } from '@aws-cdk/assert-internal'; +import { Match, Template } from '@aws-cdk/assertions'; import * as autoscaling from '@aws-cdk/aws-autoscaling'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as iam from '@aws-cdk/aws-iam'; @@ -27,7 +26,7 @@ describe('given an AutoScalingGroup and no role', () => { }); afterEach(() => { - expect(stack).toHaveResource('AWS::IAM::Role', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { AssumeRolePolicyDocument: { Version: '2012-10-17', Statement: [ @@ -54,8 +53,8 @@ describe('given an AutoScalingGroup and no role', () => { }); // THEN - expect(stack).toHaveResource('AWS::AutoScaling::LifecycleHook', { NotificationTargetARN: { 'Fn::GetAtt': ['Queue4A7E3555', 'Arn'] } }); - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::LifecycleHook', { NotificationTargetARN: { 'Fn::GetAtt': ['Queue4A7E3555', 'Arn'] } }); + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -95,8 +94,8 @@ describe('given an AutoScalingGroup and no role', () => { }); // THEN - expect(stack).toHaveResource('AWS::AutoScaling::LifecycleHook', { NotificationTargetARN: { Ref: 'TopicBFC7AF6E' } }); - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::LifecycleHook', { NotificationTargetARN: { Ref: 'TopicBFC7AF6E' } }); + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -133,13 +132,13 @@ describe('given an AutoScalingGroup and no role', () => { }); // THEN - expect(stack).toHaveResource('AWS::AutoScaling::LifecycleHook', { NotificationTargetARN: { Ref: 'ASGLifecycleHookTransTopic9B0D4842' } }); - expect(stack).toHaveResource('AWS::SNS::Subscription', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::LifecycleHook', { NotificationTargetARN: { Ref: 'ASGLifecycleHookTransTopic9B0D4842' } }); + Template.fromStack(stack).hasResourceProperties('AWS::SNS::Subscription', { Protocol: 'lambda', TopicArn: { Ref: 'ASGLifecycleHookTransTopic9B0D4842' }, Endpoint: { 'Fn::GetAtt': ['Fn9270CBC0', 'Arn'] }, }); - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -177,7 +176,7 @@ describe('given an AutoScalingGroup and no role', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::SNS::Topic', { + Template.fromStack(stack).hasResourceProperties('AWS::SNS::Topic', { KmsMasterKeyId: { 'Fn::GetAtt': [ 'keyFEDD6EC0', @@ -185,9 +184,9 @@ describe('given an AutoScalingGroup and no role', () => { ], }, }); - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { - Statement: arrayWith( + Statement: Match.arrayWith([ { Effect: 'Allow', Action: [ @@ -201,7 +200,7 @@ describe('given an AutoScalingGroup and no role', () => { ], }, }, - ), + ]), }, }); }); @@ -223,7 +222,7 @@ describe('given an AutoScalingGroup and a role', () => { }); afterEach(() => { - expect(stack).toHaveResource('AWS::IAM::Role', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { AssumeRolePolicyDocument: { Version: '2012-10-17', Statement: [ @@ -253,7 +252,7 @@ describe('given an AutoScalingGroup and a role', () => { }); // THEN - expect(stack).toHaveResource('AWS::AutoScaling::LifecycleHook', { NotificationTargetARN: { 'Fn::GetAtt': ['Queue4A7E3555', 'Arn'] } }); + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::LifecycleHook', { NotificationTargetARN: { 'Fn::GetAtt': ['Queue4A7E3555', 'Arn'] } }); }); test('can use topic as hook target with a role', () => { @@ -271,8 +270,8 @@ describe('given an AutoScalingGroup and a role', () => { }); // THEN - expect(stack).toHaveResource('AWS::AutoScaling::LifecycleHook', { NotificationTargetARN: { Ref: 'TopicBFC7AF6E' } }); - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::LifecycleHook', { NotificationTargetARN: { Ref: 'TopicBFC7AF6E' } }); + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -313,13 +312,13 @@ describe('given an AutoScalingGroup and a role', () => { }); // THEN - expect(stack).toHaveResource('AWS::AutoScaling::LifecycleHook', { NotificationTargetARN: { Ref: 'ASGLifecycleHookTransTopic9B0D4842' } }); - expect(stack).toHaveResource('AWS::SNS::Subscription', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::LifecycleHook', { NotificationTargetARN: { Ref: 'ASGLifecycleHookTransTopic9B0D4842' } }); + Template.fromStack(stack).hasResourceProperties('AWS::SNS::Subscription', { Protocol: 'lambda', TopicArn: { Ref: 'ASGLifecycleHookTransTopic9B0D4842' }, Endpoint: { 'Fn::GetAtt': ['Fn9270CBC0', 'Arn'] }, }); - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { diff --git a/packages/@aws-cdk/aws-autoscaling/package.json b/packages/@aws-cdk/aws-autoscaling/package.json index bd58a5c1c288b..cc9037521f731 100644 --- a/packages/@aws-cdk/aws-autoscaling/package.json +++ b/packages/@aws-cdk/aws-autoscaling/package.json @@ -79,7 +79,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@aws-cdk/assert-internal": "0.0.0", + "@aws-cdk/assertions": "0.0.0", "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cdk-integ-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-autoscaling/test/aspects/require-imdsv2-aspect.test.ts b/packages/@aws-cdk/aws-autoscaling/test/aspects/require-imdsv2-aspect.test.ts index 22a58f097a98b..5fa4210d2a6f5 100644 --- a/packages/@aws-cdk/aws-autoscaling/test/aspects/require-imdsv2-aspect.test.ts +++ b/packages/@aws-cdk/aws-autoscaling/test/aspects/require-imdsv2-aspect.test.ts @@ -1,8 +1,4 @@ -import { - expect as expectCDK, - haveResourceLike, -} from '@aws-cdk/assert-internal'; -import '@aws-cdk/assert-internal/jest'; +import { Match, Template } from '@aws-cdk/assertions'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as cdk from '@aws-cdk/core'; import { @@ -37,11 +33,12 @@ describe('AutoScalingGroupRequireImdsv2Aspect', () => { cdk.Aspects.of(stack).add(aspect); // THEN - expectCDK(stack).notTo(haveResourceLike('AWS::AutoScaling::LaunchConfiguration', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::LaunchConfiguration', Match.not({ MetadataOptions: { HttpTokens: 'required', }, })); + expect(asg.node.metadataEntry).toContainEqual({ data: expect.stringContaining('CfnLaunchConfiguration.MetadataOptions field is a CDK token.'), type: 'aws:cdk:warning', @@ -62,11 +59,11 @@ describe('AutoScalingGroupRequireImdsv2Aspect', () => { cdk.Aspects.of(stack).add(aspect); // THEN - expectCDK(stack).to(haveResourceLike('AWS::AutoScaling::LaunchConfiguration', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::LaunchConfiguration', { MetadataOptions: { HttpTokens: 'required', }, - })); + }); }); }); diff --git a/packages/@aws-cdk/aws-autoscaling/test/auto-scaling-group.test.ts b/packages/@aws-cdk/aws-autoscaling/test/auto-scaling-group.test.ts index 8ca98c19deeb8..945339ca86f5b 100644 --- a/packages/@aws-cdk/aws-autoscaling/test/auto-scaling-group.test.ts +++ b/packages/@aws-cdk/aws-autoscaling/test/auto-scaling-group.test.ts @@ -1,5 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; -import { ABSENT, InspectionFailure, ResourcePart } from '@aws-cdk/assert-internal'; +import { Match, Template } from '@aws-cdk/assertions'; import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as iam from '@aws-cdk/aws-iam'; @@ -22,7 +21,7 @@ describe('auto scaling group', () => { vpc, }); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ 'Parameters': { 'SsmParameterValueawsserviceamiamazonlinuxlatestamznamihvmx8664gp2C96584B6F00A464EAD1953AFF4B05118Parameter': { 'Type': 'AWS::SSM::Parameter::Value', @@ -136,8 +135,6 @@ describe('auto scaling group', () => { }, }, }); - - }); test('can set minCapacity, maxCapacity, desiredCapacity to 0', () => { @@ -153,14 +150,11 @@ describe('auto scaling group', () => { desiredCapacity: 0, }); - expect(stack).toHaveResource('AWS::AutoScaling::AutoScalingGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::AutoScalingGroup', { MinSize: '0', MaxSize: '0', DesiredCapacity: '0', - }, - ); - - + }); }); test('validation is not performed when using Tokens', () => { @@ -177,14 +171,11 @@ describe('auto scaling group', () => { }); // THEN: no exception - expect(stack).toHaveResource('AWS::AutoScaling::AutoScalingGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::AutoScalingGroup', { MinSize: '5', MaxSize: '1', DesiredCapacity: '20', - }, - ); - - + }); }); test('userdata can be overridden by image', () => { @@ -206,8 +197,6 @@ describe('auto scaling group', () => { // THEN expect(asg.userData.render()).toEqual('#!/bin/bash\nit me!'); - - }); test('userdata can be overridden at ASG directly', () => { @@ -233,8 +222,6 @@ describe('auto scaling group', () => { // THEN expect(asg.userData.render()).toEqual('#!/bin/bash\nno me!'); - - }); test('can specify only min capacity', () => { @@ -251,13 +238,10 @@ describe('auto scaling group', () => { }); // THEN - expect(stack).toHaveResource('AWS::AutoScaling::AutoScalingGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::AutoScalingGroup', { MinSize: '10', MaxSize: '10', - }, - ); - - + }); }); test('can specify only max capacity', () => { @@ -274,13 +258,10 @@ describe('auto scaling group', () => { }); // THEN - expect(stack).toHaveResource('AWS::AutoScaling::AutoScalingGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::AutoScalingGroup', { MinSize: '1', MaxSize: '10', - }, - ); - - + }); }); test('can specify only desiredCount', () => { @@ -297,14 +278,11 @@ describe('auto scaling group', () => { }); // THEN - expect(stack).toHaveResource('AWS::AutoScaling::AutoScalingGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::AutoScalingGroup', { MinSize: '1', MaxSize: '10', DesiredCapacity: '10', - }, - ); - - + }); }); test('addToRolePolicy can be used to add statements to the role policy', () => { @@ -322,7 +300,7 @@ describe('auto scaling group', () => { resources: ['*'], })); - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Version: '2012-10-17', Statement: [ @@ -352,7 +330,7 @@ describe('auto scaling group', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::AutoScaling::AutoScalingGroup', { + Template.fromStack(stack).hasResource('AWS::AutoScaling::AutoScalingGroup', { UpdatePolicy: { AutoScalingReplacingUpdate: { WillReplace: true, @@ -363,9 +341,7 @@ describe('auto scaling group', () => { MinSuccessfulInstancesPercent: 50, }, }, - }, ResourcePart.CompleteDefinition); - - + }); }); testDeprecated('can configure rolling update', () => { @@ -386,7 +362,7 @@ describe('auto scaling group', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::AutoScaling::AutoScalingGroup', { + Template.fromStack(stack).hasResource('AWS::AutoScaling::AutoScalingGroup', { UpdatePolicy: { 'AutoScalingRollingUpdate': { 'MinSuccessfulInstancesPercent': 50, @@ -395,9 +371,7 @@ describe('auto scaling group', () => { 'SuspendProcesses': ['HealthCheck', 'ReplaceUnhealthy', 'AZRebalance', 'AlarmNotification', 'ScheduledActions'], }, }, - }, ResourcePart.CompleteDefinition); - - + }); }); testDeprecated('can configure resource signals', () => { @@ -415,16 +389,14 @@ describe('auto scaling group', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::AutoScaling::AutoScalingGroup', { + Template.fromStack(stack).hasResource('AWS::AutoScaling::AutoScalingGroup', { CreationPolicy: { ResourceSignal: { Count: 5, Timeout: 'PT11M6S', }, }, - }, ResourcePart.CompleteDefinition); - - + }); }); test('can configure EC2 health check', () => { @@ -441,11 +413,9 @@ describe('auto scaling group', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::AutoScaling::AutoScalingGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::AutoScalingGroup', { HealthCheckType: 'EC2', }); - - }); test('can configure EBS health check', () => { @@ -462,12 +432,10 @@ describe('auto scaling group', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::AutoScaling::AutoScalingGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::AutoScalingGroup', { HealthCheckType: 'ELB', HealthCheckGracePeriod: 900, }); - - }); test('can add Security Group to Fleet', () => { @@ -482,7 +450,7 @@ describe('auto scaling group', () => { vpc, }); asg.addSecurityGroup(mockSecurityGroup(stack)); - expect(stack).toHaveResource('AWS::AutoScaling::LaunchConfiguration', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::LaunchConfiguration', { SecurityGroups: [ { 'Fn::GetAtt': [ @@ -493,7 +461,6 @@ describe('auto scaling group', () => { 'most-secure', ], }); - }); test('can set tags', () => { @@ -514,7 +481,7 @@ describe('auto scaling group', () => { cdk.Tags.of(asg).add('notsuper', 'caramel', { applyToLaunchedInstances: false }); // THEN - expect(stack).toHaveResource('AWS::AutoScaling::AutoScalingGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::AutoScalingGroup', { Tags: [ { Key: 'Name', @@ -533,7 +500,6 @@ describe('auto scaling group', () => { }, ], }); - }); test('allows setting spot price', () => { @@ -552,11 +518,9 @@ describe('auto scaling group', () => { // THEN expect(asg.spotPrice).toEqual('0.05'); - expect(stack).toHaveResource('AWS::AutoScaling::LaunchConfiguration', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::LaunchConfiguration', { SpotPrice: '0.05', }); - - }); test('allows association of public IP address', () => { @@ -578,11 +542,9 @@ describe('auto scaling group', () => { }); // THEN - expect(stack).toHaveResource('AWS::AutoScaling::LaunchConfiguration', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::LaunchConfiguration', { AssociatePublicIpAddress: true, - }, - ); - + }); }); test('association of public IP address requires public subnet', () => { @@ -602,7 +564,6 @@ describe('auto scaling group', () => { associatePublicIpAddress: true, }); }).toThrow(); - }); test('allows disassociation of public IP address', () => { @@ -622,11 +583,9 @@ describe('auto scaling group', () => { }); // THEN - expect(stack).toHaveResource('AWS::AutoScaling::LaunchConfiguration', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::LaunchConfiguration', { AssociatePublicIpAddress: false, - }, - ); - + }); }); test('does not specify public IP address association by default', () => { @@ -645,16 +604,9 @@ describe('auto scaling group', () => { }); // THEN - expect(stack).toHaveResource('AWS::AutoScaling::LaunchConfiguration', (resource: any, errors: InspectionFailure) => { - for (const key of Object.keys(resource)) { - if (key === 'AssociatePublicIpAddress') { - errors.failureReason = 'Has AssociatePublicIpAddress'; - return false; - } - } - return true; + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::LaunchConfiguration', { + AssociatePublicIpAddress: Match.absent(), }); - }); test('an existing security group can be specified instead of auto-created', () => { @@ -672,11 +624,9 @@ describe('auto scaling group', () => { }); // THEN - expect(stack).toHaveResource('AWS::AutoScaling::LaunchConfiguration', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::LaunchConfiguration', { SecurityGroups: ['most-secure'], - }, - ); - + }); }); test('an existing role can be specified instead of auto-created', () => { @@ -695,10 +645,9 @@ describe('auto scaling group', () => { // THEN expect(asg.role).toEqual(importedRole); - expect(stack).toHaveResource('AWS::IAM::InstanceProfile', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::InstanceProfile', { 'Roles': ['HelloDude'], }); - }); test('defaultChild is available on an ASG', () => { @@ -713,8 +662,6 @@ describe('auto scaling group', () => { // THEN expect(asg.node.defaultChild instanceof autoscaling.CfnAutoScalingGroup).toEqual(true); - - }); test('can set blockDeviceMappings', () => { @@ -755,7 +702,7 @@ describe('auto scaling group', () => { }); // THEN - expect(stack).toHaveResource('AWS::AutoScaling::LaunchConfiguration', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::LaunchConfiguration', { BlockDeviceMappings: [ { DeviceName: 'ebs', @@ -766,7 +713,7 @@ describe('auto scaling group', () => { VolumeSize: 15, VolumeType: 'io1', }, - NoDevice: ABSENT, + NoDevice: Match.absent(), }, { DeviceName: 'ebs-snapshot', @@ -776,12 +723,12 @@ describe('auto scaling group', () => { VolumeSize: 500, VolumeType: 'sc1', }, - NoDevice: ABSENT, + NoDevice: Match.absent(), }, { DeviceName: 'ephemeral', VirtualName: 'ephemeral0', - NoDevice: ABSENT, + NoDevice: Match.absent(), }, { DeviceName: 'disabled', @@ -793,8 +740,6 @@ describe('auto scaling group', () => { }, ], }); - - }); test('can configure maxInstanceLifetime', () => { @@ -809,11 +754,9 @@ describe('auto scaling group', () => { }); // THEN - expect(stack).toHaveResource('AWS::AutoScaling::AutoScalingGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::AutoScalingGroup', { 'MaxInstanceLifetime': 604800, }); - - }); test('throws if maxInstanceLifetime < 7 days', () => { @@ -830,8 +773,6 @@ describe('auto scaling group', () => { maxInstanceLifetime: cdk.Duration.days(6), }); }).toThrow(/maxInstanceLifetime must be between 7 and 365 days \(inclusive\)/); - - }); test('throws if maxInstanceLifetime > 365 days', () => { @@ -848,8 +789,6 @@ describe('auto scaling group', () => { maxInstanceLifetime: cdk.Duration.days(366), }); }).toThrow(/maxInstanceLifetime must be between 7 and 365 days \(inclusive\)/); - - }); test('can configure instance monitoring', () => { @@ -866,10 +805,9 @@ describe('auto scaling group', () => { }); // THEN - expect(stack).toHaveResource('AWS::AutoScaling::LaunchConfiguration', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::LaunchConfiguration', { InstanceMonitoring: false, }); - }); test('instance monitoring defaults to absent', () => { @@ -885,10 +823,9 @@ describe('auto scaling group', () => { }); // THEN - expect(stack).toHaveResource('AWS::AutoScaling::LaunchConfiguration', { - InstanceMonitoring: ABSENT, + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::LaunchConfiguration', { + InstanceMonitoring: Match.absent(), }); - }); test('throws if ephemeral volumeIndex < 0', () => { @@ -908,8 +845,6 @@ describe('auto scaling group', () => { }], }); }).toThrow(/volumeIndex must be a number starting from 0/); - - }); test('throws if volumeType === IO1 without iops', () => { @@ -933,8 +868,6 @@ describe('auto scaling group', () => { }], }); }).toThrow(/ops property is required with volumeType: EbsDeviceVolumeType.IO1/); - - }); test('warning if iops without volumeType', () => { @@ -959,8 +892,6 @@ describe('auto scaling group', () => { // THEN expect(asg.node.metadataEntry[0].type).toEqual(cxschema.ArtifactMetadataEntryType.WARN); expect(asg.node.metadataEntry[0].data).toEqual('iops will be ignored without volumeType: EbsDeviceVolumeType.IO1'); - - }); test('warning if iops and volumeType !== IO1', () => { @@ -986,8 +917,6 @@ describe('auto scaling group', () => { // THEN expect(asg.node.metadataEntry[0].type).toEqual(cxschema.ArtifactMetadataEntryType.WARN); expect(asg.node.metadataEntry[0].data).toEqual('iops will be ignored without volumeType: EbsDeviceVolumeType.IO1'); - - }); test('step scaling on metric', () => { @@ -1015,15 +944,13 @@ describe('auto scaling group', () => { }); // THEN - expect(stack).toHaveResource('AWS::CloudWatch::Alarm', { + Template.fromStack(stack).hasResourceProperties('AWS::CloudWatch::Alarm', { ComparisonOperator: 'LessThanOrEqualToThreshold', EvaluationPeriods: 1, MetricName: 'Metric', Namespace: 'Test', Period: 300, }); - - }); test('step scaling on MathExpression', () => { @@ -1056,11 +983,11 @@ describe('auto scaling group', () => { }); // THEN - expect(stack).not.toHaveResource('AWS::CloudWatch::Alarm', { + Template.fromStack(stack).hasResourceProperties('AWS::CloudWatch::Alarm', Match.not({ Period: 60, - }); + })); - expect(stack).toHaveResource('AWS::CloudWatch::Alarm', { + Template.fromStack(stack).hasResourceProperties('AWS::CloudWatch::Alarm', { 'ComparisonOperator': 'LessThanOrEqualToThreshold', 'EvaluationPeriods': 1, 'Metrics': [ @@ -1083,8 +1010,6 @@ describe('auto scaling group', () => { ], 'Threshold': 49, }); - - }); test('test GroupMetrics.all(), adds a single MetricsCollection with no Metrics specified', () => { @@ -1100,15 +1025,14 @@ describe('auto scaling group', () => { }); // Then - expect(stack).toHaveResource('AWS::AutoScaling::AutoScalingGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::AutoScalingGroup', { MetricsCollection: [ { Granularity: '1Minute', - Metrics: ABSENT, + Metrics: Match.absent(), }, ], }); - }); test('test can specify a subset of group metrics', () => { @@ -1135,7 +1059,7 @@ describe('auto scaling group', () => { }); // Then - expect(stack).toHaveResource('AWS::AutoScaling::AutoScalingGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::AutoScalingGroup', { MetricsCollection: [ { Granularity: '1Minute', @@ -1146,7 +1070,6 @@ describe('auto scaling group', () => { }, ], }); - }); test('test deduplication of group metrics ', () => { @@ -1165,7 +1088,7 @@ describe('auto scaling group', () => { }); // Then - expect(stack).toHaveResource('AWS::AutoScaling::AutoScalingGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::AutoScalingGroup', { MetricsCollection: [ { Granularity: '1Minute', @@ -1173,7 +1096,6 @@ describe('auto scaling group', () => { }, ], }); - }); test('allow configuring notifications', () => { @@ -1200,7 +1122,7 @@ describe('auto scaling group', () => { }); // THEN - expect(stack).toHaveResource('AWS::AutoScaling::AutoScalingGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::AutoScalingGroup', { NotificationConfigurations: [ { TopicARN: { Ref: 'MyTopic86869434' }, @@ -1216,10 +1138,7 @@ describe('auto scaling group', () => { ], }, ], - }, - ); - - + }); }); testDeprecated('throw if notification and notificationsTopics are both configured', () => { @@ -1240,7 +1159,6 @@ describe('auto scaling group', () => { }], }); }).toThrow('Cannot set \'notificationsTopic\' and \'notifications\', \'notificationsTopic\' is deprecated use \'notifications\' instead'); - }); test('notificationTypes default includes all non test NotificationType', () => { @@ -1262,7 +1180,7 @@ describe('auto scaling group', () => { }); // THEN - expect(stack).toHaveResource('AWS::AutoScaling::AutoScalingGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::AutoScalingGroup', { NotificationConfigurations: [ { TopicARN: { Ref: 'MyTopic86869434' }, @@ -1274,10 +1192,7 @@ describe('auto scaling group', () => { ], }, ], - }, - ); - - + }); }); testDeprecated('setting notificationTopic configures all non test NotificationType', () => { @@ -1295,7 +1210,7 @@ describe('auto scaling group', () => { }); // THEN - expect(stack).toHaveResource('AWS::AutoScaling::AutoScalingGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::AutoScalingGroup', { NotificationConfigurations: [ { TopicARN: { Ref: 'MyTopic86869434' }, @@ -1307,10 +1222,7 @@ describe('auto scaling group', () => { ], }, ], - }, - ); - - + }); }); test('NotificationTypes.ALL includes all non test NotificationType', () => { @@ -1333,11 +1245,9 @@ describe('auto scaling group', () => { // THEN expect(asg.areNewInstancesProtectedFromScaleIn()).toEqual(true); - expect(stack).toHaveResourceLike('AWS::AutoScaling::AutoScalingGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::AutoScalingGroup', { NewInstancesProtectedFromScaleIn: true, }); - - }); test('Can protect new instances from scale-in via setter', () => { @@ -1355,11 +1265,9 @@ describe('auto scaling group', () => { // THEN expect(asg.areNewInstancesProtectedFromScaleIn()).toEqual(true); - expect(stack).toHaveResourceLike('AWS::AutoScaling::AutoScalingGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::AutoScalingGroup', { NewInstancesProtectedFromScaleIn: true, }); - - }); test('requires imdsv2', () => { @@ -1376,7 +1284,7 @@ describe('auto scaling group', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::AutoScaling::LaunchConfiguration', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::LaunchConfiguration', { MetadataOptions: { HttpTokens: 'required', }, @@ -1400,7 +1308,7 @@ describe('auto scaling group', () => { }); // THEN - expect(stack).toHaveResource('AWS::AutoScaling::AutoScalingGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::AutoScalingGroup', { TerminationPolicies: [ 'OldestInstance', 'Default', @@ -1433,7 +1341,7 @@ test('Can set autoScalingGroupName', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::AutoScaling::AutoScalingGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::AutoScalingGroup', { AutoScalingGroupName: 'MyAsg', }); }); @@ -1470,7 +1378,7 @@ test('can use Vpc imported from unparseable list tokens', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::AutoScaling::AutoScalingGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::AutoScalingGroup', { VPCZoneIdentifier: { 'Fn::Split': [',', { 'Fn::ImportValue': 'myPrivateSubnetIds' }], }, diff --git a/packages/@aws-cdk/aws-autoscaling/test/cfn-init.test.ts b/packages/@aws-cdk/aws-autoscaling/test/cfn-init.test.ts index 2fd252e5e459d..1e34f43260c62 100644 --- a/packages/@aws-cdk/aws-autoscaling/test/cfn-init.test.ts +++ b/packages/@aws-cdk/aws-autoscaling/test/cfn-init.test.ts @@ -1,5 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; -import { anything, arrayWith, expect, haveResourceLike, ResourcePart } from '@aws-cdk/assert-internal'; +import { Match, Template } from '@aws-cdk/assertions'; import * as ec2 from '@aws-cdk/aws-ec2'; import { Duration, Stack } from '@aws-cdk/core'; import * as autoscaling from '../lib'; @@ -37,13 +36,13 @@ test('Signals.waitForAll uses desiredCapacity if available', () => { }); // THEN - expect(stack).to(haveResourceLike('AWS::AutoScaling::AutoScalingGroup', { + Template.fromStack(stack).hasResource('AWS::AutoScaling::AutoScalingGroup', { CreationPolicy: { ResourceSignal: { Count: 5, }, }, - }, ResourcePart.CompleteDefinition)); + }); }); test('Signals.waitForAll uses minCapacity if desiredCapacity is not available', () => { @@ -55,13 +54,13 @@ test('Signals.waitForAll uses minCapacity if desiredCapacity is not available', }); // THEN - expect(stack).to(haveResourceLike('AWS::AutoScaling::AutoScalingGroup', { + Template.fromStack(stack).hasResource('AWS::AutoScaling::AutoScalingGroup', { CreationPolicy: { ResourceSignal: { Count: 2, }, }, - }, ResourcePart.CompleteDefinition)); + }); }); test('Signals.waitForMinCapacity uses minCapacity', () => { @@ -72,13 +71,13 @@ test('Signals.waitForMinCapacity uses minCapacity', () => { }); // THEN - expect(stack).to(haveResourceLike('AWS::AutoScaling::AutoScalingGroup', { + Template.fromStack(stack).hasResource('AWS::AutoScaling::AutoScalingGroup', { CreationPolicy: { ResourceSignal: { Count: 2, }, }, - }, ResourcePart.CompleteDefinition)); + }); }); test('Signals.waitForCount uses given number', () => { @@ -89,13 +88,13 @@ test('Signals.waitForCount uses given number', () => { }); // THEN - expect(stack).to(haveResourceLike('AWS::AutoScaling::AutoScalingGroup', { + Template.fromStack(stack).hasResource('AWS::AutoScaling::AutoScalingGroup', { CreationPolicy: { ResourceSignal: { Count: 10, }, }, - }, ResourcePart.CompleteDefinition)); + }); }); test('When signals are given appropriate IAM policy is added', () => { @@ -106,15 +105,15 @@ test('When signals are given appropriate IAM policy is added', () => { }); // THEN - expect(stack).to(haveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { - Statement: arrayWith({ + Statement: Match.arrayWith([{ Action: 'cloudformation:SignalResource', Effect: 'Allow', Resource: { Ref: 'AWS::StackId' }, - }), + }]), }, - })); + }); }); test('UpdatePolicy.rollingUpdate() still correctly inserts IgnoreUnmodifiedGroupSizeProperties', () => { @@ -125,13 +124,13 @@ test('UpdatePolicy.rollingUpdate() still correctly inserts IgnoreUnmodifiedGroup }); // THEN - expect(stack).to(haveResourceLike('AWS::AutoScaling::AutoScalingGroup', { + Template.fromStack(stack).hasResource('AWS::AutoScaling::AutoScalingGroup', { UpdatePolicy: { AutoScalingScheduledAction: { IgnoreUnmodifiedGroupSizeProperties: true, }, }, - }, ResourcePart.CompleteDefinition)); + }); }); test('UpdatePolicy.rollingUpdate() with Signals uses those defaults', () => { @@ -146,7 +145,7 @@ test('UpdatePolicy.rollingUpdate() with Signals uses those defaults', () => { }); // THEN - expect(stack).to(haveResourceLike('AWS::AutoScaling::AutoScalingGroup', { + Template.fromStack(stack).hasResource('AWS::AutoScaling::AutoScalingGroup', { CreationPolicy: { AutoScalingCreationPolicy: { MinSuccessfulInstancesPercent: 50, @@ -163,7 +162,7 @@ test('UpdatePolicy.rollingUpdate() with Signals uses those defaults', () => { WaitOnResourceSignals: true, }, }, - }, ResourcePart.CompleteDefinition)); + }); }); test('UpdatePolicy.rollingUpdate() without Signals', () => { @@ -174,12 +173,12 @@ test('UpdatePolicy.rollingUpdate() without Signals', () => { }); // THEN - expect(stack).to(haveResourceLike('AWS::AutoScaling::AutoScalingGroup', { + Template.fromStack(stack).hasResource('AWS::AutoScaling::AutoScalingGroup', { UpdatePolicy: { AutoScalingRollingUpdate: { }, }, - }, ResourcePart.CompleteDefinition)); + }); }); test('UpdatePolicy.replacingUpdate() renders correct UpdatePolicy', () => { @@ -190,13 +189,13 @@ test('UpdatePolicy.replacingUpdate() renders correct UpdatePolicy', () => { }); // THEN - expect(stack).to(haveResourceLike('AWS::AutoScaling::AutoScalingGroup', { + Template.fromStack(stack).hasResource('AWS::AutoScaling::AutoScalingGroup', { UpdatePolicy: { AutoScalingReplacingUpdate: { WillReplace: true, }, }, - }, ResourcePart.CompleteDefinition)); + }); }); test('Using init config in ASG leads to default updatepolicy', () => { @@ -210,11 +209,11 @@ test('Using init config in ASG leads to default updatepolicy', () => { }); // THEN - expect(stack).to(haveResourceLike('AWS::AutoScaling::AutoScalingGroup', { + Template.fromStack(stack).hasResource('AWS::AutoScaling::AutoScalingGroup', { UpdatePolicy: { - AutoScalingRollingUpdate: anything(), + AutoScalingRollingUpdate: Match.anyValue(), }, - }, ResourcePart.CompleteDefinition)); + }); }); test('Using init config in ASG leads to correct UserData and permissions', () => { @@ -228,7 +227,7 @@ test('Using init config in ASG leads to correct UserData and permissions', () => }); // THEN - expect(stack).to(haveResourceLike('AWS::AutoScaling::LaunchConfiguration', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::LaunchConfiguration', { UserData: { 'Fn::Base64': { 'Fn::Join': ['', [ @@ -244,14 +243,15 @@ test('Using init config in ASG leads to correct UserData and permissions', () => ]], }, }, - })); - expect(stack).to(haveResourceLike('AWS::IAM::Policy', { + }); + + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { - Statement: arrayWith({ + Statement: Match.arrayWith([{ Action: ['cloudformation:DescribeStackResource', 'cloudformation:SignalResource'], Effect: 'Allow', Resource: { Ref: 'AWS::StackId' }, - }), + }]), }, - })); + }); }); diff --git a/packages/@aws-cdk/aws-autoscaling/test/lifecyclehooks.test.ts b/packages/@aws-cdk/aws-autoscaling/test/lifecyclehooks.test.ts index a1f3717f91042..2cecb8b227107 100644 --- a/packages/@aws-cdk/aws-autoscaling/test/lifecyclehooks.test.ts +++ b/packages/@aws-cdk/aws-autoscaling/test/lifecyclehooks.test.ts @@ -1,5 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; -import { ResourcePart } from '@aws-cdk/assert-internal'; +import { Match, Template } from '@aws-cdk/assertions'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as iam from '@aws-cdk/aws-iam'; import * as cdk from '@aws-cdk/core'; @@ -20,22 +19,22 @@ describe('lifecycle hooks', () => { }); // THEN - expect(stack).toHaveResource('AWS::AutoScaling::LifecycleHook', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::LifecycleHook', { LifecycleTransition: 'autoscaling:EC2_INSTANCE_LAUNCHING', DefaultResult: 'ABANDON', NotificationTargetARN: 'target:arn', }); // Lifecycle Hook has a dependency on the policy object - expect(stack).toHaveResource('AWS::AutoScaling::LifecycleHook', { + Template.fromStack(stack).hasResource('AWS::AutoScaling::LifecycleHook', { DependsOn: [ 'ASGLifecycleHookTransitionRoleDefaultPolicy2E50C7DB', 'ASGLifecycleHookTransitionRole3AAA6BB7', ], - }, ResourcePart.CompleteDefinition); + }); // A default role is provided - expect(stack).toHaveResource('AWS::IAM::Role', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { AssumeRolePolicyDocument: { Version: '2012-10-17', Statement: [ @@ -51,7 +50,7 @@ describe('lifecycle hooks', () => { }); // FakeNotificationTarget.bind() was executed - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Version: '2012-10-17', Statement: [ @@ -78,13 +77,13 @@ test('we can add a lifecycle hook to an ASG with no role and with no notificatio }); // THEN - expect(stack).toHaveResource('AWS::AutoScaling::LifecycleHook', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::LifecycleHook', { LifecycleTransition: 'autoscaling:EC2_INSTANCE_LAUNCHING', DefaultResult: 'ABANDON', }); // A default role is NOT provided - expect(stack).not.toHaveResource('AWS::IAM::Role', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', Match.not({ AssumeRolePolicyDocument: { Version: '2012-10-17', Statement: [ @@ -97,21 +96,10 @@ test('we can add a lifecycle hook to an ASG with no role and with no notificatio }, ], }, - }); + })); // FakeNotificationTarget.bind() was NOT executed - expect(stack).not.toHaveResource('AWS::IAM::Policy', { - PolicyDocument: { - Version: '2012-10-17', - Statement: [ - { - Action: 'action:Work', - Effect: 'Allow', - Resource: '*', - }, - ], - }, - }); + Template.fromStack(stack).resourceCountIs('AWS::IAM::Policy', 0); }); test('we can add a lifecycle hook to an ASG with a role and with a notificationTargetArn', () => { @@ -131,14 +119,14 @@ test('we can add a lifecycle hook to an ASG with a role and with a notificationT }); // THEN - expect(stack).toHaveResource('AWS::AutoScaling::LifecycleHook', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::LifecycleHook', { NotificationTargetARN: 'target:arn', LifecycleTransition: 'autoscaling:EC2_INSTANCE_LAUNCHING', DefaultResult: 'ABANDON', }); // the provided role (myrole), not the default role, is used - expect(stack).toHaveResource('AWS::IAM::Role', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { AssumeRolePolicyDocument: { Version: '2012-10-17', Statement: [ diff --git a/packages/@aws-cdk/aws-autoscaling/test/scaling.test.ts b/packages/@aws-cdk/aws-autoscaling/test/scaling.test.ts index a497efab73135..d427b6afa1ff8 100644 --- a/packages/@aws-cdk/aws-autoscaling/test/scaling.test.ts +++ b/packages/@aws-cdk/aws-autoscaling/test/scaling.test.ts @@ -1,5 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; -import { expect, haveResource, haveResourceLike } from '@aws-cdk/assert-internal'; +import { Template } from '@aws-cdk/assertions'; import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as elbv2 from '@aws-cdk/aws-elasticloadbalancingv2'; @@ -24,15 +23,13 @@ describe('scaling', () => { }); // THEN - expect(stack).to(haveResource('AWS::AutoScaling::ScalingPolicy', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::ScalingPolicy', { PolicyType: 'TargetTrackingScaling', TargetTrackingConfiguration: { PredefinedMetricSpecification: { PredefinedMetricType: 'ASGAverageCPUUtilization' }, TargetValue: 30, }, - })); - - + }); }); test('network ingress', () => { @@ -46,15 +43,13 @@ describe('scaling', () => { }); // THEN - expect(stack).to(haveResource('AWS::AutoScaling::ScalingPolicy', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::ScalingPolicy', { PolicyType: 'TargetTrackingScaling', TargetTrackingConfiguration: { PredefinedMetricSpecification: { PredefinedMetricType: 'ASGAverageNetworkIn' }, TargetValue: 100, }, - })); - - + }); }); test('network egress', () => { @@ -68,15 +63,13 @@ describe('scaling', () => { }); // THEN - expect(stack).to(haveResource('AWS::AutoScaling::ScalingPolicy', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::ScalingPolicy', { PolicyType: 'TargetTrackingScaling', TargetTrackingConfiguration: { PredefinedMetricSpecification: { PredefinedMetricType: 'ASGAverageNetworkOut' }, TargetValue: 100, }, - })); - - + }); }); test('request count per second', () => { @@ -103,7 +96,7 @@ describe('scaling', () => { ], }; - expect(stack).to(haveResource('AWS::AutoScaling::ScalingPolicy', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::ScalingPolicy', { PolicyType: 'TargetTrackingScaling', TargetTrackingConfiguration: { TargetValue: 600, @@ -122,9 +115,7 @@ describe('scaling', () => { }, }, }, - })); - - + }); }); test('request count per minute', () => { @@ -151,7 +142,7 @@ describe('scaling', () => { ], }; - expect(stack).to(haveResource('AWS::AutoScaling::ScalingPolicy', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::ScalingPolicy', { PolicyType: 'TargetTrackingScaling', TargetTrackingConfiguration: { TargetValue: 10, @@ -170,9 +161,7 @@ describe('scaling', () => { }, }, }, - })); - - + }); }); test('custom metric', () => { @@ -193,7 +182,7 @@ describe('scaling', () => { }); // THEN - expect(stack).to(haveResource('AWS::AutoScaling::ScalingPolicy', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::ScalingPolicy', { PolicyType: 'TargetTrackingScaling', TargetTrackingConfiguration: { CustomizedMetricSpecification: { @@ -204,9 +193,7 @@ describe('scaling', () => { }, TargetValue: 2, }, - })); - - + }); }); }); @@ -231,7 +218,7 @@ describe('scaling', () => { }); // THEN: scaling in policy - expect(stack).to(haveResource('AWS::AutoScaling::ScalingPolicy', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::ScalingPolicy', { MetricAggregationType: 'Average', PolicyType: 'StepScaling', StepAdjustments: [ @@ -245,17 +232,17 @@ describe('scaling', () => { ScalingAdjustment: -2, }, ], - })); + }); - expect(stack).to(haveResource('AWS::CloudWatch::Alarm', { + Template.fromStack(stack).hasResourceProperties('AWS::CloudWatch::Alarm', { ComparisonOperator: 'GreaterThanOrEqualToThreshold', Threshold: 3, AlarmActions: [{ Ref: 'FixtureASGMetricUpperPolicyC464CAFB' }], AlarmDescription: 'Upper threshold scaling alarm', - })); + }); // THEN: scaling out policy - expect(stack).to(haveResource('AWS::AutoScaling::ScalingPolicy', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::ScalingPolicy', { MetricAggregationType: 'Average', PolicyType: 'StepScaling', StepAdjustments: [ @@ -264,16 +251,14 @@ describe('scaling', () => { ScalingAdjustment: 1, }, ], - })); + }); - expect(stack).to(haveResource('AWS::CloudWatch::Alarm', { + Template.fromStack(stack).hasResourceProperties('AWS::CloudWatch::Alarm', { ComparisonOperator: 'LessThanOrEqualToThreshold', Threshold: 2, AlarmActions: [{ Ref: 'FixtureASGMetricLowerPolicy4A1CDE42' }], AlarmDescription: 'Lower threshold scaling alarm', - })); - - + }); }); }); @@ -293,11 +278,12 @@ test('step scaling from percentile metric', () => { }); // THEN - expect(stack).to(haveResourceLike('AWS::AutoScaling::ScalingPolicy', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::ScalingPolicy', { PolicyType: 'StepScaling', MetricAggregationType: 'Average', - })); - expect(stack).to(haveResource('AWS::CloudWatch::Alarm', { + }); + + Template.fromStack(stack).hasResourceProperties('AWS::CloudWatch::Alarm', { ComparisonOperator: 'GreaterThanOrEqualToThreshold', EvaluationPeriods: 1, AlarmActions: [ @@ -307,7 +293,7 @@ test('step scaling from percentile metric', () => { MetricName: 'Metric', Namespace: 'Test', Threshold: 100, - })); + }); }); test('step scaling with evaluation period configured', () => { @@ -328,18 +314,19 @@ test('step scaling with evaluation period configured', () => { }); // THEN - expect(stack).to(haveResourceLike('AWS::AutoScaling::ScalingPolicy', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::ScalingPolicy', { PolicyType: 'StepScaling', MetricAggregationType: 'Maximum', - })); - expect(stack).to(haveResource('AWS::CloudWatch::Alarm', { + }); + + Template.fromStack(stack).hasResourceProperties('AWS::CloudWatch::Alarm', { ComparisonOperator: 'GreaterThanOrEqualToThreshold', EvaluationPeriods: 10, ExtendedStatistic: 'p99', MetricName: 'Metric', Namespace: 'Test', Threshold: 100, - })); + }); }); class ASGFixture extends Construct { diff --git a/packages/@aws-cdk/aws-autoscaling/test/scheduled-action.test.ts b/packages/@aws-cdk/aws-autoscaling/test/scheduled-action.test.ts index 57789d5ae36fe..8579099ffd6cf 100644 --- a/packages/@aws-cdk/aws-autoscaling/test/scheduled-action.test.ts +++ b/packages/@aws-cdk/aws-autoscaling/test/scheduled-action.test.ts @@ -1,5 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; -import { expect, haveResource, MatchStyle } from '@aws-cdk/assert-internal'; +import { Template } from '@aws-cdk/assertions'; import * as ec2 from '@aws-cdk/aws-ec2'; import { describeDeprecated } from '@aws-cdk/cdk-build-tools'; import * as cdk from '@aws-cdk/core'; @@ -19,12 +18,10 @@ describeDeprecated('scheduled action', () => { }); // THEN - expect(stack).to(haveResource('AWS::AutoScaling::ScheduledAction', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::ScheduledAction', { Recurrence: '0 8 * * *', MinSize: 10, - })); - - + }); }); test('correctly formats date objects', () => { @@ -40,11 +37,9 @@ describeDeprecated('scheduled action', () => { }); // THEN - expect(stack).to(haveResource('AWS::AutoScaling::ScheduledAction', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::ScheduledAction', { StartTime: '2033-09-10T12:00:00Z', - })); - - + }); }); test('have timezone property', () => { @@ -60,12 +55,11 @@ describeDeprecated('scheduled action', () => { }); // THEN - expect(stack).to(haveResource('AWS::AutoScaling::ScheduledAction', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::ScheduledAction', { MinSize: 12, Recurrence: '0 12 * * *', TimeZone: 'Asia/Seoul', - })); - + }); }); test('autoscaling group has recommended updatepolicy for scheduled actions', () => { @@ -80,7 +74,7 @@ describeDeprecated('scheduled action', () => { }); // THEN - expect(stack).toMatch({ + Template.fromStack(stack).templateMatches({ Resources: { ASG46ED3070: { Type: 'AWS::AutoScaling::AutoScalingGroup', @@ -124,9 +118,7 @@ describeDeprecated('scheduled action', () => { Default: '/aws/service/ami-amazon-linux-latest/amzn-ami-hvm-x86_64-gp2', }, }, - }, MatchStyle.SUPERSET); - - + }); }); }); diff --git a/packages/@aws-cdk/aws-cloud9/README.md b/packages/@aws-cdk/aws-cloud9/README.md index 36bdc6d4a3d4c..ba418cc56dd46 100644 --- a/packages/@aws-cdk/aws-cloud9/README.md +++ b/packages/@aws-cdk/aws-cloud9/README.md @@ -23,17 +23,23 @@ This module is part of the [AWS Cloud Development Kit](https://github.com/aws/aws-cdk) project. -AWS Cloud9 is a cloud-based integrated development environment (IDE) that lets you write, run, and debug your code with just a browser. It includes a code editor, debugger, and terminal. Cloud9 comes prepackaged with essential tools for popular programming languages, including JavaScript, Python, PHP, and more, so you don’t need to install files or configure your development machine to start new projects. Since your Cloud9 IDE is cloud-based, you can work on your projects from your office, home, or anywhere using an internet-connected machine. Cloud9 also provides a seamless experience for developing serverless applications enabling you to easily define resources, debug, and switch between local and remote execution of serverless applications. With Cloud9, you can quickly share your development environment with your team, enabling you to pair program and track each other's inputs in real time. +AWS Cloud9 is a cloud-based integrated development environment (IDE) that lets you write, run, and debug your code with just a +browser. It includes a code editor, debugger, and terminal. Cloud9 comes prepackaged with essential tools for popular +programming languages, including JavaScript, Python, PHP, and more, so you don’t need to install files or configure your +development machine to start new projects. Since your Cloud9 IDE is cloud-based, you can work on your projects from your +office, home, or anywhere using an internet-connected machine. Cloud9 also provides a seamless experience for developing +serverless applications enabling you to easily define resources, debug, and switch between local and remote execution of +serverless applications. With Cloud9, you can quickly share your development environment with your team, enabling you to pair +program and track each other's inputs in real time. ## Creating EC2 Environment -EC2 Environments are defined with `Ec2Environment`. To create an EC2 environment in the private subnet, specify `subnetSelection` with private `subnetType`. +EC2 Environments are defined with `Ec2Environment`. To create an EC2 environment in the private subnet, specify +`subnetSelection` with private `subnetType`. ```ts -import * as cloud9 from '@aws-cdk/aws-cloud9'; - // create a cloud9 ec2 environment in a new VPC const vpc = new ec2.Vpc(this, 'VPC', { maxAzs: 3}); new cloud9.Ec2Environment(this, 'Cloud9Env', { vpc }); @@ -42,19 +48,19 @@ new cloud9.Ec2Environment(this, 'Cloud9Env', { vpc }); const defaultVpc = ec2.Vpc.fromLookup(this, 'DefaultVPC', { isDefault: true }); new cloud9.Ec2Environment(this, 'Cloud9Env2', { vpc: defaultVpc, - instanceType: new ec2.InstanceType('t3.large') + instanceType: new ec2.InstanceType('t3.large'), }); // or specify in a different subnetSelection const c9env = new cloud9.Ec2Environment(this, 'Cloud9Env3', { - vpc, - subnetSelection: { - subnetType: ec2.SubnetType.PRIVATE - } + vpc, + subnetSelection: { + subnetType: ec2.SubnetType.PRIVATE, + }, }); // print the Cloud9 IDE URL in the output -new cdk.CfnOutput(this, 'URL', { value: c9env.ideUrl }); +new CfnOutput(this, 'URL', { value: c9env.ideUrl }); ``` ## Cloning Repositories @@ -62,16 +68,19 @@ new cdk.CfnOutput(this, 'URL', { value: c9env.ideUrl }); Use `clonedRepositories` to clone one or multiple AWS Codecommit repositories into the environment: ```ts +import * as codecommit from '@aws-cdk/aws-codecommit'; + // create a codecommit repository to clone into the cloud9 environment const repoNew = new codecommit.Repository(this, 'RepoNew', { repositoryName: 'new-repo', }); // import an existing codecommit repository to clone into the cloud9 environment -const repoExisting = codecommit.Repository.fromRepositoryName(stack, 'RepoExisting', 'existing-repo'); +const repoExisting = codecommit.Repository.fromRepositoryName(this, 'RepoExisting', 'existing-repo'); // create a new Cloud9 environment and clone the two repositories -new cloud9.Ec2Environment(stack, 'C9Env', { +declare const vpc: ec2.Vpc; +new cloud9.Ec2Environment(this, 'C9Env', { vpc, clonedRepositories: [ cloud9.CloneRepository.fromCodeCommit(repoNew, '/src/new-repo'), diff --git a/packages/@aws-cdk/aws-cloud9/package.json b/packages/@aws-cdk/aws-cloud9/package.json index a48706cb7d913..7078ef97f408e 100644 --- a/packages/@aws-cdk/aws-cloud9/package.json +++ b/packages/@aws-cdk/aws-cloud9/package.json @@ -28,7 +28,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", diff --git a/packages/@aws-cdk/aws-cloud9/rosetta/default.ts-fixture b/packages/@aws-cdk/aws-cloud9/rosetta/default.ts-fixture new file mode 100644 index 0000000000000..e2687d0d2c5bf --- /dev/null +++ b/packages/@aws-cdk/aws-cloud9/rosetta/default.ts-fixture @@ -0,0 +1,12 @@ +// Fixture with packages imported, but nothing else +import { CfnOutput, Stack } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import * as cloud9 from '@aws-cdk/aws-cloud9'; +import * as ec2 from '@aws-cdk/aws-ec2'; + +class Fixture extends Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + /// here + } +} diff --git a/packages/@aws-cdk/aws-cloudtrail/lib/cloudtrail.ts b/packages/@aws-cdk/aws-cloudtrail/lib/cloudtrail.ts index 12cf079c5279e..76f6603652dcf 100644 --- a/packages/@aws-cdk/aws-cloudtrail/lib/cloudtrail.ts +++ b/packages/@aws-cdk/aws-cloudtrail/lib/cloudtrail.ts @@ -209,7 +209,7 @@ export class Trail extends Resource { const cloudTrailPrincipal = new iam.ServicePrincipal('cloudtrail.amazonaws.com'); - this.s3bucket = props.bucket || new s3.Bucket(this, 'S3', { encryption: s3.BucketEncryption.UNENCRYPTED }); + this.s3bucket = props.bucket || new s3.Bucket(this, 'S3', { encryption: s3.BucketEncryption.UNENCRYPTED, enforceSSL: true }); this.s3bucket.addToResourcePolicy(new iam.PolicyStatement({ resources: [this.s3bucket.bucketArn], diff --git a/packages/@aws-cdk/aws-cloudtrail/package.json b/packages/@aws-cdk/aws-cloudtrail/package.json index 6fb04cb8051e6..1d28c67dd7c28 100644 --- a/packages/@aws-cdk/aws-cloudtrail/package.json +++ b/packages/@aws-cdk/aws-cloudtrail/package.json @@ -79,7 +79,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@aws-cdk/assert-internal": "0.0.0", + "@aws-cdk/assertions": "0.0.0", "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cdk-integ-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-cloudtrail/test/cloudtrail.test.ts b/packages/@aws-cdk/aws-cloudtrail/test/cloudtrail.test.ts index a53a45b2c97ce..1492a3c1377b1 100644 --- a/packages/@aws-cdk/aws-cloudtrail/test/cloudtrail.test.ts +++ b/packages/@aws-cdk/aws-cloudtrail/test/cloudtrail.test.ts @@ -1,5 +1,4 @@ -import { ABSENT, SynthUtils } from '@aws-cdk/assert-internal'; -import '@aws-cdk/assert-internal/jest'; +import { Match, Template } from '@aws-cdk/assertions'; import * as iam from '@aws-cdk/aws-iam'; import * as kms from '@aws-cdk/aws-kms'; import * as lambda from '@aws-cdk/aws-lambda'; @@ -13,6 +12,36 @@ import { ManagementEventSources, ReadWriteType, Trail } from '../lib'; const ExpectedBucketPolicyProperties = { PolicyDocument: { Statement: [ + { + Action: 's3:*', + Condition: { + Bool: { 'aws:SecureTransport': 'false' }, + }, + Effect: 'Deny', + Principal: { + AWS: '*', + }, + Resource: [ + { + 'Fn::GetAtt': [ + 'MyAmazingCloudTrailS3A580FE27', + 'Arn', + ], + }, + { + 'Fn::Join': [ + '', + [{ + 'Fn::GetAtt': [ + 'MyAmazingCloudTrailS3A580FE27', + 'Arn', + ], + }, + '/*'], + ], + }, + ], + }, { Action: 's3:GetBucketAcl', Effect: 'Allow', @@ -69,11 +98,11 @@ describe('cloudtrail', () => { test('with no properties', () => { const stack = getTestStack(); new Trail(stack, 'MyAmazingCloudTrail'); - expect(stack).toHaveResource('AWS::CloudTrail::Trail'); - expect(stack).toHaveResource('AWS::S3::Bucket'); - expect(stack).toHaveResource('AWS::S3::BucketPolicy', ExpectedBucketPolicyProperties); - expect(stack).not.toHaveResource('AWS::Logs::LogGroup'); - const trail: any = SynthUtils.synthesize(stack).template.Resources.MyAmazingCloudTrail54516E8D; + Template.fromStack(stack).resourceCountIs('AWS::CloudTrail::Trail', 1); + Template.fromStack(stack).resourceCountIs('AWS::S3::Bucket', 1); + Template.fromStack(stack).hasResourceProperties('AWS::S3::BucketPolicy', ExpectedBucketPolicyProperties); + Template.fromStack(stack).resourceCountIs('AWS::Logs::LogGroup', 0); + const trail: any = Template.fromStack(stack).toJSON().Resources.MyAmazingCloudTrail54516E8D; expect(trail.DependsOn).toEqual(['MyAmazingCloudTrailS3Policy39C120B0']); }); @@ -98,10 +127,10 @@ describe('cloudtrail', () => { new Trail(stack, 'Trail', { bucket: Trailbucket }); - expect(stack).toHaveResource('AWS::CloudTrail::Trail'); - expect(stack).toHaveResource('AWS::S3::Bucket'); - expect(stack).toHaveResource('AWS::S3::BucketPolicy'); - expect(stack).not.toHaveResource('AWS::Logs::LogGroup'); + Template.fromStack(stack).resourceCountIs('AWS::CloudTrail::Trail', 1); + Template.fromStack(stack).resourceCountIs('AWS::S3::Bucket', 1); + Template.fromStack(stack).resourceCountIs('AWS::S3::BucketPolicy', 1); + Template.fromStack(stack).resourceCountIs('AWS::Logs::LogGroup', 0); }); test('with sns topic', () => { @@ -111,9 +140,9 @@ describe('cloudtrail', () => { new Trail(stack, 'Trail', { snsTopic: topic }); - expect(stack).toHaveResource('AWS::CloudTrail::Trail'); - expect(stack).not.toHaveResource('AWS::Logs::LogGroup'); - expect(stack).toHaveResource('AWS::SNS::TopicPolicy', { + Template.fromStack(stack).resourceCountIs('AWS::CloudTrail::Trail', 1); + Template.fromStack(stack).resourceCountIs('AWS::Logs::LogGroup', 0); + Template.fromStack(stack).hasResourceProperties('AWS::SNS::TopicPolicy', { PolicyDocument: { Statement: [ { @@ -137,7 +166,7 @@ describe('cloudtrail', () => { // WHEN new Trail(stack, 'Trail', { bucket }); - expect(stack).toHaveResource('AWS::CloudTrail::Trail', { + Template.fromStack(stack).hasResourceProperties('AWS::CloudTrail::Trail', { S3BucketName: 'somebucket', }); }); @@ -149,12 +178,46 @@ describe('cloudtrail', () => { // WHEN new Trail(stack, 'Trail', { s3KeyPrefix: 'someprefix' }); - expect(stack).toHaveResource('AWS::CloudTrail::Trail'); - expect(stack).toHaveResource('AWS::S3::Bucket'); - expect(stack).toHaveResource('AWS::S3::BucketPolicy', { + Template.fromStack(stack).resourceCountIs('AWS::CloudTrail::Trail', 1); + Template.fromStack(stack).resourceCountIs('AWS::S3::Bucket', 1); + Template.fromStack(stack).hasResourceProperties('AWS::S3::BucketPolicy', { Bucket: { Ref: 'TrailS30071F172' }, PolicyDocument: { Statement: [ + { + Action: 's3:*', + Condition: { + Bool: { + 'aws:SecureTransport': 'false', + }, + }, + Effect: 'Deny', + Principal: { + AWS: '*', + }, + Resource: [ + { + 'Fn::GetAtt': [ + 'TrailS30071F172', + 'Arn', + ], + }, + { + 'Fn::Join': [ + '', + [ + { + 'Fn::GetAtt': [ + 'TrailS30071F172', + 'Arn', + ], + }, + '/*', + ], + ], + }, + ], + }, { Action: 's3:GetBucketAcl', Effect: 'Allow', @@ -199,21 +262,21 @@ describe('cloudtrail', () => { trailName: 'UnencryptedTrail', }); - expect(stack).toHaveResource('AWS::CloudTrail::Trail', { + Template.fromStack(stack).hasResourceProperties('AWS::CloudTrail::Trail', { TrailName: 'EncryptionKeyTrail', KMSKeyId: { 'Fn::GetAtt': ['keyFEDD6EC0', 'Arn'], }, }); - expect(stack).toHaveResource('AWS::CloudTrail::Trail', { + Template.fromStack(stack).hasResourceProperties('AWS::CloudTrail::Trail', { TrailName: 'KmsKeyTrail', KMSKeyId: { 'Fn::GetAtt': ['keyFEDD6EC0', 'Arn'], }, }); - expect(stack).toHaveResource('AWS::CloudTrail::Trail', { + Template.fromStack(stack).hasResourceProperties('AWS::CloudTrail::Trail', { TrailName: 'UnencryptedTrail', - KMSKeyId: ABSENT, + KMSKeyId: Match.absent(), }); }); @@ -235,13 +298,13 @@ describe('cloudtrail', () => { sendToCloudWatchLogs: true, }); - expect(stack).toHaveResource('AWS::CloudTrail::Trail'); - expect(stack).toHaveResource('AWS::S3::Bucket'); - expect(stack).toHaveResource('AWS::S3::BucketPolicy', ExpectedBucketPolicyProperties); - expect(stack).toHaveResource('AWS::Logs::LogGroup'); - expect(stack).toHaveResource('AWS::IAM::Role'); - expect(stack).toHaveResource('AWS::Logs::LogGroup', { RetentionInDays: 365 }); - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).resourceCountIs('AWS::CloudTrail::Trail', 1); + Template.fromStack(stack).resourceCountIs('AWS::S3::Bucket', 1); + Template.fromStack(stack).hasResourceProperties('AWS::S3::BucketPolicy', ExpectedBucketPolicyProperties); + Template.fromStack(stack).resourceCountIs('AWS::Logs::LogGroup', 1); + Template.fromStack(stack).resourceCountIs('AWS::IAM::Role', 1); + Template.fromStack(stack).hasResourceProperties('AWS::Logs::LogGroup', { RetentionInDays: 365 }); + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Version: '2012-10-17', Statement: [{ @@ -255,7 +318,7 @@ describe('cloudtrail', () => { PolicyName: logsRolePolicyName, Roles: [{ Ref: 'MyAmazingCloudTrailLogsRoleF2CCF977' }], }); - const trail: any = SynthUtils.synthesize(stack).template.Resources.MyAmazingCloudTrail54516E8D; + const trail: any = Template.fromStack(stack).toJSON().Resources.MyAmazingCloudTrail54516E8D; expect(trail.DependsOn).toEqual([logsRolePolicyName, logsRoleName, 'MyAmazingCloudTrailS3Policy39C120B0']); }); @@ -266,15 +329,15 @@ describe('cloudtrail', () => { cloudWatchLogsRetention: RetentionDays.ONE_WEEK, }); - expect(stack).toHaveResource('AWS::CloudTrail::Trail'); - expect(stack).toHaveResource('AWS::S3::Bucket'); - expect(stack).toHaveResource('AWS::S3::BucketPolicy', ExpectedBucketPolicyProperties); - expect(stack).toHaveResource('AWS::Logs::LogGroup'); - expect(stack).toHaveResource('AWS::IAM::Role'); - expect(stack).toHaveResource('AWS::Logs::LogGroup', { + Template.fromStack(stack).resourceCountIs('AWS::CloudTrail::Trail', 1); + Template.fromStack(stack).resourceCountIs('AWS::S3::Bucket', 1); + Template.fromStack(stack).hasResourceProperties('AWS::S3::BucketPolicy', ExpectedBucketPolicyProperties); + Template.fromStack(stack).resourceCountIs('AWS::Logs::LogGroup', 1); + Template.fromStack(stack).resourceCountIs('AWS::IAM::Role', 1); + Template.fromStack(stack).hasResourceProperties('AWS::Logs::LogGroup', { RetentionInDays: 7, }); - const trail: any = SynthUtils.synthesize(stack).template.Resources.MyAmazingCloudTrail54516E8D; + const trail: any = Template.fromStack(stack).toJSON().Resources.MyAmazingCloudTrail54516E8D; expect(trail.DependsOn).toEqual([logsRolePolicyName, logsRoleName, 'MyAmazingCloudTrailS3Policy39C120B0']); }); @@ -289,19 +352,19 @@ describe('cloudtrail', () => { cloudWatchLogGroup, }); - expect(stack).toHaveResource('AWS::Logs::LogGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::Logs::LogGroup', { RetentionInDays: 5, }); - expect(stack).toHaveResource('AWS::CloudTrail::Trail', { + Template.fromStack(stack).hasResourceProperties('AWS::CloudTrail::Trail', { CloudWatchLogsLogGroupArn: stack.resolve(cloudWatchLogGroup.logGroupArn), }); - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { - Statement: [{ + Statement: [Match.objectLike({ Resource: stack.resolve(cloudWatchLogGroup.logGroupArn), - }], + })], }, }); }); @@ -313,7 +376,7 @@ describe('cloudtrail', () => { cloudWatchLogsRetention: RetentionDays.ONE_WEEK, }); expect(t.logGroup).toBeUndefined(); - expect(stack).not.toHaveResource('AWS::Logs::LogGroup'); + Template.fromStack(stack).resourceCountIs('AWS::Logs::LogGroup', 0); }); }); @@ -324,7 +387,7 @@ describe('cloudtrail', () => { const cloudTrail = new Trail(stack, 'MyAmazingCloudTrail'); cloudTrail.logAllS3DataEvents(); - expect(stack).toHaveResourceLike('AWS::CloudTrail::Trail', { + Template.fromStack(stack).hasResourceProperties('AWS::CloudTrail::Trail', { EventSelectors: [ { DataResources: [{ @@ -344,8 +407,8 @@ describe('cloudtrail', () => { }, ], }], - IncludeManagementEvents: ABSENT, - ReadWriteType: ABSENT, + IncludeManagementEvents: Match.absent(), + ReadWriteType: Match.absent(), }, ], }); @@ -362,7 +425,7 @@ describe('cloudtrail', () => { objectPrefix: 'prefix-1/prefix-2', }]); - expect(stack).toHaveResourceLike('AWS::CloudTrail::Trail', { + Template.fromStack(stack).hasResourceProperties('AWS::CloudTrail::Trail', { EventSelectors: [ { DataResources: [{ @@ -400,7 +463,7 @@ describe('cloudtrail', () => { const stack = getTestStack(); const cloudTrail = new Trail(stack, 'MyAmazingCloudTrail'); cloudTrail.addS3EventSelector([]); - expect(stack).toHaveResourceLike('AWS::CloudTrail::Trail', { + Template.fromStack(stack).hasResourceProperties('AWS::CloudTrail::Trail', { EventSelectors: [], }); }); @@ -411,7 +474,7 @@ describe('cloudtrail', () => { const cloudTrail = new Trail(stack, 'MyAmazingCloudTrail'); cloudTrail.logAllS3DataEvents({ includeManagementEvents: false, readWriteType: ReadWriteType.READ_ONLY }); - expect(stack).toHaveResourceLike('AWS::CloudTrail::Trail', { + Template.fromStack(stack).hasResourceProperties('AWS::CloudTrail::Trail', { EventSelectors: [ { DataResources: [{ @@ -443,7 +506,7 @@ describe('cloudtrail', () => { new Trail(stack, 'MyAmazingCloudTrail', { managementEvents: ReadWriteType.WRITE_ONLY }); - expect(stack).toHaveResourceLike('AWS::CloudTrail::Trail', { + Template.fromStack(stack).hasResourceProperties('AWS::CloudTrail::Trail', { EventSelectors: [ { IncludeManagementEvents: true, @@ -467,7 +530,7 @@ describe('cloudtrail', () => { excludeManagementEventSources: [], }); - expect(stack).toHaveResourceLike('AWS::CloudTrail::Trail', { + Template.fromStack(stack).hasResourceProperties('AWS::CloudTrail::Trail', { EventSelectors: [ { DataResources: [{ @@ -517,7 +580,7 @@ describe('cloudtrail', () => { const cloudTrail = new Trail(stack, 'MyAmazingCloudTrail'); cloudTrail.addLambdaEventSelector([lambdaFunction]); - expect(stack).toHaveResourceLike('AWS::CloudTrail::Trail', { + Template.fromStack(stack).hasResourceProperties('AWS::CloudTrail::Trail', { EventSelectors: [ { DataResources: [{ @@ -537,7 +600,7 @@ describe('cloudtrail', () => { const cloudTrail = new Trail(stack, 'MyAmazingCloudTrail'); cloudTrail.logAllLambdaDataEvents(); - expect(stack).toHaveResourceLike('AWS::CloudTrail::Trail', { + Template.fromStack(stack).hasResourceProperties('AWS::CloudTrail::Trail', { EventSelectors: [ { DataResources: [{ @@ -569,7 +632,7 @@ describe('cloudtrail', () => { managementEvents: ReadWriteType.NONE, }); - expect(stack).toHaveResourceLike('AWS::CloudTrail::Trail', { + Template.fromStack(stack).hasResourceProperties('AWS::CloudTrail::Trail', { EventSelectors: [ { IncludeManagementEvents: false, @@ -596,7 +659,7 @@ describe('cloudtrail', () => { }); // THEN - expect(stack).toHaveResource('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { EventPattern: { 'detail-type': [ 'AWS API Call via CloudTrail', @@ -612,4 +675,4 @@ describe('cloudtrail', () => { }); }); }); -}); \ No newline at end of file +}); diff --git a/packages/@aws-cdk/aws-cloudtrail/test/integ.cloudtrail.lit.expected.json b/packages/@aws-cdk/aws-cloudtrail/test/integ.cloudtrail.lit.expected.json index 5b90761ade1ff..90c9dd9724771 100644 --- a/packages/@aws-cdk/aws-cloudtrail/test/integ.cloudtrail.lit.expected.json +++ b/packages/@aws-cdk/aws-cloudtrail/test/integ.cloudtrail.lit.expected.json @@ -97,6 +97,40 @@ }, "PolicyDocument": { "Statement": [ + { + "Action": "s3:*", + "Condition": { + "Bool": { + "aws:SecureTransport": "false" + } + }, + "Effect": "Deny", + "Principal": { + "AWS": "*" + }, + "Resource": [ + { + "Fn::GetAtt": [ + "TrailS30071F172", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "TrailS30071F172", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + }, { "Action": "s3:GetBucketAcl", "Effect": "Allow", diff --git a/packages/@aws-cdk/aws-codebuild/package.json b/packages/@aws-cdk/aws-codebuild/package.json index 6bde99b6e2c63..0f16ed23d8b1d 100644 --- a/packages/@aws-cdk/aws-codebuild/package.json +++ b/packages/@aws-cdk/aws-codebuild/package.json @@ -83,7 +83,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@aws-cdk/assert-internal": "0.0.0", + "@aws-cdk/assertions": "0.0.0", "@aws-cdk/aws-sns": "0.0.0", "@aws-cdk/aws-sqs": "0.0.0", "@aws-cdk/cdk-build-tools": "0.0.0", diff --git a/packages/@aws-cdk/aws-codebuild/test/codebuild.test.ts b/packages/@aws-cdk/aws-codebuild/test/codebuild.test.ts index 1cc66df6b236f..5a058625649c3 100644 --- a/packages/@aws-cdk/aws-codebuild/test/codebuild.test.ts +++ b/packages/@aws-cdk/aws-codebuild/test/codebuild.test.ts @@ -1,5 +1,4 @@ -import { ABSENT, SynthUtils } from '@aws-cdk/assert-internal'; -import '@aws-cdk/assert-internal/jest'; +import { Match, Template } from '@aws-cdk/assertions'; import * as codecommit from '@aws-cdk/aws-codecommit'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as kms from '@aws-cdk/aws-kms'; @@ -17,7 +16,7 @@ describe('default properties', () => { new codebuild.PipelineProject(stack, 'MyProject'); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ 'Resources': { 'MyProjectRole9BBE5233': { 'Type': 'AWS::IAM::Role', @@ -177,7 +176,7 @@ describe('default properties', () => { source, }); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ 'Resources': { 'MyRepoF4F48043': { 'Type': 'AWS::CodeCommit::Repository', @@ -361,7 +360,7 @@ describe('default properties', () => { }, }); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ 'Resources': { 'MyBucketF68F3FF0': { 'Type': 'AWS::S3::Bucket', @@ -572,7 +571,7 @@ describe('default properties', () => { }), }); - expect(stack).toHaveResource('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { Source: { Type: 'GITHUB', Location: 'https://github.com/testowner/testrepo.git', @@ -584,7 +583,7 @@ describe('default properties', () => { }, }); - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { Triggers: { Webhook: true, FilterGroups: [ @@ -620,7 +619,7 @@ describe('default properties', () => { }), }); - expect(stack).toHaveResource('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { Source: { Type: 'GITHUB_ENTERPRISE', InsecureSsl: true, @@ -630,7 +629,7 @@ describe('default properties', () => { }, }); - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { Triggers: { Webhook: true, FilterGroups: [ @@ -672,7 +671,7 @@ describe('default properties', () => { }), }); - expect(stack).toHaveResource('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { Source: { Type: 'BITBUCKET', Location: 'https://bitbucket.org/testowner/testrepo.git', @@ -681,7 +680,7 @@ describe('default properties', () => { }, }); - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { Triggers: { Webhook: true, FilterGroups: [ @@ -710,7 +709,7 @@ describe('default properties', () => { }), }); - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { Triggers: { Webhook: true, BuildType: 'BUILD_BATCH', @@ -725,7 +724,7 @@ describe('default properties', () => { }, }); - expect(stack).toHaveResourceLike('AWS::IAM::Role', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { AssumeRolePolicyDocument: { Statement: [ { @@ -740,7 +739,7 @@ describe('default properties', () => { }, }); - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -809,7 +808,7 @@ describe('default properties', () => { securityGroups: [securityGroup], }); - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { 'VpcConfig': { 'SecurityGroupIds': [ { @@ -900,7 +899,7 @@ describe('default properties', () => { new codebuild.PipelineProject(stack, 'MyProject'); // THEN - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { EncryptionKey: 'alias/aws/s3', }); }); @@ -915,7 +914,7 @@ describe('default properties', () => { grantReportGroupPermissions: false, }); - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { 'PolicyDocument': { 'Statement': [ {}, // CloudWatch logs @@ -960,7 +959,7 @@ test('using timeout and path in S3 artifacts sets it correctly', () => { timeout: cdk.Duration.minutes(123), }); - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { 'Artifacts': { 'Path': 'some/path', 'Name': 'some_name', @@ -999,7 +998,7 @@ describe('secondary sources', () => { identifier: 'id', })); - expect(() => SynthUtils.synthesize(stack)).toThrow(/secondary sources/); + expect(() => Template.fromStack(stack)).toThrow(/secondary sources/); }); test('added with an identifer after the Project has been created are rendered in the template', () => { @@ -1018,7 +1017,7 @@ describe('secondary sources', () => { identifier: 'source1', })); - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { 'SecondarySources': [ { 'SourceIdentifier': 'source1', @@ -1047,7 +1046,7 @@ describe('secondary source versions', () => { version: 'someversion', })); - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { 'SecondarySources': [ { 'SourceIdentifier': 'source1', @@ -1079,7 +1078,7 @@ describe('secondary source versions', () => { identifier: 'source1', })); - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { 'SecondarySources': [ { 'SourceIdentifier': 'source1', @@ -1105,7 +1104,7 @@ describe('fileSystemLocations', () => { })], }); - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { 'FileSystemLocations': [ { 'Identifier': 'myidentifier2', @@ -1132,7 +1131,7 @@ describe('fileSystemLocations', () => { mountOptions: 'opts', })); - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { 'FileSystemLocations': [ { 'Identifier': 'myidentifier3', @@ -1177,7 +1176,7 @@ describe('secondary artifacts', () => { identifier: 'id', })); - expect(() => SynthUtils.synthesize(stack)).toThrow(/secondary artifacts/); + expect(() => Template.fromStack(stack)).toThrow(/secondary artifacts/); }); test('added with an identifier after the Project has been created are rendered in the template', () => { @@ -1197,7 +1196,7 @@ describe('secondary artifacts', () => { identifier: 'artifact1', })); - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { 'SecondaryArtifacts': [ { 'ArtifactIdentifier': 'artifact1', @@ -1225,7 +1224,7 @@ describe('secondary artifacts', () => { encryption: false, })); - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { 'SecondaryArtifacts': [ { 'ArtifactIdentifier': 'artifact1', @@ -1243,7 +1242,7 @@ describe('artifacts', () => { new codebuild.PipelineProject(stack, 'MyProject'); - expect(stack).toHaveResource('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { 'Source': { 'Type': 'CODEPIPELINE', }, @@ -1283,9 +1282,9 @@ describe('artifacts', () => { }), }); - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { 'Artifacts': { - 'Name': ABSENT, + 'Name': Match.absent(), 'ArtifactIdentifier': 'artifact1', 'OverrideArtifactName': true, }, @@ -1308,11 +1307,11 @@ describe('artifacts', () => { }), }); - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { 'Artifacts': { 'ArtifactIdentifier': 'artifact1', 'Name': 'specificname', - 'OverrideArtifactName': ABSENT, + 'OverrideArtifactName': Match.absent(), }, }); }); @@ -1334,7 +1333,7 @@ test('events', () => { project.onStateChange('OnStateChange', { target: { bind: () => ({ arn: 'ARN', id: 'ID' }) } }); project.onBuildStarted('OnBuildStarted', { target: { bind: () => ({ arn: 'ARN', id: 'ID' }) } }); - expect(stack).toHaveResource('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { 'EventPattern': { 'source': [ 'aws.codebuild', @@ -1356,7 +1355,7 @@ test('events', () => { 'State': 'ENABLED', }); - expect(stack).toHaveResource('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { 'EventPattern': { 'source': [ 'aws.codebuild', @@ -1378,7 +1377,7 @@ test('events', () => { 'State': 'ENABLED', }); - expect(stack).toHaveResource('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { 'EventPattern': { 'source': [ 'aws.codebuild', @@ -1397,7 +1396,7 @@ test('events', () => { 'State': 'ENABLED', }); - expect(stack).toHaveResource('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { 'EventPattern': { 'source': [ 'aws.codebuild', @@ -1416,7 +1415,7 @@ test('events', () => { 'State': 'ENABLED', }); - expect(stack).toHaveResource('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { 'EventPattern': { 'source': [ 'aws.codebuild', @@ -1455,7 +1454,7 @@ test('environment variables can be overridden at the project level', () => { }, }); - expect(stack).toHaveResource('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { 'Source': { 'Type': 'CODEPIPELINE', }, @@ -1554,7 +1553,7 @@ test('fromCodebuildImage', () => { }, }); - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { 'Environment': { 'Image': 'aws/codebuild/standard:4.0', }, @@ -1571,7 +1570,7 @@ describe('Windows2019 image', () => { }, }); - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { 'Environment': { 'Type': 'WINDOWS_SERVER_2019_CONTAINER', 'ComputeType': 'BUILD_GENERAL1_MEDIUM', @@ -1591,7 +1590,7 @@ describe('ARM image', () => { }, }); - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { 'Environment': { 'Type': 'ARM_CONTAINER', 'ComputeType': 'BUILD_GENERAL1_LARGE', @@ -1608,7 +1607,7 @@ describe('ARM image', () => { }, }); - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { 'Environment': { 'Type': 'ARM_CONTAINER', 'ComputeType': 'BUILD_GENERAL1_SMALL', @@ -1638,7 +1637,7 @@ describe('ARM image', () => { }, }); - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { 'Environment': { 'Type': 'ARM_CONTAINER', 'ComputeType': 'BUILD_GENERAL1_LARGE', @@ -1867,7 +1866,7 @@ test('enableBatchBuilds()', () => { throw new Error('Expecting return value with role'); } - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { BuildBatchConfig: { ServiceRole: { 'Fn::GetAtt': [ @@ -1878,7 +1877,7 @@ test('enableBatchBuilds()', () => { }, }); - expect(stack).toHaveResourceLike('AWS::IAM::Role', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { AssumeRolePolicyDocument: { Statement: [ { @@ -1893,7 +1892,7 @@ test('enableBatchBuilds()', () => { }, }); - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { diff --git a/packages/@aws-cdk/aws-codebuild/test/linux-gpu-build-image.test.ts b/packages/@aws-cdk/aws-codebuild/test/linux-gpu-build-image.test.ts index d3b0d44dde984..106836acdac21 100644 --- a/packages/@aws-cdk/aws-codebuild/test/linux-gpu-build-image.test.ts +++ b/packages/@aws-cdk/aws-codebuild/test/linux-gpu-build-image.test.ts @@ -1,5 +1,4 @@ -import { arrayWith, objectLike } from '@aws-cdk/assert-internal'; -import '@aws-cdk/assert-internal/jest'; +import { Match, Template } from '@aws-cdk/assertions'; import * as ecr from '@aws-cdk/aws-ecr'; import * as cdk from '@aws-cdk/core'; import * as codebuild from '../lib'; @@ -22,7 +21,7 @@ describe('Linux GPU build image', () => { }, }); - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { Environment: { ComputeType: 'BUILD_GENERAL1_LARGE', Image: { @@ -37,9 +36,9 @@ describe('Linux GPU build image', () => { }, }); - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { - Statement: arrayWith(objectLike({ + Statement: Match.arrayWith([Match.objectLike({ Action: [ 'ecr:BatchCheckLayerAvailability', 'ecr:GetDownloadUrlForLayer', @@ -54,7 +53,7 @@ describe('Linux GPU build image', () => { ':123456789012:repository/my-repo', ]], }, - })), + })]), }, }); }); @@ -78,7 +77,7 @@ describe('Linux GPU build image', () => { }, }); - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { Environment: { ComputeType: 'BUILD_GENERAL1_LARGE', Image: { @@ -96,9 +95,9 @@ describe('Linux GPU build image', () => { }, }); - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { - Statement: arrayWith(objectLike({ + Statement: Match.arrayWith([Match.objectLike({ Action: [ 'ecr:BatchCheckLayerAvailability', 'ecr:GetDownloadUrlForLayer', @@ -116,7 +115,7 @@ describe('Linux GPU build image', () => { { Ref: 'myrepo5DFA62E5' }, ]], }, - })), + })]), }, }); }); @@ -138,7 +137,7 @@ describe('Linux GPU build image', () => { }, }); - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { Environment: { ComputeType: 'BUILD_GENERAL1_LARGE', Image: { @@ -154,9 +153,9 @@ describe('Linux GPU build image', () => { }, }); - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { - Statement: arrayWith(objectLike({ + Statement: Match.arrayWith([Match.objectLike({ Action: [ 'ecr:BatchCheckLayerAvailability', 'ecr:GetDownloadUrlForLayer', @@ -173,7 +172,7 @@ describe('Linux GPU build image', () => { ':repository/test-repo', ]], }, - })), + })]), }, }); }); @@ -195,7 +194,7 @@ describe('Linux GPU build image', () => { }, }); - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { Environment: { ComputeType: 'BUILD_GENERAL1_LARGE', Image: { @@ -210,9 +209,9 @@ describe('Linux GPU build image', () => { }, }); - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { - Statement: arrayWith(objectLike({ + Statement: Match.arrayWith([Match.objectLike({ Action: [ 'ecr:BatchCheckLayerAvailability', 'ecr:GetDownloadUrlForLayer', @@ -227,7 +226,7 @@ describe('Linux GPU build image', () => { ':585695036304:repository/foo/bar/foo/fooo', ]], }, - })), + })]), }, }); }); diff --git a/packages/@aws-cdk/aws-codebuild/test/notification-rule.test.ts b/packages/@aws-cdk/aws-codebuild/test/notification-rule.test.ts index 45d6a65d76ce4..e0dc908185427 100644 --- a/packages/@aws-cdk/aws-codebuild/test/notification-rule.test.ts +++ b/packages/@aws-cdk/aws-codebuild/test/notification-rule.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as sns from '@aws-cdk/aws-sns'; import * as cdk from '@aws-cdk/core'; import * as codebuild from '../lib'; @@ -24,7 +24,7 @@ test('notifications rule', () => { project.notifyOnBuildFailed('NotifyOnBuildFailed', target); - expect(stack).toHaveResource('AWS::CodeStarNotifications::NotificationRule', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeStarNotifications::NotificationRule', { Name: 'MyCodebuildProjectNotifyOnBuildSucceeded77719592', DetailType: 'FULL', EventTypeIds: [ @@ -46,7 +46,7 @@ test('notifications rule', () => { ], }); - expect(stack).toHaveResource('AWS::CodeStarNotifications::NotificationRule', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeStarNotifications::NotificationRule', { Name: 'MyCodebuildProjectNotifyOnBuildFailedF680E310', DetailType: 'FULL', EventTypeIds: [ diff --git a/packages/@aws-cdk/aws-codebuild/test/project.test.ts b/packages/@aws-cdk/aws-codebuild/test/project.test.ts index bf93885378ac5..5c7a17d7eb40d 100644 --- a/packages/@aws-cdk/aws-codebuild/test/project.test.ts +++ b/packages/@aws-cdk/aws-codebuild/test/project.test.ts @@ -1,5 +1,4 @@ -import { ABSENT, objectLike, ResourcePart, arrayWith } from '@aws-cdk/assert-internal'; -import '@aws-cdk/assert-internal/jest'; +import { Match, Template } from '@aws-cdk/assertions'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as iam from '@aws-cdk/aws-iam'; import * as logs from '@aws-cdk/aws-logs'; @@ -24,7 +23,7 @@ test('can use filename as buildspec', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { Source: { BuildSpec: 'hello.yml', }, @@ -41,7 +40,7 @@ test('can use buildspec literal', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { Source: { BuildSpec: '{\n "phases": [\n "say hi"\n ]\n}', }, @@ -67,7 +66,7 @@ test('can use yamlbuildspec literal', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { Source: { BuildSpec: 'text: text\ndecimal: 10\nlist:\n - say hi\nobj:\n text: text\n decimal: 10\n list:\n - say hi\n', }, @@ -110,7 +109,7 @@ describe('GitHub source', () => { }); // THEN - expect(stack).toHaveResource('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { Source: { Type: 'GITHUB', Location: 'https://github.com/testowner/testrepo.git', @@ -134,7 +133,7 @@ describe('GitHub source', () => { }); // THEN - expect(stack).toHaveResource('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { SourceVersion: 'testbranch', }); }); @@ -153,7 +152,7 @@ describe('GitHub source', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { Source: { ReportBuildStatus: false, }, @@ -174,7 +173,7 @@ describe('GitHub source', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { Triggers: { Webhook: true, }, @@ -207,7 +206,7 @@ describe('GitHub source', () => { }); // THEN - expect(stack).toHaveResource('AWS::CodeBuild::SourceCredential', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::SourceCredential', { 'ServerType': 'GITHUB', 'AuthType': 'PERSONAL_ACCESS_TOKEN', 'Token': 'my-access-token', @@ -229,7 +228,7 @@ describe('GitHub Enterprise source', () => { }); // THEN - expect(stack).toHaveResource('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { SourceVersion: 'testbranch', }); }); @@ -244,7 +243,7 @@ describe('GitHub Enterprise source', () => { }); // THEN - expect(stack).toHaveResource('AWS::CodeBuild::SourceCredential', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::SourceCredential', { 'ServerType': 'GITHUB_ENTERPRISE', 'AuthType': 'PERSONAL_ACCESS_TOKEN', 'Token': 'my-access-token', @@ -267,7 +266,7 @@ describe('BitBucket source', () => { }); // THEN - expect(stack).toHaveResource('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { SourceVersion: 'testbranch', }); }); @@ -283,7 +282,7 @@ describe('BitBucket source', () => { }); // THEN - expect(stack).toHaveResource('AWS::CodeBuild::SourceCredential', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::SourceCredential', { 'ServerType': 'BITBUCKET', 'AuthType': 'BASIC_AUTH', 'Username': 'my-username', @@ -303,10 +302,10 @@ describe('caching', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { Cache: { Type: 'NO_CACHE', - Location: ABSENT, + Location: Match.absent(), }, }); }); @@ -327,7 +326,7 @@ describe('caching', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { Cache: { Type: 'S3', Location: { @@ -361,7 +360,7 @@ describe('caching', () => { }); // THEN - expect(stack).toHaveResource('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { SourceVersion: 's3version', }); }); @@ -381,7 +380,7 @@ describe('caching', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { Cache: { Type: 'LOCAL', Modes: [ @@ -406,10 +405,10 @@ describe('caching', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { Cache: { Type: 'NO_CACHE', - Location: ABSENT, + Location: Match.absent(), }, }); }); @@ -434,18 +433,18 @@ test('if a role is shared between projects in a VPC, the VPC Policy is only atta // - 1 is for `ec2:CreateNetworkInterfacePermission`, deduplicated as they're part of a single policy // - 1 is for `ec2:CreateNetworkInterface`, this is the separate Policy we're deduplicating // We would have found 3 if the deduplication didn't work. - expect(stack).toCountResources('AWS::IAM::Policy', 2); + Template.fromStack(stack).resourceCountIs('AWS::IAM::Policy', 2); // THEN - both Projects have a DependsOn on the same policy - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResource('AWS::CodeBuild::Project', { Properties: { Name: 'P1' }, DependsOn: ['Project1PolicyDocumentF9761562'], - }, ResourcePart.CompleteDefinition); + }); - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResource('AWS::CodeBuild::Project', { Properties: { Name: 'P1' }, DependsOn: ['Project1PolicyDocumentF9761562'], - }, ResourcePart.CompleteDefinition); + }); }); test('can use an imported Role for a Project within a VPC', () => { @@ -462,7 +461,7 @@ test('can use an imported Role for a Project within a VPC', () => { vpc, }); - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { // no need to do any assertions }); }); @@ -484,15 +483,15 @@ test('can use an imported Role with mutable = false for a Project within a VPC', vpc, }); - expect(stack).toCountResources('AWS::IAM::Policy', 0); + Template.fromStack(stack).resourceCountIs('AWS::IAM::Policy', 0); // Check that the CodeBuild project does not have a DependsOn - expect(stack).toHaveResource('AWS::CodeBuild::Project', (res: any) => { + Template.fromStack(stack).hasResource('AWS::CodeBuild::Project', (res: any) => { if (res.DependsOn && res.DependsOn.length > 0) { throw new Error(`CodeBuild project should have no DependsOn, but got: ${JSON.stringify(res, undefined, 2)}`); } return true; - }, ResourcePart.CompleteDefinition); + }); }); test('can use an ImmutableRole for a Project within a VPC', () => { @@ -512,15 +511,15 @@ test('can use an ImmutableRole for a Project within a VPC', () => { vpc, }); - expect(stack).toCountResources('AWS::IAM::Policy', 0); + Template.fromStack(stack).resourceCountIs('AWS::IAM::Policy', 0); // Check that the CodeBuild project does not have a DependsOn - expect(stack).toHaveResource('AWS::CodeBuild::Project', (res: any) => { + Template.fromStack(stack).hasResource('AWS::CodeBuild::Project', (res: any) => { if (res.DependsOn && res.DependsOn.length > 0) { throw new Error(`CodeBuild project should have no DependsOn, but got: ${JSON.stringify(res, undefined, 2)}`); } return true; - }, ResourcePart.CompleteDefinition); + }); }); test('metric method generates a valid CloudWatch metric', () => { @@ -557,7 +556,7 @@ describe('CodeBuild test reports group', () => { }); reportGroup.grantWrite(project); - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { 'PolicyDocument': { 'Statement': [ {}, @@ -598,8 +597,8 @@ describe('Environment', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { - Environment: objectLike({ + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { + Environment: Match.objectLike({ RegistryCredential: { CredentialProvider: 'SECRETS_MANAGER', Credential: { 'Ref': 'SecretA720EF05' }, @@ -625,8 +624,8 @@ describe('Environment', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { - Environment: objectLike({ + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { + Environment: Match.objectLike({ RegistryCredential: { CredentialProvider: 'SECRETS_MANAGER', Credential: 'MySecretName', @@ -655,8 +654,8 @@ describe('Environment', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { - LogsConfig: objectLike({ + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { + LogsConfig: Match.objectLike({ CloudWatchLogs: { GroupName: 'MyLogGroupName', Status: 'ENABLED', @@ -684,8 +683,8 @@ describe('Environment', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { - LogsConfig: objectLike({ + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { + LogsConfig: Match.objectLike({ CloudWatchLogs: { Status: 'DISABLED', }, @@ -713,8 +712,8 @@ describe('Environment', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { - LogsConfig: objectLike({ + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { + LogsConfig: Match.objectLike({ S3Logs: { Location: 'mybucketname/my-logs', Status: 'ENABLED', @@ -746,8 +745,8 @@ describe('Environment', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { - LogsConfig: objectLike({ + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { + LogsConfig: Match.objectLike({ CloudWatchLogs: { GroupName: 'MyLogGroupName', Status: 'ENABLED', @@ -780,8 +779,8 @@ describe('Environment', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { - Environment: objectLike({ + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { + Environment: Match.objectLike({ Certificate: { 'Fn::Join': ['', [ 'arn:', @@ -818,8 +817,8 @@ describe('EnvironmentVariables', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { - Environment: objectLike({ + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { + Environment: Match.objectLike({ EnvironmentVariables: [{ Name: 'ENV_VAR1', Type: 'PARAMETER_STORE', @@ -855,9 +854,9 @@ describe('EnvironmentVariables', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { 'PolicyDocument': { - 'Statement': arrayWith(objectLike({ + 'Statement': Match.arrayWith([Match.objectLike({ 'Action': 'ssm:GetParameters', 'Effect': 'Allow', 'Resource': [{ @@ -900,7 +899,7 @@ describe('EnvironmentVariables', () => { ], ], }], - })), + })]), }, }); }); @@ -928,14 +927,14 @@ describe('EnvironmentVariables', () => { }); // THEN - expect(stack).not.toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', Match.not({ 'PolicyDocument': { - 'Statement': arrayWith(objectLike({ + 'Statement': Match.arrayWith([Match.objectLike({ 'Action': 'ssm:GetParameters', 'Effect': 'Allow', - })), + })]), }, - }); + })); }); }); @@ -955,7 +954,7 @@ describe('EnvironmentVariables', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { 'Environment': { 'EnvironmentVariables': [ { @@ -968,9 +967,9 @@ describe('EnvironmentVariables', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { 'PolicyDocument': { - 'Statement': arrayWith({ + 'Statement': Match.arrayWith([{ 'Action': 'secretsmanager:GetSecretValue', 'Effect': 'Allow', 'Resource': { @@ -984,7 +983,7 @@ describe('EnvironmentVariables', () => { ':secret:my-secret-??????', ]], }, - }), + }]), }, }); }); @@ -1004,7 +1003,7 @@ describe('EnvironmentVariables', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { 'Environment': { 'EnvironmentVariables': [ { @@ -1017,9 +1016,9 @@ describe('EnvironmentVariables', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { 'PolicyDocument': { - 'Statement': arrayWith({ + 'Statement': Match.arrayWith([{ 'Action': 'secretsmanager:GetSecretValue', 'Effect': 'Allow', 'Resource': { @@ -1033,7 +1032,7 @@ describe('EnvironmentVariables', () => { ':secret:my-secret-??????', ]], }, - }), + }]), }, }); }); @@ -1053,7 +1052,7 @@ describe('EnvironmentVariables', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { 'Environment': { 'EnvironmentVariables': [ { @@ -1066,26 +1065,26 @@ describe('EnvironmentVariables', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { 'PolicyDocument': { - 'Statement': arrayWith({ + 'Statement': Match.arrayWith([{ 'Action': 'secretsmanager:GetSecretValue', 'Effect': 'Allow', 'Resource': 'arn:aws:secretsmanager:us-west-2:123456789012:secret:my-secret-123456*', - }), + }]), }, }); // THEN - expect(stack).not.toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', Match.not({ 'PolicyDocument': { - 'Statement': arrayWith({ + 'Statement': Match.arrayWith([{ 'Action': 'kms:Decrypt', 'Effect': 'Allow', 'Resource': 'arn:aws:kms:us-west-2:123456789012:key/*', - }), + }]), }, - }); + })); }); test('can be provided as a verbatim partial secret ARN', () => { @@ -1103,7 +1102,7 @@ describe('EnvironmentVariables', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { 'Environment': { 'EnvironmentVariables': [ { @@ -1116,26 +1115,26 @@ describe('EnvironmentVariables', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { 'PolicyDocument': { - 'Statement': arrayWith({ + 'Statement': Match.arrayWith([{ 'Action': 'secretsmanager:GetSecretValue', 'Effect': 'Allow', 'Resource': 'arn:aws:secretsmanager:us-west-2:123456789012:secret:mysecret*', - }), + }]), }, }); // THEN - expect(stack).not.toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', Match.not({ 'PolicyDocument': { - 'Statement': arrayWith({ + 'Statement': Match.arrayWith([{ 'Action': 'kms:Decrypt', 'Effect': 'Allow', 'Resource': 'arn:aws:kms:us-west-2:123456789012:key/*', - }), + }]), }, - }); + })); }); test("when provided as a verbatim partial secret ARN from another account, adds permission to decrypt keys in the Secret's account", () => { @@ -1156,13 +1155,13 @@ describe('EnvironmentVariables', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { 'PolicyDocument': { - 'Statement': arrayWith({ + 'Statement': Match.arrayWith([{ 'Action': 'kms:Decrypt', 'Effect': 'Allow', 'Resource': 'arn:aws:kms:us-west-2:901234567890:key/*', - }), + }]), }, }); }); @@ -1189,13 +1188,13 @@ describe('EnvironmentVariables', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { 'PolicyDocument': { - 'Statement': arrayWith({ + 'Statement': Match.arrayWith([{ 'Action': 'kms:Decrypt', 'Effect': 'Allow', 'Resource': 'arn:aws:kms:us-west-2:901234567890:key/*', - }), + }]), }, }); }); @@ -1216,7 +1215,7 @@ describe('EnvironmentVariables', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { 'Environment': { 'EnvironmentVariables': [ { @@ -1229,13 +1228,13 @@ describe('EnvironmentVariables', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { 'PolicyDocument': { - 'Statement': arrayWith({ + 'Statement': Match.arrayWith([{ 'Action': 'secretsmanager:GetSecretValue', 'Effect': 'Allow', 'Resource': { 'Ref': 'SecretA720EF05' }, - }), + }]), }, }); }); @@ -1260,7 +1259,7 @@ describe('EnvironmentVariables', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { 'Environment': { 'EnvironmentVariables': [ { @@ -1278,13 +1277,13 @@ describe('EnvironmentVariables', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { 'PolicyDocument': { - 'Statement': arrayWith({ + 'Statement': Match.arrayWith([{ 'Action': 'secretsmanager:GetSecretValue', 'Effect': 'Allow', 'Resource': { 'Ref': 'SecretA720EF05' }, - }), + }]), }, }); }); @@ -1305,7 +1304,7 @@ describe('EnvironmentVariables', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { 'Environment': { 'EnvironmentVariables': [ { @@ -1323,13 +1322,13 @@ describe('EnvironmentVariables', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { 'PolicyDocument': { - 'Statement': arrayWith({ + 'Statement': Match.arrayWith([{ 'Action': 'secretsmanager:GetSecretValue', 'Effect': 'Allow', 'Resource': { 'Ref': 'SecretA720EF05' }, - }), + }]), }, }); }); @@ -1350,7 +1349,7 @@ describe('EnvironmentVariables', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { 'Environment': { 'EnvironmentVariables': [ { @@ -1363,9 +1362,9 @@ describe('EnvironmentVariables', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { 'PolicyDocument': { - 'Statement': arrayWith({ + 'Statement': Match.arrayWith([{ 'Action': 'secretsmanager:GetSecretValue', 'Effect': 'Allow', 'Resource': { @@ -1379,7 +1378,7 @@ describe('EnvironmentVariables', () => { ':secret:mysecret-??????', ]], }, - }), + }]), }, }); }); @@ -1401,7 +1400,7 @@ describe('EnvironmentVariables', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { 'Environment': { 'EnvironmentVariables': [ { @@ -1414,13 +1413,13 @@ describe('EnvironmentVariables', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { 'PolicyDocument': { - 'Statement': arrayWith({ + 'Statement': Match.arrayWith([{ 'Action': 'secretsmanager:GetSecretValue', 'Effect': 'Allow', 'Resource': 'arn:aws:secretsmanager:us-west-2:123456789012:secret:mysecret*', - }), + }]), }, }); }); @@ -1442,7 +1441,7 @@ describe('EnvironmentVariables', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { 'Environment': { 'EnvironmentVariables': [ { @@ -1455,13 +1454,13 @@ describe('EnvironmentVariables', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { 'PolicyDocument': { - 'Statement': arrayWith({ + 'Statement': Match.arrayWith([{ 'Action': 'secretsmanager:GetSecretValue', 'Effect': 'Allow', 'Resource': 'arn:aws:secretsmanager:us-west-2:123456789012:secret:mysecret-123456*', - }), + }]), }, }); }); @@ -1488,7 +1487,7 @@ describe('EnvironmentVariables', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { 'Environment': { 'EnvironmentVariables': [ { @@ -1508,9 +1507,9 @@ describe('EnvironmentVariables', () => { }, }); - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { 'PolicyDocument': { - 'Statement': arrayWith({ + 'Statement': Match.arrayWith([{ 'Action': 'secretsmanager:GetSecretValue', 'Effect': 'Allow', 'Resource': { @@ -1522,13 +1521,13 @@ describe('EnvironmentVariables', () => { ':012345678912:secret:secret-name-??????', ]], }, - }), + }]), }, }); - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { 'PolicyDocument': { - 'Statement': arrayWith({ + 'Statement': Match.arrayWith([{ 'Action': 'kms:Decrypt', 'Effect': 'Allow', 'Resource': { @@ -1540,7 +1539,7 @@ describe('EnvironmentVariables', () => { ':012345678912:key/*', ]], }, - }), + }]), }, }); }); @@ -1567,7 +1566,7 @@ describe('EnvironmentVariables', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { 'Environment': { 'EnvironmentVariables': [ { @@ -1587,9 +1586,9 @@ describe('EnvironmentVariables', () => { }, }); - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { 'PolicyDocument': { - 'Statement': arrayWith({ + 'Statement': Match.arrayWith([{ 'Action': 'secretsmanager:GetSecretValue', 'Effect': 'Allow', 'Resource': { @@ -1601,13 +1600,13 @@ describe('EnvironmentVariables', () => { ':012345678912:secret:secret-name*', ]], }, - }), + }]), }, }); - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { 'PolicyDocument': { - 'Statement': arrayWith({ + 'Statement': Match.arrayWith([{ 'Action': 'kms:Decrypt', 'Effect': 'Allow', 'Resource': { @@ -1619,7 +1618,7 @@ describe('EnvironmentVariables', () => { ':012345678912:key/*', ]], }, - }), + }]), }, }); }); @@ -1644,7 +1643,7 @@ describe('EnvironmentVariables', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { 'Environment': { 'EnvironmentVariables': [ { @@ -1656,23 +1655,23 @@ describe('EnvironmentVariables', () => { }, }); - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { 'PolicyDocument': { - 'Statement': arrayWith({ + 'Statement': Match.arrayWith([{ 'Action': 'secretsmanager:GetSecretValue', 'Effect': 'Allow', 'Resource': `${secretArn}*`, - }), + }]), }, }); - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { 'PolicyDocument': { - 'Statement': arrayWith({ + 'Statement': Match.arrayWith([{ 'Action': 'kms:Decrypt', 'Effect': 'Allow', 'Resource': 'arn:aws:kms:us-west-2:901234567890:key/*', - }), + }]), }, }); }); @@ -1744,7 +1743,7 @@ describe('Timeouts', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { QueuedTimeoutInMinutes: 30, }); }); @@ -1763,7 +1762,7 @@ describe('Timeouts', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { TimeoutInMinutes: 30, }); }); @@ -1784,7 +1783,7 @@ describe('Maximum concurrency', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::Project', { ConcurrentBuildLimit: 1, }); }); diff --git a/packages/@aws-cdk/aws-codebuild/test/report-group.test.ts b/packages/@aws-cdk/aws-codebuild/test/report-group.test.ts index 4459ef4672f0c..6e653d891f3fb 100644 --- a/packages/@aws-cdk/aws-codebuild/test/report-group.test.ts +++ b/packages/@aws-cdk/aws-codebuild/test/report-group.test.ts @@ -1,5 +1,4 @@ -import { ABSENT, ResourcePart } from '@aws-cdk/assert-internal'; -import '@aws-cdk/assert-internal/jest'; +import { Match, Template } from '@aws-cdk/assertions'; import * as iam from '@aws-cdk/aws-iam'; import * as kms from '@aws-cdk/aws-kms'; import * as s3 from '@aws-cdk/aws-s3'; @@ -15,11 +14,11 @@ describe('Test Reports Groups', () => { new codebuild.ReportGroup(stack, 'ReportGroup'); - expect(stack).toHaveResourceLike('AWS::CodeBuild::ReportGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::ReportGroup', { "Type": "TEST", "ExportConfig": { "ExportConfigType": "NO_EXPORT", - "S3Destination": ABSENT, + "S3Destination": Match.absent(), }, }); }); @@ -31,7 +30,7 @@ describe('Test Reports Groups', () => { reportGroupName: 'my-report-group', }); - expect(stack).toHaveResourceLike('AWS::CodeBuild::ReportGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::ReportGroup', { "Name": 'my-report-group', }); }); @@ -51,7 +50,7 @@ describe('Test Reports Groups', () => { })); expect(reportGroup.reportGroupName).toEqual('my-report-group'); - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { "PolicyDocument": { "Statement": [ { @@ -80,15 +79,15 @@ describe('Test Reports Groups', () => { exportBucket: s3.Bucket.fromBucketName(stack, 'Bucket', 'my-bucket'), }); - expect(stack).toHaveResourceLike('AWS::CodeBuild::ReportGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::ReportGroup', { "Type": "TEST", "ExportConfig": { "ExportConfigType": "S3", "S3Destination": { "Bucket": "my-bucket", - "EncryptionKey": ABSENT, - "EncryptionDisabled": ABSENT, - "Packaging": ABSENT, + "EncryptionKey": Match.absent(), + "EncryptionDisabled": Match.absent(), + "Packaging": Match.absent(), }, }, }); @@ -106,7 +105,7 @@ describe('Test Reports Groups', () => { zipExport: true, }); - expect(stack).toHaveResourceLike('AWS::CodeBuild::ReportGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeBuild::ReportGroup', { "Type": "TEST", "ExportConfig": { "ExportConfigType": "S3", @@ -125,10 +124,10 @@ describe('Test Reports Groups', () => { new codebuild.ReportGroup(stack, 'ReportGroup'); - expect(stack).toHaveResourceLike('AWS::CodeBuild::ReportGroup', { + Template.fromStack(stack).hasResource('AWS::CodeBuild::ReportGroup', { "DeletionPolicy": "Retain", "UpdateReplacePolicy": "Retain", - }, ResourcePart.CompleteDefinition); + }); }); test('can be created with RemovalPolicy.DESTROY', () => { @@ -138,9 +137,9 @@ describe('Test Reports Groups', () => { removalPolicy: cdk.RemovalPolicy.DESTROY, }); - expect(stack).toHaveResourceLike('AWS::CodeBuild::ReportGroup', { + Template.fromStack(stack).hasResource('AWS::CodeBuild::ReportGroup', { "DeletionPolicy": "Delete", "UpdateReplacePolicy": "Delete", - }, ResourcePart.CompleteDefinition); + }); }); }); diff --git a/packages/@aws-cdk/aws-codebuild/test/untrusted-code-boundary.test.ts b/packages/@aws-cdk/aws-codebuild/test/untrusted-code-boundary.test.ts index 89933d11a7ab8..af1f4fdfb328f 100644 --- a/packages/@aws-cdk/aws-codebuild/test/untrusted-code-boundary.test.ts +++ b/packages/@aws-cdk/aws-codebuild/test/untrusted-code-boundary.test.ts @@ -1,5 +1,4 @@ -import { arrayWith } from '@aws-cdk/assert-internal'; -import '@aws-cdk/assert-internal/jest'; +import { Match, Template } from '@aws-cdk/assertions'; import * as iam from '@aws-cdk/aws-iam'; import * as cdk from '@aws-cdk/core'; import * as codebuild from '../lib'; @@ -15,7 +14,7 @@ test('can attach permissions boundary to Project', () => { iam.PermissionsBoundary.of(project).apply(new codebuild.UntrustedCodeBoundaryPolicy(stack, 'Boundary')); // THEN - expect(stack).toHaveResourceLike('AWS::IAM::Role', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { PermissionsBoundary: { Ref: 'BoundaryEA298153' }, }); }); @@ -38,13 +37,13 @@ test('can add additional statements Boundary', () => { })); // THEN - expect(stack).toHaveResourceLike('AWS::IAM::ManagedPolicy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::ManagedPolicy', { PolicyDocument: { - Statement: arrayWith({ + Statement: Match.arrayWith([{ Effect: 'Allow', Action: 'a:a', Resource: 'b', - }), + }]), }, }); -}); \ No newline at end of file +}); diff --git a/packages/@aws-cdk/aws-codedeploy/package.json b/packages/@aws-cdk/aws-codedeploy/package.json index 202cee9c796a3..c251e138255da 100644 --- a/packages/@aws-cdk/aws-codedeploy/package.json +++ b/packages/@aws-cdk/aws-codedeploy/package.json @@ -82,7 +82,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@aws-cdk/assert-internal": "0.0.0", + "@aws-cdk/assertions": "0.0.0", "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cdk-integ-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-codedeploy/test/ecs/application.test.ts b/packages/@aws-cdk/aws-codedeploy/test/ecs/application.test.ts index 6a6966167cd2f..ec130559aaff8 100644 --- a/packages/@aws-cdk/aws-codedeploy/test/ecs/application.test.ts +++ b/packages/@aws-cdk/aws-codedeploy/test/ecs/application.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as cdk from '@aws-cdk/core'; import * as codedeploy from '../../lib'; @@ -7,7 +7,7 @@ describe('CodeDeploy ECS Application', () => { const stack = new cdk.Stack(); new codedeploy.EcsApplication(stack, 'MyApp'); - expect(stack).toHaveResource('AWS::CodeDeploy::Application', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeDeploy::Application', { ComputePlatform: 'ECS', }); }); @@ -18,7 +18,7 @@ describe('CodeDeploy ECS Application', () => { applicationName: 'my-name', }); - expect(stack).toHaveResource('AWS::CodeDeploy::Application', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeDeploy::Application', { ApplicationName: 'my-name', ComputePlatform: 'ECS', }); diff --git a/packages/@aws-cdk/aws-codedeploy/test/lambda/application.test.ts b/packages/@aws-cdk/aws-codedeploy/test/lambda/application.test.ts index eaa140d0c045c..6ccbd816935ba 100644 --- a/packages/@aws-cdk/aws-codedeploy/test/lambda/application.test.ts +++ b/packages/@aws-cdk/aws-codedeploy/test/lambda/application.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as cdk from '@aws-cdk/core'; import * as codedeploy from '../../lib'; @@ -6,7 +6,7 @@ describe('CodeDeploy Lambda Application', () => { test('can be created', () => { const stack = new cdk.Stack(); new codedeploy.LambdaApplication(stack, 'MyApp'); - expect(stack).toHaveResource('AWS::CodeDeploy::Application', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeDeploy::Application', { ComputePlatform: 'Lambda', }); }); @@ -16,7 +16,7 @@ describe('CodeDeploy Lambda Application', () => { new codedeploy.LambdaApplication(stack, 'MyApp', { applicationName: 'my-name', }); - expect(stack).toHaveResource('AWS::CodeDeploy::Application', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeDeploy::Application', { ApplicationName: 'my-name', ComputePlatform: 'Lambda', }); diff --git a/packages/@aws-cdk/aws-codedeploy/test/lambda/custom-deployment-config.test.ts b/packages/@aws-cdk/aws-codedeploy/test/lambda/custom-deployment-config.test.ts index 9f69328613dac..7755402502857 100644 --- a/packages/@aws-cdk/aws-codedeploy/test/lambda/custom-deployment-config.test.ts +++ b/packages/@aws-cdk/aws-codedeploy/test/lambda/custom-deployment-config.test.ts @@ -1,5 +1,4 @@ -import { ResourcePart } from '@aws-cdk/assert-internal'; -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as lambda from '@aws-cdk/aws-lambda'; import * as cdk from '@aws-cdk/core'; import * as codedeploy from '../../lib'; @@ -45,7 +44,7 @@ test('custom resource created', () => { }); // THEN - expect(stack).toHaveResourceLike('Custom::AWS', { + Template.fromStack(stack).hasResourceProperties('Custom::AWS', { ServiceToken: { 'Fn::GetAtt': [ 'AWS679f53fac002430cb0da5b7982bd22872D164C4C', @@ -57,7 +56,7 @@ test('custom resource created', () => { Delete: '{"service":"CodeDeploy","action":"deleteDeploymentConfig","parameters":{"deploymentConfigName":"CustomConfig.LambdaCanary5Percent1Minutes"}}', }); - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -91,7 +90,7 @@ test('custom resource created with specific name', () => { }); // THEN - expect(stack).toHaveResourceLike('Custom::AWS', { + Template.fromStack(stack).hasResourceProperties('Custom::AWS', { Create: '{"service":"CodeDeploy","action":"createDeploymentConfig","parameters":{"deploymentConfigName":"MyDeploymentConfig","computePlatform":"Lambda","trafficRoutingConfig":{"type":"TimeBasedCanary","timeBasedCanary":{"canaryInterval":"1","canaryPercentage":"5"}}},"physicalResourceId":{"id":"MyDeploymentConfig"}}', Update: '{"service":"CodeDeploy","action":"createDeploymentConfig","parameters":{"deploymentConfigName":"MyDeploymentConfig","computePlatform":"Lambda","trafficRoutingConfig":{"type":"TimeBasedCanary","timeBasedCanary":{"canaryInterval":"1","canaryPercentage":"5"}}},"physicalResourceId":{"id":"MyDeploymentConfig"}}', Delete: '{"service":"CodeDeploy","action":"deleteDeploymentConfig","parameters":{"deploymentConfigName":"MyDeploymentConfig"}}', @@ -112,7 +111,7 @@ test('can create linear custom config', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::CodeDeploy::DeploymentGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeDeploy::DeploymentGroup', { DeploymentConfigName: 'CustomConfig.LambdaLinear5PercentEvery1Minutes', }); }); @@ -131,7 +130,7 @@ test('can create canary custom config', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::CodeDeploy::DeploymentGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeDeploy::DeploymentGroup', { DeploymentConfigName: 'CustomConfig.LambdaCanary5Percent1Minutes', }); }); @@ -150,7 +149,7 @@ test('dependency on the config exists to ensure ordering', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::CodeDeploy::DeploymentGroup', { + Template.fromStack(stack).hasResource('AWS::CodeDeploy::DeploymentGroup', { Properties: { DeploymentConfigName: 'CustomConfig.LambdaCanary5Percent1Minutes', }, @@ -158,5 +157,5 @@ test('dependency on the config exists to ensure ordering', () => { 'CustomConfigDeploymentConfigCustomResourcePolicy0426B684', 'CustomConfigDeploymentConfigE9E1F384', ], - }, ResourcePart.CompleteDefinition); + }); }); diff --git a/packages/@aws-cdk/aws-codedeploy/test/lambda/deployment-group.test.ts b/packages/@aws-cdk/aws-codedeploy/test/lambda/deployment-group.test.ts index 1f4b18e852cff..9333b05106220 100644 --- a/packages/@aws-cdk/aws-codedeploy/test/lambda/deployment-group.test.ts +++ b/packages/@aws-cdk/aws-codedeploy/test/lambda/deployment-group.test.ts @@ -1,5 +1,4 @@ -import { ResourcePart } from '@aws-cdk/assert-internal'; -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; import * as iam from '@aws-cdk/aws-iam'; import * as lambda from '@aws-cdk/aws-lambda'; @@ -33,7 +32,7 @@ describe('CodeDeploy Lambda DeploymentGroup', () => { deploymentConfig: codedeploy.LambdaDeploymentConfig.ALL_AT_ONCE, }); - expect(stack).toHaveResource('AWS::CodeDeploy::DeploymentGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeDeploy::DeploymentGroup', { ApplicationName: { Ref: 'MyApp3CE31C26', }, @@ -56,7 +55,7 @@ describe('CodeDeploy Lambda DeploymentGroup', () => { }, }); - expect(stack).toHaveResource('AWS::Lambda::Alias', { + Template.fromStack(stack).hasResource('AWS::Lambda::Alias', { Type: 'AWS::Lambda::Alias', Properties: { FunctionName: { @@ -80,9 +79,9 @@ describe('CodeDeploy Lambda DeploymentGroup', () => { }, }, }, - }, ResourcePart.CompleteDefinition); + }); - expect(stack).toHaveResource('AWS::IAM::Role', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { AssumeRolePolicyDocument: { Statement: [{ Action: 'sts:AssumeRole', @@ -120,7 +119,7 @@ describe('CodeDeploy Lambda DeploymentGroup', () => { deploymentGroupName: 'test', }); - expect(stack).toHaveResourceLike('AWS::CodeDeploy::DeploymentGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeDeploy::DeploymentGroup', { DeploymentGroupName: 'test', }); }); @@ -140,7 +139,7 @@ describe('CodeDeploy Lambda DeploymentGroup', () => { role: serviceRole, }); - expect(stack).toHaveResource('AWS::IAM::Role', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { AssumeRolePolicyDocument: { Statement: [{ Action: 'sts:AssumeRole', @@ -176,7 +175,7 @@ describe('CodeDeploy Lambda DeploymentGroup', () => { deploymentConfig: codedeploy.LambdaDeploymentConfig.LINEAR_10PERCENT_EVERY_1MINUTE, }); - expect(stack).toHaveResource('AWS::CodeDeploy::DeploymentGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeDeploy::DeploymentGroup', { ApplicationName: { Ref: 'MyApp3CE31C26', }, @@ -216,7 +215,7 @@ describe('CodeDeploy Lambda DeploymentGroup', () => { })], }); - expect(stack).toHaveResourceLike('AWS::CodeDeploy::DeploymentGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeDeploy::DeploymentGroup', { AlarmConfiguration: { Alarms: [{ Name: { @@ -268,7 +267,7 @@ describe('CodeDeploy Lambda DeploymentGroup', () => { deploymentConfig: codedeploy.LambdaDeploymentConfig.ALL_AT_ONCE, }); - expect(stack).toHaveResourceLike('AWS::Lambda::Alias', { + Template.fromStack(stack).hasResource('AWS::Lambda::Alias', { UpdatePolicy: { CodeDeployLambdaAliasUpdate: { ApplicationName: { @@ -282,9 +281,9 @@ describe('CodeDeploy Lambda DeploymentGroup', () => { }, }, }, - }, ResourcePart.CompleteDefinition); + }); - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyName: 'MyDGServiceRoleDefaultPolicy65E8E1EA', Roles: [{ Ref: 'MyDGServiceRole5E94FD88', @@ -316,7 +315,7 @@ describe('CodeDeploy Lambda DeploymentGroup', () => { }); group.addPreHook(mockFunction(stack, 'PreHook')); - expect(stack).toHaveResourceLike('AWS::Lambda::Alias', { + Template.fromStack(stack).hasResource('AWS::Lambda::Alias', { UpdatePolicy: { CodeDeployLambdaAliasUpdate: { ApplicationName: { @@ -330,9 +329,9 @@ describe('CodeDeploy Lambda DeploymentGroup', () => { }, }, }, - }, ResourcePart.CompleteDefinition); + }); - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyName: 'MyDGServiceRoleDefaultPolicy65E8E1EA', Roles: [{ Ref: 'MyDGServiceRole5E94FD88', @@ -364,7 +363,7 @@ describe('CodeDeploy Lambda DeploymentGroup', () => { deploymentConfig: codedeploy.LambdaDeploymentConfig.ALL_AT_ONCE, }); - expect(stack).toHaveResourceLike('AWS::Lambda::Alias', { + Template.fromStack(stack).hasResource('AWS::Lambda::Alias', { UpdatePolicy: { CodeDeployLambdaAliasUpdate: { ApplicationName: { @@ -378,9 +377,9 @@ describe('CodeDeploy Lambda DeploymentGroup', () => { }, }, }, - }, ResourcePart.CompleteDefinition); + }); - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyName: 'MyDGServiceRoleDefaultPolicy65E8E1EA', Roles: [{ Ref: 'MyDGServiceRole5E94FD88', @@ -412,7 +411,7 @@ describe('CodeDeploy Lambda DeploymentGroup', () => { }); group.addPostHook(mockFunction(stack, 'PostHook')); - expect(stack).toHaveResourceLike('AWS::Lambda::Alias', { + Template.fromStack(stack).hasResource('AWS::Lambda::Alias', { UpdatePolicy: { CodeDeployLambdaAliasUpdate: { ApplicationName: { @@ -426,9 +425,9 @@ describe('CodeDeploy Lambda DeploymentGroup', () => { }, }, }, - }, ResourcePart.CompleteDefinition); + }); - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyName: 'MyDGServiceRoleDefaultPolicy65E8E1EA', Roles: [{ Ref: 'MyDGServiceRole5E94FD88', @@ -467,7 +466,7 @@ describe('CodeDeploy Lambda DeploymentGroup', () => { })], }); - expect(stack).toHaveResourceLike('AWS::CodeDeploy::DeploymentGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeDeploy::DeploymentGroup', { AlarmConfiguration: { Alarms: [{ Name: { @@ -494,7 +493,7 @@ describe('CodeDeploy Lambda DeploymentGroup', () => { }, }); - expect(stack).toHaveResource('AWS::CodeDeploy::DeploymentGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeDeploy::DeploymentGroup', { ApplicationName: { Ref: 'MyApp3CE31C26', }, @@ -526,7 +525,7 @@ describe('CodeDeploy Lambda DeploymentGroup', () => { }, }); - expect(stack).toHaveResourceLike('AWS::CodeDeploy::DeploymentGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeDeploy::DeploymentGroup', { AutoRollbackConfiguration: { Enabled: true, Events: [ @@ -557,7 +556,7 @@ describe('CodeDeploy Lambda DeploymentGroup', () => { })], }); - expect(stack).toHaveResourceLike('AWS::CodeDeploy::DeploymentGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeDeploy::DeploymentGroup', { AutoRollbackConfiguration: { Enabled: true, Events: [ diff --git a/packages/@aws-cdk/aws-codedeploy/test/server/deployment-config.test.ts b/packages/@aws-cdk/aws-codedeploy/test/server/deployment-config.test.ts index 2838bacfdc7fc..52652e8024b28 100644 --- a/packages/@aws-cdk/aws-codedeploy/test/server/deployment-config.test.ts +++ b/packages/@aws-cdk/aws-codedeploy/test/server/deployment-config.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as cdk from '@aws-cdk/core'; import * as codedeploy from '../../lib'; @@ -12,7 +12,7 @@ describe('CodeDeploy DeploymentConfig', () => { minimumHealthyHosts: codedeploy.MinimumHealthyHosts.count(1), }); - expect(stack).toHaveResource('AWS::CodeDeploy::DeploymentConfig', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeDeploy::DeploymentConfig', { 'MinimumHealthyHosts': { 'Type': 'HOST_COUNT', 'Value': 1, @@ -27,7 +27,7 @@ describe('CodeDeploy DeploymentConfig', () => { minimumHealthyHosts: codedeploy.MinimumHealthyHosts.percentage(75), }); - expect(stack).toHaveResource('AWS::CodeDeploy::DeploymentConfig', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeDeploy::DeploymentConfig', { 'MinimumHealthyHosts': { 'Type': 'FLEET_PERCENT', 'Value': 75, diff --git a/packages/@aws-cdk/aws-codedeploy/test/server/deployment-group.test.ts b/packages/@aws-cdk/aws-codedeploy/test/server/deployment-group.test.ts index 79d20323d5a21..43acaadc3e7fc 100644 --- a/packages/@aws-cdk/aws-codedeploy/test/server/deployment-group.test.ts +++ b/packages/@aws-cdk/aws-codedeploy/test/server/deployment-group.test.ts @@ -1,5 +1,4 @@ -import { SynthUtils } from '@aws-cdk/assert-internal'; -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as autoscaling from '@aws-cdk/aws-autoscaling'; import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; import * as ec2 from '@aws-cdk/aws-ec2'; @@ -18,7 +17,7 @@ describe('CodeDeploy Server Deployment Group', () => { application, }); - expect(stack).toHaveResource('AWS::CodeDeploy::DeploymentGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeDeploy::DeploymentGroup', { 'ApplicationName': { 'Ref': 'MyApp3CE31C26', }, @@ -36,9 +35,8 @@ describe('CodeDeploy Server Deployment Group', () => { value: serverDeploymentGroup.application.applicationName, }); - expect(stack2).toHaveOutput({ - outputName: 'Output', - outputValue: 'defaultmydgapplication78dba0bb0c7580b32033', + Template.fromStack(stack2).hasOutput('Output', { + Value: 'defaultmydgapplication78dba0bb0c7580b32033', }); }); @@ -68,7 +66,7 @@ describe('CodeDeploy Server Deployment Group', () => { installAgent: true, }); - expect(stack).toHaveResource('AWS::AutoScaling::LaunchConfiguration', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::LaunchConfiguration', { 'UserData': { 'Fn::Base64': { 'Fn::Join': [ @@ -104,7 +102,7 @@ describe('CodeDeploy Server Deployment Group', () => { installAgent: true, }); - expect(stack).toHaveResource('AWS::AutoScaling::LaunchConfiguration', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::LaunchConfiguration', { 'UserData': { 'Fn::Base64': { 'Fn::Join': [ @@ -135,7 +133,7 @@ describe('CodeDeploy Server Deployment Group', () => { autoScalingGroups: [asg], }); - expect(stack).toHaveResource('AWS::CodeDeploy::DeploymentGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeDeploy::DeploymentGroup', { 'AutoScalingGroups': [ { 'Ref': 'ASG46ED3070', @@ -156,7 +154,7 @@ describe('CodeDeploy Server Deployment Group', () => { const deploymentGroup = new codedeploy.ServerDeploymentGroup(stack, 'DeploymentGroup'); deploymentGroup.addAutoScalingGroup(asg); - expect(stack).toHaveResource('AWS::CodeDeploy::DeploymentGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeDeploy::DeploymentGroup', { 'AutoScalingGroups': [ { 'Ref': 'ASG46ED3070', @@ -178,7 +176,7 @@ describe('CodeDeploy Server Deployment Group', () => { loadBalancer: codedeploy.LoadBalancer.application(targetGroup), }); - expect(stack).toHaveResource('AWS::CodeDeploy::DeploymentGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeDeploy::DeploymentGroup', { 'LoadBalancerInfo': { 'TargetGroupInfoList': [ { @@ -210,7 +208,7 @@ describe('CodeDeploy Server Deployment Group', () => { loadBalancer: codedeploy.LoadBalancer.network(targetGroup), }); - expect(stack).toHaveResource('AWS::CodeDeploy::DeploymentGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeDeploy::DeploymentGroup', { 'LoadBalancerInfo': { 'TargetGroupInfoList': [ { @@ -241,7 +239,7 @@ describe('CodeDeploy Server Deployment Group', () => { ), }); - expect(stack).toHaveResource('AWS::CodeDeploy::DeploymentGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeDeploy::DeploymentGroup', { 'Ec2TagSet': { 'Ec2TagSetList': [ { @@ -276,7 +274,7 @@ describe('CodeDeploy Server Deployment Group', () => { ), }); - expect(stack).toHaveResource('AWS::CodeDeploy::DeploymentGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeDeploy::DeploymentGroup', { 'OnPremisesTagSet': { 'OnPremisesTagSetList': [ { @@ -343,7 +341,7 @@ describe('CodeDeploy Server Deployment Group', () => { const deploymentGroup = new codedeploy.ServerDeploymentGroup(stack, 'DeploymentGroup'); deploymentGroup.addAlarm(alarm); - expect(stack).toHaveResource('AWS::CodeDeploy::DeploymentGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeDeploy::DeploymentGroup', { 'AlarmConfiguration': { 'Alarms': [ { @@ -362,7 +360,7 @@ describe('CodeDeploy Server Deployment Group', () => { new codedeploy.ServerDeploymentGroup(stack, 'DeploymentGroup'); - expect(stack).toHaveResource('AWS::CodeDeploy::DeploymentGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeDeploy::DeploymentGroup', { 'AutoRollbackConfiguration': { 'Enabled': true, 'Events': [ @@ -391,7 +389,7 @@ describe('CodeDeploy Server Deployment Group', () => { }); deploymentGroup.addAlarm(alarm); - expect(stack).toHaveResource('AWS::CodeDeploy::DeploymentGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeDeploy::DeploymentGroup', { 'AutoRollbackConfiguration': { 'Enabled': true, 'Events': [ @@ -402,7 +400,8 @@ describe('CodeDeploy Server Deployment Group', () => { }); test('setting to roll back on alarms without providing any results in an exception', () => { - const stack = new cdk.Stack(); + const app = new cdk.App(); + const stack = new cdk.Stack(app); new codedeploy.ServerDeploymentGroup(stack, 'DeploymentGroup', { autoRollback: { @@ -410,7 +409,7 @@ describe('CodeDeploy Server Deployment Group', () => { }, }); - expect(() => SynthUtils.toCloudFormation(stack)).toThrow(/deploymentInAlarm/); + expect(() => app.synth()).toThrow(/deploymentInAlarm/); }); test('can be used with an imported ALB Target Group as the load balancer', () => { @@ -424,7 +423,7 @@ describe('CodeDeploy Server Deployment Group', () => { ), }); - expect(stack).toHaveResourceLike('AWS::CodeDeploy::DeploymentGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::CodeDeploy::DeploymentGroup', { 'LoadBalancerInfo': { 'TargetGroupInfoList': [ { diff --git a/packages/@aws-cdk/aws-codeguruprofiler/README.md b/packages/@aws-cdk/aws-codeguruprofiler/README.md index 09a2ea2d6b731..6c8c54954a40e 100644 --- a/packages/@aws-cdk/aws-codeguruprofiler/README.md +++ b/packages/@aws-cdk/aws-codeguruprofiler/README.md @@ -17,7 +17,7 @@ Amazon CodeGuru Profiler collects runtime performance data from your live applic Import to your project: -```ts +```ts nofixture import * as codeguruprofiler from '@aws-cdk/aws-codeguruprofiler'; ``` @@ -27,11 +27,11 @@ Here's how to setup a profiling group and give your compute role permissions to ```ts // The execution role of your application that publishes to the ProfilingGroup via CodeGuru Profiler Profiling Agent. (the following is merely an example) -const publishAppRole = new Role(stack, 'PublishAppRole', { - assumedBy: new AccountRootPrincipal(), +const publishAppRole = new iam.Role(this, 'PublishAppRole', { + assumedBy: new iam.AccountRootPrincipal(), }); -const profilingGroup = new ProfilingGroup(stack, 'MyProfilingGroup'); +const profilingGroup = new codeguruprofiler.ProfilingGroup(this, 'MyProfilingGroup'); profilingGroup.grantPublish(publishAppRole); ``` @@ -41,7 +41,7 @@ Code Guru Profiler supports multiple compute environments. They can be configured when creating a Profiling Group by using the `computePlatform` property: ```ts -const profilingGroup = new ProfilingGroup(stack, 'MyProfilingGroup', { - computePlatform: ComputePlatform.AWS_LAMBDA, +const profilingGroup = new codeguruprofiler.ProfilingGroup(this, 'MyProfilingGroup', { + computePlatform: codeguruprofiler.ComputePlatform.AWS_LAMBDA, }); ``` diff --git a/packages/@aws-cdk/aws-codeguruprofiler/package.json b/packages/@aws-cdk/aws-codeguruprofiler/package.json index 92aaf5b19dbba..e0ca94e657487 100644 --- a/packages/@aws-cdk/aws-codeguruprofiler/package.json +++ b/packages/@aws-cdk/aws-codeguruprofiler/package.json @@ -28,7 +28,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", diff --git a/packages/@aws-cdk/aws-codeguruprofiler/rosetta/default.ts-fixture b/packages/@aws-cdk/aws-codeguruprofiler/rosetta/default.ts-fixture new file mode 100644 index 0000000000000..c809dffd674cb --- /dev/null +++ b/packages/@aws-cdk/aws-codeguruprofiler/rosetta/default.ts-fixture @@ -0,0 +1,12 @@ +// Fixture with packages imported, but nothing else +import { Stack } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import * as codeguruprofiler from '@aws-cdk/aws-codeguruprofiler'; +import * as iam from '@aws-cdk/aws-iam'; + +class Fixture extends Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + /// here + } +} diff --git a/packages/@aws-cdk/aws-codepipeline-actions/README.md b/packages/@aws-cdk/aws-codepipeline-actions/README.md index d3569b4ea5872..ff43850561651 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/README.md +++ b/packages/@aws-cdk/aws-codepipeline-actions/README.md @@ -764,6 +764,39 @@ const deployStage = pipeline.addStage({ [image definition file]: https://docs.aws.amazon.com/codepipeline/latest/userguide/pipelines-create.html#pipelines-create-image-definitions +#### Deploying ECS applications to existing services + +CodePipeline can deploy to an existing ECS service which uses the +[ECS service ARN format that contains the Cluster name](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs-account-settings.html#ecs-resource-ids). +This also works if the service is in a different account and/or region than the pipeline: + +```ts +import * as ecs from '@aws-cdk/aws-ecs'; + +const service = ecs.BaseService.fromServiceArnWithCluster(this, 'EcsService', + 'arn:aws:ecs:us-east-1:123456789012:service/myClusterName/myServiceName' +); +const pipeline = new codepipeline.Pipeline(this, 'MyPipeline'); +const buildOutput = new codepipeline.Artifact(); +// add source and build stages to the pipeline as usual... +const deployStage = pipeline.addStage({ + stageName: 'Deploy', + actions: [ + new codepipeline_actions.EcsDeployAction({ + actionName: 'DeployAction', + service: service, + input: buildOutput, + }), + ], +}); +``` + +When deploying across accounts, especially in a CDK Pipelines self-mutating pipeline, +it is recommended to provide the `role` property to the `EcsDeployAction`. +The Role will need to have permissions assigned to it for ECS deployment. +See [the CodePipeline documentation](https://docs.aws.amazon.com/codepipeline/latest/userguide/how-to-custom-role.html#how-to-update-role-new-services) +for the permissions needed. + #### Deploying ECS applications stored in a separate source code repository The idiomatic CDK way of deploying an ECS application is to have your Dockerfiles and your CDK code in the same source code repository, diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/ecs/ecs-deploy-action.test.ts b/packages/@aws-cdk/aws-codepipeline-actions/test/ecs/ecs-deploy-action.test.ts index 91976ec921e74..0841d47946a23 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/ecs/ecs-deploy-action.test.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/ecs/ecs-deploy-action.test.ts @@ -196,6 +196,84 @@ describe('ecs deploy action', () => { }); + + test('can be created by existing service with cluster ARN format', () => { + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'PipelineStack', { + env: { + region: 'pipeline-region', account: 'pipeline-account', + }, + }); + const clusterName = 'cluster-name'; + const serviceName = 'service-name'; + const region = 'service-region'; + const account = 'service-account'; + const serviceArn = `arn:aws:ecs:${region}:${account}:service/${clusterName}/${serviceName}`; + const service = ecs.BaseService.fromServiceArnWithCluster(stack, 'FargateService', serviceArn); + + const artifact = new codepipeline.Artifact('Artifact'); + const bucket = new s3.Bucket(stack, 'PipelineBucket', { + versioned: true, + removalPolicy: cdk.RemovalPolicy.DESTROY, + }); + const source = new cpactions.S3SourceAction({ + actionName: 'Source', + output: artifact, + bucket, + bucketKey: 'key', + }); + const action = new cpactions.EcsDeployAction({ + actionName: 'ECS', + service: service, + input: artifact, + }); + new codepipeline.Pipeline(stack, 'Pipeline', { + stages: [ + { + stageName: 'Source', + actions: [source], + }, + { + stageName: 'Deploy', + actions: [action], + }, + ], + }); + + Template.fromStack(stack).hasResourceProperties('AWS::CodePipeline::Pipeline', { + Stages: [ + {}, + { + Actions: [ + { + Name: 'ECS', + ActionTypeId: { + Category: 'Deploy', + Provider: 'ECS', + }, + Configuration: { + ClusterName: clusterName, + ServiceName: serviceName, + }, + Region: region, + RoleArn: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + `:iam::${account}:role/pipelinestack-support-serloyecsactionrole49867f847238c85af7c0`, + ], + ], + }, + }, + ], + }, + ], + }); + }); }); }); diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.lambda-pipeline.expected.json b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.lambda-pipeline.expected.json index 53614fd854b19..583aecbc300b4 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.lambda-pipeline.expected.json +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.lambda-pipeline.expected.json @@ -669,6 +669,40 @@ }, "PolicyDocument": { "Statement": [ + { + "Action": "s3:*", + "Condition": { + "Bool": { + "aws:SecureTransport": "false" + } + }, + "Effect": "Deny", + "Principal": { + "AWS": "*" + }, + "Resource": [ + { + "Fn::GetAtt": [ + "CloudTrailS310CD22F2", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "CloudTrailS310CD22F2", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + }, { "Action": "s3:GetBucketAcl", "Effect": "Allow", diff --git a/packages/@aws-cdk/aws-cognito/README.md b/packages/@aws-cdk/aws-cognito/README.md index ef968f5151bd2..96629cf13decd 100644 --- a/packages/@aws-cdk/aws-cognito/README.md +++ b/packages/@aws-cdk/aws-cognito/README.md @@ -91,7 +91,7 @@ new cognito.UserPool(this, 'myuserpool', { emailBody: 'Thanks for signing up to our awesome app! Your verification code is {####}', emailStyle: cognito.VerificationEmailStyle.CODE, smsMessage: 'Thanks for signing up to our awesome app! Your verification code is {####}', - } + }, }); ``` @@ -108,8 +108,8 @@ new cognito.UserPool(this, 'myuserpool', { userInvitation: { emailSubject: 'Invite to join our awesome app!', emailBody: 'Hello {username}, you have been invited to join our awesome app! Your temporary password is {####}', - smsMessage: 'Hello {username}, your temporary password for our awesome app is {####}' - } + smsMessage: 'Hello {username}, your temporary password for our awesome app is {####}', + }, }); ``` @@ -136,7 +136,7 @@ new cognito.UserPool(this, 'myuserpool', { // ... signInAliases: { username: true, - email: true + email: true, }, }); ``` @@ -165,7 +165,7 @@ new cognito.UserPool(this, 'myuserpool', { // ... // ... signInAliases: { username: true, email: true }, - autoVerify: { email: true, phone: true } + autoVerify: { email: true, phone: true }, }); ``` @@ -241,7 +241,7 @@ const poolSmsRole = new iam.Role(this, 'userpoolsmsrole', { new cognito.UserPool(this, 'myuserpool', { // ... smsRole: poolSmsRole, - smsRoleExternalId: 'c87467be-4f34-11ea-b77f-2e728ce88125' + smsRoleExternalId: 'c87467be-4f34-11ea-b77f-2e728ce88125', }); ``` @@ -327,7 +327,7 @@ Cognito to send emails through Amazon SES, which is detailed below. ```ts new cognito.UserPool(this, 'myuserpool', { - email: UserPoolEmail.withCognito('support@myawesomeapp.com'), + email: cognito.UserPoolEmail.withCognito('support@myawesomeapp.com'), }); ``` @@ -341,7 +341,7 @@ Once the SES setup is complete, the UserPool can be configured to use the SES em ```ts new cognito.UserPool(this, 'myuserpool', { - email: UserPoolEmail.withSES({ + email: cognito.UserPoolEmail.withSES({ fromEmail: 'noreply@myawesomeapp.com', fromName: 'Awesome App', replyTo: 'support@myawesomeapp.com', @@ -354,7 +354,7 @@ If the UserPool is being created in a different region, `sesRegion` must be used ```ts new cognito.UserPool(this, 'myuserpool', { - email: UserPoolEmail.withSES({ + email: cognito.UserPoolEmail.withSES({ sesRegion: 'us-east-1', fromEmail: 'noreply@myawesomeapp.com', fromName: 'Awesome App', @@ -395,7 +395,7 @@ on the construct, as so - const authChallengeFn = new lambda.Function(this, 'authChallengeFn', { runtime: lambda.Runtime.NODEJS_12_X, handler: 'index.handler', - code: lambda.Code.fromAsset(/* path to lambda asset */), + code: lambda.Code.fromAsset(path.join(__dirname, 'path/to/asset')), }); const userpool = new cognito.UserPool(this, 'myuserpool', { @@ -403,13 +403,13 @@ const userpool = new cognito.UserPool(this, 'myuserpool', { lambdaTriggers: { createAuthChallenge: authChallengeFn, // ... - } + }, }); userpool.addTrigger(cognito.UserPoolOperation.USER_MIGRATION, new lambda.Function(this, 'userMigrationFn', { runtime: lambda.Runtime.NODEJS_12_X, handler: 'index.handler', - code: lambda.Code.fromAsset(/* path to lambda asset */), + code: lambda.Code.fromAsset(path.join(__dirname, 'path/to/asset')), })); ``` @@ -428,7 +428,15 @@ Error message when running `cdk synth` or `cdk deploy`: To work around the circular dependency issue, use the `attachInlinePolicy()` API instead, as shown below. -```ts fixture=with-lambda-trigger +```ts +declare const postAuthFn: lambda.Function; + +const userpool = new cognito.UserPool(this, 'myuserpool', { + lambdaTriggers: { + postAuthentication: postAuthFn, + }, +}); + // provide permissions to describe the user pool scoped to the ARN the user pool postAuthFn.role?.attachInlinePolicy(new iam.Policy(this, 'userpool-policy', { statements: [new iam.PolicyStatement({ @@ -504,8 +512,8 @@ new cognito.UserPoolIdentityProviderAmazon(this, 'Amazon', { custom: { // custom user pool attributes go here uniqueId: cognito.ProviderAttribute.AMAZON_USER_ID, - } - } + }, + }, }); ``` @@ -530,7 +538,7 @@ and imported user pools, clients can also be created via the `UserPoolClient` co ```ts const importedPool = cognito.UserPool.fromUserPoolId(this, 'imported-pool', 'us-east-1_oiuR12Abd'); new cognito.UserPoolClient(this, 'customer-app-client', { - userPool: importedPool + userPool: importedPool, }); ``` @@ -547,7 +555,7 @@ pool.addClient('app-client', { authFlows: { userPassword: true, userSrp: true, - } + }, }); ``` @@ -575,7 +583,7 @@ pool.addClient('app-client', { scopes: [ cognito.OAuthScope.OPENID ], callbackUrls: [ 'https://my-app-domain.com/welcome' ], logoutUrls: [ 'https://my-app-domain.com/signin' ], - } + }, }); ``` @@ -605,22 +613,29 @@ pool.addClient('app-client', { supportedIdentityProviders: [ cognito.UserPoolClientIdentityProvider.AMAZON, cognito.UserPoolClientIdentityProvider.COGNITO, - ] + ], }); ``` -If the identity provider and the app client are created in the same stack, specify the dependency between both constructs to make sure that the identity provider already exists when the app client will be created. The app client cannot handle the dependency to the identity provider automatically because the client does not have access to the provider's construct. +If the identity provider and the app client are created in the same stack, specify the dependency between both constructs to +make sure that the identity provider already exists when the app client will be created. The app client cannot handle the +dependency to the identity provider automatically because the client does not have access to the provider's construct. ```ts +const pool = new cognito.UserPool(this, 'Pool'); const provider = new cognito.UserPoolIdentityProviderAmazon(this, 'Amazon', { - // ... + userPool: pool, + clientId: 'amzn-client-id', + clientSecret: 'amzn-client-secret', }); + const client = pool.addClient('app-client', { // ... supportedIdentityProviders: [ cognito.UserPoolClientIdentityProvider.AMAZON, ], -} +}); + client.node.addDependency(provider); ``` @@ -638,16 +653,17 @@ pool.addClient('app-client', { }); ``` -Clients can (and should) be allowed to read and write relevant user attributes only. Usually every client can be allowed to read the `given_name` -attribute but not every client should be allowed to set the `email_verified` attribute. +Clients can (and should) be allowed to read and write relevant user attributes only. Usually every client can be allowed to +read the `given_name` attribute but not every client should be allowed to set the `email_verified` attribute. The same criteria applies for both standard and custom attributes, more info is available at [Attribute Permissions and Scopes](https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-attributes.html#user-pool-settings-attribute-permissions-and-scopes). -The default behaviour is to allow read and write permissions on all attributes. The following code shows how this can be configured for a client. +The default behaviour is to allow read and write permissions on all attributes. The following code shows how this can be +configured for a client. ```ts const pool = new cognito.UserPool(this, 'Pool'); -const clientWriteAttributes = (new ClientAttributes()) +const clientWriteAttributes = (new cognito.ClientAttributes()) .withStandardAttributes({fullname: true, email: true}) .withCustomAttributes('favouritePizza', 'favouriteBeverage'); @@ -662,8 +678,9 @@ pool.addClient('app-client', { }); ``` -[Token revocation](https://docs.aws.amazon.com/cognito/latest/developerguide/token-revocation.html -) can be configured to be able to revoke refresh tokens in app clients. By default, token revocation is enabled for new user pools. The property can be used to enable the token revocation in existing app clients or to change the default behavior. +[Token revocation](https://docs.aws.amazon.com/cognito/latest/developerguide/token-revocation.html) +can be configured to be able to revoke refresh tokens in app clients. By default, token revocation is enabled for new user +pools. The property can be used to enable the token revocation in existing app clients or to change the default behavior. ```ts const pool = new cognito.UserPool(this, 'Pool'); @@ -687,8 +704,8 @@ app clients and configures the clients to use these scopes. ```ts const pool = new cognito.UserPool(this, 'Pool'); -const readOnlyScope = new ResourceServerScope({ scopeName: 'read', scopeDescription: 'Read-only access' }); -const fullAccessScope = new ResourceServerScope({ scopeName: '*', scopeDescription: 'Full access' }); +const readOnlyScope = new cognito.ResourceServerScope({ scopeName: 'read', scopeDescription: 'Read-only access' }); +const fullAccessScope = new cognito.ResourceServerScope({ scopeName: '*', scopeDescription: 'Full access' }); const userServer = pool.addResourceServer('ResourceServer', { identifier: 'users', @@ -699,7 +716,7 @@ const readOnlyClient = pool.addClient('read-only-client', { // ... oAuth: { // ... - scopes: [ OAuthScope.resourceServer(userServer, readOnlyScope) ], + scopes: [ cognito.OAuthScope.resourceServer(userServer, readOnlyScope) ], }, }); @@ -707,7 +724,7 @@ const fullAccessClient = pool.addClient('full-access-client', { // ... oAuth: { // ... - scopes: [ OAuthScope.resourceServer(userServer, fullAccessScope) ], + scopes: [ cognito.OAuthScope.resourceServer(userServer, fullAccessScope) ], }, }); ``` @@ -720,7 +737,8 @@ configured using domains. There are two ways to set up a domain - either the Ama with an available domain prefix, or a custom domain name can be chosen. The custom domain must be one that is already owned, and whose certificate is registered in AWS Certificate Manager. -The following code sets up a user pool domain in Amazon Cognito hosted domain with the prefix 'my-awesome-app', and another domain with the custom domain 'user.myapp.com' - +The following code sets up a user pool domain in Amazon Cognito hosted domain with the prefix 'my-awesome-app', and +another domain with the custom domain 'user.myapp.com' - ```ts const pool = new cognito.UserPool(this, 'Pool'); @@ -763,15 +781,15 @@ const client = userpool.addClient('Client', { callbackUrls: [ 'https://myapp.com/home', 'https://myapp.com/users', - ] - } -}) + ], + }, +}); const domain = userpool.addDomain('Domain', { // ... }); const signInUrl = domain.signInUrl(client, { redirectUri: 'https://myapp.com/home', // must be a URL configured under 'callbackUrls' with the client -}) +}); ``` Existing domains can be imported into CDK apps using `UserPoolDomain.fromDomainName()` API diff --git a/packages/@aws-cdk/aws-cognito/package.json b/packages/@aws-cdk/aws-cognito/package.json index 78953fd30c053..e0f10c43ca32a 100644 --- a/packages/@aws-cdk/aws-cognito/package.json +++ b/packages/@aws-cdk/aws-cognito/package.json @@ -28,7 +28,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", diff --git a/packages/@aws-cdk/aws-cognito/rosetta/default.ts-fixture b/packages/@aws-cdk/aws-cognito/rosetta/default.ts-fixture index e0b5bfc8747a3..996a448f8bfc6 100644 --- a/packages/@aws-cdk/aws-cognito/rosetta/default.ts-fixture +++ b/packages/@aws-cdk/aws-cognito/rosetta/default.ts-fixture @@ -5,6 +5,7 @@ import * as certificatemanager from '@aws-cdk/aws-certificatemanager'; import * as cognito from '@aws-cdk/aws-cognito'; import * as iam from '@aws-cdk/aws-iam'; import * as lambda from '@aws-cdk/aws-lambda'; +import * as path from 'path'; class Fixture extends Stack { constructor(scope: Construct, id: string) { diff --git a/packages/@aws-cdk/aws-cognito/rosetta/with-lambda-trigger.ts-fixture b/packages/@aws-cdk/aws-cognito/rosetta/with-lambda-trigger.ts-fixture deleted file mode 100644 index de9aa90eedfc2..0000000000000 --- a/packages/@aws-cdk/aws-cognito/rosetta/with-lambda-trigger.ts-fixture +++ /dev/null @@ -1,26 +0,0 @@ -// Fixture with packages imported, but nothing else -import { Stack } from '@aws-cdk/core'; -import { Construct } from 'constructs'; -import * as cognito from '@aws-cdk/aws-cognito'; -import * as iam from '@aws-cdk/aws-iam'; -import * as lambda from '@aws-cdk/aws-lambda'; - -class Fixture extends Stack { - constructor(scope: Construct, id: string) { - super(scope, id); - - const postAuthFn = new lambda.Function(this, 'postAuthFn', { - code: lambda.Code.fromInline('post authentication'), - runtime: lambda.Runtime.NODEJS_12_X, - handler: 'index.handler', - }); - - const userpool = new cognito.UserPool(this, 'myuserpool', { - lambdaTriggers: { - postAuthentication: postAuthFn, - }, - }); - - /// here - } -} diff --git a/packages/@aws-cdk/aws-ec2/lib/client-vpn-endpoint.ts b/packages/@aws-cdk/aws-ec2/lib/client-vpn-endpoint.ts index 3ee8e8956d3d1..f28dd43462838 100644 --- a/packages/@aws-cdk/aws-ec2/lib/client-vpn-endpoint.ts +++ b/packages/@aws-cdk/aws-ec2/lib/client-vpn-endpoint.ts @@ -150,6 +150,37 @@ export interface ClientVpnEndpointOptions { * @default true */ readonly authorizeAllUsersToVpcCidr?: boolean; + + /** + * The maximum VPN session duration time. + * + * @default ClientVpnSessionTimeout.TWENTY_FOUR_HOURS + */ + readonly sessionTimeout?: ClientVpnSessionTimeout; + + /** + * Customizable text that will be displayed in a banner on AWS provided clients + * when a VPN session is established. + * + * UTF-8 encoded characters only. Maximum of 1400 characters. + * + * @default - no banner is presented to the client + */ + readonly clientLoginBanner?: string; +} + +/** + * Maximum VPN session duration time + */ +export enum ClientVpnSessionTimeout { + /** 8 hours */ + EIGHT_HOURS = 8, + /** 10 hours */ + TEN_HOURS = 10, + /** 12 hours */ + TWELVE_HOURS = 12, + /** 24 hours */ + TWENTY_FOUR_HOURS = 24, } /** @@ -284,6 +315,12 @@ export class ClientVpnEndpoint extends Resource implements IClientVpnEndpoint { throw new Error('The name of the Lambda function must begin with the `AWSClientVPN-` prefix'); } + if (props.clientLoginBanner + && !Token.isUnresolved(props.clientLoginBanner) + && props.clientLoginBanner.length > 1400) { + throw new Error(`The maximum length for the client login banner is 1400, got ${props.clientLoginBanner.length}`); + } + const logging = props.logging ?? true; const logGroup = logging ? props.logGroup ?? new logs.LogGroup(this, 'LogGroup') @@ -317,6 +354,13 @@ export class ClientVpnEndpoint extends Resource implements IClientVpnEndpoint { transportProtocol: props.transportProtocol, vpcId: props.vpc.vpcId, vpnPort: props.port, + sessionTimeoutHours: props.sessionTimeout, + clientLoginBannerOptions: props.clientLoginBanner + ? { + enabled: true, + bannerText: props.clientLoginBanner, + } + : undefined, }); this.endpointId = endpoint.ref; diff --git a/packages/@aws-cdk/aws-ec2/package.json b/packages/@aws-cdk/aws-ec2/package.json index 5d51459d3b4d2..2eb5260bac1c4 100644 --- a/packages/@aws-cdk/aws-ec2/package.json +++ b/packages/@aws-cdk/aws-ec2/package.json @@ -693,7 +693,9 @@ "props-physical-name:@aws-cdk/aws-ec2.VpnGatewayProps", "props-physical-name:@aws-cdk/aws-ec2.ClientVpnEndpointProps", "props-physical-name:@aws-cdk/aws-ec2.ClientVpnAuthorizationRuleProps", - "props-physical-name:@aws-cdk/aws-ec2.ClientVpnRouteProps" + "props-physical-name:@aws-cdk/aws-ec2.ClientVpnRouteProps", + "duration-prop-type:@aws-cdk/aws-ec2.ClientVpnEndpointOptions.sessionTimeout", + "duration-prop-type:@aws-cdk/aws-ec2.ClientVpnEndpointProps.sessionTimeout" ] }, "stability": "stable", diff --git a/packages/@aws-cdk/aws-ec2/test/client-vpn-endpoint.test.ts b/packages/@aws-cdk/aws-ec2/test/client-vpn-endpoint.test.ts index 2bf2357f9c32f..2b087d6da1f10 100644 --- a/packages/@aws-cdk/aws-ec2/test/client-vpn-endpoint.test.ts +++ b/packages/@aws-cdk/aws-ec2/test/client-vpn-endpoint.test.ts @@ -242,6 +242,35 @@ test('client vpn endpoint with custom route', () => { }); }); +test('client vpn endpoint with custom session timeout', () => { + vpc.addClientVpnEndpoint('Endpoint', { + cidr: '10.100.0.0/16', + serverCertificateArn: 'server-certificate-arn', + clientCertificateArn: 'client-certificate-arn', + sessionTimeout: ec2.ClientVpnSessionTimeout.TEN_HOURS, + }); + + Template.fromStack(stack).hasResourceProperties('AWS::EC2::ClientVpnEndpoint', { + SessionTimeoutHours: 10, + }); +}); + +test('client vpn endpoint with client login banner', () => { + vpc.addClientVpnEndpoint('Endpoint', { + cidr: '10.100.0.0/16', + serverCertificateArn: 'server-certificate-arn', + clientCertificateArn: 'client-certificate-arn', + clientLoginBanner: 'Welcome!', + }); + + Template.fromStack(stack).hasResourceProperties('AWS::EC2::ClientVpnEndpoint', { + ClientLoginBannerOptions: { + Enabled: true, + BannerText: 'Welcome!', + }, + }); +}); + test('throws with more than 2 dns servers', () => { expect(() => vpc.addClientVpnEndpoint('Endpoint', { cidr: '10.100.0.0/16', diff --git a/packages/@aws-cdk/aws-ecs-patterns/README.md b/packages/@aws-cdk/aws-ecs-patterns/README.md index 386682e0b75d9..593421abeaa1a 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/README.md +++ b/packages/@aws-cdk/aws-ecs-patterns/README.md @@ -544,6 +544,26 @@ const queueProcessingFargateService = new ecsPatterns.QueueProcessingFargateServ }); ``` +### Set a custom container-level Healthcheck for QueueProcessingFargateService + +```ts +declare const vpc: ec2.Vpc; +declare const securityGroup: ec2.SecurityGroup; +const queueProcessingFargateService = new ecsPatterns.QueueProcessingFargateService(this, 'Service', { + vpc, + memoryLimitMiB: 512, + image: ecs.ContainerImage.fromRegistry('test'), + healthCheck: { + command: [ "CMD-SHELL", "curl -f http://localhost/ || exit 1" ], + // the properties below are optional + interval: Duration.minutes(30), + retries: 123, + startPeriod: Duration.minutes(30), + timeout: Duration.minutes(30), + }, +}); +``` + ### Set capacityProviderStrategies for QueueProcessingEc2Service ```ts diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/queue-processing-fargate-service.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/queue-processing-fargate-service.ts index 033d19dc39612..ff63c4a3502d8 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/queue-processing-fargate-service.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/queue-processing-fargate-service.ts @@ -1,5 +1,5 @@ import * as ec2 from '@aws-cdk/aws-ec2'; -import { FargatePlatformVersion, FargateService, FargateTaskDefinition } from '@aws-cdk/aws-ecs'; +import { FargatePlatformVersion, FargateService, FargateTaskDefinition, HealthCheck } from '@aws-cdk/aws-ecs'; import * as cxapi from '@aws-cdk/cx-api'; import { Construct } from 'constructs'; import { QueueProcessingServiceBase, QueueProcessingServiceBaseProps } from '../base/queue-processing-service-base'; @@ -69,6 +69,13 @@ export interface QueueProcessingFargateServiceProps extends QueueProcessingServi */ readonly containerName?: string; + /** + * The health check command and associated configuration parameters for the container. + * + * @default - Health check configuration from container. + */ + readonly healthCheck?: HealthCheck; + /** * The subnets to associate with the service. * @@ -127,6 +134,7 @@ export class QueueProcessingFargateService extends QueueProcessingServiceBase { environment: this.environment, secrets: this.secrets, logging: this.logDriver, + healthCheck: props.healthCheck, }); // The desiredCount should be removed from the fargate service when the feature flag is removed. diff --git a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.queue-processing-fargate-service-public.expected.json b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.queue-processing-fargate-service-public.expected.json index ae18adac13bbf..8af962023d1d2 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.queue-processing-fargate-service-public.expected.json +++ b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.queue-processing-fargate-service-public.expected.json @@ -604,6 +604,15 @@ } ], "Essential": true, + "HealthCheck": { + "Command": [ + "CMD-SHELL", + "curl -f http://localhost/ || exit 1" + ], + "Interval": 720, + "Retries": 34, + "Timeout": 5 + }, "Image": { "Fn::Join": [ "", diff --git a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.queue-processing-fargate-service-public.ts b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.queue-processing-fargate-service-public.ts index 5b0d74f9e3ceb..4877a7d211747 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.queue-processing-fargate-service-public.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.queue-processing-fargate-service-public.ts @@ -1,7 +1,7 @@ import * as path from 'path'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as ecs from '@aws-cdk/aws-ecs'; -import { App, Stack } from '@aws-cdk/core'; +import { App, Stack, Duration } from '@aws-cdk/core'; import { QueueProcessingFargateService } from '../../lib'; @@ -14,6 +14,11 @@ new QueueProcessingFargateService(stack, 'PublicQueueService', { memoryLimitMiB: 512, image: new ecs.AssetImage(path.join(__dirname, '..', 'sqs-reader')), assignPublicIp: true, + healthCheck: { + command: ['CMD-SHELL', 'curl -f http://localhost/ || exit 1'], + interval: Duration.minutes(12), + retries: 34, + }, }); app.synth(); diff --git a/packages/@aws-cdk/aws-ecs/README.md b/packages/@aws-cdk/aws-ecs/README.md index f1e50e28ea909..d99b830aad6dd 100644 --- a/packages/@aws-cdk/aws-ecs/README.md +++ b/packages/@aws-cdk/aws-ecs/README.md @@ -96,6 +96,15 @@ const cluster = new ecs.Cluster(this, 'Cluster', { }); ``` +The following code imports an existing cluster using the ARN which can be used to +import an Amazon ECS service either EC2 or Fargate. + +```ts +const clusterArn = 'arn:aws:ecs:us-east-1:012345678910:cluster/clusterName'; + +const cluster = ecs.Cluster.fromClusterArn(this, 'Cluster', clusterArn); +``` + To use tasks with Amazon EC2 launch-type, you have to add capacity to the cluster in order for tasks to be scheduled on your instances. Typically, you add an AutoScalingGroup with instances running the latest 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 34eb1dc409975..4e5de4c90eacf 100644 --- a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts @@ -5,10 +5,10 @@ import * as elb from '@aws-cdk/aws-elasticloadbalancing'; import * as elbv2 from '@aws-cdk/aws-elasticloadbalancingv2'; import * as iam from '@aws-cdk/aws-iam'; import * as cloudmap from '@aws-cdk/aws-servicediscovery'; -import { Annotations, Duration, IResolvable, IResource, Lazy, Resource, Stack } from '@aws-cdk/core'; +import { Annotations, Duration, IResolvable, IResource, Lazy, Resource, Stack, ArnFormat } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { LoadBalancerTargetOptions, NetworkMode, TaskDefinition } from '../base/task-definition'; -import { ICluster, CapacityProviderStrategy, ExecuteCommandLogging } from '../cluster'; +import { ICluster, CapacityProviderStrategy, ExecuteCommandLogging, Cluster } from '../cluster'; import { ContainerDefinition, Protocol } from '../container-definition'; import { CfnService } from '../ecs.generated'; import { ScalableTaskCount } from './scalable-task-count'; @@ -315,6 +315,46 @@ export interface IBaseService extends IService { */ export abstract class BaseService extends Resource implements IBaseService, elbv2.IApplicationLoadBalancerTarget, elbv2.INetworkLoadBalancerTarget, elb.ILoadBalancerTarget { + /** + * Import an existing ECS/Fargate Service using the service cluster format. + * The format is the "new" format "arn:aws:ecs:region:aws_account_id:service/cluster-name/service-name". + * @see https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs-account-settings.html#ecs-resource-ids + */ + public static fromServiceArnWithCluster(scope: Construct, id: string, serviceArn: string): IBaseService { + const stack = Stack.of(scope); + const arn = stack.splitArn(serviceArn, ArnFormat.SLASH_RESOURCE_NAME); + const resourceName = arn.resourceName; + if (!resourceName) { + throw new Error('Missing resource Name from service ARN: ${serviceArn}'); + } + const resourceNameParts = resourceName.split('/'); + if (resourceNameParts.length !== 2) { + throw new Error(`resource name ${resourceName} from service ARN: ${serviceArn} is not using the ARN cluster format`); + } + const clusterName = resourceNameParts[0]; + const serviceName = resourceNameParts[1]; + + const clusterArn = Stack.of(scope).formatArn({ + partition: arn.partition, + region: arn.region, + account: arn.account, + service: 'ecs', + resource: 'cluster', + resourceName: clusterName, + }); + + const cluster = Cluster.fromClusterArn(scope, `${id}Cluster`, clusterArn); + + class Import extends Resource implements IBaseService { + public readonly serviceArn = serviceArn; + public readonly serviceName = serviceName; + public readonly cluster = cluster; + } + + return new Import(scope, id, { + environmentFromArn: serviceArn, + }); + } /** * The security groups which manage the allowed network traffic for the service. diff --git a/packages/@aws-cdk/aws-ecs/lib/cluster.ts b/packages/@aws-cdk/aws-ecs/lib/cluster.ts index 1dc50ac97ae49..8e48e2be59cec 100644 --- a/packages/@aws-cdk/aws-ecs/lib/cluster.ts +++ b/packages/@aws-cdk/aws-ecs/lib/cluster.ts @@ -6,7 +6,7 @@ import * as kms from '@aws-cdk/aws-kms'; import * as logs from '@aws-cdk/aws-logs'; import * as s3 from '@aws-cdk/aws-s3'; import * as cloudmap from '@aws-cdk/aws-servicediscovery'; -import { Duration, Lazy, IResource, Resource, Stack, Aspects, IAspect, IConstruct } from '@aws-cdk/core'; +import { Duration, Lazy, IResource, Resource, Stack, Aspects, IAspect, IConstruct, ArnFormat } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { BottleRocketImage, EcsOptimizedAmi } from './amis'; import { InstanceDrainHook } from './drain-hook/instance-drain-hook'; @@ -105,6 +105,41 @@ export class Cluster extends Resource implements ICluster { return new ImportedCluster(scope, id, attrs); } + /** + * Import an existing cluster to the stack from the cluster ARN. + * This does not provide access to the vpc, hasEc2Capacity, or connections - + * use the `fromClusterAttributes` method to access those properties. + */ + public static fromClusterArn(scope: Construct, id: string, clusterArn: string): ICluster { + const stack = Stack.of(scope); + const arn = stack.splitArn(clusterArn, ArnFormat.SLASH_RESOURCE_NAME); + const clusterName = arn.resourceName; + + if (!clusterName) { + throw new Error(`Missing required Cluster Name from Cluster ARN: ${clusterArn}`); + } + + const errorSuffix = 'is not available for a Cluster imported using fromClusterArn(), please use fromClusterAttributes() instead.'; + + class Import extends Resource implements ICluster { + public readonly clusterArn = clusterArn; + public readonly clusterName = clusterName!; + get hasEc2Capacity(): boolean { + throw new Error(`hasEc2Capacity ${errorSuffix}`); + } + get connections(): ec2.Connections { + throw new Error(`connections ${errorSuffix}`); + } + get vpc(): ec2.IVpc { + throw new Error(`vpc ${errorSuffix}`); + } + } + + return new Import(scope, id, { + environmentFromArn: clusterArn, + }); + } + /** * Manage the allowed network connections for the cluster with Security Groups. */ diff --git a/packages/@aws-cdk/aws-ecs/test/base-service.test.ts b/packages/@aws-cdk/aws-ecs/test/base-service.test.ts new file mode 100644 index 0000000000000..6e3b563cf4e1e --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/test/base-service.test.ts @@ -0,0 +1,44 @@ +import * as cdk from '@aws-cdk/core'; +import * as ecs from '../lib'; + +let stack: cdk.Stack; + +beforeEach(() => { + stack = new cdk.Stack(); +}); + +describe('When import an ECS Service', () => { + test('with serviceArnWithCluster', () => { + // GIVEN + const clusterName = 'cluster-name'; + const serviceName = 'my-http-service'; + const region = 'service-region'; + const account = 'service-account'; + const serviceArn = `arn:aws:ecs:${region}:${account}:service/${clusterName}/${serviceName}`; + + // WHEN + const service = ecs.BaseService.fromServiceArnWithCluster(stack, 'Service', serviceArn); + + // THEN + expect(service.serviceArn).toEqual(serviceArn); + expect(service.serviceName).toEqual(serviceName); + expect(service.env.account).toEqual(account); + expect(service.env.region).toEqual(region); + + expect(service.cluster.clusterName).toEqual(clusterName); + expect(service.cluster.env.account).toEqual(account); + expect(service.cluster.env.region).toEqual(region); + }); + + test('throws an expection if no resourceName provided on fromServiceArnWithCluster', () => { + expect(() => { + ecs.BaseService.fromServiceArnWithCluster(stack, 'Service', 'arn:aws:ecs:service-region:service-account:service'); + }).toThrowError(/Missing resource Name from service ARN/); + }); + + test('throws an expection if not using cluster arn format on fromServiceArnWithCluster', () => { + expect(() => { + ecs.BaseService.fromServiceArnWithCluster(stack, 'Service', 'arn:aws:ecs:service-region:service-account:service/my-http-service'); + }).toThrowError(/is not using the ARN cluster format/); + }); +}); diff --git a/packages/@aws-cdk/aws-ecs/test/cluster.test.ts b/packages/@aws-cdk/aws-ecs/test/cluster.test.ts index f6b34f76439c0..d167c30989ded 100644 --- a/packages/@aws-cdk/aws-ecs/test/cluster.test.ts +++ b/packages/@aws-cdk/aws-ecs/test/cluster.test.ts @@ -2140,6 +2140,30 @@ describe('cluster', () => { }); + + test('When importing ECS Cluster via Arn', () => { + // GIVEN + const stack = new cdk.Stack(); + const clusterName = 'my-cluster'; + const region = 'service-region'; + const account = 'service-account'; + const cluster = ecs.Cluster.fromClusterArn(stack, 'Cluster', `arn:aws:ecs:${region}:${account}:cluster/${clusterName}`); + + // THEN + expect(cluster.clusterName).toEqual(clusterName); + expect(cluster.env.region).toEqual(region); + expect(cluster.env.account).toEqual(account); + }); + + test('throws error when import ECS Cluster without resource name in arn', () => { + // GIVEN + const stack = new cdk.Stack(); + + // THEN + expect(() => { + ecs.Cluster.fromClusterArn(stack, 'Cluster', 'arn:aws:ecs:service-region:service-account:cluster'); + }).toThrowError(/Missing required Cluster Name from Cluster ARN: /); + }); }); test('can add ASG capacity via Capacity Provider by not specifying machineImageType', () => { diff --git a/packages/@aws-cdk/aws-eks-legacy/package.json b/packages/@aws-cdk/aws-eks-legacy/package.json index 685633b13c976..c05d56e6b3ddb 100644 --- a/packages/@aws-cdk/aws-eks-legacy/package.json +++ b/packages/@aws-cdk/aws-eks-legacy/package.json @@ -77,7 +77,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@aws-cdk/assert-internal": "0.0.0", + "@aws-cdk/assertions": "0.0.0", "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cdk-integ-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-eks-legacy/test/awsauth.test.ts b/packages/@aws-cdk/aws-eks-legacy/test/awsauth.test.ts index 3a5f28f648441..f642836ded6e2 100644 --- a/packages/@aws-cdk/aws-eks-legacy/test/awsauth.test.ts +++ b/packages/@aws-cdk/aws-eks-legacy/test/awsauth.test.ts @@ -1,6 +1,6 @@ -import '@aws-cdk/assert-internal/jest'; -import { describeDeprecated } from '@aws-cdk/cdk-build-tools'; +import { Template } from '@aws-cdk/assertions'; import * as iam from '@aws-cdk/aws-iam'; +import { describeDeprecated } from '@aws-cdk/cdk-build-tools'; import { Cluster, KubernetesResource } from '../lib'; import { AwsAuth } from '../lib/aws-auth'; import { testFixtureNoVpc } from './util'; @@ -17,7 +17,7 @@ describeDeprecated('awsauth', () => { new AwsAuth(stack, 'AwsAuth', { cluster }); // THEN - expect(stack).toHaveResource(KubernetesResource.RESOURCE_TYPE, { + Template.fromStack(stack).hasResourceProperties(KubernetesResource.RESOURCE_TYPE, { Manifest: JSON.stringify([{ apiVersion: 'v1', kind: 'ConfigMap', @@ -44,8 +44,8 @@ describeDeprecated('awsauth', () => { cluster.awsAuth.addAccount('5566776655'); // THEN - expect(stack).toCountResources(KubernetesResource.RESOURCE_TYPE, 1); - expect(stack).toHaveResource(KubernetesResource.RESOURCE_TYPE, { + Template.fromStack(stack).resourceCountIs(KubernetesResource.RESOURCE_TYPE, 1); + Template.fromStack(stack).hasResourceProperties(KubernetesResource.RESOURCE_TYPE, { Manifest: { 'Fn::Join': [ '', @@ -106,7 +106,7 @@ describeDeprecated('awsauth', () => { cluster.awsAuth.addUserMapping(user, { groups: ['group2'] }); // THEN - expect(stack).toHaveResource(KubernetesResource.RESOURCE_TYPE, { + Template.fromStack(stack).hasResourceProperties(KubernetesResource.RESOURCE_TYPE, { Manifest: { 'Fn::Join': [ '', diff --git a/packages/@aws-cdk/aws-eks-legacy/test/cluster.test.ts b/packages/@aws-cdk/aws-eks-legacy/test/cluster.test.ts index b7f1666fb79b0..61188e858110a 100644 --- a/packages/@aws-cdk/aws-eks-legacy/test/cluster.test.ts +++ b/packages/@aws-cdk/aws-eks-legacy/test/cluster.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as iam from '@aws-cdk/aws-iam'; import { describeDeprecated } from '@aws-cdk/cdk-build-tools'; @@ -18,7 +18,7 @@ describeDeprecated('cluster', () => { new eks.Cluster(stack, 'Cluster', { vpc, kubectlEnabled: false, defaultCapacity: 0 }); // THEN - expect(stack).toHaveResourceLike('AWS::EKS::Cluster', { + Template.fromStack(stack).hasResourceProperties('AWS::EKS::Cluster', { ResourcesVpcConfig: { SubnetIds: [ { Ref: 'VPCPublicSubnet1SubnetB4246D30' }, @@ -40,7 +40,7 @@ describeDeprecated('cluster', () => { new eks.Cluster(stack, 'cluster'); // THEN - expect(stack).toHaveResource('AWS::EC2::VPC'); + Template.fromStack(stack).resourceCountIs('AWS::EC2::VPC', 1); }); @@ -55,8 +55,8 @@ describeDeprecated('cluster', () => { // THEN expect(cluster.defaultCapacity).toBeDefined(); - expect(stack).toHaveResource('AWS::AutoScaling::AutoScalingGroup', { DesiredCapacity: '2' }); - expect(stack).toHaveResource('AWS::AutoScaling::LaunchConfiguration', { InstanceType: 'm5.large' }); + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::AutoScalingGroup', { DesiredCapacity: '2' }); + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::LaunchConfiguration', { InstanceType: 'm5.large' }); }); @@ -72,8 +72,8 @@ describeDeprecated('cluster', () => { // THEN expect(cluster.defaultCapacity).toBeDefined(); - expect(stack).toHaveResource('AWS::AutoScaling::AutoScalingGroup', { DesiredCapacity: '10' }); - expect(stack).toHaveResource('AWS::AutoScaling::LaunchConfiguration', { InstanceType: 'm2.xlarge' }); + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::AutoScalingGroup', { DesiredCapacity: '10' }); + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::LaunchConfiguration', { InstanceType: 'm2.xlarge' }); }); @@ -86,8 +86,8 @@ describeDeprecated('cluster', () => { // THEN expect(cluster.defaultCapacity).toBeUndefined(); - expect(stack).not.toHaveResource('AWS::AutoScaling::AutoScalingGroup'); - expect(stack).not.toHaveResource('AWS::AutoScaling::LaunchConfiguration'); + Template.fromStack(stack).resourceCountIs('AWS::AutoScaling::AutoScalingGroup', 0); + Template.fromStack(stack).resourceCountIs('AWS::AutoScaling::LaunchConfiguration', 0); }); }); @@ -100,7 +100,7 @@ describeDeprecated('cluster', () => { new eks.Cluster(stack, 'Cluster', { vpc, kubectlEnabled: false, defaultCapacity: 0 }); // THEN - expect(stack).toHaveResource('AWS::EC2::Subnet', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::Subnet', { Tags: [ { Key: 'aws-cdk:subnet-name', Value: 'Private' }, { Key: 'aws-cdk:subnet-type', Value: 'Private' }, @@ -120,7 +120,7 @@ describeDeprecated('cluster', () => { new eks.Cluster(stack, 'Cluster', { vpc, kubectlEnabled: false, defaultCapacity: 0 }); // THEN - expect(stack).toHaveResource('AWS::EC2::Subnet', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::Subnet', { MapPublicIpOnLaunch: true, Tags: [ { Key: 'aws-cdk:subnet-name', Value: 'Public' }, @@ -144,7 +144,7 @@ describeDeprecated('cluster', () => { }); // THEN - expect(stack).toHaveResource('AWS::AutoScaling::AutoScalingGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::AutoScalingGroup', { Tags: [ { Key: { 'Fn::Join': ['', ['kubernetes.io/cluster/', { Ref: 'ClusterEB0386A7' }]] }, @@ -182,7 +182,7 @@ describeDeprecated('cluster', () => { new cdk.CfnOutput(stack2, 'ClusterARN', { value: imported.clusterArn }); // THEN - expect(stack2).toMatchTemplate({ + Template.fromStack(stack2).templateMatches({ Outputs: { ClusterARN: { Value: { @@ -216,7 +216,7 @@ describeDeprecated('cluster', () => { new eks.Cluster(stack, 'Cluster', { vpc, mastersRole: role, defaultCapacity: 0 }); // THEN - expect(stack).toHaveResource(eks.KubernetesResource.RESOURCE_TYPE, { + Template.fromStack(stack).hasResourceProperties(eks.KubernetesResource.RESOURCE_TYPE, { Manifest: { 'Fn::Join': [ '', @@ -247,11 +247,11 @@ describeDeprecated('cluster', () => { cluster.addResource('manifest2', { bar: 123 }, { boor: [1, 2, 3] }); // THEN - expect(stack).toHaveResource(eks.KubernetesResource.RESOURCE_TYPE, { + Template.fromStack(stack).hasResourceProperties(eks.KubernetesResource.RESOURCE_TYPE, { Manifest: '[{"foo":123}]', }); - expect(stack).toHaveResource(eks.KubernetesResource.RESOURCE_TYPE, { + Template.fromStack(stack).hasResourceProperties(eks.KubernetesResource.RESOURCE_TYPE, { Manifest: '[{"bar":123},{"boor":[1,2,3]}]', }); @@ -269,7 +269,7 @@ describeDeprecated('cluster', () => { }); // THEN - expect(stack).toHaveResource(eks.KubernetesResource.RESOURCE_TYPE, { + Template.fromStack(stack).hasResourceProperties(eks.KubernetesResource.RESOURCE_TYPE, { Manifest: { 'Fn::Join': [ '', @@ -302,7 +302,7 @@ describeDeprecated('cluster', () => { }); // THEN - expect(stack).not.toHaveResource(eks.KubernetesResource.RESOURCE_TYPE); + Template.fromStack(stack).resourceCountIs(eks.KubernetesResource.RESOURCE_TYPE, 0); }); @@ -317,7 +317,7 @@ describeDeprecated('cluster', () => { }); // THEN - expect(stack).not.toHaveResource(eks.KubernetesResource.RESOURCE_TYPE); + Template.fromStack(stack).resourceCountIs(eks.KubernetesResource.RESOURCE_TYPE, 0); }); @@ -524,7 +524,7 @@ describeDeprecated('cluster', () => { }); // THEN - expect(stack).toHaveResource(eks.KubernetesResource.RESOURCE_TYPE, { Manifest: JSON.stringify(spotInterruptHandler()) }); + Template.fromStack(stack).hasResourceProperties(eks.KubernetesResource.RESOURCE_TYPE, { Manifest: JSON.stringify(spotInterruptHandler()) }); }); @@ -540,7 +540,7 @@ describeDeprecated('cluster', () => { }); // THEN - expect(stack).not.toHaveResource(eks.KubernetesResource.RESOURCE_TYPE); + Template.fromStack(stack).resourceCountIs(eks.KubernetesResource.RESOURCE_TYPE, 0); }); diff --git a/packages/@aws-cdk/aws-eks-legacy/test/helm-chart.test.ts b/packages/@aws-cdk/aws-eks-legacy/test/helm-chart.test.ts index 9606f892d5f1c..90b814a8bc0b3 100644 --- a/packages/@aws-cdk/aws-eks-legacy/test/helm-chart.test.ts +++ b/packages/@aws-cdk/aws-eks-legacy/test/helm-chart.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import { describeDeprecated } from '@aws-cdk/cdk-build-tools'; import * as eks from '../lib'; import { testFixtureCluster } from './util'; @@ -15,7 +15,7 @@ describeDeprecated('helm chart', () => { new eks.HelmChart(stack, 'MyChart', { cluster, chart: 'chart' }); // THEN - expect(stack).toHaveResource(eks.HelmChart.RESOURCE_TYPE, { Namespace: 'default' }); + Template.fromStack(stack).hasResourceProperties(eks.HelmChart.RESOURCE_TYPE, { Namespace: 'default' }); }); test('should have a lowercase default release name', () => { @@ -26,7 +26,7 @@ describeDeprecated('helm chart', () => { new eks.HelmChart(stack, 'MyChart', { cluster, chart: 'chart' }); // THEN - expect(stack).toHaveResource(eks.HelmChart.RESOURCE_TYPE, { Release: 'stackmychartff398361' }); + Template.fromStack(stack).hasResourceProperties(eks.HelmChart.RESOURCE_TYPE, { Release: 'stackmychartff398361' }); }); test('should trim the last 63 of the default release name', () => { @@ -37,7 +37,7 @@ describeDeprecated('helm chart', () => { new eks.HelmChart(stack, 'MyChartNameWhichISMostProbablyLongerThenSixtyThreeCharacters', { cluster, chart: 'chart' }); // THEN - expect(stack).toHaveResource(eks.HelmChart.RESOURCE_TYPE, { Release: 'rtnamewhichismostprobablylongerthensixtythreecharactersb800614d' }); + Template.fromStack(stack).hasResourceProperties(eks.HelmChart.RESOURCE_TYPE, { Release: 'rtnamewhichismostprobablylongerthensixtythreecharactersb800614d' }); }); test('with values', () => { @@ -48,7 +48,7 @@ describeDeprecated('helm chart', () => { new eks.HelmChart(stack, 'MyChart', { cluster, chart: 'chart', values: { foo: 123 } }); // THEN - expect(stack).toHaveResource(eks.HelmChart.RESOURCE_TYPE, { Values: '{\"foo\":123}' }); + Template.fromStack(stack).hasResourceProperties(eks.HelmChart.RESOURCE_TYPE, { Values: '{\"foo\":123}' }); }); }); diff --git a/packages/@aws-cdk/aws-eks-legacy/test/manifest.test.ts b/packages/@aws-cdk/aws-eks-legacy/test/manifest.test.ts index 9a3de8f587f4c..2558945d61e94 100644 --- a/packages/@aws-cdk/aws-eks-legacy/test/manifest.test.ts +++ b/packages/@aws-cdk/aws-eks-legacy/test/manifest.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import { describeDeprecated } from '@aws-cdk/cdk-build-tools'; import { Cluster, KubernetesResource } from '../lib'; import { testFixtureNoVpc } from './util'; @@ -69,7 +69,7 @@ describeDeprecated('manifest', () => { manifest, }); - expect(stack).toHaveResource(KubernetesResource.RESOURCE_TYPE, { + Template.fromStack(stack).hasResourceProperties(KubernetesResource.RESOURCE_TYPE, { Manifest: JSON.stringify(manifest), }); diff --git a/packages/@aws-cdk/aws-eks/package.json b/packages/@aws-cdk/aws-eks/package.json index 79ea6601f9fff..799f07066b7e7 100644 --- a/packages/@aws-cdk/aws-eks/package.json +++ b/packages/@aws-cdk/aws-eks/package.json @@ -79,7 +79,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@aws-cdk/assert-internal": "0.0.0", + "@aws-cdk/assertions": "0.0.0", "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cdk-integ-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-eks/test/alb-controller.test.ts b/packages/@aws-cdk/aws-eks/test/alb-controller.test.ts index 7a1720a93f844..66d2d3f0ff1fd 100644 --- a/packages/@aws-cdk/aws-eks/test/alb-controller.test.ts +++ b/packages/@aws-cdk/aws-eks/test/alb-controller.test.ts @@ -1,7 +1,7 @@ import * as fs from 'fs'; import * as path from 'path'; +import { Template } from '@aws-cdk/assertions'; import * as iam from '@aws-cdk/aws-iam'; -import '@aws-cdk/assert-internal/jest'; import { Cluster, KubernetesVersion, AlbController, AlbControllerVersion, HelmChart } from '../lib'; import { testFixture } from './util'; @@ -40,7 +40,7 @@ test('can configure a custom repository', () => { repository: 'custom', }); - expect(stack).toHaveResource(HelmChart.RESOURCE_TYPE, { + Template.fromStack(stack).hasResourceProperties(HelmChart.RESOURCE_TYPE, { Values: { 'Fn::Join': [ '', diff --git a/packages/@aws-cdk/aws-eks/test/awsauth.test.ts b/packages/@aws-cdk/aws-eks/test/awsauth.test.ts index 0f6d3cc4b5891..c141965738084 100644 --- a/packages/@aws-cdk/aws-eks/test/awsauth.test.ts +++ b/packages/@aws-cdk/aws-eks/test/awsauth.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as iam from '@aws-cdk/aws-iam'; import * as cdk from '@aws-cdk/core'; import { Cluster, KubernetesManifest, KubernetesVersion } from '../lib'; @@ -58,7 +58,7 @@ describe('aws auth', () => { new AwsAuth(stack, 'AwsAuth', { cluster }); // THEN - expect(stack).toHaveResource(KubernetesManifest.RESOURCE_TYPE, { + Template.fromStack(stack).hasResourceProperties(KubernetesManifest.RESOURCE_TYPE, { Manifest: JSON.stringify([{ apiVersion: 'v1', kind: 'ConfigMap', @@ -85,8 +85,8 @@ describe('aws auth', () => { cluster.awsAuth.addAccount('5566776655'); // THEN - expect(stack).toCountResources(KubernetesManifest.RESOURCE_TYPE, 1); - expect(stack).toHaveResource(KubernetesManifest.RESOURCE_TYPE, { + Template.fromStack(stack).resourceCountIs(KubernetesManifest.RESOURCE_TYPE, 1); + Template.fromStack(stack).hasResourceProperties(KubernetesManifest.RESOURCE_TYPE, { Manifest: { 'Fn::Join': [ '', @@ -175,7 +175,7 @@ describe('aws auth', () => { cluster.awsAuth.addUserMapping(user, { groups: ['group2'] }); // THEN - expect(stack).toHaveResource(KubernetesManifest.RESOURCE_TYPE, { + Template.fromStack(stack).hasResourceProperties(KubernetesManifest.RESOURCE_TYPE, { Manifest: { 'Fn::Join': [ '', @@ -236,7 +236,7 @@ describe('aws auth', () => { cluster.awsAuth.addMastersRole(role); // THEN - expect(stack).toHaveResource(KubernetesManifest.RESOURCE_TYPE, { + Template.fromStack(stack).hasResourceProperties(KubernetesManifest.RESOURCE_TYPE, { Manifest: { 'Fn::Join': [ '', diff --git a/packages/@aws-cdk/aws-eks/test/cluster.test.ts b/packages/@aws-cdk/aws-eks/test/cluster.test.ts index e3c32b1e345c2..77556af1a0da4 100644 --- a/packages/@aws-cdk/aws-eks/test/cluster.test.ts +++ b/packages/@aws-cdk/aws-eks/test/cluster.test.ts @@ -1,7 +1,6 @@ import * as fs from 'fs'; import * as path from 'path'; -import '@aws-cdk/assert-internal/jest'; -import { SynthUtils } from '@aws-cdk/assert-internal'; +import { Match, Template } from '@aws-cdk/assertions'; import * as asg from '@aws-cdk/aws-autoscaling'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as iam from '@aws-cdk/aws-iam'; @@ -32,7 +31,7 @@ describe('cluster', () => { }, }); - expect(stack).toHaveResource('Custom::AWSCDK-EKS-HelmChart', { + Template.fromStack(stack).hasResourceProperties('Custom::AWSCDK-EKS-HelmChart', { Chart: 'aws-load-balancer-controller', }); expect(cluster.albController).toBeDefined(); @@ -51,8 +50,9 @@ describe('cluster', () => { const nested = stack.node.tryFindChild('@aws-cdk/aws-eks.ClusterResourceProvider') as cdk.NestedStack; - const template = SynthUtils.toCloudFormation(nested); - expect(template.Resources.OnEventHandler42BEBAE0.Properties.Environment).toEqual({ Variables: { foo: 'bar' } }); + Template.fromStack(nested).hasResourceProperties('AWS::Lambda::Function', { + Environment: { Variables: { foo: 'bar' } }, + }); }); test('can specify security group to cluster resource handler', () => { @@ -70,8 +70,11 @@ describe('cluster', () => { const nested = stack.node.tryFindChild('@aws-cdk/aws-eks.ClusterResourceProvider') as cdk.NestedStack; - const template = SynthUtils.toCloudFormation(nested); - expect(template.Resources.OnEventHandler42BEBAE0.Properties.VpcConfig.SecurityGroupIds).toEqual([{ Ref: 'referencetoStackProxyInstanceSG80B79D87GroupId' }]); + Template.fromStack(nested).hasResourceProperties('AWS::Lambda::Function', { + VpcConfig: { + SecurityGroupIds: [{ Ref: 'referencetoStackProxyInstanceSG80B79D87GroupId' }], + }, + }); }); test('throws when trying to place cluster handlers in a vpc with no private subnets', () => { @@ -150,7 +153,7 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResourceLike('Custom::AWSCDK-EKS-Cluster', { + Template.fromStack(stack).hasResourceProperties('Custom::AWSCDK-EKS-Cluster', { Config: { resourcesVpcConfig: { subnetIds: { @@ -162,8 +165,6 @@ describe('cluster', () => { }, }, }); - - }); }); @@ -176,8 +177,6 @@ describe('cluster', () => { }); expect(() => cluster.clusterSecurityGroup).toThrow(/"clusterSecurityGroup" is not defined for this imported cluster/); - - }); test('can place cluster handlers in the cluster vpc', () => { @@ -190,10 +189,11 @@ describe('cluster', () => { }); const nested = stack.node.tryFindChild('@aws-cdk/aws-eks.ClusterResourceProvider') as cdk.NestedStack; - const template = SynthUtils.toCloudFormation(nested); + const template = Template.fromStack(nested); + const resources = template.findResources('AWS::Lambda::Function'); function assertFunctionPlacedInVpc(id: string) { - expect(template.Resources[id].Properties.VpcConfig.SubnetIds).toEqual([ + expect(resources[id].Properties.VpcConfig.SubnetIds).toEqual([ { Ref: 'referencetoStackClusterDefaultVpcPrivateSubnet1SubnetA64D1BF0Ref' }, { Ref: 'referencetoStackClusterDefaultVpcPrivateSubnet2Subnet32D85AB8Ref' }, ]); @@ -204,8 +204,6 @@ describe('cluster', () => { assertFunctionPlacedInVpc('ProviderframeworkonEvent83C1D0A7'); assertFunctionPlacedInVpc('ProviderframeworkisComplete26D7B0CB'); assertFunctionPlacedInVpc('ProviderframeworkonTimeout0B47CA38'); - - }); test('can access cluster security group for imported cluster with cluster security group id', () => { @@ -241,13 +239,12 @@ describe('cluster', () => { instanceType: new ec2.InstanceType('t2.medium'), }); - const template = SynthUtils.toCloudFormation(stack); - expect(template.Resources.ClusterselfmanagedLaunchConfigA5B57EF6.Properties.SecurityGroups).toEqual([ - { 'Fn::GetAtt': ['ClusterselfmanagedInstanceSecurityGroup64468C3A', 'GroupId'] }, - { 'Fn::GetAtt': ['Cluster9EE0221C', 'ClusterSecurityGroupId'] }, - ]); - - + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::LaunchConfiguration', { + SecurityGroups: [ + { 'Fn::GetAtt': ['ClusterselfmanagedInstanceSecurityGroup64468C3A', 'GroupId'] }, + { 'Fn::GetAtt': ['Cluster9EE0221C', 'ClusterSecurityGroupId'] }, + ], + }); }); test('security group of self-managed asg is not tagged with owned', () => { @@ -264,11 +261,10 @@ describe('cluster', () => { instanceType: new ec2.InstanceType('t2.medium'), }); - const template = SynthUtils.toCloudFormation(stack); - // make sure the "kubernetes.io/cluster/: owned" tag isn't here. - expect(template.Resources.ClusterselfmanagedInstanceSecurityGroup64468C3A.Properties.Tags).toEqual([ - { Key: 'Name', Value: 'Stack/Cluster/self-managed' }, - ]); + let template = Template.fromStack(stack); + template.hasResourceProperties('AWS::EC2::SecurityGroup', { + Tags: [{ Key: 'Name', Value: 'Stack/Cluster/self-managed' }], + }); }); test('connect autoscaling group with imported cluster', () => { @@ -296,11 +292,12 @@ describe('cluster', () => { // WHEN importedCluster.connectAutoScalingGroupCapacity(selfManaged, {}); - const template = SynthUtils.toCloudFormation(stack); - expect(template.Resources.selfmanagedLaunchConfigD41289EB.Properties.SecurityGroups).toEqual([ - { 'Fn::GetAtt': ['selfmanagedInstanceSecurityGroupEA6D80C9', 'GroupId'] }, - { 'Fn::GetAtt': ['Cluster9EE0221C', 'ClusterSecurityGroupId'] }, - ]); + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::LaunchConfiguration', { + SecurityGroups: [ + { 'Fn::GetAtt': ['selfmanagedInstanceSecurityGroupEA6D80C9', 'GroupId'] }, + { 'Fn::GetAtt': ['Cluster9EE0221C', 'ClusterSecurityGroupId'] }, + ], + }); }); test('cluster security group is attached when connecting self-managed nodes', () => { @@ -323,13 +320,12 @@ describe('cluster', () => { // WHEN cluster.connectAutoScalingGroupCapacity(selfManaged, {}); - const template = SynthUtils.toCloudFormation(stack); - expect(template.Resources.selfmanagedLaunchConfigD41289EB.Properties.SecurityGroups).toEqual([ - { 'Fn::GetAtt': ['selfmanagedInstanceSecurityGroupEA6D80C9', 'GroupId'] }, - { 'Fn::GetAtt': ['Cluster9EE0221C', 'ClusterSecurityGroupId'] }, - ]); - - + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::LaunchConfiguration', { + SecurityGroups: [ + { 'Fn::GetAtt': ['selfmanagedInstanceSecurityGroupEA6D80C9', 'GroupId'] }, + { 'Fn::GetAtt': ['Cluster9EE0221C', 'ClusterSecurityGroupId'] }, + ], + }); }); test('spot interrupt handler is not added if spotInterruptHandler is false when connecting self-managed nodes', () => { @@ -370,8 +366,6 @@ describe('cluster', () => { const someConstruct = new constructs.Construct(stack, 'SomeConstruct'); expect(() => cluster.addCdk8sChart('chart', someConstruct)).toThrow(/Invalid cdk8s chart. Must contain a \'toJson\' method, but found undefined/); - - }); test('throws when a core construct is added as cdk8s chart', () => { @@ -387,8 +381,6 @@ describe('cluster', () => { const someConstruct = new cdk.Construct(stack, 'SomeConstruct'); expect(() => cluster.addCdk8sChart('chart', someConstruct)).toThrow(/Invalid cdk8s chart. Must contain a \'toJson\' method, but found undefined/); - - }); test('cdk8s chart can be added to cluster', () => { @@ -417,7 +409,7 @@ describe('cluster', () => { cluster.addCdk8sChart('cdk8s-chart', chart); - expect(stack).toHaveResourceLike('Custom::AWSCDK-EKS-KubernetesResource', { + Template.fromStack(stack).hasResourceProperties('Custom::AWSCDK-EKS-KubernetesResource', { Manifest: { 'Fn::Join': [ '', @@ -431,8 +423,6 @@ describe('cluster', () => { ], }, }); - - }); test('cluster connections include both control plane and cluster security group', () => { @@ -487,8 +477,6 @@ describe('cluster', () => { // make sure we can synth (no circular dependencies between the stacks) app.synth(); - - }); test('can declare a manifest with a token from a different stack than the cluster that depends on the cluster stack', () => { @@ -706,7 +694,7 @@ describe('cluster', () => { new eks.Cluster(stack, 'Cluster', { vpc, defaultCapacity: 0, version: CLUSTER_VERSION, prune: false }); // THEN - expect(stack).toHaveResourceLike('Custom::AWSCDK-EKS-Cluster', { + Template.fromStack(stack).hasResourceProperties('Custom::AWSCDK-EKS-Cluster', { Config: { roleArn: { 'Fn::GetAtt': ['ClusterRoleFA261979', 'Arn'] }, version: '1.21', @@ -733,7 +721,7 @@ describe('cluster', () => { new eks.Cluster(stack, 'cluster', { version: CLUSTER_VERSION, prune: false }); // THEN - expect(stack).toHaveResource('AWS::EC2::VPC'); + Template.fromStack(stack).hasResourceProperties('AWS::EC2::VPC', Match.anyValue()); }); @@ -748,7 +736,7 @@ describe('cluster', () => { // THEN expect(cluster.defaultNodegroup).toBeDefined(); - expect(stack).toHaveResource('AWS::EKS::Nodegroup', { + Template.fromStack(stack).hasResourceProperties('AWS::EKS::Nodegroup', { InstanceTypes: [ 'm5.large', ], @@ -775,7 +763,7 @@ describe('cluster', () => { // THEN expect(cluster.defaultNodegroup).toBeDefined(); - expect(stack).toHaveResource('AWS::EKS::Nodegroup', { + Template.fromStack(stack).hasResourceProperties('AWS::EKS::Nodegroup', { ScalingConfig: { DesiredSize: 10, MaxSize: 10, @@ -795,8 +783,8 @@ describe('cluster', () => { // THEN expect(cluster.defaultCapacity).toBeUndefined(); - expect(stack).not.toHaveResource('AWS::AutoScaling::AutoScalingGroup'); - expect(stack).not.toHaveResource('AWS::AutoScaling::LaunchConfiguration'); + Template.fromStack(stack).resourceCountIs('AWS::AutoScaling::AutoScalingGroup', 0); + Template.fromStack(stack).resourceCountIs('AWS::AutoScaling::LaunchConfiguration', 0); }); }); @@ -809,7 +797,7 @@ describe('cluster', () => { new eks.Cluster(stack, 'Cluster', { vpc, defaultCapacity: 0, version: CLUSTER_VERSION, prune: false }); // THEN - expect(stack).toHaveResource('AWS::EC2::Subnet', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::Subnet', { Tags: [ { Key: 'aws-cdk:subnet-name', Value: 'Private' }, { Key: 'aws-cdk:subnet-type', Value: 'Private' }, @@ -829,7 +817,7 @@ describe('cluster', () => { new eks.Cluster(stack, 'Cluster', { vpc, defaultCapacity: 0, version: CLUSTER_VERSION, prune: false }); // THEN - expect(stack).toHaveResource('AWS::EC2::Subnet', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::Subnet', { MapPublicIpOnLaunch: true, Tags: [ { Key: 'aws-cdk:subnet-name', Value: 'Public' }, @@ -857,9 +845,9 @@ describe('cluster', () => { instanceType: new ec2.InstanceType('t2.medium'), }); - const template = SynthUtils.toCloudFormation(stack); - expect(template.Resources.ClusterASG0E4BA723.UpdatePolicy).toEqual({ AutoScalingScheduledAction: { IgnoreUnmodifiedGroupSizeProperties: true } }); - + Template.fromStack(stack).hasResource('AWS::AutoScaling::AutoScalingGroup', { + UpdatePolicy: { AutoScalingScheduledAction: { IgnoreUnmodifiedGroupSizeProperties: true } }, + }); }); test('adding capacity creates an ASG with tags', () => { @@ -878,7 +866,7 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResource('AWS::AutoScaling::AutoScalingGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::AutoScalingGroup', { Tags: [ { Key: { 'Fn::Join': ['', ['kubernetes.io/cluster/', { Ref: 'Cluster9EE0221C' }]] }, @@ -919,7 +907,7 @@ describe('cluster', () => { // THEN expect(cluster.defaultNodegroup).toBeDefined(); - expect(stack).toHaveResource('AWS::EKS::Nodegroup', { + Template.fromStack(stack).hasResourceProperties('AWS::EKS::Nodegroup', { ScalingConfig: { DesiredSize: 10, MaxSize: 10, @@ -946,7 +934,7 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResource('AWS::AutoScaling::AutoScalingGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::AutoScalingGroup', { Tags: [ { Key: { 'Fn::Join': ['', ['kubernetes.io/cluster/', { Ref: 'Cluster9EE0221C' }]] }, @@ -1000,67 +988,112 @@ describe('cluster', () => { expect(cluster.kubectlProvider).toEqual(kubectlProvider); }); - test('import cluster with existing kubectl provider function should work as expected with resources relying on kubectl getOrCreate', () => { + describe('import cluster with existing kubectl provider function should work as expected with resources relying on kubectl getOrCreate', () => { + test('creates helm chart', () => { + const { stack } = testFixture(); - const { stack } = testFixture(); + const handlerRole = iam.Role.fromRoleArn(stack, 'HandlerRole', 'arn:aws:iam::123456789012:role/lambda-role'); + const kubectlProvider = KubectlProvider.fromKubectlProviderAttributes(stack, 'KubectlProvider', { + functionArn: 'arn:aws:lambda:us-east-2:123456789012:function:my-function:1', + kubectlRoleArn: 'arn:aws:iam::123456789012:role/kubectl-role', + handlerRole: handlerRole, + }); - const handlerRole = iam.Role.fromRoleArn(stack, 'HandlerRole', 'arn:aws:iam::123456789012:role/lambda-role'); - const kubectlProvider = KubectlProvider.fromKubectlProviderAttributes(stack, 'KubectlProvider', { - functionArn: 'arn:aws:lambda:us-east-2:123456789012:function:my-function:1', - kubectlRoleArn: 'arn:aws:iam::123456789012:role/kubectl-role', - handlerRole: handlerRole, - }); + const cluster = eks.Cluster.fromClusterAttributes(stack, 'Cluster', { + clusterName: 'cluster', + kubectlProvider: kubectlProvider, + }); - const cluster = eks.Cluster.fromClusterAttributes(stack, 'Cluster', { - clusterName: 'cluster', - kubectlProvider: kubectlProvider, - }); + new eks.HelmChart(stack, 'Chart', { + cluster: cluster, + chart: 'chart', + }); - new eks.HelmChart(stack, 'Chart', { - cluster: cluster, - chart: 'chart', + Template.fromStack(stack).hasResourceProperties('Custom::AWSCDK-EKS-HelmChart', { + ServiceToken: kubectlProvider.serviceToken, + RoleArn: kubectlProvider.roleArn, + }); }); - expect(stack).toHaveResourceLike('Custom::AWSCDK-EKS-HelmChart', { - ServiceToken: kubectlProvider.serviceToken, - RoleArn: kubectlProvider.roleArn, - }); + test('creates Kubernetes patch', () => { + const { stack } = testFixture(); - new eks.KubernetesPatch(stack, 'Patch', { - cluster: cluster, - applyPatch: {}, - restorePatch: {}, - resourceName: 'PatchResource', - }); + const handlerRole = iam.Role.fromRoleArn(stack, 'HandlerRole', 'arn:aws:iam::123456789012:role/lambda-role'); + const kubectlProvider = KubectlProvider.fromKubectlProviderAttributes(stack, 'KubectlProvider', { + functionArn: 'arn:aws:lambda:us-east-2:123456789012:function:my-function:1', + kubectlRoleArn: 'arn:aws:iam::123456789012:role/kubectl-role', + handlerRole: handlerRole, + }); - expect(stack).toHaveResourceLike('Custom::AWSCDK-EKS-KubernetesPatch', { - ServiceToken: kubectlProvider.serviceToken, - RoleArn: kubectlProvider.roleArn, - }); + const cluster = eks.Cluster.fromClusterAttributes(stack, 'Cluster', { + clusterName: 'cluster', + kubectlProvider: kubectlProvider, + }); - new eks.KubernetesManifest(stack, 'Manifest', { - cluster: cluster, - manifest: [], - }); + new eks.HelmChart(stack, 'Chart', { + cluster: cluster, + chart: 'chart', + }); - expect(stack).toHaveResourceLike('Custom::AWSCDK-EKS-KubernetesResource', { - ServiceToken: kubectlProvider.serviceToken, - RoleArn: kubectlProvider.roleArn, - }); + new eks.KubernetesPatch(stack, 'Patch', { + cluster: cluster, + applyPatch: {}, + restorePatch: {}, + resourceName: 'PatchResource', + }); - new eks.KubernetesObjectValue(stack, 'ObjectValue', { - cluster: cluster, - jsonPath: '', - objectName: 'name', - objectType: 'type', + Template.fromStack(stack).hasResourceProperties('Custom::AWSCDK-EKS-KubernetesPatch', { + ServiceToken: kubectlProvider.serviceToken, + RoleArn: kubectlProvider.roleArn, + }); }); - expect(stack).toHaveResourceLike('Custom::AWSCDK-EKS-KubernetesObjectValue', { - ServiceToken: kubectlProvider.serviceToken, - RoleArn: kubectlProvider.roleArn, - }); + test('creates Kubernetes object value', () => { + const { stack } = testFixture(); + + const handlerRole = iam.Role.fromRoleArn(stack, 'HandlerRole', 'arn:aws:iam::123456789012:role/lambda-role'); + const kubectlProvider = KubectlProvider.fromKubectlProviderAttributes(stack, 'KubectlProvider', { + functionArn: 'arn:aws:lambda:us-east-2:123456789012:function:my-function:1', + kubectlRoleArn: 'arn:aws:iam::123456789012:role/kubectl-role', + handlerRole: handlerRole, + }); + + const cluster = eks.Cluster.fromClusterAttributes(stack, 'Cluster', { + clusterName: 'cluster', + kubectlProvider: kubectlProvider, + }); + + new eks.HelmChart(stack, 'Chart', { + cluster: cluster, + chart: 'chart', + }); + + new eks.KubernetesPatch(stack, 'Patch', { + cluster: cluster, + applyPatch: {}, + restorePatch: {}, + resourceName: 'PatchResource', + }); + + new eks.KubernetesManifest(stack, 'Manifest', { + cluster: cluster, + manifest: [], + }); + + new eks.KubernetesObjectValue(stack, 'ObjectValue', { + cluster: cluster, + jsonPath: '', + objectName: 'name', + objectType: 'type', + }); - expect(cluster.kubectlProvider).not.toBeInstanceOf(eks.KubectlProvider); + Template.fromStack(stack).hasResourceProperties('Custom::AWSCDK-EKS-KubernetesObjectValue', { + ServiceToken: kubectlProvider.serviceToken, + RoleArn: kubectlProvider.roleArn, + }); + + expect(cluster.kubectlProvider).not.toBeInstanceOf(eks.KubectlProvider); + }); }); test('import cluster with new kubectl private subnets', () => { @@ -1111,7 +1144,7 @@ describe('cluster', () => { new cdk.CfnOutput(stack2, 'ClusterARN', { value: imported.clusterArn }); // THEN - expect(stack2).toMatchTemplate({ + Template.fromStack(stack2).templateMatches({ Outputs: { ClusterARN: { Value: { @@ -1154,7 +1187,7 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResource(eks.KubernetesManifest.RESOURCE_TYPE, { + Template.fromStack(stack).hasResourceProperties(eks.KubernetesManifest.RESOURCE_TYPE, { Manifest: { 'Fn::Join': [ '', @@ -1197,11 +1230,11 @@ describe('cluster', () => { cluster.addManifest('manifest2', { bar: 123 }, { boor: [1, 2, 3] }); // THEN - expect(stack).toHaveResource(eks.KubernetesManifest.RESOURCE_TYPE, { + Template.fromStack(stack).hasResourceProperties(eks.KubernetesManifest.RESOURCE_TYPE, { Manifest: '[{"foo":123}]', }); - expect(stack).toHaveResource(eks.KubernetesManifest.RESOURCE_TYPE, { + Template.fromStack(stack).hasResourceProperties(eks.KubernetesManifest.RESOURCE_TYPE, { Manifest: '[{"bar":123},{"boor":[1,2,3]}]', }); @@ -1224,7 +1257,7 @@ describe('cluster', () => { app.synth(); // no cyclic dependency (see https://github.com/aws/aws-cdk/issues/7231) // expect a single resource in the 2nd stack - expect(stack2).toMatchTemplate({ + Template.fromStack(stack2).templateMatches({ Resources: { myresource49C6D325: { Type: 'Custom::AWSCDK-EKS-KubernetesResource', @@ -1261,7 +1294,7 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResource(eks.KubernetesManifest.RESOURCE_TYPE, { + Template.fromStack(stack).hasResourceProperties(eks.KubernetesManifest.RESOURCE_TYPE, { Manifest: { 'Fn::Join': [ '', @@ -1313,7 +1346,7 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResource(eks.KubernetesManifest.RESOURCE_TYPE, { + Template.fromStack(stack).hasResourceProperties(eks.KubernetesManifest.RESOURCE_TYPE, { Manifest: { 'Fn::Join': [ '', @@ -1531,7 +1564,7 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResource(eks.HelmChart.RESOURCE_TYPE, { + Template.fromStack(stack).hasResourceProperties(eks.HelmChart.RESOURCE_TYPE, { Release: 'stackclusterchartspotinterrupthandlerdec62e07', Chart: 'aws-node-termination-handler', Values: '{\"nodeSelector\":{\"lifecycle\":\"Ec2Spot\"}}', @@ -1575,7 +1608,7 @@ describe('cluster', () => { }); // THEN - expect(stack).toCountResources(eks.HelmChart.RESOURCE_TYPE, 1); + Template.fromStack(stack).resourceCountIs(eks.HelmChart.RESOURCE_TYPE, 1); }); @@ -1653,7 +1686,7 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::EKS::Nodegroup', { + Template.fromStack(stack).hasResourceProperties('AWS::EKS::Nodegroup', { AmiType: 'AL2_ARM_64', }); @@ -1674,7 +1707,7 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::EKS::Nodegroup', { + Template.fromStack(stack).hasResourceProperties('AWS::EKS::Nodegroup', { AmiType: 'AL2_ARM_64', }); @@ -1695,7 +1728,7 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::EKS::Nodegroup', { + Template.fromStack(stack).hasResourceProperties('AWS::EKS::Nodegroup', { AmiType: 'AL2_ARM_64', }); @@ -1801,7 +1834,7 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResource('Custom::AWSCDK-EKS-Cluster', { + Template.fromStack(stack).hasResourceProperties('Custom::AWSCDK-EKS-Cluster', { Config: { name: 'my-cluster-name', roleArn: { 'Fn::GetAtt': ['MyClusterRoleBA20FE72', 'Arn'] }, @@ -1823,7 +1856,7 @@ describe('cluster', () => { }); // role can be assumed by 3 lambda handlers (2 for the cluster resource and 1 for the kubernetes resource) - expect(stack).toHaveResource('AWS::IAM::Role', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { AssumeRolePolicyDocument: { Statement: [ { @@ -1844,7 +1877,7 @@ describe('cluster', () => { }); // policy allows creation role to pass the cluster role and to interact with the cluster (given we know the explicit cluster name) - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -1963,7 +1996,7 @@ describe('cluster', () => { new eks.Cluster(stack, 'MyCluster', { version: CLUSTER_VERSION, prune: false }); // THEN - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -2046,7 +2079,7 @@ describe('cluster', () => { // THEN const providerStack = stack.node.tryFindChild('@aws-cdk/aws-eks.KubectlProvider') as cdk.NestedStack; - expect(providerStack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(providerStack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -2089,7 +2122,7 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResource('Custom::AWSCDK-EKS-KubernetesPatch', { + Template.fromStack(stack).hasResourceProperties('Custom::AWSCDK-EKS-KubernetesPatch', { ResourceName: 'deployment/coredns', ResourceNamespace: 'kube-system', ApplyPatchJson: '{"spec":{"template":{"metadata":{"annotations":{"eks.amazonaws.com/compute-type":"fargate"}}}}}', @@ -2116,7 +2149,7 @@ describe('cluster', () => { // THEN expect(provider).toEqual(cluster.openIdConnectProvider); - expect(stack).toHaveResource('Custom::AWSCDKOpenIdConnectProvider', { + Template.fromStack(stack).hasResourceProperties('Custom::AWSCDKOpenIdConnectProvider', { ServiceToken: { 'Fn::GetAtt': [ 'CustomAWSCDKOpenIdConnectProviderCustomResourceProviderHandlerF2C543E0', @@ -2152,7 +2185,7 @@ describe('cluster', () => { const sanitized = YAML.parse(fileContents); // THEN - expect(stack).toHaveResource(eks.KubernetesManifest.RESOURCE_TYPE, { + Template.fromStack(stack).hasResourceProperties(eks.KubernetesManifest.RESOURCE_TYPE, { Manifest: JSON.stringify([sanitized]), }); @@ -2219,7 +2252,7 @@ describe('cluster', () => { // THEN const providerStack = stack.node.tryFindChild('@aws-cdk/aws-eks.KubectlProvider') as cdk.NestedStack; - expect(providerStack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(providerStack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -2260,11 +2293,11 @@ describe('cluster', () => { // the kubectl provider is inside a nested stack. const nested = stack.node.tryFindChild('@aws-cdk/aws-eks.KubectlProvider') as cdk.NestedStack; - const template = SynthUtils.toCloudFormation(nested); - expect(template.Resources.ProviderframeworkonEvent83C1D0A7.Properties.VpcConfig.SecurityGroupIds).toEqual( - [{ Ref: 'referencetoStackCluster18DFEAC17ClusterSecurityGroupId' }]); - - + Template.fromStack(nested).hasResourceProperties('AWS::Lambda::Function', { + VpcConfig: { + SecurityGroupIds: [{ Ref: 'referencetoStackCluster18DFEAC17ClusterSecurityGroupId' }], + }, + }); }); test('kubectl provider passes environment to lambda', () => { @@ -2293,7 +2326,7 @@ describe('cluster', () => { // the kubectl provider is inside a nested stack. const nested = stack.node.tryFindChild('@aws-cdk/aws-eks.KubectlProvider') as cdk.NestedStack; - expect(nested).toHaveResource('AWS::Lambda::Function', { + Template.fromStack(nested).hasResourceProperties('AWS::Lambda::Function', { Environment: { Variables: { Foo: 'Bar', @@ -2332,7 +2365,7 @@ describe('cluster', () => { // the kubectl provider is inside a nested stack. const nested = stack.node.tryFindChild('@aws-cdk/aws-eks.KubectlProvider') as cdk.NestedStack; - expect(nested).toHaveResourceLike('AWS::Lambda::Function', { + Template.fromStack(nested).hasResourceProperties('AWS::Lambda::Function', { Role: { Ref: 'referencetoStackKubectlIamRole02F8947EArn', }, @@ -2362,12 +2395,12 @@ describe('cluster', () => { }); const nested = stack.node.tryFindChild('@aws-cdk/aws-eks.KubectlProvider') as cdk.NestedStack; - const template = SynthUtils.toCloudFormation(nested); // we don't attach vpc config in case endpoint is public only, regardless of whether // the vpc has private subnets or not. - expect(template.Resources.Handler886CB40B.Properties.VpcConfig).toEqual(undefined); - + Template.fromStack(nested).hasResourceProperties('AWS::Lambda::Function', { + VpcConfig: Match.absent(), + }); }); @@ -2382,13 +2415,12 @@ describe('cluster', () => { }); const nested = stack.node.tryFindChild('@aws-cdk/aws-eks.KubectlProvider') as cdk.NestedStack; - const template = SynthUtils.toCloudFormation(nested); // we don't attach vpc config in case endpoint is public only, regardless of whether // the vpc has private subnets or not. - expect(template.Resources.Handler886CB40B.Properties.VpcConfig).toEqual(undefined); - - + Template.fromStack(nested).hasResourceProperties('AWS::Lambda::Function', { + VpcConfig: Match.absent(), + }); }); test('private without private subnets', () => { @@ -2417,13 +2449,10 @@ describe('cluster', () => { }); const nested = stack.node.tryFindChild('@aws-cdk/aws-eks.KubectlProvider') as cdk.NestedStack; - const template = SynthUtils.toCloudFormation(nested); - - // handler should have vpc config - expect(template.Resources.Handler886CB40B.Properties.VpcConfig.SubnetIds.length).not.toEqual(0); - expect(template.Resources.Handler886CB40B.Properties.VpcConfig.SecurityGroupIds.length).not.toEqual(0); - + const functions = Template.fromStack(nested).findResources('AWS::Lambda::Function'); + expect(functions.Handler886CB40B.Properties.VpcConfig.SubnetIds.length).not.toEqual(0); + expect(functions.Handler886CB40B.Properties.VpcConfig.SecurityGroupIds.length).not.toEqual(0); }); test('private and non restricted public without private subnets', () => { @@ -2437,12 +2466,12 @@ describe('cluster', () => { }); const nested = stack.node.tryFindChild('@aws-cdk/aws-eks.KubectlProvider') as cdk.NestedStack; - const template = SynthUtils.toCloudFormation(nested); // we don't have private subnets, but we don't need them since public access // is not restricted. - expect(template.Resources.Handler886CB40B.Properties.VpcConfig).toEqual(undefined); - + Template.fromStack(nested).hasResourceProperties('AWS::Lambda::Function', { + VpcConfig: Match.absent(), + }); }); @@ -2456,13 +2485,11 @@ describe('cluster', () => { }); const nested = stack.node.tryFindChild('@aws-cdk/aws-eks.KubectlProvider') as cdk.NestedStack; - const template = SynthUtils.toCloudFormation(nested); // we have private subnets so we should use them. - expect(template.Resources.Handler886CB40B.Properties.VpcConfig.SubnetIds.length).not.toEqual(0); - expect(template.Resources.Handler886CB40B.Properties.VpcConfig.SecurityGroupIds.length).not.toEqual(0); - - + const functions = Template.fromStack(nested).findResources('AWS::Lambda::Function'); + expect(functions.Handler886CB40B.Properties.VpcConfig.SubnetIds.length).not.toEqual(0); + expect(functions.Handler886CB40B.Properties.VpcConfig.SecurityGroupIds.length).not.toEqual(0); }); test('private and restricted public without private subnets', () => { @@ -2490,12 +2517,11 @@ describe('cluster', () => { }); const nested = stack.node.tryFindChild('@aws-cdk/aws-eks.KubectlProvider') as cdk.NestedStack; - const template = SynthUtils.toCloudFormation(nested); // we have private subnets so we should use them. - expect(template.Resources.Handler886CB40B.Properties.VpcConfig.SubnetIds.length).not.toEqual(0); - expect(template.Resources.Handler886CB40B.Properties.VpcConfig.SecurityGroupIds.length).not.toEqual(0); - + const functions = Template.fromStack(nested).findResources('AWS::Lambda::Function'); + expect(functions.Handler886CB40B.Properties.VpcConfig.SubnetIds.length).not.toEqual(0); + expect(functions.Handler886CB40B.Properties.VpcConfig.SecurityGroupIds.length).not.toEqual(0); }); @@ -2552,13 +2578,9 @@ describe('cluster', () => { }); const nested = stack.node.tryFindChild('@aws-cdk/aws-eks.KubectlProvider') as cdk.NestedStack; - const template = SynthUtils.toCloudFormation(nested); - - expect(template.Resources.Handler886CB40B.Properties.VpcConfig.SubnetIds).toEqual([ - 'subnet-private-in-us-east-1a', - ]); - - + Template.fromStack(nested).hasResourceProperties('AWS::Lambda::Function', { + VpcConfig: { SubnetIds: ['subnet-private-in-us-east-1a'] }, + }); }); test('private endpoint access selects only private subnets from looked up vpc with concrete subnet selection', () => { @@ -2620,13 +2642,9 @@ describe('cluster', () => { }); const nested = stack.node.tryFindChild('@aws-cdk/aws-eks.KubectlProvider') as cdk.NestedStack; - const template = SynthUtils.toCloudFormation(nested); - - expect(template.Resources.Handler886CB40B.Properties.VpcConfig.SubnetIds).toEqual([ - 'subnet-private-in-us-east-1a', - ]); - - + Template.fromStack(nested).hasResourceProperties('AWS::Lambda::Function', { + VpcConfig: { SubnetIds: ['subnet-private-in-us-east-1a'] }, + }); }); test('private endpoint access selects only private subnets from managed vpc with concrete subnet selection', () => { @@ -2650,14 +2668,14 @@ describe('cluster', () => { }); const nested = stack.node.tryFindChild('@aws-cdk/aws-eks.KubectlProvider') as cdk.NestedStack; - const template = SynthUtils.toCloudFormation(nested); - - expect(template.Resources.Handler886CB40B.Properties.VpcConfig.SubnetIds).toEqual([ - { Ref: 'referencetoStackVpcPrivateSubnet1Subnet8E6A14CBRef' }, - 'subnet-unknown', - ]); - - + Template.fromStack(nested).hasResourceProperties('AWS::Lambda::Function', { + VpcConfig: { + SubnetIds: [ + { Ref: 'referencetoStackVpcPrivateSubnet1Subnet8E6A14CBRef' }, + 'subnet-unknown', + ], + }, + }); }); test('private endpoint access considers specific subnet selection', () => { @@ -2676,13 +2694,9 @@ describe('cluster', () => { }); const nested = stack.node.tryFindChild('@aws-cdk/aws-eks.KubectlProvider') as cdk.NestedStack; - const template = SynthUtils.toCloudFormation(nested); - - expect(template.Resources.Handler886CB40B.Properties.VpcConfig.SubnetIds).toEqual([ - 'subnet1', - ]); - - + Template.fromStack(nested).hasResourceProperties('AWS::Lambda::Function', { + VpcConfig: { SubnetIds: ['subnet1'] }, + }); }); test('can configure private endpoint access', () => { @@ -2737,7 +2751,7 @@ describe('cluster', () => { // the kubectl provider is inside a nested stack. const nested = stack.node.tryFindChild('@aws-cdk/aws-eks.KubectlProvider') as cdk.NestedStack; - expect(nested).toHaveResource('AWS::Lambda::Function', { + Template.fromStack(nested).hasResourceProperties('AWS::Lambda::Function', { VpcConfig: { SecurityGroupIds: [ { @@ -2803,10 +2817,8 @@ describe('cluster', () => { // the kubectl provider is inside a nested stack. const nested = stack.node.tryFindChild('@aws-cdk/aws-eks.KubectlProvider') as cdk.NestedStack; - const template = SynthUtils.toCloudFormation(nested); - expect(16).toEqual(template.Resources.Handler886CB40B.Properties.VpcConfig.SubnetIds.length); - - + const functions = Template.fromStack(nested).findResources('AWS::Lambda::Function'); + expect(functions.Handler886CB40B.Properties.VpcConfig.SubnetIds.length).toEqual(16); }); test('kubectl provider considers vpc subnet selection', () => { @@ -2855,7 +2867,7 @@ describe('cluster', () => { // the kubectl provider is inside a nested stack. const nested = stack.node.tryFindChild('@aws-cdk/aws-eks.KubectlProvider') as cdk.NestedStack; - expect(nested).toHaveResource('AWS::Lambda::Function', { + Template.fromStack(nested).hasResourceProperties('AWS::Lambda::Function', { VpcConfig: { SecurityGroupIds: [ { @@ -2937,10 +2949,11 @@ describe('cluster', () => { const expectedKubernetesGetId = 'Cluster1myserviceLoadBalancerAddress198CCB03'; - const template = SynthUtils.toCloudFormation(stack); + let template = Template.fromStack(stack); + const resources = template.findResources('Custom::AWSCDK-EKS-KubernetesObjectValue'); // make sure the custom resource is created correctly - expect(template.Resources[expectedKubernetesGetId].Properties).toEqual({ + expect(resources[expectedKubernetesGetId].Properties).toEqual({ ServiceToken: { 'Fn::GetAtt': [ 'awscdkawseksKubectlProviderNestedStackawscdkawseksKubectlProviderNestedStackResourceA7AEBA6B', @@ -2964,8 +2977,9 @@ describe('cluster', () => { }); // make sure the attribute points to the expected custom resource and extracts the correct attribute - expect(template.Outputs.LoadBalancerAddress.Value).toEqual({ 'Fn::GetAtt': [expectedKubernetesGetId, 'Value'] }); - + template.hasOutput('LoadBalancerAddress', { + Value: { 'Fn::GetAtt': [expectedKubernetesGetId, 'Value'] }, + }); }); test('custom kubectl layer can be provided', () => { @@ -2982,7 +2996,7 @@ describe('cluster', () => { // THEN const providerStack = stack.node.tryFindChild('@aws-cdk/aws-eks.KubectlProvider') as cdk.NestedStack; - expect(providerStack).toHaveResource('AWS::Lambda::Function', { + Template.fromStack(providerStack).hasResourceProperties('AWS::Lambda::Function', { Layers: ['arn:of:layer'], }); @@ -3002,7 +3016,7 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResourceLike('Custom::AWSCDK-EKS-Cluster', { + Template.fromStack(stack).hasResourceProperties('Custom::AWSCDK-EKS-Cluster', { Config: { encryptionConfig: [{ provider: { @@ -3070,7 +3084,7 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResourceLike('Custom::AWSCDK-EKS-Cluster', { + Template.fromStack(stack).hasResourceProperties('Custom::AWSCDK-EKS-Cluster', { Config: { kubernetesNetworkConfig: { serviceIpv4Cidr: customCidr, diff --git a/packages/@aws-cdk/aws-eks/test/fargate.test.ts b/packages/@aws-cdk/aws-eks/test/fargate.test.ts index 776da61d1561a..ddd195c7574d4 100644 --- a/packages/@aws-cdk/aws-eks/test/fargate.test.ts +++ b/packages/@aws-cdk/aws-eks/test/fargate.test.ts @@ -1,5 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; -import { ResourcePart } from '@aws-cdk/assert-internal'; +import { Template } from '@aws-cdk/assertions'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as iam from '@aws-cdk/aws-iam'; import * as kms from '@aws-cdk/aws-kms'; @@ -21,7 +20,7 @@ describe('fargate', () => { }); // THEN - expect(stack).toHaveResource('Custom::AWSCDK-EKS-FargateProfile', { + Template.fromStack(stack).hasResourceProperties('Custom::AWSCDK-EKS-FargateProfile', { Config: { clusterName: { Ref: 'MyCluster8AD82BF8' }, podExecutionRoleArn: { 'Fn::GetAtt': ['MyClusterfargateprofileMyProfilePodExecutionRole4795C054', 'Arn'] }, @@ -43,7 +42,7 @@ describe('fargate', () => { }); // THEN - expect(stack).toHaveResource('Custom::AWSCDK-EKS-FargateProfile', { + Template.fromStack(stack).hasResourceProperties('Custom::AWSCDK-EKS-FargateProfile', { Config: { clusterName: { Ref: 'MyCluster8AD82BF8' }, podExecutionRoleArn: { 'Fn::GetAtt': ['MyClusterfargateprofileMyProfilePodExecutionRole4795C054', 'Arn'] }, @@ -67,7 +66,7 @@ describe('fargate', () => { }); // THEN - expect(stack).toHaveResource('Custom::AWSCDK-EKS-FargateProfile', { + Template.fromStack(stack).hasResourceProperties('Custom::AWSCDK-EKS-FargateProfile', { Config: { clusterName: { Ref: 'MyCluster8AD82BF8' }, podExecutionRoleArn: { 'Fn::GetAtt': ['MyRoleF48FFE04', 'Arn'] }, @@ -91,7 +90,7 @@ describe('fargate', () => { Tags.of(cluster).add('propTag', '123'); // THEN - expect(stack).toHaveResource('Custom::AWSCDK-EKS-FargateProfile', { + Template.fromStack(stack).hasResourceProperties('Custom::AWSCDK-EKS-FargateProfile', { Config: { selectors: [{ namespace: 'default' }], clusterName: { Ref: 'MyCluster8AD82BF8' }, @@ -122,7 +121,7 @@ describe('fargate', () => { }); // THEN - expect(stack).toHaveResource('Custom::AWSCDK-EKS-FargateProfile', { + Template.fromStack(stack).hasResourceProperties('Custom::AWSCDK-EKS-FargateProfile', { Config: { clusterName: { Ref: 'MyCluster8AD82BF8' }, podExecutionRoleArn: { 'Fn::GetAtt': ['MyClusterfargateprofileMyProfilePodExecutionRole4795C054', 'Arn'] }, @@ -161,7 +160,7 @@ describe('fargate', () => { new eks.FargateCluster(stack, 'FargateCluster', { version: CLUSTER_VERSION }); // THEN - expect(stack).toHaveResource('Custom::AWSCDK-EKS-KubernetesPatch', { + Template.fromStack(stack).hasResourceProperties('Custom::AWSCDK-EKS-KubernetesPatch', { ResourceName: 'deployment/coredns', ResourceNamespace: 'kube-system', ApplyPatchJson: '{"spec":{"template":{"metadata":{"annotations":{"eks.amazonaws.com/compute-type":"fargate"}}}}}', @@ -171,7 +170,7 @@ describe('fargate', () => { }, }); - expect(stack).toHaveResource('Custom::AWSCDK-EKS-FargateProfile', { + Template.fromStack(stack).hasResourceProperties('Custom::AWSCDK-EKS-FargateProfile', { Config: { clusterName: { Ref: 'FargateCluster019F03E8', @@ -204,7 +203,7 @@ describe('fargate', () => { }); // THEN - expect(stack).toHaveResource('Custom::AWSCDK-EKS-FargateProfile', { + Template.fromStack(stack).hasResourceProperties('Custom::AWSCDK-EKS-FargateProfile', { Config: { clusterName: { Ref: 'FargateCluster019F03E8', @@ -238,7 +237,7 @@ describe('fargate', () => { }); // THEN - expect(stack).toHaveResource('Custom::AWSCDK-EKS-FargateProfile', { + Template.fromStack(stack).hasResourceProperties('Custom::AWSCDK-EKS-FargateProfile', { Config: { clusterName: { Ref: 'FargateCluster019F03E8', @@ -272,14 +271,14 @@ describe('fargate', () => { }); // THEN - expect(stack).toHaveResource('Custom::AWSCDK-EKS-FargateProfile', { + Template.fromStack(stack).hasResourceProperties('Custom::AWSCDK-EKS-FargateProfile', { Config: { clusterName: { Ref: 'MyCluster8AD82BF8' }, podExecutionRoleArn: { 'Fn::GetAtt': ['MyClusterfargateprofileMyProfile1PodExecutionRole794E9E37', 'Arn'] }, selectors: [{ namespace: 'namespace1' }], }, }); - expect(stack).toHaveResource('Custom::AWSCDK-EKS-FargateProfile', { + Template.fromStack(stack).hasResource('Custom::AWSCDK-EKS-FargateProfile', { Properties: { ServiceToken: { 'Fn::GetAtt': [ @@ -298,7 +297,7 @@ describe('fargate', () => { 'MyClusterfargateprofileMyProfile1PodExecutionRole794E9E37', 'MyClusterfargateprofileMyProfile1879D501A', ], - }, ResourcePart.CompleteDefinition); + }); }); @@ -311,7 +310,7 @@ describe('fargate', () => { new eks.FargateCluster(stack, 'FargateCluster', { version: CLUSTER_VERSION }); // THEN - expect(stack).toHaveResource('Custom::AWSCDK-EKS-KubernetesResource', { + Template.fromStack(stack).hasResourceProperties('Custom::AWSCDK-EKS-KubernetesResource', { Manifest: { 'Fn::Join': [ '', @@ -353,7 +352,7 @@ describe('fargate', () => { new eks.FargateCluster(stack, 'FargateCluster', { version: CLUSTER_VERSION }); // THEN - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -442,7 +441,7 @@ describe('fargate', () => { }); // THEN - expect(stack).toHaveResourceLike('Custom::AWSCDK-EKS-Cluster', { + Template.fromStack(stack).hasResourceProperties('Custom::AWSCDK-EKS-Cluster', { Config: { encryptionConfig: [{ provider: { diff --git a/packages/@aws-cdk/aws-eks/test/helm-chart.test.ts b/packages/@aws-cdk/aws-eks/test/helm-chart.test.ts index cfc7961f5e5e5..5f0c7536a0872 100644 --- a/packages/@aws-cdk/aws-eks/test/helm-chart.test.ts +++ b/packages/@aws-cdk/aws-eks/test/helm-chart.test.ts @@ -1,5 +1,5 @@ -import '@aws-cdk/assert-internal/jest'; import * as path from 'path'; +import { Template } from '@aws-cdk/assertions'; import { Asset } from '@aws-cdk/aws-s3-assets'; import { Duration } from '@aws-cdk/core'; import * as eks from '../lib'; @@ -17,7 +17,7 @@ describe('helm chart', () => { new eks.HelmChart(stack, 'MyChart', { cluster, chart: 'chart' }); // THEN - expect(stack).toHaveResource(eks.HelmChart.RESOURCE_TYPE, { Namespace: 'default' }); + Template.fromStack(stack).hasResourceProperties(eks.HelmChart.RESOURCE_TYPE, { Namespace: 'default' }); }); test('should have a lowercase default release name', () => { // GIVEN @@ -27,7 +27,7 @@ describe('helm chart', () => { new eks.HelmChart(stack, 'MyChart', { cluster, chart: 'chart' }); // THEN - expect(stack).toHaveResource(eks.HelmChart.RESOURCE_TYPE, { + Template.fromStack(stack).hasResourceProperties(eks.HelmChart.RESOURCE_TYPE, { Release: 'stackmychartff398361', }); }); @@ -92,7 +92,7 @@ describe('helm chart', () => { new eks.HelmChart(stack, 'MyChart', { cluster, chartAsset }); // THEN - expect(stack).toHaveResource(eks.HelmChart.RESOURCE_TYPE, { + Template.fromStack(stack).hasResourceProperties(eks.HelmChart.RESOURCE_TYPE, { ChartAssetURL: { 'Fn::Join': [ '', @@ -144,7 +144,7 @@ describe('helm chart', () => { }); // THEN - expect(stack).toHaveResource(eks.HelmChart.RESOURCE_TYPE, { + Template.fromStack(stack).hasResourceProperties(eks.HelmChart.RESOURCE_TYPE, { Release: 'hismostprobablylongerthanfiftythreecharacterscaf15d09', }); }); @@ -156,7 +156,7 @@ describe('helm chart', () => { new eks.HelmChart(stack, 'MyChart', { cluster, chart: 'chart', values: { foo: 123 } }); // THEN - expect(stack).toHaveResource(eks.HelmChart.RESOURCE_TYPE, { Values: '{"foo":123}' }); + Template.fromStack(stack).hasResourceProperties(eks.HelmChart.RESOURCE_TYPE, { Values: '{"foo":123}' }); }); test('should support create namespaces by default', () => { // GIVEN @@ -166,7 +166,7 @@ describe('helm chart', () => { new eks.HelmChart(stack, 'MyChart', { cluster, chart: 'chart' }); // THEN - expect(stack).toHaveResource(eks.HelmChart.RESOURCE_TYPE, { CreateNamespace: true }); + Template.fromStack(stack).hasResourceProperties(eks.HelmChart.RESOURCE_TYPE, { CreateNamespace: true }); }); test('should support create namespaces when explicitly specified', () => { // GIVEN @@ -176,7 +176,7 @@ describe('helm chart', () => { new eks.HelmChart(stack, 'MyChart', { cluster, chart: 'chart', createNamespace: true }); // THEN - expect(stack).toHaveResource(eks.HelmChart.RESOURCE_TYPE, { CreateNamespace: true }); + Template.fromStack(stack).hasResourceProperties(eks.HelmChart.RESOURCE_TYPE, { CreateNamespace: true }); }); test('should not create namespaces when disabled', () => { // GIVEN @@ -186,7 +186,8 @@ describe('helm chart', () => { new eks.HelmChart(stack, 'MyChart', { cluster, chart: 'chart', createNamespace: false }); // THEN - expect(stack).not.toHaveResource(eks.HelmChart.RESOURCE_TYPE, { CreateNamespace: true }); + const charts = Template.fromStack(stack).findResources(eks.HelmChart.RESOURCE_TYPE, { CreateNamespace: true }); + expect(Object.keys(charts).length).toEqual(0); }); test('should support waiting until everything is completed before marking release as successful', () => { // GIVEN @@ -196,7 +197,7 @@ describe('helm chart', () => { new eks.HelmChart(stack, 'MyWaitingChart', { cluster, chart: 'chart', wait: true }); // THEN - expect(stack).toHaveResource(eks.HelmChart.RESOURCE_TYPE, { Wait: true }); + Template.fromStack(stack).hasResourceProperties(eks.HelmChart.RESOURCE_TYPE, { Wait: true }); }); test('should default to not waiting before marking release as successful', () => { // GIVEN @@ -206,7 +207,9 @@ describe('helm chart', () => { new eks.HelmChart(stack, 'MyWaitingChart', { cluster, chart: 'chart' }); // THEN - expect(stack).not.toHaveResource(eks.HelmChart.RESOURCE_TYPE, { Wait: true }); + const charts = Template.fromStack(stack).findResources(eks.HelmChart.RESOURCE_TYPE, { Wait: true }); + expect(Object.keys(charts).length).toEqual(0); + }); test('should enable waiting when specified', () => { // GIVEN @@ -216,7 +219,7 @@ describe('helm chart', () => { new eks.HelmChart(stack, 'MyWaitingChart', { cluster, chart: 'chart', wait: true }); // THEN - expect(stack).toHaveResource(eks.HelmChart.RESOURCE_TYPE, { Wait: true }); + Template.fromStack(stack).hasResourceProperties(eks.HelmChart.RESOURCE_TYPE, { Wait: true }); }); test('should disable waiting when specified as false', () => { // GIVEN @@ -226,7 +229,8 @@ describe('helm chart', () => { new eks.HelmChart(stack, 'MyWaitingChart', { cluster, chart: 'chart', wait: false }); // THEN - expect(stack).not.toHaveResource(eks.HelmChart.RESOURCE_TYPE, { Wait: true }); + const charts = Template.fromStack(stack).findResources(eks.HelmChart.RESOURCE_TYPE, { Wait: true }); + expect(Object.keys(charts).length).toEqual(0); }); test('should timeout only after 10 minutes', () => { @@ -241,7 +245,7 @@ describe('helm chart', () => { }); // THEN - expect(stack).toHaveResource(eks.HelmChart.RESOURCE_TYPE, { Timeout: '600s' }); + Template.fromStack(stack).hasResourceProperties(eks.HelmChart.RESOURCE_TYPE, { Timeout: '600s' }); }); }); }); diff --git a/packages/@aws-cdk/aws-eks/test/k8s-manifest.test.ts b/packages/@aws-cdk/aws-eks/test/k8s-manifest.test.ts index 9b0295c23efff..34b2a47cc6d14 100644 --- a/packages/@aws-cdk/aws-eks/test/k8s-manifest.test.ts +++ b/packages/@aws-cdk/aws-eks/test/k8s-manifest.test.ts @@ -1,5 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; -import { SynthUtils } from '@aws-cdk/assert-internal'; +import { Template } from '@aws-cdk/assertions'; import { CfnResource, Stack } from '@aws-cdk/core'; import { Cluster, KubernetesManifest, KubernetesVersion, HelmChart } from '../lib'; import { testFixtureNoVpc, testFixtureCluster } from './util'; @@ -72,7 +71,7 @@ describe('k8s manifest', () => { manifest, }); - expect(stack).toHaveResource(KubernetesManifest.RESOURCE_TYPE, { + Template.fromStack(stack).hasResourceProperties(KubernetesManifest.RESOURCE_TYPE, { Manifest: JSON.stringify(manifest), }); @@ -91,13 +90,13 @@ describe('k8s manifest', () => { cluster.addHelmChart('helm', { chart: 'hello-world' }); // THEN - expect(stack).toHaveResource(KubernetesManifest.RESOURCE_TYPE, { + Template.fromStack(stack).hasResourceProperties(KubernetesManifest.RESOURCE_TYPE, { Manifest: '[{"bar":2334}]', ClusterName: 'my-cluster-name', RoleArn: 'arn:aws:iam::1111111:role/iam-role-that-has-masters-access', }); - expect(stack).toHaveResource(HelmChart.RESOURCE_TYPE, { + Template.fromStack(stack).hasResourceProperties(HelmChart.RESOURCE_TYPE, { ClusterName: 'my-cluster-name', RoleArn: 'arn:aws:iam::1111111:role/iam-role-that-has-masters-access', Release: 'myclustercharthelm78d2c26a', @@ -140,7 +139,7 @@ describe('k8s manifest', () => { }); // THEN - expect(stack).toHaveResource(KubernetesManifest.RESOURCE_TYPE, { + Template.fromStack(stack).hasResourceProperties(KubernetesManifest.RESOURCE_TYPE, { Manifest: JSON.stringify([{ apiVersion: 'v1beta1', kind: 'Foo', @@ -182,7 +181,7 @@ describe('k8s manifest', () => { ); // THEN - expect(stack).toHaveResource(KubernetesManifest.RESOURCE_TYPE, { + Template.fromStack(stack).hasResourceProperties(KubernetesManifest.RESOURCE_TYPE, { Manifest: JSON.stringify([ { apiVersion: 'v1beta', @@ -244,7 +243,7 @@ describe('k8s manifest', () => { }); // THEN - expect(stack).toHaveResource(KubernetesManifest.RESOURCE_TYPE, { + Template.fromStack(stack).hasResourceProperties(KubernetesManifest.RESOURCE_TYPE, { Manifest: JSON.stringify([ { apiVersion: 'v1beta', @@ -259,7 +258,7 @@ describe('k8s manifest', () => { PruneLabel: 'aws.cdk.eks/prune-c89a5983505f58231ac2a9a86fd82735ccf2308eac', }); - expect(stack).toHaveResource(KubernetesManifest.RESOURCE_TYPE, { + Template.fromStack(stack).hasResourceProperties(KubernetesManifest.RESOURCE_TYPE, { Manifest: JSON.stringify([ { apiVersion: 'v1', @@ -297,7 +296,7 @@ describe('k8s manifest', () => { }); // THEN - expect(stack).toHaveResource(KubernetesManifest.RESOURCE_TYPE, { + Template.fromStack(stack).hasResourceProperties(KubernetesManifest.RESOURCE_TYPE, { Manifest: JSON.stringify([{ malformed: { resource: 'yes' } }]), PruneLabel: 'aws.cdk.eks/prune-c89a5983505f58231ac2a9a86fd82735ccf2308eac', }); @@ -314,7 +313,7 @@ describe('k8s manifest', () => { cluster.addManifest('m1', ['foo']); // THEN - expect(stack).toHaveResource(KubernetesManifest.RESOURCE_TYPE, { + Template.fromStack(stack).hasResourceProperties(KubernetesManifest.RESOURCE_TYPE, { Manifest: JSON.stringify([['foo']]), PruneLabel: 'aws.cdk.eks/prune-c89a5983505f58231ac2a9a86fd82735ccf2308eac', }); @@ -347,7 +346,7 @@ describe('k8s manifest', () => { }); // THEN - const template = SynthUtils.synthesize(stack).template; + const template = Template.fromStack(stack).toJSON(); const m1 = template.Resources.Clustermanifestm1E5FBE3C1.Properties; const m2 = template.Resources.m201F909C5.Properties; diff --git a/packages/@aws-cdk/aws-eks/test/k8s-object-value.test.ts b/packages/@aws-cdk/aws-eks/test/k8s-object-value.test.ts index 8bc87ba294763..8d29fa19d6471 100644 --- a/packages/@aws-cdk/aws-eks/test/k8s-object-value.test.ts +++ b/packages/@aws-cdk/aws-eks/test/k8s-object-value.test.ts @@ -1,4 +1,3 @@ -import '@aws-cdk/assert-internal/jest'; import { App, Stack, Duration } from '@aws-cdk/core'; import * as eks from '../lib'; import { KubernetesObjectValue } from '../lib/k8s-object-value'; diff --git a/packages/@aws-cdk/aws-eks/test/k8s-patch.test.ts b/packages/@aws-cdk/aws-eks/test/k8s-patch.test.ts index c59851500eff0..4040a070c15a0 100644 --- a/packages/@aws-cdk/aws-eks/test/k8s-patch.test.ts +++ b/packages/@aws-cdk/aws-eks/test/k8s-patch.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import { Names, Stack } from '@aws-cdk/core'; import * as eks from '../lib'; import { KubernetesPatch, PatchType } from '../lib/k8s-patch'; @@ -20,7 +20,7 @@ describe('k8s patch', () => { }); // THEN - expect(stack).toHaveResource('Custom::AWSCDK-EKS-KubernetesPatch', { + Template.fromStack(stack).hasResourceProperties('Custom::AWSCDK-EKS-KubernetesPatch', { ServiceToken: { 'Fn::GetAtt': [ 'awscdkawseksKubectlProviderNestedStackawscdkawseksKubectlProviderNestedStackResourceA7AEBA6B', @@ -59,7 +59,7 @@ describe('k8s patch', () => { restorePatch: { restore: { patch: 123 } }, resourceName: 'myResourceName', }); - expect(stack).toHaveResource('Custom::AWSCDK-EKS-KubernetesPatch', { + Template.fromStack(stack).hasResourceProperties('Custom::AWSCDK-EKS-KubernetesPatch', { PatchType: 'strategic', }); @@ -92,15 +92,15 @@ describe('k8s patch', () => { patchType: PatchType.STRATEGIC, }); - expect(stack).toHaveResource('Custom::AWSCDK-EKS-KubernetesPatch', { + Template.fromStack(stack).hasResourceProperties('Custom::AWSCDK-EKS-KubernetesPatch', { ResourceName: 'jsonPatchResource', PatchType: 'json', }); - expect(stack).toHaveResource('Custom::AWSCDK-EKS-KubernetesPatch', { + Template.fromStack(stack).hasResourceProperties('Custom::AWSCDK-EKS-KubernetesPatch', { ResourceName: 'mergePatchResource', PatchType: 'merge', }); - expect(stack).toHaveResource('Custom::AWSCDK-EKS-KubernetesPatch', { + Template.fromStack(stack).hasResourceProperties('Custom::AWSCDK-EKS-KubernetesPatch', { ResourceName: 'strategicPatchResource', PatchType: 'strategic', }); diff --git a/packages/@aws-cdk/aws-eks/test/nodegroup.test.ts b/packages/@aws-cdk/aws-eks/test/nodegroup.test.ts index 717b091acf2aa..dd8d0aa1274cd 100644 --- a/packages/@aws-cdk/aws-eks/test/nodegroup.test.ts +++ b/packages/@aws-cdk/aws-eks/test/nodegroup.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as ec2 from '@aws-cdk/aws-ec2'; import { testDeprecated } from '@aws-cdk/cdk-build-tools'; import * as cdk from '@aws-cdk/core'; @@ -95,7 +95,7 @@ describe('node group', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::EKS::Nodegroup', { + Template.fromStack(stack).hasResourceProperties('AWS::EKS::Nodegroup', { AmiType: 'AL2_x86_64_GPU', }); @@ -114,7 +114,7 @@ describe('node group', () => { new eks.Nodegroup(stack, 'Nodegroup', { cluster }); // THEN - expect(stack).toHaveResourceLike('AWS::EKS::Nodegroup', { + Template.fromStack(stack).hasResourceProperties('AWS::EKS::Nodegroup', { ClusterName: { Ref: 'Cluster9EE0221C', }, @@ -159,7 +159,7 @@ describe('node group', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::EKS::Nodegroup', { + Template.fromStack(stack).hasResourceProperties('AWS::EKS::Nodegroup', { ClusterName: { Ref: 'Cluster9EE0221C', }, @@ -204,7 +204,7 @@ describe('node group', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::EKS::Nodegroup', { + Template.fromStack(stack).hasResourceProperties('AWS::EKS::Nodegroup', { ClusterName: { Ref: 'Cluster9EE0221C', }, @@ -254,7 +254,7 @@ describe('node group', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::EKS::Nodegroup', { + Template.fromStack(stack).hasResourceProperties('AWS::EKS::Nodegroup', { AmiType: 'AL2_x86_64', }); }); @@ -281,7 +281,7 @@ describe('node group', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::EKS::Nodegroup', { + Template.fromStack(stack).hasResourceProperties('AWS::EKS::Nodegroup', { AmiType: 'AL2_ARM_64', }); }); @@ -309,7 +309,7 @@ describe('node group', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::EKS::Nodegroup', { + Template.fromStack(stack).hasResourceProperties('AWS::EKS::Nodegroup', { AmiType: 'AL2_x86_64_GPU', }); }); @@ -463,9 +463,9 @@ describe('node group', () => { }); /** - * BOTTOEROCKET_X86_64 with defined instance types w/o launchTemplateSpec should deploy correctly. + * BOTTLEROCKET_X86_64 with defined instance types w/o launchTemplateSpec should deploy correctly. */ - test('BOTTOEROCKET_X86_64 with defined instance types w/o launchTemplateSpec should deploy correctly', () => { + test('BOTTLEROCKET_X86_64 with defined instance types w/o launchTemplateSpec should deploy correctly', () => { // GIVEN const { stack, vpc } = testFixture(); const cluster = new eks.Cluster(stack, 'Cluster', { @@ -480,15 +480,15 @@ describe('node group', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::EKS::Nodegroup', { + Template.fromStack(stack).hasResourceProperties('AWS::EKS::Nodegroup', { AmiType: 'BOTTLEROCKET_x86_64', }); }); /** - * BOTTOEROCKET_ARM_64 with defined instance types w/o launchTemplateSpec should deploy correctly. + * BOTTLEROCKET_ARM_64 with defined instance types w/o launchTemplateSpec should deploy correctly. */ - test('BOTTOEROCKET_ARM_64 with defined instance types w/o launchTemplateSpec should deploy correctly', () => { + test('BOTTLEROCKET_ARM_64 with defined instance types w/o launchTemplateSpec should deploy correctly', () => { // GIVEN const { stack, vpc } = testFixture(); const cluster = new eks.Cluster(stack, 'Cluster', { @@ -503,7 +503,7 @@ describe('node group', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::EKS::Nodegroup', { + Template.fromStack(stack).hasResourceProperties('AWS::EKS::Nodegroup', { AmiType: 'BOTTLEROCKET_ARM_64', }); }); @@ -521,7 +521,7 @@ describe('node group', () => { new eks.Nodegroup(stack, 'Nodegroup', { cluster }); // THEN - expect(stack).toHaveResource(eks.KubernetesManifest.RESOURCE_TYPE, { + Template.fromStack(stack).hasResourceProperties(eks.KubernetesManifest.RESOURCE_TYPE, { Manifest: { 'Fn::Join': [ '', @@ -582,7 +582,7 @@ describe('node group', () => { }, }); // THEN - expect(stack).toHaveResource('AWS::EKS::Nodegroup', { + Template.fromStack(stack).hasResourceProperties('AWS::EKS::Nodegroup', { RemoteAccess: { Ec2SshKey: 'foo', SourceSecurityGroups: [ @@ -611,7 +611,7 @@ describe('node group', () => { new eks.Nodegroup(stack, 'Nodegroup', { cluster, forceUpdate: false }); // THEN - expect(stack).toHaveResourceLike('AWS::EKS::Nodegroup', { + Template.fromStack(stack).hasResourceProperties('AWS::EKS::Nodegroup', { ForceUpdateEnabled: false, }); @@ -633,7 +633,7 @@ describe('node group', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::EKS::Nodegroup', { + Template.fromStack(stack).hasResourceProperties('AWS::EKS::Nodegroup', { InstanceTypes: [ 'm5.large', ], @@ -658,7 +658,7 @@ describe('node group', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::EKS::Nodegroup', { + Template.fromStack(stack).hasResourceProperties('AWS::EKS::Nodegroup', { InstanceTypes: [ 'm5.large', ], @@ -687,7 +687,7 @@ describe('node group', () => { capacityType: eks.CapacityType.SPOT, }); // THEN - expect(stack).toHaveResourceLike('AWS::EKS::Nodegroup', { + Template.fromStack(stack).hasResourceProperties('AWS::EKS::Nodegroup', { InstanceTypes: [ 'm5.large', 't3.large', @@ -718,7 +718,7 @@ describe('node group', () => { capacityType: eks.CapacityType.ON_DEMAND, }); // THEN - expect(stack).toHaveResourceLike('AWS::EKS::Nodegroup', { + Template.fromStack(stack).hasResourceProperties('AWS::EKS::Nodegroup', { InstanceTypes: [ 'm5.large', 't3.large', @@ -765,7 +765,7 @@ describe('node group', () => { capacityType: eks.CapacityType.SPOT, }); // THEN - expect(stack).toHaveResourceLike('AWS::EKS::Nodegroup', { + Template.fromStack(stack).hasResourceProperties('AWS::EKS::Nodegroup', { CapacityType: 'SPOT', }); @@ -830,7 +830,7 @@ describe('node group', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::EKS::Nodegroup', { + Template.fromStack(stack).hasResourceProperties('AWS::EKS::Nodegroup', { RemoteAccess: { Ec2SshKey: 'foo', }, @@ -856,7 +856,7 @@ describe('node group', () => { new cdk.CfnOutput(stack2, 'NodegroupName', { value: imported.nodegroupName }); // THEN - expect(stack2).toMatchTemplate({ + Template.fromStack(stack2).templateMatches({ Outputs: { NodegroupName: { Value: { @@ -880,7 +880,7 @@ describe('node group', () => { cluster.addNodegroupCapacity('ng'); // THEN - expect(stack).toHaveResourceLike('AWS::EKS::Nodegroup', { + Template.fromStack(stack).hasResourceProperties('AWS::EKS::Nodegroup', { ClusterName: { Ref: 'Cluster9EE0221C', }, @@ -929,7 +929,7 @@ describe('node group', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::EKS::Nodegroup', { + Template.fromStack(stack).hasResourceProperties('AWS::EKS::Nodegroup', { ClusterName: { Ref: 'Cluster9EE0221C', }, @@ -984,7 +984,7 @@ describe('node group', () => { desiredSize: 4, }); // THEN - expect(stack).toHaveResourceLike('AWS::EKS::Nodegroup', { + Template.fromStack(stack).hasResourceProperties('AWS::EKS::Nodegroup', { ScalingConfig: { MinSize: 2, MaxSize: 6, @@ -1010,7 +1010,7 @@ describe('node group', () => { desiredSize: cdk.Lazy.number({ produce: () => 20 }), }); // THEN - expect(stack).toHaveResourceLike('AWS::EKS::Nodegroup', { + Template.fromStack(stack).hasResourceProperties('AWS::EKS::Nodegroup', { ScalingConfig: { MinSize: 5, MaxSize: 1, @@ -1050,7 +1050,7 @@ describe('node group', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::EKS::Nodegroup', { + Template.fromStack(stack).hasResourceProperties('AWS::EKS::Nodegroup', { LaunchTemplate: { Id: { Ref: 'LaunchTemplate', diff --git a/packages/@aws-cdk/aws-eks/test/service-account.test.ts b/packages/@aws-cdk/aws-eks/test/service-account.test.ts index e457598ccaa4f..e4db2f6680a97 100644 --- a/packages/@aws-cdk/aws-eks/test/service-account.test.ts +++ b/packages/@aws-cdk/aws-eks/test/service-account.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as iam from '@aws-cdk/aws-iam'; import * as eks from '../lib'; import { testFixture, testFixtureCluster } from './util'; @@ -15,7 +15,7 @@ describe('service account', () => { new eks.ServiceAccount(stack, 'MyServiceAccount', { cluster }); // THEN - expect(stack).toHaveResource(eks.KubernetesManifest.RESOURCE_TYPE, { + Template.fromStack(stack).hasResourceProperties(eks.KubernetesManifest.RESOURCE_TYPE, { ServiceToken: { 'Fn::GetAtt': [ 'awscdkawseksKubectlProviderNestedStackawscdkawseksKubectlProviderNestedStackResourceA7AEBA6B', @@ -38,7 +38,7 @@ describe('service account', () => { ], }, }); - expect(stack).toHaveResource(iam.CfnRole.CFN_RESOURCE_TYPE_NAME, { + Template.fromStack(stack).hasResourceProperties(iam.CfnRole.CFN_RESOURCE_TYPE_NAME, { AssumeRolePolicyDocument: { Statement: [ { @@ -73,7 +73,7 @@ describe('service account', () => { cluster.addServiceAccount('MyOtherServiceAccount'); // THEN - expect(stack).toHaveResource(eks.KubernetesManifest.RESOURCE_TYPE, { + Template.fromStack(stack).hasResourceProperties(eks.KubernetesManifest.RESOURCE_TYPE, { ServiceToken: { 'Fn::GetAtt': [ 'awscdkawseksKubectlProviderNestedStackawscdkawseksKubectlProviderNestedStackResourceA7AEBA6B', @@ -122,7 +122,7 @@ describe('service account', () => { cluster.addServiceAccount('MyServiceAccount'); - expect(stack).toHaveResource(eks.KubernetesManifest.RESOURCE_TYPE, { + Template.fromStack(stack).hasResourceProperties(eks.KubernetesManifest.RESOURCE_TYPE, { ServiceToken: { 'Fn::GetAtt': [ 'StackClusterF0EB02FAKubectlProviderNestedStackStackClusterF0EB02FAKubectlProviderNestedStackResource739D12C4', @@ -147,7 +147,7 @@ describe('service account', () => { }, }); - expect(stack).toHaveResource(iam.CfnRole.CFN_RESOURCE_TYPE_NAME, { + Template.fromStack(stack).hasResourceProperties(iam.CfnRole.CFN_RESOURCE_TYPE_NAME, { AssumeRolePolicyDocument: { Statement: [ { diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/nlb/network-load-balancer.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/nlb/network-load-balancer.ts index b54ae026b86fb..93e4e7cdd9b7a 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/nlb/network-load-balancer.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/nlb/network-load-balancer.ts @@ -1,7 +1,5 @@ import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; import * as ec2 from '@aws-cdk/aws-ec2'; -import { PolicyStatement, ServicePrincipal } from '@aws-cdk/aws-iam'; -import { IBucket } from '@aws-cdk/aws-s3'; import * as cxschema from '@aws-cdk/cloud-assembly-schema'; import { Resource } from '@aws-cdk/core'; import * as cxapi from '@aws-cdk/cx-api'; @@ -125,41 +123,6 @@ export class NetworkLoadBalancer extends BaseLoadBalancer implements INetworkLoa }); } - /** - * Enable access logging for this load balancer. - * - * A region must be specified on the stack containing the load balancer; you cannot enable logging on - * environment-agnostic stacks. See https://docs.aws.amazon.com/cdk/latest/guide/environments.html - * - * This is extending the BaseLoadBalancer.logAccessLogs method to match the bucket permissions described - * at https://docs.aws.amazon.com/elasticloadbalancing/latest/network/load-balancer-access-logs.html#access-logging-bucket-requirements - */ - public logAccessLogs(bucket: IBucket, prefix?: string) { - super.logAccessLogs(bucket, prefix); - - const logsDeliveryServicePrincipal = new ServicePrincipal('delivery.logs.amazonaws.com'); - - bucket.addToResourcePolicy( - new PolicyStatement({ - actions: ['s3:PutObject'], - principals: [logsDeliveryServicePrincipal], - resources: [ - bucket.arnForObjects(`${prefix ? prefix + '/' : ''}AWSLogs/${this.stack.account}/*`), - ], - conditions: { - StringEquals: { 's3:x-amz-acl': 'bucket-owner-full-control' }, - }, - }), - ); - bucket.addToResourcePolicy( - new PolicyStatement({ - actions: ['s3:GetBucketAcl'], - principals: [logsDeliveryServicePrincipal], - resources: [bucket.bucketArn], - }), - ); - } - /** * Return the given named metric for this Network Load Balancer * @@ -326,4 +289,4 @@ class LookedUpNetworkLoadBalancer extends Resource implements INetworkLoadBalanc ...props, }); } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-load-balancer.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-load-balancer.ts index 345583bf9e5f8..0fcb0f4916c87 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-load-balancer.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-load-balancer.ts @@ -1,5 +1,6 @@ import * as ec2 from '@aws-cdk/aws-ec2'; import * as iam from '@aws-cdk/aws-iam'; +import { PolicyStatement, ServicePrincipal } from '@aws-cdk/aws-iam'; import * as s3 from '@aws-cdk/aws-s3'; import * as cxschema from '@aws-cdk/cloud-assembly-schema'; import { ContextProvider, IResource, Lazy, Resource, Stack, Token } from '@aws-cdk/core'; @@ -258,7 +259,27 @@ export abstract class BaseLoadBalancer extends Resource { throw new Error(`Cannot enable access logging; don't know ELBv2 account for region ${region}`); } + const logsDeliveryServicePrincipal = new ServicePrincipal('delivery.logs.amazonaws.com'); bucket.grantPut(new iam.AccountPrincipal(account), `${(prefix ? prefix + '/' : '')}AWSLogs/${Stack.of(this).account}/*`); + bucket.addToResourcePolicy( + new PolicyStatement({ + actions: ['s3:PutObject'], + principals: [logsDeliveryServicePrincipal], + resources: [ + bucket.arnForObjects(`${prefix ? prefix + '/' : ''}AWSLogs/${this.stack.account}/*`), + ], + conditions: { + StringEquals: { 's3:x-amz-acl': 'bucket-owner-full-control' }, + }, + }), + ); + bucket.addToResourcePolicy( + new PolicyStatement({ + actions: ['s3:GetBucketAcl'], + principals: [logsDeliveryServicePrincipal], + resources: [bucket.bucketArn], + }), + ); // make sure the bucket's policy is created before the ALB (see https://github.com/aws/aws-cdk/issues/1633) this.node.addDependency(bucket); diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/load-balancer.test.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/load-balancer.test.ts index 4b89f79b75d31..4502b0c1d32e0 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/load-balancer.test.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/load-balancer.test.ts @@ -2,7 +2,7 @@ import { Match, Template } from '@aws-cdk/assertions'; import { Metric } from '@aws-cdk/aws-cloudwatch'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as s3 from '@aws-cdk/aws-s3'; -import { testFutureBehavior } from '@aws-cdk/cdk-build-tools/lib/feature-flag'; +import { testFutureBehavior, testLegacyBehavior } from '@aws-cdk/cdk-build-tools/lib/feature-flag'; import * as cdk from '@aws-cdk/core'; import * as cxapi from '@aws-cdk/cx-api'; import * as elbv2 from '../../lib'; @@ -146,105 +146,208 @@ describe('tests', () => { expect(loadBalancer.listeners).toContain(listener); }); - testFutureBehavior('Access logging', s3GrantWriteCtx, cdk.App, (app) => { - // GIVEN - const stack = new cdk.Stack(app, undefined, { env: { region: 'us-east-1' } }); - const vpc = new ec2.Vpc(stack, 'Stack'); - const bucket = new s3.Bucket(stack, 'AccessLoggingBucket'); - const lb = new elbv2.ApplicationLoadBalancer(stack, 'LB', { vpc }); + describe('logAccessLogs', () => { - // WHEN - lb.logAccessLogs(bucket); + function loggingSetup(app: cdk.App): { stack: cdk.Stack, bucket: s3.Bucket, lb: elbv2.ApplicationLoadBalancer } { + const stack = new cdk.Stack(app, undefined, { env: { region: 'us-east-1' } }); + const vpc = new ec2.Vpc(stack, 'Stack'); + const bucket = new s3.Bucket(stack, 'AccessLoggingBucket'); + const lb = new elbv2.ApplicationLoadBalancer(stack, 'LB', { vpc }); + return { stack, bucket, lb }; + } - // THEN + test('sets load balancer attributes', () => { + // GIVEN + const app = new cdk.App(); + const { stack, bucket, lb } = loggingSetup(app); - // verify that the LB attributes reference the bucket - Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::LoadBalancer', { - LoadBalancerAttributes: Match.arrayWith([ - { - Key: 'access_logs.s3.enabled', - Value: 'true', - }, - { - Key: 'access_logs.s3.bucket', - Value: { Ref: 'AccessLoggingBucketA6D88F29' }, - }, - { - Key: 'access_logs.s3.prefix', - Value: '', - }, - ]), - }); + // WHEN + lb.logAccessLogs(bucket); - // verify the bucket policy allows the ALB to put objects in the bucket - Template.fromStack(stack).hasResourceProperties('AWS::S3::BucketPolicy', { - PolicyDocument: { - Version: '2012-10-17', - Statement: [ + //THEN + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::LoadBalancer', { + LoadBalancerAttributes: Match.arrayWith([ { - Action: ['s3:PutObject', 's3:Abort*'], - Effect: 'Allow', - Principal: { AWS: { 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':iam::127311923021:root']] } }, - Resource: { - 'Fn::Join': ['', [{ 'Fn::GetAtt': ['AccessLoggingBucketA6D88F29', 'Arn'] }, '/AWSLogs/', - { Ref: 'AWS::AccountId' }, '/*']], - }, + Key: 'access_logs.s3.enabled', + Value: 'true', }, - ], - }, + { + Key: 'access_logs.s3.bucket', + Value: { Ref: 'AccessLoggingBucketA6D88F29' }, + }, + { + Key: 'access_logs.s3.prefix', + Value: '', + }, + ]), + }); }); - // verify the ALB depends on the bucket *and* the bucket policy - Template.fromStack(stack).hasResource('AWS::ElasticLoadBalancingV2::LoadBalancer', { - DependsOn: ['AccessLoggingBucketPolicy700D7CC6', 'AccessLoggingBucketA6D88F29'], + test('adds a dependency on the bucket', () => { + // GIVEN + const app = new cdk.App(); + const { stack, bucket, lb } = loggingSetup(app); + + // WHEN + lb.logAccessLogs(bucket); + + // THEN + // verify the ALB depends on the bucket *and* the bucket policy + Template.fromStack(stack).hasResource('AWS::ElasticLoadBalancingV2::LoadBalancer', { + DependsOn: ['AccessLoggingBucketPolicy700D7CC6', 'AccessLoggingBucketA6D88F29'], + }); }); - }); - testFutureBehavior('access logging with prefix', s3GrantWriteCtx, cdk.App, (app) => { - // GIVEN - const stack = new cdk.Stack(app, undefined, { env: { region: 'us-east-1' } }); - const vpc = new ec2.Vpc(stack, 'Stack'); - const bucket = new s3.Bucket(stack, 'AccessLoggingBucket'); - const lb = new elbv2.ApplicationLoadBalancer(stack, 'LB', { vpc }); + testLegacyBehavior('legacy bucket permissions', cdk.App, (app) => { + const { stack, bucket, lb } = loggingSetup(app); - // WHEN - lb.logAccessLogs(bucket, 'prefix-of-access-logs'); + // WHEN + lb.logAccessLogs(bucket); - // THEN - // verify that the LB attributes reference the bucket - Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::LoadBalancer', { - LoadBalancerAttributes: Match.arrayWith([ - { - Key: 'access_logs.s3.enabled', - Value: 'true', - }, - { - Key: 'access_logs.s3.bucket', - Value: { Ref: 'AccessLoggingBucketA6D88F29' }, + // THEN + // verify the bucket policy allows the ALB to put objects in the bucket + Template.fromStack(stack).hasResourceProperties('AWS::S3::BucketPolicy', { + PolicyDocument: { + Version: '2012-10-17', + Statement: [ + { + Action: ['s3:PutObject*', 's3:Abort*'], + Effect: 'Allow', + Principal: { AWS: { 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':iam::127311923021:root']] } }, + Resource: { + 'Fn::Join': ['', [{ 'Fn::GetAtt': ['AccessLoggingBucketA6D88F29', 'Arn'] }, '/AWSLogs/', + { Ref: 'AWS::AccountId' }, '/*']], + }, + }, + { + Action: 's3:PutObject', + Effect: 'Allow', + Principal: { Service: 'delivery.logs.amazonaws.com' }, + Resource: { + 'Fn::Join': ['', [{ 'Fn::GetAtt': ['AccessLoggingBucketA6D88F29', 'Arn'] }, '/AWSLogs/', + { Ref: 'AWS::AccountId' }, '/*']], + }, + Condition: { StringEquals: { 's3:x-amz-acl': 'bucket-owner-full-control' } }, + }, + { + Action: 's3:GetBucketAcl', + Effect: 'Allow', + Principal: { Service: 'delivery.logs.amazonaws.com' }, + Resource: { + 'Fn::GetAtt': ['AccessLoggingBucketA6D88F29', 'Arn'], + }, + }, + ], }, - { - Key: 'access_logs.s3.prefix', - Value: 'prefix-of-access-logs', + }); + }); + + testFutureBehavior('logging bucket permissions', s3GrantWriteCtx, cdk.App, (app) => { + // GIVEN + const { stack, bucket, lb } = loggingSetup(app); + + // WHEN + lb.logAccessLogs(bucket); + + // THEN + // verify the bucket policy allows the ALB to put objects in the bucket + Template.fromStack(stack).hasResourceProperties('AWS::S3::BucketPolicy', { + PolicyDocument: { + Version: '2012-10-17', + Statement: [ + { + Action: ['s3:PutObject', 's3:Abort*'], + Effect: 'Allow', + Principal: { AWS: { 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':iam::127311923021:root']] } }, + Resource: { + 'Fn::Join': ['', [{ 'Fn::GetAtt': ['AccessLoggingBucketA6D88F29', 'Arn'] }, '/AWSLogs/', + { Ref: 'AWS::AccountId' }, '/*']], + }, + }, + { + Action: 's3:PutObject', + Effect: 'Allow', + Principal: { Service: 'delivery.logs.amazonaws.com' }, + Resource: { + 'Fn::Join': ['', [{ 'Fn::GetAtt': ['AccessLoggingBucketA6D88F29', 'Arn'] }, '/AWSLogs/', + { Ref: 'AWS::AccountId' }, '/*']], + }, + Condition: { StringEquals: { 's3:x-amz-acl': 'bucket-owner-full-control' } }, + }, + { + Action: 's3:GetBucketAcl', + Effect: 'Allow', + Principal: { Service: 'delivery.logs.amazonaws.com' }, + Resource: { + 'Fn::GetAtt': ['AccessLoggingBucketA6D88F29', 'Arn'], + }, + }, + ], }, - ]), + }); }); - // verify the bucket policy allows the ALB to put objects in the bucket - Template.fromStack(stack).hasResourceProperties('AWS::S3::BucketPolicy', { - PolicyDocument: { - Version: '2012-10-17', - Statement: [ + testFutureBehavior('access logging with prefix', s3GrantWriteCtx, cdk.App, (app) => { + // GIVEN + const { stack, bucket, lb } = loggingSetup(app); + + // WHEN + lb.logAccessLogs(bucket, 'prefix-of-access-logs'); + + // THEN + // verify that the LB attributes reference the bucket + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::LoadBalancer', { + LoadBalancerAttributes: Match.arrayWith([ { - Action: ['s3:PutObject', 's3:Abort*'], - Effect: 'Allow', - Principal: { AWS: { 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':iam::127311923021:root']] } }, - Resource: { - 'Fn::Join': ['', [{ 'Fn::GetAtt': ['AccessLoggingBucketA6D88F29', 'Arn'] }, '/prefix-of-access-logs/AWSLogs/', - { Ref: 'AWS::AccountId' }, '/*']], - }, + Key: 'access_logs.s3.enabled', + Value: 'true', + }, + { + Key: 'access_logs.s3.bucket', + Value: { Ref: 'AccessLoggingBucketA6D88F29' }, + }, + { + Key: 'access_logs.s3.prefix', + Value: 'prefix-of-access-logs', }, - ], - }, + ]), + }); + + // verify the bucket policy allows the ALB to put objects in the bucket + Template.fromStack(stack).hasResourceProperties('AWS::S3::BucketPolicy', { + PolicyDocument: { + Version: '2012-10-17', + Statement: [ + { + Action: ['s3:PutObject', 's3:Abort*'], + Effect: 'Allow', + Principal: { AWS: { 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':iam::127311923021:root']] } }, + Resource: { + 'Fn::Join': ['', [{ 'Fn::GetAtt': ['AccessLoggingBucketA6D88F29', 'Arn'] }, '/prefix-of-access-logs/AWSLogs/', + { Ref: 'AWS::AccountId' }, '/*']], + }, + }, + { + Action: 's3:PutObject', + Effect: 'Allow', + Principal: { Service: 'delivery.logs.amazonaws.com' }, + Resource: { + 'Fn::Join': ['', [{ 'Fn::GetAtt': ['AccessLoggingBucketA6D88F29', 'Arn'] }, '/prefix-of-access-logs/AWSLogs/', + { Ref: 'AWS::AccountId' }, '/*']], + }, + Condition: { StringEquals: { 's3:x-amz-acl': 'bucket-owner-full-control' } }, + }, + { + Action: 's3:GetBucketAcl', + Effect: 'Allow', + Principal: { Service: 'delivery.logs.amazonaws.com' }, + Resource: { + 'Fn::GetAtt': ['AccessLoggingBucketA6D88F29', 'Arn'], + }, + }, + ], + }, + }); }); }); diff --git a/packages/@aws-cdk/aws-events/package.json b/packages/@aws-cdk/aws-events/package.json index f871d4ec7b6a4..57af72c704d52 100644 --- a/packages/@aws-cdk/aws-events/package.json +++ b/packages/@aws-cdk/aws-events/package.json @@ -81,7 +81,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@aws-cdk/assert-internal": "0.0.0", + "@aws-cdk/assertions": "0.0.0", "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-events/test/archive.test.ts b/packages/@aws-cdk/aws-events/test/archive.test.ts index 0c37a0ec4ae39..8119961738cf1 100644 --- a/packages/@aws-cdk/aws-events/test/archive.test.ts +++ b/packages/@aws-cdk/aws-events/test/archive.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import { Duration, Stack } from '@aws-cdk/core'; import { EventBus } from '../lib'; import { Archive } from '../lib/archive'; @@ -9,7 +9,7 @@ describe('archive', () => { const stack = new Stack(); // WHEN - let eventBus = new EventBus(stack, 'Bus'); + const eventBus = new EventBus(stack, 'Bus'); new Archive(stack, 'Archive', { sourceEventBus: eventBus, @@ -20,11 +20,11 @@ describe('archive', () => { }); // THEN - expect(stack).toHaveResource('AWS::Events::EventBus', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::EventBus', { Name: 'Bus', }); - expect(stack).toHaveResource('AWS::Events::Archive', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Archive', { EventPattern: { account: [{ Ref: 'AWS::AccountId', @@ -38,15 +38,14 @@ describe('archive', () => { ], }, }); - - }); + test('creates an archive for an EventBus with a pattern including a detailType property', () => { // GIVEN const stack = new Stack(); // WHEN - let eventBus = new EventBus(stack, 'Bus'); + const eventBus = new EventBus(stack, 'Bus'); new Archive(stack, 'Archive', { sourceEventBus: eventBus, @@ -58,11 +57,11 @@ describe('archive', () => { }); // THEN - expect(stack).toHaveResource('AWS::Events::EventBus', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::EventBus', { Name: 'Bus', }); - expect(stack).toHaveResource('AWS::Events::Archive', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Archive', { EventPattern: { 'account': [{ Ref: 'AWS::AccountId', @@ -77,7 +76,5 @@ describe('archive', () => { ], }, }); - - }); }); diff --git a/packages/@aws-cdk/aws-events/test/event-bus.test.ts b/packages/@aws-cdk/aws-events/test/event-bus.test.ts index 71f089a58b23c..34406986c608e 100644 --- a/packages/@aws-cdk/aws-events/test/event-bus.test.ts +++ b/packages/@aws-cdk/aws-events/test/event-bus.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as iam from '@aws-cdk/aws-iam'; import { testDeprecated } from '@aws-cdk/cdk-build-tools'; import { Aws, CfnResource, Stack, Arn, App, PhysicalName, CfnOutput } from '@aws-cdk/core'; @@ -13,7 +13,7 @@ describe('event bus', () => { new EventBus(stack, 'Bus'); // THEN - expect(stack).toHaveResource('AWS::Events::EventBus', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::EventBus', { Name: 'Bus', }); }); @@ -26,7 +26,7 @@ describe('event bus', () => { new EventBus(stack, 'Bus', {}); // THEN - expect(stack).toHaveResource('AWS::Events::EventBus', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::EventBus', { Name: 'Bus', }); }); @@ -41,11 +41,9 @@ describe('event bus', () => { }); // THEN - expect(stack).toHaveResource('AWS::Events::EventBus', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::EventBus', { Name: 'myEventBus', }); - - }); test('partner event bus', () => { @@ -58,12 +56,10 @@ describe('event bus', () => { }); // THEN - expect(stack).toHaveResource('AWS::Events::EventBus', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::EventBus', { Name: 'aws.partner/PartnerName/acct1/repo1', EventSourceName: 'aws.partner/PartnerName/acct1/repo1', }); - - }); test('imported event bus', () => { @@ -82,12 +78,10 @@ describe('event bus', () => { }, }); - expect(stack).toHaveResource('Test::Resource', { + Template.fromStack(stack).hasResourceProperties('Test::Resource', { EventBusArn1: { 'Fn::GetAtt': ['BusEA82B648', 'Arn'] }, EventBusArn2: { 'Fn::GetAtt': ['BusEA82B648', 'Arn'] }, }); - - }); test('imported event bus from name', () => { @@ -99,8 +93,6 @@ describe('event bus', () => { // WHEN expect(stack.resolve(eventBus.eventBusName)).toEqual(stack.resolve(importEB.eventBusName)); - - }); test('same account imported event bus has right resource env', () => { @@ -113,8 +105,6 @@ describe('event bus', () => { // WHEN expect(stack.resolve(importEB.env.account)).toEqual({ 'Fn::Select': [4, { 'Fn::Split': [':', { 'Fn::GetAtt': ['BusEA82B648', 'Arn'] }] }] }); expect(stack.resolve(importEB.env.region)).toEqual({ 'Fn::Select': [3, { 'Fn::Split': [':', { 'Fn::GetAtt': ['BusEA82B648', 'Arn'] }] }] }); - - }); test('cross account imported event bus has right resource env', () => { @@ -134,8 +124,6 @@ describe('event bus', () => { // WHEN expect(importEB.env.account).toEqual(arnParts.account); expect(importEB.env.region).toEqual(arnParts.region); - - }); test('can get bus name', () => { @@ -154,11 +142,9 @@ describe('event bus', () => { }); // THEN - expect(stack).toHaveResource('Test::Resource', { + Template.fromStack(stack).hasResourceProperties('Test::Resource', { EventBusName: { Ref: 'BusEA82B648' }, }); - - }); test('can get bus arn', () => { @@ -177,11 +163,9 @@ describe('event bus', () => { }); // THEN - expect(stack).toHaveResource('Test::Resource', { + Template.fromStack(stack).hasResourceProperties('Test::Resource', { EventBusArn: { 'Fn::GetAtt': ['BusEA82B648', 'Arn'] }, }); - - }); test('event bus name cannot be default', () => { @@ -197,8 +181,6 @@ describe('event bus', () => { expect(() => { createInvalidBus(); }).toThrow(/'eventBusName' must not be 'default'/); - - }); test('event bus name cannot contain slash', () => { @@ -214,8 +196,6 @@ describe('event bus', () => { expect(() => { createInvalidBus(); }).toThrow(/'eventBusName' must not contain '\/'/); - - }); test('event bus cannot have name and source name', () => { @@ -232,8 +212,6 @@ describe('event bus', () => { expect(() => { createInvalidBus(); }).toThrow(/'eventBusName' and 'eventSourceName' cannot both be provided/); - - }); test('event bus name cannot be empty string', () => { @@ -249,8 +227,6 @@ describe('event bus', () => { expect(() => { createInvalidBus(); }).toThrow(/'eventBusName' must satisfy: /); - - }); test('does not throw if eventBusName is a token', () => { @@ -261,8 +237,6 @@ describe('event bus', () => { expect(() => new EventBus(stack, 'EventBus', { eventBusName: Aws.STACK_NAME, })).not.toThrow(); - - }); test('event bus source name must follow pattern', () => { @@ -278,8 +252,6 @@ describe('event bus', () => { expect(() => { createInvalidBus(); }).toThrow(/'eventSourceName' must satisfy: \/\^aws/); - - }); test('event bus source name cannot be empty string', () => { @@ -295,8 +267,6 @@ describe('event bus', () => { expect(() => { createInvalidBus(); }).toThrow(/'eventSourceName' must satisfy: /); - - }); testDeprecated('can grant PutEvents', () => { @@ -310,7 +280,7 @@ describe('event bus', () => { EventBus.grantPutEvents(role); // THEN - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -327,8 +297,6 @@ describe('event bus', () => { }, ], }); - - }); test('can grant PutEvents using grantAllPutEvents', () => { @@ -342,7 +310,7 @@ describe('event bus', () => { EventBus.grantAllPutEvents(role); // THEN - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -359,9 +327,8 @@ describe('event bus', () => { }, ], }); - - }); + test('can grant PutEvents to a specific event bus', () => { // GIVEN const stack = new Stack(); @@ -375,7 +342,7 @@ describe('event bus', () => { eventBus.grantPutEventsTo(role); // THEN - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -397,9 +364,8 @@ describe('event bus', () => { }, ], }); - - }); + test('can archive events', () => { // GIVEN const stack = new Stack(); @@ -415,11 +381,11 @@ describe('event bus', () => { }); // THEN - expect(stack).toHaveResource('AWS::Events::EventBus', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::EventBus', { Name: 'Bus', }); - expect(stack).toHaveResource('AWS::Events::Archive', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Archive', { SourceArn: { 'Fn::GetAtt': [ 'BusEA82B648', @@ -448,9 +414,8 @@ describe('event bus', () => { RetentionDays: 0, ArchiveName: 'MyArchive', }); - - }); + test('can archive events from an imported EventBus', () => { // GIVEN const stack = new Stack(); @@ -468,11 +433,11 @@ describe('event bus', () => { }); // THEN - expect(stack).toHaveResource('AWS::Events::EventBus', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::EventBus', { Name: 'Bus', }); - expect(stack).toHaveResource('AWS::Events::Archive', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Archive', { SourceArn: { 'Fn::GetAtt': [ 'BusEA82B648', @@ -524,9 +489,8 @@ describe('event bus', () => { RetentionDays: 0, ArchiveName: 'MyArchive', }); - - }); + test('cross account event bus uses generated physical name', () => { // GIVEN const app = new App(); @@ -551,7 +515,7 @@ describe('event bus', () => { new CfnOutput(stack2, 'BusName', { value: bus1.eventBusName }); // THEN - expect(stack1).toHaveResource('AWS::Events::EventBus', { + Template.fromStack(stack1).hasResourceProperties('AWS::Events::EventBus', { Name: 'stack1stack1busca19bdf8ab2e51b62a5a', }); }); diff --git a/packages/@aws-cdk/aws-events/test/input.test.ts b/packages/@aws-cdk/aws-events/test/input.test.ts index 76ec2cd2d2bd9..2c5536620a9fb 100644 --- a/packages/@aws-cdk/aws-events/test/input.test.ts +++ b/packages/@aws-cdk/aws-events/test/input.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import { User } from '@aws-cdk/aws-iam'; import * as cdk from '@aws-cdk/core'; import { EventField, IRuleTarget, RuleTargetInput, Schedule } from '../lib'; @@ -17,14 +17,13 @@ describe('input', () => { rule.addTarget(new SomeTarget(RuleTargetInput.fromObject({ SomeObject: 'withAValue' }))); // THEN - expect(stack).toHaveResourceLike('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { Targets: [ { Input: '{"SomeObject":"withAValue"}', }, ], }); - }); test('can use joined JSON containing refs in JSON object', () => { @@ -41,7 +40,7 @@ describe('input', () => { }))); // THEN - expect(stack).toHaveResourceLike('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { Targets: [ { InputTransformer: { @@ -62,8 +61,6 @@ describe('input', () => { }, ], }); - - }); test('can use joined JSON containing refs in JSON object with tricky inputs', () => { @@ -80,7 +77,7 @@ describe('input', () => { }))); // THEN - expect(stack).toHaveResourceLike('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { Targets: [ { InputTransformer: { @@ -101,8 +98,6 @@ describe('input', () => { }, ], }); - - }); test('can use joined JSON containing refs in JSON object and concat', () => { @@ -119,7 +114,7 @@ describe('input', () => { }))); // THEN - expect(stack).toHaveResourceLike('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { Targets: [ { InputTransformer: { @@ -140,8 +135,6 @@ describe('input', () => { }, ], }); - - }); test('can use joined JSON containing refs in JSON object and quotes', () => { @@ -158,7 +151,7 @@ describe('input', () => { }))); // THEN - expect(stack).toHaveResourceLike('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { Targets: [ { InputTransformer: { @@ -179,8 +172,6 @@ describe('input', () => { }, ], }); - - }); test('can use joined JSON containing refs in JSON object and multiple keys', () => { @@ -197,7 +188,7 @@ describe('input', () => { }))); // THEN - expect(stack).toHaveResourceLike('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { Targets: [ { InputTransformer: { @@ -218,8 +209,6 @@ describe('input', () => { }, ], }); - - }); test('can use token', () => { @@ -234,7 +223,7 @@ describe('input', () => { rule.addTarget(new SomeTarget(RuleTargetInput.fromObject({ userArn: user.userArn }))); // THEN - expect(stack).toHaveResourceLike('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { Targets: [ { Input: { @@ -255,7 +244,6 @@ describe('input', () => { }, ], }); - }); }); @@ -271,15 +259,13 @@ describe('input', () => { rule.addTarget(new SomeTarget(RuleTargetInput.fromMultilineText('I have\nmultiple lines'))); // THEN - expect(stack).toHaveResourceLike('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { Targets: [ { Input: '"I have"\n"multiple lines"', }, ], }); - - }); test('escaped newlines are not interpreted as newlines', () => { @@ -290,18 +276,16 @@ describe('input', () => { }); // WHEN - rule.addTarget(new SomeTarget(RuleTargetInput.fromMultilineText('this is not\\na real newline'))), + rule.addTarget(new SomeTarget(RuleTargetInput.fromMultilineText('this is not\\na real newline'))); // THEN - expect(stack).toHaveResourceLike('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { Targets: [ { Input: '"this is not\\\\na real newline"', }, ], }); - - }); test('can use Tokens in text templates', () => { @@ -317,15 +301,13 @@ describe('input', () => { rule.addTarget(new SomeTarget(RuleTargetInput.fromText(`hello ${world}`))); // THEN - expect(stack).toHaveResourceLike('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { Targets: [ { Input: '"hello world"', }, ], }); - - }); }); }); diff --git a/packages/@aws-cdk/aws-events/test/rule.test.ts b/packages/@aws-cdk/aws-events/test/rule.test.ts index 21782c089b40c..1cbd776441fce 100644 --- a/packages/@aws-cdk/aws-events/test/rule.test.ts +++ b/packages/@aws-cdk/aws-events/test/rule.test.ts @@ -1,5 +1,5 @@ /* eslint-disable object-curly-newline */ -import '@aws-cdk/assert-internal/jest'; +import { Match, Template } from '@aws-cdk/assertions'; import * as iam from '@aws-cdk/aws-iam'; import * as cdk from '@aws-cdk/core'; import { EventBus, EventField, IRule, IRuleTarget, RuleTargetConfig, RuleTargetInput, Schedule } from '../lib'; @@ -15,7 +15,7 @@ describe('rule', () => { schedule: Schedule.rate(cdk.Duration.minutes(10)), }); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ 'Resources': { 'MyRuleA44AB831': { 'Type': 'AWS::Events::Rule', @@ -26,7 +26,6 @@ describe('rule', () => { }, }, }); - }); test('can get rule name', () => { @@ -42,11 +41,9 @@ describe('rule', () => { }, }); - expect(stack).toHaveResource('Test::Resource', { + Template.fromStack(stack).hasResourceProperties('Test::Resource', { RuleName: { Ref: 'MyRuleA44AB831' }, }); - - }); test('get rate as token', () => { @@ -60,18 +57,15 @@ describe('rule', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { 'Name': 'rateInMinutes', 'ScheduleExpression': 'rate(5 minutes)', }); - - }); test('Seconds is not an allowed value for Schedule rate', () => { const lazyDuration = cdk.Duration.seconds(cdk.Lazy.number({ produce: () => 5 })); expect(() => Schedule.rate(lazyDuration)).toThrow(/Allowed units for scheduling/i); - }); test('Millis is not an allowed value for Schedule rate', () => { @@ -79,7 +73,6 @@ describe('rule', () => { // THEN expect(() => Schedule.rate(lazyDuration)).toThrow(/Allowed units for scheduling/i); - }); test('rule with physical name', () => { @@ -93,11 +86,9 @@ describe('rule', () => { }); // THEN - expect(stack).toHaveResource('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { Name: 'PhysicalName', }); - - }); test('eventPattern is rendered properly', () => { @@ -119,7 +110,7 @@ describe('rule', () => { }, }); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ 'Resources': { 'MyRuleA44AB831': { 'Type': 'AWS::Events::Rule', @@ -140,8 +131,6 @@ describe('rule', () => { }, }, }); - - }); test('fails synthesis if neither eventPattern nor scheudleExpression are specified', () => { @@ -149,7 +138,6 @@ describe('rule', () => { const stack = new cdk.Stack(app, 'MyStack'); new Rule(stack, 'Rule'); expect(() => app.synth()).toThrow(/Either 'eventPattern' or 'schedule' must be defined/); - }); test('addEventPattern can be used to add filters', () => { @@ -173,7 +161,7 @@ describe('rule', () => { }, }); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ 'Resources': { 'MyRuleA44AB831': { 'Type': 'AWS::Events::Rule', @@ -202,7 +190,6 @@ describe('rule', () => { }, }, }); - }); test('addEventPattern can de-duplicate filters and keep the order', () => { @@ -217,7 +204,7 @@ describe('rule', () => { detailType: ['EC2 Instance State-change Notification', 'AWS API Call via CloudTrail'], }); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ 'Resources': { 'MyRuleA44AB831': { 'Type': 'AWS::Events::Rule', @@ -233,7 +220,6 @@ describe('rule', () => { }, }, }); - }); test('targets can be added via props or addTarget with input transformer', () => { @@ -261,7 +247,7 @@ describe('rule', () => { rule.addTarget(t2); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ 'Resources': { 'EventRule5A491D2C': { 'Type': 'AWS::Events::Rule', @@ -291,7 +277,6 @@ describe('rule', () => { }, }, }); - }); test('input template can contain tokens', () => { @@ -337,7 +322,7 @@ describe('rule', () => { }), }); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ 'Resources': { 'EventRule5A491D2C': { 'Type': 'AWS::Events::Rule', @@ -378,8 +363,6 @@ describe('rule', () => { }, }, }); - - }); test('target can declare role which will be used', () => { @@ -404,7 +387,7 @@ describe('rule', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { 'Targets': [ { 'Arn': 'ARN2', @@ -413,8 +396,6 @@ describe('rule', () => { }, ], }); - - }); test('in cross-account scenario, target role is only used in target account', () => { @@ -442,7 +423,7 @@ describe('rule', () => { }); // THEN - expect(ruleStack).toHaveResourceLike('AWS::Events::Rule', { + Template.fromStack(ruleStack).hasResourceProperties('AWS::Events::Rule', { Targets: [ { Arn: { 'Fn::Join': ['', [ @@ -453,7 +434,7 @@ describe('rule', () => { }, ], }); - expect(targetStack).toHaveResourceLike('AWS::Events::Rule', { + Template.fromStack(targetStack).hasResourceProperties('AWS::Events::Rule', { 'Targets': [ { 'Arn': 'ARN2', @@ -462,8 +443,6 @@ describe('rule', () => { }, ], }); - - }); test('asEventRuleTarget can use the ruleArn and a uniqueId of the rule', () => { @@ -490,7 +469,6 @@ describe('rule', () => { expect(stack.resolve(receivedRuleArn)).toEqual(stack.resolve(rule.ruleArn)); expect(receivedRuleId).toEqual(cdk.Names.uniqueId(rule)); - }); test('fromEventRuleArn', () => { @@ -503,7 +481,6 @@ describe('rule', () => { // THEN expect(importedRule.ruleArn).toEqual('arn:aws:events:us-east-2:123456789012:rule/example'); expect(importedRule.ruleName).toEqual('example'); - }); test('rule can be disabled', () => { @@ -517,11 +494,9 @@ describe('rule', () => { }); // THEN - expect(stack).toHaveResource('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { 'State': 'DISABLED', }); - - }); test('can add multiple targets with the same id', () => { @@ -535,7 +510,7 @@ describe('rule', () => { rule.addTarget(new SomeTarget()); // THEN - expect(stack).toHaveResource('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { Targets: [ { 'Arn': 'ARN1', @@ -553,8 +528,6 @@ describe('rule', () => { }, ], }); - - }); test('sqsParameters are generated when they are specified in target props', () => { @@ -572,7 +545,7 @@ describe('rule', () => { targets: [t1], }); - expect(stack).toHaveResource('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { Targets: [ { 'Arn': 'ARN1', @@ -583,7 +556,6 @@ describe('rule', () => { }, ], }); - }); test('associate rule with event bus', () => { @@ -600,13 +572,11 @@ describe('rule', () => { }); // THEN - expect(stack).toHaveResource('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { EventBusName: { Ref: 'EventBus7B8748AA', }, }); - - }); test('throws with eventBus and schedule', () => { @@ -620,7 +590,6 @@ describe('rule', () => { schedule: Schedule.rate(cdk.Duration.minutes(10)), eventBus, })).toThrow(/Cannot associate rule with 'eventBus' when using 'schedule'/); - }); test('allow an imported target if is in the same account and region', () => { @@ -639,7 +608,7 @@ describe('rule', () => { rule.addTarget(new SomeTarget('T', resource)); - expect(sourceStack).toHaveResource('AWS::Events::Rule', { + Template.fromStack(sourceStack).hasResourceProperties('AWS::Events::Rule', { Targets: [ { 'Arn': 'ARN1', @@ -650,8 +619,6 @@ describe('rule', () => { }, ], }); - - }); describe('for cross-account and/or cross-region targets', () => { @@ -668,8 +635,6 @@ describe('rule', () => { expect(() => { rule.addTarget(new SomeTarget('T', resource)); }).toThrow(/You need to provide a concrete region/); - - }); test('requires that the target stack specify a concrete account', () => { @@ -685,8 +650,6 @@ describe('rule', () => { expect(() => { rule.addTarget(new SomeTarget('T', resource)); }).toThrow(/You need to provide a concrete account for the target stack when using cross-account or cross-region events/); - - }); test('requires that the target stack specify a concrete region', () => { @@ -703,8 +666,6 @@ describe('rule', () => { expect(() => { rule.addTarget(new SomeTarget('T', resource)); }).toThrow(/You need to provide a concrete region for the target stack when using cross-account or cross-region events/); - - }); test('creates cross-account targets if in the same region', () => { @@ -726,7 +687,7 @@ describe('rule', () => { rule.addTarget(new SomeTarget('T', resource)); - expect(sourceStack).toHaveResourceLike('AWS::Events::Rule', { + Template.fromStack(sourceStack).hasResourceProperties('AWS::Events::Rule', { 'State': 'ENABLED', 'Targets': [ { @@ -745,7 +706,7 @@ describe('rule', () => { ], }); - expect(targetStack).toHaveResource('AWS::Events::Rule', { + Template.fromStack(targetStack).hasResourceProperties('AWS::Events::Rule', { Targets: [ { 'Arn': 'ARN1', @@ -756,8 +717,6 @@ describe('rule', () => { }, ], }); - - }); test('creates cross-region targets', () => { @@ -779,7 +738,7 @@ describe('rule', () => { rule.addTarget(new SomeTarget('T', resource)); - expect(sourceStack).toHaveResourceLike('AWS::Events::Rule', { + Template.fromStack(sourceStack).hasResourceProperties('AWS::Events::Rule', { 'State': 'ENABLED', 'Targets': [ { @@ -798,7 +757,7 @@ describe('rule', () => { ], }); - expect(targetStack).toHaveResource('AWS::Events::Rule', { + Template.fromStack(targetStack).hasResourceProperties('AWS::Events::Rule', { Targets: [ { 'Arn': 'ARN1', @@ -809,8 +768,6 @@ describe('rule', () => { }, ], }); - - }); test('do not create duplicated targets', () => { @@ -834,7 +791,7 @@ describe('rule', () => { // same target should be skipped rule.addTarget(new SomeTarget('T1', resource)); - expect(sourceStack).toHaveResourceLike('AWS::Events::Rule', { + Template.fromStack(sourceStack).hasResourceProperties('AWS::Events::Rule', { 'State': 'ENABLED', 'Targets': [ { @@ -853,7 +810,7 @@ describe('rule', () => { ], }); - expect(sourceStack).not.toHaveResourceLike('AWS::Events::Rule', { + Template.fromStack(sourceStack).hasResourceProperties('AWS::Events::Rule', Match.not({ 'State': 'ENABLED', 'Targets': [ { @@ -870,9 +827,7 @@ describe('rule', () => { }, }, ], - }); - - + })); }); test('requires that the target is not imported', () => { @@ -893,8 +848,6 @@ describe('rule', () => { expect(() => { rule.addTarget(new SomeTarget('T', resource)); }).toThrow(/Cannot create a cross-account or cross-region rule for an imported resource/); - - }); test('requires that the source and target stacks be part of the same App', () => { @@ -911,8 +864,6 @@ describe('rule', () => { expect(() => { rule.addTarget(new SomeTarget('T', resource)); }).toThrow(/Event stack and target stack must belong to the same CDK app/); - - }); test('generates the correct rules in the source and target stacks when eventPattern is passed in the constructor', () => { @@ -944,7 +895,7 @@ describe('rule', () => { rule.addTarget(new SomeTarget('T1', resource1)); rule.addTarget(new SomeTarget('T2', resource2)); - expect(sourceStack).toHaveResourceLike('AWS::Events::Rule', { + Template.fromStack(sourceStack).hasResourceProperties('AWS::Events::Rule', { 'EventPattern': { 'source': [ 'some-event', @@ -968,7 +919,7 @@ describe('rule', () => { ], }); - expect(targetStack).toHaveResourceLike('AWS::Events::Rule', { + Template.fromStack(targetStack).hasResourceProperties('AWS::Events::Rule', { 'EventPattern': { 'source': [ 'some-event', @@ -982,7 +933,7 @@ describe('rule', () => { }, ], }); - expect(targetStack).toHaveResourceLike('AWS::Events::Rule', { + Template.fromStack(targetStack).hasResourceProperties('AWS::Events::Rule', { 'EventPattern': { 'source': [ 'some-event', @@ -998,13 +949,11 @@ describe('rule', () => { }); const eventBusPolicyStack = app.node.findChild(`EventBusPolicy-${sourceAccount}-us-west-2-${targetAccount}`) as cdk.Stack; - expect(eventBusPolicyStack).toHaveResourceLike('AWS::Events::EventBusPolicy', { + Template.fromStack(eventBusPolicyStack).hasResourceProperties('AWS::Events::EventBusPolicy', { 'Action': 'events:PutEvents', 'StatementId': `Allow-account-${sourceAccount}`, 'Principal': sourceAccount, }); - - }); test('generates the correct rule in the target stack when addEventPattern in the source rule is used', () => { @@ -1034,7 +983,7 @@ describe('rule', () => { source: ['some-event'], }); - expect(targetStack).toHaveResourceLike('AWS::Events::Rule', { + Template.fromStack(targetStack).hasResourceProperties('AWS::Events::Rule', { 'EventPattern': { 'source': [ 'some-event', @@ -1048,8 +997,6 @@ describe('rule', () => { }, ], }); - - }); }); }); diff --git a/packages/@aws-cdk/aws-events/test/schedule.test.ts b/packages/@aws-cdk/aws-events/test/schedule.test.ts index d853da9ba6c30..9dea322c62d0a 100644 --- a/packages/@aws-cdk/aws-events/test/schedule.test.ts +++ b/packages/@aws-cdk/aws-events/test/schedule.test.ts @@ -8,7 +8,6 @@ describe('schedule', () => { minute: '0/10', weekDay: 'MON-FRI', }).expressionString); - }); test('cron expressions day and dow are mutex: given month day', () => { @@ -18,7 +17,6 @@ describe('schedule', () => { hour: '8', day: '1', }).expressionString); - }); test('cron expressions day and dow are mutex: given neither', () => { @@ -27,28 +25,24 @@ describe('schedule', () => { minute: '0', hour: '10', }).expressionString); - }); test('rate must be whole number of minutes', () => { expect(() => { events.Schedule.rate(Duration.minutes(0.13456)); }).toThrow(/'0.13456 minutes' cannot be converted into a whole number of seconds/); - }); test('rate must be whole number', () => { expect(() => { events.Schedule.rate(Duration.minutes(1/8)); }).toThrow(/'0.125 minutes' cannot be converted into a whole number of seconds/); - }); test('rate cannot be 0', () => { expect(() => { events.Schedule.rate(Duration.days(0)); }).toThrow(/Duration cannot be 0/); - }); test('rate can be from a token', () => { @@ -56,41 +50,35 @@ describe('schedule', () => { const lazyDuration = Duration.minutes(Lazy.number({ produce: () => 5 })); const rate = events.Schedule.rate(lazyDuration); expect('rate(5 minutes)').toEqual(stack.resolve(rate).expressionString); - }); test('rate can be in minutes', () => { expect('rate(10 minutes)').toEqual( events.Schedule.rate(Duration.minutes(10)) .expressionString); - }); test('rate can be in days', () => { expect('rate(10 days)').toEqual( events.Schedule.rate(Duration.days(10)) .expressionString); - }); test('rate can be in hours', () => { expect('rate(10 hours)').toEqual( events.Schedule.rate(Duration.hours(10)) .expressionString); - }); test('rate can be in seconds', () => { expect('rate(2 minutes)').toEqual( events.Schedule.rate(Duration.seconds(120)) .expressionString); - }); test('rate must not be in seconds when specified as a token', () => { expect(() => { events.Schedule.rate(Duration.seconds(Lazy.number({ produce: () => 5 }))); }).toThrow(/Allowed units for scheduling/); - }); }); diff --git a/packages/@aws-cdk/aws-events/test/util.test.ts b/packages/@aws-cdk/aws-events/test/util.test.ts index eb354ca9c48a0..b885041163316 100644 --- a/packages/@aws-cdk/aws-events/test/util.test.ts +++ b/packages/@aws-cdk/aws-events/test/util.test.ts @@ -23,21 +23,18 @@ describe('util', () => { case: [1], }, }); - }); test('merge into an empty destination', () => { expect(mergeEventPattern(undefined, { foo: ['123'] })).toEqual({ foo: ['123'] }); expect(mergeEventPattern(undefined, { foo: { bar: ['123'] } })).toEqual({ foo: { bar: ['123'] } }); expect(mergeEventPattern({ }, { foo: { bar: ['123'] } })).toEqual({ foo: { bar: ['123'] } }); - }); test('fails if a field is not an array', () => { expect(() => mergeEventPattern(undefined, 123)).toThrow(/Invalid event pattern '123', expecting an object or an array/); expect(() => mergeEventPattern(undefined, 'Hello')).toThrow(/Invalid event pattern '"Hello"', expecting an object or an array/); expect(() => mergeEventPattern(undefined, { foo: '123' })).toThrow(/Invalid event pattern field { foo: "123" }. All fields must be arrays/); - }); test('fails if mismatch between dest and src', () => { @@ -52,7 +49,6 @@ describe('util', () => { }, }, })).toThrow(/Invalid event pattern field array. Type mismatch between existing pattern \[1\] and added pattern \{"value":\["hello"\]\}/); - }); test('deduplicate match values in pattern array', () => { @@ -90,7 +86,6 @@ describe('util', () => { 'detail-type': ['AWS API Call via CloudTrail'], 'time': [{ prefix: '2017-10-02' }, { prefix: '2017-10-03' }], }); - }); }); }); diff --git a/packages/@aws-cdk/aws-iam/package.json b/packages/@aws-cdk/aws-iam/package.json index abe60b8a954a0..4396fd3ab1fad 100644 --- a/packages/@aws-cdk/aws-iam/package.json +++ b/packages/@aws-cdk/aws-iam/package.json @@ -79,7 +79,6 @@ }, "license": "Apache-2.0", "devDependencies": { - "@aws-cdk/assert-internal": "0.0.0", "@aws-cdk/assertions": "0.0.0", "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cdk-integ-tools": "0.0.0", diff --git a/packages/@aws-cdk/aws-iam/test/access-key.test.ts b/packages/@aws-cdk/aws-iam/test/access-key.test.ts index fe54ffef2b159..c63bfa7d739c2 100644 --- a/packages/@aws-cdk/aws-iam/test/access-key.test.ts +++ b/packages/@aws-cdk/aws-iam/test/access-key.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import { App, Stack } from '@aws-cdk/core'; import { AccessKey, AccessKeyStatus, User } from '../lib'; @@ -13,7 +13,7 @@ describe('IAM Access keys', () => { new AccessKey(stack, 'MyAccessKey', { user }); // THEN - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ Resources: { MyUserDC45028B: { Type: 'AWS::IAM::User', @@ -38,7 +38,7 @@ describe('IAM Access keys', () => { new AccessKey(stack, 'MyAccessKey', { user, status: AccessKeyStatus.ACTIVE }); // THEN - expect(stack).toHaveResourceLike('AWS::IAM::AccessKey', { Status: 'Active' }); + Template.fromStack(stack).hasResourceProperties('AWS::IAM::AccessKey', { Status: 'Active' }); }); test('inactive status is specified with correct capitalization', () => { @@ -54,7 +54,7 @@ describe('IAM Access keys', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::IAM::AccessKey', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::AccessKey', { Status: 'Inactive', }); }); diff --git a/packages/@aws-cdk/aws-iam/test/auto-cross-stack-refs.test.ts b/packages/@aws-cdk/aws-iam/test/auto-cross-stack-refs.test.ts index 1f8384c600fb9..4ede7daf57355 100644 --- a/packages/@aws-cdk/aws-iam/test/auto-cross-stack-refs.test.ts +++ b/packages/@aws-cdk/aws-iam/test/auto-cross-stack-refs.test.ts @@ -1,5 +1,4 @@ -import { SynthUtils } from '@aws-cdk/assert-internal'; -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as cdk from '@aws-cdk/core'; import * as iam from '../lib'; @@ -25,7 +24,7 @@ describe('automatic cross-stack references', () => { // // THEN - expect(stackWithUser).toMatchTemplate({ + Template.fromStack(stackWithUser).templateMatches({ Resources: { User00B015A1: { Type: 'AWS::IAM::User', @@ -35,7 +34,7 @@ describe('automatic cross-stack references', () => { }, }, }); - expect(stackWithGroup).toMatchTemplate({ + Template.fromStack(stackWithGroup).templateMatches({ Outputs: { ExportsOutputRefGroupC77FDACD8CF7DD5B: { Value: { Ref: 'GroupC77FDACD' }, @@ -59,6 +58,6 @@ describe('automatic cross-stack references', () => { group.addUser(user); // THEN - expect(() => SynthUtils.synthesize(stack1)).toThrow(/Cannot reference across apps/); + expect(() => cdk.App.of(stack1)!.synth()).toThrow(/Cannot reference across apps/); }); }); diff --git a/packages/@aws-cdk/aws-iam/test/cross-account.test.ts b/packages/@aws-cdk/aws-iam/test/cross-account.test.ts index 21ca3ce48c945..12eef2860ce1e 100644 --- a/packages/@aws-cdk/aws-iam/test/cross-account.test.ts +++ b/packages/@aws-cdk/aws-iam/test/cross-account.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as cdk from '@aws-cdk/core'; import * as constructs from 'constructs'; import * as iam from '../lib'; @@ -191,7 +191,7 @@ function doGrant(resource: FakeResource, principal: iam.IPrincipal) { } function assertTrustCreated(stack: cdk.Stack, principal: any) { - expect(stack).toHaveResource('Test::Fake::Resource', { + Template.fromStack(stack).hasResourceProperties('Test::Fake::Resource', { ResourcePolicy: { Statement: [ { @@ -207,17 +207,17 @@ function assertTrustCreated(stack: cdk.Stack, principal: any) { } function noTrustCreated(stack: cdk.Stack) { - expect(stack).not.toHaveResourceLike('Test::Fake::Resource', { + expect(Template.fromStack(stack).findResources('Test::Fake::Resource', { ResourcePolicy: { Statement: [ {}, ], }, - }); + })).toEqual({}); } function assertPolicyCreated(stack: cdk.Stack) { - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -232,5 +232,5 @@ function assertPolicyCreated(stack: cdk.Stack) { } function noPolicyCreated(stack: cdk.Stack) { - expect(stack).not.toHaveResource('AWS::IAM::Policy'); + Template.fromStack(stack).resourceCountIs('AWS::IAM::Policy', 0); } diff --git a/packages/@aws-cdk/aws-iam/test/escape-hatch.test.ts b/packages/@aws-cdk/aws-iam/test/escape-hatch.test.ts index b9467638c7307..f22639c83c447 100644 --- a/packages/@aws-cdk/aws-iam/test/escape-hatch.test.ts +++ b/packages/@aws-cdk/aws-iam/test/escape-hatch.test.ts @@ -1,7 +1,7 @@ // tests for the L1 escape hatches (overrides). those are in the IAM module // because we want to verify them end-to-end, as a complement to the unit // tests in the @aws-cdk/core module -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import { Stack } from '@aws-cdk/core'; import * as iam from '../lib'; @@ -17,7 +17,7 @@ describe('IAM escape hatches', () => { const cfn = user.node.findChild('Resource') as iam.CfnUser; cfn.addPropertyOverride('UserName', 'OverriddenUserName'); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ 'Resources': { 'user2C2B57AE': { 'Type': 'AWS::IAM::User', @@ -39,7 +39,7 @@ describe('IAM escape hatches', () => { cfn.addPropertyOverride('Hello.World', 'Boom'); // THEN - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ 'Resources': { 'user2C2B57AE': { 'Type': 'AWS::IAM::User', @@ -69,7 +69,7 @@ describe('IAM escape hatches', () => { cfn.addOverride('UpdatePolicy.UseOnlineResharding.Type', 'None'); // THEN - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ 'Resources': { 'user2C2B57AE': { 'Type': 'AWS::IAM::User', diff --git a/packages/@aws-cdk/aws-iam/test/grant.test.ts b/packages/@aws-cdk/aws-iam/test/grant.test.ts index 04b2466ea21da..1a4ca30016d6c 100644 --- a/packages/@aws-cdk/aws-iam/test/grant.test.ts +++ b/packages/@aws-cdk/aws-iam/test/grant.test.ts @@ -1,5 +1,4 @@ -import { ResourcePart } from '@aws-cdk/assert-internal'; -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import { CfnResource, Resource, Stack } from '@aws-cdk/core'; import { Construct } from 'constructs'; import * as iam from '../lib'; @@ -109,9 +108,9 @@ function applyGrantWithDependencyTo(principal: iam.IPrincipal) { } function expectDependencyOn(id: string) { - expect(stack).toHaveResource('CDK::Test::SomeResource', (props: any) => { + Template.fromStack(stack).hasResource('CDK::Test::SomeResource', (props: any) => { return (props?.DependsOn ?? []).includes(id); - }, ResourcePart.CompleteDefinition); + }); } class FakeResourceWithPolicy extends Resource implements iam.IResourceWithPolicy { diff --git a/packages/@aws-cdk/aws-iam/test/group.test.ts b/packages/@aws-cdk/aws-iam/test/group.test.ts index 6fc7129a3a1d9..781d2b2ece0b5 100644 --- a/packages/@aws-cdk/aws-iam/test/group.test.ts +++ b/packages/@aws-cdk/aws-iam/test/group.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import { App, Stack } from '@aws-cdk/core'; import { Group, ManagedPolicy, User } from '../lib'; @@ -8,7 +8,7 @@ describe('IAM groups', () => { const stack = new Stack(app, 'MyStack'); new Group(stack, 'MyGroup'); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ Resources: { MyGroupCBA54B1B: { Type: 'AWS::IAM::Group' } }, }); }); @@ -22,7 +22,7 @@ describe('IAM groups', () => { user1.addToGroup(group); group.addUser(user2); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ Resources: { MyGroupCBA54B1B: { Type: 'AWS::IAM::Group' }, @@ -50,7 +50,7 @@ describe('IAM groups', () => { }); // THEN - expect(stack).toHaveResource('AWS::IAM::Group', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Group', { ManagedPolicyArns: [ { 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':iam::aws:policy/asdf']] }, ], diff --git a/packages/@aws-cdk/aws-iam/test/immutable-role.test.ts b/packages/@aws-cdk/aws-iam/test/immutable-role.test.ts index 3ba5c870f8659..12cb38dc9542b 100644 --- a/packages/@aws-cdk/aws-iam/test/immutable-role.test.ts +++ b/packages/@aws-cdk/aws-iam/test/immutable-role.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import { Stack } from '@aws-cdk/core'; import * as iam from '../lib'; @@ -33,7 +33,7 @@ describe('ImmutableRole', () => { immutableRole.attachInlinePolicy(policy); - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { 'PolicyDocument': { 'Statement': [ { @@ -58,7 +58,7 @@ describe('ImmutableRole', () => { immutableRole.addManagedPolicy({ managedPolicyArn: 'Arn2' }); - expect(stack).toHaveResourceLike('AWS::IAM::Role', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { 'ManagedPolicyArns': [ 'Arn1', ], @@ -76,7 +76,7 @@ describe('ImmutableRole', () => { actions: ['s3:*'], })); - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { 'PolicyDocument': { 'Version': '2012-10-17', 'Statement': [ @@ -98,7 +98,7 @@ describe('ImmutableRole', () => { resourceArns: ['*'], }); - expect(stack).not.toHaveResourceLike('AWS::IAM::Policy', { + expect(Template.fromStack(stack).findResources('AWS::IAM::Policy', { 'PolicyDocument': { 'Statement': [ { @@ -108,7 +108,7 @@ describe('ImmutableRole', () => { }, ], }, - }); + })).toEqual({}); }); // this pattern is used here: diff --git a/packages/@aws-cdk/aws-iam/test/lazy-role.test.ts b/packages/@aws-cdk/aws-iam/test/lazy-role.test.ts index 34d8919ccd14c..a21baf9fb5963 100644 --- a/packages/@aws-cdk/aws-iam/test/lazy-role.test.ts +++ b/packages/@aws-cdk/aws-iam/test/lazy-role.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as cdk from '@aws-cdk/core'; import * as iam from '../lib'; @@ -13,7 +13,7 @@ describe('IAM lazy role', () => { }); // THEN - expect(stack).not.toHaveResource('AWS::IAM::Role'); + Template.fromStack(stack).resourceCountIs('AWS::IAM::Role', 0); }); test('creates the resource when a property is read', () => { @@ -27,7 +27,7 @@ describe('IAM lazy role', () => { // THEN expect(roleArn).not.toBeNull(); - expect(stack).toHaveResource('AWS::IAM::Role', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { AssumeRolePolicyDocument: { Version: '2012-10-17', Statement: [{ diff --git a/packages/@aws-cdk/aws-iam/test/managed-policy.test.ts b/packages/@aws-cdk/aws-iam/test/managed-policy.test.ts index 9ed969b4a0afe..718915956d29b 100644 --- a/packages/@aws-cdk/aws-iam/test/managed-policy.test.ts +++ b/packages/@aws-cdk/aws-iam/test/managed-policy.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as cdk from '@aws-cdk/core'; import { Group, ManagedPolicy, PolicyDocument, PolicyStatement, Role, ServicePrincipal, User } from '../lib'; @@ -49,7 +49,7 @@ describe('managed policy', () => { const group = new Group(stack, 'MyGroup'); group.addManagedPolicy(policy); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ Resources: { MyManagedPolicy9F3720AE: { Type: 'AWS::IAM::ManagedPolicy', @@ -89,7 +89,7 @@ describe('managed policy', () => { }), }); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ Resources: { MyManagedPolicy9F3720AE: { Type: 'AWS::IAM::ManagedPolicy', @@ -120,7 +120,7 @@ describe('managed policy', () => { statements: [new PolicyStatement({ resources: ['arn'], actions: ['sns:Subscribe'] })], }); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ Resources: { MyManagedPolicy9F3720AE: { Type: 'AWS::IAM::ManagedPolicy', @@ -148,7 +148,7 @@ describe('managed policy', () => { const group = new Group(stack, 'MyGroup'); group.addManagedPolicy(policy); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ Resources: { MyManagedPolicy9F3720AE: { Type: 'AWS::IAM::ManagedPolicy', @@ -192,7 +192,7 @@ describe('managed policy', () => { statements: [new PolicyStatement({ resources: ['*'], actions: ['dynamodb:PutItem'] })], }); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ Resources: { User1E278A736: { Type: 'AWS::IAM::User' }, Group1BEBD4686: { Type: 'AWS::IAM::Group' }, @@ -248,7 +248,7 @@ describe('managed policy', () => { p.attachToRole(role); p.attachToRole(role); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ Resources: { MyManagedPolicy9F3720AE: { Type: 'AWS::IAM::ManagedPolicy', @@ -295,7 +295,7 @@ describe('managed policy', () => { p.attachToRole(new Role(stack, 'Role1', { assumedBy: new ServicePrincipal('test.service') })); p.addStatements(new PolicyStatement({ resources: ['*'], actions: ['dynamodb:GetItem'] })); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ Resources: { MyManagedPolicy9F3720AE: { Type: 'AWS::IAM::ManagedPolicy', @@ -346,7 +346,7 @@ describe('managed policy', () => { policy.addStatements(new PolicyStatement({ resources: ['*'], actions: ['*'] })); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ Resources: { MyManagedPolicy9F3720AE: { Type: 'AWS::IAM::ManagedPolicy', @@ -390,7 +390,7 @@ describe('managed policy', () => { group.addManagedPolicy(policy); role.addManagedPolicy(policy); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ Resources: { MyUserDC45028B: { Type: 'AWS::IAM::User', @@ -466,7 +466,7 @@ describe('managed policy', () => { group.addManagedPolicy(policy); role.addManagedPolicy(policy); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ Resources: { MyUserDC45028B: { Type: 'AWS::IAM::User', @@ -594,7 +594,7 @@ describe('managed policy', () => { value: mp.managedPolicyArn, }); - expect(stack2).toMatchTemplate({ + Template.fromStack(stack2).templateMatches({ Outputs: { Output: { Value: { diff --git a/packages/@aws-cdk/aws-iam/test/oidc-provider.test.ts b/packages/@aws-cdk/aws-iam/test/oidc-provider.test.ts index 805e7bbb66ac0..eda196a97aa5f 100644 --- a/packages/@aws-cdk/aws-iam/test/oidc-provider.test.ts +++ b/packages/@aws-cdk/aws-iam/test/oidc-provider.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import { App, Stack, Token } from '@aws-cdk/core'; import * as sinon from 'sinon'; import * as iam from '../lib'; @@ -20,7 +20,7 @@ describe('OpenIdConnectProvider resource', () => { }); // THEN - expect(stack).toHaveResource('Custom::AWSCDKOpenIdConnectProvider', { + Template.fromStack(stack).hasResourceProperties('Custom::AWSCDKOpenIdConnectProvider', { Url: 'https://openid-endpoint', }); }); @@ -61,7 +61,7 @@ describe('OpenIdConnectProvider resource', () => { }); // THEN - expect(stack).toHaveResource('Custom::AWSCDKOpenIdConnectProvider', { + Template.fromStack(stack).hasResourceProperties('Custom::AWSCDKOpenIdConnectProvider', { Url: 'https://my-url', ClientIDList: ['client1', 'client2'], ThumbprintList: ['thumb1'], @@ -103,7 +103,7 @@ describe('custom resource provider infrastructure', () => { new iam.OpenIdConnectProvider(stack, 'Provider1', { url: 'provider1' }); // THEN - expect(stack).toHaveResource('AWS::IAM::Role', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { Policies: [ { PolicyName: 'Inline', diff --git a/packages/@aws-cdk/aws-iam/test/permissions-boundary.test.ts b/packages/@aws-cdk/aws-iam/test/permissions-boundary.test.ts index ab304f3481a15..b30137798074f 100644 --- a/packages/@aws-cdk/aws-iam/test/permissions-boundary.test.ts +++ b/packages/@aws-cdk/aws-iam/test/permissions-boundary.test.ts @@ -1,6 +1,5 @@ import * as path from 'path'; -import { ABSENT } from '@aws-cdk/assert-internal'; -import '@aws-cdk/assert-internal/jest'; +import { Match, Template } from '@aws-cdk/assertions'; import { App, CfnResource, CustomResourceProvider, CustomResourceProviderRuntime, Stack } from '@aws-cdk/core'; import * as iam from '../lib'; @@ -21,7 +20,7 @@ test('apply imported boundary to a role', () => { iam.PermissionsBoundary.of(role).apply(iam.ManagedPolicy.fromAwsManagedPolicyName('ReadOnlyAccess')); // THEN - expect(stack).toHaveResource('AWS::IAM::Role', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { PermissionsBoundary: { 'Fn::Join': ['', [ 'arn:', @@ -40,7 +39,7 @@ test('apply imported boundary to a user', () => { iam.PermissionsBoundary.of(user).apply(iam.ManagedPolicy.fromAwsManagedPolicyName('ReadOnlyAccess')); // THEN - expect(stack).toHaveResource('AWS::IAM::User', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::User', { PermissionsBoundary: { 'Fn::Join': ['', [ 'arn:', @@ -68,7 +67,7 @@ test('apply newly created boundary to a role', () => { })); // THEN - expect(stack).toHaveResource('AWS::IAM::Role', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { PermissionsBoundary: { Ref: 'Policy23B91518' }, }); }); @@ -91,7 +90,7 @@ test('apply boundary to role created by a custom resource', () => { })); // THEN - expect(stack).toHaveResource('AWS::IAM::Role', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { PermissionsBoundary: { Ref: 'Policy23B91518' }, }); }); @@ -113,7 +112,7 @@ test('apply boundary to users created via CfnResource', () => { })); // THEN - expect(stack).toHaveResource('AWS::IAM::User', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::User', { PermissionsBoundary: { Ref: 'Policy23B91518' }, }); }); @@ -135,7 +134,7 @@ test('apply boundary to roles created via CfnResource', () => { })); // THEN - expect(stack).toHaveResource('AWS::IAM::Role', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { PermissionsBoundary: { Ref: 'Policy23B91518' }, }); }); @@ -149,8 +148,8 @@ test('unapply inherited boundary from a user: order 1', () => { iam.PermissionsBoundary.of(user).clear(); // THEN - expect(stack).toHaveResource('AWS::IAM::User', { - PermissionsBoundary: ABSENT, + Template.fromStack(stack).hasResourceProperties('AWS::IAM::User', { + PermissionsBoundary: Match.absent(), }); }); @@ -163,7 +162,7 @@ test('unapply inherited boundary from a user: order 2', () => { iam.PermissionsBoundary.of(stack).apply(iam.ManagedPolicy.fromAwsManagedPolicyName('ReadOnlyAccess')); // THEN - expect(stack).toHaveResource('AWS::IAM::User', { - PermissionsBoundary: ABSENT, + Template.fromStack(stack).hasResourceProperties('AWS::IAM::User', { + PermissionsBoundary: Match.absent(), }); }); diff --git a/packages/@aws-cdk/aws-iam/test/policy-document.test.ts b/packages/@aws-cdk/aws-iam/test/policy-document.test.ts index 16a9904087647..ca759d438ab89 100644 --- a/packages/@aws-cdk/aws-iam/test/policy-document.test.ts +++ b/packages/@aws-cdk/aws-iam/test/policy-document.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import { testDeprecated } from '@aws-cdk/cdk-build-tools'; import { Lazy, Stack, Token } from '@aws-cdk/core'; import { @@ -492,7 +492,7 @@ describe('IAM policy document', () => { ), }); - expect(stack).toHaveResourceLike('AWS::IAM::Role', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { AssumeRolePolicyDocument: { Statement: [ { @@ -568,7 +568,7 @@ describe('IAM policy document', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::IAM::Role', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { AssumeRolePolicyDocument: { Statement: [ { diff --git a/packages/@aws-cdk/aws-iam/test/policy-statement.test.ts b/packages/@aws-cdk/aws-iam/test/policy-statement.test.ts index 09b6e30b1e4c0..7498ee2814d80 100644 --- a/packages/@aws-cdk/aws-iam/test/policy-statement.test.ts +++ b/packages/@aws-cdk/aws-iam/test/policy-statement.test.ts @@ -1,4 +1,3 @@ -import '@aws-cdk/assert-internal/jest'; import { Stack } from '@aws-cdk/core'; import { AnyPrincipal, Group, PolicyDocument, PolicyStatement } from '../lib'; diff --git a/packages/@aws-cdk/aws-iam/test/policy.test.ts b/packages/@aws-cdk/aws-iam/test/policy.test.ts index cb6c2fc88cf52..ea40450756935 100644 --- a/packages/@aws-cdk/aws-iam/test/policy.test.ts +++ b/packages/@aws-cdk/aws-iam/test/policy.test.ts @@ -1,5 +1,4 @@ -import { ResourcePart } from '@aws-cdk/assert-internal'; -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import { App, CfnResource, Stack } from '@aws-cdk/core'; import { AnyPrincipal, CfnPolicy, Group, Policy, PolicyDocument, PolicyStatement, Role, ServicePrincipal, User } from '../lib'; @@ -28,7 +27,7 @@ describe('IAM policy', () => { const group = new Group(stack, 'MyGroup'); group.attachInlinePolicy(policy); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ Resources: { MyPolicy39D66CF6: @@ -69,7 +68,7 @@ describe('IAM policy', () => { const group = new Group(stack, 'MyGroup'); group.attachInlinePolicy(policy); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ Resources: { MyPolicy39D66CF6: { Type: 'AWS::IAM::Policy', @@ -96,7 +95,7 @@ describe('IAM policy', () => { const user = new User(stack, 'MyUser'); user.attachInlinePolicy(policy); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ Resources: { MyPolicy39D66CF6: @@ -135,7 +134,7 @@ describe('IAM policy', () => { statements: [new PolicyStatement({ resources: ['*'], actions: ['dynamodb:PutItem'] })], }); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ Resources: { User1E278A736: { Type: 'AWS::IAM::User' }, @@ -186,7 +185,7 @@ describe('IAM policy', () => { p.attachToUser(user); p.attachToUser(user); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ Resources: { MyPolicy39D66CF6: @@ -219,7 +218,7 @@ describe('IAM policy', () => { p.attachToRole(new Role(stack, 'Role1', { assumedBy: new ServicePrincipal('test.service') })); p.addStatements(new PolicyStatement({ resources: ['*'], actions: ['dynamodb:GetItem'] })); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ Resources: { MyTestPolicy316BDB50: @@ -275,7 +274,7 @@ describe('IAM policy', () => { policy.addStatements(new PolicyStatement({ resources: ['*'], actions: ['*'] })); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ Resources: { MyPolicy39D66CF6: @@ -349,7 +348,7 @@ describe('IAM policy', () => { test("generated policy name is the same as the logical id if it's shorter than 128 characters", () => { createPolicyWithLogicalId(stack, 'Foo'); - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { 'PolicyName': 'Foo', }); }); @@ -360,7 +359,7 @@ describe('IAM policy', () => { createPolicyWithLogicalId(stack, logicalIdOver128); - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { 'PolicyName': logicalId128, }); @@ -385,7 +384,7 @@ describe('IAM policy', () => { res.node.addDependency(pol); // THEN - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ Resources: { Resource: { Type: 'Some::Resource', @@ -411,10 +410,10 @@ describe('IAM policy', () => { res.node.addDependency(pol); // THEN - expect(stack).toHaveResource('Some::Resource', { + Template.fromStack(stack).hasResource('Some::Resource', { Type: 'Some::Resource', DependsOn: ['Pol0FE9AD5D'], - }, ResourcePart.CompleteDefinition); + }); }); test('empty policy is OK if force=false', () => { diff --git a/packages/@aws-cdk/aws-iam/test/principals.test.ts b/packages/@aws-cdk/aws-iam/test/principals.test.ts index fdbeaa0dc87d4..7a07a50e80fb9 100644 --- a/packages/@aws-cdk/aws-iam/test/principals.test.ts +++ b/packages/@aws-cdk/aws-iam/test/principals.test.ts @@ -1,4 +1,3 @@ -import '@aws-cdk/assert-internal/jest'; import { Template } from '@aws-cdk/assertions'; import { App, CfnOutput, Stack } from '@aws-cdk/core'; import * as iam from '../lib'; @@ -21,7 +20,7 @@ test('use of cross-stack role reference does not lead to URLSuffix being exporte // THEN app.synth(); - expect(first).toMatchTemplate({ + Template.fromStack(first).templateMatches({ Resources: { Role1ABCC5F0: { Type: 'AWS::IAM::Role', @@ -145,7 +144,7 @@ test('SAML principal', () => { // THEN expect(stack.resolve(principal.federated)).toStrictEqual({ Ref: 'MyProvider730BA1C8' }); - expect(stack).toHaveResource('AWS::IAM::Role', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { AssumeRolePolicyDocument: { Statement: [ { @@ -214,7 +213,7 @@ test('PrincipalWithConditions.addCondition should work', () => { }); // THEN - expect(stack).toHaveResource('AWS::IAM::Role', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { AssumeRolePolicyDocument: { Statement: [ { @@ -277,7 +276,7 @@ test('Can enable session tags', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::IAM::Role', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { AssumeRolePolicyDocument: { Statement: [ { diff --git a/packages/@aws-cdk/aws-iam/test/role.from-role-arn.test.ts b/packages/@aws-cdk/aws-iam/test/role.from-role-arn.test.ts index 68e36b388fa0f..d03dd908370da 100644 --- a/packages/@aws-cdk/aws-iam/test/role.from-role-arn.test.ts +++ b/packages/@aws-cdk/aws-iam/test/role.from-role-arn.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import { App, Aws, CfnElement, Lazy, Stack } from '@aws-cdk/core'; import { AnyPrincipal, ArnPrincipal, IRole, Policy, PolicyStatement, Role } from '../lib'; @@ -196,7 +196,7 @@ describe('IAM Role.fromRoleArn', () => { }); test("does NOT generate a default Policy resource pointing at the imported role's physical name", () => { - expect(roleStack).not.toHaveResourceLike('AWS::IAM::Policy'); + Template.fromStack(roleStack).resourceCountIs('AWS::IAM::Policy', 0); }); }); @@ -283,7 +283,7 @@ describe('IAM Role.fromRoleArn', () => { }); test("does NOT generate a default Policy resource pointing at the imported role's physical name", () => { - expect(roleStack).not.toHaveResourceLike('AWS::IAM::Policy'); + Template.fromStack(roleStack).resourceCountIs('AWS::IAM::Policy', 0); }); }); @@ -322,7 +322,7 @@ describe('IAM Role.fromRoleArn', () => { }); test("does NOT generate a default Policy resource pointing at the imported role's physical name", () => { - expect(roleStack).not.toHaveResourceLike('AWS::IAM::Policy'); + Template.fromStack(roleStack).resourceCountIs('AWS::IAM::Policy', 0); }); }); }); @@ -357,7 +357,7 @@ describe('IAM Role.fromRoleArn', () => { assumedBy: new ArnPrincipal(importedRole.roleName), }); - expect(roleStack).toHaveResourceLike('AWS::IAM::Role', { + Template.fromStack(roleStack).hasResourceProperties('AWS::IAM::Role', { 'AssumeRolePolicyDocument': { 'Statement': [ { @@ -515,7 +515,7 @@ describe('IAM Role.fromRoleArn', () => { roles: [importedRole], }); - expect(roleStack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(roleStack).hasResourceProperties('AWS::IAM::Policy', { 'Roles': [ 'codebuild-role', ], @@ -535,7 +535,7 @@ describe('IAM Role.fromRoleArn', () => { roles: [importedRole], }); - expect(roleStack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(roleStack).hasResourceProperties('AWS::IAM::Policy', { 'Roles': [ 'codebuild-role', ], @@ -611,5 +611,5 @@ function _assertStackContainsPolicyResource(stack: Stack, roleNames: any[], name expected.PolicyName = nameOfPolicy; } - expect(stack).toHaveResourceLike('AWS::IAM::Policy', expected); + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', expected); } diff --git a/packages/@aws-cdk/aws-iam/test/role.test.ts b/packages/@aws-cdk/aws-iam/test/role.test.ts index ffa1ccd305c58..e0927e14b80ad 100644 --- a/packages/@aws-cdk/aws-iam/test/role.test.ts +++ b/packages/@aws-cdk/aws-iam/test/role.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import { testDeprecated } from '@aws-cdk/cdk-build-tools'; import { Duration, Stack, App } from '@aws-cdk/core'; import { AnyPrincipal, ArnPrincipal, CompositePrincipal, FederatedPrincipal, ManagedPolicy, PolicyStatement, Role, ServicePrincipal, User, Policy, PolicyDocument } from '../lib'; @@ -11,7 +11,7 @@ describe('IAM role', () => { assumedBy: new ServicePrincipal('sns.amazonaws.com'), }); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ Resources: { MyRoleF48FFE04: @@ -45,7 +45,7 @@ describe('IAM role', () => { role.grantPassRole(user); // THEN - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -70,7 +70,7 @@ describe('IAM role', () => { }); // THEN - expect(stack).toHaveResource('AWS::IAM::Role', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { AssumeRolePolicyDocument: { Statement: [ { @@ -98,7 +98,7 @@ describe('IAM role', () => { }); // THEN - expect(stack).toHaveResource('AWS::IAM::Role', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { AssumeRolePolicyDocument: { Statement: [ { @@ -126,7 +126,7 @@ describe('IAM role', () => { }); // THEN - expect(stack).toHaveResource('AWS::IAM::Role', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { AssumeRolePolicyDocument: { Statement: [ { @@ -147,13 +147,13 @@ describe('IAM role', () => { // by default we don't expect a role policy const before = new Stack(); new Role(before, 'MyRole', { assumedBy: new ServicePrincipal('sns.amazonaws.com') }); - expect(before).not.toHaveResource('AWS::IAM::Policy'); + Template.fromStack(before).resourceCountIs('AWS::IAM::Policy', 0); // add a policy to the role const after = new Stack(); const afterRole = new Role(after, 'MyRole', { assumedBy: new ServicePrincipal('sns.amazonaws.com') }); afterRole.addToPolicy(new PolicyStatement({ resources: ['myresource'], actions: ['service:myaction'] })); - expect(after).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(after).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -183,7 +183,7 @@ describe('IAM role', () => { }); role.addManagedPolicy({ managedPolicyArn: 'managed3' }); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ Resources: { MyRoleF48FFE04: @@ -218,7 +218,7 @@ describe('IAM role', () => { new Role(stack, 'MyRole', { assumedBy: cognitoPrincipal }); - expect(stack).toHaveResource('AWS::IAM::Role', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { AssumeRolePolicyDocument: { Version: '2012-10-17', Statement: [ @@ -240,7 +240,7 @@ describe('IAM role', () => { test('is not specified by default', () => { const stack = new Stack(); new Role(stack, 'MyRole', { assumedBy: new ServicePrincipal('sns.amazonaws.com') }); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ Resources: { MyRoleF48FFE04: { Type: 'AWS::IAM::Role', @@ -268,7 +268,7 @@ describe('IAM role', () => { new Role(stack, 'MyRole', { maxSessionDuration: Duration.seconds(3700), assumedBy: new ServicePrincipal('sns.amazonaws.com') }); - expect(stack).toHaveResource('AWS::IAM::Role', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { MaxSessionDuration: 3700, }); }); @@ -301,7 +301,7 @@ describe('IAM role', () => { ), }); - expect(stack).toHaveResource('AWS::IAM::Role', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { AssumeRolePolicyDocument: { Statement: [ { @@ -335,7 +335,7 @@ describe('IAM role', () => { permissionsBoundary, }); - expect(stack).toHaveResource('AWS::IAM::Role', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { PermissionsBoundary: { 'Fn::Join': [ '', @@ -363,7 +363,7 @@ describe('IAM role', () => { assumedBy: new AnyPrincipal(), }); - expect(stack).toHaveResource('AWS::IAM::Role', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { AssumeRolePolicyDocument: { Statement: [ { @@ -385,7 +385,7 @@ describe('IAM role', () => { description: 'This is a role description.', }); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ Resources: { MyRoleF48FFE04: @@ -418,7 +418,7 @@ describe('IAM role', () => { description: '', }); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ Resources: { MyRoleF48FFE04: @@ -555,7 +555,7 @@ test('managed policy ARNs are deduplicated', () => { }); role.addManagedPolicy(ManagedPolicy.fromAwsManagedPolicyName('SuperDeveloper')); - expect(stack).toHaveResource('AWS::IAM::Role', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { ManagedPolicyArns: [ { 'Fn::Join': [ diff --git a/packages/@aws-cdk/aws-iam/test/saml-provider.test.ts b/packages/@aws-cdk/aws-iam/test/saml-provider.test.ts index 83ade3741fb3a..edcbde4685296 100644 --- a/packages/@aws-cdk/aws-iam/test/saml-provider.test.ts +++ b/packages/@aws-cdk/aws-iam/test/saml-provider.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import { Stack } from '@aws-cdk/core'; import { SamlMetadataDocument, SamlProvider } from '../lib'; @@ -12,7 +12,7 @@ test('SAML provider', () => { metadataDocument: SamlMetadataDocument.fromXml('document'), }); - expect(stack).toHaveResource('AWS::IAM::SAMLProvider', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::SAMLProvider', { SamlMetadataDocument: 'document', }); }); @@ -23,7 +23,7 @@ test('SAML provider name', () => { name: 'provider-name', }); - expect(stack).toHaveResource('AWS::IAM::SAMLProvider', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::SAMLProvider', { SamlMetadataDocument: 'document', Name: 'provider-name', }); diff --git a/packages/@aws-cdk/aws-iam/test/user.test.ts b/packages/@aws-cdk/aws-iam/test/user.test.ts index d7435b08cbf24..83e610e43a51b 100644 --- a/packages/@aws-cdk/aws-iam/test/user.test.ts +++ b/packages/@aws-cdk/aws-iam/test/user.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import { App, SecretValue, Stack, Token } from '@aws-cdk/core'; import { Group, ManagedPolicy, Policy, PolicyStatement, User } from '../lib'; @@ -7,7 +7,7 @@ describe('IAM user', () => { const app = new App(); const stack = new Stack(app, 'MyStack'); new User(stack, 'MyUser'); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ Resources: { MyUserDC45028B: { Type: 'AWS::IAM::User' } }, }); }); @@ -19,7 +19,7 @@ describe('IAM user', () => { password: SecretValue.plainText('1234'), }); - expect(stack).toMatchTemplate({ + Template.fromStack(stack).templateMatches({ Resources: { MyUserDC45028B: @@ -48,7 +48,7 @@ describe('IAM user', () => { }); // THEN - expect(stack).toHaveResource('AWS::IAM::User', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::User', { ManagedPolicyArns: [ { 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':iam::aws:policy/asdf']] }, ], @@ -65,7 +65,7 @@ describe('IAM user', () => { permissionsBoundary, }); - expect(stack).toHaveResource('AWS::IAM::User', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::User', { PermissionsBoundary: { 'Fn::Join': [ '', @@ -212,7 +212,7 @@ describe('IAM user', () => { })); // THEN - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { Users: ['john'], PolicyDocument: { Statement: [ @@ -243,7 +243,7 @@ describe('IAM user', () => { })); // THEN - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { Users: ['john'], PolicyDocument: { Statement: [ @@ -270,7 +270,7 @@ describe('IAM user', () => { otherGroup.addUser(user); // THEN - expect(stack).toHaveResource('AWS::IAM::UserToGroupAddition', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::UserToGroupAddition', { GroupName: { Ref: 'GroupC77FDACD', }, @@ -279,7 +279,7 @@ describe('IAM user', () => { ], }); - expect(stack).toHaveResource('AWS::IAM::UserToGroupAddition', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::UserToGroupAddition', { GroupName: { Ref: 'OtherGroup85E5C653', }, diff --git a/packages/@aws-cdk/aws-iotevents/README.md b/packages/@aws-cdk/aws-iotevents/README.md index 6dc6a681636cc..fe071d7baecc6 100644 --- a/packages/@aws-cdk/aws-iotevents/README.md +++ b/packages/@aws-cdk/aws-iotevents/README.md @@ -40,15 +40,30 @@ Import it into your code: import * as iotevents from '@aws-cdk/aws-iotevents'; ``` -## `Input` +## `DetectorModel` -Add an AWS IoT Events input to your stack: +The following example creates an AWS IoT Events detector model to your stack. +The detector model need a reference to at least one AWS IoT Events input. +AWS IoT Events inputs enable the detector to get MQTT payload values from IoT Core rules. ```ts import * as iotevents from '@aws-cdk/aws-iotevents'; -new iotevents.Input(this, 'MyInput', { - inputName: 'my_input', +const input = new iotevents.Input(this, 'MyInput', { + inputName: 'my_input', // optional attributeJsonPaths: ['payload.temperature'], }); + +const onlineState = new iotevents.State({ + stateName: 'online', + onEnter: [{ + eventName: 'test-event', + condition: iotevents.Expression.currentInput(input), + }], +}); + +new iotevents.DetectorModel(this, 'MyDetectorModel', { + detectorModelName: 'test-detector-model', // optional + initialState: onlineState, +}); ``` diff --git a/packages/@aws-cdk/aws-iotevents/lib/detector-model.ts b/packages/@aws-cdk/aws-iotevents/lib/detector-model.ts new file mode 100644 index 0000000000000..2a5d270fb0cde --- /dev/null +++ b/packages/@aws-cdk/aws-iotevents/lib/detector-model.ts @@ -0,0 +1,82 @@ +import * as iam from '@aws-cdk/aws-iam'; +import { Resource, IResource } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { CfnDetectorModel } from './iotevents.generated'; +import { State } from './state'; + +/** + * Represents an AWS IoT Events detector model + */ +export interface IDetectorModel extends IResource { + /** + * The name of the detector model. + * + * @attribute + */ + readonly detectorModelName: string; +} + +/** + * Properties for defining an AWS IoT Events detector model + */ +export interface DetectorModelProps { + /** + * The name of the detector model. + * + * @default - CloudFormation will generate a unique name of the detector model + */ + readonly detectorModelName?: string; + + /** + * The state that is entered at the creation of each detector. + */ + readonly initialState: State; + + /** + * The role that grants permission to AWS IoT Events to perform its operations. + * + * @default - a role will be created with default permissions + */ + readonly role?: iam.IRole; +} + +/** + * Defines an AWS IoT Events detector model in this stack. + */ +export class DetectorModel extends Resource implements IDetectorModel { + /** + * Import an existing detector model. + */ + public static fromDetectorModelName(scope: Construct, id: string, detectorModelName: string): IDetectorModel { + return new class extends Resource implements IDetectorModel { + public readonly detectorModelName = detectorModelName; + }(scope, id); + } + + public readonly detectorModelName: string; + + constructor(scope: Construct, id: string, props: DetectorModelProps) { + super(scope, id, { + physicalName: props.detectorModelName, + }); + + if (!props.initialState._onEnterEventsHaveAtLeastOneCondition()) { + throw new Error('Detector Model must have at least one Input with a condition'); + } + + const role = props.role ?? new iam.Role(this, 'DetectorModelRole', { + assumedBy: new iam.ServicePrincipal('iotevents.amazonaws.com'), + }); + + const resource = new CfnDetectorModel(this, 'Resource', { + detectorModelName: this.physicalName, + detectorModelDefinition: { + initialStateName: props.initialState.stateName, + states: [props.initialState._toStateJson()], + }, + roleArn: role.roleArn, + }); + + this.detectorModelName = this.getResourceNameAttribute(resource.ref); + } +} diff --git a/packages/@aws-cdk/aws-iotevents/lib/event.ts b/packages/@aws-cdk/aws-iotevents/lib/event.ts new file mode 100644 index 0000000000000..610469db9c32c --- /dev/null +++ b/packages/@aws-cdk/aws-iotevents/lib/event.ts @@ -0,0 +1,18 @@ +import { Expression } from './expression'; + +/** + * Specifies the actions to be performed when the condition evaluates to TRUE. + */ +export interface Event { + /** + * The name of the event. + */ + readonly eventName: string; + + /** + * The Boolean expression that, when TRUE, causes the actions to be performed. + * + * @default - none (the actions are always executed) + */ + readonly condition?: Expression; +} diff --git a/packages/@aws-cdk/aws-iotevents/lib/expression.ts b/packages/@aws-cdk/aws-iotevents/lib/expression.ts new file mode 100644 index 0000000000000..27fdf069c1b9f --- /dev/null +++ b/packages/@aws-cdk/aws-iotevents/lib/expression.ts @@ -0,0 +1,75 @@ +import { IInput } from './input'; + +/** + * Expression for events in Detector Model state + * @see https://docs.aws.amazon.com/iotevents/latest/developerguide/iotevents-expressions.html + */ +export abstract class Expression { + /** + * Create a expression from the given string + */ + public static fromString(value: string): Expression { + return new StringExpression(value); + } + + /** + * Create a expression for function `currentInput()`. + * It is evaluated to true if the specified input message was received. + */ + public static currentInput(input: IInput): Expression { + return this.fromString(`currentInput("${input.inputName}")`); + } + + /** + * Create a expression for get an input attribute as `$input.TemperatureInput.temperatures[2]`. + */ + public static inputAttribute(input: IInput, path: string): Expression { + return this.fromString(`$input.${input.inputName}.${path}`); + } + + /** + * Create a expression for the Equal operator + */ + public static eq(left: Expression, right: Expression): Expression { + return new BinaryOperationExpression(left, '==', right); + } + + /** + * Create a expression for the AND operator + */ + public static and(left: Expression, right: Expression): Expression { + return new BinaryOperationExpression(left, '&&', right); + } + + constructor() { + } + + /** + * this is called to evaluate the expression + */ + public abstract evaluate(): string; +} + +class StringExpression extends Expression { + constructor(private readonly value: string) { + super(); + } + + public evaluate() { + return this.value; + } +} + +class BinaryOperationExpression extends Expression { + constructor( + private readonly left: Expression, + private readonly operator: string, + private readonly right: Expression, + ) { + super(); + } + + public evaluate() { + return `${this.left.evaluate()} ${this.operator} ${this.right.evaluate()}`; + } +} diff --git a/packages/@aws-cdk/aws-iotevents/lib/index.ts b/packages/@aws-cdk/aws-iotevents/lib/index.ts index 3851e30984391..24913635ebe50 100644 --- a/packages/@aws-cdk/aws-iotevents/lib/index.ts +++ b/packages/@aws-cdk/aws-iotevents/lib/index.ts @@ -1,4 +1,8 @@ +export * from './detector-model'; +export * from './event'; +export * from './expression'; export * from './input'; +export * from './state'; // AWS::IoTEvents CloudFormation Resources: export * from './iotevents.generated'; diff --git a/packages/@aws-cdk/aws-iotevents/lib/state.ts b/packages/@aws-cdk/aws-iotevents/lib/state.ts new file mode 100644 index 0000000000000..e16d911d60004 --- /dev/null +++ b/packages/@aws-cdk/aws-iotevents/lib/state.ts @@ -0,0 +1,65 @@ +import { Event } from './event'; +import { CfnDetectorModel } from './iotevents.generated'; + +/** + * Properties for defining a state of a detector + */ +export interface StateProps { + /** + * The name of the state. + */ + readonly stateName: string; + + /** + * Specifies the events on enter. the conditions of the events are evaluated when the state is entered. + * If the condition is `TRUE`, the actions of the event are performed. + * + * @default - events on enter will not be set + */ + readonly onEnter?: Event[]; +} + +/** + * Defines a state of a detector + */ +export class State { + /** + * The name of the state + */ + public readonly stateName: string; + + constructor(private readonly props: StateProps) { + this.stateName = props.stateName; + } + + /** + * Return the state property JSON + * + * @internal + */ + public _toStateJson(): CfnDetectorModel.StateProperty { + const { stateName, onEnter } = this.props; + return { + stateName, + onEnter: onEnter && { events: getEventJson(onEnter) }, + }; + } + + /** + * returns true if this state has at least one condition via events + * + * @internal + */ + public _onEnterEventsHaveAtLeastOneCondition(): boolean { + return this.props.onEnter?.some(event => event.condition) ?? false; + } +} + +function getEventJson(events: Event[]): CfnDetectorModel.EventProperty[] { + return events.map(e => { + return { + eventName: e.eventName, + condition: e.condition?.evaluate(), + }; + }); +} diff --git a/packages/@aws-cdk/aws-iotevents/package.json b/packages/@aws-cdk/aws-iotevents/package.json index 339b7d938a853..f9ac79e55395a 100644 --- a/packages/@aws-cdk/aws-iotevents/package.json +++ b/packages/@aws-cdk/aws-iotevents/package.json @@ -83,10 +83,12 @@ "jest": "^27.4.7" }, "dependencies": { + "@aws-cdk/aws-iam": "0.0.0", "@aws-cdk/core": "0.0.0", "constructs": "^3.3.69" }, "peerDependencies": { + "@aws-cdk/aws-iam": "0.0.0", "@aws-cdk/core": "0.0.0", "constructs": "^3.3.69" }, diff --git a/packages/@aws-cdk/aws-iotevents/test/detector-model.test.ts b/packages/@aws-cdk/aws-iotevents/test/detector-model.test.ts new file mode 100644 index 0000000000000..d6fbadd5baf9b --- /dev/null +++ b/packages/@aws-cdk/aws-iotevents/test/detector-model.test.ts @@ -0,0 +1,311 @@ +import { Match, Template } from '@aws-cdk/assertions'; +import * as iam from '@aws-cdk/aws-iam'; +import * as cdk from '@aws-cdk/core'; +import * as iotevents from '../lib'; + +let stack: cdk.Stack; +beforeEach(() => { + stack = new cdk.Stack(); +}); + +test('Default property', () => { + // WHEN + new iotevents.DetectorModel(stack, 'MyDetectorModel', { + initialState: new iotevents.State({ + stateName: 'test-state', + onEnter: [{ + eventName: 'test-eventName', + condition: iotevents.Expression.fromString('test-eventCondition'), + }], + }), + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::IoTEvents::DetectorModel', { + DetectorModelDefinition: { + InitialStateName: 'test-state', + States: [{ + StateName: 'test-state', + OnEnter: { + Events: [{ + EventName: 'test-eventName', + Condition: 'test-eventCondition', + }], + }, + }], + }, + RoleArn: { + 'Fn::GetAtt': ['MyDetectorModelDetectorModelRoleF2FB4D88', 'Arn'], + }, + }); + + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { + AssumeRolePolicyDocument: { + Statement: [{ + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { Service: 'iotevents.amazonaws.com' }, + }], + }, + }); +}); + +test('can get detector model name', () => { + // GIVEN + const detectorModel = new iotevents.DetectorModel(stack, 'MyDetectorModel', { + initialState: new iotevents.State({ + stateName: 'test-state', + onEnter: [{ + eventName: 'test-eventName', + condition: iotevents.Expression.fromString('test-eventCondition'), + }], + }), + }); + + // WHEN + new cdk.CfnResource(stack, 'Res', { + type: 'Test::Resource', + properties: { + DetectorModelName: detectorModel.detectorModelName, + }, + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('Test::Resource', { + DetectorModelName: { Ref: 'MyDetectorModel559C0B0E' }, + }); +}); + +test('can set physical name', () => { + // WHEN + new iotevents.DetectorModel(stack, 'MyDetectorModel', { + detectorModelName: 'test-detector-model', + initialState: new iotevents.State({ + stateName: 'test-state', + onEnter: [{ + eventName: 'test-eventName', + condition: iotevents.Expression.fromString('test-eventCondition'), + }], + }), + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::IoTEvents::DetectorModel', { + DetectorModelName: 'test-detector-model', + }); +}); + +test('can set multiple events to State', () => { + // WHEN + new iotevents.DetectorModel(stack, 'MyDetectorModel', { + initialState: new iotevents.State({ + stateName: 'test-state', + onEnter: [ + { + eventName: 'test-eventName1', + condition: iotevents.Expression.fromString('test-eventCondition'), + }, + { + eventName: 'test-eventName2', + }, + ], + }), + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::IoTEvents::DetectorModel', { + DetectorModelDefinition: { + States: [ + Match.objectLike({ + OnEnter: { + Events: [ + { + EventName: 'test-eventName1', + Condition: 'test-eventCondition', + }, + { + EventName: 'test-eventName2', + }, + ], + }, + }), + ], + }, + }); +}); + +test('can set role', () => { + // WHEN + const role = iam.Role.fromRoleArn(stack, 'test-role', 'arn:aws:iam::123456789012:role/ForTest'); + new iotevents.DetectorModel(stack, 'MyDetectorModel', { + role, + initialState: new iotevents.State({ + stateName: 'test-state', + onEnter: [{ + eventName: 'test-eventName', + condition: iotevents.Expression.fromString('test-eventCondition'), + }], + }), + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::IoTEvents::DetectorModel', { + RoleArn: 'arn:aws:iam::123456789012:role/ForTest', + }); +}); + +test('can import a DetectorModel by detectorModelName', () => { + // WHEN + const detectorModelName = 'detector-model-name'; + const detectorModel = iotevents.DetectorModel.fromDetectorModelName(stack, 'ExistingDetectorModel', detectorModelName); + + // THEN + expect(detectorModel).toMatchObject({ + detectorModelName: detectorModelName, + }); +}); + +test('cannot create without condition', () => { + expect(() => { + new iotevents.DetectorModel(stack, 'MyDetectorModel', { + initialState: new iotevents.State({ + stateName: 'test-state', + onEnter: [{ + eventName: 'test-eventName', + }], + }), + }); + }).toThrow('Detector Model must have at least one Input with a condition'); +}); + +test('cannot create without event', () => { + expect(() => { + new iotevents.DetectorModel(stack, 'MyDetectorModel', { + initialState: new iotevents.State({ + stateName: 'test-state', + }), + }); + }).toThrow('Detector Model must have at least one Input with a condition'); +}); + +describe('Expression', () => { + test('currentInput', () => { + // WHEN + const input = iotevents.Input.fromInputName(stack, 'MyInput', 'test-input'); + new iotevents.DetectorModel(stack, 'MyDetectorModel', { + initialState: new iotevents.State({ + stateName: 'test-state', + onEnter: [{ + eventName: 'test-eventName', + condition: iotevents.Expression.currentInput(input), + }], + }), + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::IoTEvents::DetectorModel', { + DetectorModelDefinition: { + States: [ + Match.objectLike({ + OnEnter: { + Events: [Match.objectLike({ + Condition: 'currentInput("test-input")', + })], + }, + }), + ], + }, + }); + }); + + test('inputAttribute', () => { + // WHEN + const input = iotevents.Input.fromInputName(stack, 'MyInput', 'test-input'); + new iotevents.DetectorModel(stack, 'MyDetectorModel', { + initialState: new iotevents.State({ + stateName: 'test-state', + onEnter: [{ + eventName: 'test-eventName', + condition: iotevents.Expression.inputAttribute(input, 'json.path'), + }], + }), + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::IoTEvents::DetectorModel', { + DetectorModelDefinition: { + States: [ + Match.objectLike({ + OnEnter: { + Events: [Match.objectLike({ + Condition: '$input.test-input.json.path', + })], + }, + }), + ], + }, + }); + }); + + test('eq', () => { + // WHEN + new iotevents.DetectorModel(stack, 'MyDetectorModel', { + initialState: new iotevents.State({ + stateName: 'test-state', + onEnter: [{ + eventName: 'test-eventName', + condition: iotevents.Expression.eq( + iotevents.Expression.fromString('"aaa"'), + iotevents.Expression.fromString('"bbb"'), + ), + }], + }), + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::IoTEvents::DetectorModel', { + DetectorModelDefinition: { + States: [ + Match.objectLike({ + OnEnter: { + Events: [Match.objectLike({ + Condition: '"aaa" == "bbb"', + })], + }, + }), + ], + }, + }); + }); + + test('eq', () => { + // WHEN + new iotevents.DetectorModel(stack, 'MyDetectorModel', { + initialState: new iotevents.State({ + stateName: 'test-state', + onEnter: [{ + eventName: 'test-eventName', + condition: iotevents.Expression.and( + iotevents.Expression.fromString('true'), + iotevents.Expression.fromString('false'), + ), + }], + }), + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::IoTEvents::DetectorModel', { + DetectorModelDefinition: { + States: [ + Match.objectLike({ + OnEnter: { + Events: [Match.objectLike({ + Condition: 'true && false', + })], + }, + }), + ], + }, + }); + }); +}); diff --git a/packages/@aws-cdk/aws-iotevents/test/integ.detector-model.expected.json b/packages/@aws-cdk/aws-iotevents/test/integ.detector-model.expected.json index 1f5d452b5475d..f97d40bc6da25 100644 --- a/packages/@aws-cdk/aws-iotevents/test/integ.detector-model.expected.json +++ b/packages/@aws-cdk/aws-iotevents/test/integ.detector-model.expected.json @@ -12,6 +12,66 @@ }, "InputName": "test_input" } + }, + "MyDetectorModelDetectorModelRoleF2FB4D88": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "iotevents.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "MyDetectorModel559C0B0E": { + "Type": "AWS::IoTEvents::DetectorModel", + "Properties": { + "DetectorModelDefinition": { + "InitialStateName": "online", + "States": [ + { + "OnEnter": { + "Events": [ + { + "Condition": { + "Fn::Join": [ + "", + [ + "currentInput(\"", + { + "Ref": "MyInput08947B23" + }, + "\") && $input.", + { + "Ref": "MyInput08947B23" + }, + ".payload.temperature == 31.5" + ] + ] + }, + "EventName": "test-event" + } + ] + }, + "StateName": "online" + } + ] + }, + "RoleArn": { + "Fn::GetAtt": [ + "MyDetectorModelDetectorModelRoleF2FB4D88", + "Arn" + ] + }, + "DetectorModelName": "test-detector-model" + } } } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-iotevents/test/integ.detector-model.ts b/packages/@aws-cdk/aws-iotevents/test/integ.detector-model.ts index cb900c83a3f44..8eeef110d5b8a 100644 --- a/packages/@aws-cdk/aws-iotevents/test/integ.detector-model.ts +++ b/packages/@aws-cdk/aws-iotevents/test/integ.detector-model.ts @@ -1,18 +1,37 @@ import * as cdk from '@aws-cdk/core'; import * as iotevents from '../lib'; -const app = new cdk.App(); - class TestStack extends cdk.Stack { constructor(scope: cdk.App, id: string, props?: cdk.StackProps) { super(scope, id, props); - new iotevents.Input(this, 'MyInput', { + const input = new iotevents.Input(this, 'MyInput', { inputName: 'test_input', attributeJsonPaths: ['payload.temperature'], }); + + const onlineState = new iotevents.State({ + stateName: 'online', + onEnter: [{ + eventName: 'test-event', + // meaning `condition: 'currentInput("test_input") && $input.test_input.payload.temperature == 31.5'` + condition: iotevents.Expression.and( + iotevents.Expression.currentInput(input), + iotevents.Expression.eq( + iotevents.Expression.inputAttribute(input, 'payload.temperature'), + iotevents.Expression.fromString('31.5'), + ), + ), + }], + }); + + new iotevents.DetectorModel(this, 'MyDetectorModel', { + detectorModelName: 'test-detector-model', + initialState: onlineState, + }); } } -new TestStack(app, 'test-stack'); +const app = new cdk.App(); +new TestStack(app, 'detector-model-test-stack'); app.synth(); diff --git a/packages/@aws-cdk/aws-rds/package.json b/packages/@aws-cdk/aws-rds/package.json index bd56c7dd94539..0da51c0d59691 100644 --- a/packages/@aws-cdk/aws-rds/package.json +++ b/packages/@aws-cdk/aws-rds/package.json @@ -79,7 +79,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@aws-cdk/assert-internal": "0.0.0", + "@aws-cdk/assertions": "0.0.0", "@aws-cdk/aws-events-targets": "0.0.0", "@aws-cdk/aws-lambda": "0.0.0", "@aws-cdk/cdk-build-tools": "0.0.0", diff --git a/packages/@aws-cdk/aws-rds/test/cluster-engine.test.ts b/packages/@aws-cdk/aws-rds/test/cluster-engine.test.ts index 826c988688c24..f6e3824092442 100644 --- a/packages/@aws-cdk/aws-rds/test/cluster-engine.test.ts +++ b/packages/@aws-cdk/aws-rds/test/cluster-engine.test.ts @@ -1,4 +1,3 @@ -import '@aws-cdk/assert-internal/jest'; import { AuroraEngineVersion, AuroraMysqlEngineVersion, AuroraPostgresEngineVersion, DatabaseClusterEngine } from '../lib'; describe('cluster engine', () => { @@ -11,8 +10,6 @@ describe('cluster engine', () => { // THEN expect(family).toEqual('aurora5.6'); - - }); test("default parameterGroupFamily for versionless Aurora MySQL cluster engine is 'aurora-mysql5.7'", () => { @@ -24,8 +21,6 @@ describe('cluster engine', () => { // THEN expect(family).toEqual('aurora-mysql5.7'); - - }); test('default parameterGroupFamily for versionless Aurora PostgreSQL is not defined', () => { @@ -37,8 +32,6 @@ describe('cluster engine', () => { // THEN expect(family).toEqual(undefined); - - }); test('cluster parameter group correctly determined for AURORA and given version', () => { @@ -52,8 +45,6 @@ describe('cluster engine', () => { // THEN expect(family).toEqual('aurora5.6'); - - }); test('cluster parameter group correctly determined for AURORA_MYSQL and given version', () => { @@ -67,8 +58,6 @@ describe('cluster engine', () => { // THEN expect(family).toEqual('aurora-mysql5.7'); - - }); test('cluster parameter group correctly determined for AURORA_MYSQL and given version 3', () => { @@ -95,8 +84,6 @@ describe('cluster engine', () => { // THEN expect(family).toEqual('aurora-postgresql11'); - - }); test('parameter group family', () => { @@ -117,8 +104,6 @@ describe('cluster engine', () => { 'aurora-postgresql9.6'); expect(DatabaseClusterEngine.auroraPostgres({ version: AuroraPostgresEngineVersion.of('10.0', '10') }).parameterGroupFamily).toEqual( 'aurora-postgresql10'); - - }); test('supported log types', () => { @@ -126,6 +111,5 @@ describe('cluster engine', () => { expect(DatabaseClusterEngine.aurora({ version: AuroraEngineVersion.VER_1_22_2 }).supportedLogTypes).toEqual(mysqlLogTypes); expect(DatabaseClusterEngine.auroraMysql({ version: AuroraMysqlEngineVersion.VER_2_08_1 }).supportedLogTypes).toEqual(mysqlLogTypes); expect(DatabaseClusterEngine.auroraPostgres({ version: AuroraPostgresEngineVersion.VER_9_6_9 }).supportedLogTypes).toEqual(['postgresql']); - }); -}); \ No newline at end of file +}); diff --git a/packages/@aws-cdk/aws-rds/test/cluster.test.ts b/packages/@aws-cdk/aws-rds/test/cluster.test.ts index b3dbdfc81d007..1681f397c5a9f 100644 --- a/packages/@aws-cdk/aws-rds/test/cluster.test.ts +++ b/packages/@aws-cdk/aws-rds/test/cluster.test.ts @@ -1,5 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; -import { ABSENT, ResourcePart, SynthUtils } from '@aws-cdk/assert-internal'; +import { Match, Template } from '@aws-cdk/assertions'; import * as ec2 from '@aws-cdk/aws-ec2'; import { ManagedPolicy, Role, ServicePrincipal } from '@aws-cdk/aws-iam'; import * as kms from '@aws-cdk/aws-kms'; @@ -34,7 +33,7 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResource('AWS::RDS::DBCluster', { + Template.fromStack(stack).hasResource('AWS::RDS::DBCluster', { Properties: { Engine: 'aurora', DBSubnetGroupName: { Ref: 'DatabaseSubnets56F17B9A' }, @@ -46,13 +45,13 @@ describe('cluster', () => { }, DeletionPolicy: 'Snapshot', UpdateReplacePolicy: 'Snapshot', - }, ResourcePart.CompleteDefinition); + }); - expect(stack).toCountResources('AWS::RDS::DBInstance', 2); - expect(stack).toHaveResource('AWS::RDS::DBInstance', { + Template.fromStack(stack).resourceCountIs('AWS::RDS::DBInstance', 2); + Template.fromStack(stack).hasResource('AWS::RDS::DBInstance', { DeletionPolicy: 'Delete', UpdateReplacePolicy: 'Delete', - }, ResourcePart.CompleteDefinition); + }); }); test('validates that the number of instances is not a deploy-time value', () => { @@ -91,7 +90,7 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResource('AWS::RDS::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBCluster', { Engine: 'aurora', DBSubnetGroupName: { Ref: 'DatabaseSubnets56F17B9A' }, MasterUsername: 'admin', @@ -146,7 +145,7 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResource('AWS::RDS::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBCluster', { Engine: 'aurora', DBSubnetGroupName: { Ref: 'DatabaseSubnets56F17B9A' }, MasterUsername: 'admin', @@ -182,7 +181,7 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResource('AWS::RDS::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBCluster', { DBClusterParameterGroupName: { Ref: 'ParamsA8366201' }, }); }); @@ -201,10 +200,10 @@ describe('cluster', () => { removalPolicy: cdk.RemovalPolicy.RETAIN, }); - expect(stack).toHaveResourceLike('AWS::RDS::DBSubnetGroup', { + Template.fromStack(stack).hasResource('AWS::RDS::DBSubnetGroup', { DeletionPolicy: 'Retain', UpdateReplacePolicy: 'Retain', - }, ResourcePart.CompleteDefinition); + }); }); test('creates a secret when master credentials are not specified', () => { @@ -226,7 +225,7 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResource('AWS::RDS::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBCluster', { MasterUsername: { 'Fn::Join': [ '', @@ -253,7 +252,7 @@ describe('cluster', () => { }, }); - expect(stack).toHaveResource('AWS::SecretsManager::Secret', { + Template.fromStack(stack).hasResourceProperties('AWS::SecretsManager::Secret', { GenerateSecretString: { ExcludeCharacters: '\"@/\\', GenerateStringKey: 'password', @@ -282,7 +281,7 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResource('AWS::RDS::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBCluster', { KmsKeyId: { 'Fn::GetAtt': [ 'Key961B73FD', @@ -316,7 +315,7 @@ describe('cluster', () => { }, }); - expect(stack).toHaveResource('AWS::RDS::DBInstance', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBInstance', { DBParameterGroupName: { Ref: 'ParameterGroup5E32DECB', }, @@ -343,7 +342,7 @@ describe('cluster', () => { }, }); - expect(stack).toHaveResource('AWS::RDS::DBInstance', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBInstance', { EnablePerformanceInsights: true, PerformanceInsightsRetentionPeriod: 731, PerformanceInsightsKMSKeyId: { 'Fn::GetAtt': ['Key961B73FD', 'Arn'] }, @@ -367,7 +366,7 @@ describe('cluster', () => { }, }); - expect(stack).toHaveResource('AWS::RDS::DBInstance', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBInstance', { EnablePerformanceInsights: true, PerformanceInsightsRetentionPeriod: 731, }); @@ -408,7 +407,7 @@ describe('cluster', () => { }, }); - expect(stack).toHaveResource('AWS::RDS::DBInstance', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBInstance', { AutoMinorVersionUpgrade: false, }); }); @@ -427,7 +426,7 @@ describe('cluster', () => { }, }); - expect(stack).toHaveResourceLike('AWS::RDS::DBInstance', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBInstance', { AllowMajorVersionUpgrade: true, }); }); @@ -446,7 +445,7 @@ describe('cluster', () => { }, }); - expect(stack).toHaveResourceLike('AWS::RDS::DBInstance', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBInstance', { DeleteAutomatedBackups: false, }); }); @@ -471,7 +470,7 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResource('AWS::RDS::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBCluster', { Engine: 'aurora-mysql', EngineVersion: '5.7.mysql_aurora.2.04.4', }); @@ -497,12 +496,10 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResource('AWS::RDS::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBCluster', { Engine: 'aurora-postgresql', EngineVersion: '10.7', }); - - }); test('cluster exposes different read and write endpoints', () => { @@ -546,7 +543,7 @@ describe('cluster', () => { cluster.connections.allowToAnyIpv4(ec2.Port.tcp(443)); // THEN - expect(stack).toHaveResource('AWS::EC2::SecurityGroupEgress', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroupEgress', { GroupId: 'sg-123456789', }); }); @@ -559,8 +556,6 @@ describe('cluster', () => { }); expect(cluster.clusterIdentifier).toEqual('identifier'); - - }); test('minimal imported cluster throws on accessing attributes for unprovided parameters', () => { @@ -621,8 +616,6 @@ describe('cluster', () => { account: '12345', region: 'us-test-1', }); - - }); test('cluster with enabled monitoring', () => { @@ -645,14 +638,14 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResource('AWS::RDS::DBInstance', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBInstance', { MonitoringInterval: 60, MonitoringRoleArn: { 'Fn::GetAtt': ['DatabaseMonitoringRole576991DA', 'Arn'], }, - }, ResourcePart.Properties); + }); - expect(stack).toHaveResource('AWS::IAM::Role', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { AssumeRolePolicyDocument: { Statement: [ { @@ -680,8 +673,6 @@ describe('cluster', () => { }, ], }); - - }); test('create a cluster with imported monitoring role', () => { @@ -712,14 +703,12 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResource('AWS::RDS::DBInstance', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBInstance', { MonitoringInterval: 60, MonitoringRoleArn: { 'Fn::GetAtt': ['MonitoringRole90457BF9', 'Arn'], }, - }, ResourcePart.Properties); - - + }); }); test('addRotationSingleUser()', () => { @@ -737,7 +726,7 @@ describe('cluster', () => { // WHEN cluster.addRotationSingleUser(); - expect(stack).toHaveResource('AWS::SecretsManager::RotationSchedule', { + Template.fromStack(stack).hasResourceProperties('AWS::SecretsManager::RotationSchedule', { SecretId: { Ref: 'DatabaseSecretAttachmentE5D1B020', }, @@ -768,7 +757,7 @@ describe('cluster', () => { const userSecret = new DatabaseSecret(stack, 'UserSecret', { username: 'user' }); cluster.addRotationMultiUser('user', { secret: userSecret.attach(cluster) }); - expect(stack).toHaveResource('AWS::SecretsManager::RotationSchedule', { + Template.fromStack(stack).hasResourceProperties('AWS::SecretsManager::RotationSchedule', { SecretId: { Ref: 'UserSecretAttachment16ACBE6D', }, @@ -783,7 +772,7 @@ describe('cluster', () => { }, }); - expect(stack).toHaveResourceLike('AWS::Serverless::Application', { + Template.fromStack(stack).hasResourceProperties('AWS::Serverless::Application', { Parameters: { masterSecretArn: { Ref: 'DatabaseSecretAttachmentE5D1B020', @@ -822,13 +811,13 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResource('AWS::SecretsManager::RotationSchedule', { + Template.fromStack(stack).hasResourceProperties('AWS::SecretsManager::RotationSchedule', { RotationRules: { AutomaticallyAfterDays: 15, }, }); - expect(stack).toHaveResource('AWS::Serverless::Application', { + Template.fromStack(stack).hasResourceProperties('AWS::Serverless::Application', { Parameters: { endpoint: { 'Fn::Join': ['', [ @@ -882,7 +871,7 @@ describe('cluster', () => { // Rotation in isolated subnet with access to Secrets Manager API via endpoint cluster.addRotationSingleUser({ endpoint }); - expect(stack).toHaveResource('AWS::Serverless::Application', { + Template.fromStack(stack).hasResourceProperties('AWS::Serverless::Application', { Parameters: { endpoint: { 'Fn::Join': ['', [ @@ -933,8 +922,6 @@ describe('cluster', () => { // THEN expect(() => cluster.addRotationSingleUser()).toThrow(/without secret/); - - }); test('throws when trying to add single user rotation multiple times', () => { @@ -955,8 +942,6 @@ describe('cluster', () => { // THEN expect(() => cluster.addRotationSingleUser()).toThrow(/A single user rotation was already added to this cluster/); - - }); test('create a cluster with s3 import role', () => { @@ -983,7 +968,7 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResource('AWS::RDS::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBCluster', { AssociatedRoles: [{ RoleArn: { 'Fn::GetAtt': [ @@ -994,7 +979,7 @@ describe('cluster', () => { }], }); - expect(stack).toHaveResource('AWS::RDS::DBClusterParameterGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBClusterParameterGroup', { Family: 'aurora5.6', Parameters: { aurora_load_from_s3_role: { @@ -1005,8 +990,6 @@ describe('cluster', () => { }, }, }); - - }); test('create a cluster with s3 import buckets', () => { @@ -1031,7 +1014,7 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResource('AWS::RDS::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBCluster', { AssociatedRoles: [{ RoleArn: { 'Fn::GetAtt': [ @@ -1042,7 +1025,7 @@ describe('cluster', () => { }], }); - expect(stack).toHaveResource('AWS::RDS::DBClusterParameterGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBClusterParameterGroup', { Family: 'aurora5.6', Parameters: { aurora_load_from_s3_role: { @@ -1054,7 +1037,7 @@ describe('cluster', () => { }, }); - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -1091,8 +1074,6 @@ describe('cluster', () => { Version: '2012-10-17', }, }); - - }); test('cluster with s3 import bucket adds supported feature name to IAM role', () => { @@ -1119,7 +1100,7 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResource('AWS::RDS::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBCluster', { AssociatedRoles: [{ RoleArn: { 'Fn::GetAtt': [ @@ -1130,8 +1111,6 @@ describe('cluster', () => { FeatureName: 's3Import', }], }); - - }); test('throws when s3 import bucket or s3 export bucket is supplied for a Postgres version that does not support it', () => { @@ -1175,8 +1154,6 @@ describe('cluster', () => { s3ExportBuckets: [bucket], }); }).toThrow(/s3Export is not supported for Postgres version: 10.4. Use a version that supports the s3Export feature./); - - }); test('cluster with s3 export bucket adds supported feature name to IAM role', () => { @@ -1203,7 +1180,7 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResource('AWS::RDS::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBCluster', { AssociatedRoles: [{ RoleArn: { 'Fn::GetAtt': [ @@ -1214,8 +1191,6 @@ describe('cluster', () => { FeatureName: 's3Export', }], }); - - }); test('create a cluster with s3 export role', () => { @@ -1242,7 +1217,7 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResource('AWS::RDS::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBCluster', { AssociatedRoles: [{ RoleArn: { 'Fn::GetAtt': [ @@ -1253,7 +1228,7 @@ describe('cluster', () => { }], }); - expect(stack).toHaveResource('AWS::RDS::DBClusterParameterGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBClusterParameterGroup', { Family: 'aurora5.6', Parameters: { aurora_select_into_s3_role: { @@ -1264,8 +1239,6 @@ describe('cluster', () => { }, }, }); - - }); testFutureBehavior('create a cluster with s3 export buckets', { [cxapi.S3_GRANT_WRITE_WITHOUT_ACL]: true }, cdk.App, (app) => { @@ -1290,7 +1263,7 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResource('AWS::RDS::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBCluster', { AssociatedRoles: [{ RoleArn: { 'Fn::GetAtt': [ @@ -1301,7 +1274,7 @@ describe('cluster', () => { }], }); - expect(stack).toHaveResource('AWS::RDS::DBClusterParameterGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBClusterParameterGroup', { Family: 'aurora5.6', Parameters: { aurora_select_into_s3_role: { @@ -1313,7 +1286,7 @@ describe('cluster', () => { }, }); - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -1353,8 +1326,6 @@ describe('cluster', () => { Version: '2012-10-17', }, }); - - }); test('create a cluster with s3 import and export buckets', () => { @@ -1381,7 +1352,7 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResource('AWS::RDS::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBCluster', { AssociatedRoles: [{ RoleArn: { 'Fn::GetAtt': [ @@ -1400,7 +1371,7 @@ describe('cluster', () => { }], }); - expect(stack).toHaveResource('AWS::RDS::DBClusterParameterGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBClusterParameterGroup', { Family: 'aurora5.6', Parameters: { aurora_load_from_s3_role: { @@ -1417,8 +1388,6 @@ describe('cluster', () => { }, }, }); - - }); test('create a cluster with s3 import and export buckets and custom parameter group', () => { @@ -1453,7 +1422,7 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResource('AWS::RDS::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBCluster', { AssociatedRoles: [{ RoleArn: { 'Fn::GetAtt': [ @@ -1472,7 +1441,7 @@ describe('cluster', () => { }], }); - expect(stack).toHaveResource('AWS::RDS::DBClusterParameterGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBClusterParameterGroup', { Family: 'aurora5.6', Parameters: { key: 'value', @@ -1490,8 +1459,6 @@ describe('cluster', () => { }, }, }); - - }); test('PostgreSQL cluster with s3 export buckets does not generate custom parameter group and specifies the correct port', () => { @@ -1518,7 +1485,7 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::RDS::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBCluster', { AssociatedRoles: [{ RoleArn: { 'Fn::GetAtt': [ @@ -1531,9 +1498,7 @@ describe('cluster', () => { Port: 5432, }); - expect(stack).not.toHaveResource('AWS::RDS::DBClusterParameterGroup'); - - + Template.fromStack(stack).resourceCountIs('AWS::RDS::DBClusterParameterGroup', 0); }); test('unversioned PostgreSQL cluster can be used with s3 import and s3 export buckets', () => { @@ -1560,7 +1525,7 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResource('AWS::RDS::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBCluster', { AssociatedRoles: [ { FeatureName: 's3Import', @@ -1582,8 +1547,6 @@ describe('cluster', () => { }, ], }); - - }); test("Aurora PostgreSQL cluster uses a different default master username than 'admin', which is a reserved word", () => { @@ -1600,13 +1563,11 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::SecretsManager::Secret', { + Template.fromStack(stack).hasResourceProperties('AWS::SecretsManager::Secret', { GenerateSecretString: { SecretStringTemplate: '{"username":"postgres"}', }, }); - - }); test('MySQL cluster without S3 exports or imports references the correct default ParameterGroup', () => { @@ -1628,13 +1589,11 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::RDS::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBCluster', { DBClusterParameterGroupName: 'default.aurora-mysql5.7', }); - expect(stack).not.toHaveResource('AWS::RDS::DBClusterParameterGroup'); - - + Template.fromStack(stack).resourceCountIs('AWS::RDS::DBClusterParameterGroup', 0); }); test('throws when s3ExportRole and s3ExportBuckets properties are both specified', () => { @@ -1661,8 +1620,6 @@ describe('cluster', () => { s3ExportRole: exportRole, s3ExportBuckets: [exportBucket], })).toThrow(); - - }); test('throws when s3ImportRole and s3ImportBuckets properties are both specified', () => { @@ -1689,8 +1646,6 @@ describe('cluster', () => { s3ImportRole: importRole, s3ImportBuckets: [importBucket], })).toThrow(); - - }); test('can set CloudWatch log exports', () => { @@ -1713,11 +1668,9 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::RDS::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBCluster', { EnableCloudwatchLogsExports: ['error', 'general', 'slowquery', 'audit'], }); - - }); test('can set CloudWatch log retention', () => { @@ -1741,7 +1694,7 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResource('Custom::LogRetention', { + Template.fromStack(stack).hasResourceProperties('Custom::LogRetention', { ServiceToken: { 'Fn::GetAtt': [ 'LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aFD4BFC8A', @@ -1751,7 +1704,7 @@ describe('cluster', () => { LogGroupName: { 'Fn::Join': ['', ['/aws/rds/cluster/', { Ref: 'DatabaseB269D8BB' }, '/error']] }, RetentionInDays: 90, }); - expect(stack).toHaveResource('Custom::LogRetention', { + Template.fromStack(stack).hasResourceProperties('Custom::LogRetention', { ServiceToken: { 'Fn::GetAtt': [ 'LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aFD4BFC8A', @@ -1761,8 +1714,6 @@ describe('cluster', () => { LogGroupName: { 'Fn::Join': ['', ['/aws/rds/cluster/', { Ref: 'DatabaseB269D8BB' }, '/general']] }, RetentionInDays: 90, }); - - }); test('throws if given unsupported CloudWatch log exports', () => { @@ -1784,8 +1735,6 @@ describe('cluster', () => { cloudwatchLogsExports: ['error', 'general', 'slowquery', 'audit', 'thislogdoesnotexist', 'neitherdoesthisone'], }); }).toThrow(/Unsupported logs for the current engine type: thislogdoesnotexist,neitherdoesthisone/); - - }); test('can set deletion protection', () => { @@ -1808,16 +1757,15 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::RDS::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBCluster', { DeletionProtection: true, }); - - }); test('does not throw (but adds a node error) if a (dummy) VPC does not have sufficient subnets', () => { // GIVEN - const stack = testStack(); + const app = new cdk.App(); + const stack = testStack(app, 'TestStack'); const vpc = ec2.Vpc.fromLookup(stack, 'VPC', { isDefault: true }); // WHEN @@ -1837,11 +1785,9 @@ describe('cluster', () => { }); // THEN - const art = SynthUtils.synthesize(stack); + const art = app.synth().getStackArtifact('TestStack'); const meta = art.findMetadataByType('aws:cdk:error'); expect(meta[0].data).toEqual('Cluster requires at least 2 subnets, got 0'); - - }); test('create a cluster from a snapshot', () => { @@ -1859,7 +1805,7 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResource('AWS::RDS::DBCluster', { + Template.fromStack(stack).hasResource('AWS::RDS::DBCluster', { Properties: { Engine: 'aurora', EngineVersion: '5.6.mysql_aurora.1.22.2', @@ -1870,9 +1816,9 @@ describe('cluster', () => { }, DeletionPolicy: 'Snapshot', UpdateReplacePolicy: 'Snapshot', - }, ResourcePart.CompleteDefinition); + }); - expect(stack).toCountResources('AWS::RDS::DBInstance', 2); + Template.fromStack(stack).resourceCountIs('AWS::RDS::DBInstance', 2); expect(cluster.instanceIdentifiers).toHaveLength(2); expect(stack.resolve(cluster.instanceIdentifiers[0])).toEqual({ @@ -1915,12 +1861,10 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::RDS::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBCluster', { DBSubnetGroupName: 'my-subnet-group', }); - expect(stack).toCountResources('AWS::RDS::DBSubnetGroup', 0); - - + Template.fromStack(stack).resourceCountIs('AWS::RDS::DBSubnetGroup', 0); }); test('defaultChild returns the DB Cluster', () => { @@ -1941,8 +1885,6 @@ describe('cluster', () => { // THEN expect(cluster.node.defaultChild instanceof CfnDBCluster).toBeTruthy(); - - }); test('fromGeneratedSecret', () => { @@ -1960,7 +1902,7 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResource('AWS::RDS::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBCluster', { MasterUsername: 'admin', // username is a string MasterUserPassword: { 'Fn::Join': [ @@ -1994,7 +1936,7 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResource('AWS::SecretsManager::Secret', { + Template.fromStack(stack).hasResourceProperties('AWS::SecretsManager::Secret', { ReplicaRegions: [ { Region: 'eu-west-1', @@ -2023,7 +1965,7 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::SecretsManager::Secret', { + Template.fromStack(stack).hasResourceProperties('AWS::SecretsManager::Secret', { Name: secretName, }); }); @@ -2044,7 +1986,7 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::SecretsManager::Secret', { + Template.fromStack(stack).hasResourceProperties('AWS::SecretsManager::Secret', { Name: secretName, }); }); @@ -2066,12 +2008,10 @@ describe('cluster', () => { }, }); // THEN - expect(stack).toHaveResource('AWS::RDS::DBInstance', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBInstance', { Engine: 'aurora', PubliclyAccessible: true, }); - - }); test('can set public accessibility for database cluster with instances in public subnet', () => { @@ -2091,12 +2031,10 @@ describe('cluster', () => { }, }); // THEN - expect(stack).toHaveResource('AWS::RDS::DBInstance', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBInstance', { Engine: 'aurora', PubliclyAccessible: false, }); - - }); test('database cluster instances in public subnet should by default have publiclyAccessible set to true', () => { @@ -2115,12 +2053,10 @@ describe('cluster', () => { }, }); // THEN - expect(stack).toHaveResource('AWS::RDS::DBInstance', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBInstance', { Engine: 'aurora', PubliclyAccessible: true, }); - - }); test('changes the case of the cluster identifier if the lowercaseDbIdentifier feature flag is enabled', () => { @@ -2140,7 +2076,7 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::RDS::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBCluster', { DBClusterIdentifier: clusterIdentifier.toLowerCase(), }); }); @@ -2160,7 +2096,7 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::RDS::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBCluster', { DBClusterIdentifier: clusterIdentifier, }); }); @@ -2179,7 +2115,7 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::RDS::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBCluster', { CopyTagsToSnapshot: true, }); }); @@ -2199,7 +2135,7 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::RDS::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBCluster', { CopyTagsToSnapshot: false, }); }); @@ -2219,7 +2155,7 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::RDS::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBCluster', { CopyTagsToSnapshot: true, }); }); @@ -2239,7 +2175,7 @@ describe('cluster', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::RDS::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBCluster', { BacktrackWindow: 24 * 60 * 60, }); }); @@ -2247,8 +2183,8 @@ describe('cluster', () => { test.each([ [cdk.RemovalPolicy.RETAIN, 'Retain', 'Retain', 'Retain'], - [cdk.RemovalPolicy.SNAPSHOT, 'Snapshot', 'Delete', ABSENT], - [cdk.RemovalPolicy.DESTROY, 'Delete', 'Delete', ABSENT], + [cdk.RemovalPolicy.SNAPSHOT, 'Snapshot', 'Delete', Match.absent()], + [cdk.RemovalPolicy.DESTROY, 'Delete', 'Delete', Match.absent()], ])('if Cluster RemovalPolicy is \'%s\', the DBCluster has DeletionPolicy \'%s\', the DBInstance has \'%s\' and the DBSubnetGroup has \'%s\'', (clusterRemovalPolicy, clusterValue, instanceValue, subnetValue) => { const stack = new cdk.Stack(); @@ -2264,25 +2200,25 @@ test.each([ }); // THEN - expect(stack).toHaveResourceLike('AWS::RDS::DBCluster', { + Template.fromStack(stack).hasResource('AWS::RDS::DBCluster', { DeletionPolicy: clusterValue, UpdateReplacePolicy: clusterValue, - }, ResourcePart.CompleteDefinition); + }); - expect(stack).toHaveResourceLike('AWS::RDS::DBInstance', { + Template.fromStack(stack).hasResource('AWS::RDS::DBInstance', { DeletionPolicy: instanceValue, UpdateReplacePolicy: instanceValue, - }, ResourcePart.CompleteDefinition); + }); - expect(stack).toHaveResourceLike('AWS::RDS::DBSubnetGroup', { + Template.fromStack(stack).hasResource('AWS::RDS::DBSubnetGroup', { DeletionPolicy: subnetValue, - }, ResourcePart.CompleteDefinition); + }); }); test.each([ [cdk.RemovalPolicy.RETAIN, 'Retain', 'Retain', 'Retain'], - [cdk.RemovalPolicy.SNAPSHOT, 'Snapshot', 'Delete', ABSENT], - [cdk.RemovalPolicy.DESTROY, 'Delete', 'Delete', ABSENT], + [cdk.RemovalPolicy.SNAPSHOT, 'Snapshot', 'Delete', Match.absent()], + [cdk.RemovalPolicy.DESTROY, 'Delete', 'Delete', Match.absent()], ])('if Cluster RemovalPolicy is \'%s\', the DBCluster has DeletionPolicy \'%s\', the DBInstance has \'%s\' and the DBSubnetGroup has \'%s\'', (clusterRemovalPolicy, clusterValue, instanceValue, subnetValue) => { const stack = new cdk.Stack(); @@ -2298,25 +2234,24 @@ test.each([ }); // THEN - expect(stack).toHaveResourceLike('AWS::RDS::DBCluster', { + Template.fromStack(stack).hasResource('AWS::RDS::DBCluster', { DeletionPolicy: clusterValue, UpdateReplacePolicy: clusterValue, - }, ResourcePart.CompleteDefinition); + }); - expect(stack).toHaveResourceLike('AWS::RDS::DBInstance', { + Template.fromStack(stack).hasResource('AWS::RDS::DBInstance', { DeletionPolicy: instanceValue, UpdateReplacePolicy: instanceValue, - }, ResourcePart.CompleteDefinition); + }); - expect(stack).toHaveResourceLike('AWS::RDS::DBSubnetGroup', { + Template.fromStack(stack).hasResource('AWS::RDS::DBSubnetGroup', { DeletionPolicy: subnetValue, UpdateReplacePolicy: subnetValue, - }, ResourcePart.CompleteDefinition); + }); }); - -function testStack(app?: cdk.App) { - const stack = new cdk.Stack(app, undefined, { env: { account: '12345', region: 'us-test-1' } }); +function testStack(app?: cdk.App, stackId?: string) { + const stack = new cdk.Stack(app, stackId, { env: { account: '12345', region: 'us-test-1' } }); stack.node.setContext('availability-zones:12345:us-test-1', ['us-test-1a', 'us-test-1b']); return stack; } diff --git a/packages/@aws-cdk/aws-rds/test/database-secret.test.ts b/packages/@aws-cdk/aws-rds/test/database-secret.test.ts index 9fe7793536a1c..424fe4164fd74 100644 --- a/packages/@aws-cdk/aws-rds/test/database-secret.test.ts +++ b/packages/@aws-cdk/aws-rds/test/database-secret.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import { CfnResource, Stack } from '@aws-cdk/core'; import { DatabaseSecret } from '../lib'; import { DEFAULT_PASSWORD_EXCLUDE_CHARS } from '../lib/private/util'; @@ -14,7 +14,7 @@ describe('database secret', () => { }); // THEN - expect(stack).toHaveResource('AWS::SecretsManager::Secret', { + Template.fromStack(stack).hasResourceProperties('AWS::SecretsManager::Secret', { Description: { 'Fn::Join': [ '', @@ -35,8 +35,6 @@ describe('database secret', () => { }); expect(getSecretLogicalId(dbSecret, stack)).toEqual('SecretA720EF05'); - - }); test('with master secret', () => { @@ -54,7 +52,7 @@ describe('database secret', () => { }); // THEN - expect(stack).toHaveResource('AWS::SecretsManager::Secret', { + Template.fromStack(stack).hasResourceProperties('AWS::SecretsManager::Secret', { GenerateSecretString: { ExcludeCharacters: '"@/\\', GenerateStringKey: 'password', @@ -73,8 +71,6 @@ describe('database secret', () => { }, }, }); - - }); test('replace on password critera change', () => { @@ -106,8 +102,6 @@ describe('database secret', () => { replaceOnPasswordCriteriaChanges: true, }); expect(dbSecretlogicalId).not.toEqual(getSecretLogicalId(otherSecret2, stack)); - - }); }); diff --git a/packages/@aws-cdk/aws-rds/test/database-secretmanager.test.ts b/packages/@aws-cdk/aws-rds/test/database-secretmanager.test.ts index 135704a893f75..8d1da9e04b1b6 100644 --- a/packages/@aws-cdk/aws-rds/test/database-secretmanager.test.ts +++ b/packages/@aws-cdk/aws-rds/test/database-secretmanager.test.ts @@ -1,5 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; -import { ResourcePart } from '@aws-cdk/assert-internal'; +import { Template } from '@aws-cdk/assertions'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as secretsmanager from '@aws-cdk/aws-secretsmanager'; import * as cdk from '@aws-cdk/core'; @@ -21,7 +20,7 @@ describe('database secret manager', () => { }); // THEN - expect(stack).toHaveResource('AWS::RDS::DBCluster', { + Template.fromStack(stack).hasResource('AWS::RDS::DBCluster', { Properties: { Engine: 'aurora-postgresql', DBClusterParameterGroupName: 'default.aurora-postgresql10', @@ -65,9 +64,7 @@ describe('database secret manager', () => { }, DeletionPolicy: 'Snapshot', UpdateReplacePolicy: 'Snapshot', - }, ResourcePart.CompleteDefinition); - - + }); }); }); diff --git a/packages/@aws-cdk/aws-rds/test/instance-engine.test.ts b/packages/@aws-cdk/aws-rds/test/instance-engine.test.ts index 99ed162c4f5eb..f9a93f3bca867 100644 --- a/packages/@aws-cdk/aws-rds/test/instance-engine.test.ts +++ b/packages/@aws-cdk/aws-rds/test/instance-engine.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as iam from '@aws-cdk/aws-iam'; import * as cdk from '@aws-cdk/core'; import * as rds from '../lib'; @@ -10,8 +10,6 @@ describe('instance engine', () => { const family = engine.parameterGroupFamily; expect(family).toEqual(undefined); - - }); test('default parameterGroupFamily for versionless MySQL instance engine is not defined', () => { @@ -20,8 +18,6 @@ describe('instance engine', () => { const family = engine.parameterGroupFamily; expect(family).toEqual(undefined); - - }); test('default parameterGroupFamily for versionless PostgreSQL instance engine is not defined', () => { @@ -30,8 +26,6 @@ describe('instance engine', () => { const family = engine.parameterGroupFamily; expect(family).toEqual(undefined); - - }); test("default parameterGroupFamily for versionless Oracle SE instance engine is 'oracle-se-11.2'", () => { @@ -40,8 +34,6 @@ describe('instance engine', () => { const family = engine.parameterGroupFamily; expect(family).toEqual('oracle-se-11.2'); - - }); test("default parameterGroupFamily for versionless Oracle SE 1 instance engine is 'oracle-se1-11.2'", () => { @@ -50,8 +42,6 @@ describe('instance engine', () => { const family = engine.parameterGroupFamily; expect(family).toEqual('oracle-se1-11.2'); - - }); test('default parameterGroupFamily for versionless Oracle SE 2 instance engine is not defined', () => { @@ -60,8 +50,6 @@ describe('instance engine', () => { const family = engine.parameterGroupFamily; expect(family).toEqual(undefined); - - }); test('default parameterGroupFamily for versionless Oracle EE instance engine is not defined', () => { @@ -70,8 +58,6 @@ describe('instance engine', () => { const family = engine.parameterGroupFamily; expect(family).toEqual(undefined); - - }); test('default parameterGroupFamily for versionless SQL Server SE instance engine is not defined', () => { @@ -80,8 +66,6 @@ describe('instance engine', () => { const family = engine.parameterGroupFamily; expect(family).toEqual(undefined); - - }); test('default parameterGroupFamily for versionless SQL Server EX instance engine is not defined', () => { @@ -90,8 +74,6 @@ describe('instance engine', () => { const family = engine.parameterGroupFamily; expect(family).toEqual(undefined); - - }); test('default parameterGroupFamily for versionless SQL Server Web instance engine is not defined', () => { @@ -100,8 +82,6 @@ describe('instance engine', () => { const family = engine.parameterGroupFamily; expect(family).toEqual(undefined); - - }); test('default parameterGroupFamily for versionless SQL Server EE instance engine is not defined', () => { @@ -110,8 +90,6 @@ describe('instance engine', () => { const family = engine.parameterGroupFamily; expect(family).toEqual(undefined); - - }); describe('Oracle engine bindToInstance', () => { @@ -122,8 +100,6 @@ describe('instance engine', () => { const engineConfig = engine.bindToInstance(new cdk.Stack(), {}); expect(engineConfig.features?.s3Import).toEqual('S3_INTEGRATION'); expect(engineConfig.features?.s3Export).toEqual('S3_INTEGRATION'); - - }); test('s3 import/export - creates an option group if needed', () => { @@ -136,15 +112,13 @@ describe('instance engine', () => { }); expect(engineConfig.optionGroup).toBeDefined(); - expect(stack).toHaveResourceLike('AWS::RDS::OptionGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::OptionGroup', { EngineName: 'oracle-se2', OptionConfigurations: [{ OptionName: 'S3_INTEGRATION', OptionVersion: '1.0', }], }); - - }); test('s3 import/export - appends to an existing option group if it exists', () => { @@ -163,7 +137,7 @@ describe('instance engine', () => { }); expect(engineConfig.optionGroup).toEqual(optionGroup); - expect(stack).toHaveResourceLike('AWS::RDS::OptionGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::OptionGroup', { EngineName: 'oracle-se2', OptionConfigurations: [{ OptionName: 'MY_OPTION_CONFIG', @@ -173,8 +147,6 @@ describe('instance engine', () => { OptionVersion: '1.0', }], }); - - }); }); @@ -185,8 +157,6 @@ describe('instance engine', () => { const engineConfig = engine.bindToInstance(new cdk.Stack(), {}); expect(engineConfig.features?.s3Import).toEqual('S3_INTEGRATION'); expect(engineConfig.features?.s3Export).toEqual('S3_INTEGRATION'); - - }); test('s3 import/export - throws if roles are not equal', () => { @@ -200,8 +170,6 @@ describe('instance engine', () => { expect(() => engine.bindToInstance(new cdk.Stack(), { s3ImportRole })).not.toThrow(); expect(() => engine.bindToInstance(new cdk.Stack(), { s3ExportRole })).not.toThrow(); expect(() => engine.bindToInstance(new cdk.Stack(), { s3ImportRole, s3ExportRole: s3ImportRole })).not.toThrow(); - - }); test('s3 import/export - creates an option group if needed', () => { @@ -214,7 +182,7 @@ describe('instance engine', () => { }); expect(engineConfig.optionGroup).toBeDefined(); - expect(stack).toHaveResourceLike('AWS::RDS::OptionGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::OptionGroup', { EngineName: 'sqlserver-se', OptionConfigurations: [{ OptionName: 'SQLSERVER_BACKUP_RESTORE', @@ -224,8 +192,6 @@ describe('instance engine', () => { }], }], }); - - }); test('s3 import/export - appends to an existing option group if it exists', () => { @@ -244,7 +210,7 @@ describe('instance engine', () => { }); expect(engineConfig.optionGroup).toEqual(optionGroup); - expect(stack).toHaveResourceLike('AWS::RDS::OptionGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::OptionGroup', { EngineName: 'sqlserver-se', OptionConfigurations: [{ OptionName: 'MY_OPTION_CONFIG', @@ -257,8 +223,6 @@ describe('instance engine', () => { }], }], }); - - }); }); @@ -269,8 +233,6 @@ describe('instance engine', () => { const engineConfig = engineNewerVersion.bindToInstance(new cdk.Stack(), {}); expect(engineConfig.features?.s3Import).toEqual(undefined); expect(engineConfig.features?.s3Export).toEqual(undefined); - - }); test('returns s3 import/export feature if the version supports it', () => { @@ -279,8 +241,6 @@ describe('instance engine', () => { const engineConfig = engineNewerVersion.bindToInstance(new cdk.Stack(), {}); expect(engineConfig.features?.s3Import).toEqual('s3Import'); expect(engineConfig.features?.s3Export).toEqual('s3Export'); - - }); }); }); diff --git a/packages/@aws-cdk/aws-rds/test/instance.test.ts b/packages/@aws-cdk/aws-rds/test/instance.test.ts index 484ad765cb790..29ced7cbb1940 100644 --- a/packages/@aws-cdk/aws-rds/test/instance.test.ts +++ b/packages/@aws-cdk/aws-rds/test/instance.test.ts @@ -1,5 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; -import { ABSENT, ResourcePart, anything } from '@aws-cdk/assert-internal'; +import { Match, Template } from '@aws-cdk/assertions'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as targets from '@aws-cdk/aws-events-targets'; import { ManagedPolicy, Role, ServicePrincipal, AccountPrincipal } from '@aws-cdk/aws-iam'; @@ -49,7 +48,7 @@ describe('instance', () => { }); // THEN - expect(stack).toHaveResource('AWS::RDS::DBInstance', { + Template.fromStack(stack).hasResource('AWS::RDS::DBInstance', { Properties: { DBInstanceClass: 'db.t2.medium', AllocatedStorage: '100', @@ -117,9 +116,9 @@ describe('instance', () => { }, DeletionPolicy: 'Snapshot', UpdateReplacePolicy: 'Snapshot', - }, ResourcePart.CompleteDefinition); + }); - expect(stack).toHaveResource('AWS::RDS::DBSubnetGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBSubnetGroup', { DBSubnetGroupDescription: 'Subnet group for Instance database', SubnetIds: [ { @@ -131,11 +130,11 @@ describe('instance', () => { ], }); - expect(stack).toHaveResource('AWS::EC2::SecurityGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroup', { GroupDescription: 'Security group for Instance database', }); - expect(stack).toHaveResource('AWS::IAM::Role', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { AssumeRolePolicyDocument: { Statement: [ { @@ -164,7 +163,7 @@ describe('instance', () => { ], }); - expect(stack).toHaveResource('AWS::SecretsManager::Secret', { + Template.fromStack(stack).hasResourceProperties('AWS::SecretsManager::Secret', { Description: { 'Fn::Join': [ '', @@ -184,7 +183,7 @@ describe('instance', () => { }, }); - expect(stack).toHaveResource('AWS::SecretsManager::SecretTargetAttachment', { + Template.fromStack(stack).hasResourceProperties('AWS::SecretsManager::SecretTargetAttachment', { SecretId: { Ref: 'InstanceSecret478E0A47', }, @@ -194,9 +193,7 @@ describe('instance', () => { TargetType: 'AWS::RDS::DBInstance', }); - expect(stack).toCountResources('Custom::LogRetention', 4); - - + Template.fromStack(stack).resourceCountIs('Custom::LogRetention', 4); }); test('throws when create database with specific AZ and multiAZ enabled', () => { @@ -239,7 +236,7 @@ describe('instance', () => { parameterGroup, }); - expect(stack).toHaveResource('AWS::RDS::DBInstance', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBInstance', { DBParameterGroupName: { Ref: 'ParameterGroup5E32DECB', }, @@ -247,8 +244,6 @@ describe('instance', () => { Ref: 'OptionGroupACA43DC1', }, }); - - }); test('can specify subnet type', () => { @@ -263,13 +258,13 @@ describe('instance', () => { }, }); - expect(stack).toHaveResource('AWS::RDS::DBInstance', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBInstance', { DBSubnetGroupName: { Ref: 'InstanceSubnetGroupF2CBA54F', }, PubliclyAccessible: false, }); - expect(stack).toHaveResource('AWS::RDS::DBSubnetGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBSubnetGroup', { DBSubnetGroupDescription: 'Subnet group for Instance database', SubnetIds: [ { @@ -280,8 +275,6 @@ describe('instance', () => { }, ], }); - - }); describe('DatabaseInstanceFromSnapshot', () => { @@ -293,11 +286,9 @@ describe('instance', () => { vpc, }); - expect(stack).toHaveResource('AWS::RDS::DBInstance', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBInstance', { DBSnapshotIdentifier: 'my-snapshot', }); - - }); test('can generate a new snapshot password', () => { @@ -310,8 +301,8 @@ describe('instance', () => { }), }); - expect(stack).toHaveResourceLike('AWS::RDS::DBInstance', { - MasterUsername: ABSENT, + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBInstance', { + MasterUsername: Match.absent(), MasterUserPassword: { 'Fn::Join': ['', [ '{{resolve:secretsmanager:', @@ -320,7 +311,7 @@ describe('instance', () => { ]], }, }); - expect(stack).toHaveResource('AWS::SecretsManager::Secret', { + Template.fromStack(stack).hasResourceProperties('AWS::SecretsManager::Secret', { Description: { 'Fn::Join': ['', ['Generated by the CDK for stack: ', { Ref: 'AWS::StackName' }]], }, @@ -331,8 +322,6 @@ describe('instance', () => { SecretStringTemplate: '{"username":"admin"}', }, }); - - }); test('fromGeneratedSecret with replica regions', () => { @@ -345,7 +334,7 @@ describe('instance', () => { }), }); - expect(stack).toHaveResource('AWS::SecretsManager::Secret', { + Template.fromStack(stack).hasResourceProperties('AWS::SecretsManager::Secret', { ReplicaRegions: [ { Region: 'eu-west-1', @@ -361,8 +350,6 @@ describe('instance', () => { vpc, credentials: { generatePassword: true }, })).toThrow(/`credentials` `username` must be specified when `generatePassword` is set to true/); - - }); test('can set a new snapshot password from an existing SecretValue', () => { @@ -374,12 +361,10 @@ describe('instance', () => { }); // TODO - Expect this to be broken - expect(stack).toHaveResourceLike('AWS::RDS::DBInstance', { - MasterUsername: ABSENT, + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBInstance', { + MasterUsername: Match.absent(), MasterUserPassword: 'mysecretpassword', }); - - }); test('can set a new snapshot password from an existing Secret', () => { @@ -394,14 +379,12 @@ describe('instance', () => { credentials: rds.SnapshotCredentials.fromSecret(secret), }); - expect(stack).toHaveResourceLike('AWS::RDS::DBInstance', { - MasterUsername: ABSENT, + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBInstance', { + MasterUsername: Match.absent(), MasterUserPassword: { 'Fn::Join': ['', ['{{resolve:secretsmanager:', { Ref: 'DBSecretD58955BC' }, ':SecretString:password::}}']], }, }); - - }); test('can create a new database instance with fromDatabaseInstanceAttributes using a token for the port', () => { @@ -425,9 +408,9 @@ describe('instance', () => { }); // THEN - expect(stack).toHaveOutput({ - exportName: 'databaseUrl', - outputValue: { + Template.fromStack(stack).hasOutput('portOutput', { + Export: { Name: 'databaseUrl' }, + Value: { Ref: 'DatabasePort', }, }); @@ -449,7 +432,7 @@ describe('instance', () => { }); // THEN - expect(stack).toHaveResource('AWS::RDS::DBInstance', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBInstance', { SourceDBInstanceIdentifier: { 'Fn::Join': ['', [ 'arn:', @@ -466,8 +449,6 @@ describe('instance', () => { Ref: 'ReadReplicaSubnetGroup680C605C', }, }); - - }); test('on event', () => { @@ -485,7 +466,7 @@ describe('instance', () => { instance.onEvent('InstanceEvent', { target: new targets.LambdaFunction(fn) }); // THEN - expect(stack).toHaveResource('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { EventPattern: { source: [ 'aws.rds', @@ -528,8 +509,6 @@ describe('instance', () => { }, ], }); - - }); test('on event without target', () => { @@ -542,7 +521,7 @@ describe('instance', () => { instance.onEvent('InstanceEvent'); // THEN - expect(stack).toHaveResource('AWS::Events::Rule', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { EventPattern: { source: [ 'aws.rds', @@ -574,8 +553,6 @@ describe('instance', () => { ], }, }); - - }); test('can use metricCPUUtilization', () => { @@ -593,8 +570,6 @@ describe('instance', () => { period: cdk.Duration.minutes(5), statistic: 'Average', }); - - }); test('can resolve endpoint port and socket address', () => { @@ -618,8 +593,6 @@ describe('instance', () => { ], ], }); - - }); test('can deactivate backup', () => { @@ -631,11 +604,9 @@ describe('instance', () => { }); // THEN - expect(stack).toHaveResource('AWS::RDS::DBInstance', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBInstance', { BackupRetentionPeriod: 0, }); - - }); test('imported instance with imported security group with allowAllOutbound set to false', () => { @@ -652,11 +623,9 @@ describe('instance', () => { instance.connections.allowToAnyIpv4(ec2.Port.tcp(443)); // THEN - expect(stack).toHaveResource('AWS::EC2::SecurityGroupEgress', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroupEgress', { GroupId: 'sg-123456789', }); - - }); test('create an instance with imported monitoring role', () => { @@ -676,14 +645,12 @@ describe('instance', () => { }); // THEN - expect(stack).toHaveResource('AWS::RDS::DBInstance', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBInstance', { MonitoringInterval: 60, MonitoringRoleArn: { 'Fn::GetAtt': ['MonitoringRole90457BF9', 'Arn'], }, - }, ResourcePart.Properties); - - + }); }); test('create an instance with an existing security group', () => { @@ -700,11 +667,11 @@ describe('instance', () => { instance.connections.allowDefaultPortFromAnyIpv4(); // THEN - expect(stack).toHaveResource('AWS::RDS::DBInstance', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBInstance', { VPCSecurityGroups: ['sg-123456789'], }); - expect(stack).toHaveResource('AWS::EC2::SecurityGroupIngress', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroupIngress', { FromPort: { 'Fn::GetAtt': [ 'InstanceC1063A87', @@ -719,8 +686,6 @@ describe('instance', () => { ], }, }); - - }); test('addRotationSingleUser()', () => { @@ -734,7 +699,7 @@ describe('instance', () => { instance.addRotationSingleUser(); // THEN - expect(stack).toHaveResource('AWS::SecretsManager::RotationSchedule', { + Template.fromStack(stack).hasResourceProperties('AWS::SecretsManager::RotationSchedule', { SecretId: { Ref: 'DatabaseSecretAttachmentE5D1B020', }, @@ -762,7 +727,7 @@ describe('instance', () => { instance.addRotationMultiUser('user', { secret: userSecret.attach(instance) }); // THEN - expect(stack).toHaveResource('AWS::SecretsManager::RotationSchedule', { + Template.fromStack(stack).hasResourceProperties('AWS::SecretsManager::RotationSchedule', { SecretId: { Ref: 'UserSecretAttachment16ACBE6D', }, @@ -777,7 +742,7 @@ describe('instance', () => { }, }); - expect(stack).toHaveResourceLike('AWS::Serverless::Application', { + Template.fromStack(stack).hasResourceProperties('AWS::Serverless::Application', { Parameters: { masterSecretArn: { Ref: 'DatabaseSecretAttachmentE5D1B020', @@ -812,13 +777,13 @@ describe('instance', () => { }); // THEN - expect(stack).toHaveResource('AWS::SecretsManager::RotationSchedule', { + Template.fromStack(stack).hasResourceProperties('AWS::SecretsManager::RotationSchedule', { RotationRules: { AutomaticallyAfterDays: 15, }, }); - expect(stack).toHaveResource('AWS::Serverless::Application', { + Template.fromStack(stack).hasResourceProperties('AWS::Serverless::Application', { Parameters: { endpoint: { 'Fn::Join': ['', [ @@ -870,7 +835,7 @@ describe('instance', () => { instance.addRotationSingleUser({ endpoint }); // THEN - expect(stack).toHaveResource('AWS::Serverless::Application', { + Template.fromStack(stack).hasResourceProperties('AWS::Serverless::Application', { Parameters: { endpoint: { 'Fn::Join': ['', [ @@ -910,8 +875,6 @@ describe('instance', () => { // THEN expect(() => instance.addRotationSingleUser()).toThrow(/without secret/); - - }); test('throws when trying to add single user rotation multiple times', () => { @@ -927,8 +890,6 @@ describe('instance', () => { // THEN expect(() => instance.addRotationSingleUser()).toThrow(/A single user rotation was already added to this instance/); - - }); test('throws when timezone is set for non-sqlserver database engine', () => { @@ -953,8 +914,6 @@ describe('instance', () => { vpc, })).toThrow(/timezone property can not be configured for/); }); - - }); test('create an instance from snapshot with maximum allocated storage', () => { @@ -967,12 +926,10 @@ describe('instance', () => { maxAllocatedStorage: 200, }); - expect(stack).toHaveResource('AWS::RDS::DBInstance', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBInstance', { DBSnapshotIdentifier: 'my-snapshot', MaxAllocatedStorage: 200, }); - - }); test('create a DB instance with maximum allocated storage', () => { @@ -985,12 +942,10 @@ describe('instance', () => { }); // THEN - expect(stack).toHaveResource('AWS::RDS::DBInstance', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBInstance', { BackupRetentionPeriod: 0, MaxAllocatedStorage: 250, }); - - }); test('iam authentication - off by default', () => { @@ -999,11 +954,9 @@ describe('instance', () => { vpc, }); - expect(stack).toHaveResourceLike('AWS::RDS::DBInstance', { - EnableIAMDatabaseAuthentication: ABSENT, + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBInstance', { + EnableIAMDatabaseAuthentication: Match.absent(), }); - - }); test('createGrant - creates IAM policy and enables IAM auth', () => { @@ -1016,10 +969,10 @@ describe('instance', () => { }); instance.grantConnect(role); - expect(stack).toHaveResourceLike('AWS::RDS::DBInstance', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBInstance', { EnableIAMDatabaseAuthentication: true, }); - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [{ Effect: 'Allow', @@ -1031,8 +984,6 @@ describe('instance', () => { Version: '2012-10-17', }, }); - - }); test('createGrant - throws if IAM auth disabled', () => { @@ -1046,8 +997,6 @@ describe('instance', () => { }); expect(() => { instance.grantConnect(role); }).toThrow(/Cannot grant connect when IAM authentication is disabled/); - - }); test('domain - sets domain property', () => { @@ -1061,11 +1010,9 @@ describe('instance', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::RDS::DBInstance', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBInstance', { Domain: domain, }); - - }); test('domain - uses role if provided', () => { @@ -1081,12 +1028,10 @@ describe('instance', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::RDS::DBInstance', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBInstance', { Domain: domain, DomainIAMRoleName: stack.resolve(role.roleName), }); - - }); test('domain - creates role if not provided', () => { @@ -1100,12 +1045,12 @@ describe('instance', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::RDS::DBInstance', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBInstance', { Domain: domain, - DomainIAMRoleName: anything(), + DomainIAMRoleName: Match.anyValue(), }); - expect(stack).toHaveResource('AWS::IAM::Role', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { AssumeRolePolicyDocument: { Statement: [ { @@ -1133,8 +1078,6 @@ describe('instance', () => { }, ], }); - - }); test('throws when domain is set for mariadb database engine', () => { @@ -1161,8 +1104,6 @@ describe('instance', () => { vpc, })).toThrow(expectedError); }); - - }); describe('performance insights', () => { @@ -1175,13 +1116,11 @@ describe('instance', () => { performanceInsightEncryptionKey: new kms.Key(stack, 'Key'), }); - expect(stack).toHaveResource('AWS::RDS::DBInstance', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBInstance', { EnablePerformanceInsights: true, PerformanceInsightsRetentionPeriod: 731, PerformanceInsightsKMSKeyId: { 'Fn::GetAtt': ['Key961B73FD', 'Arn'] }, }); - - }); test('setting performance insights fields enables performance insights', () => { @@ -1191,12 +1130,10 @@ describe('instance', () => { performanceInsightRetention: rds.PerformanceInsightRetention.LONG_TERM, }); - expect(stack).toHaveResource('AWS::RDS::DBInstance', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBInstance', { EnablePerformanceInsights: true, PerformanceInsightsRetentionPeriod: 731, }); - - }); test('throws if performance insights fields are set but performance insights is disabled', () => { @@ -1208,8 +1145,6 @@ describe('instance', () => { performanceInsightRetention: rds.PerformanceInsightRetention.DEFAULT, }); }).toThrow(/`enablePerformanceInsights` disabled, but `performanceInsightRetention` or `performanceInsightEncryptionKey` was set/); - - }); }); @@ -1220,12 +1155,10 @@ describe('instance', () => { subnetGroup: rds.SubnetGroup.fromSubnetGroupName(stack, 'SubnetGroup', 'my-subnet-group'), }); - expect(stack).toHaveResourceLike('AWS::RDS::DBInstance', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBInstance', { DBSubnetGroupName: 'my-subnet-group', }); - expect(stack).toCountResources('AWS::RDS::DBSubnetGroup', 0); - - + Template.fromStack(stack).resourceCountIs('AWS::RDS::DBSubnetGroup', 0); }); test('defaultChild returns the DB Instance', () => { @@ -1236,8 +1169,6 @@ describe('instance', () => { // THEN expect(instance.node.defaultChild instanceof rds.CfnDBInstance).toBeTruthy(); - - }); test("PostgreSQL database instance uses a different default master username than 'admin', which is a reserved word", () => { @@ -1249,13 +1180,11 @@ describe('instance', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::SecretsManager::Secret', { + Template.fromStack(stack).hasResourceProperties('AWS::SecretsManager::Secret', { GenerateSecretString: { SecretStringTemplate: '{"username":"postgres"}', }, }); - - }); describe('S3 Import/Export', () => { @@ -1269,7 +1198,7 @@ describe('instance', () => { s3ExportBuckets: [new s3.Bucket(stack, 'S3Export')], }); - expect(stack).toHaveResource('AWS::RDS::DBInstance', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBInstance', { AssociatedRoles: [ { FeatureName: 'S3_INTEGRATION', @@ -1280,7 +1209,7 @@ describe('instance', () => { }); // Can read from import bucket, and read/write from export bucket - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [{ Action: [ @@ -1312,8 +1241,6 @@ describe('instance', () => { Version: '2012-10-17', }, }); - - }); test('throws if using s3 import on unsupported engine', () => { @@ -1335,8 +1262,6 @@ describe('instance', () => { s3ImportRole, }); }).toThrow(/Engine 'mysql-8.0.19' does not support S3 import/); - - }); test('throws if using s3 export on unsupported engine', () => { @@ -1358,8 +1283,6 @@ describe('instance', () => { s3ExportRole: s3ExportRole, }); }).toThrow(/Engine 'mysql-8.0.19' does not support S3 export/); - - }); test('throws if provided two different roles for import/export', () => { @@ -1378,8 +1301,6 @@ describe('instance', () => { s3ExportRole, }); }).toThrow(/S3 import and export roles must be the same/); - - }); }); @@ -1392,7 +1313,7 @@ describe('instance', () => { }); // THEN - expect(stack).toHaveResource('AWS::RDS::DBInstance', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBInstance', { MasterUsername: 'postgres', // username is a string MasterUserPassword: { 'Fn::Join': [ @@ -1420,7 +1341,7 @@ describe('instance', () => { }); // THEN - expect(stack).toHaveResource('AWS::SecretsManager::Secret', { + Template.fromStack(stack).hasResourceProperties('AWS::SecretsManager::Secret', { ReplicaRegions: [ { Region: 'eu-west-1', @@ -1438,7 +1359,7 @@ describe('instance', () => { }); // THEN - expect(stack).toHaveResource('AWS::RDS::DBInstance', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBInstance', { MasterUsername: 'postgres', // username is a string MasterUserPassword: '{{resolve:ssm-secure:/dbPassword:1}}', // reference to SSM }); @@ -1460,7 +1381,7 @@ describe('instance', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::SecretsManager::Secret', { + Template.fromStack(stack).hasResourceProperties('AWS::SecretsManager::Secret', { Name: secretName, }); }); @@ -1477,7 +1398,7 @@ describe('instance', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::SecretsManager::Secret', { + Template.fromStack(stack).hasResourceProperties('AWS::SecretsManager::Secret', { Name: secretName, }); }); @@ -1494,11 +1415,9 @@ describe('instance', () => { publiclyAccessible: false, }); - expect(stack).toHaveResource('AWS::RDS::DBInstance', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBInstance', { PubliclyAccessible: false, }); - - }); test('can set publiclyAccessible to true with private subnets', () => { @@ -1513,7 +1432,7 @@ describe('instance', () => { publiclyAccessible: true, }); - expect(stack).toHaveResource('AWS::RDS::DBInstance', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBInstance', { PubliclyAccessible: true, }); }); @@ -1537,7 +1456,7 @@ describe('instance', () => { } ); // THEN - expect(stack).toHaveResource('AWS::RDS::DBInstance', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBInstance', { DBInstanceIdentifier: instanceIdentifier.toLowerCase(), }); }); @@ -1559,7 +1478,7 @@ describe('instance', () => { } ); // THEN - expect(stack).toHaveResource('AWS::RDS::DBInstance', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBInstance', { DBInstanceIdentifier: instanceIdentifier, }); }); @@ -1605,7 +1524,7 @@ describe('instance', () => { }); // THEN - expect(stack).toHaveResource('AWS::RDS::DBInstance', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBInstance', { DBParameterGroupName: { Ref: 'ParameterGroup5E32DECB', }, @@ -1622,7 +1541,7 @@ describe('instance', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::RDS::DBInstance', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBInstance', { Port: '3306', }); }); @@ -1642,7 +1561,7 @@ describe('instance', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::RDS::DBInstance', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBInstance', { Port: { Ref: 'Port', }, @@ -1652,8 +1571,8 @@ describe('instance', () => { test.each([ [cdk.RemovalPolicy.RETAIN, 'Retain', 'Retain'], - [cdk.RemovalPolicy.SNAPSHOT, 'Snapshot', ABSENT], - [cdk.RemovalPolicy.DESTROY, 'Delete', ABSENT], + [cdk.RemovalPolicy.SNAPSHOT, 'Snapshot', Match.absent()], + [cdk.RemovalPolicy.DESTROY, 'Delete', Match.absent()], ])('if Instance RemovalPolicy is \'%s\', the instance has DeletionPolicy \'%s\' and the DBSubnetGroup has \'%s\'', (instanceRemovalPolicy, instanceValue, subnetValue) => { // GIVEN stack = new cdk.Stack(); @@ -1670,15 +1589,15 @@ test.each([ }); // THEN - expect(stack).toHaveResourceLike('AWS::RDS::DBInstance', { + Template.fromStack(stack).hasResource('AWS::RDS::DBInstance', { DeletionPolicy: instanceValue, UpdateReplacePolicy: instanceValue, - }, ResourcePart.CompleteDefinition); + }); - expect(stack).toHaveResourceLike('AWS::RDS::DBSubnetGroup', { + Template.fromStack(stack).hasResource('AWS::RDS::DBSubnetGroup', { DeletionPolicy: subnetValue, UpdateReplacePolicy: subnetValue, - }, ResourcePart.CompleteDefinition); + }); }); describe('cross-account instance', () => { @@ -1707,7 +1626,7 @@ describe('cross-account instance', () => { value: instance.instanceIdentifier, }); - expect(outputStack).toMatchTemplate({ + Template.fromStack(outputStack).templateMatches({ Outputs: { DatabaseInstanceArn: { Value: { diff --git a/packages/@aws-cdk/aws-rds/test/option-group.test.ts b/packages/@aws-cdk/aws-rds/test/option-group.test.ts index a0ab91ac0dfdf..5e8fb5b9f5261 100644 --- a/packages/@aws-cdk/aws-rds/test/option-group.test.ts +++ b/packages/@aws-cdk/aws-rds/test/option-group.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as cdk from '@aws-cdk/core'; import { DatabaseInstanceEngine, OptionGroup, OracleEngineVersion } from '../lib'; @@ -21,7 +21,7 @@ describe('option group', () => { }); // THEN - expect(stack).toHaveResource('AWS::RDS::OptionGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::OptionGroup', { EngineName: 'oracle-se2', MajorEngineVersion: '12.1', OptionConfigurations: [ @@ -30,8 +30,6 @@ describe('option group', () => { }, ], }); - - }); test('option group with new security group', () => { @@ -55,7 +53,7 @@ describe('option group', () => { optionGroup.optionConnections.OEM.connections.allowDefaultPortFromAnyIpv4(); // THEN - expect(stack).toHaveResource('AWS::RDS::OptionGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::OptionGroup', { OptionConfigurations: [ { OptionName: 'OEM', @@ -72,7 +70,7 @@ describe('option group', () => { ], }); - expect(stack).toHaveResource('AWS::EC2::SecurityGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroup', { GroupDescription: 'Security group for OEM option', SecurityGroupIngress: [ { @@ -87,8 +85,6 @@ describe('option group', () => { Ref: 'VPCB9E5F0B4', }, }); - - }); test('option group with existing security group', () => { @@ -113,7 +109,7 @@ describe('option group', () => { }); // THEN - expect(stack).toHaveResource('AWS::RDS::OptionGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::OptionGroup', { OptionConfigurations: [ { OptionName: 'OEM', @@ -129,8 +125,6 @@ describe('option group', () => { }, ], }); - - }); test('throws when using an option with port and no vpc', () => { @@ -149,7 +143,5 @@ describe('option group', () => { }, ], })).toThrow(/`port`.*`vpc`/); - - }); }); diff --git a/packages/@aws-cdk/aws-rds/test/parameter-group.test.ts b/packages/@aws-cdk/aws-rds/test/parameter-group.test.ts index bf8e789aee0ad..594fab914310a 100644 --- a/packages/@aws-cdk/aws-rds/test/parameter-group.test.ts +++ b/packages/@aws-cdk/aws-rds/test/parameter-group.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as cdk from '@aws-cdk/core'; import { DatabaseClusterEngine, ParameterGroup } from '../lib'; @@ -17,10 +17,8 @@ describe('parameter group', () => { }); // THEN - expect(stack).toCountResources('AWS::RDS::DBParameterGroup', 0); - expect(stack).toCountResources('AWS::RDS::DBClusterParameterGroup', 0); - - + Template.fromStack(stack).resourceCountIs('AWS::RDS::DBParameterGroup', 0); + Template.fromStack(stack).resourceCountIs('AWS::RDS::DBClusterParameterGroup', 0); }); test('create a parameter group when bound to an instance', () => { @@ -38,15 +36,13 @@ describe('parameter group', () => { parameterGroup.bindToInstance({}); // THEN - expect(stack).toHaveResource('AWS::RDS::DBParameterGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBParameterGroup', { Description: 'desc', Family: 'aurora5.6', Parameters: { key: 'value', }, }); - - }); test('create a parameter group when bound to a cluster', () => { @@ -64,15 +60,13 @@ describe('parameter group', () => { parameterGroup.bindToCluster({}); // THEN - expect(stack).toHaveResource('AWS::RDS::DBClusterParameterGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBClusterParameterGroup', { Description: 'desc', Family: 'aurora5.6', Parameters: { key: 'value', }, }); - - }); test('creates 2 parameter groups when bound to a cluster and an instance', () => { @@ -91,10 +85,8 @@ describe('parameter group', () => { parameterGroup.bindToInstance({}); // THEN - expect(stack).toCountResources('AWS::RDS::DBParameterGroup', 1); - expect(stack).toCountResources('AWS::RDS::DBClusterParameterGroup', 1); - - + Template.fromStack(stack).resourceCountIs('AWS::RDS::DBParameterGroup', 1); + Template.fromStack(stack).resourceCountIs('AWS::RDS::DBClusterParameterGroup', 1); }); test('Add an additional parameter to an existing parameter group', () => { @@ -114,7 +106,7 @@ describe('parameter group', () => { clusterParameterGroup.addParameter('key2', 'value2'); // THEN - expect(stack).toHaveResource('AWS::RDS::DBClusterParameterGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBClusterParameterGroup', { Description: 'desc', Family: 'aurora5.6', Parameters: { @@ -122,7 +114,5 @@ describe('parameter group', () => { key2: 'value2', }, }); - - }); }); diff --git a/packages/@aws-cdk/aws-rds/test/proxy.test.ts b/packages/@aws-cdk/aws-rds/test/proxy.test.ts index 9cd48e7686dd9..f98e36bdf3647 100644 --- a/packages/@aws-cdk/aws-rds/test/proxy.test.ts +++ b/packages/@aws-cdk/aws-rds/test/proxy.test.ts @@ -1,5 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; -import { ABSENT, ResourcePart } from '@aws-cdk/assert-internal'; +import { Match, Template } from '@aws-cdk/assertions'; import * as ec2 from '@aws-cdk/aws-ec2'; import { AccountPrincipal, Role } from '@aws-cdk/aws-iam'; import * as secretsmanager from '@aws-cdk/aws-secretsmanager'; @@ -15,8 +14,6 @@ describe('proxy', () => { beforeEach(() => { stack = new cdk.Stack(); vpc = new ec2.Vpc(stack, 'VPC'); - - }); test('create a DB proxy from an instance', () => { @@ -36,7 +33,7 @@ describe('proxy', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::RDS::DBProxy', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBProxy', { Auth: [ { AuthScheme: 'SECRETS', @@ -66,7 +63,7 @@ describe('proxy', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::RDS::DBProxyTargetGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBProxyTargetGroup', { DBProxyName: { Ref: 'ProxyCB0DFB71', }, @@ -78,8 +75,6 @@ describe('proxy', () => { ], TargetGroupName: 'default', }); - - }); test('create a DB proxy from a cluster', () => { @@ -99,7 +94,7 @@ describe('proxy', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::RDS::DBProxy', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBProxy', { Auth: [ { AuthScheme: 'SECRETS', @@ -127,7 +122,7 @@ describe('proxy', () => { }, ], }); - expect(stack).toHaveResourceLike('AWS::RDS::DBProxyTargetGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBProxyTargetGroup', { DBProxyName: { Ref: 'ProxyCB0DFB71', }, @@ -137,10 +132,10 @@ describe('proxy', () => { Ref: 'DatabaseB269D8BB', }, ], - DBInstanceIdentifiers: ABSENT, + DBInstanceIdentifiers: Match.absent(), TargetGroupName: 'default', }); - expect(stack).toHaveResourceLike('AWS::EC2::SecurityGroupIngress', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroupIngress', { IpProtocol: 'tcp', Description: 'Allow connections to the database Cluster from the Proxy', FromPort: { @@ -156,8 +151,6 @@ describe('proxy', () => { 'Fn::GetAtt': ['DatabaseB269D8BB', 'Endpoint.Port'], }, }); - - }); test('One or more secrets are required.', () => { @@ -175,8 +168,6 @@ describe('proxy', () => { vpc, }); }).toThrow('One or more secrets are required.'); - - }); test('fails when trying to create a proxy for a target without an engine', () => { @@ -191,8 +182,6 @@ describe('proxy', () => { secrets: [new secretsmanager.Secret(stack, 'Secret')], }); }).toThrow(/Could not determine engine for proxy target 'Default\/Cluster'\. Please provide it explicitly when importing the resource/); - - }); test("fails when trying to create a proxy for a target with an engine that doesn't have engineFamily", () => { @@ -213,8 +202,6 @@ describe('proxy', () => { secrets: [new secretsmanager.Secret(stack, 'Secret')], }); }).toThrow(/Engine 'mariadb-10\.0\.24' does not support proxies/); - - }); test('correctly creates a proxy for an imported Cluster if its engine is known', () => { @@ -232,20 +219,18 @@ describe('proxy', () => { secrets: [new secretsmanager.Secret(stack, 'Secret')], }); - expect(stack).toHaveResourceLike('AWS::RDS::DBProxy', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBProxy', { EngineFamily: 'POSTGRESQL', }); - expect(stack).toHaveResourceLike('AWS::RDS::DBProxyTargetGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBProxyTargetGroup', { DBClusterIdentifiers: [ 'my-cluster', ], }); - expect(stack).toHaveResourceLike('AWS::EC2::SecurityGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroup', { GroupDescription: 'SecurityGroup for Database Proxy', VpcId: { Ref: 'VPCB9E5F0B4' }, }); - - }); describe('imported Proxies', () => { @@ -256,8 +241,6 @@ describe('proxy', () => { endpoint: 'my-endpoint', securityGroups: [], }); - - }); test('grant rds-db:connect in grantConnect() with a dbUser explicitly passed', () => { @@ -269,7 +252,7 @@ describe('proxy', () => { importedDbProxy.grantConnect(role, databaseUser); // THEN - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [{ Effect: 'Allow', @@ -289,8 +272,6 @@ describe('proxy', () => { Version: '2012-10-17', }, }); - - }); test('throws when grantConnect() is used without a dbUser', () => { @@ -303,8 +284,6 @@ describe('proxy', () => { expect(() => { importedDbProxy.grantConnect(role); }).toThrow(/For imported Database Proxies, the dbUser is required in grantConnect/); - - }); }); @@ -328,7 +307,7 @@ describe('proxy', () => { proxy.grantConnect(role); // THEN - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [{ Effect: 'Allow', @@ -362,8 +341,6 @@ describe('proxy', () => { Version: '2012-10-17', }, }); - - }); test('new Proxy with multiple Secrets cannot use grantConnect() without a dbUser passed', () => { @@ -391,8 +368,6 @@ describe('proxy', () => { expect(() => { proxy.grantConnect(role); }).toThrow(/When the Proxy contains multiple Secrets, you must pass a dbUser explicitly to grantConnect/); - - }); test('DBProxyTargetGroup should have dependency on the proxy targets', () => { @@ -412,7 +387,7 @@ describe('proxy', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::RDS::DBProxyTargetGroup', { + Template.fromStack(stack).hasResource('AWS::RDS::DBProxyTargetGroup', { Properties: { DBProxyName: { Ref: 'proxy3A1DA9C7', @@ -429,8 +404,6 @@ describe('proxy', () => { 'clusterSecurityGroupF441DCEA', 'clusterSubnets81E3593F', ], - }, ResourcePart.CompleteDefinition); - - + }); }); }); diff --git a/packages/@aws-cdk/aws-rds/test/serverless-cluster-from-snapshot.test.ts b/packages/@aws-cdk/aws-rds/test/serverless-cluster-from-snapshot.test.ts index 4953fa6da7a71..489cd91ae861d 100644 --- a/packages/@aws-cdk/aws-rds/test/serverless-cluster-from-snapshot.test.ts +++ b/packages/@aws-cdk/aws-rds/test/serverless-cluster-from-snapshot.test.ts @@ -1,5 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; -import { ABSENT, ResourcePart } from '@aws-cdk/assert-internal'; +import { Match, Template } from '@aws-cdk/assertions'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as kms from '@aws-cdk/aws-kms'; import * as cdk from '@aws-cdk/core'; @@ -18,7 +17,7 @@ describe('serverless cluster from snapshot', () => { }); // THEN - expect(stack).toHaveResource('AWS::RDS::DBCluster', { + Template.fromStack(stack).hasResource('AWS::RDS::DBCluster', { Properties: { Engine: 'aurora-mysql', DBClusterParameterGroupName: 'default.aurora-mysql5.7', @@ -39,7 +38,7 @@ describe('serverless cluster from snapshot', () => { }, DeletionPolicy: 'Snapshot', UpdateReplacePolicy: 'Snapshot', - }, ResourcePart.CompleteDefinition); + }); }); test('can generate a new snapshot password', () => { @@ -57,8 +56,8 @@ describe('serverless cluster from snapshot', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::RDS::DBCluster', { - MasterUsername: ABSENT, + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBCluster', { + MasterUsername: Match.absent(), MasterUserPassword: { 'Fn::Join': ['', [ '{{resolve:secretsmanager:', @@ -67,7 +66,7 @@ describe('serverless cluster from snapshot', () => { ]], }, }); - expect(stack).toHaveResource('AWS::SecretsManager::Secret', { + Template.fromStack(stack).hasResourceProperties('AWS::SecretsManager::Secret', { Description: { 'Fn::Join': ['', ['Generated by the CDK for stack: ', { Ref: 'AWS::StackName' }]], }, @@ -95,7 +94,7 @@ describe('serverless cluster from snapshot', () => { }); // THEN - expect(stack).toHaveResource('AWS::SecretsManager::Secret', { + Template.fromStack(stack).hasResourceProperties('AWS::SecretsManager::Secret', { ReplicaRegions: [ { Region: 'eu-west-1', @@ -130,8 +129,8 @@ describe('serverless cluster from snapshot', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::RDS::DBCluster', { - MasterUsername: ABSENT, + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBCluster', { + MasterUsername: Match.absent(), MasterUserPassword: 'mysecretpassword', }); }); @@ -153,8 +152,8 @@ describe('serverless cluster from snapshot', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::RDS::DBCluster', { - MasterUsername: ABSENT, + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBCluster', { + MasterUsername: Match.absent(), MasterUserPassword: { 'Fn::Join': ['', ['{{resolve:secretsmanager:', { Ref: 'DBSecretD58955BC' }, ':SecretString:password::}}']], }, @@ -162,8 +161,8 @@ describe('serverless cluster from snapshot', () => { }); }); -function testStack(app?: cdk.App, id?: string): cdk.Stack { - const stack = new cdk.Stack(app, id, { env: { account: '12345', region: 'us-test-1' } }); +function testStack(): cdk.Stack { + const stack = new cdk.Stack(undefined, undefined, { env: { account: '12345', region: 'us-test-1' } }); stack.node.setContext('availability-zones:12345:us-test-1', ['us-test-1a', 'us-test-1b']); return stack; } diff --git a/packages/@aws-cdk/aws-rds/test/serverless-cluster.test.ts b/packages/@aws-cdk/aws-rds/test/serverless-cluster.test.ts index dff8d035b78a3..3849a99cdeb02 100644 --- a/packages/@aws-cdk/aws-rds/test/serverless-cluster.test.ts +++ b/packages/@aws-cdk/aws-rds/test/serverless-cluster.test.ts @@ -1,5 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; -import { ResourcePart, SynthUtils } from '@aws-cdk/assert-internal'; +import { Template } from '@aws-cdk/assertions'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as iam from '@aws-cdk/aws-iam'; import * as kms from '@aws-cdk/aws-kms'; @@ -25,7 +24,7 @@ describe('serverless cluster', () => { }); // THEN - expect(stack).toHaveResource('AWS::RDS::DBCluster', { + Template.fromStack(stack).hasResource('AWS::RDS::DBCluster', { Properties: { Engine: 'aurora-postgresql', DBClusterParameterGroupName: 'default.aurora-postgresql10', @@ -47,9 +46,7 @@ describe('serverless cluster', () => { }, DeletionPolicy: 'Snapshot', UpdateReplacePolicy: 'Snapshot', - }, ResourcePart.CompleteDefinition); - - + }); }); test('can create a Serverless Cluster with Aurora Mysql database engine', () => { @@ -64,7 +61,7 @@ describe('serverless cluster', () => { }); // THEN - expect(stack).toHaveResource('AWS::RDS::DBCluster', { + Template.fromStack(stack).hasResource('AWS::RDS::DBCluster', { Properties: { Engine: 'aurora-mysql', DBClusterParameterGroupName: 'default.aurora-mysql5.7', @@ -108,8 +105,7 @@ describe('serverless cluster', () => { }, DeletionPolicy: 'Snapshot', UpdateReplacePolicy: 'Snapshot', - }, ResourcePart.CompleteDefinition); - + }); }); test('can create a Serverless cluster with imported vpc and security group', () => { @@ -129,7 +125,7 @@ describe('serverless cluster', () => { }); // THEN - expect(stack).toHaveResource('AWS::RDS::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBCluster', { Engine: 'aurora-postgresql', DBClusterParameterGroupName: 'default.aurora-postgresql10', EngineMode: 'serverless', @@ -160,8 +156,6 @@ describe('serverless cluster', () => { }, VpcSecurityGroupIds: ['SecurityGroupId12345'], }); - - }); test("sets the retention policy of the SubnetGroup to 'Retain' if the Serverless Cluster is created with 'Retain'", () => { @@ -174,12 +168,10 @@ describe('serverless cluster', () => { removalPolicy: cdk.RemovalPolicy.RETAIN, }); - expect(stack).toHaveResourceLike('AWS::RDS::DBSubnetGroup', { + Template.fromStack(stack).hasResource('AWS::RDS::DBSubnetGroup', { DeletionPolicy: 'Retain', UpdateReplacePolicy: 'Retain', - }, ResourcePart.CompleteDefinition); - - + }); }); test('creates a secret when master credentials are not specified', () => { @@ -198,7 +190,7 @@ describe('serverless cluster', () => { }); // THEN - expect(stack).toHaveResource('AWS::RDS::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBCluster', { MasterUsername: { 'Fn::Join': [ '', @@ -225,7 +217,7 @@ describe('serverless cluster', () => { }, }); - expect(stack).toHaveResource('AWS::SecretsManager::Secret', { + Template.fromStack(stack).hasResourceProperties('AWS::SecretsManager::Secret', { GenerateSecretString: { ExcludeCharacters: '"@/\\', GenerateStringKey: 'password', @@ -233,8 +225,6 @@ describe('serverless cluster', () => { SecretStringTemplate: '{"username":"myuser"}', }, }); - - }); test('create an Serverless cluster with custom KMS key for storage', () => { @@ -250,7 +240,7 @@ describe('serverless cluster', () => { }); // THEN - expect(stack).toHaveResource('AWS::RDS::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBCluster', { KmsKeyId: { 'Fn::GetAtt': [ 'Key961B73FD', @@ -258,8 +248,6 @@ describe('serverless cluster', () => { ], }, }); - - }); test('create a cluster using a specific version of Postgresql', () => { @@ -276,13 +264,11 @@ describe('serverless cluster', () => { }); // THEN - expect(stack).toHaveResource('AWS::RDS::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBCluster', { Engine: 'aurora-postgresql', EngineMode: 'serverless', EngineVersion: '10.7', }); - - }); test('cluster exposes different read and write endpoints', () => { @@ -302,8 +288,6 @@ describe('serverless cluster', () => { // THEN expect(stack.resolve(cluster.clusterEndpoint)).not .toEqual(stack.resolve(cluster.clusterReadEndpoint)); - - }); test('imported cluster with imported security group honors allowAllOutbound', () => { @@ -324,11 +308,9 @@ describe('serverless cluster', () => { cluster.connections.allowToAnyIpv4(ec2.Port.tcp(443)); // THEN - expect(stack).toHaveResource('AWS::EC2::SecurityGroupEgress', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroupEgress', { GroupId: 'sg-123456789', }); - - }); test('can import a serverless cluster with minimal attributes', () => { @@ -339,8 +321,6 @@ describe('serverless cluster', () => { }); expect(cluster.clusterIdentifier).toEqual('identifier'); - - }); test('minimal imported cluster throws on accessing attributes for missing parameters', () => { @@ -352,8 +332,6 @@ describe('serverless cluster', () => { expect(() => cluster.clusterEndpoint).toThrow(/Cannot access `clusterEndpoint` of an imported cluster/); expect(() => cluster.clusterReadEndpoint).toThrow(/Cannot access `clusterReadEndpoint` of an imported cluster/); - - }); test('imported cluster can access properties if attributes are provided', () => { @@ -371,8 +349,6 @@ describe('serverless cluster', () => { expect(cluster.clusterEndpoint.socketAddress).toEqual('addr:3306'); expect(cluster.clusterReadEndpoint.socketAddress).toEqual('reader-address:3306'); - - }); test('throws when trying to add rotation to a serverless cluster without secret', () => { @@ -392,8 +368,6 @@ describe('serverless cluster', () => { // THEN expect(() => cluster.addRotationSingleUser()).toThrow(/without secret/); - - }); test('throws when trying to add single user rotation multiple times', () => { @@ -411,8 +385,6 @@ describe('serverless cluster', () => { // THEN expect(() => cluster.addRotationSingleUser()).toThrow(/A single user rotation was already added to this cluster/); - - }); test('can set deletion protection', () => { @@ -428,11 +400,9 @@ describe('serverless cluster', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::RDS::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBCluster', { DeletionProtection: true, }); - - }); test('can set backup retention', () => { @@ -448,16 +418,15 @@ describe('serverless cluster', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::RDS::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBCluster', { BackupRetentionPeriod: 2, }); - - }); test('does not throw (but adds a node error) if a (dummy) VPC does not have sufficient subnets', () => { // GIVEN - const stack = testStack(); + const app = new cdk.App(); + const stack = testStack(app, 'TestStack'); const vpc = ec2.Vpc.fromLookup(stack, 'VPC', { isDefault: true }); // WHEN @@ -470,11 +439,9 @@ describe('serverless cluster', () => { }); // THEN - const art = SynthUtils.synthesize(stack); + const art = app.synth().getStackArtifact('TestStack'); const meta = art.findMetadataByType('aws:cdk:error'); expect(meta[0].data).toEqual('Cluster requires at least 2 subnets, got 0'); - - }); test('can set scaling configuration', () => { @@ -494,7 +461,7 @@ describe('serverless cluster', () => { }); //THEN - expect(stack).toHaveResource('AWS::RDS::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBCluster', { ScalingConfiguration: { AutoPause: true, MaxCapacity: 128, @@ -502,8 +469,6 @@ describe('serverless cluster', () => { SecondsUntilAutoPause: 600, }, }); - - }); test('can enable Data API', () => { @@ -519,11 +484,9 @@ describe('serverless cluster', () => { }); //THEN - expect(stack).toHaveResource('AWS::RDS::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBCluster', { EnableHttpEndpoint: true, }); - - }); test('default scaling options', () => { @@ -539,13 +502,11 @@ describe('serverless cluster', () => { }); //THEN - expect(stack).toHaveResource('AWS::RDS::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBCluster', { ScalingConfiguration: { AutoPause: true, }, }); - - }); test('auto pause is disabled if a time of zero is specified', () => { @@ -563,13 +524,11 @@ describe('serverless cluster', () => { }); //THEN - expect(stack).toHaveResource('AWS::RDS::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBCluster', { ScalingConfiguration: { AutoPause: false, }, }); - - }); test('throws when invalid auto pause time is specified', () => { @@ -595,8 +554,6 @@ describe('serverless cluster', () => { autoPause: cdk.Duration.days(2), }, })).toThrow(/auto pause time must be between 5 minutes and 1 day./); - - }); test('throws when invalid backup retention period is specified', () => { @@ -618,8 +575,6 @@ describe('serverless cluster', () => { vpc, backupRetention: cdk.Duration.days(36), })).toThrow(/backup retention period must be between 1 and 35 days. received: 36/); - - }); test('throws error when min capacity is greater than max capacity', () => { @@ -637,8 +592,6 @@ describe('serverless cluster', () => { maxCapacity: AuroraCapacityUnit.ACU_1, }, })).toThrow(/maximum capacity must be greater than or equal to minimum capacity./); - - }); test('check that clusterArn property works', () => { @@ -669,7 +622,6 @@ describe('serverless cluster', () => { ], ], }); - }); test('can grant Data API access', () => { @@ -687,7 +639,7 @@ describe('serverless cluster', () => { cluster.grantDataApiAccess(user); // THEN - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -721,8 +673,6 @@ describe('serverless cluster', () => { }, ], }); - - }); test('can grant Data API access on imported cluster with given secret', () => { @@ -742,7 +692,7 @@ describe('serverless cluster', () => { cluster.grantDataApiAccess(user); // THEN - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -776,8 +726,6 @@ describe('serverless cluster', () => { }, ], }); - - }); test('grant Data API access enables the Data API', () => { @@ -794,11 +742,9 @@ describe('serverless cluster', () => { cluster.grantDataApiAccess(user); //THEN - expect(stack).toHaveResource('AWS::RDS::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBCluster', { EnableHttpEndpoint: true, }); - - }); test('grant Data API access throws if the Data API is disabled', () => { @@ -814,8 +760,6 @@ describe('serverless cluster', () => { // WHEN expect(() => cluster.grantDataApiAccess(user)).toThrow(/Cannot grant Data API access when the Data API is disabled/); - - }); test('changes the case of the cluster identifier if the lowercaseDbIdentifier feature flag is enabled', () => { @@ -835,11 +779,9 @@ describe('serverless cluster', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::RDS::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBCluster', { DBClusterIdentifier: clusterIdentifier.toLowerCase(), }); - - }); test('does not change the case of the cluster identifier if the lowercaseDbIdentifier feature flag is disabled', () => { @@ -857,11 +799,9 @@ describe('serverless cluster', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::RDS::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBCluster', { DBClusterIdentifier: clusterIdentifier, }); - - }); }); diff --git a/packages/@aws-cdk/aws-rds/test/sql-server/sql-server.instance-engine.test.ts b/packages/@aws-cdk/aws-rds/test/sql-server/sql-server.instance-engine.test.ts index 72615d08f2093..a591dfd4a7bf1 100644 --- a/packages/@aws-cdk/aws-rds/test/sql-server/sql-server.instance-engine.test.ts +++ b/packages/@aws-cdk/aws-rds/test/sql-server/sql-server.instance-engine.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as core from '@aws-cdk/core'; import * as rds from '../../lib'; @@ -12,11 +12,9 @@ describe('sql server instance engine', () => { }), }).bindToInstance({}); - expect(stack).toHaveResourceLike('AWS::RDS::DBParameterGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBParameterGroup', { Family: 'sqlserver-web-11.0', }); - - }); test("has MajorEngineVersion ending in '11.00' for major version 11", () => { @@ -35,11 +33,9 @@ describe('sql server instance engine', () => { ], }); - expect(stack).toHaveResourceLike('AWS::RDS::OptionGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::OptionGroup', { MajorEngineVersion: '11.00', }); - - }); }); }); diff --git a/packages/@aws-cdk/aws-rds/test/subnet-group.test.ts b/packages/@aws-cdk/aws-rds/test/subnet-group.test.ts index 1dc9718258deb..44fd4e24482a8 100644 --- a/packages/@aws-cdk/aws-rds/test/subnet-group.test.ts +++ b/packages/@aws-cdk/aws-rds/test/subnet-group.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as cdk from '@aws-cdk/core'; import * as rds from '../lib'; @@ -10,7 +10,6 @@ describe('subnet group', () => { beforeEach(() => { stack = new cdk.Stack(); vpc = new ec2.Vpc(stack, 'VPC'); - }); test('creates a subnet group from minimal properties', () => { @@ -19,15 +18,13 @@ describe('subnet group', () => { vpc, }); - expect(stack).toHaveResource('AWS::RDS::DBSubnetGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBSubnetGroup', { DBSubnetGroupDescription: 'MyGroup', SubnetIds: [ { Ref: 'VPCPrivateSubnet1Subnet8BCA10E0' }, { Ref: 'VPCPrivateSubnet2SubnetCFCDAA7A' }, ], }); - - }); test('creates a subnet group from all properties', () => { @@ -38,7 +35,7 @@ describe('subnet group', () => { vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE }, }); - expect(stack).toHaveResource('AWS::RDS::DBSubnetGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBSubnetGroup', { DBSubnetGroupDescription: 'My Shared Group', DBSubnetGroupName: 'sharedgroup', SubnetIds: [ @@ -46,8 +43,6 @@ describe('subnet group', () => { { Ref: 'VPCPrivateSubnet2SubnetCFCDAA7A' }, ], }); - - }); test('correctly creates a subnet group with a deploy-time value for its name', () => { @@ -59,13 +54,11 @@ describe('subnet group', () => { vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE }, }); - expect(stack).toHaveResourceLike('AWS::RDS::DBSubnetGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBSubnetGroup', { DBSubnetGroupName: { Ref: 'Parameter', }, }); - - }); describe('subnet selection', () => { @@ -75,15 +68,13 @@ describe('subnet group', () => { vpc, }); - expect(stack).toHaveResource('AWS::RDS::DBSubnetGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBSubnetGroup', { DBSubnetGroupDescription: 'MyGroup', SubnetIds: [ { Ref: 'VPCPrivateSubnet1Subnet8BCA10E0' }, { Ref: 'VPCPrivateSubnet2SubnetCFCDAA7A' }, ], }); - - }); test('can specify subnet type', () => { @@ -93,14 +84,13 @@ describe('subnet group', () => { vpcSubnets: { subnetType: ec2.SubnetType.PUBLIC }, }); - expect(stack).toHaveResource('AWS::RDS::DBSubnetGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBSubnetGroup', { DBSubnetGroupDescription: 'MyGroup', SubnetIds: [ { Ref: 'VPCPublicSubnet1SubnetB4246D30' }, { Ref: 'VPCPublicSubnet2Subnet74179F39' }, ], }); - }); }); @@ -108,8 +98,5 @@ describe('subnet group', () => { const subnetGroup = rds.SubnetGroup.fromSubnetGroupName(stack, 'Group', 'my-subnet-group'); expect(subnetGroup.subnetGroupName).toEqual('my-subnet-group'); - - }); - }); diff --git a/packages/@aws-cdk/aws-s3/README.md b/packages/@aws-cdk/aws-s3/README.md index 4f2592b9c633a..47138a3d30ec6 100644 --- a/packages/@aws-cdk/aws-s3/README.md +++ b/packages/@aws-cdk/aws-s3/README.md @@ -249,6 +249,33 @@ const bucket = s3.Bucket.fromBucketAttributes(this, 'ImportedBucket', { bucket.addEventNotification(s3.EventType.OBJECT_CREATED, new s3n.SnsDestination(topic)); ``` +When you add an event notification to a bucket, a custom resource is created to +manage the notifications. By default, a new role is created for the Lambda +function that implements this feature. If you want to use your own role instead, +you should provide it in the `Bucket` constructor: + +```ts +declare const myRole: iam.IRole; +const bucket = new s3.Bucket(this, 'MyBucket', { + notificationsHandlerRole: myRole, +}); +``` + +Whatever role you provide, the CDK will try to modify it by adding the +permissions from `AWSLambdaBasicExecutionRole` (an AWS managed policy) as well +as the permissions `s3:PutBucketNotification` and `s3:GetBucketNotification`. +If you’re passing an imported role, and you don’t want this to happen, configure +it to be immutable: + +```ts +const importedRole = iam.Role.fromRoleArn(this, 'role', 'arn:aws:iam::123456789012:role/RoleName', { + mutable: false, +}); +``` + +> If you provide an imported immutable role, make sure that it has at least all +> the permissions mentioned above. Otherwise, the deployment will fail! + [S3 Bucket Notifications]: https://docs.aws.amazon.com/AmazonS3/latest/dev/NotificationHowTo.html diff --git a/packages/@aws-cdk/aws-s3/lib/bucket.ts b/packages/@aws-cdk/aws-s3/lib/bucket.ts index ea25e2c01467b..6cc76d3736488 100644 --- a/packages/@aws-cdk/aws-s3/lib/bucket.ts +++ b/packages/@aws-cdk/aws-s3/lib/bucket.ts @@ -356,9 +356,7 @@ export interface IBucket extends IResource { } /** - * A reference to a bucket. The easiest way to instantiate is to call - * `bucket.export()`. Then, the consumer can use `Bucket.import(this, ref)` and - * get a `Bucket`. + * A reference to a bucket outside this stack */ export interface BucketAttributes { /** @@ -429,6 +427,13 @@ export interface BucketAttributes { * @default - it's assumed the bucket is in the same region as the scope it's being imported into */ readonly region?: string; + + /** + * The role to be used by the notifications handler + * + * @default - a new role will be created. + */ + readonly notificationsHandlerRole?: iam.IRole; } /** @@ -486,14 +491,12 @@ export abstract class BucketBase extends Resource implements IBucket { */ protected abstract disallowPublicAccess?: boolean; - private readonly notifications: BucketNotifications; + private notifications?: BucketNotifications; + + protected notificationsHandlerRole?: iam.IRole; constructor(scope: Construct, id: string, props: ResourceProps = {}) { super(scope, id, props); - - // defines a BucketNotifications construct. Notice that an actual resource will only - // be added if there are notifications added, so we don't need to condition this. - this.notifications = new BucketNotifications(this, 'Notifications', { bucket: this }); } /** @@ -838,7 +841,17 @@ export abstract class BucketBase extends Resource implements IBucket { * https://docs.aws.amazon.com/AmazonS3/latest/dev/NotificationHowTo.html */ public addEventNotification(event: EventType, dest: IBucketNotificationDestination, ...filters: NotificationKeyFilter[]) { - this.notifications.addNotification(event, dest, ...filters); + this.withNotifications(notifications => notifications.addNotification(event, dest, ...filters)); + } + + private withNotifications(cb: (notifications: BucketNotifications) => void) { + if (!this.notifications) { + this.notifications = new BucketNotifications(this, 'Notifications', { + bucket: this, + handlerRole: this.notificationsHandlerRole, + }); + } + cb(this.notifications); } /** @@ -1461,6 +1474,13 @@ export interface BucketProps { */ readonly transferAcceleration?: boolean; + /** + * The role to be used by the notifications handler + * + * @default - a new role will be created. + */ + readonly notificationsHandlerRole?: iam.IRole; + /** * Inteligent Tiering Configurations * @@ -1544,6 +1564,7 @@ export class Bucket extends BucketBase { public policy?: BucketPolicy = undefined; protected autoCreatePolicy = false; protected disallowPublicAccess = false; + protected notificationsHandlerRole = attrs.notificationsHandlerRole; /** * Exports this bucket from the stack. @@ -1631,6 +1652,8 @@ export class Bucket extends BucketBase { physicalName: props.bucketName, }); + this.notificationsHandlerRole = props.notificationsHandlerRole; + const { bucketEncryption, encryptionKey } = this.parseEncryption(props); Bucket.validateBucketName(this.physicalName); 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 c093487a8f105..5a3955a96da9f 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 @@ -7,6 +7,10 @@ import * as cdk from '@aws-cdk/core'; // eslint-disable-next-line no-duplicate-imports, import/order import { Construct } from '@aws-cdk/core'; +export class NotificationsResourceHandlerProps { + role?: iam.IRole; +} + /** * A Lambda-based custom resource handler that provisions S3 bucket * notifications for a bucket. @@ -31,14 +35,14 @@ export class NotificationsResourceHandler extends Construct { * * @returns The ARN of the custom resource lambda function. */ - public static singleton(context: Construct) { + public static singleton(context: Construct, props: NotificationsResourceHandlerProps = {}) { const root = cdk.Stack.of(context); // well-known logical id to ensure stack singletonity const logicalId = 'BucketNotificationsHandler050a0587b7544547bf325f094a3db834'; let lambda = root.node.tryFindChild(logicalId) as NotificationsResourceHandler; if (!lambda) { - lambda = new NotificationsResourceHandler(root, logicalId); + lambda = new NotificationsResourceHandler(root, logicalId, props); } return lambda; @@ -53,19 +57,19 @@ export class NotificationsResourceHandler extends Construct { /** * The role of the handler's lambda function. */ - public readonly role: iam.Role; + public readonly role: iam.IRole; - constructor(scope: Construct, id: string) { + constructor(scope: Construct, id: string, props: NotificationsResourceHandlerProps = {}) { super(scope, id); - this.role = new iam.Role(this, 'Role', { + this.role = props.role ?? new iam.Role(this, 'Role', { assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'), - managedPolicies: [ - iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaBasicExecutionRole'), - ], }); - this.role.addToPolicy(new iam.PolicyStatement({ + this.role.addManagedPolicy( + iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaBasicExecutionRole'), + ); + this.role.addToPrincipalPolicy(new iam.PolicyStatement({ actions: ['s3:PutBucketNotification'], resources: ['*'], })); @@ -95,4 +99,8 @@ export class NotificationsResourceHandler extends Construct { this.functionArn = resource.getAtt('Arn').toString(); } + + public addToRolePolicy(statement: iam.PolicyStatement) { + this.role.addToPrincipalPolicy(statement); + } } diff --git a/packages/@aws-cdk/aws-s3/lib/notifications-resource/notifications-resource.ts b/packages/@aws-cdk/aws-s3/lib/notifications-resource/notifications-resource.ts index d5190f1a6a913..6bc50ec5b6064 100644 --- a/packages/@aws-cdk/aws-s3/lib/notifications-resource/notifications-resource.ts +++ b/packages/@aws-cdk/aws-s3/lib/notifications-resource/notifications-resource.ts @@ -13,6 +13,11 @@ interface NotificationsProps { * The bucket to manage notifications for. */ bucket: IBucket; + + /** + * The role to be used by the lambda handler + */ + handlerRole?: iam.IRole; } /** @@ -36,10 +41,12 @@ export class BucketNotifications extends Construct { private readonly topicNotifications = new Array(); private resource?: cdk.CfnResource; private readonly bucket: IBucket; + private readonly handlerRole?: iam.IRole; constructor(scope: Construct, id: string, props: NotificationsProps) { super(scope, id); this.bucket = props.bucket; + this.handlerRole = props.handlerRole; } /** @@ -102,12 +109,14 @@ export class BucketNotifications extends Construct { */ private createResourceOnce() { if (!this.resource) { - const handler = NotificationsResourceHandler.singleton(this); + const handler = NotificationsResourceHandler.singleton(this, { + role: this.handlerRole, + }); const managed = this.bucket instanceof Bucket; if (!managed) { - handler.role.addToPolicy(new iam.PolicyStatement({ + handler.addToRolePolicy(new iam.PolicyStatement({ actions: ['s3:GetBucketNotification'], resources: ['*'], })); diff --git a/packages/@aws-cdk/aws-s3/test/integ.bucket-auto-delete-objects.expected.json b/packages/@aws-cdk/aws-s3/test/integ.bucket-auto-delete-objects.expected.json index f5cf756e8a75d..da2c8cf503fe6 100644 --- a/packages/@aws-cdk/aws-s3/test/integ.bucket-auto-delete-objects.expected.json +++ b/packages/@aws-cdk/aws-s3/test/integ.bucket-auto-delete-objects.expected.json @@ -110,7 +110,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameters84e9b89449fe2573e51d08cc143e21116ed4608c6db56afffcb4ad85c8130709S3Bucket2C6C817C" + "Ref": "AssetParametersbe270bbdebe0851c887569796e3997437cca54ce86893ed94788500448e92824S3Bucket09A62232" }, "S3Key": { "Fn::Join": [ @@ -123,7 +123,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters84e9b89449fe2573e51d08cc143e21116ed4608c6db56afffcb4ad85c8130709S3VersionKeyFA215BD6" + "Ref": "AssetParametersbe270bbdebe0851c887569796e3997437cca54ce86893ed94788500448e92824S3VersionKeyA28118BE" } ] } @@ -136,7 +136,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters84e9b89449fe2573e51d08cc143e21116ed4608c6db56afffcb4ad85c8130709S3VersionKeyFA215BD6" + "Ref": "AssetParametersbe270bbdebe0851c887569796e3997437cca54ce86893ed94788500448e92824S3VersionKeyA28118BE" } ] } @@ -228,7 +228,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameters618bbe9863c0edd5c4ca2e24b5063762f020fafec018cd06f57e2bd9f2f48abfS3BucketE1985B35" + "Ref": "AssetParameters31552cb1c5c4cdb0d9502dc59c3cd63cb519dcb7e320e60965c75940297ae3b6S3BucketB51EC107" }, "S3Key": { "Fn::Join": [ @@ -241,7 +241,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters618bbe9863c0edd5c4ca2e24b5063762f020fafec018cd06f57e2bd9f2f48abfS3VersionKey610C6DE2" + "Ref": "AssetParameters31552cb1c5c4cdb0d9502dc59c3cd63cb519dcb7e320e60965c75940297ae3b6S3VersionKey2B267DB5" } ] } @@ -254,7 +254,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters618bbe9863c0edd5c4ca2e24b5063762f020fafec018cd06f57e2bd9f2f48abfS3VersionKey610C6DE2" + "Ref": "AssetParameters31552cb1c5c4cdb0d9502dc59c3cd63cb519dcb7e320e60965c75940297ae3b6S3VersionKey2B267DB5" } ] } @@ -297,29 +297,29 @@ } }, "Parameters": { - "AssetParameters84e9b89449fe2573e51d08cc143e21116ed4608c6db56afffcb4ad85c8130709S3Bucket2C6C817C": { + "AssetParametersbe270bbdebe0851c887569796e3997437cca54ce86893ed94788500448e92824S3Bucket09A62232": { "Type": "String", - "Description": "S3 bucket for asset \"84e9b89449fe2573e51d08cc143e21116ed4608c6db56afffcb4ad85c8130709\"" + "Description": "S3 bucket for asset \"be270bbdebe0851c887569796e3997437cca54ce86893ed94788500448e92824\"" }, - "AssetParameters84e9b89449fe2573e51d08cc143e21116ed4608c6db56afffcb4ad85c8130709S3VersionKeyFA215BD6": { + "AssetParametersbe270bbdebe0851c887569796e3997437cca54ce86893ed94788500448e92824S3VersionKeyA28118BE": { "Type": "String", - "Description": "S3 key for asset version \"84e9b89449fe2573e51d08cc143e21116ed4608c6db56afffcb4ad85c8130709\"" + "Description": "S3 key for asset version \"be270bbdebe0851c887569796e3997437cca54ce86893ed94788500448e92824\"" }, - "AssetParameters84e9b89449fe2573e51d08cc143e21116ed4608c6db56afffcb4ad85c8130709ArtifactHash17D48178": { + "AssetParametersbe270bbdebe0851c887569796e3997437cca54ce86893ed94788500448e92824ArtifactHash76F8FCF2": { "Type": "String", - "Description": "Artifact hash for asset \"84e9b89449fe2573e51d08cc143e21116ed4608c6db56afffcb4ad85c8130709\"" + "Description": "Artifact hash for asset \"be270bbdebe0851c887569796e3997437cca54ce86893ed94788500448e92824\"" }, - "AssetParameters618bbe9863c0edd5c4ca2e24b5063762f020fafec018cd06f57e2bd9f2f48abfS3BucketE1985B35": { + "AssetParameters31552cb1c5c4cdb0d9502dc59c3cd63cb519dcb7e320e60965c75940297ae3b6S3BucketB51EC107": { "Type": "String", - "Description": "S3 bucket for asset \"618bbe9863c0edd5c4ca2e24b5063762f020fafec018cd06f57e2bd9f2f48abf\"" + "Description": "S3 bucket for asset \"31552cb1c5c4cdb0d9502dc59c3cd63cb519dcb7e320e60965c75940297ae3b6\"" }, - "AssetParameters618bbe9863c0edd5c4ca2e24b5063762f020fafec018cd06f57e2bd9f2f48abfS3VersionKey610C6DE2": { + "AssetParameters31552cb1c5c4cdb0d9502dc59c3cd63cb519dcb7e320e60965c75940297ae3b6S3VersionKey2B267DB5": { "Type": "String", - "Description": "S3 key for asset version \"618bbe9863c0edd5c4ca2e24b5063762f020fafec018cd06f57e2bd9f2f48abf\"" + "Description": "S3 key for asset version \"31552cb1c5c4cdb0d9502dc59c3cd63cb519dcb7e320e60965c75940297ae3b6\"" }, - "AssetParameters618bbe9863c0edd5c4ca2e24b5063762f020fafec018cd06f57e2bd9f2f48abfArtifactHash467DFC33": { + "AssetParameters31552cb1c5c4cdb0d9502dc59c3cd63cb519dcb7e320e60965c75940297ae3b6ArtifactHashEE982197": { "Type": "String", - "Description": "Artifact hash for asset \"618bbe9863c0edd5c4ca2e24b5063762f020fafec018cd06f57e2bd9f2f48abf\"" + "Description": "Artifact hash for asset \"31552cb1c5c4cdb0d9502dc59c3cd63cb519dcb7e320e60965c75940297ae3b6\"" } } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-s3/test/integ.bucket-inventory.expected.json b/packages/@aws-cdk/aws-s3/test/integ.bucket-inventory.expected.json index f5610756ad71e..a142de99be8b0 100644 --- a/packages/@aws-cdk/aws-s3/test/integ.bucket-inventory.expected.json +++ b/packages/@aws-cdk/aws-s3/test/integ.bucket-inventory.expected.json @@ -155,4 +155,4 @@ } } } -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-s3/test/integ.bucket-sharing.lit.expected.json b/packages/@aws-cdk/aws-s3/test/integ.bucket-sharing.lit.expected.json index 4197e9179b4ff..083db4157734a 100644 --- a/packages/@aws-cdk/aws-s3/test/integ.bucket-sharing.lit.expected.json +++ b/packages/@aws-cdk/aws-s3/test/integ.bucket-sharing.lit.expected.json @@ -71,4 +71,4 @@ } } } -] +] \ No newline at end of file diff --git a/packages/@aws-cdk/aws-s3/test/integ.bucket.expected.json b/packages/@aws-cdk/aws-s3/test/integ.bucket.expected.json index 58cd3c5760961..b58901930d7c1 100644 --- a/packages/@aws-cdk/aws-s3/test/integ.bucket.expected.json +++ b/packages/@aws-cdk/aws-s3/test/integ.bucket.expected.json @@ -173,4 +173,4 @@ } } } -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-s3/test/integ.bucket.url.lit.expected.json b/packages/@aws-cdk/aws-s3/test/integ.bucket.url.lit.expected.json index 37a7d24a40029..6ab5dcbfef26e 100644 --- a/packages/@aws-cdk/aws-s3/test/integ.bucket.url.lit.expected.json +++ b/packages/@aws-cdk/aws-s3/test/integ.bucket.url.lit.expected.json @@ -44,7 +44,10 @@ [ "https://", { - "Fn::GetAtt": ["MyBucketF68F3FF0", "RegionalDomainName"] + "Fn::GetAtt": [ + "MyBucketF68F3FF0", + "RegionalDomainName" + ] }, "/myfolder/myfile.txt" ] @@ -58,7 +61,10 @@ [ "https://", { - "Fn::GetAtt": ["MyBucketF68F3FF0", "DomainName"] + "Fn::GetAtt": [ + "MyBucketF68F3FF0", + "DomainName" + ] }, "/myfolder/myfile.txt" ] @@ -80,4 +86,4 @@ } } } -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-s3/test/notification.test.ts b/packages/@aws-cdk/aws-s3/test/notification.test.ts index fbc8e1aa45a49..411852018d081 100644 --- a/packages/@aws-cdk/aws-s3/test/notification.test.ts +++ b/packages/@aws-cdk/aws-s3/test/notification.test.ts @@ -1,4 +1,5 @@ import { Template } from '@aws-cdk/assertions'; +import * as iam from '@aws-cdk/aws-iam'; import * as cdk from '@aws-cdk/core'; import * as s3 from '../lib'; @@ -30,6 +31,29 @@ describe('notification', () => { }); }); + test('can specify a custom role for the notifications handler of imported buckets', () => { + const stack = new cdk.Stack(); + + const importedRole = iam.Role.fromRoleArn(stack, 'role', 'arn:aws:iam::111111111111:role/DevsNotAllowedToTouch'); + + const bucket = s3.Bucket.fromBucketAttributes(stack, 'MyBucket', { + bucketName: 'foo-bar', + notificationsHandlerRole: importedRole, + }); + + bucket.addEventNotification(s3.EventType.OBJECT_CREATED, { + bind: () => ({ + arn: 'ARN', + type: s3.BucketNotificationDestinationType.TOPIC, + }), + }); + + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', { + Description: 'AWS CloudFormation handler for "Custom::S3BucketNotifications" resources (@aws-cdk/aws-s3)', + Role: 'arn:aws:iam::111111111111:role/DevsNotAllowedToTouch', + }); + }); + test('can specify prefix and suffix filter rules', () => { const stack = new cdk.Stack(); diff --git a/packages/@aws-cdk/aws-signer/README.md b/packages/@aws-cdk/aws-signer/README.md index 2de7797eb344f..6c95da4668e7a 100644 --- a/packages/@aws-cdk/aws-signer/README.md +++ b/packages/@aws-cdk/aws-signer/README.md @@ -27,7 +27,7 @@ to sign a zip file. For more information go to [Signing Platforms in AWS Signer] AWS Signer provides a pre-defined set of signing platforms. They are available in the CDK as - -```ts +```text Platform.AWS_IOT_DEVICE_MANAGEMENT_SHA256_ECDSA Platform.AWS_LAMBDA_SHA384_ECDSA Platform.AMAZON_FREE_RTOS_TI_CC3220SF @@ -43,11 +43,9 @@ For more information, visit [Signing Profiles in AWS Signer](https://docs.aws.am The following code sets up a signing profile for signing lambda code bundles - ```ts -import * as signer from '@aws-cdk/aws-signer'; - const signingProfile = new signer.SigningProfile(this, 'SigningProfile', { platform: signer.Platform.AWS_LAMBDA_SHA384_ECDSA, -} ); +}); ``` A signing profile is valid by default for 135 months. This can be modified by specifying the `signatureValidityPeriod` property. diff --git a/packages/@aws-cdk/aws-signer/package.json b/packages/@aws-cdk/aws-signer/package.json index 064c381b6a88a..7f44205727a9b 100644 --- a/packages/@aws-cdk/aws-signer/package.json +++ b/packages/@aws-cdk/aws-signer/package.json @@ -7,6 +7,13 @@ "jsii": { "outdir": "dist", "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + }, "targets": { "dotnet": { "namespace": "Amazon.CDK.AWS.Signer", diff --git a/packages/@aws-cdk/aws-signer/rosetta/default.ts-fixture b/packages/@aws-cdk/aws-signer/rosetta/default.ts-fixture new file mode 100644 index 0000000000000..443e1e88b241a --- /dev/null +++ b/packages/@aws-cdk/aws-signer/rosetta/default.ts-fixture @@ -0,0 +1,11 @@ +// Fixture with packages imported, but nothing else +import { Stack } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import * as signer from '@aws-cdk/aws-signer'; + +class Fixture extends Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + /// here + } +} diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/states/pass.ts b/packages/@aws-cdk/aws-stepfunctions/lib/states/pass.ts index fb5e52d2831b2..bff92d431cbab 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/states/pass.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/states/pass.ts @@ -116,7 +116,7 @@ export interface PassProps { /** * Define a Pass in the state machine * - * A Pass state can be used to transform the current exeuction's state. + * A Pass state can be used to transform the current execution's state. */ export class Pass extends State implements INextable { public readonly endStates: INextable[]; diff --git a/packages/@aws-cdk/aws-synthetics/lib/canary.ts b/packages/@aws-cdk/aws-synthetics/lib/canary.ts index 1a4ebaa7326ab..c078fd0718ce7 100644 --- a/packages/@aws-cdk/aws-synthetics/lib/canary.ts +++ b/packages/@aws-cdk/aws-synthetics/lib/canary.ts @@ -299,9 +299,13 @@ export class Canary extends cdk.Resource { resources: ['*'], actions: ['s3:ListAllMyBuckets'], }), + new iam.PolicyStatement({ + resources: [this.artifactsBucket.bucketArn], + actions: ['s3:GetBucketLocation'], + }), new iam.PolicyStatement({ resources: [this.artifactsBucket.arnForObjects(`${prefix ? prefix+'/*' : '*'}`)], - actions: ['s3:PutObject', 's3:GetBucketLocation'], + actions: ['s3:PutObject'], }), new iam.PolicyStatement({ resources: ['*'], diff --git a/packages/@aws-cdk/aws-synthetics/test/integ.asset.expected.json b/packages/@aws-cdk/aws-synthetics/test/integ.asset.expected.json index 7d614f08201b7..256de95e7be25 100644 --- a/packages/@aws-cdk/aws-synthetics/test/integ.asset.expected.json +++ b/packages/@aws-cdk/aws-synthetics/test/integ.asset.expected.json @@ -41,10 +41,17 @@ "Resource": "*" }, { - "Action": [ - "s3:PutObject", - "s3:GetBucketLocation" - ], + "Action": "s3:GetBucketLocation", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "MyCanaryArtifactsBucket89975E6D", + "Arn" + ] + } + }, + { + "Action": "s3:PutObject", "Effect": "Allow", "Resource": { "Fn::Join": [ @@ -197,10 +204,17 @@ "Resource": "*" }, { - "Action": [ - "s3:PutObject", - "s3:GetBucketLocation" - ], + "Action": "s3:GetBucketLocation", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "MyCanaryArtifactsBucket89975E6D", + "Arn" + ] + } + }, + { + "Action": "s3:PutObject", "Effect": "Allow", "Resource": { "Fn::Join": [ diff --git a/packages/@aws-cdk/aws-synthetics/test/integ.canary.expected.json b/packages/@aws-cdk/aws-synthetics/test/integ.canary.expected.json index 5411264651330..2425fad01de67 100644 --- a/packages/@aws-cdk/aws-synthetics/test/integ.canary.expected.json +++ b/packages/@aws-cdk/aws-synthetics/test/integ.canary.expected.json @@ -30,10 +30,17 @@ "Resource": "*" }, { - "Action": [ - "s3:PutObject", - "s3:GetBucketLocation" - ], + "Action": "s3:GetBucketLocation", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "mytestbucket8DC16178", + "Arn" + ] + } + }, + { + "Action": "s3:PutObject", "Effect": "Allow", "Resource": { "Fn::Join": [ @@ -210,10 +217,17 @@ "Resource": "*" }, { - "Action": [ - "s3:PutObject", - "s3:GetBucketLocation" - ], + "Action": "s3:GetBucketLocation", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "MyCanaryOneArtifactsBucketDF4A487D", + "Arn" + ] + } + }, + { + "Action": "s3:PutObject", "Effect": "Allow", "Resource": { "Fn::Join": [ @@ -424,10 +438,17 @@ "Resource": "*" }, { - "Action": [ - "s3:PutObject", - "s3:GetBucketLocation" - ], + "Action": "s3:GetBucketLocation", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "MyCanaryTwoArtifactsBucket79B179B6", + "Arn" + ] + } + }, + { + "Action": "s3:PutObject", "Effect": "Allow", "Resource": { "Fn::Join": [ @@ -638,10 +659,17 @@ "Resource": "*" }, { - "Action": [ - "s3:PutObject", - "s3:GetBucketLocation" - ], + "Action": "s3:GetBucketLocation", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "MyCanaryThreeArtifactsBucket894E857E", + "Arn" + ] + } + }, + { + "Action": "s3:PutObject", "Effect": "Allow", "Resource": { "Fn::Join": [ @@ -852,10 +880,17 @@ "Resource": "*" }, { - "Action": [ - "s3:PutObject", - "s3:GetBucketLocation" - ], + "Action": "s3:GetBucketLocation", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "MyPythonCanaryArtifactsBucket7AE88133", + "Arn" + ] + } + }, + { + "Action": "s3:PutObject", "Effect": "Allow", "Resource": { "Fn::Join": [ diff --git a/packages/@aws-cdk/cfnspec/spec-source/cfn-docs/cfn-docs.json b/packages/@aws-cdk/cfnspec/spec-source/cfn-docs/cfn-docs.json index 584d3f0218535..c29d65e7113ad 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/cfn-docs/cfn-docs.json +++ b/packages/@aws-cdk/cfnspec/spec-source/cfn-docs/cfn-docs.json @@ -1721,7 +1721,7 @@ "Name": "A name for the configuration profile.", "RetrievalRoleArn": "The ARN of an IAM role with permission to access the configuration at the specified `LocationUri` .\n\n> A retrieval role ARN is not required for configurations stored in the AWS AppConfig hosted configuration store. It is required for all other sources that store your configuration.", "Tags": "Metadata to assign to the configuration profile. Tags help organize and categorize your AWS AppConfig resources. Each tag consists of a key and an optional value, both of which you define.", - "Type": "The type of configurations that the configuration profile contains. A configuration can be a feature flag used for enabling or disabling new features or a freeform configuration used to introduce changes to your application.", + "Type": "The type of configurations contained in the profile. AWS AppConfig supports `feature flags` and `freeform` configurations. We recommend you create feature flag configurations to enable or disable new features and freeform configurations to distribute configurations to an application. When calling this API, enter one of the following values for `Type` :\n\n`AWS.AppConfig.FeatureFlags`\n\n`AWS.Freeform`", "Validators": "A list of methods for validating the configuration." } }, @@ -1821,7 +1821,7 @@ "attributes": { "Ref": "`Ref` returns the version number." }, - "description": "Create a new configuration in the AWS AppConfig hosted configuration store. Configurations must be 64 KB or smaller. The AWS AppConfig hosted configuration store provides the following benefits over other configuration store options.\n\n- You don't need to set up and configure other services such as Amazon Simple Storage Service ( Amazon S3 ) or Parameter Store.\n- You don't need to configure AWS Identity and Access Management ( IAM ) permissions to use the configuration store.\n- You can store configurations in any content type.\n- There is no cost to use the store.\n- You can create a configuration and add it to the store when you create a configuration profile.", + "description": "Create a new configuration in the AWS AppConfig hosted configuration store. Configurations must be 1 MB or smaller. The AWS AppConfig hosted configuration store provides the following benefits over other configuration store options.\n\n- You don't need to set up and configure other services such as Amazon Simple Storage Service ( Amazon S3 ) or Parameter Store.\n- You don't need to configure AWS Identity and Access Management ( IAM ) permissions to use the configuration store.\n- You can store configurations in any content type.\n- There is no cost to use the store.\n- You can create a configuration and add it to the store when you create a configuration profile.", "properties": { "ApplicationId": "The application ID.", "ConfigurationProfileId": "The configuration profile ID.", @@ -5384,11 +5384,19 @@ "description": "The `AWS::AutoScaling::WarmPool` resource creates a pool of pre-initialized EC2 instances that sits alongside the Auto Scaling group. Whenever your application needs to scale out, the Auto Scaling group can draw on the warm pool to meet its new desired capacity.\n\nWhen you create a warm pool, you can define a minimum size. When your Auto Scaling group scales out and the size of the warm pool shrinks, Amazon EC2 Auto Scaling launches new instances into the warm pool to maintain its minimum size.\n\nFor more information, see [Warm pools for Amazon EC2 Auto Scaling](https://docs.aws.amazon.com/autoscaling/ec2/userguide/ec2-auto-scaling-warm-pools.html) in the *Amazon EC2 Auto Scaling User Guide* .\n\n> CloudFormation supports the `UpdatePolicy` attribute for Auto Scaling groups. During an update, if `UpdatePolicy` is set to `AutoScalingRollingUpdate` , CloudFormation replaces `InService` instances only. Instances in the warm pool are not replaced. The difference in which instances are replaced can potentially result in different instance configurations after the stack update completes. If `UpdatePolicy` is set to `AutoScalingReplacingUpdate` , you do not encounter this issue because CloudFormation replaces both the Auto Scaling group and the warm pool.", "properties": { "AutoScalingGroupName": "The name of the Auto Scaling group.", + "InstanceReusePolicy": "", "MaxGroupPreparedCapacity": "Specifies the maximum number of instances that are allowed to be in the warm pool or in any state except `Terminated` for the Auto Scaling group. This is an optional property. Specify it only if you do not want the warm pool size to be determined by the difference between the group's maximum capacity and its desired capacity.\n\n> If a value for `MaxGroupPreparedCapacity` is not specified, Amazon EC2 Auto Scaling launches and maintains the difference between the group's maximum capacity and its desired capacity. If you specify a value for `MaxGroupPreparedCapacity` , Amazon EC2 Auto Scaling uses the difference between the `MaxGroupPreparedCapacity` and the desired capacity instead.\n> \n> The size of the warm pool is dynamic. Only when `MaxGroupPreparedCapacity` and `MinSize` are set to the same value does the warm pool have an absolute size. \n\nIf the desired capacity of the Auto Scaling group is higher than the `MaxGroupPreparedCapacity` , the capacity of the warm pool is 0, unless you specify a value for `MinSize` . To remove a value that you previously set, include the property but specify -1 for the value.", "MinSize": "Specifies the minimum number of instances to maintain in the warm pool. This helps you to ensure that there is always a certain number of warmed instances available to handle traffic spikes. Defaults to 0 if not specified.", "PoolState": "Sets the instance state to transition to after the lifecycle actions are complete. Default is `Stopped` ." } }, + "AWS::AutoScaling::WarmPool.InstanceReusePolicy": { + "attributes": {}, + "description": "", + "properties": { + "ReuseOnScaleIn": "" + } + }, "AWS::AutoScalingPlans::ScalingPlan": { "attributes": { "Ref": "When you pass the logical ID of an `AWS::AutoScalingPlans::ScalingPlan` resource to the intrinsic `Ref` function, the function returns the Amazon Resource Name (ARN) of the scaling plan. The format of the ARN is as follows:\n\n`arn:aws:autoscaling: *region* : *123456789012:* scalingPlan:scalingPlanName/ *plan-name* :scalingPlanVersion/ *plan-version*`\n\nFor more information about using the `Ref` function, see [Ref](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-ref.html) ." @@ -5869,7 +5877,7 @@ }, "description": "The `AWS::CE::CostCategory` resource creates groupings of cost that you can use across products in the AWS Billing and Cost Management console, such as Cost Explorer and AWS Budgets. For more information, see [Managing Your Costs with Cost Categories](https://docs.aws.amazon.com/awsaccountbilling/latest/aboutv2/manage-cost-categories.html) in the *AWS Billing and Cost Management User Guide* .", "properties": { - "DefaultValue": "", + "DefaultValue": "The default value for the cost category.", "Name": "The unique name of the Cost Category.", "RuleVersion": "The rule schema version in this particular Cost Category.", "Rules": "The array of CostCategoryRule in JSON array format.\n\n> Rules are processed in order. If there are multiple rules that match the line item, then the first rule to match is used to determine that Cost Category value.", @@ -10923,12 +10931,14 @@ "AuthenticationOptions": "Information about the authentication method to be used to authenticate clients.", "ClientCidrBlock": "The IPv4 address range, in CIDR notation, from which to assign client IP addresses. The address range cannot overlap with the local CIDR of the VPC in which the associated subnet is located, or the routes that you add manually. The address range cannot be changed after the Client VPN endpoint has been created. The CIDR block should be /22 or greater.", "ClientConnectOptions": "The options for managing connection authorization for new client connections.", + "ClientLoginBannerOptions": "Options for enabling a customizable text banner that will be displayed on AWS provided clients when a VPN session is established.", "ConnectionLogOptions": "Information about the client connection logging options.\n\nIf you enable client connection logging, data about client connections is sent to a Cloudwatch Logs log stream. The following information is logged:\n\n- Client connection requests\n- Client connection results (successful and unsuccessful)\n- Reasons for unsuccessful client connection requests\n- Client connection termination time", "Description": "A brief description of the Client VPN endpoint.", "DnsServers": "Information about the DNS servers to be used for DNS resolution. A Client VPN endpoint can have up to two DNS servers. If no DNS server is specified, the DNS address configured on the device is used for the DNS server.", "SecurityGroupIds": "The IDs of one or more security groups to apply to the target network. You must also specify the ID of the VPC that contains the security groups.", "SelfServicePortal": "Specify whether to enable the self-service portal for the Client VPN endpoint.\n\nDefault Value: `enabled`", "ServerCertificateArn": "The ARN of the server certificate. For more information, see the [AWS Certificate Manager User Guide](https://docs.aws.amazon.com/acm/latest/userguide/) .", + "SessionTimeoutHours": "The maximum VPN session duration time in hours.\n\nValid values: `8 | 10 | 12 | 24`\n\nDefault value: `24`", "SplitTunnel": "Indicates whether split-tunnel is enabled on the AWS Client VPN endpoint.\n\nBy default, split-tunnel on a VPN endpoint is disabled.\n\nFor information about split-tunnel VPN endpoints, see [Split-tunnel AWS Client VPN endpoint](https://docs.aws.amazon.com/vpn/latest/clientvpn-admin/split-tunnel-vpn.html) in the *AWS Client VPN Administrator Guide* .", "TagSpecifications": "The tags to apply to the Client VPN endpoint during creation.", "TransportProtocol": "The transport protocol to be used by the VPN session.\n\nDefault value: `udp`", @@ -10961,6 +10971,14 @@ "LambdaFunctionArn": "The Amazon Resource Name (ARN) of the AWS Lambda function used for connection authorization." } }, + "AWS::EC2::ClientVpnEndpoint.ClientLoginBannerOptions": { + "attributes": {}, + "description": "Options for enabling a customizable text banner that will be displayed on AWS provided clients when a VPN session is established.", + "properties": { + "BannerText": "Customizable text that will be displayed in a banner on AWS provided clients when a VPN session is established. UTF-8 encoded characters only. Maximum of 1400 characters.", + "Enabled": "Enable or disable a customizable text banner that will be displayed on AWS provided clients when a VPN session is established.\n\nValid values: `true | false`\n\nDefault value: `false`" + } + }, "AWS::EC2::ClientVpnEndpoint.ConnectionLogOptions": { "attributes": {}, "description": "Describes the client connection logging options for the Client VPN endpoint.", @@ -11346,6 +11364,7 @@ }, "AWS::EC2::Host": { "attributes": { + "HostId": "The ID of the host.", "Ref": "`Ref` returns the host ID, such as `h-0ab123c45d67ef89` ." }, "description": "Allocates a fully dedicated physical server for launching EC2 instances. Because the host is fully dedicated for your use, it can help you address compliance requirements and reduce costs by allowing you to use your existing server-bound software licenses. For more information, see [Dedicated Hosts](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/dedicated-hosts-overview.html) in the *Amazon EC2 User Guide for Linux Instances* .", @@ -13380,6 +13399,7 @@ }, "AWS::EC2::VPNGatewayRoutePropagation": { "attributes": { + "Id": "The ID of the VPN gateway.", "Ref": "`Ref` returns the ID of the VPN gateway." }, "description": "Enables a virtual private gateway (VGW) to propagate routes to the specified route table of a VPC.\n\nIf you reference a VPN gateway that is in the same template as your VPN gateway route propagation, you must explicitly declare a dependency on the VPN gateway attachment. The `AWS::EC2::VPNGatewayRoutePropagation` resource cannot use the VPN gateway until it has successfully attached to the VPC. Add a [DependsOn Attribute](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-attribute-dependson.html) in the `AWS::EC2::VPNGatewayRoutePropagation` resource to explicitly declare a dependency on the VPN gateway attachment.", @@ -17868,6 +17888,14 @@ "Path": "The path of the JDBC target." } }, + "AWS::Glue::Crawler.MongoDBTarget": { + "attributes": {}, + "description": "Specifies an Amazon DocumentDB or MongoDB data store to crawl.", + "properties": { + "ConnectionName": "The name of the connection to use to connect to the Amazon DocumentDB or MongoDB target.", + "Path": "The path of the Amazon DocumentDB or MongoDB target (database/collection)." + } + }, "AWS::Glue::Crawler.RecrawlPolicy": { "attributes": {}, "description": "When crawling an Amazon S3 data source after the first crawl is complete, specifies whether to crawl the entire dataset again or to crawl only folders that were added since the last crawler run. For more information, see [Incremental Crawls in AWS Glue](https://docs.aws.amazon.com/glue/latest/dg/incremental-crawls.html) in the developer guide.", @@ -17880,8 +17908,11 @@ "description": "Specifies a data store in Amazon Simple Storage Service (Amazon S3).", "properties": { "ConnectionName": "The name of a connection which allows a job or crawler to access data in Amazon S3 within an Amazon Virtual Private Cloud environment (Amazon VPC).", + "DlqEventQueueArn": "", + "EventQueueArn": "", "Exclusions": "A list of glob patterns used to exclude from the crawl. For more information, see [Catalog Tables with a Crawler](https://docs.aws.amazon.com/glue/latest/dg/add-crawler.html) .", - "Path": "The path to the Amazon S3 target." + "Path": "The path to the Amazon S3 target.", + "SampleSize": "Sets the number of files in each leaf folder to be crawled when crawling sample files in a dataset. If not set, all the files are crawled. A valid value is an integer between 1 and 249." } }, "AWS::Glue::Crawler.Schedule": { @@ -17906,6 +17937,7 @@ "CatalogTargets": "Specifies AWS Glue Data Catalog targets.", "DynamoDBTargets": "Specifies Amazon DynamoDB targets.", "JdbcTargets": "Specifies JDBC targets.", + "MongoDBTargets": "A list of Mongo DB targets.", "S3Targets": "Specifies Amazon Simple Storage Service (Amazon S3) targets." } }, @@ -20424,7 +20456,7 @@ "properties": { "DisplayName": "Field that represents a friendly name in the console for the custom metric; it doesn't have to be unique. Don't use this name as the metric identifier in the device metric report. Can be updated.", "MetricName": "The name of the custom metric. This will be used in the metric report submitted from the device/thing. It shouldn't begin with `aws:` . Cannot be updated once it's defined.", - "MetricType": "The type of the custom metric. Types include `string-list` , `ip-address-list` , `number-list` , and `number` .", + "MetricType": "The type of the custom metric. Types include `string-list` , `ip-address-list` , and `number-list` .", "Tags": "Metadata that can be used to manage the custom metric." } }, @@ -20529,13 +20561,13 @@ }, "AWS::IoT::Logging": { "attributes": { - "Ref": "" + "Ref": "`Ref` returns the log ID. For example:\n\n`{\"Ref\": \"Log-12345\"}`" }, - "description": "", + "description": "Configure logging.", "properties": { - "AccountId": "", - "DefaultLogLevel": "", - "RoleArn": "" + "AccountId": "The account ID.", + "DefaultLogLevel": "The default log level.Valid Values: `DEBUG | INFO | ERROR | WARN | DISABLED`", + "RoleArn": "The role ARN used for the log." } }, "AWS::IoT::MitigationAction": { @@ -20653,14 +20685,14 @@ }, "AWS::IoT::ResourceSpecificLogging": { "attributes": { - "Ref": "", - "TargetId": "" + "Ref": "`Ref` returns the resource-specific log ID. For example:\n\n`{\"Ref\": \"MyResourceLog-12345\" }`", + "TargetId": "The target Id." }, - "description": "", + "description": "Configure resource-specific logging.", "properties": { - "LogLevel": "", - "TargetName": "", - "TargetType": "" + "LogLevel": "The default log level.Valid Values: `DEBUG | INFO | ERROR | WARN | DISABLED`", + "TargetName": "The target name.", + "TargetType": "The target type. Valid Values: `DEFAULT | THING_GROUP`" } }, "AWS::IoT::ScheduledAudit": { @@ -20927,7 +20959,7 @@ "attributes": {}, "description": "Describes an action that writes data to an Amazon Kinesis Firehose stream.", "properties": { - "BatchMode": "", + "BatchMode": "Whether to deliver the Kinesis Data Firehose stream as a batch by using [`PutRecordBatch`](https://docs.aws.amazon.com/firehose/latest/APIReference/API_PutRecordBatch.html) . The default value is `false` .\n\nWhen `batchMode` is `true` and the rule's SQL statement evaluates to an Array, each Array element forms one record in the [`PutRecordBatch`](https://docs.aws.amazon.com/firehose/latest/APIReference/API_PutRecordBatch.html) request. The resulting array can't have more than 500 records.", "DeliveryStreamName": "The delivery stream name.", "RoleArn": "The IAM role that grants access to the Amazon Kinesis Firehose stream.", "Separator": "A character separator that will be used to separate records written to the Firehose stream. Valid values are: '\\n' (newline), '\\t' (tab), '\\r\\n' (Windows newline), ',' (comma)." @@ -20962,7 +20994,7 @@ "attributes": {}, "description": "Sends message data to an AWS IoT Analytics channel.", "properties": { - "BatchMode": "", + "BatchMode": "Whether to process the action as a batch. The default value is `false` .\n\nWhen `batchMode` is `true` and the rule SQL statement evaluates to an Array, each Array element is delivered as a separate message when passed by [`BatchPutMessage`](https://docs.aws.amazon.com/iotanalytics/latest/APIReference/API_BatchPutMessage.html) The resulting array can't have more than 100 messages.", "ChannelName": "The name of the IoT Analytics channel to which message data will be sent.", "RoleArn": "The ARN of the role which has a policy that grants IoT Analytics permission to send message data via IoT Analytics (iotanalytics:BatchPutMessage)." } @@ -20971,7 +21003,7 @@ "attributes": {}, "description": "Sends an input to an AWS IoT Events detector.", "properties": { - "BatchMode": "", + "BatchMode": "Whether to process the event actions as a batch. The default value is `false` .\n\nWhen `batchMode` is `true` , you can't specify a `messageId` .\n\nWhen `batchMode` is `true` and the rule SQL statement evaluates to an Array, each Array element is treated as a separate message when Events by calling [`BatchPutMessage`](https://docs.aws.amazon.com/iotevents/latest/apireference/API_iotevents-data_BatchPutMessage.html) . The resulting array can't have more than 10 messages.", "InputName": "The name of the AWS IoT Events input.", "MessageId": "The ID of the message. The default `messageId` is a new UUID value.\n\nWhen `batchMode` is `true` , you can't specify a `messageId` --a new UUID value will be assigned.\n\nAssign a value to this property to ensure that only one input (message) with a given `messageId` will be processed by an AWS IoT Events detector.", "RoleArn": "The ARN of the role that grants AWS IoT permission to send an input to an AWS IoT Events detector. (\"Action\":\"iotevents:BatchPutMessage\")." @@ -22190,7 +22222,8 @@ "attributes": {}, "description": "Contains a gateway's platform information.", "properties": { - "Greengrass": "A gateway that runs on AWS IoT Greengrass ." + "Greengrass": "A gateway that runs on AWS IoT Greengrass .", + "GreengrassV2": "" } }, "AWS::IoTSiteWise::Gateway.Greengrass": { @@ -22200,6 +22233,13 @@ "GroupArn": "The [ARN](https://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html) of the Greengrass group. For more information about how to find a group's ARN, see [ListGroups](https://docs.aws.amazon.com/greengrass/latest/apireference/listgroups-get.html) and [GetGroup](https://docs.aws.amazon.com/greengrass/latest/apireference/getgroup-get.html) in the *AWS IoT Greengrass API Reference* ." } }, + "AWS::IoTSiteWise::Gateway.GreengrassV2": { + "attributes": {}, + "description": "", + "properties": { + "CoreDeviceThingName": "" + } + }, "AWS::IoTSiteWise::Portal": { "attributes": { "PortalArn": "The [ARN](https://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html) of the portal, which has the following format.\n\n`arn:${Partition}:iotsitewise:${Region}:${Account}:portal/${PortalId}`", @@ -23051,7 +23091,7 @@ }, "AWS::Kendra::DataSource.WebCrawlerConfiguration": { "attributes": {}, - "description": "", + "description": "Provides the configuration information required for Amazon Kendra Web Crawler.", "properties": { "AuthenticationConfiguration": "Provides configuration information required to connect to websites using authentication.\n\nYou can connect to websites using basic authentication of user name and password.\n\nYou must provide the website host name and port number. For example, the host name of https://a.example.com/page1.html is \"a.example.com\" and the port is 443, the standard port for HTTPS. You use a secret in [AWS Secrets Manager](https://docs.aws.amazon.com/secretsmanager/latest/userguide/intro.html) to store your authentication credentials.", "CrawlDepth": "Specifies the number of levels in a website that you want to crawl.\n\nThe first level begins from the website seed or starting point URL. For example, if a website has 3 levels \u2013 index level (i.e. seed in this example), sections level, and subsections level \u2013 and you are only interested in crawling information up to the sections level (i.e. levels 0-1), you can set your depth to 1.\n\nThe default crawl depth is set to 2.", @@ -24393,7 +24433,7 @@ "attributes": {}, "description": "A structure for the resource.", "properties": { - "DataLocationResource": "Currently not supported by AWS CloudFormation .", + "DataLocationResource": "A structure for a data location object where permissions are granted or revoked.", "DatabaseResource": "A structure for the database object.", "TableResource": "A structure for the table object. A table is a metadata definition that represents your data. You can Grant and Revoke table privileges to a principal.", "TableWithColumnsResource": "Currently not supported by AWS CloudFormation ." @@ -24406,12 +24446,12 @@ "CatalogId": "", "DatabaseName": "The name of the database for the table. Unique to a Data Catalog. A database is a set of associated table definitions organized into a logical group. You can Grant and Revoke database privileges to a principal.", "Name": "The name of the table.", - "TableWildcard": "" + "TableWildcard": "An empty object representing all tables under a database. If this field is specified instead of the `Name` field, all tables under `DatabaseName` will have permission changes applied." } }, "AWS::LakeFormation::Permissions.TableWildcard": { "attributes": {}, - "description": "", + "description": "A wildcard object representing every table under a database.", "properties": {} }, "AWS::LakeFormation::Permissions.TableWithColumnsResource": { @@ -25433,6 +25473,101 @@ "GetObject": "Specifies the anonymous access to all objects in a bucket.\n\nThe following options can be specified:\n\n- `public` - Sets all objects in the bucket to public (read-only), making them readable by everyone on the internet.\n\nIf the `GetObject` value is set to `public` , then all objects in the bucket default to public regardless of the `allowPublicOverrides` value.\n- `private` - Sets all objects in the bucket to private, making them readable only by you and anyone that you grant access to.\n\nIf the `GetObject` value is set to `private` , and the `allowPublicOverrides` value is set to `true` , then all objects in the bucket default to private unless they are configured with a `public-read` ACL. Individual objects with a `public-read` ACL are readable by everyone on the internet." } }, + "AWS::Lightsail::Certificate": { + "attributes": { + "CertificateArn": "The Amazon Resource Name (ARN) of the certificate.", + "Ref": "", + "Status": "The validation status of the certificate." + }, + "description": "The `AWS::Lightsail::Certificate` resource specifies an SSL/TLS certificate that you can use with a content delivery network (CDN) distribution and a container service.\n\n> For information about certificates that you can use with a load balancer, see [AWS::Lightsail::LoadBalancerTlsCertificate](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lightsail-loadbalancertlscertificate.html) .", + "properties": { + "CertificateName": "The name of the certificate.", + "DomainName": "The domain name of the certificate.", + "SubjectAlternativeNames": "An array of strings that specify the alternate domains (such as `example.org` ) and subdomains (such as `blog.example.com` ) of the certificate.", + "Tags": "An array of key-value pairs to apply to this resource.\n\nFor more information, see [Tag](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-resource-tags.html) in the *AWS CloudFormation User Guide* .\n\n> The `Value` of `Tags` is optional for Lightsail resources." + } + }, + "AWS::Lightsail::Container": { + "attributes": { + "ContainerArn": "The Amazon Resource Name (ARN) of the container.", + "Ref": "", + "Url": "The publicly accessible URL of the container service.\n\nIf no public endpoint is specified in the current deployment, this URL returns a 404 response." + }, + "description": "The `AWS::Lightsail::Container` resource specifies a container service.\n\nA Lightsail container service is a compute resource to which you can deploy containers.", + "properties": { + "ContainerServiceDeployment": "An object that describes the current container deployment of the container service.", + "IsDisabled": "A Boolean value indicating whether the container service is disabled.", + "Power": "The power specification of the container service.\n\nThe power specifies the amount of RAM, the number of vCPUs, and the base price of the container service.", + "PublicDomainNames": "The public domain name of the container service, such as `example.com` and `www.example.com` .\n\nYou can specify up to four public domain names for a container service. The domain names that you specify are used when you create a deployment with a container that is configured as the public endpoint of your container service.\n\nIf you don't specify public domain names, then you can use the default domain of the container service.\n\n> You must create and validate an SSL/TLS certificate before you can use public domain names with your container service. Use the [AWS::Lightsail::Certificate](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lightsail-certificate.html) resource to create a certificate for the public domain names that you want to use with your container service.", + "Scale": "The scale specification of the container service.\n\nThe scale specifies the allocated compute nodes of the container service.", + "ServiceName": "The name of the container service.", + "Tags": "An array of key-value pairs to apply to this resource.\n\nFor more information, see [Tag](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-resource-tags.html) in the *AWS CloudFormation User Guide* .\n\n> The `Value` of `Tags` is optional for Lightsail resources." + } + }, + "AWS::Lightsail::Container.Container": { + "attributes": {}, + "description": "`Container` is a property of the [ContainerServiceDeployment](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lightsail-container-containerservicedeployment.html) property. It describes the settings of a container that will be launched, or that is launched, to an Amazon Lightsail container service.", + "properties": { + "Command": "The launch command for the container.", + "ContainerName": "The name of the container.", + "Environment": "The environment variables of the container.", + "Image": "The name of the image used for the container.\n\nContainer images that are sourced from (registered and stored on) your container service start with a colon ( `:` ). For example, if your container service name is `container-service-1` , the container image label is `mystaticsite` , and you want to use the third version ( `3` ) of the registered container image, then you should specify `:container-service-1.mystaticsite.3` . To use the latest version of a container image, specify `latest` instead of a version number (for example, `:container-service-1.mystaticsite.latest` ). Your container service will automatically use the highest numbered version of the registered container image.\n\nContainer images that are sourced from a public registry like Docker Hub don\u2019t start with a colon. For example, `nginx:latest` or `nginx` .", + "Ports": "An object that describes the open firewall ports and protocols of the container." + } + }, + "AWS::Lightsail::Container.ContainerServiceDeployment": { + "attributes": {}, + "description": "`ContainerServiceDeployment` is a property of the [AWS::Lightsail::Container](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lightsail-container.html) resource. It describes a container deployment configuration of a container service.\n\nA deployment specifies the settings, such as the ports and launch command, of containers that are deployed to your container service.", + "properties": { + "Containers": "An object that describes the configuration for the containers of the deployment.", + "PublicEndpoint": "An object that describes the endpoint of the deployment." + } + }, + "AWS::Lightsail::Container.EnvironmentVariable": { + "attributes": {}, + "description": "`EnvironmentVariable` is a property of the [Container](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lightsail-container-container.html) property. It describes the environment variables of a container on a container service which are key-value parameters that provide dynamic configuration of the application or script run by the container.", + "properties": { + "Value": "The environment variable value.", + "Variable": "The environment variable key." + } + }, + "AWS::Lightsail::Container.HealthCheckConfig": { + "attributes": {}, + "description": "`HealthCheckConfig` is a property of the [PublicEndpoint](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lightsail-container-publicendpoint.html) property. It describes the healthcheck configuration of a container deployment on a container service.", + "properties": { + "HealthyThreshold": "The number of consecutive health check successes required before moving the container to the `Healthy` state. The default value is `2` .", + "IntervalSeconds": "The approximate interval, in seconds, between health checks of an individual container. You can specify between `5` and `300` seconds. The default value is `5` .", + "Path": "The path on the container on which to perform the health check. The default value is `/` .", + "SuccessCodes": "The HTTP codes to use when checking for a successful response from a container. You can specify values between `200` and `499` . You can specify multiple values (for example, `200,202` ) or a range of values (for example, `200-299` ).", + "TimeoutSeconds": "The amount of time, in seconds, during which no response means a failed health check. You can specify between `2` and `60` seconds. The default value is `2` .", + "UnhealthyThreshold": "The number of consecutive health check failures required before moving the container to the `Unhealthy` state. The default value is `2` ." + } + }, + "AWS::Lightsail::Container.PortInfo": { + "attributes": {}, + "description": "`PortInfo` is a property of the [Container](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lightsail-container-container.html) property. It describes the ports to open and the protocols to use for a container on a Amazon Lightsail container service.", + "properties": { + "Port": "The open firewall ports of the container.", + "Protocol": "The protocol name for the open ports.\n\n*Allowed values* : `HTTP` | `HTTPS` | `TCP` | `UDP`" + } + }, + "AWS::Lightsail::Container.PublicDomainName": { + "attributes": {}, + "description": "`PublicDomainName` is a property of the [AWS::Lightsail::Container](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lightsail-container.html) resource. It describes the public domain names to use with a container service, such as `example.com` and `www.example.com` . It also describes the certificates to use with a container service.", + "properties": { + "CertificateName": "The name of the certificate for the public domains.", + "DomainNames": "The public domain names to use with the container service." + } + }, + "AWS::Lightsail::Container.PublicEndpoint": { + "attributes": {}, + "description": "`PublicEndpoint` is a property of the [ContainerServiceDeployment](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lightsail-container-containerservicedeployment.html) property. It describes describes the settings of the public endpoint of a container on a container service.", + "properties": { + "ContainerName": "The name of the container entry of the deployment that the endpoint configuration applies to.", + "ContainerPort": "The port of the specified container to which traffic is forwarded to.", + "HealthCheckConfig": "An object that describes the health check configuration of the container." + } + }, "AWS::Lightsail::Database": { "attributes": { "DatabaseArn": "The Amazon Resource Name (ARN) of the database (for example, `arn:aws:lightsail:us-east-2:123456789101:RelationalDatabase/244ad76f-8aad-4741-809f-12345EXAMPLE` ).", @@ -25509,6 +25644,89 @@ "SnapshotTimeOfDay": "The daily time when an automatic snapshot will be created.\n\nConstraints:\n\n- Must be in `HH:00` format, and in an hourly increment.\n- Specified in Coordinated Universal Time (UTC).\n- The snapshot will be automatically created between the time specified and up to 45 minutes after." } }, + "AWS::Lightsail::Distribution": { + "attributes": { + "AbleToUpdateBundle": "Indicates whether you can update the distribution\u2019s current bundle to another bundle.", + "DistributionArn": "The Amazon Resource Name (ARN) of the distribution.", + "Ref": "", + "Status": "The status of the distribution." + }, + "description": "The `AWS::Lightsail::Distribution` resource specifies a content delivery network (CDN) distribution. You can create distributions only in the `us-east-1` AWS Region.\n\nA distribution is a globally distributed network of caching servers that improve the performance of your website or web application hosted on a Lightsail instance, static content hosted on a Lightsail bucket, or through a Lightsail load balancer.", + "properties": { + "BundleId": "The ID of the bundle applied to the distribution.", + "CacheBehaviorSettings": "An object that describes the cache behavior settings of the distribution.", + "CacheBehaviors": "An array of objects that describe the per-path cache behavior of the distribution.", + "CertificateName": "The name of the SSL/TLS certificate attached to the distribution.", + "DefaultCacheBehavior": "An object that describes the default cache behavior of the distribution.", + "DistributionName": "The name of the distribution", + "IpAddressType": "The IP address type of the distribution.\n\nThe possible values are `ipv4` for IPv4 only, and `dualstack` for IPv4 and IPv6.", + "IsEnabled": "A Boolean value indicating whether the distribution is enabled.", + "Origin": "An object that describes the origin resource of the distribution, such as a Lightsail instance, bucket, or load balancer.\n\nThe distribution pulls, caches, and serves content from the origin.", + "Tags": "An array of key-value pairs to apply to this resource.\n\nFor more information, see [Tag](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-resource-tags.html) in the *AWS CloudFormation User Guide* .\n\n> The `Value` of `Tags` is optional for Lightsail resources." + } + }, + "AWS::Lightsail::Distribution.CacheBehavior": { + "attributes": {}, + "description": "`CacheBehavior` is a property of the [AWS::Lightsail::Distribution](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lightsail-distribution.html) resource. It describes the default cache behavior of an Amazon Lightsail content delivery network (CDN) distribution.", + "properties": { + "Behavior": "The cache behavior of the distribution.\n\nThe following cache behaviors can be specified:\n\n- *`cache`* - This option is best for static sites. When specified, your distribution caches and serves your entire website as static content. This behavior is ideal for websites with static content that doesn't change depending on who views it, or for websites that don't use cookies, headers, or query strings to personalize content.\n- *`dont-cache`* - This option is best for sites that serve a mix of static and dynamic content. When specified, your distribution caches and serves only the content that is specified in the distribution\u2019s `CacheBehaviorPerPath` parameter. This behavior is ideal for websites or web applications that use cookies, headers, and query strings to personalize content for individual users." + } + }, + "AWS::Lightsail::Distribution.CacheBehaviorPerPath": { + "attributes": {}, + "description": "`CacheBehaviorPerPath` is a property of the [AWS::Lightsail::Distribution](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lightsail-distribution.html) resource. It describes the per-path cache behavior of an Amazon Lightsail content delivery network (CDN) distribution.\n\nUse a per-path cache behavior to override the default cache behavior of a distribution, or to add an exception to it. For example, if you set the `CacheBehavior` to `cache` , you can use a per-path cache behavior to specify a directory, file, or file type that your distribution will cache. If you don\u2019t want your distribution to cache a specified directory, file, or file type, set the per-path cache behavior to `dont-cache` .", + "properties": { + "Behavior": "The cache behavior for the specified path.\n\nYou can specify one of the following per-path cache behaviors:\n\n- *`cache`* - This behavior caches the specified path.\n- *`dont-cache`* - This behavior doesn't cache the specified path.", + "Path": "The path to a directory or file to cache, or not cache. Use an asterisk symbol to specify wildcard directories ( `path/to/assets/*` ), and file types ( `*.html` , `*jpg` , `*js` ). Directories and file paths are case-sensitive.\n\nExamples:\n\n- Specify the following to cache all files in the document root of an Apache web server running on a instance.\n\n`var/www/html/`\n- Specify the following file to cache only the index page in the document root of an Apache web server.\n\n`var/www/html/index.html`\n- Specify the following to cache only the .html files in the document root of an Apache web server.\n\n`var/www/html/*.html`\n- Specify the following to cache only the .jpg, .png, and .gif files in the images sub-directory of the document root of an Apache web server.\n\n`var/www/html/images/*.jpg`\n\n`var/www/html/images/*.png`\n\n`var/www/html/images/*.gif`\n\nSpecify the following to cache all files in the images subdirectory of the document root of an Apache web server.\n\n`var/www/html/images/`" + } + }, + "AWS::Lightsail::Distribution.CacheSettings": { + "attributes": {}, + "description": "`CacheSettings` is a property of the [AWS::Lightsail::Distribution](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lightsail-distribution.html) resource. It describes the cache settings of an Amazon Lightsail content delivery network (CDN) distribution.\n\nThese settings apply only to your distribution\u2019s `CacheBehaviors` that have a `Behavior` of `cache` . This includes the `DefaultCacheBehavior` .", + "properties": { + "AllowedHTTPMethods": "The HTTP methods that are processed and forwarded to the distribution's origin.\n\nYou can specify the following options:\n\n- `GET,HEAD` - The distribution forwards the `GET` and `HEAD` methods.\n- `GET,HEAD,OPTIONS` - The distribution forwards the `GET` , `HEAD` , and `OPTIONS` methods.\n- `GET,HEAD,OPTIONS,PUT,PATCH,POST,DELETE` - The distribution forwards the `GET` , `HEAD` , `OPTIONS` , `PUT` , `PATCH` , `POST` , and `DELETE` methods.\n\nIf you specify `GET,HEAD,OPTIONS,PUT,PATCH,POST,DELETE` , you might need to restrict access to your distribution's origin so users can't perform operations that you don't want them to. For example, you might not want users to have permission to delete objects from your origin.", + "CachedHTTPMethods": "The HTTP method responses that are cached by your distribution.\n\nYou can specify the following options:\n\n- `GET,HEAD` - The distribution caches responses to the `GET` and `HEAD` methods.\n- `GET,HEAD,OPTIONS` - The distribution caches responses to the `GET` , `HEAD` , and `OPTIONS` methods.", + "DefaultTTL": "The default amount of time that objects stay in the distribution's cache before the distribution forwards another request to the origin to determine whether the content has been updated.\n\n> The value specified applies only when the origin does not add HTTP headers such as `Cache-Control max-age` , `Cache-Control s-maxage` , and `Expires` to objects.", + "ForwardedCookies": "An object that describes the cookies that are forwarded to the origin. Your content is cached based on the cookies that are forwarded.", + "ForwardedHeaders": "An object that describes the headers that are forwarded to the origin. Your content is cached based on the headers that are forwarded.", + "ForwardedQueryStrings": "An object that describes the query strings that are forwarded to the origin. Your content is cached based on the query strings that are forwarded.", + "MaximumTTL": "The maximum amount of time that objects stay in the distribution's cache before the distribution forwards another request to the origin to determine whether the object has been updated.\n\nThe value specified applies only when the origin adds HTTP headers such as `Cache-Control max-age` , `Cache-Control s-maxage` , and `Expires` to objects.", + "MinimumTTL": "The minimum amount of time that objects stay in the distribution's cache before the distribution forwards another request to the origin to determine whether the object has been updated.\n\nA value of `0` must be specified for `minimumTTL` if the distribution is configured to forward all headers to the origin." + } + }, + "AWS::Lightsail::Distribution.CookieObject": { + "attributes": {}, + "description": "`CookieObject` is a property of the [CacheSettings](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lightsail-distribution-cachesettings.html) property. It describes whether an Amazon Lightsail content delivery network (CDN) distribution forwards cookies to the origin and, if so, which ones.\n\nFor the cookies that you specify, your distribution caches separate versions of the specified content based on the cookie values in viewer requests.", + "properties": { + "CookiesAllowList": "The specific cookies to forward to your distribution's origin.", + "Option": "Specifies which cookies to forward to the distribution's origin for a cache behavior.\n\nUse one of the following configurations for your distribution:\n\n- *`all`* - Forwards all cookies to your origin.\n- *`none`* - Doesn\u2019t forward cookies to your origin.\n- *`allow-list`* - Forwards only the cookies that you specify using the `CookiesAllowList` parameter." + } + }, + "AWS::Lightsail::Distribution.HeaderObject": { + "attributes": {}, + "description": "`HeaderObject` is a property of the [CacheSettings](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lightsail-distribution-cachesettings.html) property. It describes the request headers used by your distribution, which caches your content based on the request headers.\n\nFor the headers that you specify, your distribution caches separate versions of the specified content based on the header values in viewer requests. For example, suppose that viewer requests for logo.jpg contain a custom product header that has a value of either acme or apex. Also, suppose that you configure your distribution to cache your content based on values in the product header. Your distribution forwards the product header to the origin and caches the response from the origin once for each header value.", + "properties": { + "HeadersAllowList": "The specific headers to forward to your distribution's origin.", + "Option": "The headers that you want your distribution to forward to your origin. Your distribution caches your content based on these headers.\n\nUse one of the following configurations for your distribution:\n\n- *`all`* - Forwards all headers to your origin..\n- *`none`* - Forwards only the default headers.\n- *`allow-list`* - Forwards only the headers that you specify using the `HeadersAllowList` parameter." + } + }, + "AWS::Lightsail::Distribution.InputOrigin": { + "attributes": {}, + "description": "`InputOrigin` is a property of the [AWS::Lightsail::Distribution](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lightsail-distribution.html) resource. It describes the origin resource of an Amazon Lightsail content delivery network (CDN) distribution.\n\nAn origin can be a instance, bucket, or load balancer. A distribution pulls content from an origin, caches it, and serves it to viewers through a worldwide network of edge servers.", + "properties": { + "Name": "The name of the origin resource.", + "ProtocolPolicy": "The protocol that your Amazon Lightsail distribution uses when establishing a connection with your origin to pull content.", + "RegionName": "The AWS Region name of the origin resource." + } + }, + "AWS::Lightsail::Distribution.QueryStringObject": { + "attributes": {}, + "description": "`QueryStringObject` is a property of the [CacheSettings](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lightsail-distribution-cachesettings.html) property. It describes the query string parameters that an Amazon Lightsail content delivery network (CDN) distribution to bases caching on.\n\nFor the query strings that you specify, your distribution caches separate versions of the specified content based on the query string values in viewer requests.", + "properties": { + "Option": "Indicates whether the distribution forwards and caches based on query strings.", + "QueryStringsAllowList": "The specific query strings that the distribution forwards to the origin.\n\nYour distribution caches content based on the specified query strings.\n\nIf the `option` parameter is true, then your distribution forwards all query strings, regardless of what you specify using the `QueryStringsAllowList` parameter." + } + }, "AWS::Lightsail::Instance": { "attributes": { "Hardware.CpuCount": "The number of vCPUs the instance has.", @@ -32158,7 +32376,7 @@ "AllowMajorVersionUpgrade": "A value that indicates whether major version upgrades are allowed. Changing this parameter doesn't result in an outage and the change is asynchronously applied as soon as possible.\n\nConstraints: Major version upgrades must be allowed when specifying a value for the `EngineVersion` parameter that is a different major version than the DB instance's current version.", "AssociatedRoles": "The AWS Identity and Access Management (IAM) roles associated with the DB instance.", "AutoMinorVersionUpgrade": "A value that indicates whether minor engine upgrades are applied automatically to the DB instance during the maintenance window. By default, minor engine upgrades are applied automatically.", - "AvailabilityZone": "The Availability Zone (AZ) where the database will be created. For information on AWS Regions and Availability Zones, see [Regions and Availability Zones](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Concepts.RegionsAndAvailabilityZones.html) .\n\n*Amazon Aurora*\n\nNot applicable. Availability Zones are managed by the DB cluster.\n\nDefault: A random, system-chosen Availability Zone in the endpoint's AWS Region.\n\nExample: `us-east-1d`\n\nConstraint: The `AvailabilityZone` parameter can't be specified if the DB instance is a Multi-AZ deployment. The specified Availability Zone must be in the same AWS Region as the current endpoint.\n\n> If you're creating a DB instance in an RDS on VMware environment, specify the identifier of the custom Availability Zone to create the DB instance in.\n> \n> For more information about RDS on VMware, see the [RDS on VMware User Guide.](https://docs.aws.amazon.com/AmazonRDS/latest/RDSonVMwareUserGuide/rds-on-vmware.html)", + "AvailabilityZone": "The Availability Zone that the database instance will be created in.\n\nDefault: A random, system-chosen Availability Zone in the endpoint's region.\n\nExample: `us-east-1d`\n\nConstraint: The AvailabilityZone parameter cannot be specified if the MultiAZ parameter is set to `true` . The specified Availability Zone must be in the same region as the current endpoint.", "BackupRetentionPeriod": "The number of days for which automated backups are retained. Setting this parameter to a positive number enables backups. Setting this parameter to 0 disables automated backups.\n\n*Amazon Aurora*\n\nNot applicable. The retention period for automated backups is managed by the DB cluster.\n\nDefault: 1\n\nConstraints:\n\n- Must be a value from 0 to 35\n- Can't be set to 0 if the DB instance is a source to read replicas", "CACertificateIdentifier": "The identifier of the CA certificate for this DB instance.\n\n> Specifying or updating this property triggers a reboot. \n\nFor more information about CA certificate identifiers for RDS DB engines, see [Rotating Your SSL/TLS Certificate](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.SSL-certificate-rotation.html) in the *Amazon RDS User Guide* .\n\nFor more information about CA certificate identifiers for Aurora DB engines, see [Rotating Your SSL/TLS Certificate](https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/UsingWithRDS.SSL-certificate-rotation.html) in the *Amazon Aurora User Guide* .", "CharacterSetName": "For supported engines, indicates that the DB instance should be associated with the specified character set.\n\n*Amazon Aurora*\n\nNot applicable. The character set is managed by the DB cluster. For more information, see [AWS::RDS::DBCluster](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-rds-dbcluster.html) .", @@ -32230,7 +32448,7 @@ }, "description": "The `AWS::RDS::DBParameterGroup` resource creates a custom parameter group for an RDS database family.\n\nThis type can be declared in a template and referenced in the `DBParameterGroupName` property of an `[AWS::RDS::DBInstance](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-rds-database-instance.html)` resource.\n\nFor information about configuring parameters for Amazon RDS DB instances, see [Working with DB parameter groups](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_WorkingWithParamGroups.html) in the *Amazon RDS User Guide* .\n\nFor information about configuring parameters for Amazon Aurora DB instances, see [Working with DB parameter groups and DB cluster parameter groups](https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/USER_WorkingWithParamGroups.html) in the *Amazon Aurora User Guide* .\n\n> Applying a parameter group to a DB instance may require the DB instance to reboot, resulting in a database outage for the duration of the reboot.", "properties": { - "Description": "Provides the customer-specified description for this DB parameter group.", + "Description": "Provides the customer-specified description for this DB Parameter Group.", "Family": "The DB parameter group family name. A DB parameter group can be associated with one and only one DB parameter group family, and can be applied only to a DB instance running a DB engine and engine version compatible with that DB parameter group family.\n\n> The DB parameter group family can't be changed when updating a DB parameter group. \n\nTo list all of the available parameter group families, use the following command:\n\n`aws rds describe-db-engine-versions --query \"DBEngineVersions[].DBParameterGroupFamily\"`\n\nThe output contains duplicates.\n\nFor more information, see `[CreateDBParameterGroup](https://docs.aws.amazon.com//AmazonRDS/latest/APIReference/API_CreateDBParameterGroup.html)` .", "Parameters": "An array of parameter names and values for the parameter update. At least one parameter name and value must be supplied. Subsequent arguments are optional.\n\nFor more information about DB parameters and DB parameter groups for Amazon RDS DB engines, see [Working with DB Parameter Groups](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_WorkingWithParamGroups.html) in the *Amazon RDS User Guide* .\n\nFor more information about DB cluster and DB instance parameters and parameter groups for Amazon Aurora DB engines, see [Working with DB Parameter Groups and DB Cluster Parameter Groups](https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/USER_WorkingWithParamGroups.html) in the *Amazon Aurora User Guide* .\n\n> AWS CloudFormation doesn't support specifying an apply method for each individual parameter. The default apply method for each parameter is used.", "Tags": "Tags to assign to the DB parameter group." @@ -32334,7 +32552,7 @@ "properties": { "DBSecurityGroupIngress": "Ingress rules to be applied to the DB security group.", "EC2VpcId": "The identifier of an Amazon VPC. This property indicates the VPC that this DB security group belongs to.\n\n> The `EC2VpcId` property is for backward compatibility with older regions, and is no longer recommended for providing security information to an RDS DB instance.", - "GroupDescription": "Provides the description of the DB security group.", + "GroupDescription": "Provides the description of the DB Security Group.", "Tags": "Tags to assign to the DB security group." } }, @@ -32343,9 +32561,9 @@ "description": "The `Ingress` property type specifies an individual ingress rule within an `AWS::RDS::DBSecurityGroup` resource.", "properties": { "CIDRIP": "The IP range to authorize.", - "EC2SecurityGroupId": "Id of the EC2 security group to authorize. For VPC DB security groups, `EC2SecurityGroupId` must be provided. Otherwise, `EC2SecurityGroupOwnerId` and either `EC2SecurityGroupName` or `EC2SecurityGroupId` must be provided.", - "EC2SecurityGroupName": "Name of the EC2 security group to authorize. For VPC DB security groups, `EC2SecurityGroupId` must be provided. Otherwise, `EC2SecurityGroupOwnerId` and either `EC2SecurityGroupName` or `EC2SecurityGroupId` must be provided.", - "EC2SecurityGroupOwnerId": "AWS account number of the owner of the EC2 security group specified in the `EC2SecurityGroupName` parameter. The AWS access key ID isn't an acceptable value. For VPC DB security groups, `EC2SecurityGroupId` must be provided. Otherwise, `EC2SecurityGroupOwnerId` and either `EC2SecurityGroupName` or `EC2SecurityGroupId` must be provided." + "EC2SecurityGroupId": "Id of the EC2 Security Group to authorize. For VPC DB Security Groups, `EC2SecurityGroupId` must be provided. Otherwise, EC2SecurityGroupOwnerId and either `EC2SecurityGroupName` or `EC2SecurityGroupId` must be provided.", + "EC2SecurityGroupName": "Name of the EC2 Security Group to authorize. For VPC DB Security Groups, `EC2SecurityGroupId` must be provided. Otherwise, EC2SecurityGroupOwnerId and either `EC2SecurityGroupName` or `EC2SecurityGroupId` must be provided.", + "EC2SecurityGroupOwnerId": "AWS Account Number of the owner of the EC2 Security Group specified in the EC2SecurityGroupName parameter. The AWS Access Key ID is not an acceptable value. For VPC DB Security Groups, `EC2SecurityGroupId` must be provided. Otherwise, EC2SecurityGroupOwnerId and either `EC2SecurityGroupName` or `EC2SecurityGroupId` must be provided." } }, "AWS::RDS::DBSecurityGroupIngress": { @@ -32355,10 +32573,10 @@ "description": "The `AWS::RDS::DBSecurityGroupIngress` resource enables ingress to a DB security group using one of two forms of authorization. First, you can add EC2 or VPC security groups to the DB security group if the application using the database is running on EC2 or VPC instances. Second, IP ranges are available if the application accessing your database is running on the Internet.\n\nThis type supports updates. For more information about updating stacks, see [AWS CloudFormation Stacks Updates](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks.html) .\n\nFor details about the settings for DB security group ingress, see [AuthorizeDBSecurityGroupIngress](https://docs.aws.amazon.com/AmazonRDS/latest/APIReference/API_AuthorizeDBSecurityGroupIngress.html) .", "properties": { "CIDRIP": "The IP range to authorize.", - "DBSecurityGroupName": "The name of the DB security group to add authorization to.", - "EC2SecurityGroupId": "Id of the EC2 security group to authorize. For VPC DB security groups, `EC2SecurityGroupId` must be provided. Otherwise, `EC2SecurityGroupOwnerId` and either `EC2SecurityGroupName` or `EC2SecurityGroupId` must be provided.", - "EC2SecurityGroupName": "Name of the EC2 security group to authorize. For VPC DB security groups, `EC2SecurityGroupId` must be provided. Otherwise, `EC2SecurityGroupOwnerId` and either `EC2SecurityGroupName` or `EC2SecurityGroupId` must be provided.", - "EC2SecurityGroupOwnerId": "AWS account number of the owner of the EC2 security group specified in the `EC2SecurityGroupName` parameter. The AWS access key ID isn't an acceptable value. For VPC DB security groups, `EC2SecurityGroupId` must be provided. Otherwise, `EC2SecurityGroupOwnerId` and either `EC2SecurityGroupName` or `EC2SecurityGroupId` must be provided." + "DBSecurityGroupName": "The name of the DB Security Group to add authorization to.", + "EC2SecurityGroupId": "Id of the EC2 Security Group to authorize. For VPC DB Security Groups, `EC2SecurityGroupId` must be provided. Otherwise, EC2SecurityGroupOwnerId and either `EC2SecurityGroupName` or `EC2SecurityGroupId` must be provided.", + "EC2SecurityGroupName": "Name of the EC2 Security Group to authorize. For VPC DB Security Groups, `EC2SecurityGroupId` must be provided. Otherwise, EC2SecurityGroupOwnerId and either `EC2SecurityGroupName` or `EC2SecurityGroupId` must be provided.", + "EC2SecurityGroupOwnerId": "AWS Account Number of the owner of the EC2 Security Group specified in the EC2SecurityGroupName parameter. The AWS Access Key ID is not an acceptable value. For VPC DB Security Groups, `EC2SecurityGroupId` must be provided. Otherwise, EC2SecurityGroupOwnerId and either `EC2SecurityGroupName` or `EC2SecurityGroupId` must be provided." } }, "AWS::RDS::DBSubnetGroup": { @@ -32367,9 +32585,9 @@ }, "description": "The `AWS::RDS::DBSubnetGroup` resource creates a database subnet group. Subnet groups must contain at least two subnets in two different Availability Zones in the same region.\n\nFor more information, see [Working with DB subnet groups](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_VPC.WorkingWithRDSInstanceinaVPC.html#USER_VPC.Subnets) in the *Amazon RDS User Guide* .", "properties": { - "DBSubnetGroupDescription": "The description for the DB subnet group.", + "DBSubnetGroupDescription": "The description for the DB Subnet Group.", "DBSubnetGroupName": "The name for the DB subnet group. This value is stored as a lowercase string.\n\nConstraints: Must contain no more than 255 lowercase alphanumeric characters or hyphens. Must not be \"Default\".\n\nExample: `mysubnetgroup`", - "SubnetIds": "The EC2 Subnet IDs for the DB subnet group.", + "SubnetIds": "The EC2 Subnet IDs for the DB Subnet Group.", "Tags": "Tags to assign to the DB subnet group." } }, @@ -32379,8 +32597,8 @@ }, "description": "The `AWS::RDS::EventSubscription` resource allows you to receive notifications for Amazon Relational Database Service events through the Amazon Simple Notification Service (Amazon SNS). For more information, see [Using Amazon RDS Event Notification](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_Events.html) in the *Amazon RDS User Guide* .", "properties": { - "Enabled": "A value that indicates whether to activate the subscription. If the event notification subscription isn't activated, the subscription is created but not active.", - "EventCategories": "A list of event categories for a particular source type ( `SourceType` ) that you want to subscribe to. You can see a list of the categories for a given source type in the \"Amazon RDS event categories and event messages\" section of the [*Amazon RDS User Guide*](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_Events.Messages.html) or the [*Amazon Aurora User Guide*](https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/USER_Events.Messages.html) . You can also see this list by using the `DescribeEventCategories` operation.", + "Enabled": "A Boolean value; set to *true* to activate the subscription, set to *false* to create the subscription but not active it.", + "EventCategories": "A list of event categories for a SourceType that you want to subscribe to. You can see a list of the categories for a given SourceType in the [Events](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_Events.html) topic in the Amazon RDS User Guide or by using the *DescribeEventCategories* action.", "SnsTopicArn": "The Amazon Resource Name (ARN) of the SNS topic created for event notification. The ARN is created by Amazon SNS when you create a topic and subscribe to it.", "SourceIds": "The list of identifiers of the event sources for which events are returned. If not specified, then all sources are included in the response. An identifier must begin with a letter and must contain only ASCII letters, digits, and hyphens. It can't end with a hyphen or contain two consecutive hyphens.\n\nConstraints:\n\n- If a `SourceIds` value is supplied, `SourceType` must also be provided.\n- If the source type is a DB instance, a `DBInstanceIdentifier` value must be supplied.\n- If the source type is a DB cluster, a `DBClusterIdentifier` value must be supplied.\n- If the source type is a DB parameter group, a `DBParameterGroupName` value must be supplied.\n- If the source type is a DB security group, a `DBSecurityGroupName` value must be supplied.\n- If the source type is a DB snapshot, a `DBSnapshotIdentifier` value must be supplied.\n- If the source type is a DB cluster snapshot, a `DBClusterSnapshotIdentifier` value must be supplied.", "SourceType": "The type of source that is generating the events. For example, if you want to be notified of events generated by a DB instance, set this parameter to `db-instance` . If this value isn't specified, all events are returned.\n\nValid values: `db-instance` | `db-cluster` | `db-parameter-group` | `db-security-group` | `db-snapshot` | `db-cluster-snapshot`" @@ -32439,7 +32657,7 @@ }, "description": "Creates a CloudWatch RUM app monitor, which you can use to collect telemetry data from your application and send it to CloudWatch RUM. The data includes performance and reliability information such as page load time, client-side errors, and user behavior.\n\nAfter you create an app monitor, sign in to the CloudWatch RUM console to get the JavaScript code snippet to add to your web application. For more information, see [How do I find a code snippet that I've already generated?](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch-RUM-find-code-snippet.html)", "properties": { - "AppMonitorConfiguration": "A structure that contains much of the configuration data for the app monitor. If you are using Amazon Cognito for authorization, you must include this structure in your request, and it must include the ID of the Amazon Cognito identity pool to use for authorization. If you don't include `AppMonitorConfiguration` , you must set up your own authorization method. For more information, see [Authorize your application to send data to AWS](https://docs.aws.amazon.com/monitoring/CloudWatch-RUM-get-started-authorization.html) .\n\nIf you omit this argument, the sample rate used for CloudWatch RUM is set to 10% of the user sessions.", + "AppMonitorConfiguration": "A structure that contains much of the configuration data for the app monitor. If you are using Amazon Cognito for authorization, you must include this structure in your request, and it must include the ID of the Amazon Cognito identity pool to use for authorization. If you don't include `AppMonitorConfiguration` , you must set up your own authorization method. For more information, see [Authorize your application to send data to AWS](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch-RUM-get-started-authorization.html) .\n\nIf you omit this argument, the sample rate used for CloudWatch RUM is set to 10% of the user sessions.", "CwLogEnabled": "Data collected by CloudWatch RUM is kept by RUM for 30 days and then deleted. This parameter specifies whether CloudWatch RUM sends a copy of this telemetry data to Amazon CloudWatch Logs in your account. This enables you to keep the telemetry data for more than 30 days, but it does incur Amazon CloudWatch Logs charges.\n\nIf you omit this parameter, the default is `false` .", "Domain": "The top-level internet domain name for which your application has administrative authority. This parameter is required.", "Name": "A name for the app monitor. This parameter is required.", @@ -36639,6 +36857,7 @@ }, "description": "The `AWS::SageMaker::Pipeline` resource creates shell scripts that run when you create and/or start a SageMaker Pipeline. For information about SageMaker Pipelines, see [SageMaker Pipelines](https://docs.aws.amazon.com/sagemaker/latest/dg/pipelines.html) in the *Amazon SageMaker Developer Guide* .", "properties": { + "ParallelismConfiguration": "The parallelism configuration applied to the pipeline.", "PipelineDefinition": "The definition of the pipeline. This can be either a JSON string or an Amazon S3 location.", "PipelineDescription": "The description of the pipeline.", "PipelineDisplayName": "The display name of the pipeline.", diff --git a/packages/@aws-cdk/pipelines/lib/blueprint/step.ts b/packages/@aws-cdk/pipelines/lib/blueprint/step.ts index 2bd785a7c7640..65f4636b2ecbc 100644 --- a/packages/@aws-cdk/pipelines/lib/blueprint/step.ts +++ b/packages/@aws-cdk/pipelines/lib/blueprint/step.ts @@ -13,6 +13,10 @@ import { FileSet, IFileSetProducer } from './file-set'; export abstract class Step implements IFileSetProducer { /** * Define a sequence of steps to be executed in order. + * + * If you need more fine-grained step ordering, use the `addStepDependency()` + * API. For example, if you want `secondStep` to occur after `firstStep`, call + * `secondStep.addStepDependency(firstStep)`. */ public static sequence(steps: Step[]): Step[] { for (let i = 1; i < steps.length; i++) { diff --git a/packages/aws-cdk/lib/api/hotswap/lambda-functions.ts b/packages/aws-cdk/lib/api/hotswap/lambda-functions.ts index d2966e756b69d..ac87e25c29655 100644 --- a/packages/aws-cdk/lib/api/hotswap/lambda-functions.ts +++ b/packages/aws-cdk/lib/api/hotswap/lambda-functions.ts @@ -1,5 +1,6 @@ import { Writable } from 'stream'; import * as archiver from 'archiver'; +import * as AWS from 'aws-sdk'; import { flatMap } from '../../util'; import { ISDK } from '../aws-auth'; import { CfnEvaluationException, EvaluateCloudFormationTemplate } from '../evaluate-cloudformation-template'; @@ -232,7 +233,7 @@ class LambdaFunctionHotswapOperation implements HotswapOperation { const operations: Promise[] = []; if (resource.code !== undefined) { - const updateFunctionCodePromise = lambda.updateFunctionCode({ + const updateFunctionCodeResponse = await lambda.updateFunctionCode({ FunctionName: this.lambdaFunctionResource.physicalName, S3Bucket: resource.code.s3Bucket, S3Key: resource.code.s3Key, @@ -240,17 +241,10 @@ class LambdaFunctionHotswapOperation implements HotswapOperation { ZipFile: resource.code.functionCodeZip, }).promise(); + await this.waitForLambdasCodeUpdateToFinish(updateFunctionCodeResponse, lambda); + // only if the code changed is there any point in publishing a new Version if (this.lambdaFunctionResource.publishVersion) { - // we need to wait for the code update to be done before publishing a new Version - await updateFunctionCodePromise; - // if we don't wait for the Function to finish updating, - // we can get a "The operation cannot be performed at this time. An update is in progress for resource:" - // error when publishing a new Version - await lambda.waitFor('functionUpdated', { - FunctionName: this.lambdaFunctionResource.physicalName, - }).promise(); - const publishVersionPromise = lambda.publishVersion({ FunctionName: this.lambdaFunctionResource.physicalName, }).promise(); @@ -269,8 +263,6 @@ class LambdaFunctionHotswapOperation implements HotswapOperation { } else { operations.push(publishVersionPromise); } - } else { - operations.push(updateFunctionCodePromise); } } @@ -304,6 +296,53 @@ class LambdaFunctionHotswapOperation implements HotswapOperation { // run all of our updates in parallel return Promise.all(operations); } + + /** + * After a Lambda Function is updated, it cannot be updated again until the + * `State=Active` and the `LastUpdateStatus=Successful`. + * + * Depending on the configuration of the Lambda Function this could happen relatively quickly + * or very slowly. For example, Zip based functions _not_ in a VPC can take ~1 second whereas VPC + * or Container functions can take ~25 seconds (and 'idle' VPC functions can take minutes). + */ + private async waitForLambdasCodeUpdateToFinish(currentFunctionConfiguration: AWS.Lambda.FunctionConfiguration, lambda: AWS.Lambda): Promise { + const functionIsInVpcOrUsesDockerForCode = currentFunctionConfiguration.VpcConfig?.VpcId || + currentFunctionConfiguration.PackageType === 'Image'; + + // if the function is deployed in a VPC or if it is a container image function + // then the update will take much longer and we can wait longer between checks + // otherwise, the update will be quick, so a 1-second delay is fine + const delaySeconds = functionIsInVpcOrUsesDockerForCode ? 5 : 1; + + // configure a custom waiter to wait for the function update to complete + (lambda as any).api.waiters.updateFunctionCodeToFinish = { + name: 'UpdateFunctionCodeToFinish', + operation: 'getFunction', + // equates to 1 minute for zip function not in a VPC and + // 5 minutes for container functions or function in a VPC + maxAttempts: 60, + delay: delaySeconds, + acceptors: [ + { + matcher: 'path', + argument: "Configuration.LastUpdateStatus == 'Successful' && Configuration.State == 'Active'", + expected: true, + state: 'success', + }, + { + matcher: 'path', + argument: 'Configuration.LastUpdateStatus', + expected: 'Failed', + state: 'failure', + }, + ], + }; + + const updateFunctionCodeWaiter = new (AWS as any).ResourceWaiter(lambda, 'updateFunctionCodeToFinish'); + await updateFunctionCodeWaiter.wait({ + FunctionName: this.lambdaFunctionResource.physicalName, + }).promise(); + } } /** diff --git a/packages/aws-cdk/test/api/hotswap/hotswap-deployments.test.ts b/packages/aws-cdk/test/api/hotswap/hotswap-deployments.test.ts index 00a6b2095a569..67b0117a8d7ae 100644 --- a/packages/aws-cdk/test/api/hotswap/hotswap-deployments.test.ts +++ b/packages/aws-cdk/test/api/hotswap/hotswap-deployments.test.ts @@ -8,7 +8,7 @@ let mockGetEndpointSuffix: () => string; beforeEach(() => { hotswapMockSdkProvider = setup.setupHotswapTests(); - mockUpdateLambdaCode = jest.fn(); + mockUpdateLambdaCode = jest.fn().mockReturnValue({}); mockUpdateMachineDefinition = jest.fn(); mockGetEndpointSuffix = jest.fn(() => 'amazonaws.com'); hotswapMockSdkProvider.stubLambda({ diff --git a/packages/aws-cdk/test/api/hotswap/hotswap-test-setup.ts b/packages/aws-cdk/test/api/hotswap/hotswap-test-setup.ts index e47b127e68766..7b6aebb9a81ee 100644 --- a/packages/aws-cdk/test/api/hotswap/hotswap-test-setup.ts +++ b/packages/aws-cdk/test/api/hotswap/hotswap-test-setup.ts @@ -83,8 +83,29 @@ export class HotswapMockSdkProvider { }); } - public stubLambda(stubs: SyncHandlerSubsetOf) { - this.mockSdkProvider.stubLambda(stubs); + public stubLambda( + stubs: SyncHandlerSubsetOf, + serviceStubs?: SyncHandlerSubsetOf, + additionalProperties: { [key: string]: any } = {}, + ): void { + this.mockSdkProvider.stubLambda(stubs, { + api: { + waiters: {}, + }, + makeRequest() { + return { + promise: () => Promise.resolve({}), + response: {}, + addListeners: () => {}, + }; + }, + ...serviceStubs, + ...additionalProperties, + }); + } + + public getLambdaApiWaiters(): { [key: string]: any } { + return (this.mockSdkProvider.sdk.lambda() as any).api.waiters; } public setUpdateProjectMock(mockUpdateProject: (input: codebuild.UpdateProjectInput) => codebuild.UpdateProjectOutput) { diff --git a/packages/aws-cdk/test/api/hotswap/lambda-functions-docker-hotswap-deployments.test.ts b/packages/aws-cdk/test/api/hotswap/lambda-functions-docker-hotswap-deployments.test.ts index 9aef8b8778cf0..3ed53e5fd908b 100644 --- a/packages/aws-cdk/test/api/hotswap/lambda-functions-docker-hotswap-deployments.test.ts +++ b/packages/aws-cdk/test/api/hotswap/lambda-functions-docker-hotswap-deployments.test.ts @@ -5,16 +5,26 @@ let mockUpdateLambdaCode: (params: Lambda.Types.UpdateFunctionCodeRequest) => La let mockTagResource: (params: Lambda.Types.TagResourceRequest) => {}; let mockUntagResource: (params: Lambda.Types.UntagResourceRequest) => {}; let hotswapMockSdkProvider: setup.HotswapMockSdkProvider; +let mockMakeRequest: (operation: string, params: any) => AWS.Request; beforeEach(() => { hotswapMockSdkProvider = setup.setupHotswapTests(); - mockUpdateLambdaCode = jest.fn(); + mockUpdateLambdaCode = jest.fn().mockReturnValue({ + PackageType: 'Image', + }); mockTagResource = jest.fn(); mockUntagResource = jest.fn(); + mockMakeRequest = jest.fn().mockReturnValue({ + promise: () => Promise.resolve({}), + response: {}, + addListeners: () => {}, + }); hotswapMockSdkProvider.stubLambda({ updateFunctionCode: mockUpdateLambdaCode, tagResource: mockTagResource, untagResource: mockUntagResource, + }, { + makeRequest: mockMakeRequest, }); }); @@ -65,3 +75,53 @@ test('calls the updateLambdaCode() API when it receives only a code difference i ImageUri: 'new-image', }); }); + +test('calls the getFunction() API with a delay of 5', async () => { + // GIVEN + setup.setCurrentCfnStackTemplate({ + Resources: { + Func: { + Type: 'AWS::Lambda::Function', + Properties: { + Code: { + ImageUri: 'current-image', + }, + FunctionName: 'my-function', + }, + Metadata: { + 'aws:asset:path': 'old-path', + }, + }, + }, + }); + const cdkStackArtifact = setup.cdkStackArtifactOf({ + template: { + Resources: { + Func: { + Type: 'AWS::Lambda::Function', + Properties: { + Code: { + ImageUri: 'new-image', + }, + FunctionName: 'my-function', + }, + Metadata: { + 'aws:asset:path': 'new-path', + }, + }, + }, + }, + }); + + // WHEN + await hotswapMockSdkProvider.tryHotswapDeployment(cdkStackArtifact); + + // THEN + expect(mockMakeRequest).toHaveBeenCalledWith('getFunction', { FunctionName: 'my-function' }); + expect(hotswapMockSdkProvider.getLambdaApiWaiters()).toEqual(expect.objectContaining({ + updateFunctionCodeToFinish: expect.objectContaining({ + name: 'UpdateFunctionCodeToFinish', + delay: 5, + }), + })); +}); diff --git a/packages/aws-cdk/test/api/hotswap/lambda-functions-hotswap-deployments.test.ts b/packages/aws-cdk/test/api/hotswap/lambda-functions-hotswap-deployments.test.ts index 0b43563f233d9..d96b8530bce88 100644 --- a/packages/aws-cdk/test/api/hotswap/lambda-functions-hotswap-deployments.test.ts +++ b/packages/aws-cdk/test/api/hotswap/lambda-functions-hotswap-deployments.test.ts @@ -4,17 +4,25 @@ import * as setup from './hotswap-test-setup'; let mockUpdateLambdaCode: (params: Lambda.Types.UpdateFunctionCodeRequest) => Lambda.Types.FunctionConfiguration; let mockTagResource: (params: Lambda.Types.TagResourceRequest) => {}; let mockUntagResource: (params: Lambda.Types.UntagResourceRequest) => {}; +let mockMakeRequest: (operation: string, params: any) => AWS.Request; let hotswapMockSdkProvider: setup.HotswapMockSdkProvider; beforeEach(() => { hotswapMockSdkProvider = setup.setupHotswapTests(); - mockUpdateLambdaCode = jest.fn(); + mockUpdateLambdaCode = jest.fn().mockReturnValue({}); mockTagResource = jest.fn(); mockUntagResource = jest.fn(); + mockMakeRequest = jest.fn().mockReturnValue({ + promise: () => Promise.resolve({}), + response: {}, + addListeners: () => {}, + }); hotswapMockSdkProvider.stubLambda({ updateFunctionCode: mockUpdateLambdaCode, tagResource: mockTagResource, untagResource: mockUntagResource, + }, { + makeRequest: mockMakeRequest, }); }); @@ -539,3 +547,177 @@ test('does not call the updateLambdaCode() API when a resource with type that is expect(deployStackResult).toBeUndefined(); expect(mockUpdateLambdaCode).not.toHaveBeenCalled(); }); + +test('calls getFunction() after function code is updated with delay 1', async () => { + // GIVEN + setup.setCurrentCfnStackTemplate({ + Resources: { + Func: { + Type: 'AWS::Lambda::Function', + Properties: { + Code: { + S3Bucket: 'current-bucket', + S3Key: 'current-key', + }, + FunctionName: 'my-function', + }, + Metadata: { + 'aws:asset:path': 'old-path', + }, + }, + }, + }); + const cdkStackArtifact = setup.cdkStackArtifactOf({ + template: { + Resources: { + Func: { + Type: 'AWS::Lambda::Function', + Properties: { + Code: { + S3Bucket: 'current-bucket', + S3Key: 'new-key', + }, + FunctionName: 'my-function', + }, + Metadata: { + 'aws:asset:path': 'new-path', + }, + }, + }, + }, + }); + + // WHEN + await hotswapMockSdkProvider.tryHotswapDeployment(cdkStackArtifact); + + // THEN + expect(mockMakeRequest).toHaveBeenCalledWith('getFunction', { FunctionName: 'my-function' }); + expect(hotswapMockSdkProvider.getLambdaApiWaiters()).toEqual(expect.objectContaining({ + updateFunctionCodeToFinish: expect.objectContaining({ + name: 'UpdateFunctionCodeToFinish', + delay: 1, + }), + })); +}); + +test('calls getFunction() after function code is updated and VpcId is empty string with delay 1', async () => { + // GIVEN + mockUpdateLambdaCode = jest.fn().mockReturnValue({ + VpcConfig: { + VpcId: '', + }, + }); + hotswapMockSdkProvider.stubLambda({ + updateFunctionCode: mockUpdateLambdaCode, + tagResource: mockTagResource, + untagResource: mockUntagResource, + }); + setup.setCurrentCfnStackTemplate({ + Resources: { + Func: { + Type: 'AWS::Lambda::Function', + Properties: { + Code: { + S3Bucket: 'current-bucket', + S3Key: 'current-key', + }, + FunctionName: 'my-function', + }, + Metadata: { + 'aws:asset:path': 'old-path', + }, + }, + }, + }); + const cdkStackArtifact = setup.cdkStackArtifactOf({ + template: { + Resources: { + Func: { + Type: 'AWS::Lambda::Function', + Properties: { + Code: { + S3Bucket: 'current-bucket', + S3Key: 'new-key', + }, + FunctionName: 'my-function', + }, + Metadata: { + 'aws:asset:path': 'new-path', + }, + }, + }, + }, + }); + + // WHEN + await hotswapMockSdkProvider.tryHotswapDeployment(cdkStackArtifact); + + // THEN + expect(hotswapMockSdkProvider.getLambdaApiWaiters()).toEqual(expect.objectContaining({ + updateFunctionCodeToFinish: expect.objectContaining({ + name: 'UpdateFunctionCodeToFinish', + delay: 1, + }), + })); +}); + +test('calls getFunction() after function code is updated on a VPC function with delay 5', async () => { + // GIVEN + mockUpdateLambdaCode = jest.fn().mockReturnValue({ + VpcConfig: { + VpcId: 'abc', + }, + }); + hotswapMockSdkProvider.stubLambda({ + updateFunctionCode: mockUpdateLambdaCode, + tagResource: mockTagResource, + untagResource: mockUntagResource, + }); + setup.setCurrentCfnStackTemplate({ + Resources: { + Func: { + Type: 'AWS::Lambda::Function', + Properties: { + Code: { + S3Bucket: 'current-bucket', + S3Key: 'current-key', + }, + FunctionName: 'my-function', + }, + Metadata: { + 'aws:asset:path': 'old-path', + }, + }, + }, + }); + const cdkStackArtifact = setup.cdkStackArtifactOf({ + template: { + Resources: { + Func: { + Type: 'AWS::Lambda::Function', + Properties: { + Code: { + S3Bucket: 'current-bucket', + S3Key: 'new-key', + }, + FunctionName: 'my-function', + }, + Metadata: { + 'aws:asset:path': 'new-path', + }, + }, + }, + }, + }); + + // WHEN + await hotswapMockSdkProvider.tryHotswapDeployment(cdkStackArtifact); + + // THEN + expect(hotswapMockSdkProvider.getLambdaApiWaiters()).toEqual(expect.objectContaining({ + updateFunctionCodeToFinish: expect.objectContaining({ + name: 'UpdateFunctionCodeToFinish', + delay: 5, + }), + })); +}); diff --git a/packages/aws-cdk/test/api/hotswap/lambda-functions-inline-hotswap-deployments.test.ts b/packages/aws-cdk/test/api/hotswap/lambda-functions-inline-hotswap-deployments.test.ts index 13554cc655dcb..478fd80b538bf 100644 --- a/packages/aws-cdk/test/api/hotswap/lambda-functions-inline-hotswap-deployments.test.ts +++ b/packages/aws-cdk/test/api/hotswap/lambda-functions-inline-hotswap-deployments.test.ts @@ -8,7 +8,7 @@ let hotswapMockSdkProvider: setup.HotswapMockSdkProvider; beforeEach(() => { hotswapMockSdkProvider = setup.setupHotswapTests(); - mockUpdateLambdaCode = jest.fn(); + mockUpdateLambdaCode = jest.fn().mockReturnValue({}); mockTagResource = jest.fn(); mockUntagResource = jest.fn(); hotswapMockSdkProvider.stubLambda({ diff --git a/packages/aws-cdk/test/api/hotswap/lambda-versions-aliases-hotswap-deployments.test.ts b/packages/aws-cdk/test/api/hotswap/lambda-versions-aliases-hotswap-deployments.test.ts index 4596bb2c96ac0..f937be7c5a5a0 100644 --- a/packages/aws-cdk/test/api/hotswap/lambda-versions-aliases-hotswap-deployments.test.ts +++ b/packages/aws-cdk/test/api/hotswap/lambda-versions-aliases-hotswap-deployments.test.ts @@ -8,7 +8,7 @@ let hotswapMockSdkProvider: setup.HotswapMockSdkProvider; beforeEach(() => { hotswapMockSdkProvider = setup.setupHotswapTests(); - mockUpdateLambdaCode = jest.fn(); + mockUpdateLambdaCode = jest.fn().mockReturnValue({}); mockPublishVersion = jest.fn(); mockUpdateAlias = jest.fn(); hotswapMockSdkProvider.stubLambda({ diff --git a/packages/aws-cdk/test/api/logs/logs-monitor.test.ts b/packages/aws-cdk/test/api/logs/logs-monitor.test.ts index becb3d97dfbf3..930e6427dbab7 100644 --- a/packages/aws-cdk/test/api/logs/logs-monitor.test.ts +++ b/packages/aws-cdk/test/api/logs/logs-monitor.test.ts @@ -17,20 +17,13 @@ afterAll(() => { monitor.deactivate(); }); -let TIMESTAMP: number; -let HUMAN_TIME: string; - -beforeAll(() => { - TIMESTAMP = new Date().getTime(); - HUMAN_TIME = new Date(TIMESTAMP).toLocaleTimeString(); -}); - test('continue to the next page if it exists', async () => { // GIVEN + const eventDate = new Date(T0 + 102 * 1000); sdk.stubCloudWatchLogs({ filterLogEvents() { return { - events: [event(102, 'message')], + events: [event(102, 'message', eventDate)], nextToken: 'some-token', }; }, @@ -50,22 +43,23 @@ test('continue to the next page if it exists', async () => { await sleep(1000); // THEN + const expectedLocaleTimeString = eventDate.toLocaleTimeString(); expect(stderrMock).toHaveBeenCalledTimes(2); expect(stderrMock.mock.calls[0][0]).toContain( - `[${blue('loggroup')}] ${yellow(HUMAN_TIME)} message`, + `[${blue('loggroup')}] ${yellow(expectedLocaleTimeString)} message`, ); expect(stderrMock.mock.calls[1][0]).toContain( - `[${blue('loggroup')}] ${yellow(new Date(T100).toLocaleTimeString())} >>> \`watch\` shows only the first 100 log messages - the rest have been truncated...`, + `[${blue('loggroup')}] ${yellow(expectedLocaleTimeString)} >>> \`watch\` shows only the first 100 log messages - the rest have been truncated...`, ); }); const T0 = 1597837230504; const T100 = T0 + 100 * 1000; -function event(nr: number, message: string): AWS.CloudWatchLogs.FilteredLogEvent { +function event(nr: number, message: string, timestamp: Date): AWS.CloudWatchLogs.FilteredLogEvent { return { eventId: `${nr}`, message, - timestamp: new Date(T0 * nr * 1000).getTime(), - ingestionTime: new Date(T0 * nr * 1000).getTime(), + timestamp: timestamp.getTime(), + ingestionTime: timestamp.getTime(), }; } diff --git a/packages/aws-cdk/test/util/mock-sdk.ts b/packages/aws-cdk/test/util/mock-sdk.ts index a1ed61a431366..bb8eaae251c47 100644 --- a/packages/aws-cdk/test/util/mock-sdk.ts +++ b/packages/aws-cdk/test/util/mock-sdk.ts @@ -102,8 +102,8 @@ export class MockSdkProvider extends SdkProvider { (this.sdk as any).ssm = jest.fn().mockReturnValue(partialAwsService(stubs)); } - public stubLambda(stubs: SyncHandlerSubsetOf) { - (this.sdk as any).lambda = jest.fn().mockReturnValue(partialAwsService(stubs)); + public stubLambda(stubs: SyncHandlerSubsetOf, additionalProperties: { [key: string]: any } = {}) { + (this.sdk as any).lambda = jest.fn().mockReturnValue(partialAwsService(stubs, additionalProperties)); } public stubStepFunctions(stubs: SyncHandlerSubsetOf) {