From 52dc123ba8696abcfad99d8093e98cd39b5b104f Mon Sep 17 00:00:00 2001 From: comcalvi <66279577+comcalvi@users.noreply.github.com> Date: Fri, 19 Jun 2020 20:53:26 -0400 Subject: [PATCH 01/21] feat(cfn-include): add support for retrieving parameter objects (#8658) Resolves #8657 --- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../@aws-cdk/cloudformation-include/README.md | 2 +- .../cloudformation-include/lib/cfn-include.ts | 53 +++++++++++++++++-- .../bucket-with-parameters.json | 47 ++++++++++++++++ .../test/valid-templates.test.ts | 34 ++++++++++++ 4 files changed, 130 insertions(+), 6 deletions(-) create mode 100644 packages/@aws-cdk/cloudformation-include/test/test-templates/bucket-with-parameters.json diff --git a/packages/@aws-cdk/cloudformation-include/README.md b/packages/@aws-cdk/cloudformation-include/README.md index 4023f2b214069..188f9989075ea 100644 --- a/packages/@aws-cdk/cloudformation-include/README.md +++ b/packages/@aws-cdk/cloudformation-include/README.md @@ -115,7 +115,7 @@ All items unchecked below are currently not supported. ### Ability to retrieve CloudFormation objects from the template: - [x] Resources -- [ ] Parameters +- [x] Parameters - [x] Conditions - [ ] Outputs diff --git a/packages/@aws-cdk/cloudformation-include/lib/cfn-include.ts b/packages/@aws-cdk/cloudformation-include/lib/cfn-include.ts index 07c6dee3bfcbf..b7f712c23af56 100644 --- a/packages/@aws-cdk/cloudformation-include/lib/cfn-include.ts +++ b/packages/@aws-cdk/cloudformation-include/lib/cfn-include.ts @@ -23,6 +23,7 @@ export interface CfnIncludeProps { export class CfnInclude extends core.CfnElement { private readonly conditions: { [conditionName: string]: core.CfnCondition } = {}; private readonly resources: { [logicalId: string]: core.CfnResource } = {}; + private readonly parameters: { [logicalId: string]: core.CfnParameter } = {}; private readonly template: any; private readonly preserveLogicalIds: boolean; @@ -35,12 +36,17 @@ export class CfnInclude extends core.CfnElement { // ToDo implement preserveLogicalIds=false this.preserveLogicalIds = true; - // first, instantiate the conditions + // instantiate all parameters + for (const logicalId of Object.keys(this.template.Parameters || {})) { + this.createParameter(logicalId); + } + + // instantiate the conditions for (const conditionName of Object.keys(this.template.Conditions || {})) { this.createCondition(conditionName); } - // then, instantiate all resources as CDK L1 objects + // instantiate all resources as CDK L1 objects for (const logicalId of Object.keys(this.template.Resources || {})) { this.getOrCreateResource(logicalId); } @@ -72,7 +78,7 @@ export class CfnInclude extends core.CfnElement { /** * Returns the CfnCondition object from the 'Conditions' - * section of the CloudFormation template with the give name. + * section of the CloudFormation template with the given name. * Any modifications performed on that object will be reflected in the resulting CDK template. * * If a Condition with the given name is not present in the template, @@ -88,14 +94,32 @@ export class CfnInclude extends core.CfnElement { return ret; } + /** + * Returns the CfnParameter object from the 'Parameters' + * section of the included template + * Any modifications performed on that object will be reflected in the resulting CDK template. + * + * If a Parameter with the given name is not present in the template, + * throws an exception. + * + * @param parameterName the name of the parameter to retrieve + */ + public getParameter(parameterName: string): core.CfnParameter { + const ret = this.parameters[parameterName]; + if (!ret) { + throw new Error(`Parameter with name '${parameterName}' was not found in the template`); + } + return ret; + } + /** @internal */ public _toCloudFormation(): object { const ret: { [section: string]: any } = {}; for (const section of Object.keys(this.template)) { // render all sections of the template unchanged, - // except Conditions and Resources, which will be taken care of by the created L1s - if (section !== 'Conditions' && section !== 'Resources') { + // except Conditions, Resources, and Parameters, which will be taken care of by the created L1s + if (section !== 'Conditions' && section !== 'Resources' && section !== 'Parameters') { ret[section] = this.template[section]; } } @@ -103,6 +127,25 @@ export class CfnInclude extends core.CfnElement { return ret; } + private createParameter(logicalId: string): void { + const expression = cfn_parse.FromCloudFormation.parseValue(this.template.Parameters[logicalId]); + const cfnParameter = new core.CfnParameter(this, logicalId, { + type: expression.Type, + default: expression.Default, + allowedPattern: expression.AllowedPattern, + constraintDescription: expression.ConstraintDescription, + description: expression.Description, + maxLength: expression.MaxLength, + maxValue: expression.MaxValue, + minLength: expression.MinLength, + minValue: expression.MinValue, + noEcho: expression.NoEcho, + }); + + cfnParameter.overrideLogicalId(logicalId); + this.parameters[logicalId] = cfnParameter; + } + private createCondition(conditionName: string): void { // ToDo condition expressions can refer to other conditions - // will be important when implementing preserveLogicalIds=false diff --git a/packages/@aws-cdk/cloudformation-include/test/test-templates/bucket-with-parameters.json b/packages/@aws-cdk/cloudformation-include/test/test-templates/bucket-with-parameters.json new file mode 100644 index 0000000000000..deec4ffa24e81 --- /dev/null +++ b/packages/@aws-cdk/cloudformation-include/test/test-templates/bucket-with-parameters.json @@ -0,0 +1,47 @@ +{ + "Parameters": { + "BucketName": { + "Default": "MyS3Bucket", + "AllowedPattern": "^[a-zA-Z0-9]*$", + "ConstraintDescription": "a string consisting only of alphanumeric characters", + "Description": "The name of your bucket", + "MaxLength": "10", + "MinLength": "1", + "Type": "String", + "NoEcho": "true" + }, + "CorsMaxAge": { + "Default": "3", + "Description": "the time in seconds that a browser will cache the preflight response", + "MaxValue": "300", + "MinValue": "0", + "Type": "Number", + "NoEcho": "true" + } + }, + "Resources": { + "Bucket": { + "Type": "AWS::S3::Bucket", + "Properties": { + "BucketName": { + "Ref": "BucketName" + }, + "CorsConfiguration": { + "CorsRules": [{ + "AllowedMethods": [ + "GET", + "POST" + ], + "AllowedOrigins": [ + "origin1", + "origin2" + ], + "MaxAge": { + "Ref": "CorsMaxAge" + } + }] + } + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/cloudformation-include/test/valid-templates.test.ts b/packages/@aws-cdk/cloudformation-include/test/valid-templates.test.ts index 1855c46acab5a..f09331895f079 100644 --- a/packages/@aws-cdk/cloudformation-include/test/valid-templates.test.ts +++ b/packages/@aws-cdk/cloudformation-include/test/valid-templates.test.ts @@ -251,6 +251,40 @@ describe('CDK Include', () => { ); }); + test("correctly parses templates with parameters", () => { + const cfnTemplate = includeTestTemplate(stack, 'bucket-with-parameters.json'); + const param = cfnTemplate.getParameter('BucketName'); + new s3.CfnBucket(stack, 'NewBucket', { + bucketName: param.valueAsString, + }); + + const originalTemplate = loadTestFileToJsObject('bucket-with-parameters.json'); + expect(stack).toMatchTemplate({ + "Resources": { + ...originalTemplate.Resources, + "NewBucket": { + "Type": "AWS::S3::Bucket", + "Properties": { + "BucketName": { + "Ref": "BucketName", + }, + }, + }, + }, + "Parameters": { + ...originalTemplate.Parameters, + }, + }); + }); + + test('getParameter() throws an exception if asked for a Parameter with a name that is not present in the template', () => { + const cfnTemplate = includeTestTemplate(stack, 'bucket-with-parameters.json'); + + expect(() => { + cfnTemplate.getParameter('FakeBucketNameThatDoesNotExist'); + }).toThrow(/Parameter with name 'FakeBucketNameThatDoesNotExist' was not found in the template/); + }); + test('reflects changes to a retrieved CfnCondition object in the resulting template', () => { const cfnTemplate = includeTestTemplate(stack, 'resource-attribute-condition.json'); const alwaysFalseCondition = cfnTemplate.getCondition('AlwaysFalseCond'); From 1e6d293f4c445318b11bd6fe998325688a675807 Mon Sep 17 00:00:00 2001 From: Barun Ray Date: Mon, 22 Jun 2020 13:22:32 +0530 Subject: [PATCH 02/21] feat(cloudwatch): CompositeAlarm (#8498) * fix https://github.com/aws/aws-cdk/issues/8462 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-cloudwatch/README.md | 23 ++++ .../aws-cloudwatch/lib/alarm-action.ts | 2 +- .../@aws-cdk/aws-cloudwatch/lib/alarm-base.ts | 96 +++++++++++++ .../@aws-cdk/aws-cloudwatch/lib/alarm-rule.ts | 127 +++++++++++++++++ packages/@aws-cdk/aws-cloudwatch/lib/alarm.ts | 70 +--------- .../aws-cloudwatch/lib/composite-alarm.ts | 129 ++++++++++++++++++ packages/@aws-cdk/aws-cloudwatch/lib/graph.ts | 2 +- packages/@aws-cdk/aws-cloudwatch/lib/index.ts | 3 + packages/@aws-cdk/aws-cloudwatch/package.json | 3 +- .../test/integ.composite-alarm.expected.json | 82 +++++++++++ .../test/integ.composite-alarm.ts | 61 +++++++++ .../test/test.composite-alarm.ts | 98 +++++++++++++ 12 files changed, 627 insertions(+), 69 deletions(-) create mode 100644 packages/@aws-cdk/aws-cloudwatch/lib/alarm-base.ts create mode 100644 packages/@aws-cdk/aws-cloudwatch/lib/alarm-rule.ts create mode 100644 packages/@aws-cdk/aws-cloudwatch/lib/composite-alarm.ts create mode 100644 packages/@aws-cdk/aws-cloudwatch/test/integ.composite-alarm.expected.json create mode 100644 packages/@aws-cdk/aws-cloudwatch/test/integ.composite-alarm.ts create mode 100644 packages/@aws-cdk/aws-cloudwatch/test/test.composite-alarm.ts diff --git a/packages/@aws-cdk/aws-cloudwatch/README.md b/packages/@aws-cdk/aws-cloudwatch/README.md index e1eca96157a15..03d0d9abeafd0 100644 --- a/packages/@aws-cdk/aws-cloudwatch/README.md +++ b/packages/@aws-cdk/aws-cloudwatch/README.md @@ -174,6 +174,29 @@ const alarm = new cloudwatch.Alarm(stack, 'Alarm', { /* ... */ }); alarm.addAlarmAction(new cw_actions.SnsAction(topic)); ``` +### Composite Alarms + +[Composite Alarms](https://aws.amazon.com/about-aws/whats-new/2020/03/amazon-cloudwatch-now-allows-you-to-combine-multiple-alarms/) +can be created from existing Alarm resources. + +```ts +const alarmRule = AlarmRule.anyOf( + AlarmRule.allOf( + AlarmRule.anyOf( + alarm1, + AlarmRule.fromAlarm(alarm2, AlarmState.OK), + alarm3, + ), + AlarmRule.not(AlarmRule.fromAlarm(alarm4, AlarmState.INSUFFICIENT_DATA)), + ), + AlarmRule.fromBoolean(false), +); + +new CompositeAlarm(this, 'MyAwesomeCompositeAlarm', { + alarmRule, +}); +``` + ### A note on units In CloudWatch, Metrics datums are emitted with units, such as `seconds` or diff --git a/packages/@aws-cdk/aws-cloudwatch/lib/alarm-action.ts b/packages/@aws-cdk/aws-cloudwatch/lib/alarm-action.ts index 51714fc9ff760..7b48d0f055873 100644 --- a/packages/@aws-cdk/aws-cloudwatch/lib/alarm-action.ts +++ b/packages/@aws-cdk/aws-cloudwatch/lib/alarm-action.ts @@ -1,5 +1,5 @@ import { Construct } from '@aws-cdk/core'; -import { IAlarm } from './alarm'; +import { IAlarm } from './alarm-base'; /** * Interface for objects that can be the targets of CloudWatch alarm actions diff --git a/packages/@aws-cdk/aws-cloudwatch/lib/alarm-base.ts b/packages/@aws-cdk/aws-cloudwatch/lib/alarm-base.ts new file mode 100644 index 0000000000000..996079718edba --- /dev/null +++ b/packages/@aws-cdk/aws-cloudwatch/lib/alarm-base.ts @@ -0,0 +1,96 @@ +import { IResource, Resource } from '@aws-cdk/core'; +import { IAlarmAction } from './alarm-action'; + +/** + * Interface for Alarm Rule. + */ +export interface IAlarmRule { + + /** + * serialized representation of Alarm Rule to be used when building the Composite Alarm resource. + */ + renderAlarmRule(): string; + +} + +/** + * Represents a CloudWatch Alarm + */ +export interface IAlarm extends IAlarmRule, IResource { + /** + * Alarm ARN (i.e. arn:aws:cloudwatch:::alarm:Foo) + * + * @attribute + */ + readonly alarmArn: string; + + /** + * Name of the alarm + * + * @attribute + */ + readonly alarmName: string; +} + +/** + * The base class for Alarm and CompositeAlarm resources. + */ +export abstract class AlarmBase extends Resource implements IAlarm { + + /** + * @attribute + */ + public abstract readonly alarmArn: string; + public abstract readonly alarmName: string; + + protected alarmActionArns?: string[]; + protected insufficientDataActionArns?: string[]; + protected okActionArns?: string[]; + + /** + * AlarmRule indicating ALARM state for Alarm. + */ + public renderAlarmRule(): string { + return `ALARM(${this.alarmArn})`; + } + + /** + * Trigger this action if the alarm fires + * + * Typically the ARN of an SNS topic or ARN of an AutoScaling policy. + */ + public addAlarmAction(...actions: IAlarmAction[]) { + if (this.alarmActionArns === undefined) { + this.alarmActionArns = []; + } + + this.alarmActionArns.push(...actions.map(a => a.bind(this, this).alarmActionArn)); + } + + /** + * Trigger this action if there is insufficient data to evaluate the alarm + * + * Typically the ARN of an SNS topic or ARN of an AutoScaling policy. + */ + public addInsufficientDataAction(...actions: IAlarmAction[]) { + if (this.insufficientDataActionArns === undefined) { + this.insufficientDataActionArns = []; + } + + this.insufficientDataActionArns.push(...actions.map(a => a.bind(this, this).alarmActionArn)); + } + + /** + * Trigger this action if the alarm returns from breaching state into ok state + * + * Typically the ARN of an SNS topic or ARN of an AutoScaling policy. + */ + public addOkAction(...actions: IAlarmAction[]) { + if (this.okActionArns === undefined) { + this.okActionArns = []; + } + + this.okActionArns.push(...actions.map(a => a.bind(this, this).alarmActionArn)); + } + +} diff --git a/packages/@aws-cdk/aws-cloudwatch/lib/alarm-rule.ts b/packages/@aws-cdk/aws-cloudwatch/lib/alarm-rule.ts new file mode 100644 index 0000000000000..f860bcb095d44 --- /dev/null +++ b/packages/@aws-cdk/aws-cloudwatch/lib/alarm-rule.ts @@ -0,0 +1,127 @@ +import { IAlarm, IAlarmRule } from './alarm-base'; + +/** + * Enumeration indicates state of Alarm used in building Alarm Rule. + */ +export enum AlarmState { + + /** + * State indicates resource is in ALARM + */ + ALARM = 'ALARM', + + /** + * State indicates resource is not in ALARM + */ + OK = 'OK', + + /** + * State indicates there is not enough data to determine is resource is in ALARM + */ + INSUFFICIENT_DATA = 'INSUFFICIENT_DATA', + +} + +/** + * Enumeration of supported Composite Alarms operators. + */ +enum Operator { + + AND = 'AND', + OR = 'OR', + NOT = 'NOT', + +} + +/** + * Class with static functions to build AlarmRule for Composite Alarms. + */ +export class AlarmRule { + + /** + * function to join all provided AlarmRules with AND operator. + * + * @param operands IAlarmRules to be joined with AND operator. + */ + public static allOf(...operands: IAlarmRule[]): IAlarmRule { + return this.concat(Operator.AND, ...operands); + } + + /** + * function to join all provided AlarmRules with OR operator. + * + * @param operands IAlarmRules to be joined with OR operator. + */ + public static anyOf(...operands: IAlarmRule[]): IAlarmRule { + return this.concat(Operator.OR, ...operands); + } + + /** + * function to wrap provided AlarmRule in NOT operator. + * + * @param operand IAlarmRule to be wrapped in NOT operator. + */ + public static not(operand: IAlarmRule): IAlarmRule { + // tslint:disable-next-line:new-parens + return new class implements IAlarmRule { + public renderAlarmRule(): string { + return `(NOT (${operand.renderAlarmRule()}))`; + } + }; + } + + /** + * function to build TRUE/FALSE intent for Rule Expression. + * + * @param value boolean value to be used in rule expression. + */ + public static fromBoolean(value: boolean): IAlarmRule { + // tslint:disable-next-line:new-parens + return new class implements IAlarmRule { + public renderAlarmRule(): string { + return `${String(value).toUpperCase()}`; + } + }; + } + + /** + * function to build Rule Expression for given IAlarm and AlarmState. + * + * @param alarm IAlarm to be used in Rule Expression. + * @param alarmState AlarmState to be used in Rule Expression. + */ + public static fromAlarm(alarm: IAlarm, alarmState: AlarmState): IAlarmRule { + // tslint:disable-next-line:new-parens + return new class implements IAlarmRule { + public renderAlarmRule(): string { + return `${alarmState}(${alarm.alarmArn})`; + } + }; + } + + /** + * function to build Rule Expression for given Alarm Rule string. + * + * @param alarmRule string to be used in Rule Expression. + */ + public static fromString(alarmRule: string): IAlarmRule { + // tslint:disable-next-line:new-parens + return new class implements IAlarmRule { + public renderAlarmRule(): string { + return alarmRule; + } + }; + } + + private static concat(operator: Operator, ...operands: IAlarmRule[]): IAlarmRule { + // tslint:disable-next-line:new-parens + return new class implements IAlarmRule { + public renderAlarmRule(): string { + const expression = operands + .map(operand => `${operand.renderAlarmRule()}`) + .join(` ${operator} `); + return `(${expression})`; + } + }; + } +} diff --git a/packages/@aws-cdk/aws-cloudwatch/lib/alarm.ts b/packages/@aws-cdk/aws-cloudwatch/lib/alarm.ts index 4145922fcfdf7..a422b99daa0db 100644 --- a/packages/@aws-cdk/aws-cloudwatch/lib/alarm.ts +++ b/packages/@aws-cdk/aws-cloudwatch/lib/alarm.ts @@ -1,5 +1,5 @@ -import { Construct, IResource, Lazy, Resource, Stack, Token } from '@aws-cdk/core'; -import { IAlarmAction } from './alarm-action'; +import { Construct, Lazy, Stack, Token } from '@aws-cdk/core'; +import { AlarmBase, IAlarm } from './alarm-base'; import { CfnAlarm, CfnAlarmProps } from './cloudwatch.generated'; import { HorizontalAnnotation } from './graph'; import { CreateAlarmOptions } from './metric'; @@ -9,25 +9,6 @@ import { dropUndefined } from './private/object'; import { MetricSet } from './private/rendering'; import { parseStatistic } from './private/statistic'; -/** - * Represents a CloudWatch Alarm - */ -export interface IAlarm extends IResource { - /** - * Alarm ARN (i.e. arn:aws:cloudwatch:::alarm:Foo) - * - * @attribute - */ - readonly alarmArn: string; - - /** - * Name of the alarm - * - * @attribute - */ - readonly alarmName: string; -} - /** * Properties for Alarms */ @@ -107,7 +88,7 @@ export enum TreatMissingData { /** * An alarm on a CloudWatch metric */ -export class Alarm extends Resource implements IAlarm { +export class Alarm extends AlarmBase { /** * Import an existing CloudWatch alarm provided an ARN @@ -117,7 +98,7 @@ export class Alarm extends Resource implements IAlarm { * @param alarmArn Alarm ARN (i.e. arn:aws:cloudwatch:::alarm:Foo) */ public static fromAlarmArn(scope: Construct, id: string, alarmArn: string): IAlarm { - class Import extends Resource implements IAlarm { + class Import extends AlarmBase implements IAlarm { public readonly alarmArn = alarmArn; public readonly alarmName = Stack.of(scope).parseArn(alarmArn, ':').resourceName!; } @@ -143,10 +124,6 @@ export class Alarm extends Resource implements IAlarm { */ public readonly metric: IMetric; - private alarmActionArns?: string[]; - private insufficientDataActionArns?: string[]; - private okActionArns?: string[]; - /** * This metric as an annotation */ @@ -214,45 +191,6 @@ export class Alarm extends Resource implements IAlarm { }; } - /** - * Trigger this action if the alarm fires - * - * Typically the ARN of an SNS topic or ARN of an AutoScaling policy. - */ - public addAlarmAction(...actions: IAlarmAction[]) { - if (this.alarmActionArns === undefined) { - this.alarmActionArns = []; - } - - this.alarmActionArns.push(...actions.map(a => a.bind(this, this).alarmActionArn)); - } - - /** - * Trigger this action if there is insufficient data to evaluate the alarm - * - * Typically the ARN of an SNS topic or ARN of an AutoScaling policy. - */ - public addInsufficientDataAction(...actions: IAlarmAction[]) { - if (this.insufficientDataActionArns === undefined) { - this.insufficientDataActionArns = []; - } - - this.insufficientDataActionArns.push(...actions.map(a => a.bind(this, this).alarmActionArn)); - } - - /** - * Trigger this action if the alarm returns from breaching state into ok state - * - * Typically the ARN of an SNS topic or ARN of an AutoScaling policy. - */ - public addOkAction(...actions: IAlarmAction[]) { - if (this.okActionArns === undefined) { - this.okActionArns = []; - } - - this.okActionArns.push(...actions.map(a => a.bind(this, this).alarmActionArn)); - } - /** * Turn this alarm into a horizontal annotation * diff --git a/packages/@aws-cdk/aws-cloudwatch/lib/composite-alarm.ts b/packages/@aws-cdk/aws-cloudwatch/lib/composite-alarm.ts new file mode 100644 index 0000000000000..4d20f5f3f1300 --- /dev/null +++ b/packages/@aws-cdk/aws-cloudwatch/lib/composite-alarm.ts @@ -0,0 +1,129 @@ +import { Construct, Lazy, Stack } from '@aws-cdk/core'; +import { AlarmBase, IAlarm, IAlarmRule } from './alarm-base'; +import { CfnCompositeAlarm } from './cloudwatch.generated'; + +/** + * Properties for creating a Composite Alarm + */ +export interface CompositeAlarmProps { + + /** + * Whether the actions for this alarm are enabled + * + * @default true + */ + readonly actionsEnabled?: boolean; + + /** + * Description for the alarm + * + * @default No description + */ + readonly alarmDescription?: string; + + /** + * Name of the alarm + * + * @default Automatically generated name + */ + readonly compositeAlarmName?: string; + + /** + * Expression that specifies which other alarms are to be evaluated to determine this composite alarm's state. + */ + readonly alarmRule: IAlarmRule; + +} + +/** + * A Composite Alarm based on Alarm Rule. + */ +export class CompositeAlarm extends AlarmBase { + + /** + * Import an existing CloudWatch composite alarm provided an Name. + * + * @param scope The parent creating construct (usually `this`) + * @param id The construct's name + * @param compositeAlarmName Composite Alarm Name + */ + public static fromCompositeAlarmName(scope: Construct, id: string, compositeAlarmName: string): IAlarm { + const stack = Stack.of(scope); + + return this.fromCompositeAlarmArn(scope, id, stack.formatArn({ + service: 'cloudwatch', + resource: 'alarm', + resourceName: compositeAlarmName, + })); + } + + /** + * Import an existing CloudWatch composite alarm provided an ARN. + * + * @param scope The parent creating construct (usually `this`) + * @param id The construct's name + * @param compositeAlarmArn Composite Alarm ARN (i.e. arn:aws:cloudwatch:::alarm/CompositeAlarmName) + */ + public static fromCompositeAlarmArn(scope: Construct, id: string, compositeAlarmArn: string): IAlarm { + class Import extends AlarmBase implements IAlarm { + public readonly alarmArn = compositeAlarmArn; + public readonly alarmName = Stack.of(scope).parseArn(compositeAlarmArn).resourceName!; + } + return new Import(scope, id); + } + + /** + * ARN of this alarm + * + * @attribute + */ + public readonly alarmArn: string; + + /** + * Name of this alarm. + * + * @attribute + */ + public readonly alarmName: string; + + private readonly alarmRule: string; + + constructor(scope: Construct, id: string, props: CompositeAlarmProps) { + super(scope, id, { + physicalName: props.compositeAlarmName ?? Lazy.stringValue({ produce: () => this.generateUniqueId() }), + }); + + if (props.alarmRule.renderAlarmRule().length > 10240) { + throw new Error('Alarm Rule expression cannot be greater than 10240 characters, please reduce the conditions in the Alarm Rule'); + } + + this.alarmRule = props.alarmRule.renderAlarmRule(); + + const alarm = new CfnCompositeAlarm(this, 'Resource', { + alarmName: this.physicalName, + alarmRule: this.alarmRule, + alarmDescription: props.alarmDescription, + actionsEnabled: props.actionsEnabled, + alarmActions: Lazy.listValue({ produce: () => this.alarmActionArns }), + insufficientDataActions: Lazy.listValue({ produce: (() => this.insufficientDataActionArns) }), + okActions: Lazy.listValue({ produce: () => this.okActionArns }), + }); + + this.alarmName = this.getResourceNameAttribute(alarm.ref); + this.alarmArn = this.getResourceArnAttribute(alarm.attrArn, { + service: 'cloudwatch', + resource: 'alarm', + resourceName: this.physicalName, + }); + + } + + private generateUniqueId(): string { + const name = this.node.uniqueId; + if (name.length > 240) { + return name.substring(0, 120) + name.substring(name.length - 120); + } + return name; + } + +} diff --git a/packages/@aws-cdk/aws-cloudwatch/lib/graph.ts b/packages/@aws-cdk/aws-cloudwatch/lib/graph.ts index 52b8f7a03687f..56e5de94d2acd 100644 --- a/packages/@aws-cdk/aws-cloudwatch/lib/graph.ts +++ b/packages/@aws-cdk/aws-cloudwatch/lib/graph.ts @@ -1,5 +1,5 @@ import * as cdk from '@aws-cdk/core'; -import { IAlarm } from './alarm'; +import { IAlarm } from './alarm-base'; import { IMetric } from './metric-types'; import { allMetricsGraphJson } from './private/rendering'; import { ConcreteWidget } from './widget'; diff --git a/packages/@aws-cdk/aws-cloudwatch/lib/index.ts b/packages/@aws-cdk/aws-cloudwatch/lib/index.ts index 8fd53065f689d..05d2ce4f254a6 100644 --- a/packages/@aws-cdk/aws-cloudwatch/lib/index.ts +++ b/packages/@aws-cdk/aws-cloudwatch/lib/index.ts @@ -1,5 +1,8 @@ export * from './alarm'; export * from './alarm-action'; +export * from './alarm-base'; +export * from './alarm-rule'; +export * from './composite-alarm'; export * from './dashboard'; export * from './graph'; export * from './layout'; diff --git a/packages/@aws-cdk/aws-cloudwatch/package.json b/packages/@aws-cdk/aws-cloudwatch/package.json index 2f4d2e1fe139d..7d9738d6f98b3 100644 --- a/packages/@aws-cdk/aws-cloudwatch/package.json +++ b/packages/@aws-cdk/aws-cloudwatch/package.json @@ -99,7 +99,8 @@ "props-default-doc:@aws-cdk/aws-cloudwatch.MetricRenderingProperties.color", "props-default-doc:@aws-cdk/aws-cloudwatch.MetricRenderingProperties.label", "props-default-doc:@aws-cdk/aws-cloudwatch.MetricRenderingProperties.stat", - "duration-prop-type:@aws-cdk/aws-cloudwatch.MetricExpressionConfig.period" + "duration-prop-type:@aws-cdk/aws-cloudwatch.MetricExpressionConfig.period", + "resource-attribute:@aws-cdk/aws-cloudwatch.CompositeAlarm.compositeAlarmArn" ] }, "engines": { diff --git a/packages/@aws-cdk/aws-cloudwatch/test/integ.composite-alarm.expected.json b/packages/@aws-cdk/aws-cloudwatch/test/integ.composite-alarm.expected.json new file mode 100644 index 0000000000000..4febb9e015123 --- /dev/null +++ b/packages/@aws-cdk/aws-cloudwatch/test/integ.composite-alarm.expected.json @@ -0,0 +1,82 @@ +{ + "Resources": { + "Alarm1F9009D71": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 3, + "MetricName": "Metric", + "Namespace": "CDK/Test", + "Period": 300, + "Statistic": "Average", + "Threshold": 100 + } + }, + "Alarm2A7122E13": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 3, + "MetricName": "Metric", + "Namespace": "CDK/Test", + "Period": 300, + "Statistic": "Average", + "Threshold": 1000 + } + }, + "Alarm32341D8D9": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 3, + "MetricName": "Metric", + "Namespace": "CDK/Test", + "Period": 300, + "Statistic": "Average", + "Threshold": 10000 + } + }, + "Alarm4671832C8": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 3, + "MetricName": "Metric", + "Namespace": "CDK/Test", + "Period": 300, + "Statistic": "Average", + "Threshold": 100000 + } + }, + "CompositeAlarmF4C3D082": { + "Type": "AWS::CloudWatch::CompositeAlarm", + "Properties": { + "AlarmName": "CompositeAlarmIntegrationTestCompositeAlarm742D2FBA", + "AlarmRule": { + "Fn::Join": [ + "", + [ + "(((ALARM(", + { + "Fn::GetAtt": [ "Alarm1F9009D71", "Arn" ] + }, + ") OR OK(", + { + "Fn::GetAtt": [ "Alarm2A7122E13", "Arn" ] + }, + ") OR ALARM(", + { + "Fn::GetAtt": [ "Alarm32341D8D9", "Arn" ] + }, + ")) AND (NOT (INSUFFICIENT_DATA(", + { + "Fn::GetAtt":[ "Alarm4671832C8", "Arn" ] + }, + ")))) OR FALSE)" + ] + ] + } + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cloudwatch/test/integ.composite-alarm.ts b/packages/@aws-cdk/aws-cloudwatch/test/integ.composite-alarm.ts new file mode 100644 index 0000000000000..7e2e215a13aab --- /dev/null +++ b/packages/@aws-cdk/aws-cloudwatch/test/integ.composite-alarm.ts @@ -0,0 +1,61 @@ +import { App, Stack, StackProps } from '@aws-cdk/core'; +import { Alarm, AlarmRule, AlarmState, CompositeAlarm, Metric } from '../lib'; + +class CompositeAlarmIntegrationTest extends Stack { + + constructor(scope: App, id: string, props?: StackProps) { + super(scope, id, props); + + const testMetric = new Metric({ + namespace: 'CDK/Test', + metricName: 'Metric', + }); + + const alarm1 = new Alarm(this, 'Alarm1', { + metric: testMetric, + threshold: 100, + evaluationPeriods: 3, + }); + + const alarm2 = new Alarm(this, 'Alarm2', { + metric: testMetric, + threshold: 1000, + evaluationPeriods: 3, + }); + + const alarm3 = new Alarm(this, 'Alarm3', { + metric: testMetric, + threshold: 10000, + evaluationPeriods: 3, + }); + + const alarm4 = new Alarm(this, 'Alarm4', { + metric: testMetric, + threshold: 100000, + evaluationPeriods: 3, + }); + + const alarmRule = AlarmRule.anyOf( + AlarmRule.allOf( + AlarmRule.anyOf( + alarm1, + AlarmRule.fromAlarm(alarm2, AlarmState.OK), + alarm3, + ), + AlarmRule.not(AlarmRule.fromAlarm(alarm4, AlarmState.INSUFFICIENT_DATA)), + ), + AlarmRule.fromBoolean(false), + ); + + new CompositeAlarm(this, 'CompositeAlarm', { + alarmRule, + }); + } + +} + +const app = new App(); + +new CompositeAlarmIntegrationTest(app, 'CompositeAlarmIntegrationTest'); + +app.synth(); diff --git a/packages/@aws-cdk/aws-cloudwatch/test/test.composite-alarm.ts b/packages/@aws-cdk/aws-cloudwatch/test/test.composite-alarm.ts new file mode 100644 index 0000000000000..633d792f79401 --- /dev/null +++ b/packages/@aws-cdk/aws-cloudwatch/test/test.composite-alarm.ts @@ -0,0 +1,98 @@ +import { expect, haveResource } from '@aws-cdk/assert'; +import { Stack } from '@aws-cdk/core'; +import { Test } from 'nodeunit'; +import { Alarm, AlarmRule, AlarmState, CompositeAlarm, Metric } from '../lib'; + +export = { + 'test alarm rule expression builder'(test: Test) { + const stack = new Stack(); + + const testMetric = new Metric({ + namespace: 'CDK/Test', + metricName: 'Metric', + }); + + const alarm1 = new Alarm(stack, 'Alarm1', { + metric: testMetric, + threshold: 100, + evaluationPeriods: 3, + }); + + const alarm2 = new Alarm(stack, 'Alarm2', { + metric: testMetric, + threshold: 1000, + evaluationPeriods: 3, + }); + + const alarm3 = new Alarm(stack, 'Alarm3', { + metric: testMetric, + threshold: 10000, + evaluationPeriods: 3, + }); + + const alarm4 = new Alarm(stack, 'Alarm4', { + metric: testMetric, + threshold: 100000, + evaluationPeriods: 3, + }); + + const alarmRule = AlarmRule.anyOf( + AlarmRule.allOf( + AlarmRule.anyOf( + alarm1, + AlarmRule.fromAlarm(alarm2, AlarmState.OK), + alarm3, + ), + AlarmRule.not(AlarmRule.fromAlarm(alarm4, AlarmState.INSUFFICIENT_DATA)), + ), + AlarmRule.fromBoolean(false), + ); + + new CompositeAlarm(stack, 'CompositeAlarm', { + alarmRule, + }); + + expect(stack).to(haveResource('AWS::CloudWatch::CompositeAlarm', { + AlarmName: 'CompositeAlarm', + AlarmRule: { + 'Fn::Join': [ + '', + [ + '(((ALARM(', + { + 'Fn::GetAtt': [ + 'Alarm1F9009D71', + 'Arn', + ], + }, + ') OR OK(', + { + 'Fn::GetAtt': [ + 'Alarm2A7122E13', + 'Arn', + ], + }, + ') OR ALARM(', + { + 'Fn::GetAtt': [ + 'Alarm32341D8D9', + 'Arn', + ], + }, + ')) AND (NOT (INSUFFICIENT_DATA(', + { + 'Fn::GetAtt': [ + 'Alarm4671832C8', + 'Arn', + ], + }, + ')))) OR FALSE)', + ], + ], + }, + })); + + test.done(); + }, + +}; From 06afc80a2f0c862074c047651c07d009bd740663 Mon Sep 17 00:00:00 2001 From: Rich Nahra <34785097+rich-nahra@users.noreply.github.com> Date: Mon, 22 Jun 2020 05:53:40 -0400 Subject: [PATCH 03/21] docs(rds): updates subnet selection (#8527) CDK examples should follow well architected best practices whenever possible. In this case, the CDK is placing RDS on public subnet which most would never do. *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-rds/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-rds/README.md b/packages/@aws-cdk/aws-rds/README.md index 5ce914f99f47a..64d7b7b19528b 100644 --- a/packages/@aws-cdk/aws-rds/README.md +++ b/packages/@aws-cdk/aws-rds/README.md @@ -28,7 +28,7 @@ const cluster = new DatabaseCluster(this, 'Database', { instanceProps: { instanceType: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.SMALL), vpcSubnets: { - subnetType: ec2.SubnetType.PUBLIC, + subnetType: ec2.SubnetType.PRIVATE, }, vpc } From a186c24918fddc697270b794b6603add5a47e947 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Mon, 22 Jun 2020 12:38:39 +0200 Subject: [PATCH 04/21] fix(cli): crash on tiny reported terminal width (#8675) Fix a monitor crash that happens when a narrow terminal width is reported, leading the calculation for progress bar filler to go negative. Fixes #8667. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../lib/api/util/cloudformation/stack-activity-monitor.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/aws-cdk/lib/api/util/cloudformation/stack-activity-monitor.ts b/packages/aws-cdk/lib/api/util/cloudformation/stack-activity-monitor.ts index ce1ed37b59876..c1ac8a8194120 100644 --- a/packages/aws-cdk/lib/api/util/cloudformation/stack-activity-monitor.ts +++ b/packages/aws-cdk/lib/api/util/cloudformation/stack-activity-monitor.ts @@ -490,7 +490,7 @@ export class CurrentActivityPrinter extends ActivityPrinterBase { const lines = []; // Add a progress bar at the top - const progressWidth = Math.min((this.block.width ?? 80) - PROGRESSBAR_EXTRA_SPACE - 1, MAX_PROGRESSBAR_WIDTH); + const progressWidth = Math.max(Math.min((this.block.width ?? 80) - PROGRESSBAR_EXTRA_SPACE - 1, MAX_PROGRESSBAR_WIDTH), MIN_PROGRESSBAR_WIDTH); const prog = this.progressBar(progressWidth); if (prog) { lines.push(' ' + prog, ''); @@ -550,12 +550,13 @@ export class CurrentActivityPrinter extends ActivityPrinterBase { private progressBar(width: number) { if (!this.resourcesTotal) { return ''; } const fraction = Math.min(this.resourcesDone / this.resourcesTotal, 1); - const chars = (width - 2) * fraction; + const innerWidth = Math.max(1, width - 2); + const chars = innerWidth * fraction; const remainder = chars - Math.floor(chars); const fullChars = FULL_BLOCK.repeat(Math.floor(chars)); const partialChar = PARTIAL_BLOCK[Math.floor(remainder * PARTIAL_BLOCK.length)]; - const filler = '·'.repeat(width - 2 - Math.floor(chars) - (partialChar ? 1 : 0)); + const filler = '·'.repeat(innerWidth - Math.floor(chars) - (partialChar ? 1 : 0)); const color = this.rollingBack ? colors.yellow : colors.green; @@ -572,6 +573,7 @@ export class CurrentActivityPrinter extends ActivityPrinterBase { const FULL_BLOCK = '█'; const PARTIAL_BLOCK = ['', '▏', '▎', '▍', '▌', '▋', '▊', '▉']; const MAX_PROGRESSBAR_WIDTH = 60; +const MIN_PROGRESSBAR_WIDTH = 10; const PROGRESSBAR_EXTRA_SPACE = 2 /* leading spaces */ + 2 /* brackets */ + 4 /* progress number decoration */ + 6 /* 2 progress numbers up to 999 */; function colorFromStatusResult(status?: string) { From fe71364b6cd8274e937cc2dc9185249dcbbb9388 Mon Sep 17 00:00:00 2001 From: kaizen3031593 <36202692+kaizen3031593@users.noreply.github.com> Date: Mon, 22 Jun 2020 12:35:37 -0400 Subject: [PATCH 05/21] feat(stepfunctions): grant APIs for state machine construct (#8486) This PR closes #5933. It adds additional grant APIs to supplement `grantStartExecution()`. API additions to `state-machine.ts`: - `grantRead()` method that grants read access to a role. - `grantTaskResponse()` method that grants task response access to a role. - `grantExecution()` method that allows user to specify what action to map onto all executions. - `grant()` method that allows user to specify what action to map onto the state machine. API additions to `activity.ts`: - `grant()` method that allows user to specify what action to map onto the activity. The idea behind these API methods is to mimic the convention of other `grant()` APIs in other modules. This is slightly more difficult with Step Functions because of the multiple resources that IAM actions can map onto. The rationale is that `grant()` in `state-machine.ts` will handle all custom permissions for the state machine, `grantExecution()` in `state-machine.ts` will handle all custom permissions for the executions, and `grant()` in `activity.ts` will handle all custom permissions for activities. `grantRead()` and `grantTaskResponse()` are convenience APIs that were specified in the original issue for this feature. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-stepfunctions/README.md | 132 +++++++ .../aws-stepfunctions/lib/activity.ts | 15 + .../aws-stepfunctions/lib/state-machine.ts | 116 +++++- .../aws-stepfunctions/test/activity.test.ts | 30 ++ .../test/integ.custom-state.expected.json | 4 +- .../test/integ.state-machine.expected.json | 148 +++++++ .../test/integ.state-machine.ts | 28 ++ .../test/state-machine-resources.test.ts | 360 +++++++++++++++++- 8 files changed, 819 insertions(+), 14 deletions(-) create mode 100644 packages/@aws-cdk/aws-stepfunctions/test/integ.state-machine.expected.json create mode 100644 packages/@aws-cdk/aws-stepfunctions/test/integ.state-machine.ts diff --git a/packages/@aws-cdk/aws-stepfunctions/README.md b/packages/@aws-cdk/aws-stepfunctions/README.md index 0cb349358ba0f..e8ed1e4fffef1 100644 --- a/packages/@aws-cdk/aws-stepfunctions/README.md +++ b/packages/@aws-cdk/aws-stepfunctions/README.md @@ -458,6 +458,22 @@ const activity = new stepfunctions.Activity(this, 'Activity'); new cdk.CfnOutput(this, 'ActivityArn', { value: activity.activityArn }); ``` +### Activity-Level Permissions + +Granting IAM permissions to an activity can be achieved by calling the `grant(principal, actions)` API: + +```ts +const activity = new stepfunctions.Activity(this, 'Activity'); + +const role = new iam.Role(stack, 'Role', { + assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'), +}); + +activity.grant(role, 'states:SendTaskSuccess'); +``` + +This will grant the IAM principal the specified actions onto the activity. + ## Metrics `Task` object expose various metrics on the execution of that particular task. For example, @@ -508,6 +524,122 @@ new stepfunctions.StateMachine(stack, 'MyStateMachine', { }); ``` +## State Machine Permission Grants + +IAM roles, users, or groups which need to be able to work with a State Machine should be granted IAM permissions. + +Any object that implements the `IGrantable` interface (has an associated principal) can be granted permissions by calling: + +- `stateMachine.grantStartExecution(principal)` - grants the principal the ability to execute the state machine +- `stateMachine.grantRead(principal)` - grants the principal read access +- `stateMachine.grantTaskResponse(principal)` - grants the principal the ability to send task tokens to the state machine +- `stateMachine.grantExecution(principal, actions)` - grants the principal execution-level permissions for the IAM actions specified +- `stateMachine.grant(principal, actions)` - grants the principal state-machine-level permissions for the IAM actions specified + +### Start Execution Permission + +Grant permission to start an execution of a state machine by calling the `grantStartExecution()` API. + +```ts +const role = new iam.Role(stack, 'Role', { + assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'), +}); + +const stateMachine = new stepfunction.StateMachine(stack, 'StateMachine', { + definition, +}); + +// Give role permission to start execution of state machine +stateMachine.grantStartExecution(role); +``` + +The following permission is provided to a service principal by the `grantStartExecution()` API: + +- `states:StartExecution` - to state machine + +### Read Permissions + +Grant `read` access to a state machine by calling the `grantRead()` API. + +```ts +const role = new iam.Role(stack, 'Role', { + assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'), +}); + +const stateMachine = new stepfunction.StateMachine(stack, 'StateMachine', { + definition, +}); + +// Give role read access to state machine +stateMachine.grantRead(role); +``` + +The following read permissions are provided to a service principal by the `grantRead()` API: + +- `states:ListExecutions` - to state machine +- `states:ListStateMachines` - to state machine +- `states:DescribeExecution` - to executions +- `states:DescribeStateMachineForExecution` - to executions +- `states:GetExecutionHistory` - to executions +- `states:ListActivities` - to `*` +- `states:DescribeStateMachine` - to `*` +- `states:DescribeActivity` - to `*` + +### Task Response Permissions + +Grant permission to allow task responses to a state machine by calling the `grantTaskResponse()` API: + +```ts +const role = new iam.Role(stack, 'Role', { + assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'), +}); + +const stateMachine = new stepfunction.StateMachine(stack, 'StateMachine', { + definition, +}); + +// Give role task response permissions to the state machine +stateMachine.grantTaskResponse(role); +``` + +The following read permissions are provided to a service principal by the `grantRead()` API: + +- `states:SendTaskSuccess` - to state machine +- `states:SendTaskFailure` - to state machine +- `states:SendTaskHeartbeat` - to state machine + +### Execution-level Permissions + +Grant execution-level permissions to a state machine by calling the `grantExecution()` API: + +```ts +const role = new iam.Role(stack, 'Role', { + assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'), +}); + +const stateMachine = new stepfunction.StateMachine(stack, 'StateMachine', { + definition, +}); + +// Give role permission to get execution history of ALL executions for the state machine +stateMachine.grantExecution(role, 'states:GetExecutionHistory'); +``` + +### Custom Permissions + +You can add any set of permissions to a state machine by calling the `grant()` API. + +```ts +const user = new iam.User(stack, 'MyUser'); + +const stateMachine = new stepfunction.StateMachine(stack, 'StateMachine', { + definition, +}); + +//give user permission to send task success to the state machine +stateMachine.grant(user, 'states:SendTaskSuccess'); +``` + ## Import Any Step Functions state machine that has been created outside the stack can be imported diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/activity.ts b/packages/@aws-cdk/aws-stepfunctions/lib/activity.ts index 97bcbb070b06a..ef97f45e6f8b3 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/activity.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/activity.ts @@ -1,4 +1,5 @@ import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; +import * as iam from '@aws-cdk/aws-iam'; import { Construct, IResource, Lazy, Resource, Stack } from '@aws-cdk/core'; import { CfnActivity } from './stepfunctions.generated'; @@ -73,6 +74,20 @@ export class Activity extends Resource implements IActivity { this.activityName = this.getResourceNameAttribute(resource.attrName); } + /** + * Grant the given identity permissions on this Activity + * + * @param identity The principal + * @param actions The list of desired actions + */ + public grant(identity: iam.IGrantable, ...actions: string[]) { + return iam.Grant.addToPrincipal({ + grantee: identity, + actions, + resourceArns: [this.activityArn], + }); + } + /** * Return the given named metric for this Activity * diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/state-machine.ts b/packages/@aws-cdk/aws-stepfunctions/lib/state-machine.ts index ab1a6f34d86a4..a6632a7e5d35d 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/state-machine.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/state-machine.ts @@ -1,7 +1,7 @@ import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; import * as iam from '@aws-cdk/aws-iam'; import * as logs from '@aws-cdk/aws-logs'; -import { Construct, Duration, IResource, Resource, Stack } from '@aws-cdk/core'; +import { Arn, Construct, Duration, IResource, Resource, Stack } from '@aws-cdk/core'; import { StateGraph } from './state-graph'; import { CfnStateMachine } from './stepfunctions.generated'; import { IChainable } from './types'; @@ -124,6 +124,7 @@ export interface StateMachineProps { * A new or imported state machine. */ abstract class StateMachineBase extends Resource implements IStateMachine { + /** * Import a state machine */ @@ -131,7 +132,6 @@ abstract class StateMachineBase extends Resource implements IStateMachine { class Import extends StateMachineBase { public readonly stateMachineArn = stateMachineArn; } - return new Import(scope, id); } @@ -148,6 +148,88 @@ abstract class StateMachineBase extends Resource implements IStateMachine { resourceArns: [this.stateMachineArn], }); } + + /** + * Grant the given identity permissions to read results from state + * machine. + */ + public grantRead(identity: iam.IGrantable): iam.Grant { + iam.Grant.addToPrincipal({ + grantee: identity, + actions: [ + 'states:ListExecutions', + 'states:ListStateMachines', + ], + resourceArns: [this.stateMachineArn], + }); + iam.Grant.addToPrincipal({ + grantee: identity, + actions: [ + 'states:DescribeExecution', + 'states:DescribeStateMachineForExecution', + 'states:GetExecutionHistory', + ], + resourceArns: [`${this.executionArn()}:*`], + }); + return iam.Grant.addToPrincipal({ + grantee: identity, + actions: [ + 'states:ListActivities', + 'states:DescribeStateMachine', + 'states:DescribeActivity', + ], + resourceArns: ['*'], + }); + } + + /** + * Grant the given identity task response permissions on a state machine + */ + public grantTaskResponse(identity: iam.IGrantable): iam.Grant { + return iam.Grant.addToPrincipal({ + grantee: identity, + actions: [ + 'states:SendTaskSuccess', + 'states:SendTaskFailure', + 'states:SendTaskHeartbeat', + ], + resourceArns: [this.stateMachineArn], + }); + } + + /** + * Grant the given identity permissions on all executions of the state machine + */ + public grantExecution(identity: iam.IGrantable, ...actions: string[]) { + return iam.Grant.addToPrincipal({ + grantee: identity, + actions, + resourceArns: [`${this.executionArn()}:*`], + }); + } + + /** + * Grant the given identity custom permissions + */ + public grant(identity: iam.IGrantable, ...actions: string[]): iam.Grant { + return iam.Grant.addToPrincipal({ + grantee: identity, + actions, + resourceArns: [this.stateMachineArn], + }); + } + + /** + * Returns the pattern for the execution ARN's of the state machine + */ + private executionArn(): string { + return Stack.of(this).formatArn({ + resource: 'execution', + service: 'states', + resourceName: Arn.parse(this.stateMachineArn, ':').resourceName, + sep: ':', + }); + } } /** @@ -341,4 +423,34 @@ export interface IStateMachine extends IResource { * @param identity The principal */ grantStartExecution(identity: iam.IGrantable): iam.Grant; + + /** + * Grant the given identity read permissions for this state machine + * + * @param identity The principal + */ + grantRead(identity: iam.IGrantable): iam.Grant; + + /** + * Grant the given identity read permissions for this state machine + * + * @param identity The principal + */ + grantTaskResponse(identity: iam.IGrantable): iam.Grant; + + /** + * Grant the given identity permissions for all executions of a state machine + * + * @param identity The principal + * @param actions The list of desired actions + */ + grantExecution(identity: iam.IGrantable, ...actions: string[]): iam.Grant; + + /** + * Grant the given identity custom permissions + * + * @param identity The principal + * @param actions The list of desired actions + */ + grant(identity: iam.IGrantable, ...actions: string[]): iam.Grant; } diff --git a/packages/@aws-cdk/aws-stepfunctions/test/activity.test.ts b/packages/@aws-cdk/aws-stepfunctions/test/activity.test.ts index 7c637b22a1fdf..b2e8b4dca9b6e 100644 --- a/packages/@aws-cdk/aws-stepfunctions/test/activity.test.ts +++ b/packages/@aws-cdk/aws-stepfunctions/test/activity.test.ts @@ -1,4 +1,6 @@ +import { arrayWith, objectLike } from '@aws-cdk/assert'; import '@aws-cdk/assert/jest'; +import * as iam from '@aws-cdk/aws-iam'; import * as cdk from '@aws-cdk/core'; import * as stepfunctions from '../lib'; @@ -41,4 +43,32 @@ describe('Activity', () => { statistic: 'Sum', }); }); + + test('Activity can grant permissions to a role', () => { + // GIVEN + const stack = new cdk.Stack(); + + const role = new iam.Role(stack, 'Role', { + assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'), + }); + + const activity = new stepfunctions.Activity(stack, 'Activity'); + + // WHEN + activity.grant(role, 'states:SendTaskSuccess'); + + // THEN + expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + PolicyDocument: { + Statement: arrayWith(objectLike({ + Action: 'states:SendTaskSuccess', + Effect: 'Allow', + Resource: { + Ref: 'Activity04690B0A', + }, + })), + }, + }); + + }); }); diff --git a/packages/@aws-cdk/aws-stepfunctions/test/integ.custom-state.expected.json b/packages/@aws-cdk/aws-stepfunctions/test/integ.custom-state.expected.json index 311e28c953615..d89acdeed9123 100644 --- a/packages/@aws-cdk/aws-stepfunctions/test/integ.custom-state.expected.json +++ b/packages/@aws-cdk/aws-stepfunctions/test/integ.custom-state.expected.json @@ -31,13 +31,13 @@ "StateMachine2E01A3A5": { "Type": "AWS::StepFunctions::StateMachine", "Properties": { - "DefinitionString": "{\"StartAt\":\"my custom task\",\"States\":{\"my custom task\":{\"Next\":\"final step\",\"Type\":\"Task\",\"Resource\":\"arn:aws:states:::dynamodb:putItem\",\"Parameters\":{\"TableName\":\"my-cool-table\",\"Item\":{\"id\":{\"S\":\"my-entry\"}}},\"ResultPath\":null},\"final step\":{\"Type\":\"Pass\",\"End\":true}},\"TimeoutSeconds\":30}", "RoleArn": { "Fn::GetAtt": [ "StateMachineRoleB840431D", "Arn" ] - } + }, + "DefinitionString": "{\"StartAt\":\"my custom task\",\"States\":{\"my custom task\":{\"Next\":\"final step\",\"Type\":\"Task\",\"Resource\":\"arn:aws:states:::dynamodb:putItem\",\"Parameters\":{\"TableName\":\"my-cool-table\",\"Item\":{\"id\":{\"S\":\"my-entry\"}}},\"ResultPath\":null},\"final step\":{\"Type\":\"Pass\",\"End\":true}},\"TimeoutSeconds\":30}" }, "DependsOn": [ "StateMachineRoleB840431D" diff --git a/packages/@aws-cdk/aws-stepfunctions/test/integ.state-machine.expected.json b/packages/@aws-cdk/aws-stepfunctions/test/integ.state-machine.expected.json new file mode 100644 index 0000000000000..3899a0ed71b99 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions/test/integ.state-machine.expected.json @@ -0,0 +1,148 @@ +{ + "Resources": { + "Role1ABCC5F0": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "RoleDefaultPolicy5FFB7DAB": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "states:ListExecutions", + "states:ListStateMachines" + ], + "Effect": "Allow", + "Resource": { + "Ref": "StateMachine2E01A3A5" + } + }, + { + "Action": [ + "states:DescribeExecution", + "states:DescribeStateMachineForExecution", + "states:GetExecutionHistory" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":states:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":execution:", + { + "Fn::Select": [ + 6, + { + "Fn::Split": [ + ":", + { + "Ref": "StateMachine2E01A3A5" + } + ] + } + ] + }, + ":*" + ] + ] + } + }, + { + "Action": [ + "states:ListActivities", + "states:DescribeStateMachine", + "states:DescribeActivity" + ], + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": "states:SendTaskSuccess", + "Effect": "Allow", + "Resource": { + "Ref": "StateMachine2E01A3A5" + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "RoleDefaultPolicy5FFB7DAB", + "Roles": [ + { + "Ref": "Role1ABCC5F0" + } + ] + } + }, + "StateMachineRoleB840431D": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": { + "Fn::Join": [ + "", + [ + "states.", + { + "Ref": "AWS::Region" + }, + ".amazonaws.com" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "StateMachine2E01A3A5": { + "Type": "AWS::StepFunctions::StateMachine", + "Properties": { + "RoleArn": { + "Fn::GetAtt": [ + "StateMachineRoleB840431D", + "Arn" + ] + }, + "DefinitionString": "{\"StartAt\":\"wait time\",\"States\":{\"wait time\":{\"Type\":\"Wait\",\"SecondsPath\":\"$.waitSeconds\",\"End\":true}}}" + }, + "DependsOn": [ + "StateMachineRoleB840431D" + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions/test/integ.state-machine.ts b/packages/@aws-cdk/aws-stepfunctions/test/integ.state-machine.ts new file mode 100644 index 0000000000000..74d8d02eb7f2d --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions/test/integ.state-machine.ts @@ -0,0 +1,28 @@ +import * as iam from '@aws-cdk/aws-iam'; +import * as cdk from '@aws-cdk/core'; +import * as sfn from '../lib'; +/* + * Stack verification steps: + * + * -- aws stepfunctions describe-state-machine --state-machine-arn has a status of `ACTIVE` + * -- aws iam get-role-policy --role-name --policy-name has all actions mapped to respective resources. + */ +const app = new cdk.App(); +const stack = new cdk.Stack(app, 'aws-stepfunctions-integ'); + +const wait = new sfn.Wait(stack, 'wait time', { + time: sfn.WaitTime.secondsPath('$.waitSeconds'), +}); + +const role = new iam.Role(stack, 'Role', { + assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'), +}); + +const stateMachine = new sfn.StateMachine(stack, 'StateMachine', { + definition: wait, +}); + +stateMachine.grantRead(role); +stateMachine.grant(role, 'states:SendTaskSuccess'); + +app.synth(); diff --git a/packages/@aws-cdk/aws-stepfunctions/test/state-machine-resources.test.ts b/packages/@aws-cdk/aws-stepfunctions/test/state-machine-resources.test.ts index 012a193087b9b..67fcd36752f80 100644 --- a/packages/@aws-cdk/aws-stepfunctions/test/state-machine-resources.test.ts +++ b/packages/@aws-cdk/aws-stepfunctions/test/state-machine-resources.test.ts @@ -1,4 +1,4 @@ -import { ResourcePart } from '@aws-cdk/assert'; +import { arrayWith, objectLike, ResourcePart } from '@aws-cdk/assert'; import '@aws-cdk/assert/jest'; import * as iam from '@aws-cdk/aws-iam'; import * as cdk from '@aws-cdk/core'; @@ -183,25 +183,243 @@ describe('State Machine Resources', () => { stateMachine.grantStartExecution(role); // THEN - expect(stack).toHaveResource('AWS::IAM::Policy', { + expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + PolicyDocument: { + Statement: arrayWith(objectLike({ + Action: 'states:StartExecution', + Effect: 'Allow', + Resource: { + Ref: 'StateMachine2E01A3A5', + }, + })), + }, + }); + + }), + + test('Created state machine can grant read access to a role', () => { + // GIVEN + const stack = new cdk.Stack(); + const task = new stepfunctions.Task(stack, 'Task', { + task: { + bind: () => ({ resourceArn: 'resource' }), + }, + }); + const stateMachine = new stepfunctions.StateMachine(stack, 'StateMachine', { + definition: task, + }); + const role = new iam.Role(stack, 'Role', { + assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'), + }); + + // WHEN + stateMachine.grantRead(role); + + // THEN + expect(stack).toHaveResourceLike('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { - Action: 'states:StartExecution', + Action: [ + 'states:ListExecutions', + 'states:ListStateMachines', + ], + Effect: 'Allow', + Resource: { + Ref: 'StateMachine2E01A3A5', + }, + }, + { + Action: [ + 'states:DescribeExecution', + 'states:DescribeStateMachineForExecution', + 'states:GetExecutionHistory', + ], + Effect: 'Allow', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':states:', + { + Ref: 'AWS::Region', + }, + ':', + { + Ref: 'AWS::AccountId', + }, + ':execution:', + { + 'Fn::Select': [ + 6, + { + 'Fn::Split': [ + ':', + { + Ref: 'StateMachine2E01A3A5', + }, + ], + }, + ], + }, + ':*', + ], + ], + }, + }, + { + Action: [ + 'states:ListActivities', + 'states:DescribeStateMachine', + 'states:DescribeActivity', + ], + Effect: 'Allow', + Resource: '*', + }, + ], + }}, + ); + + }), + + test('Created state machine can grant task response actions to the state machine', () => { + // GIVEN + const stack = new cdk.Stack(); + const task = new stepfunctions.Task(stack, 'Task', { + task: { + bind: () => ({ resourceArn: 'resource' }), + }, + }); + const stateMachine = new stepfunctions.StateMachine(stack, 'StateMachine', { + definition: task, + }); + const role = new iam.Role(stack, 'Role', { + assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'), + }); + + // WHEN + stateMachine.grantTaskResponse(role); + + // THEN + expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: [ + 'states:SendTaskSuccess', + 'states:SendTaskFailure', + 'states:SendTaskHeartbeat', + ], + Effect: 'Allow', + Resource: { + Ref: 'StateMachine2E01A3A5', + }, + }, + ], + }, + }); + }), + + test('Created state machine can grant actions to the executions', () => { + // GIVEN + const stack = new cdk.Stack(); + const task = new stepfunctions.Task(stack, 'Task', { + task: { + bind: () => ({ resourceArn: 'resource' }), + }, + }); + const stateMachine = new stepfunctions.StateMachine(stack, 'StateMachine', { + definition: task, + }); + const role = new iam.Role(stack, 'Role', { + assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'), + }); + + // WHEN + stateMachine.grantExecution(role, 'states:GetExecutionHistory'); + + // THEN + expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: 'states:GetExecutionHistory', + Effect: 'Allow', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':states:', + { + Ref: 'AWS::Region', + }, + ':', + { + Ref: 'AWS::AccountId', + }, + ':execution:', + { + 'Fn::Select': [ + 6, + { + 'Fn::Split': [ + ':', + { + Ref: 'StateMachine2E01A3A5', + }, + ], + }, + ], + }, + ':*', + ], + ], + }, + }, + ], + }, + }); + }), + + test('Created state machine can grant actions to a role', () => { + // GIVEN + const stack = new cdk.Stack(); + const task = new stepfunctions.Task(stack, 'Task', { + task: { + bind: () => ({ resourceArn: 'resource' }), + }, + }); + const stateMachine = new stepfunctions.StateMachine(stack, 'StateMachine', { + definition: task, + }); + const role = new iam.Role(stack, 'Role', { + assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'), + }); + + // WHEN + stateMachine.grant(role, 'states:ListExecution'); + + // THEN + expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: 'states:ListExecution', Effect: 'Allow', Resource: { Ref: 'StateMachine2E01A3A5', }, }, ], - Version: '2012-10-17', }, - PolicyName: 'RoleDefaultPolicy5FFB7DAB', - Roles: [ - { - Ref: 'Role1ABCC5F0', - }, - ], }); }), @@ -239,6 +457,128 @@ describe('State Machine Resources', () => { }); }), + test('Imported state machine can grant read access to a role', () => { + // GIVEN + const stack = new cdk.Stack(); + const stateMachineArn = 'arn:aws:states:::my-state-machine'; + const stateMachine = stepfunctions.StateMachine.fromStateMachineArn(stack, 'StateMachine', stateMachineArn); + const role = new iam.Role(stack, 'Role', { + assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'), + }); + + // WHEN + stateMachine.grantRead(role); + + // THEN + expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: [ + 'states:ListExecutions', + 'states:ListStateMachines', + ], + Effect: 'Allow', + Resource: stateMachineArn, + }, + { + Action: [ + 'states:DescribeExecution', + 'states:DescribeStateMachineForExecution', + 'states:GetExecutionHistory', + ], + Effect: 'Allow', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':states:', + { + Ref: 'AWS::Region', + }, + ':', + { + Ref: 'AWS::AccountId', + }, + ':execution:*', + ], + ], + }, + }, + { + Action: [ + 'states:ListActivities', + 'states:DescribeStateMachine', + 'states:DescribeActivity', + ], + Effect: 'Allow', + Resource: '*', + }, + ], + }}, + ); + }), + + test('Imported state machine can task response permissions to the state machine', () => { + // GIVEN + const stack = new cdk.Stack(); + const stateMachineArn = 'arn:aws:states:::my-state-machine'; + const stateMachine = stepfunctions.StateMachine.fromStateMachineArn(stack, 'StateMachine', stateMachineArn); + const role = new iam.Role(stack, 'Role', { + assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'), + }); + + // WHEN + stateMachine.grantTaskResponse(role); + + // THEN + expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: [ + 'states:SendTaskSuccess', + 'states:SendTaskFailure', + 'states:SendTaskHeartbeat', + ], + Effect: 'Allow', + Resource: stateMachineArn, + }, + ], + }, + }); + }), + + test('Imported state machine can grant access to a role', () => { + // GIVEN + const stack = new cdk.Stack(); + const stateMachineArn = 'arn:aws:states:::my-state-machine'; + const stateMachine = stepfunctions.StateMachine.fromStateMachineArn(stack, 'StateMachine', stateMachineArn); + const role = new iam.Role(stack, 'Role', { + assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'), + }); + + // WHEN + stateMachine.grant(role, 'states:ListExecution'); + + // THEN + expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: 'states:ListExecution', + Effect: 'Allow', + Resource: stateMachine.stateMachineArn, + }, + ], + }, + }); + }), + test('Pass should render InputPath / Parameters / OutputPath correctly', () => { // GIVEN const stack = new cdk.Stack(); From b27d5a3ca76e0ce23d8e3ff5c57241e9d4bcff5d Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 22 Jun 2020 18:20:16 +0000 Subject: [PATCH 06/21] chore(deps): bump aws-sdk from 2.699.0 to 2.701.0 (#8682) Bumps [aws-sdk](https://github.com/aws/aws-sdk-js) from 2.699.0 to 2.701.0. - [Release notes](https://github.com/aws/aws-sdk-js/releases) - [Changelog](https://github.com/aws/aws-sdk-js/blob/master/CHANGELOG.md) - [Commits](https://github.com/aws/aws-sdk-js/compare/v2.699.0...v2.701.0) Signed-off-by: dependabot-preview[bot] Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com> --- packages/@aws-cdk/aws-cloudfront/package.json | 2 +- packages/@aws-cdk/aws-cloudtrail/package.json | 2 +- packages/@aws-cdk/aws-codebuild/package.json | 2 +- packages/@aws-cdk/aws-codecommit/package.json | 2 +- packages/@aws-cdk/aws-dynamodb/package.json | 2 +- packages/@aws-cdk/aws-eks/package.json | 2 +- packages/@aws-cdk/aws-events-targets/package.json | 2 +- packages/@aws-cdk/aws-lambda/package.json | 2 +- packages/@aws-cdk/aws-route53/package.json | 2 +- packages/@aws-cdk/aws-sqs/package.json | 2 +- packages/@aws-cdk/custom-resources/package.json | 2 +- packages/aws-cdk/package.json | 2 +- packages/cdk-assets/package.json | 2 +- yarn.lock | 8 ++++---- 14 files changed, 17 insertions(+), 17 deletions(-) diff --git a/packages/@aws-cdk/aws-cloudfront/package.json b/packages/@aws-cdk/aws-cloudfront/package.json index bf719b1b3d869..e3674e2a78d23 100644 --- a/packages/@aws-cdk/aws-cloudfront/package.json +++ b/packages/@aws-cdk/aws-cloudfront/package.json @@ -64,7 +64,7 @@ "devDependencies": { "@aws-cdk/assert": "0.0.0", "@types/nodeunit": "^0.0.31", - "aws-sdk": "^2.699.0", + "aws-sdk": "^2.701.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-cloudtrail/package.json b/packages/@aws-cdk/aws-cloudtrail/package.json index 3fc5308269de3..313e07c9ec980 100644 --- a/packages/@aws-cdk/aws-cloudtrail/package.json +++ b/packages/@aws-cdk/aws-cloudtrail/package.json @@ -64,7 +64,7 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", - "aws-sdk": "^2.699.0", + "aws-sdk": "^2.701.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-codebuild/package.json b/packages/@aws-cdk/aws-codebuild/package.json index 6f986a4237889..32f594f3a465b 100644 --- a/packages/@aws-cdk/aws-codebuild/package.json +++ b/packages/@aws-cdk/aws-codebuild/package.json @@ -70,7 +70,7 @@ "@aws-cdk/aws-sns": "0.0.0", "@aws-cdk/aws-sqs": "0.0.0", "@types/nodeunit": "^0.0.31", - "aws-sdk": "^2.699.0", + "aws-sdk": "^2.701.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-codecommit/package.json b/packages/@aws-cdk/aws-codecommit/package.json index 41dd725f755fc..9ba29e3bd18b5 100644 --- a/packages/@aws-cdk/aws-codecommit/package.json +++ b/packages/@aws-cdk/aws-codecommit/package.json @@ -70,7 +70,7 @@ "@aws-cdk/assert": "0.0.0", "@aws-cdk/aws-sns": "0.0.0", "@types/nodeunit": "^0.0.31", - "aws-sdk": "^2.699.0", + "aws-sdk": "^2.701.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-dynamodb/package.json b/packages/@aws-cdk/aws-dynamodb/package.json index c5dc0819124b4..6f73d8ad25dfc 100644 --- a/packages/@aws-cdk/aws-dynamodb/package.json +++ b/packages/@aws-cdk/aws-dynamodb/package.json @@ -65,7 +65,7 @@ "devDependencies": { "@aws-cdk/assert": "0.0.0", "@types/jest": "^26.0.0", - "aws-sdk": "^2.699.0", + "aws-sdk": "^2.701.0", "aws-sdk-mock": "^5.1.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", diff --git a/packages/@aws-cdk/aws-eks/package.json b/packages/@aws-cdk/aws-eks/package.json index 4ea262000916b..9ad0aaff863bd 100644 --- a/packages/@aws-cdk/aws-eks/package.json +++ b/packages/@aws-cdk/aws-eks/package.json @@ -64,7 +64,7 @@ "devDependencies": { "@aws-cdk/assert": "0.0.0", "@types/nodeunit": "^0.0.31", - "aws-sdk": "^2.699.0", + "aws-sdk": "^2.701.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-events-targets/package.json b/packages/@aws-cdk/aws-events-targets/package.json index 08853301a4c63..925ff1d5d504f 100644 --- a/packages/@aws-cdk/aws-events-targets/package.json +++ b/packages/@aws-cdk/aws-events-targets/package.json @@ -68,7 +68,7 @@ "devDependencies": { "@aws-cdk/assert": "0.0.0", "@aws-cdk/aws-codecommit": "0.0.0", - "aws-sdk": "^2.699.0", + "aws-sdk": "^2.701.0", "aws-sdk-mock": "^5.1.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", diff --git a/packages/@aws-cdk/aws-lambda/package.json b/packages/@aws-cdk/aws-lambda/package.json index d46b25cee471c..2e7885cc6b2c5 100644 --- a/packages/@aws-cdk/aws-lambda/package.json +++ b/packages/@aws-cdk/aws-lambda/package.json @@ -71,7 +71,7 @@ "@types/lodash": "^4.14.155", "@types/nodeunit": "^0.0.31", "@types/sinon": "^9.0.4", - "aws-sdk": "^2.699.0", + "aws-sdk": "^2.701.0", "aws-sdk-mock": "^5.1.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", diff --git a/packages/@aws-cdk/aws-route53/package.json b/packages/@aws-cdk/aws-route53/package.json index d406063cea940..b5d0d495b18a0 100644 --- a/packages/@aws-cdk/aws-route53/package.json +++ b/packages/@aws-cdk/aws-route53/package.json @@ -64,7 +64,7 @@ "devDependencies": { "@aws-cdk/assert": "0.0.0", "@types/nodeunit": "^0.0.31", - "aws-sdk": "^2.699.0", + "aws-sdk": "^2.701.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-sqs/package.json b/packages/@aws-cdk/aws-sqs/package.json index a6f5971457b23..daee0dbdfaff9 100644 --- a/packages/@aws-cdk/aws-sqs/package.json +++ b/packages/@aws-cdk/aws-sqs/package.json @@ -65,7 +65,7 @@ "@aws-cdk/assert": "0.0.0", "@aws-cdk/aws-s3": "0.0.0", "@types/nodeunit": "^0.0.31", - "aws-sdk": "^2.699.0", + "aws-sdk": "^2.701.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/custom-resources/package.json b/packages/@aws-cdk/custom-resources/package.json index b9e70560fa7d1..8f42ec3114691 100644 --- a/packages/@aws-cdk/custom-resources/package.json +++ b/packages/@aws-cdk/custom-resources/package.json @@ -73,7 +73,7 @@ "@types/aws-lambda": "^8.10.39", "@types/fs-extra": "^8.1.0", "@types/sinon": "^9.0.4", - "aws-sdk": "^2.699.0", + "aws-sdk": "^2.701.0", "aws-sdk-mock": "^5.1.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", diff --git a/packages/aws-cdk/package.json b/packages/aws-cdk/package.json index 026594e1ad5b2..04a6e35d4364f 100644 --- a/packages/aws-cdk/package.json +++ b/packages/aws-cdk/package.json @@ -72,7 +72,7 @@ "@aws-cdk/cx-api": "0.0.0", "@aws-cdk/region-info": "0.0.0", "archiver": "^4.0.1", - "aws-sdk": "^2.699.0", + "aws-sdk": "^2.701.0", "camelcase": "^6.0.0", "cdk-assets": "0.0.0", "colors": "^1.4.0", diff --git a/packages/cdk-assets/package.json b/packages/cdk-assets/package.json index ce045cd8738b7..33f19b00ead30 100644 --- a/packages/cdk-assets/package.json +++ b/packages/cdk-assets/package.json @@ -47,7 +47,7 @@ "@aws-cdk/cdk-assets-schema": "0.0.0", "@aws-cdk/cx-api": "0.0.0", "archiver": "^4.0.1", - "aws-sdk": "^2.699.0", + "aws-sdk": "^2.701.0", "glob": "^7.1.6", "yargs": "^15.3.1" }, diff --git a/yarn.lock b/yarn.lock index 70c63ca45eb21..6ed07a3bfb59d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2123,10 +2123,10 @@ aws-sdk-mock@^5.1.0: sinon "^9.0.1" traverse "^0.6.6" -aws-sdk@^2.637.0, aws-sdk@^2.699.0: - version "2.699.0" - resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.699.0.tgz#e77b6ffa4c860882e2779c060b74fab33d42f554" - integrity sha512-EC431z/+i/cJgOgnDpOJ8Fa6+p7Oo1vIvdm/uJqP9tJX3+pxi/M/tvQavfz4yAlLBFqjQwxa8nrPisby0Mr5MQ== +aws-sdk@^2.637.0, aws-sdk@^2.701.0: + version "2.701.0" + resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.701.0.tgz#9827b7eb2bf87722c348d5195c17620b5fe1bead" + integrity sha512-95fWGr6gvyRH408VP5LQAycB8Wqw665ll7q6/soscGFD+2HuQ+gB5qpSlXJwOgSggKQCaExWoQnDABBoTSXM2w== dependencies: buffer "4.9.2" events "1.1.1" From 2b6099456a69b6de72832f07c972f30625378cc8 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 22 Jun 2020 22:18:28 +0000 Subject: [PATCH 07/21] chore(deps-dev): bump @types/lodash from 4.14.155 to 4.14.156 (#8687) Bumps [@types/lodash](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/lodash) from 4.14.155 to 4.14.156. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/lodash) Signed-off-by: dependabot-preview[bot] Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com> --- packages/@aws-cdk/aws-codepipeline-actions/package.json | 2 +- packages/@aws-cdk/aws-lambda/package.json | 2 +- packages/@aws-cdk/core/package.json | 2 +- yarn.lock | 8 ++++---- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/@aws-cdk/aws-codepipeline-actions/package.json b/packages/@aws-cdk/aws-codepipeline-actions/package.json index 65c3d3b886f1f..557f8079b9b34 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/package.json +++ b/packages/@aws-cdk/aws-codepipeline-actions/package.json @@ -64,7 +64,7 @@ "devDependencies": { "@aws-cdk/assert": "0.0.0", "@aws-cdk/aws-cloudtrail": "0.0.0", - "@types/lodash": "^4.14.155", + "@types/lodash": "^4.14.156", "@types/nodeunit": "^0.0.31", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", diff --git a/packages/@aws-cdk/aws-lambda/package.json b/packages/@aws-cdk/aws-lambda/package.json index 2e7885cc6b2c5..749f83184de41 100644 --- a/packages/@aws-cdk/aws-lambda/package.json +++ b/packages/@aws-cdk/aws-lambda/package.json @@ -68,7 +68,7 @@ "devDependencies": { "@aws-cdk/assert": "0.0.0", "@types/aws-lambda": "^8.10.39", - "@types/lodash": "^4.14.155", + "@types/lodash": "^4.14.156", "@types/nodeunit": "^0.0.31", "@types/sinon": "^9.0.4", "aws-sdk": "^2.701.0", diff --git a/packages/@aws-cdk/core/package.json b/packages/@aws-cdk/core/package.json index 0c99c177e2027..cee96ce573d7e 100644 --- a/packages/@aws-cdk/core/package.json +++ b/packages/@aws-cdk/core/package.json @@ -151,7 +151,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/lodash": "^4.14.155", + "@types/lodash": "^4.14.156", "@types/node": "^10.17.26", "@types/nodeunit": "^0.0.31", "@types/minimatch": "^3.0.3", diff --git a/yarn.lock b/yarn.lock index 6ed07a3bfb59d..66b3e3ecd8c0d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1501,10 +1501,10 @@ dependencies: jszip "*" -"@types/lodash@^4.14.155": - version "4.14.155" - resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.155.tgz#e2b4514f46a261fd11542e47519c20ebce7bc23a" - integrity sha512-vEcX7S7aPhsBCivxMwAANQburHBtfN9RdyXFk84IJmu2Z4Hkg1tOFgaslRiEqqvoLtbCBi6ika1EMspE+NZ9Lg== +"@types/lodash@^4.14.156": + version "4.14.156" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.156.tgz#cbe30909c89a1feeb7c60803e785344ea0ec82d1" + integrity sha512-l2AgHXcKUwx2DsvP19wtRPqZ4NkONjmorOdq4sMcxIjqdIuuV/ULo2ftuv4NUpevwfW7Ju/UKLqo0ZXuEt/8lQ== "@types/md5@^2.2.0": version "2.2.0" From 2024f169ea44fed0578dfd7191c6a23e77713d0c Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 22 Jun 2020 23:10:28 +0000 Subject: [PATCH 08/21] chore(deps): bump ts-jest from 26.1.0 to 26.1.1 (#8688) Bumps [ts-jest](https://github.com/kulshekhar/ts-jest) from 26.1.0 to 26.1.1. - [Release notes](https://github.com/kulshekhar/ts-jest/releases) - [Changelog](https://github.com/kulshekhar/ts-jest/blob/master/CHANGELOG.md) - [Commits](https://github.com/kulshekhar/ts-jest/compare/v26.1.0...v26.1.1) Signed-off-by: dependabot-preview[bot] Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com> --- packages/@aws-cdk/assert/package.json | 2 +- packages/@aws-cdk/aws-dynamodb/package.json | 2 +- packages/@aws-cdk/aws-sam/package.json | 2 +- packages/@aws-cdk/cloudformation-diff/package.json | 2 +- packages/@aws-cdk/cloudformation-include/package.json | 2 +- packages/@monocdk-experiment/assert/package.json | 2 +- packages/aws-cdk/package.json | 2 +- tools/cdk-build-tools/package.json | 2 +- yarn.lock | 8 ++++---- 9 files changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/@aws-cdk/assert/package.json b/packages/@aws-cdk/assert/package.json index edf1445b82fd2..815abfab9ddc1 100644 --- a/packages/@aws-cdk/assert/package.json +++ b/packages/@aws-cdk/assert/package.json @@ -25,7 +25,7 @@ "cdk-build-tools": "0.0.0", "jest": "^25.5.4", "pkglint": "0.0.0", - "ts-jest": "^26.1.0" + "ts-jest": "^26.1.1" }, "dependencies": { "@aws-cdk/cloudformation-diff": "0.0.0", diff --git a/packages/@aws-cdk/aws-dynamodb/package.json b/packages/@aws-cdk/aws-dynamodb/package.json index 6f73d8ad25dfc..b8494139b050f 100644 --- a/packages/@aws-cdk/aws-dynamodb/package.json +++ b/packages/@aws-cdk/aws-dynamodb/package.json @@ -73,7 +73,7 @@ "jest": "^25.5.4", "pkglint": "0.0.0", "sinon": "^9.0.2", - "ts-jest": "^26.1.0" + "ts-jest": "^26.1.1" }, "dependencies": { "@aws-cdk/aws-applicationautoscaling": "0.0.0", diff --git a/packages/@aws-cdk/aws-sam/package.json b/packages/@aws-cdk/aws-sam/package.json index d472fc497b882..f0c48b343da21 100644 --- a/packages/@aws-cdk/aws-sam/package.json +++ b/packages/@aws-cdk/aws-sam/package.json @@ -70,7 +70,7 @@ "cfn2ts": "0.0.0", "jest": "^25.5.4", "pkglint": "0.0.0", - "ts-jest": "^26.1.0" + "ts-jest": "^26.1.1" }, "dependencies": { "@aws-cdk/core": "0.0.0", diff --git a/packages/@aws-cdk/cloudformation-diff/package.json b/packages/@aws-cdk/cloudformation-diff/package.json index 4916f1702a90e..4d128ebfc9579 100644 --- a/packages/@aws-cdk/cloudformation-diff/package.json +++ b/packages/@aws-cdk/cloudformation-diff/package.json @@ -36,7 +36,7 @@ "fast-check": "^1.25.1", "jest": "^25.5.4", "pkglint": "0.0.0", - "ts-jest": "^26.1.0" + "ts-jest": "^26.1.1" }, "repository": { "url": "https://github.com/aws/aws-cdk.git", diff --git a/packages/@aws-cdk/cloudformation-include/package.json b/packages/@aws-cdk/cloudformation-include/package.json index 4ba1c65e4ae9e..cad1c8249b4fe 100644 --- a/packages/@aws-cdk/cloudformation-include/package.json +++ b/packages/@aws-cdk/cloudformation-include/package.json @@ -307,7 +307,7 @@ "cdk-build-tools": "0.0.0", "jest": "^25.4.0", "pkglint": "0.0.0", - "ts-jest": "^26.1.0" + "ts-jest": "^26.1.1" }, "bundledDependencies": [ "yaml" diff --git a/packages/@monocdk-experiment/assert/package.json b/packages/@monocdk-experiment/assert/package.json index cc6753fd598bf..a83382da131d0 100644 --- a/packages/@monocdk-experiment/assert/package.json +++ b/packages/@monocdk-experiment/assert/package.json @@ -41,7 +41,7 @@ "cdk-build-tools": "0.0.0", "jest": "^25.5.4", "pkglint": "0.0.0", - "ts-jest": "^26.1.0", + "ts-jest": "^26.1.1", "@monocdk-experiment/rewrite-imports": "0.0.0", "monocdk-experiment": "0.0.0", "constructs": "^3.0.2" diff --git a/packages/aws-cdk/package.json b/packages/aws-cdk/package.json index 04a6e35d4364f..f2721d1c3a1db 100644 --- a/packages/aws-cdk/package.json +++ b/packages/aws-cdk/package.json @@ -62,7 +62,7 @@ "mockery": "^2.1.0", "pkglint": "0.0.0", "sinon": "^9.0.2", - "ts-jest": "^26.1.0", + "ts-jest": "^26.1.1", "ts-mock-imports": "^1.2.6" }, "dependencies": { diff --git a/tools/cdk-build-tools/package.json b/tools/cdk-build-tools/package.json index 6cab04c888862..d2675316a5192 100644 --- a/tools/cdk-build-tools/package.json +++ b/tools/cdk-build-tools/package.json @@ -53,7 +53,7 @@ "jsii-pacmak": "^1.7.0", "nodeunit": "^0.11.3", "nyc": "^15.1.0", - "ts-jest": "^26.1.0", + "ts-jest": "^26.1.1", "tslint": "^5.20.1", "typescript": "~3.9.5", "yargs": "^15.3.1", diff --git a/yarn.lock b/yarn.lock index 66b3e3ecd8c0d..a6249085b40d7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9426,10 +9426,10 @@ trivial-deferred@^1.0.1: resolved "https://registry.yarnpkg.com/trivial-deferred/-/trivial-deferred-1.0.1.tgz#376d4d29d951d6368a6f7a0ae85c2f4d5e0658f3" integrity sha1-N21NKdlR1jaKb3oK6FwvTV4GWPM= -ts-jest@^26.1.0: - version "26.1.0" - resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-26.1.0.tgz#e9070fc97b3ea5557a48b67c631c74eb35e15417" - integrity sha512-JbhQdyDMYN5nfKXaAwCIyaWLGwevcT2/dbqRPsQeh6NZPUuXjZQZEfeLb75tz0ubCIgEELNm6xAzTe5NXs5Y4Q== +ts-jest@^26.1.1: + version "26.1.1" + resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-26.1.1.tgz#b98569b8a4d4025d966b3d40c81986dd1c510f8d" + integrity sha512-Lk/357quLg5jJFyBQLnSbhycnB3FPe+e9i7ahxokyXxAYoB0q1pPmqxxRPYr4smJic1Rjcf7MXDBhZWgxlli0A== dependencies: bs-logger "0.x" buffer-from "1.x" From 6b5d77b452bccb35564d6acee118112156149eb0 Mon Sep 17 00:00:00 2001 From: Hyeonsoo David Lee Date: Tue, 23 Jun 2020 09:38:21 +0900 Subject: [PATCH 09/21] fix(appsync): Not to throw an Error even if 'additionalAuthorizationModes' is undefined (#8673) fix(appsync): Not to throw an Error even if 'additionalAuthorizationModes' is undefined fixes #8666 #8668 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../@aws-cdk/aws-appsync/lib/graphqlapi.ts | 12 +++++------ .../@aws-cdk/aws-appsync/test/appsync.test.ts | 21 ++++++++++++++++--- 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/packages/@aws-cdk/aws-appsync/lib/graphqlapi.ts b/packages/@aws-cdk/aws-appsync/lib/graphqlapi.ts index 2d4f802f72048..bbc92625c5749 100644 --- a/packages/@aws-cdk/aws-appsync/lib/graphqlapi.ts +++ b/packages/@aws-cdk/aws-appsync/lib/graphqlapi.ts @@ -335,12 +335,7 @@ export class GraphQLApi extends Construct { props.authorizationConfig.defaultAuthorization.userPoolConfig!, ) : undefined, - additionalAuthenticationProviders: props.authorizationConfig - ?.additionalAuthorizationModes!.length - ? this.formatAdditionalAuthorizationModes( - props.authorizationConfig!.additionalAuthorizationModes!, - ) - : undefined, + additionalAuthenticationProviders: this.formatAdditionalAuthenticationProviders(props), }); this.apiId = this.api.attrApiId; @@ -569,6 +564,11 @@ export class GraphQLApi extends Construct { [], ); } + + private formatAdditionalAuthenticationProviders(props: GraphQLApiProps): CfnGraphQLApi.AdditionalAuthenticationProviderProperty[] | undefined { + const authModes = props.authorizationConfig?.additionalAuthorizationModes; + return authModes ? this.formatAdditionalAuthorizationModes(authModes) : undefined; + } } /** diff --git a/packages/@aws-cdk/aws-appsync/test/appsync.test.ts b/packages/@aws-cdk/aws-appsync/test/appsync.test.ts index e394ef336bfb4..edb263eed6800 100644 --- a/packages/@aws-cdk/aws-appsync/test/appsync.test.ts +++ b/packages/@aws-cdk/aws-appsync/test/appsync.test.ts @@ -1,6 +1,21 @@ import '@aws-cdk/assert/jest'; -import {} from '../lib'; +import * as cdk from '@aws-cdk/core'; +import * as path from 'path'; +import * as appsync from '../lib'; -test('No tests are specified for this package', () => { - expect(true).toBe(true); +test('should not throw an Error', () => { + // Given + const stack = new cdk.Stack(); + + // When + const when = () => { + new appsync.GraphQLApi(stack, 'api', { + authorizationConfig: {}, + name: 'api', + schemaDefinitionFile: path.join(__dirname, 'schema.graphql'), + }); + }; + + // Then + expect(when).not.toThrow(); }); From cb3b861eca1d57ef9f4a3bef33fade440a1bf5c7 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Tue, 23 Jun 2020 02:57:15 +0000 Subject: [PATCH 10/21] chore(deps): bump @typescript-eslint/eslint-plugin from 3.3.0 to 3.4.0 (#8693) Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 3.3.0 to 3.4.0. - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v3.4.0/packages/eslint-plugin) Signed-off-by: dependabot-preview[bot] Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com> --- tools/cdk-build-tools/package.json | 2 +- yarn.lock | 29 +++++++++++++++-------------- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/tools/cdk-build-tools/package.json b/tools/cdk-build-tools/package.json index d2675316a5192..0305be2513a6b 100644 --- a/tools/cdk-build-tools/package.json +++ b/tools/cdk-build-tools/package.json @@ -39,7 +39,7 @@ "pkglint": "0.0.0" }, "dependencies": { - "@typescript-eslint/eslint-plugin": "^3.3.0", + "@typescript-eslint/eslint-plugin": "^3.4.0", "@typescript-eslint/parser": "^2.19.2", "awslint": "0.0.0", "colors": "^1.4.0", diff --git a/yarn.lock b/yarn.lock index a6249085b40d7..9d8fabb0cd299 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1647,12 +1647,13 @@ resolved "https://registry.yarnpkg.com/@types/yarnpkg__lockfile/-/yarnpkg__lockfile-1.1.3.tgz#38fb31d82ed07dea87df6bd565721d11979fd761" integrity sha512-mhdQq10tYpiNncMkg1vovCud5jQm+rWeRVz6fxjCJlY6uhDlAn9GnMSmBa2DQwqPf/jS5YR0K/xChDEh1jdOQg== -"@typescript-eslint/eslint-plugin@^3.3.0": - version "3.3.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-3.3.0.tgz#89518e5c5209a349bde161c3489b0ec187ae5d37" - integrity sha512-Ybx/wU75Tazz6nU2d7nN6ll0B98odoiYLXwcuwS5WSttGzK46t0n7TPRQ4ozwcTv82UY6TQoIvI+sJfTzqK9dQ== +"@typescript-eslint/eslint-plugin@^3.4.0": + version "3.4.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-3.4.0.tgz#8378062e6be8a1d049259bdbcf27ce5dfbeee62b" + integrity sha512-wfkpiqaEVhZIuQRmudDszc01jC/YR7gMSxa6ulhggAe/Hs0KVIuo9wzvFiDbG3JD5pRFQoqnf4m7REDsUvBnMQ== dependencies: - "@typescript-eslint/experimental-utils" "3.3.0" + "@typescript-eslint/experimental-utils" "3.4.0" + debug "^4.1.1" functional-red-black-tree "^1.0.1" regexpp "^3.0.0" semver "^7.3.2" @@ -1668,13 +1669,13 @@ eslint-scope "^5.0.0" eslint-utils "^2.0.0" -"@typescript-eslint/experimental-utils@3.3.0": - version "3.3.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-3.3.0.tgz#d72a946e056a83d4edf97f3411cceb639b0b8c87" - integrity sha512-d4pGIAbu/tYsrPrdHCQ5xfadJGvlkUxbeBB56nO/VGmEDi/sKmfa5fGty5t5veL1OyJBrUmSiRn1R1qfVDydrg== +"@typescript-eslint/experimental-utils@3.4.0": + version "3.4.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-3.4.0.tgz#8a44dfc6fb7f1d071937b390fe27608ebda122b8" + integrity sha512-rHPOjL43lOH1Opte4+dhC0a/+ks+8gOBwxXnyrZ/K4OTAChpSjP76fbI8Cglj7V5GouwVAGaK+xVwzqTyE/TPw== dependencies: "@types/json-schema" "^7.0.3" - "@typescript-eslint/typescript-estree" "3.3.0" + "@typescript-eslint/typescript-estree" "3.4.0" eslint-scope "^5.0.0" eslint-utils "^2.0.0" @@ -1701,10 +1702,10 @@ semver "^6.3.0" tsutils "^3.17.1" -"@typescript-eslint/typescript-estree@3.3.0": - version "3.3.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-3.3.0.tgz#841ffed25c29b0049ebffb4c2071268a34558a2a" - integrity sha512-3SqxylENltEvJsjjMSDCUx/edZNSC7wAqifUU1Ywp//0OWEZwMZJfecJud9XxJ/40rAKEbJMKBOQzeOjrLJFzQ== +"@typescript-eslint/typescript-estree@3.4.0": + version "3.4.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-3.4.0.tgz#6a787eb70b48969e4cd1ea67b057083f96dfee29" + integrity sha512-zKwLiybtt4uJb4mkG5q2t6+W7BuYx2IISiDNV+IY68VfoGwErDx/RfVI7SWL4gnZ2t1A1ytQQwZ+YOJbHHJ2rw== dependencies: debug "^4.1.1" eslint-visitor-keys "^1.1.0" From f048e4ec67dce24a76a9262990c30a5e21106bf0 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Tue, 23 Jun 2020 03:48:38 +0000 Subject: [PATCH 11/21] chore(deps): bump aws-sdk from 2.701.0 to 2.702.0 (#8694) Bumps [aws-sdk](https://github.com/aws/aws-sdk-js) from 2.701.0 to 2.702.0. - [Release notes](https://github.com/aws/aws-sdk-js/releases) - [Changelog](https://github.com/aws/aws-sdk-js/blob/master/CHANGELOG.md) - [Commits](https://github.com/aws/aws-sdk-js/compare/v2.701.0...v2.702.0) Signed-off-by: dependabot-preview[bot] Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com> --- packages/@aws-cdk/aws-cloudfront/package.json | 2 +- packages/@aws-cdk/aws-cloudtrail/package.json | 2 +- packages/@aws-cdk/aws-codebuild/package.json | 2 +- packages/@aws-cdk/aws-codecommit/package.json | 2 +- packages/@aws-cdk/aws-dynamodb/package.json | 2 +- packages/@aws-cdk/aws-eks/package.json | 2 +- packages/@aws-cdk/aws-events-targets/package.json | 2 +- packages/@aws-cdk/aws-lambda/package.json | 2 +- packages/@aws-cdk/aws-route53/package.json | 2 +- packages/@aws-cdk/aws-sqs/package.json | 2 +- packages/@aws-cdk/custom-resources/package.json | 2 +- packages/aws-cdk/package.json | 2 +- packages/cdk-assets/package.json | 2 +- yarn.lock | 8 ++++---- 14 files changed, 17 insertions(+), 17 deletions(-) diff --git a/packages/@aws-cdk/aws-cloudfront/package.json b/packages/@aws-cdk/aws-cloudfront/package.json index e3674e2a78d23..832a9d018c362 100644 --- a/packages/@aws-cdk/aws-cloudfront/package.json +++ b/packages/@aws-cdk/aws-cloudfront/package.json @@ -64,7 +64,7 @@ "devDependencies": { "@aws-cdk/assert": "0.0.0", "@types/nodeunit": "^0.0.31", - "aws-sdk": "^2.701.0", + "aws-sdk": "^2.702.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-cloudtrail/package.json b/packages/@aws-cdk/aws-cloudtrail/package.json index 313e07c9ec980..c043cc6d8eace 100644 --- a/packages/@aws-cdk/aws-cloudtrail/package.json +++ b/packages/@aws-cdk/aws-cloudtrail/package.json @@ -64,7 +64,7 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", - "aws-sdk": "^2.701.0", + "aws-sdk": "^2.702.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-codebuild/package.json b/packages/@aws-cdk/aws-codebuild/package.json index 32f594f3a465b..b02ad1fce2c75 100644 --- a/packages/@aws-cdk/aws-codebuild/package.json +++ b/packages/@aws-cdk/aws-codebuild/package.json @@ -70,7 +70,7 @@ "@aws-cdk/aws-sns": "0.0.0", "@aws-cdk/aws-sqs": "0.0.0", "@types/nodeunit": "^0.0.31", - "aws-sdk": "^2.701.0", + "aws-sdk": "^2.702.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-codecommit/package.json b/packages/@aws-cdk/aws-codecommit/package.json index 9ba29e3bd18b5..8948b2e5409f5 100644 --- a/packages/@aws-cdk/aws-codecommit/package.json +++ b/packages/@aws-cdk/aws-codecommit/package.json @@ -70,7 +70,7 @@ "@aws-cdk/assert": "0.0.0", "@aws-cdk/aws-sns": "0.0.0", "@types/nodeunit": "^0.0.31", - "aws-sdk": "^2.701.0", + "aws-sdk": "^2.702.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-dynamodb/package.json b/packages/@aws-cdk/aws-dynamodb/package.json index b8494139b050f..8b2443716af69 100644 --- a/packages/@aws-cdk/aws-dynamodb/package.json +++ b/packages/@aws-cdk/aws-dynamodb/package.json @@ -65,7 +65,7 @@ "devDependencies": { "@aws-cdk/assert": "0.0.0", "@types/jest": "^26.0.0", - "aws-sdk": "^2.701.0", + "aws-sdk": "^2.702.0", "aws-sdk-mock": "^5.1.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", diff --git a/packages/@aws-cdk/aws-eks/package.json b/packages/@aws-cdk/aws-eks/package.json index 9ad0aaff863bd..4baf3c26c1287 100644 --- a/packages/@aws-cdk/aws-eks/package.json +++ b/packages/@aws-cdk/aws-eks/package.json @@ -64,7 +64,7 @@ "devDependencies": { "@aws-cdk/assert": "0.0.0", "@types/nodeunit": "^0.0.31", - "aws-sdk": "^2.701.0", + "aws-sdk": "^2.702.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-events-targets/package.json b/packages/@aws-cdk/aws-events-targets/package.json index 925ff1d5d504f..3c08a160af05f 100644 --- a/packages/@aws-cdk/aws-events-targets/package.json +++ b/packages/@aws-cdk/aws-events-targets/package.json @@ -68,7 +68,7 @@ "devDependencies": { "@aws-cdk/assert": "0.0.0", "@aws-cdk/aws-codecommit": "0.0.0", - "aws-sdk": "^2.701.0", + "aws-sdk": "^2.702.0", "aws-sdk-mock": "^5.1.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", diff --git a/packages/@aws-cdk/aws-lambda/package.json b/packages/@aws-cdk/aws-lambda/package.json index 749f83184de41..7c9a4917451be 100644 --- a/packages/@aws-cdk/aws-lambda/package.json +++ b/packages/@aws-cdk/aws-lambda/package.json @@ -71,7 +71,7 @@ "@types/lodash": "^4.14.156", "@types/nodeunit": "^0.0.31", "@types/sinon": "^9.0.4", - "aws-sdk": "^2.701.0", + "aws-sdk": "^2.702.0", "aws-sdk-mock": "^5.1.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", diff --git a/packages/@aws-cdk/aws-route53/package.json b/packages/@aws-cdk/aws-route53/package.json index b5d0d495b18a0..82da2e16f98eb 100644 --- a/packages/@aws-cdk/aws-route53/package.json +++ b/packages/@aws-cdk/aws-route53/package.json @@ -64,7 +64,7 @@ "devDependencies": { "@aws-cdk/assert": "0.0.0", "@types/nodeunit": "^0.0.31", - "aws-sdk": "^2.701.0", + "aws-sdk": "^2.702.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-sqs/package.json b/packages/@aws-cdk/aws-sqs/package.json index daee0dbdfaff9..9f5091f3f5afb 100644 --- a/packages/@aws-cdk/aws-sqs/package.json +++ b/packages/@aws-cdk/aws-sqs/package.json @@ -65,7 +65,7 @@ "@aws-cdk/assert": "0.0.0", "@aws-cdk/aws-s3": "0.0.0", "@types/nodeunit": "^0.0.31", - "aws-sdk": "^2.701.0", + "aws-sdk": "^2.702.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/custom-resources/package.json b/packages/@aws-cdk/custom-resources/package.json index 8f42ec3114691..580f3245f7646 100644 --- a/packages/@aws-cdk/custom-resources/package.json +++ b/packages/@aws-cdk/custom-resources/package.json @@ -73,7 +73,7 @@ "@types/aws-lambda": "^8.10.39", "@types/fs-extra": "^8.1.0", "@types/sinon": "^9.0.4", - "aws-sdk": "^2.701.0", + "aws-sdk": "^2.702.0", "aws-sdk-mock": "^5.1.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", diff --git a/packages/aws-cdk/package.json b/packages/aws-cdk/package.json index f2721d1c3a1db..5453e9d650138 100644 --- a/packages/aws-cdk/package.json +++ b/packages/aws-cdk/package.json @@ -72,7 +72,7 @@ "@aws-cdk/cx-api": "0.0.0", "@aws-cdk/region-info": "0.0.0", "archiver": "^4.0.1", - "aws-sdk": "^2.701.0", + "aws-sdk": "^2.702.0", "camelcase": "^6.0.0", "cdk-assets": "0.0.0", "colors": "^1.4.0", diff --git a/packages/cdk-assets/package.json b/packages/cdk-assets/package.json index 33f19b00ead30..52f0a2b0d475e 100644 --- a/packages/cdk-assets/package.json +++ b/packages/cdk-assets/package.json @@ -47,7 +47,7 @@ "@aws-cdk/cdk-assets-schema": "0.0.0", "@aws-cdk/cx-api": "0.0.0", "archiver": "^4.0.1", - "aws-sdk": "^2.701.0", + "aws-sdk": "^2.702.0", "glob": "^7.1.6", "yargs": "^15.3.1" }, diff --git a/yarn.lock b/yarn.lock index 9d8fabb0cd299..39735396bccfe 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2124,10 +2124,10 @@ aws-sdk-mock@^5.1.0: sinon "^9.0.1" traverse "^0.6.6" -aws-sdk@^2.637.0, aws-sdk@^2.701.0: - version "2.701.0" - resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.701.0.tgz#9827b7eb2bf87722c348d5195c17620b5fe1bead" - integrity sha512-95fWGr6gvyRH408VP5LQAycB8Wqw665ll7q6/soscGFD+2HuQ+gB5qpSlXJwOgSggKQCaExWoQnDABBoTSXM2w== +aws-sdk@^2.637.0, aws-sdk@^2.702.0: + version "2.702.0" + resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.702.0.tgz#c5bb9a6caeb7d938b803095073fe981ff29e4244" + integrity sha512-FKRve3NOKeUKxFXeD6VfiIhXpIhym/yFdy7higxUObmsj2ssM/e7Iud79gUHRAKJW5fzusdtkBCAVBjotRGxew== dependencies: buffer "4.9.2" events "1.1.1" From 7e2be93b2068052e60914f674ed5f86766373c81 Mon Sep 17 00:00:00 2001 From: Elad Ben-Israel Date: Tue, 23 Jun 2020 15:49:28 +0300 Subject: [PATCH 12/21] chore: incorporate cdk-assets-schema into cloud-assembly-schema (#8482) assets.json is part of the cloud assembly and as such we want to make sure it's schema and compatibility is managed together with the cloud assembly manifest. To do that, we extended the cloud assembly schema "root" to include two keys `manifest` and `assets` and incorporated all the types from cdk-assets-schema into the JSON schema. Now we can leverage the compatibility checks and versioning from cloud-assembly-schema instead of the ones we had in cdk-assets-schema. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- allowed-breaking-changes.txt | 12 ++ package.json | 2 - packages/@aws-cdk/cdk-assets-schema/README.md | 11 +- .../@aws-cdk/cdk-assets-schema/lib/index.ts | 7 +- .../lib/private/my-package-json.ts | 25 --- .../lib/private/schema-helpers.ts | 65 ------- .../cdk-assets-schema/lib/validate.ts | 104 ---------- .../@aws-cdk/cdk-assets-schema/package.json | 11 +- .../cdk-assets-schema/test/dummy.test.ts | 3 + .../@aws-cdk/cloud-assembly-schema/.gitignore | 4 +- .../lib/assets}/aws-destination.ts | 0 .../lib/assets}/docker-image-asset.ts | 4 +- .../lib/assets}/file-asset.ts | 2 +- .../cloud-assembly-schema/lib/assets/index.ts | 4 + .../lib/assets/schema.ts} | 8 +- .../{ => cloud-assembly}/artifact-schema.ts | 0 .../{ => cloud-assembly}/context-queries.ts | 0 .../lib/cloud-assembly/index.ts | 4 + .../{ => cloud-assembly}/metadata-schema.ts | 0 .../lib/{ => cloud-assembly}/schema.ts | 0 .../cloud-assembly-schema/lib/index.ts | 6 +- .../cloud-assembly-schema/lib/manifest.ts | 100 +++++++--- .../schema/assets.schema.json | 184 ++++++++++++++++++ .../scripts/update-schema.sh | 5 +- .../scripts/update-schema.ts | 53 +++-- .../test/assets.test.ts} | 47 +++-- .../test/manifest.test.ts | 86 +++----- .../cloud-assembly-schema/test/schema.test.ts | 50 +++++ .../stack-synthesizers/default-synthesizer.ts | 9 +- packages/@aws-cdk/core/package.json | 2 - .../test.new-style-synthesis.ts | 6 +- packages/@aws-cdk/core/test/test.app.ts | 1 - .../@aws-cdk/core/test/test.runtime-info.ts | 1 - packages/aws-cdk/lib/assets.ts | 7 +- .../lib/util/asset-manifest-builder.ts | 10 +- packages/aws-cdk/package.json | 1 - packages/cdk-assets/lib/asset-manifest.ts | 13 +- .../cdk-assets/lib/private/handlers/files.ts | 2 +- packages/cdk-assets/package.json | 2 +- .../cdk-assets/test/docker-images.test.ts | 6 +- packages/cdk-assets/test/files.test.ts | 6 +- packages/cdk-assets/test/manifest.test.ts | 4 +- packages/cdk-assets/test/placeholders.test.ts | 4 +- packages/cdk-assets/test/progress.test.ts | 4 +- packages/cdk-assets/test/zipping.test.ts | 4 +- packages/cdk-assets/tsconfig.json | 3 - packages/decdk/package.json | 1 - packages/monocdk-experiment/package.json | 1 - tools/pkglint/lib/rules.ts | 1 - 49 files changed, 481 insertions(+), 404 deletions(-) delete mode 100644 packages/@aws-cdk/cdk-assets-schema/lib/private/my-package-json.ts delete mode 100644 packages/@aws-cdk/cdk-assets-schema/lib/private/schema-helpers.ts delete mode 100644 packages/@aws-cdk/cdk-assets-schema/lib/validate.ts create mode 100644 packages/@aws-cdk/cdk-assets-schema/test/dummy.test.ts rename packages/@aws-cdk/{cdk-assets-schema/lib => cloud-assembly-schema/lib/assets}/aws-destination.ts (100%) rename packages/@aws-cdk/{cdk-assets-schema/lib => cloud-assembly-schema/lib/assets}/docker-image-asset.ts (90%) rename packages/@aws-cdk/{cdk-assets-schema/lib => cloud-assembly-schema/lib/assets}/file-asset.ts (94%) create mode 100644 packages/@aws-cdk/cloud-assembly-schema/lib/assets/index.ts rename packages/@aws-cdk/{cdk-assets-schema/lib/manifest-schema.ts => cloud-assembly-schema/lib/assets/schema.ts} (73%) rename packages/@aws-cdk/cloud-assembly-schema/lib/{ => cloud-assembly}/artifact-schema.ts (100%) rename packages/@aws-cdk/cloud-assembly-schema/lib/{ => cloud-assembly}/context-queries.ts (100%) create mode 100644 packages/@aws-cdk/cloud-assembly-schema/lib/cloud-assembly/index.ts rename packages/@aws-cdk/cloud-assembly-schema/lib/{ => cloud-assembly}/metadata-schema.ts (100%) rename packages/@aws-cdk/cloud-assembly-schema/lib/{ => cloud-assembly}/schema.ts (100%) create mode 100644 packages/@aws-cdk/cloud-assembly-schema/schema/assets.schema.json rename packages/@aws-cdk/{cdk-assets-schema/test/validate.test.ts => cloud-assembly-schema/test/assets.test.ts} (71%) create mode 100644 packages/@aws-cdk/cloud-assembly-schema/test/schema.test.ts diff --git a/allowed-breaking-changes.txt b/allowed-breaking-changes.txt index e174e6ace55d6..fd44c1df2ce34 100644 --- a/allowed-breaking-changes.txt +++ b/allowed-breaking-changes.txt @@ -7,3 +7,15 @@ removed:@aws-cdk/core.BootstraplessSynthesizer.DEFAULT_ASSET_PUBLISHING_ROLE_ARN removed:@aws-cdk/core.DefaultStackSynthesizer.DEFAULT_ASSET_PUBLISHING_ROLE_ARN removed:@aws-cdk/core.DefaultStackSynthesizerProps.assetPublishingExternalId removed:@aws-cdk/core.DefaultStackSynthesizerProps.assetPublishingRoleArn + +# merge asset manifest schema into cloud assembly schema +removed:@aws-cdk/cdk-assets-schema.AssetManifestSchema +removed:@aws-cdk/cdk-assets-schema.AwsDestination +removed:@aws-cdk/cdk-assets-schema.DockerImageAsset +removed:@aws-cdk/cdk-assets-schema.DockerImageDestination +removed:@aws-cdk/cdk-assets-schema.DockerImageSource +removed:@aws-cdk/cdk-assets-schema.FileAsset +removed:@aws-cdk/cdk-assets-schema.FileDestination +removed:@aws-cdk/cdk-assets-schema.FileSource +removed:@aws-cdk/cdk-assets-schema.ManifestFile +removed:@aws-cdk/cdk-assets-schema.FileAssetPackaging diff --git a/package.json b/package.json index 1f8cda209553d..65f78e85c6d42 100644 --- a/package.json +++ b/package.json @@ -54,8 +54,6 @@ "@aws-cdk/aws-ecr-assets/minimatch/**", "@aws-cdk/aws-lambda-nodejs/parcel-bundler", "@aws-cdk/aws-lambda-nodejs/parcel-bundler/**", - "@aws-cdk/cdk-assets-schema/semver", - "@aws-cdk/cdk-assets-schema/semver/**", "@aws-cdk/cloud-assembly-schema/jsonschema", "@aws-cdk/cloud-assembly-schema/jsonschema/**", "@aws-cdk/cloud-assembly-schema/semver", diff --git a/packages/@aws-cdk/cdk-assets-schema/README.md b/packages/@aws-cdk/cdk-assets-schema/README.md index 5e7f95245d74d..01fbb7c07584f 100644 --- a/packages/@aws-cdk/cdk-assets-schema/README.md +++ b/packages/@aws-cdk/cdk-assets-schema/README.md @@ -2,14 +2,11 @@ --- -![cdk-constructs: Stable](https://img.shields.io/badge/cdk--constructs-stable-success.svg?style=for-the-badge) +![Deprecated](https://img.shields.io/badge/deprecated-critical.svg?style=for-the-badge) + +> This API may emit warnings. Backward compatibility is not guaranteed. --- -This module contains the schema definitions for the Asset Manifest. - -We expose them via JSII so that they are checked for backwards compatibility -by the `jsii-diff` tool; routines exist in `validate.ts` which will return -them, so that the structs can only be strengthened (i.e., existing fields -may not be removed or made optional). +This schema is now part of @aws-cdk/cloud-assembly-schema. \ No newline at end of file diff --git a/packages/@aws-cdk/cdk-assets-schema/lib/index.ts b/packages/@aws-cdk/cdk-assets-schema/lib/index.ts index e985ed8e216b3..860b95e527e47 100644 --- a/packages/@aws-cdk/cdk-assets-schema/lib/index.ts +++ b/packages/@aws-cdk/cdk-assets-schema/lib/index.ts @@ -1,5 +1,2 @@ -export * from './manifest-schema'; -export * from './docker-image-asset'; -export * from './file-asset'; -export * from './aws-destination'; -export * from './validate'; +// tslint:disable-next-line: no-console +console.error('error: @aws-cdk/cdk-assets-schema has been merged into @aws-cdk/cloud-assembly-schema'); diff --git a/packages/@aws-cdk/cdk-assets-schema/lib/private/my-package-json.ts b/packages/@aws-cdk/cdk-assets-schema/lib/private/my-package-json.ts deleted file mode 100644 index 63c8e347f8709..0000000000000 --- a/packages/@aws-cdk/cdk-assets-schema/lib/private/my-package-json.ts +++ /dev/null @@ -1,25 +0,0 @@ -import * as path from 'path'; - -// tslint:disable:no-var-requires - -/** - * Get my package JSON. - * - * In principle it's just '__dirname/../../package.json', but in the monocdk - * it will live at a different location. So search upwards. - */ -export function loadMyPackageJson(): any { - let dir = path.resolve(__dirname, '..', '..'); - while (true) { - try { - // eslint-disable-next-line @typescript-eslint/no-require-imports - return require(path.join(dir, 'package.json')); - } catch (e) { - if (e.code !== 'MODULE_NOT_FOUND') { throw e; } - - const newdir = path.dirname(dir); - if (newdir === dir) { throw new Error(`No package.json found upward of ${__dirname}`); } - dir = newdir; - } - } -} \ No newline at end of file diff --git a/packages/@aws-cdk/cdk-assets-schema/lib/private/schema-helpers.ts b/packages/@aws-cdk/cdk-assets-schema/lib/private/schema-helpers.ts deleted file mode 100644 index 9ae1d1fcdb39a..0000000000000 --- a/packages/@aws-cdk/cdk-assets-schema/lib/private/schema-helpers.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { FileAssetPackaging } from '../file-asset'; - -/** - * Validate that a given key is of a given type in an object - * - * If not optional, the key is considered required. - * - * Uses predicate validators that return a 'witness', so we can use the return type of - * the validator function to infer the actual type of the value and enrich the type information - * of the given input field. - * - * In effect, validators should be written like this: - * - * if (!valid(input)) { throw; } - * return input; - */ -export function expectKey R>(obj: A, key: K, validate: P, optional?: boolean): - asserts obj is A & {[k in K]: ReturnType

} { - if (typeof obj !== 'object' || obj === null || (!(key in obj) && !optional)) { - throw new Error(`Expected key '${key}' missing: ${JSON.stringify(obj)}`); - } - - if (key in obj) { - try { - validate((obj as any)[key]); - } catch (e) { - throw new Error(`${key}: ${e.message}`); - } - } -} - -export function isString(x: unknown): string { - if (typeof x !== 'string') { throw new Error(`Expected a string, got '${x}'`); } - return x; -} - -export function isMapOf(pred: (e: unknown) => T): (x: unknown) => Record { - return x => { - assertIsObject(x); - - Object.values(x).forEach(pred); - - return x as Record; - }; -} - -export function isObjectAnd(p: (x: object) => A): (x: unknown) => A { - return x => { - assertIsObject(x); - return p(x); - }; -} - -export function assertIsObject(x: unknown): asserts x is object { - if (typeof x !== 'object' || x === null) { throw new Error(`Expected a map, got '${x}'`); } -} - -export function isFileAssetPackaging(x: unknown): FileAssetPackaging { - const str = isString(x); - const validValues = Object.values(FileAssetPackaging) as string[]; // Explicit cast needed because this is a string-valued enum - if (!validValues.includes(str)) { - throw new Error(`Expected a FileAssetPackaging (one of ${validValues.map(v => `'${v}'`).join(', ')}), got '${str}'`); - } - return x as any; -} diff --git a/packages/@aws-cdk/cdk-assets-schema/lib/validate.ts b/packages/@aws-cdk/cdk-assets-schema/lib/validate.ts deleted file mode 100644 index 2660a6adae98f..0000000000000 --- a/packages/@aws-cdk/cdk-assets-schema/lib/validate.ts +++ /dev/null @@ -1,104 +0,0 @@ -import * as semver from 'semver'; -import { DockerImageAsset } from './docker-image-asset'; -import { FileAsset } from './file-asset'; -import { ManifestFile } from './manifest-schema'; -import { loadMyPackageJson } from './private/my-package-json'; -import { assertIsObject, expectKey, isFileAssetPackaging, isMapOf, isObjectAnd, isString } from './private/schema-helpers'; - -const PACKAGE_VERSION = loadMyPackageJson().version; - -/** - * Static class with loader routines - * - * This class mostly exists to put the schema structs into input position - * (taken into a function), so that the jsii-diff checker will make sure all - * structs are only allowed to be weakened in future updates. For example, - * it is now allowed to add new required fields, since old CDK frameworks - * would not be emitting those fields yet. - * - * At the same time, we might as well validate the structure so code doesn't - * barf on invalid disk input. - */ -export class AssetManifestSchema { - /** - * Validate the given structured object as a valid ManifestFile schema - */ - public static validate(file: any): asserts file is ManifestFile { - const obj: unknown = file; - - if (typeof obj !== 'object' || obj === null) { - throw new Error(`Expected object, got '${obj}`); - } - expectKey(obj, 'version', isString); - - // Current tool must be >= the version used to write the manifest - // (disregarding MVs) which we can verify by ^-prefixing the file version. - if (!semver.satisfies(AssetManifestSchema.currentVersion(), `^${obj.version}`)) { - throw new Error(`Need CDK Tools >= '${obj.version}' to read this file (current is '${AssetManifestSchema.currentVersion()}')`); - } - - expectKey(obj, 'files', isMapOf(isObjectAnd(isFileAsset)), true); - expectKey(obj, 'dockerImages', isMapOf(isObjectAnd(isDockerImageAsset)), true); - } - - /** - * Take a ManifestFile as input - * - * The presence of this method makes sure the struct is only ever weakened - * in future releases. - */ - public static input(file: ManifestFile) { - this.validate(file); - } - - /** - * Return the version of the schema module - */ - public static currentVersion(): string { - return PACKAGE_VERSION; - } -} - -function isFileAsset(entry: object): FileAsset { - expectKey(entry, 'source', source => { - assertIsObject(source); - expectKey(source, 'path', isString); - expectKey(source, 'packaging', isFileAssetPackaging, true); - return source; - }); - - expectKey(entry, 'destinations', isMapOf(destination => { - assertIsObject(destination); - expectKey(destination, 'region', isString, true); - expectKey(destination, 'assumeRoleArn', isString, true); - expectKey(destination, 'assumeRoleExternalId', isString, true); - expectKey(destination, 'bucketName', isString); - expectKey(destination, 'objectKey', isString); - return destination; - })); - - return entry; -} - -function isDockerImageAsset(entry: object): DockerImageAsset { - expectKey(entry, 'source', source => { - assertIsObject(source); - expectKey(source, 'directory', isString); - expectKey(source, 'dockerFile', isString, true); - expectKey(source, 'dockerBuildTarget', isString, true); - expectKey(source, 'dockerBuildArgs', isMapOf(isString), true); - return source; - }); - - expectKey(entry, 'destinations', isMapOf(destination => { - assertIsObject(destination); - expectKey(destination, 'region', isString, true); - expectKey(destination, 'assumeRoleArn', isString, true); - expectKey(destination, 'assumeRoleExternalId', isString, true); - expectKey(destination, 'repositoryName', isString); - expectKey(destination, 'imageTag', isString); - return destination; - })); - - return entry; -} diff --git a/packages/@aws-cdk/cdk-assets-schema/package.json b/packages/@aws-cdk/cdk-assets-schema/package.json index 43f4a115a2aec..279b10397a6db 100644 --- a/packages/@aws-cdk/cdk-assets-schema/package.json +++ b/packages/@aws-cdk/cdk-assets-schema/package.json @@ -44,9 +44,6 @@ "url": "https://aws.amazon.com", "organization": true }, - "dependencies": { - "semver": "^7.2.2" - }, "license": "Apache-2.0", "devDependencies": { "@types/jest": "^26.0.0", @@ -64,17 +61,15 @@ "cdk" ], "homepage": "https://github.com/aws/aws-cdk", - "bundledDependencies": [ - "semver" - ], "engines": { "node": ">= 10.13.0 <13 || >=13.7.0" }, - "stability": "stable", + "stability": "deprecated", "awscdkio": { "announce": false }, - "maturity": "stable", + "deprecated": "merged into @aws-cdk/cloud-assembly-schema", + "maturity": "deprecated", "cdk-build": { "jest": true } diff --git a/packages/@aws-cdk/cdk-assets-schema/test/dummy.test.ts b/packages/@aws-cdk/cdk-assets-schema/test/dummy.test.ts new file mode 100644 index 0000000000000..bdb221e0c7dc8 --- /dev/null +++ b/packages/@aws-cdk/cdk-assets-schema/test/dummy.test.ts @@ -0,0 +1,3 @@ +test('dummy', () => { + expect(true).toBeTruthy(); +}); diff --git a/packages/@aws-cdk/cloud-assembly-schema/.gitignore b/packages/@aws-cdk/cloud-assembly-schema/.gitignore index 072151f41441c..06b07c0980981 100644 --- a/packages/@aws-cdk/cloud-assembly-schema/.gitignore +++ b/packages/@aws-cdk/cloud-assembly-schema/.gitignore @@ -1,5 +1,5 @@ -lib/*.js -test/*.js +lib/**/*.js +test/**/*.js scripts/*.js *.js.map *.d.ts diff --git a/packages/@aws-cdk/cdk-assets-schema/lib/aws-destination.ts b/packages/@aws-cdk/cloud-assembly-schema/lib/assets/aws-destination.ts similarity index 100% rename from packages/@aws-cdk/cdk-assets-schema/lib/aws-destination.ts rename to packages/@aws-cdk/cloud-assembly-schema/lib/assets/aws-destination.ts diff --git a/packages/@aws-cdk/cdk-assets-schema/lib/docker-image-asset.ts b/packages/@aws-cdk/cloud-assembly-schema/lib/assets/docker-image-asset.ts similarity index 90% rename from packages/@aws-cdk/cdk-assets-schema/lib/docker-image-asset.ts rename to packages/@aws-cdk/cloud-assembly-schema/lib/assets/docker-image-asset.ts index 2538113f741f5..ebec6ab166fbb 100644 --- a/packages/@aws-cdk/cdk-assets-schema/lib/docker-image-asset.ts +++ b/packages/@aws-cdk/cloud-assembly-schema/lib/assets/docker-image-asset.ts @@ -12,7 +12,7 @@ export interface DockerImageAsset { /** * Destinations for this file asset */ - readonly destinations: Record; + readonly destinations: { [id: string]: DockerImageDestination }; } /** @@ -45,7 +45,7 @@ export interface DockerImageSource { * * @default - No additional build arguments */ - readonly dockerBuildArgs?: Record; + readonly dockerBuildArgs?: { [name: string]: string }; } /** diff --git a/packages/@aws-cdk/cdk-assets-schema/lib/file-asset.ts b/packages/@aws-cdk/cloud-assembly-schema/lib/assets/file-asset.ts similarity index 94% rename from packages/@aws-cdk/cdk-assets-schema/lib/file-asset.ts rename to packages/@aws-cdk/cloud-assembly-schema/lib/assets/file-asset.ts index 7e50514547366..efa6cd4384bbe 100644 --- a/packages/@aws-cdk/cdk-assets-schema/lib/file-asset.ts +++ b/packages/@aws-cdk/cloud-assembly-schema/lib/assets/file-asset.ts @@ -12,7 +12,7 @@ export interface FileAsset { /** * Destinations for this file asset */ - readonly destinations: Record; + readonly destinations: { [id: string]: FileDestination }; } /** diff --git a/packages/@aws-cdk/cloud-assembly-schema/lib/assets/index.ts b/packages/@aws-cdk/cloud-assembly-schema/lib/assets/index.ts new file mode 100644 index 0000000000000..49c126e3f2d9b --- /dev/null +++ b/packages/@aws-cdk/cloud-assembly-schema/lib/assets/index.ts @@ -0,0 +1,4 @@ +export * from './schema'; +export * from './docker-image-asset'; +export * from './file-asset'; +export * from './aws-destination'; diff --git a/packages/@aws-cdk/cdk-assets-schema/lib/manifest-schema.ts b/packages/@aws-cdk/cloud-assembly-schema/lib/assets/schema.ts similarity index 73% rename from packages/@aws-cdk/cdk-assets-schema/lib/manifest-schema.ts rename to packages/@aws-cdk/cloud-assembly-schema/lib/assets/schema.ts index a48094d779f3e..7f5b33da127cd 100644 --- a/packages/@aws-cdk/cdk-assets-schema/lib/manifest-schema.ts +++ b/packages/@aws-cdk/cloud-assembly-schema/lib/assets/schema.ts @@ -4,7 +4,7 @@ import { FileAsset } from './file-asset'; /** * Definitions for the asset manifest */ -export interface ManifestFile { +export interface AssetManifest { /** * Version of the manifest */ @@ -15,12 +15,12 @@ export interface ManifestFile { * * @default - No files */ - readonly files?: Record; + readonly files?: { [id: string]: FileAsset }; /** * The Docker image assets in this manifest * * @default - No Docker images */ - readonly dockerImages?: Record; -} \ No newline at end of file + readonly dockerImages?: { [id: string]: DockerImageAsset }; +} diff --git a/packages/@aws-cdk/cloud-assembly-schema/lib/artifact-schema.ts b/packages/@aws-cdk/cloud-assembly-schema/lib/cloud-assembly/artifact-schema.ts similarity index 100% rename from packages/@aws-cdk/cloud-assembly-schema/lib/artifact-schema.ts rename to packages/@aws-cdk/cloud-assembly-schema/lib/cloud-assembly/artifact-schema.ts diff --git a/packages/@aws-cdk/cloud-assembly-schema/lib/context-queries.ts b/packages/@aws-cdk/cloud-assembly-schema/lib/cloud-assembly/context-queries.ts similarity index 100% rename from packages/@aws-cdk/cloud-assembly-schema/lib/context-queries.ts rename to packages/@aws-cdk/cloud-assembly-schema/lib/cloud-assembly/context-queries.ts diff --git a/packages/@aws-cdk/cloud-assembly-schema/lib/cloud-assembly/index.ts b/packages/@aws-cdk/cloud-assembly-schema/lib/cloud-assembly/index.ts new file mode 100644 index 0000000000000..931538d80cf11 --- /dev/null +++ b/packages/@aws-cdk/cloud-assembly-schema/lib/cloud-assembly/index.ts @@ -0,0 +1,4 @@ +export * from './schema'; +export * from './metadata-schema'; +export * from './artifact-schema'; +export * from './context-queries'; diff --git a/packages/@aws-cdk/cloud-assembly-schema/lib/metadata-schema.ts b/packages/@aws-cdk/cloud-assembly-schema/lib/cloud-assembly/metadata-schema.ts similarity index 100% rename from packages/@aws-cdk/cloud-assembly-schema/lib/metadata-schema.ts rename to packages/@aws-cdk/cloud-assembly-schema/lib/cloud-assembly/metadata-schema.ts diff --git a/packages/@aws-cdk/cloud-assembly-schema/lib/schema.ts b/packages/@aws-cdk/cloud-assembly-schema/lib/cloud-assembly/schema.ts similarity index 100% rename from packages/@aws-cdk/cloud-assembly-schema/lib/schema.ts rename to packages/@aws-cdk/cloud-assembly-schema/lib/cloud-assembly/schema.ts diff --git a/packages/@aws-cdk/cloud-assembly-schema/lib/index.ts b/packages/@aws-cdk/cloud-assembly-schema/lib/index.ts index e9e9aa6a5863d..50c2dc35a5eab 100644 --- a/packages/@aws-cdk/cloud-assembly-schema/lib/index.ts +++ b/packages/@aws-cdk/cloud-assembly-schema/lib/index.ts @@ -1,5 +1,3 @@ +export * from './cloud-assembly'; +export * from './assets'; export * from './manifest'; -export * from './schema'; -export * from './metadata-schema'; -export * from './artifact-schema'; -export * from './context-queries'; diff --git a/packages/@aws-cdk/cloud-assembly-schema/lib/manifest.ts b/packages/@aws-cdk/cloud-assembly-schema/lib/manifest.ts index c2e53fe2c626b..66275b88e312b 100644 --- a/packages/@aws-cdk/cloud-assembly-schema/lib/manifest.ts +++ b/packages/@aws-cdk/cloud-assembly-schema/lib/manifest.ts @@ -1,51 +1,92 @@ import * as fs from 'fs'; import * as jsonschema from 'jsonschema'; import * as semver from 'semver'; -import { ArtifactMetadataEntryType } from './metadata-schema'; -import * as assembly from './schema'; +import * as assets from './assets'; +import * as assembly from './cloud-assembly'; // this prefix is used by the CLI to identify this specific error. // in which case we want to instruct the user to upgrade his CLI. // see exec.ts#createAssembly export const VERSION_MISMATCH: string = 'Cloud assembly schema version mismatch'; +// tslint:disable: no-var-requires + +// eslint-disable-next-line @typescript-eslint/no-require-imports +const ASSETS_SCHEMA = require('../schema/assets.schema.json'); + +// eslint-disable-next-line @typescript-eslint/no-require-imports +const ASSEMBLY_SCHEMA = require('../schema/cloud-assembly.schema.json'); + +/** + * Version is shared for both manifests + */ +// eslint-disable-next-line @typescript-eslint/no-require-imports +const SCHEMA_VERSION = require('../schema/cloud-assembly.version.json').version; + +// tslint:enable: no-var-requires + /** * Protocol utility class. */ export class Manifest { /** - * Save manifest to file. + * Validates and saves the cloud assembly manifest to file. + * + * @param manifest - manifest. + * @param filePath - output file path. + */ + public static saveAssemblyManifest(manifest: assembly.AssemblyManifest, filePath: string) { + Manifest.saveManifest(manifest, filePath, ASSEMBLY_SCHEMA); + } + + /** + * Load and validates the cloud assembly manifest from file. + * + * @param filePath - path to the manifest file. + */ + public static loadAssemblyManifest(filePath: string): assembly.AssemblyManifest { + return Manifest.loadManifest(filePath, ASSEMBLY_SCHEMA, obj => Manifest.patchStackTags(obj)); + } + + /** + * Validates and saves the asset manifest to file. * * @param manifest - manifest. + * @param filePath - output file path. */ - public static save(manifest: assembly.AssemblyManifest, filePath: string) { - fs.writeFileSync(filePath, JSON.stringify(manifest, undefined, 2)); + public static saveAssetManifest(manifest: assets.AssetManifest, filePath: string) { + Manifest.saveManifest(manifest, filePath, ASSETS_SCHEMA); } /** - * Load manifest from file. + * Load and validates the asset manifest from file. * * @param filePath - path to the manifest file. */ - public static load(filePath: string): assembly.AssemblyManifest { - const raw: assembly.AssemblyManifest = JSON.parse(fs.readFileSync(filePath, { encoding: 'utf-8' })); - Manifest.patchStackTags(raw); - Manifest.validate(raw); - return raw; + public static loadAssetManifest(filePath: string): assets.AssetManifest { + return this.loadManifest(filePath, ASSETS_SCHEMA); } /** * Fetch the current schema version number. */ public static version(): string { - // eslint-disable-next-line @typescript-eslint/no-require-imports - return require('../schema/cloud-assembly.version.json').version; + return SCHEMA_VERSION; } - // eslint-disable-next-line @typescript-eslint/no-require-imports - private static schema: jsonschema.Schema = require('../schema/cloud-assembly.schema.json'); + /** + * Deprecated + * @deprecated use `saveAssemblyManifest()` + */ + public static save(manifest: assembly.AssemblyManifest, filePath: string) { return this.saveAssemblyManifest(manifest, filePath); } + + /** + * Deprecated + * @deprecated use `loadAssemblyManifest()` + */ + public static load(filePath: string): assembly.AssemblyManifest { return this.loadAssemblyManifest(filePath); } - private static validate(manifest: assembly.AssemblyManifest) { + private static validate(manifest: { version: string }, schema: jsonschema.Schema) { function parseVersion(version: string) { const ver = semver.valid(version); @@ -67,7 +108,7 @@ export class Manifest { // now validate the format is good. const validator = new jsonschema.Validator(); - const result = validator.validate(manifest, Manifest.schema, { + const result = validator.validate(manifest, schema, { // does exist but is not in the TypeScript definitions nestedErrors: true, @@ -81,6 +122,21 @@ export class Manifest { } + private static saveManifest(manifest: any, filePath: string, schema: jsonschema.Schema) { + const withVersion = { ...manifest, version: Manifest.version() }; + Manifest.validate(withVersion, schema); + fs.writeFileSync(filePath, JSON.stringify(withVersion, undefined, 2)); + } + + private static loadManifest(filePath: string, schema: jsonschema.Schema, preprocess?: (obj: any) => any) { + let obj = JSON.parse(fs.readFileSync(filePath, { encoding: 'utf-8' })); + if (preprocess) { + obj = preprocess(obj); + } + Manifest.validate(obj, schema); + return obj; + } + /** * This requires some explaining... * @@ -101,18 +157,16 @@ export class Manifest { if (artifact.type === assembly.ArtifactType.AWS_CLOUDFORMATION_STACK) { for (const metadataEntries of Object.values(artifact.metadata || [])) { for (const metadataEntry of metadataEntries) { - if (metadataEntry.type === ArtifactMetadataEntryType.STACK_TAGS && metadataEntry.data) { - + if (metadataEntry.type === assembly.ArtifactMetadataEntryType.STACK_TAGS && metadataEntry.data) { const metadataAny = metadataEntry as any; - - metadataAny.data = metadataAny.data.map((t: any) => { - return { key: t.Key, value: t.Value }; - }); + metadataAny.data = metadataAny.data.map((t: any) => ({ key: t.Key, value: t.Value })); } } } } } + + return manifest; } private constructor() {} diff --git a/packages/@aws-cdk/cloud-assembly-schema/schema/assets.schema.json b/packages/@aws-cdk/cloud-assembly-schema/schema/assets.schema.json new file mode 100644 index 0000000000000..bbd61aae66813 --- /dev/null +++ b/packages/@aws-cdk/cloud-assembly-schema/schema/assets.schema.json @@ -0,0 +1,184 @@ +{ + "$ref": "#/definitions/AssetManifest", + "definitions": { + "AssetManifest": { + "description": "Definitions for the asset manifest", + "type": "object", + "properties": { + "version": { + "description": "Version of the manifest", + "type": "string" + }, + "files": { + "description": "The file assets in this manifest (Default - No files)", + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/FileAsset" + } + }, + "dockerImages": { + "description": "The Docker image assets in this manifest (Default - No Docker images)", + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/DockerImageAsset" + } + } + }, + "required": [ + "version" + ] + }, + "FileAsset": { + "description": "A file asset", + "type": "object", + "properties": { + "source": { + "$ref": "#/definitions/FileSource", + "description": "Source description for file assets" + }, + "destinations": { + "description": "Destinations for this file asset", + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/FileDestination" + } + } + }, + "required": [ + "destinations", + "source" + ] + }, + "FileSource": { + "description": "Describe the source of a file asset", + "type": "object", + "properties": { + "path": { + "description": "The filesystem object to upload\n\nThis path is relative to the asset manifest location.", + "type": "string" + }, + "packaging": { + "description": "Packaging method (Default FILE)", + "enum": [ + "file", + "zip" + ], + "type": "string" + } + }, + "required": [ + "path" + ] + }, + "FileDestination": { + "description": "Where in S3 a file asset needs to be published", + "type": "object", + "properties": { + "bucketName": { + "description": "The name of the bucket", + "type": "string" + }, + "objectKey": { + "description": "The destination object key", + "type": "string" + }, + "region": { + "description": "The region where this asset will need to be published (Default - Current region)", + "type": "string" + }, + "assumeRoleArn": { + "description": "The role that needs to be assumed while publishing this asset (Default - No role will be assumed)", + "type": "string" + }, + "assumeRoleExternalId": { + "description": "The ExternalId that needs to be supplied while assuming this role (Default - No ExternalId will be supplied)", + "type": "string" + } + }, + "required": [ + "bucketName", + "objectKey" + ] + }, + "DockerImageAsset": { + "description": "A file asset", + "type": "object", + "properties": { + "source": { + "$ref": "#/definitions/DockerImageSource", + "description": "Source description for file assets" + }, + "destinations": { + "description": "Destinations for this file asset", + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/DockerImageDestination" + } + } + }, + "required": [ + "destinations", + "source" + ] + }, + "DockerImageSource": { + "description": "Properties for how to produce a Docker image from a source", + "type": "object", + "properties": { + "directory": { + "description": "The directory containing the Docker image build instructions.\n\nThis path is relative to the asset manifest location.", + "type": "string" + }, + "dockerFile": { + "description": "The name of the file with build instructions (Default Dockerfile)", + "type": "string" + }, + "dockerBuildTarget": { + "description": "Target build stage in a Dockerfile with multiple build stages (Default - The last stage in the Dockerfile)", + "type": "string" + }, + "dockerBuildArgs": { + "description": "Additional build arguments (Default - No additional build arguments)", + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "required": [ + "directory" + ] + }, + "DockerImageDestination": { + "description": "Where to publish docker images", + "type": "object", + "properties": { + "repositoryName": { + "description": "Name of the ECR repository to publish to", + "type": "string" + }, + "imageTag": { + "description": "Tag of the image to publish", + "type": "string" + }, + "region": { + "description": "The region where this asset will need to be published (Default - Current region)", + "type": "string" + }, + "assumeRoleArn": { + "description": "The role that needs to be assumed while publishing this asset (Default - No role will be assumed)", + "type": "string" + }, + "assumeRoleExternalId": { + "description": "The ExternalId that needs to be supplied while assuming this role (Default - No ExternalId will be supplied)", + "type": "string" + } + }, + "required": [ + "imageTag", + "repositoryName" + ] + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/@aws-cdk/cloud-assembly-schema/scripts/update-schema.sh b/packages/@aws-cdk/cloud-assembly-schema/scripts/update-schema.sh index 424e104e1dc85..fa6322b9c29d3 100755 --- a/packages/@aws-cdk/cloud-assembly-schema/scripts/update-schema.sh +++ b/packages/@aws-cdk/cloud-assembly-schema/scripts/update-schema.sh @@ -5,8 +5,7 @@ packagedir=$(cd ${scriptsdir}/.. && pwd) # Output OUTPUT_DIR="${packagedir}/schema" -OUTPUT_FILE="${OUTPUT_DIR}/cloud-assembly.schema.json" - mkdir -p ${OUTPUT_DIR} -node -e "require('${packagedir}/scripts/update-schema.js').generate('${OUTPUT_FILE}', true)" +# regenerate JSON schema and bumps the version +node -e "require('${packagedir}/scripts/update-schema.js').update()" diff --git a/packages/@aws-cdk/cloud-assembly-schema/scripts/update-schema.ts b/packages/@aws-cdk/cloud-assembly-schema/scripts/update-schema.ts index 948295e1b5cb8..2c8ac1c50f236 100644 --- a/packages/@aws-cdk/cloud-assembly-schema/scripts/update-schema.ts +++ b/packages/@aws-cdk/cloud-assembly-schema/scripts/update-schema.ts @@ -9,26 +9,50 @@ function log(message: string) { console.log(message); } -function bump() { +/** + * Where schemas are committed. + */ +const SCHEMA_DIR = path.resolve(__dirname, '../schema'); + +const SCHEMA_DEFINITIONS: { [schemaName: string]: { rootTypeName: string } } = { + 'assets': { rootTypeName: 'AssetManifest' }, + 'cloud-assembly': { rootTypeName: 'AssemblyManifest' }, +}; + +export const SCHEMAS = Object.keys(SCHEMA_DEFINITIONS); + +export function update() { + for (const s of SCHEMAS) { + generateSchema(s); + } + + bump(); +} - const metadataPath = '../schema/cloud-assembly.version.json'; +export function bump() { + const versionFile = path.join(SCHEMA_DIR, 'cloud-assembly.version.json'); // eslint-disable-next-line @typescript-eslint/no-require-imports - const metadata = require(metadataPath); + const metadata = require(versionFile); const oldVersion = metadata.version; const newVersion = semver.inc(oldVersion, 'major'); log(`Updating schema version: ${oldVersion} -> ${newVersion}`); - - const out = path.join(__dirname, metadataPath); - fs.writeFileSync(out, JSON.stringify({version: newVersion})); - + fs.writeFileSync(versionFile, JSON.stringify({version: newVersion})); } -export function generate(out: string, shouldBump: boolean) { +/** + * Generates a schema from typescript types. + * @returns JSON schema + * @param schemaName the schema to generate + * @param shouldBump writes a new version of the schema and bumps the major version + */ +export function generateSchema(schemaName: string, saveToFile: boolean = true) { + const spec = SCHEMA_DEFINITIONS[schemaName]; + const out = saveToFile ? path.join(SCHEMA_DIR, `${schemaName}.schema.json`) : ''; - const settings = { + const settings: Partial = { required: true, ref: true, topRef: true, @@ -40,23 +64,18 @@ export function generate(out: string, shouldBump: boolean) { strictNullChecks: true, }; - const program = tjs.getProgramFromFiles([path.join(__dirname, '../lib/schema.d.ts')], compilerOptions); - const schema = tjs.generateSchema(program, 'AssemblyManifest', settings); + const program = tjs.getProgramFromFiles([ path.join(__dirname, '../lib/index.d.ts') ], compilerOptions); + const schema = tjs.generateSchema(program, spec.rootTypeName, settings); augmentDescription(schema); addAnyMetadataEntry(schema); - if (shouldBump) { - bump(); - } - if (out) { log(`Generating schema to ${out}`); fs.writeFileSync(out, JSON.stringify(schema, null, 4)); } return schema; - } /** @@ -105,5 +124,5 @@ function augmentDescription(schema: any) { * compatibility checks. */ function addAnyMetadataEntry(schema: any) { - schema.definitions.MetadataEntry.properties.data.anyOf.push({description: 'Free form data.'}); + schema.definitions.MetadataEntry?.properties.data.anyOf.push({description: 'Free form data.'}); } diff --git a/packages/@aws-cdk/cdk-assets-schema/test/validate.test.ts b/packages/@aws-cdk/cloud-assembly-schema/test/assets.test.ts similarity index 71% rename from packages/@aws-cdk/cdk-assets-schema/test/validate.test.ts rename to packages/@aws-cdk/cloud-assembly-schema/test/assets.test.ts index 145ae265aec5f..62aebfa26e6ee 100644 --- a/packages/@aws-cdk/cdk-assets-schema/test/validate.test.ts +++ b/packages/@aws-cdk/cloud-assembly-schema/test/assets.test.ts @@ -1,10 +1,13 @@ -import { AssetManifestSchema, FileAssetPackaging } from '../lib'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { FileAssetPackaging, Manifest } from '../lib'; describe('Docker image asset', () => { test('valid input', () => { expect(() => { - AssetManifestSchema.validate({ - version: AssetManifestSchema.currentVersion(), + validate({ + version: Manifest.version(), dockerImages: { asset: { source: { @@ -25,8 +28,8 @@ describe('Docker image asset', () => { test('invalid input', () => { expect(() => { - AssetManifestSchema.validate({ - version: AssetManifestSchema.currentVersion(), + validate({ + version: Manifest.version(), dockerImages: { asset: { source: {}, @@ -34,7 +37,7 @@ describe('Docker image asset', () => { }, }, }); - }).toThrow(/dockerImages: source: Expected key 'directory' missing/); + }).toThrow(/instance\.dockerImages\.asset\.source requires property \"directory\"/); }); }); @@ -42,8 +45,8 @@ describe('File asset', () => { describe('valid input', () => { test('without packaging', () => { expect(() => { - AssetManifestSchema.validate({ - version: AssetManifestSchema.currentVersion(), + validate({ + version: Manifest.version(), files: { asset: { source: { @@ -65,8 +68,8 @@ describe('File asset', () => { for (const packaging of Object.values(FileAssetPackaging)) { test(`with "${packaging}" packaging`, () => { expect(() => { - AssetManifestSchema.validate({ - version: AssetManifestSchema.currentVersion(), + validate({ + version: Manifest.version(), files: { asset: { source: { @@ -91,8 +94,8 @@ describe('File asset', () => { describe('invalid input', () => { test('bad "source.path" property', () => { expect(() => { - AssetManifestSchema.validate({ - version: AssetManifestSchema.currentVersion(), + validate({ + version: Manifest.version(), files: { asset: { source: { @@ -108,13 +111,13 @@ describe('File asset', () => { }, }, }); - }).toThrow(/Expected a string, got '3'/); + }).toThrow(/instance\.files\.asset\.source\.path is not of a type\(s\) string/); }); test('bad "source.packaging" property', () => { expect(() => { - AssetManifestSchema.validate({ - version: AssetManifestSchema.currentVersion(), + validate({ + version: Manifest.version(), files: { asset: { source: { @@ -131,7 +134,19 @@ describe('File asset', () => { }, }, }); - }).toThrow(/Expected a FileAssetPackaging \(one of [^)]+\), got 'BLACK_HOLE'/); + }).toThrow(/instance\.files\.asset\.source\.packaging is not one of enum values: file,zip/); }); }); }); + +function validate(manifest: any) { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'assets.test.')); + const filePath = path.join(dir, 'manifest.json'); + fs.writeFileSync(filePath, JSON.stringify(manifest, undefined, 2)); + try { + Manifest.loadAssetManifest(filePath); + } finally { + fs.unlinkSync(filePath); + fs.rmdirSync(dir); + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/cloud-assembly-schema/test/manifest.test.ts b/packages/@aws-cdk/cloud-assembly-schema/test/manifest.test.ts index 40d801812006c..c8e5554662164 100644 --- a/packages/@aws-cdk/cloud-assembly-schema/test/manifest.test.ts +++ b/packages/@aws-cdk/cloud-assembly-schema/test/manifest.test.ts @@ -10,29 +10,6 @@ function fixture(name: string) { return path.join(FIXTURES, name, 'manifest.json'); } -function clone(obj: any) { - return JSON.parse(JSON.stringify(obj)); -} - -function removeStringKeys(obj: any, keys: string[]) { - - function _recurse(o: any) { - for (const prop in o) { - if (keys.includes(prop) && typeof o[prop] === 'string') { - delete o[prop]; - } else if (typeof o[prop] === 'object') { - _recurse(o[prop]); - } - } - } - - const cloned = clone(obj); - _recurse(cloned); - - return cloned; - -} - test('manifest save', () => { const outdir = fs.mkdtempSync(path.join(os.tmpdir(), 'schema-tests')); @@ -40,57 +17,36 @@ test('manifest save', () => { const assemblyManifest: AssemblyManifest = { version: 'version', + runtime: { + libraries: { lib1: '1.2.3' }, + }, }; - Manifest.save(assemblyManifest, manifestFile); + Manifest.saveAssemblyManifest(assemblyManifest, manifestFile); const saved = JSON.parse(fs.readFileSync(manifestFile, { encoding: 'utf-8' })); - expect(saved).toEqual(assemblyManifest); - -}); - -test('if this test fails, run "yarn update-schema"', () => { - - // when we compare schemas we ignore changes the - // description that is generated from the ts docstrings. - const docStringFields = [ - 'description', - ]; - - // eslint-disable-next-line @typescript-eslint/no-require-imports - const schema = require('../scripts/update-schema.js'); - - const expected = removeStringKeys(schema.generate(), docStringFields); - - // eslint-disable-next-line @typescript-eslint/no-require-imports - const actual = removeStringKeys(require('../schema/cloud-assembly.schema.json'), docStringFields); - - try { - expect(actual).toEqual(expected); - } catch (err) { - // I couldn't for the life of me figure out how to provide additional error message - // to jest...any ideas? - err.message = `Whoops, Looks like the schema has changed. Did you forget to run 'yarn update-schema'?\n\n${err.message}`; - throw err; - } + expect(saved).toEqual({ + ...assemblyManifest, + version: Manifest.version(), // version is forced + }); }); test('manifest load', () => { - const loaded = Manifest.load(fixture('only-version')); + const loaded = Manifest.loadAssemblyManifest(fixture('only-version')); expect(loaded).toMatchSnapshot(); }); test('manifest load fails for invalid nested property', () => { - expect(() => Manifest.load(fixture('invalid-nested-property'))).toThrow(/Invalid assembly manifest/); + expect(() => Manifest.loadAssemblyManifest(fixture('invalid-nested-property'))).toThrow(/Invalid assembly manifest/); }); test('manifest load fails for invalid artifact type', () => { - expect(() => Manifest.load(fixture('invalid-artifact-type'))).toThrow(/Invalid assembly manifest/); + expect(() => Manifest.loadAssemblyManifest(fixture('invalid-artifact-type'))).toThrow(/Invalid assembly manifest/); }); test('manifest load fails on higher major version', () => { - expect(() => Manifest.load(fixture('high-version'))).toThrow(/Cloud assembly schema version mismatch/); + expect(() => Manifest.loadAssemblyManifest(fixture('high-version'))).toThrow(/Cloud assembly schema version mismatch/); }); // once we start introducing minor version bumps that are considered @@ -108,9 +64,10 @@ test('manifest load fails on higher minor version', () => { version: newVersion, }; - Manifest.save(assemblyManifest, manifestFile); + // can't use saveAssemblyManifest because it will force the correct version + fs.writeFileSync(manifestFile, JSON.stringify(assemblyManifest)); - expect(() => Manifest.load(manifestFile)).toThrow(/Cloud assembly schema version mismatch/); + expect(() => Manifest.loadAssemblyManifest(manifestFile)).toThrow(/Cloud assembly schema version mismatch/); } }); @@ -129,24 +86,25 @@ test('manifest load fails on higher patch version', () => { version: newVersion, }; - Manifest.save(assemblyManifest, manifestFile); + // can't use saveAssemblyManifest because it will force the correct version + fs.writeFileSync(manifestFile, JSON.stringify(assemblyManifest)); - expect(() => Manifest.load(manifestFile)).toThrow(/Cloud assembly schema version mismatch/); + expect(() => Manifest.loadAssemblyManifest(manifestFile)).toThrow(/Cloud assembly schema version mismatch/); } }); test('manifest load fails on invalid version', () => { - expect(() => Manifest.load(fixture('invalid-version'))).toThrow(/Invalid semver string/); + expect(() => Manifest.loadAssemblyManifest(fixture('invalid-version'))).toThrow(/Invalid semver string/); }); test('manifest load succeeds on unknown properties', () => { - const manifest = Manifest.load(fixture('unknown-property')); + const manifest = Manifest.loadAssemblyManifest(fixture('unknown-property')); expect(manifest.version).toEqual('0.0.0'); }); test('stack-tags are deserialized properly', () => { - const m: AssemblyManifest = Manifest.load(fixture('with-stack-tags')); + const m: AssemblyManifest = Manifest.loadAssemblyManifest(fixture('with-stack-tags')); if (m.artifacts?.stack?.metadata?.AwsCdkPlaygroundBatch[0].data) { const entry = m.artifacts.stack.metadata.AwsCdkPlaygroundBatch[0].data as StackTagsMetadataEntry; @@ -159,7 +117,7 @@ test('stack-tags are deserialized properly', () => { test('can access random metadata', () => { - const loaded = Manifest.load(fixture('random-metadata')); + const loaded = Manifest.loadAssemblyManifest(fixture('random-metadata')); const randomArray = loaded.artifacts?.stack.metadata?.AwsCdkPlaygroundBatch[0].data; const randomNumber = loaded.artifacts?.stack.metadata?.AwsCdkPlaygroundBatch[1].data; const randomMap = loaded.artifacts?.stack.metadata?.AwsCdkPlaygroundBatch[2].data; diff --git a/packages/@aws-cdk/cloud-assembly-schema/test/schema.test.ts b/packages/@aws-cdk/cloud-assembly-schema/test/schema.test.ts new file mode 100644 index 0000000000000..9178ed54bed6d --- /dev/null +++ b/packages/@aws-cdk/cloud-assembly-schema/test/schema.test.ts @@ -0,0 +1,50 @@ +import { generateSchema, SCHEMAS } from '../scripts/update-schema'; + +test('if this test fails, run "yarn update-schema"', () => { + + // when we compare schemas we ignore changes the + // description that is generated from the ts docstrings. + const docStringFields = [ + 'description', + ]; + + for (const schemaName of SCHEMAS) { + const expected = removeStringKeys(generateSchema(schemaName, false), docStringFields); + + // eslint-disable-next-line @typescript-eslint/no-require-imports + const actual = removeStringKeys(require(`../schema/${schemaName}.schema.json`), docStringFields); + + try { + expect(actual).toEqual(expected); + } catch (err) { + // I couldn't for the life of me figure out how to provide additional error message + // to jest...any ideas? + err.message = `Whoops, Looks like the schema has changed. Did you forget to run 'yarn update-schema'?\n\n${err.message}`; + throw err; + } + } + +}); + +function removeStringKeys(obj: any, keys: string[]) { + + function _recurse(o: any) { + for (const prop in o) { + if (keys.includes(prop) && typeof o[prop] === 'string') { + delete o[prop]; + } else if (typeof o[prop] === 'object') { + _recurse(o[prop]); + } + } + } + + const cloned = clone(obj); + _recurse(cloned); + + return cloned; + +} + +function clone(obj: any) { + return JSON.parse(JSON.stringify(obj)); +} diff --git a/packages/@aws-cdk/core/lib/stack-synthesizers/default-synthesizer.ts b/packages/@aws-cdk/core/lib/stack-synthesizers/default-synthesizer.ts index 2021e71355566..98111a43e0e42 100644 --- a/packages/@aws-cdk/core/lib/stack-synthesizers/default-synthesizer.ts +++ b/packages/@aws-cdk/core/lib/stack-synthesizers/default-synthesizer.ts @@ -1,4 +1,3 @@ -import * as asset_schema from '@aws-cdk/cdk-assets-schema'; import * as cxschema from '@aws-cdk/cloud-assembly-schema'; import * as cxapi from '@aws-cdk/cx-api'; import * as fs from 'fs'; @@ -197,8 +196,8 @@ export class DefaultStackSynthesizer implements IStackSynthesizer { private fileAssetPublishingRoleArn?: string; private imageAssetPublishingRoleArn?: string; - private readonly files: NonNullable = {}; - private readonly dockerImages: NonNullable = {}; + private readonly files: NonNullable = {}; + private readonly dockerImages: NonNullable = {}; constructor(private readonly props: DefaultStackSynthesizerProps = {}) { } @@ -390,8 +389,8 @@ export class DefaultStackSynthesizer implements IStackSynthesizer { const manifestFile = `${artifactId}.json`; const outPath = path.join(session.assembly.outdir, manifestFile); - const manifest: asset_schema.ManifestFile = { - version: asset_schema.AssetManifestSchema.currentVersion(), + const manifest: cxschema.AssetManifest = { + version: cxschema.Manifest.version(), files: this.files, dockerImages: this.dockerImages, }; diff --git a/packages/@aws-cdk/core/package.json b/packages/@aws-cdk/core/package.json index cee96ce573d7e..bc6acb2a443dc 100644 --- a/packages/@aws-cdk/core/package.json +++ b/packages/@aws-cdk/core/package.json @@ -169,7 +169,6 @@ "fs-extra": "^9.0.1", "minimatch": "^3.0.4", "@aws-cdk/cx-api": "0.0.0", - "@aws-cdk/cdk-assets-schema": "0.0.0", "@aws-cdk/cloud-assembly-schema": "0.0.0", "constructs": "^3.0.2" }, @@ -179,7 +178,6 @@ ], "homepage": "https://github.com/aws/aws-cdk", "peerDependencies": { - "@aws-cdk/cdk-assets-schema": "0.0.0", "@aws-cdk/cloud-assembly-schema": "0.0.0", "@aws-cdk/cx-api": "0.0.0", "constructs": "^3.0.2" diff --git a/packages/@aws-cdk/core/test/stack-synthesis/test.new-style-synthesis.ts b/packages/@aws-cdk/core/test/stack-synthesis/test.new-style-synthesis.ts index 43591b9931148..32af9084a26d1 100644 --- a/packages/@aws-cdk/core/test/stack-synthesis/test.new-style-synthesis.ts +++ b/packages/@aws-cdk/core/test/stack-synthesis/test.new-style-synthesis.ts @@ -1,4 +1,4 @@ -import * as asset_schema from '@aws-cdk/cdk-assets-schema'; +import * as cxschema from '@aws-cdk/cloud-assembly-schema'; import * as cxapi from '@aws-cdk/cx-api'; import * as fs from 'fs'; import { Test } from 'nodeunit'; @@ -40,7 +40,7 @@ export = { // THEN - the template is in the asset manifest const manifestArtifact = asm.artifacts.filter(isAssetManifest)[0]; test.ok(manifestArtifact); - const manifest: asset_schema.ManifestFile = JSON.parse(fs.readFileSync(manifestArtifact.file, { encoding: 'utf-8' })); + const manifest: cxschema.AssetManifest = JSON.parse(fs.readFileSync(manifestArtifact.file, { encoding: 'utf-8' })); const firstFile = (manifest.files ? manifest.files[Object.keys(manifest.files)[0]] : undefined) ?? {}; @@ -190,7 +190,7 @@ function isAssetManifest(x: cxapi.CloudArtifact): x is cxapi.AssetManifestArtifa return x instanceof cxapi.AssetManifestArtifact; } -function readAssetManifest(asm: cxapi.CloudAssembly): asset_schema.ManifestFile { +function readAssetManifest(asm: cxapi.CloudAssembly): cxschema.AssetManifest { const manifestArtifact = asm.artifacts.filter(isAssetManifest)[0]; if (!manifestArtifact) { throw new Error('no asset manifest in assembly'); } diff --git a/packages/@aws-cdk/core/test/test.app.ts b/packages/@aws-cdk/core/test/test.app.ts index eef7f2db88d33..e127671529712 100644 --- a/packages/@aws-cdk/core/test/test.app.ts +++ b/packages/@aws-cdk/core/test/test.app.ts @@ -293,7 +293,6 @@ export = { test.deepEqual(libs, { '@aws-cdk/core': version, '@aws-cdk/cx-api': version, - '@aws-cdk/cdk-assets-schema': version, '@aws-cdk/cloud-assembly-schema': version, 'jsii-runtime': `node.js/${process.version}`, }); diff --git a/packages/@aws-cdk/core/test/test.runtime-info.ts b/packages/@aws-cdk/core/test/test.runtime-info.ts index 84abdf017124b..e307633628c37 100644 --- a/packages/@aws-cdk/core/test/test.runtime-info.ts +++ b/packages/@aws-cdk/core/test/test.runtime-info.ts @@ -26,7 +26,6 @@ export = { '@aws-cdk/core': version, '@aws-cdk/cx-api': version, '@aws-cdk/cloud-assembly-schema': version, - '@aws-cdk/cdk-assets-schema': version, '@aws-solutions-konstruk/foo': mockVersion, 'jsii-runtime': `node.js/${process.version}`, }); diff --git a/packages/aws-cdk/lib/assets.ts b/packages/aws-cdk/lib/assets.ts index f621d1dbca514..dec8d01f52ba0 100644 --- a/packages/aws-cdk/lib/assets.ts +++ b/packages/aws-cdk/lib/assets.ts @@ -1,5 +1,4 @@ // tslint:disable-next-line:max-line-length -import * as asset_schema from '@aws-cdk/cdk-assets-schema'; import * as cxschema from '@aws-cdk/cloud-assembly-schema'; import * as cxapi from '@aws-cdk/cx-api'; import * as colors from 'colors'; @@ -60,7 +59,7 @@ async function prepareAsset(asset: cxschema.AssetMetadataEntry, assetManifest: A asset, assetManifest, toolkitInfo, - asset.packaging === 'zip' ? asset_schema.FileAssetPackaging.ZIP_DIRECTORY : asset_schema.FileAssetPackaging.FILE); + asset.packaging === 'zip' ? cxschema.FileAssetPackaging.ZIP_DIRECTORY : cxschema.FileAssetPackaging.FILE); case 'container-image': return await prepareDockerImageAsset(asset, assetManifest, toolkitInfo); default: @@ -73,9 +72,9 @@ function prepareFileAsset( asset: cxschema.FileAssetMetadataEntry, assetManifest: AssetManifestBuilder, toolkitInfo: ToolkitInfo, - packaging: asset_schema.FileAssetPackaging): Record { + packaging: cxschema.FileAssetPackaging): Record { - const extension = packaging === asset_schema.FileAssetPackaging.ZIP_DIRECTORY ? '.zip' : path.extname(asset.path); + const extension = packaging === cxschema.FileAssetPackaging.ZIP_DIRECTORY ? '.zip' : path.extname(asset.path); const baseName = `${asset.sourceHash}${extension}`; // Simplify key: assets/abcdef/abcdef.zip is kinda silly and unnecessary, so if they're the same just pick one component. const s3Prefix = asset.id === asset.sourceHash ? 'assets/' : `assets/${asset.id}/`; diff --git a/packages/aws-cdk/lib/util/asset-manifest-builder.ts b/packages/aws-cdk/lib/util/asset-manifest-builder.ts index 2e03e4e74c165..b2127c27ec30c 100644 --- a/packages/aws-cdk/lib/util/asset-manifest-builder.ts +++ b/packages/aws-cdk/lib/util/asset-manifest-builder.ts @@ -1,14 +1,14 @@ -import * as asset_schema from '@aws-cdk/cdk-assets-schema'; +import * as cxschema from '@aws-cdk/cloud-assembly-schema'; import * as cdk_assets from 'cdk-assets'; export class AssetManifestBuilder { - private readonly manifest: asset_schema.ManifestFile = { - version: asset_schema.AssetManifestSchema.currentVersion(), + private readonly manifest: cxschema.AssetManifest = { + version: cxschema.Manifest.version(), files: {}, dockerImages: {}, }; - public addFileAsset(id: string, source: asset_schema.FileSource, destination: asset_schema.FileDestination) { + public addFileAsset(id: string, source: cxschema.FileSource, destination: cxschema.FileDestination) { this.manifest.files![id] = { source, destinations: { @@ -17,7 +17,7 @@ export class AssetManifestBuilder { }; } - public addDockerImageAsset(id: string, source: asset_schema.DockerImageSource, destination: asset_schema.DockerImageDestination) { + public addDockerImageAsset(id: string, source: cxschema.DockerImageSource, destination: cxschema.DockerImageDestination) { this.manifest.dockerImages![id] = { source, destinations: { diff --git a/packages/aws-cdk/package.json b/packages/aws-cdk/package.json index 5453e9d650138..00c1ef7bd8285 100644 --- a/packages/aws-cdk/package.json +++ b/packages/aws-cdk/package.json @@ -66,7 +66,6 @@ "ts-mock-imports": "^1.2.6" }, "dependencies": { - "@aws-cdk/cdk-assets-schema": "0.0.0", "@aws-cdk/cloud-assembly-schema": "0.0.0", "@aws-cdk/cloudformation-diff": "0.0.0", "@aws-cdk/cx-api": "0.0.0", diff --git a/packages/cdk-assets/lib/asset-manifest.ts b/packages/cdk-assets/lib/asset-manifest.ts index 3353e49c0357c..d08c6f5daee61 100644 --- a/packages/cdk-assets/lib/asset-manifest.ts +++ b/packages/cdk-assets/lib/asset-manifest.ts @@ -1,5 +1,5 @@ -import { AssetManifestSchema, DockerImageDestination, DockerImageSource, FileDestination, - FileSource, ManifestFile } from '@aws-cdk/cdk-assets-schema'; +import { AssetManifest as AssetManifestSchema, DockerImageDestination, DockerImageSource, + FileDestination, FileSource, Manifest } from '@aws-cdk/cloud-assembly-schema'; import * as fs from 'fs'; import * as path from 'path'; @@ -17,10 +17,7 @@ export class AssetManifest { */ public static fromFile(fileName: string) { try { - const obj = JSON.parse(fs.readFileSync(fileName, { encoding: 'utf-8' })); - - AssetManifestSchema.validate(obj); - + const obj = Manifest.loadAssetManifest(fileName); return new AssetManifest(path.dirname(fileName), obj); } catch (e) { throw new Error(`Canot read asset manifest '${fileName}': ${e.message}`); @@ -50,7 +47,7 @@ export class AssetManifest { */ public readonly directory: string; - constructor(directory: string, private readonly manifest: ManifestFile) { + constructor(directory: string, private readonly manifest: AssetManifestSchema) { this.directory = directory; } @@ -64,7 +61,7 @@ export class AssetManifest { public select(selection?: DestinationPattern[]): AssetManifest { if (selection === undefined) { return this; } - const ret: ManifestFile & Required> + const ret: AssetManifestSchema & Required> = { version: this.manifest.version, dockerImages: {}, files: {} }; for (const assetType of ASSET_TYPES) { diff --git a/packages/cdk-assets/lib/private/handlers/files.ts b/packages/cdk-assets/lib/private/handlers/files.ts index bedf2e5a02aa8..b369bcd8117dd 100644 --- a/packages/cdk-assets/lib/private/handlers/files.ts +++ b/packages/cdk-assets/lib/private/handlers/files.ts @@ -1,4 +1,4 @@ -import { FileAssetPackaging } from '@aws-cdk/cdk-assets-schema'; +import { FileAssetPackaging } from '@aws-cdk/cloud-assembly-schema'; import { createReadStream, promises as fs } from 'fs'; import * as path from 'path'; import { FileManifestEntry } from '../../asset-manifest'; diff --git a/packages/cdk-assets/package.json b/packages/cdk-assets/package.json index 52f0a2b0d475e..5b8becf96001f 100644 --- a/packages/cdk-assets/package.json +++ b/packages/cdk-assets/package.json @@ -44,7 +44,7 @@ "pkglint": "0.0.0" }, "dependencies": { - "@aws-cdk/cdk-assets-schema": "0.0.0", + "@aws-cdk/cloud-assembly-schema": "0.0.0", "@aws-cdk/cx-api": "0.0.0", "archiver": "^4.0.1", "aws-sdk": "^2.702.0", diff --git a/packages/cdk-assets/test/docker-images.test.ts b/packages/cdk-assets/test/docker-images.test.ts index e5cf997832cf4..65b3cb2601128 100644 --- a/packages/cdk-assets/test/docker-images.test.ts +++ b/packages/cdk-assets/test/docker-images.test.ts @@ -1,6 +1,6 @@ jest.mock('child_process'); -import { AssetManifestSchema } from '@aws-cdk/cdk-assets-schema'; +import { Manifest } from '@aws-cdk/cloud-assembly-schema'; import * as mockfs from 'mock-fs'; import { AssetManifest, AssetPublishing } from '../lib'; import { mockAws, mockedApiFailure, mockedApiResult } from './mock-aws'; @@ -11,7 +11,7 @@ const absoluteDockerPath = '/simple/cdk.out/dockerdir'; beforeEach(() => { mockfs({ '/simple/cdk.out/assets.json': JSON.stringify({ - version: AssetManifestSchema.currentVersion(), + version: Manifest.version(), dockerImages: { theAsset: { source: { @@ -30,7 +30,7 @@ beforeEach(() => { }), '/simple/cdk.out/dockerdir/Dockerfile': 'FROM scratch', '/abs/cdk.out/assets.json': JSON.stringify({ - version: AssetManifestSchema.currentVersion(), + version: Manifest.version(), dockerImages: { theAsset: { source: { diff --git a/packages/cdk-assets/test/files.test.ts b/packages/cdk-assets/test/files.test.ts index cbd7b08ba32a1..f5105fda9a036 100644 --- a/packages/cdk-assets/test/files.test.ts +++ b/packages/cdk-assets/test/files.test.ts @@ -1,4 +1,4 @@ -import { AssetManifestSchema } from '@aws-cdk/cdk-assets-schema'; +import { Manifest } from '@aws-cdk/cloud-assembly-schema'; import * as mockfs from 'mock-fs'; import { AssetManifest, AssetPublishing } from '../lib'; import { mockAws, mockedApiResult, mockUpload } from './mock-aws'; @@ -7,7 +7,7 @@ let aws: ReturnType; beforeEach(() => { mockfs({ '/simple/cdk.out/assets.json': JSON.stringify({ - version: AssetManifestSchema.currentVersion(), + version: Manifest.version(), files: { theAsset: { source: { @@ -26,7 +26,7 @@ beforeEach(() => { }), '/simple/cdk.out/some_file': 'FILE_CONTENTS', '/abs/cdk.out/assets.json': JSON.stringify({ - version: AssetManifestSchema.currentVersion(), + version: Manifest.version(), files: { theAsset: { source: { diff --git a/packages/cdk-assets/test/manifest.test.ts b/packages/cdk-assets/test/manifest.test.ts index 8f5b379208d81..605d6922b5e08 100644 --- a/packages/cdk-assets/test/manifest.test.ts +++ b/packages/cdk-assets/test/manifest.test.ts @@ -1,11 +1,11 @@ -import { AssetManifestSchema } from '@aws-cdk/cdk-assets-schema'; +import { Manifest } from '@aws-cdk/cloud-assembly-schema'; import * as mockfs from 'mock-fs'; import { AssetManifest, DestinationIdentifier, DestinationPattern, DockerImageManifestEntry, FileManifestEntry } from '../lib'; beforeEach(() => { mockfs({ '/simple/cdk.out/assets.json': JSON.stringify({ - version: AssetManifestSchema.currentVersion(), + version: Manifest.version(), files: { asset1: { type: 'file', diff --git a/packages/cdk-assets/test/placeholders.test.ts b/packages/cdk-assets/test/placeholders.test.ts index 76d82fa76e497..9baf077d957e1 100644 --- a/packages/cdk-assets/test/placeholders.test.ts +++ b/packages/cdk-assets/test/placeholders.test.ts @@ -1,4 +1,4 @@ -import { AssetManifestSchema } from '@aws-cdk/cdk-assets-schema'; +import { Manifest } from '@aws-cdk/cloud-assembly-schema'; import * as mockfs from 'mock-fs'; import { AssetManifest, AssetPublishing } from '../lib'; import { mockAws, mockedApiResult } from './mock-aws'; @@ -7,7 +7,7 @@ let aws: ReturnType; beforeEach(() => { mockfs({ '/simple/cdk.out/assets.json': JSON.stringify({ - version: AssetManifestSchema.currentVersion(), + version: Manifest.version(), files: { fileAsset: { type: 'file', diff --git a/packages/cdk-assets/test/progress.test.ts b/packages/cdk-assets/test/progress.test.ts index b082e4529c69a..d18af611465ba 100644 --- a/packages/cdk-assets/test/progress.test.ts +++ b/packages/cdk-assets/test/progress.test.ts @@ -1,4 +1,4 @@ -import { AssetManifestSchema } from '@aws-cdk/cdk-assets-schema'; +import { Manifest } from '@aws-cdk/cloud-assembly-schema'; import * as mockfs from 'mock-fs'; import { AssetManifest, AssetPublishing, EventType, IPublishProgress, IPublishProgressListener } from '../lib'; import { mockAws, mockedApiResult, mockUpload } from './mock-aws'; @@ -7,7 +7,7 @@ let aws: ReturnType; beforeEach(() => { mockfs({ '/simple/cdk.out/assets.json': JSON.stringify({ - version: AssetManifestSchema.currentVersion(), + version: Manifest.version(), files: { theAsset: { source: { diff --git a/packages/cdk-assets/test/zipping.test.ts b/packages/cdk-assets/test/zipping.test.ts index fa568f3829f18..cb6c21dae68a2 100644 --- a/packages/cdk-assets/test/zipping.test.ts +++ b/packages/cdk-assets/test/zipping.test.ts @@ -1,5 +1,5 @@ // Separate test file since the archiving module doesn't work well with 'mock-fs' -import { AssetManifestSchema } from '@aws-cdk/cdk-assets-schema'; +import { Manifest } from '@aws-cdk/cloud-assembly-schema'; import { AssetManifest, AssetPublishing } from '../lib'; import * as bockfs from './bockfs'; import { mockAws, mockedApiResult, mockUpload } from './mock-aws'; @@ -8,7 +8,7 @@ let aws: ReturnType; beforeEach(() => { bockfs({ '/simple/cdk.out/assets.json': JSON.stringify({ - version: AssetManifestSchema.currentVersion(), + version: Manifest.version(), files: { theAsset: { source: { diff --git a/packages/cdk-assets/tsconfig.json b/packages/cdk-assets/tsconfig.json index 21692ad1da733..04e0404f04442 100644 --- a/packages/cdk-assets/tsconfig.json +++ b/packages/cdk-assets/tsconfig.json @@ -23,9 +23,6 @@ ], "exclude": [ "lib/init-templates/*/typescript/**/*.ts" - ], - "references": [ - { "path": "../@aws-cdk/cdk-assets-schema" } ] } diff --git a/packages/decdk/package.json b/packages/decdk/package.json index b89bb874eae53..76fb6b76b2ed7 100644 --- a/packages/decdk/package.json +++ b/packages/decdk/package.json @@ -170,7 +170,6 @@ "@aws-cdk/aws-wafregional": "0.0.0", "@aws-cdk/aws-wafv2": "0.0.0", "@aws-cdk/aws-workspaces": "0.0.0", - "@aws-cdk/cdk-assets-schema": "0.0.0", "@aws-cdk/cfnspec": "0.0.0", "@aws-cdk/cloud-assembly-schema": "0.0.0", "@aws-cdk/core": "0.0.0", diff --git a/packages/monocdk-experiment/package.json b/packages/monocdk-experiment/package.json index 10e68d49cb898..435d045416c85 100644 --- a/packages/monocdk-experiment/package.json +++ b/packages/monocdk-experiment/package.json @@ -240,7 +240,6 @@ "@aws-cdk/aws-wafregional": "0.0.0", "@aws-cdk/aws-wafv2": "0.0.0", "@aws-cdk/aws-workspaces": "0.0.0", - "@aws-cdk/cdk-assets-schema": "0.0.0", "@aws-cdk/cloud-assembly-schema": "0.0.0", "@aws-cdk/cloudformation-include": "0.0.0", "@aws-cdk/core": "0.0.0", diff --git a/tools/pkglint/lib/rules.ts b/tools/pkglint/lib/rules.ts index 439d13ad358f8..d52dfc9b6a672 100644 --- a/tools/pkglint/lib/rules.ts +++ b/tools/pkglint/lib/rules.ts @@ -808,7 +808,6 @@ export class MustDependonCdkByPointVersions extends ValidationRule { const ignore = [ '@aws-cdk/cloudformation-diff', '@aws-cdk/cfnspec', - '@aws-cdk/cdk-assets-schema', '@aws-cdk/cx-api', '@aws-cdk/cloud-assembly-schema', '@aws-cdk/region-info' From dde0ef52cc0cdbc40fd212f518f3cee4f30450b9 Mon Sep 17 00:00:00 2001 From: Pahud Hsieh Date: Tue, 23 Jun 2020 23:19:23 +0800 Subject: [PATCH 13/21] feat(efs): access point (#8631) This PR adds the [Amazon EFS Access Point ](https://docs.aws.amazon.com/efs/latest/ug/efs-access-points.html)construct support. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-efs/README.md | 21 + packages/@aws-cdk/aws-efs/lib/access-point.ts | 164 +++++ packages/@aws-cdk/aws-efs/lib/index.ts | 1 + packages/@aws-cdk/aws-efs/package.json | 6 + .../aws-efs/test/access-point.test.ts | 79 +++ .../aws-efs/test/integ.efs.expected.json | 566 ++++++++++++++++++ packages/@aws-cdk/aws-efs/test/integ.efs.ts | 26 + 7 files changed, 863 insertions(+) create mode 100644 packages/@aws-cdk/aws-efs/lib/access-point.ts create mode 100644 packages/@aws-cdk/aws-efs/test/access-point.test.ts create mode 100644 packages/@aws-cdk/aws-efs/test/integ.efs.expected.json create mode 100644 packages/@aws-cdk/aws-efs/test/integ.efs.ts diff --git a/packages/@aws-cdk/aws-efs/README.md b/packages/@aws-cdk/aws-efs/README.md index 40329a9803780..ec2ff3352755a 100644 --- a/packages/@aws-cdk/aws-efs/README.md +++ b/packages/@aws-cdk/aws-efs/README.md @@ -37,6 +37,27 @@ const fileSystem = new FileSystem(this, 'EfsFileSystem', { }); ``` +### Access Point + +An access point is an application-specific view into an EFS file system that applies an operating system user and +group, and a file system path, to any file system request made through the access point. The operating system user +and group override any identity information provided by the NFS client. The file system path is exposed as the +access point's root directory. Applications using the access point can only access data in its own directory and +below. To learn more, see [Mounting a File System Using EFS Access Points](https://docs.aws.amazon.com/efs/latest/ug/efs-access-points.html). + +Use `AccessPoint` to create an access point: + +```ts +new AccessPoint(stack, 'AccessPoint', { + fileSystem +}); +``` + +By default, when you create an access point, the root(`/`) directory is exposed to the client connecting to +the access point. You may specify custom path with the `path` property. If `path` does not exist, it will be +created with the settings defined in the `creationInfo`. See +[Creating Access Points](https://docs.aws.amazon.com/efs/latest/ug/create-access-point.html) for more details. + ### Connecting To control who can access the EFS, use the `.connections` attribute. EFS has diff --git a/packages/@aws-cdk/aws-efs/lib/access-point.ts b/packages/@aws-cdk/aws-efs/lib/access-point.ts new file mode 100644 index 0000000000000..39c9fabb68a04 --- /dev/null +++ b/packages/@aws-cdk/aws-efs/lib/access-point.ts @@ -0,0 +1,164 @@ +import { Construct, IResource, Resource, Stack } from '@aws-cdk/core'; +import { IFileSystem } from './efs-file-system'; +import { CfnAccessPoint } from './efs.generated'; + +/** + * Represents an EFS AccessPoint + */ +export interface IAccessPoint extends IResource { + /** + * The ID of the AccessPoint + * + * @attribute + */ + readonly accessPointId: string; + + /** + * The ARN of the AccessPoint + * + * @attribute + */ + readonly accessPointArn: string; +} + +/** + * Permissions as POSIX ACL + */ +export interface Acl { + /** + * Specifies the POSIX user ID to apply to the RootDirectory. Accepts values from 0 to 2^32 (4294967295). + */ + readonly ownerUid: string; + + /** + * Specifies the POSIX group ID to apply to the RootDirectory. Accepts values from 0 to 2^32 (4294967295). + */ + readonly ownerGid: string; + + /** + * Specifies the POSIX permissions to apply to the RootDirectory, in the format of an octal number representing + * the file's mode bits. + */ + readonly permissions: string; +} + +/** + * Represents the PosixUser + */ +export interface PosixUser { + /** + * The POSIX user ID used for all file system operations using this access point. + */ + readonly uid: string; + + /** + * The POSIX group ID used for all file system operations using this access point. + */ + readonly gid: string; + + /** + * Secondary POSIX group IDs used for all file system operations using this access point. + * + * @default - None + */ + readonly secondaryGids?: string[]; +} + +/** + * Properties for the AccessPoint + */ +export interface AccessPointProps { + /** + * The efs filesystem + */ + readonly fileSystem: IFileSystem; + + /** + * Specifies the POSIX IDs and permissions to apply when creating the access point's root directory. If the + * root directory specified by `path` does not exist, EFS creates the root directory and applies the + * permissions specified here. If the specified `path` does not exist, you must specify `createAcl`. + * + * @default - None. The directory specified by `path` must exist. + */ + readonly createAcl?: Acl; + + /** + * Specifies the path on the EFS file system to expose as the root directory to NFS clients using the access point + * to access the EFS file system + * + * @default '/' + */ + readonly path?: string; + + /** + * The full POSIX identity, including the user ID, group ID, and any secondary group IDs, on the access point + * that is used for all file system operations performed by NFS clients using the access point. + * + * Specify this to enforce a user identity using an access point. + * + * @see - [Enforcing a User Identity Using an Access Point](https://docs.aws.amazon.com/efs/latest/ug/efs-access-points.html) + * + * @default - user identity not enforced + */ + readonly posixUser?: PosixUser; +} + +/** + * Represents the AccessPoint + */ +export class AccessPoint extends Resource implements IAccessPoint { + /** + * Import an existing Access Point + */ + public static fromAccessPointId(scope: Construct, id: string, accessPointId: string): IAccessPoint { + class Import extends Resource implements IAccessPoint { + public readonly accessPointId = accessPointId; + public readonly accessPointArn = Stack.of(scope).formatArn({ + service: 'elasticfilesystem', + resource: 'access-point', + resourceName: accessPointId, + }); + } + return new Import(scope, id); + } + + /** + * The ARN of the Access Point + * @attribute + */ + public readonly accessPointArn: string; + + /** + * The ID of the Access Point + * @attribute + */ + public readonly accessPointId: string; + + constructor(scope: Construct, id: string, props: AccessPointProps) { + super(scope, id); + + const resource = new CfnAccessPoint(scope, 'Resource', { + fileSystemId: props.fileSystem.fileSystemId, + rootDirectory: { + creationInfo: props.createAcl ? { + ownerGid: props.createAcl.ownerGid, + ownerUid: props.createAcl.ownerUid, + permissions: props.createAcl.permissions, + } : undefined, + path: props.path, + }, + posixUser: props.posixUser ? { + uid: props.posixUser.uid, + gid: props.posixUser.gid, + secondaryGids: props.posixUser.secondaryGids, + } : undefined, + }); + + this.accessPointId = resource.ref; + this.accessPointArn = Stack.of(scope).formatArn({ + service: 'elasticfilesystem', + resource: 'access-point', + resourceName: this.accessPointId, + }); + } +} diff --git a/packages/@aws-cdk/aws-efs/lib/index.ts b/packages/@aws-cdk/aws-efs/lib/index.ts index 808b7a1162e5f..91aec43b31ea8 100644 --- a/packages/@aws-cdk/aws-efs/lib/index.ts +++ b/packages/@aws-cdk/aws-efs/lib/index.ts @@ -1,3 +1,4 @@ // AWS::EFS CloudFormation Resources: +export * from './access-point'; export * from './efs-file-system'; export * from './efs.generated'; diff --git a/packages/@aws-cdk/aws-efs/package.json b/packages/@aws-cdk/aws-efs/package.json index f17fa315f03a2..0710626c4c7e2 100644 --- a/packages/@aws-cdk/aws-efs/package.json +++ b/packages/@aws-cdk/aws-efs/package.json @@ -65,6 +65,7 @@ "devDependencies": { "@aws-cdk/assert": "0.0.0", "cdk-build-tools": "0.0.0", + "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0" }, @@ -88,6 +89,11 @@ "engines": { "node": ">= 10.13.0 <13 || >=13.7.0" }, + "awslint": { + "exclude": [ + "props-physical-name:@aws-cdk/aws-efs.AccessPointProps" + ] + }, "stability": "experimental", "maturity": "experimental", "awscdkio": { diff --git a/packages/@aws-cdk/aws-efs/test/access-point.test.ts b/packages/@aws-cdk/aws-efs/test/access-point.test.ts new file mode 100644 index 0000000000000..6b7343b1e5f52 --- /dev/null +++ b/packages/@aws-cdk/aws-efs/test/access-point.test.ts @@ -0,0 +1,79 @@ +import { expect as expectCDK, haveResource } from '@aws-cdk/assert'; +import * as ec2 from '@aws-cdk/aws-ec2'; +import { Stack } from '@aws-cdk/core'; +import { AccessPoint, FileSystem } from '../lib'; + +let stack: Stack; +let vpc: ec2.Vpc; +let fileSystem: FileSystem; + +beforeEach(() => { + stack = new Stack(); + vpc = new ec2.Vpc(stack, 'VPC'); + fileSystem = new FileSystem(stack, 'EfsFileSystem', { + vpc, + }); +}); + +test('default access point is created correctly', () => { + // WHEN + new AccessPoint(stack, 'MyAccessPoint', { + fileSystem, + }); + // THEN + expectCDK(stack).to(haveResource('AWS::EFS::AccessPoint')); +}); + +test('import correctly', () => { + // WHEN + const ap = new AccessPoint(stack, 'MyAccessPoint', { + fileSystem, + }); + const imported = AccessPoint.fromAccessPointId(stack, 'ImportedAccessPoint', ap.accessPointId); + // THEN + expect(imported.accessPointId).toEqual(ap.accessPointId); +}); + +test('custom access point is created correctly', () => { + // WHEN + new AccessPoint(stack, 'MyAccessPoint', { + fileSystem, + createAcl: { + ownerGid: '1000', + ownerUid: '1000', + permissions: '755', + }, + path: '/export/share', + posixUser: { + gid: '1000', + uid: '1000', + secondaryGids: [ + '1001', + '1002', + ], + }, + + }); + // THEN + expectCDK(stack).to(haveResource('AWS::EFS::AccessPoint', { + FileSystemId: { + Ref: 'EfsFileSystem37910666', + }, + PosixUser: { + Gid: '1000', + SecondaryGids: [ + '1001', + '1002', + ], + Uid: '1000', + }, + RootDirectory: { + CreationInfo: { + OwnerGid: '1000', + OwnerUid: '1000', + Permissions: '755', + }, + Path: '/export/share', + }, + })); +}); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-efs/test/integ.efs.expected.json b/packages/@aws-cdk/aws-efs/test/integ.efs.expected.json new file mode 100644 index 0000000000000..f5eeff3153703 --- /dev/null +++ b/packages/@aws-cdk/aws-efs/test/integ.efs.expected.json @@ -0,0 +1,566 @@ +{ + "Resources": { + "Vpc8378EB38": { + "Type": "AWS::EC2::VPC", + "Properties": { + "CidrBlock": "10.0.0.0/16", + "EnableDnsHostnames": true, + "EnableDnsSupport": true, + "InstanceTenancy": "default", + "Tags": [ + { + "Key": "Name", + "Value": "test-efs-integ/Vpc" + } + ] + } + }, + "VpcPublicSubnet1Subnet5C2D37C4": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.0.0/19", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1a", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + }, + { + "Key": "Name", + "Value": "test-efs-integ/Vpc/PublicSubnet1" + } + ] + } + }, + "VpcPublicSubnet1RouteTable6C95E38E": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "test-efs-integ/Vpc/PublicSubnet1" + } + ] + } + }, + "VpcPublicSubnet1RouteTableAssociation97140677": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet1RouteTable6C95E38E" + }, + "SubnetId": { + "Ref": "VpcPublicSubnet1Subnet5C2D37C4" + } + } + }, + "VpcPublicSubnet1DefaultRoute3DA9E72A": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet1RouteTable6C95E38E" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VpcIGWD7BA715C" + } + }, + "DependsOn": [ + "VpcVPCGWBF912B6E" + ] + }, + "VpcPublicSubnet1EIPD7E02669": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc", + "Tags": [ + { + "Key": "Name", + "Value": "test-efs-integ/Vpc/PublicSubnet1" + } + ] + } + }, + "VpcPublicSubnet1NATGateway4D7517AA": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "VpcPublicSubnet1EIPD7E02669", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "VpcPublicSubnet1Subnet5C2D37C4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "test-efs-integ/Vpc/PublicSubnet1" + } + ] + } + }, + "VpcPublicSubnet2Subnet691E08A3": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.32.0/19", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1b", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + }, + { + "Key": "Name", + "Value": "test-efs-integ/Vpc/PublicSubnet2" + } + ] + } + }, + "VpcPublicSubnet2RouteTable94F7E489": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "test-efs-integ/Vpc/PublicSubnet2" + } + ] + } + }, + "VpcPublicSubnet2RouteTableAssociationDD5762D8": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet2RouteTable94F7E489" + }, + "SubnetId": { + "Ref": "VpcPublicSubnet2Subnet691E08A3" + } + } + }, + "VpcPublicSubnet2DefaultRoute97F91067": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet2RouteTable94F7E489" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VpcIGWD7BA715C" + } + }, + "DependsOn": [ + "VpcVPCGWBF912B6E" + ] + }, + "VpcPublicSubnet3SubnetBE12F0B6": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.64.0/19", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1c", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + }, + { + "Key": "Name", + "Value": "test-efs-integ/Vpc/PublicSubnet3" + } + ] + } + }, + "VpcPublicSubnet3RouteTable93458DBB": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "test-efs-integ/Vpc/PublicSubnet3" + } + ] + } + }, + "VpcPublicSubnet3RouteTableAssociation1F1EDF02": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet3RouteTable93458DBB" + }, + "SubnetId": { + "Ref": "VpcPublicSubnet3SubnetBE12F0B6" + } + } + }, + "VpcPublicSubnet3DefaultRoute4697774F": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet3RouteTable93458DBB" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VpcIGWD7BA715C" + } + }, + "DependsOn": [ + "VpcVPCGWBF912B6E" + ] + }, + "VpcPrivateSubnet1Subnet536B997A": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.96.0/19", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1a", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + }, + { + "Key": "Name", + "Value": "test-efs-integ/Vpc/PrivateSubnet1" + } + ] + } + }, + "VpcPrivateSubnet1RouteTableB2C5B500": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "test-efs-integ/Vpc/PrivateSubnet1" + } + ] + } + }, + "VpcPrivateSubnet1RouteTableAssociation70C59FA6": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet1RouteTableB2C5B500" + }, + "SubnetId": { + "Ref": "VpcPrivateSubnet1Subnet536B997A" + } + } + }, + "VpcPrivateSubnet1DefaultRouteBE02A9ED": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet1RouteTableB2C5B500" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VpcPublicSubnet1NATGateway4D7517AA" + } + } + }, + "VpcPrivateSubnet2Subnet3788AAA1": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.128.0/19", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1b", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + }, + { + "Key": "Name", + "Value": "test-efs-integ/Vpc/PrivateSubnet2" + } + ] + } + }, + "VpcPrivateSubnet2RouteTableA678073B": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "test-efs-integ/Vpc/PrivateSubnet2" + } + ] + } + }, + "VpcPrivateSubnet2RouteTableAssociationA89CAD56": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet2RouteTableA678073B" + }, + "SubnetId": { + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" + } + } + }, + "VpcPrivateSubnet2DefaultRoute060D2087": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet2RouteTableA678073B" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VpcPublicSubnet1NATGateway4D7517AA" + } + } + }, + "VpcPrivateSubnet3SubnetF258B56E": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.160.0/19", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1c", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + }, + { + "Key": "Name", + "Value": "test-efs-integ/Vpc/PrivateSubnet3" + } + ] + } + }, + "VpcPrivateSubnet3RouteTableD98824C7": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "test-efs-integ/Vpc/PrivateSubnet3" + } + ] + } + }, + "VpcPrivateSubnet3RouteTableAssociation16BDDC43": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet3RouteTableD98824C7" + }, + "SubnetId": { + "Ref": "VpcPrivateSubnet3SubnetF258B56E" + } + } + }, + "VpcPrivateSubnet3DefaultRoute94B74F0D": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet3RouteTableD98824C7" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VpcPublicSubnet1NATGateway4D7517AA" + } + } + }, + "VpcIGWD7BA715C": { + "Type": "AWS::EC2::InternetGateway", + "Properties": { + "Tags": [ + { + "Key": "Name", + "Value": "test-efs-integ/Vpc" + } + ] + } + }, + "VpcVPCGWBF912B6E": { + "Type": "AWS::EC2::VPCGatewayAttachment", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "InternetGatewayId": { + "Ref": "VpcIGWD7BA715C" + } + } + }, + "FileSystem8A8E25C0": { + "Type": "AWS::EFS::FileSystem", + "Properties": { + "FileSystemTags": [ + { + "Key": "Name", + "Value": "test-efs-integ/FileSystem" + } + ] + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "FileSystemEfsSecurityGroup212D3ACB": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "test-efs-integ/FileSystem/EfsSecurityGroup", + "SecurityGroupEgress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow all outbound traffic by default", + "IpProtocol": "-1" + } + ], + "Tags": [ + { + "Key": "Name", + "Value": "test-efs-integ/FileSystem" + } + ], + "VpcId": { + "Ref": "Vpc8378EB38" + } + } + }, + "FileSystemEfsMountTarget1586453F0": { + "Type": "AWS::EFS::MountTarget", + "Properties": { + "FileSystemId": { + "Ref": "FileSystem8A8E25C0" + }, + "SecurityGroups": [ + { + "Fn::GetAtt": [ + "FileSystemEfsSecurityGroup212D3ACB", + "GroupId" + ] + } + ], + "SubnetId": { + "Ref": "VpcPrivateSubnet1Subnet536B997A" + } + } + }, + "FileSystemEfsMountTarget24B8EBB43": { + "Type": "AWS::EFS::MountTarget", + "Properties": { + "FileSystemId": { + "Ref": "FileSystem8A8E25C0" + }, + "SecurityGroups": [ + { + "Fn::GetAtt": [ + "FileSystemEfsSecurityGroup212D3ACB", + "GroupId" + ] + } + ], + "SubnetId": { + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" + } + } + }, + "FileSystemEfsMountTarget37C2F9139": { + "Type": "AWS::EFS::MountTarget", + "Properties": { + "FileSystemId": { + "Ref": "FileSystem8A8E25C0" + }, + "SecurityGroups": [ + { + "Fn::GetAtt": [ + "FileSystemEfsSecurityGroup212D3ACB", + "GroupId" + ] + } + ], + "SubnetId": { + "Ref": "VpcPrivateSubnet3SubnetF258B56E" + } + } + }, + "Resource": { + "Type": "AWS::EFS::AccessPoint", + "Properties": { + "FileSystemId": { + "Ref": "FileSystem8A8E25C0" + }, + "PosixUser": { + "Gid": "1000", + "Uid": "1000" + }, + "RootDirectory": { + "CreationInfo": { + "OwnerGid": "1000", + "OwnerUid": "1000", + "Permissions": "755" + }, + "Path": "/custom-path" + } + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-efs/test/integ.efs.ts b/packages/@aws-cdk/aws-efs/test/integ.efs.ts new file mode 100644 index 0000000000000..3906e374b8130 --- /dev/null +++ b/packages/@aws-cdk/aws-efs/test/integ.efs.ts @@ -0,0 +1,26 @@ +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as cdk from '@aws-cdk/core'; +import { AccessPoint, FileSystem } from '../lib'; + +const app = new cdk.App(); +const stack = new cdk.Stack(app, 'test-efs-integ'); + +const vpc = new ec2.Vpc(stack, 'Vpc', { maxAzs: 3, natGateways: 1}); + +const fileSystem = new FileSystem(stack, 'FileSystem', { + vpc, +}); + +new AccessPoint(stack, 'AccessPoint', { + fileSystem, + createAcl: { + ownerGid: '1000', + ownerUid: '1000', + permissions: '755', + }, + path: '/custom-path', + posixUser: { + gid: '1000', + uid: '1000', + }, +}); From 61434ebd5bf46158ea16f5c4a45955074a65f83e Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Wed, 24 Jun 2020 01:08:52 +0000 Subject: [PATCH 14/21] chore(deps): bump aws-sdk from 2.702.0 to 2.703.0 (#8704) Bumps [aws-sdk](https://github.com/aws/aws-sdk-js) from 2.702.0 to 2.703.0. - [Release notes](https://github.com/aws/aws-sdk-js/releases) - [Changelog](https://github.com/aws/aws-sdk-js/blob/master/CHANGELOG.md) - [Commits](https://github.com/aws/aws-sdk-js/compare/v2.702.0...v2.703.0) Signed-off-by: dependabot-preview[bot] Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com> --- packages/@aws-cdk/aws-cloudfront/package.json | 2 +- packages/@aws-cdk/aws-cloudtrail/package.json | 2 +- packages/@aws-cdk/aws-codebuild/package.json | 2 +- packages/@aws-cdk/aws-codecommit/package.json | 2 +- packages/@aws-cdk/aws-dynamodb/package.json | 2 +- packages/@aws-cdk/aws-eks/package.json | 2 +- packages/@aws-cdk/aws-events-targets/package.json | 2 +- packages/@aws-cdk/aws-lambda/package.json | 2 +- packages/@aws-cdk/aws-route53/package.json | 2 +- packages/@aws-cdk/aws-sqs/package.json | 2 +- packages/@aws-cdk/custom-resources/package.json | 2 +- packages/aws-cdk/package.json | 2 +- packages/cdk-assets/package.json | 2 +- yarn.lock | 8 ++++---- 14 files changed, 17 insertions(+), 17 deletions(-) diff --git a/packages/@aws-cdk/aws-cloudfront/package.json b/packages/@aws-cdk/aws-cloudfront/package.json index 832a9d018c362..50148bd7eb8af 100644 --- a/packages/@aws-cdk/aws-cloudfront/package.json +++ b/packages/@aws-cdk/aws-cloudfront/package.json @@ -64,7 +64,7 @@ "devDependencies": { "@aws-cdk/assert": "0.0.0", "@types/nodeunit": "^0.0.31", - "aws-sdk": "^2.702.0", + "aws-sdk": "^2.703.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-cloudtrail/package.json b/packages/@aws-cdk/aws-cloudtrail/package.json index c043cc6d8eace..c0f699140ba69 100644 --- a/packages/@aws-cdk/aws-cloudtrail/package.json +++ b/packages/@aws-cdk/aws-cloudtrail/package.json @@ -64,7 +64,7 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", - "aws-sdk": "^2.702.0", + "aws-sdk": "^2.703.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-codebuild/package.json b/packages/@aws-cdk/aws-codebuild/package.json index b02ad1fce2c75..f72cf8072fa80 100644 --- a/packages/@aws-cdk/aws-codebuild/package.json +++ b/packages/@aws-cdk/aws-codebuild/package.json @@ -70,7 +70,7 @@ "@aws-cdk/aws-sns": "0.0.0", "@aws-cdk/aws-sqs": "0.0.0", "@types/nodeunit": "^0.0.31", - "aws-sdk": "^2.702.0", + "aws-sdk": "^2.703.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-codecommit/package.json b/packages/@aws-cdk/aws-codecommit/package.json index 8948b2e5409f5..39f382e390a3f 100644 --- a/packages/@aws-cdk/aws-codecommit/package.json +++ b/packages/@aws-cdk/aws-codecommit/package.json @@ -70,7 +70,7 @@ "@aws-cdk/assert": "0.0.0", "@aws-cdk/aws-sns": "0.0.0", "@types/nodeunit": "^0.0.31", - "aws-sdk": "^2.702.0", + "aws-sdk": "^2.703.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-dynamodb/package.json b/packages/@aws-cdk/aws-dynamodb/package.json index 8b2443716af69..8be1059887c3a 100644 --- a/packages/@aws-cdk/aws-dynamodb/package.json +++ b/packages/@aws-cdk/aws-dynamodb/package.json @@ -65,7 +65,7 @@ "devDependencies": { "@aws-cdk/assert": "0.0.0", "@types/jest": "^26.0.0", - "aws-sdk": "^2.702.0", + "aws-sdk": "^2.703.0", "aws-sdk-mock": "^5.1.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", diff --git a/packages/@aws-cdk/aws-eks/package.json b/packages/@aws-cdk/aws-eks/package.json index 4baf3c26c1287..9a7d145f01c93 100644 --- a/packages/@aws-cdk/aws-eks/package.json +++ b/packages/@aws-cdk/aws-eks/package.json @@ -64,7 +64,7 @@ "devDependencies": { "@aws-cdk/assert": "0.0.0", "@types/nodeunit": "^0.0.31", - "aws-sdk": "^2.702.0", + "aws-sdk": "^2.703.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-events-targets/package.json b/packages/@aws-cdk/aws-events-targets/package.json index 3c08a160af05f..18f0665c69a02 100644 --- a/packages/@aws-cdk/aws-events-targets/package.json +++ b/packages/@aws-cdk/aws-events-targets/package.json @@ -68,7 +68,7 @@ "devDependencies": { "@aws-cdk/assert": "0.0.0", "@aws-cdk/aws-codecommit": "0.0.0", - "aws-sdk": "^2.702.0", + "aws-sdk": "^2.703.0", "aws-sdk-mock": "^5.1.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", diff --git a/packages/@aws-cdk/aws-lambda/package.json b/packages/@aws-cdk/aws-lambda/package.json index 7c9a4917451be..6b79895bf02c6 100644 --- a/packages/@aws-cdk/aws-lambda/package.json +++ b/packages/@aws-cdk/aws-lambda/package.json @@ -71,7 +71,7 @@ "@types/lodash": "^4.14.156", "@types/nodeunit": "^0.0.31", "@types/sinon": "^9.0.4", - "aws-sdk": "^2.702.0", + "aws-sdk": "^2.703.0", "aws-sdk-mock": "^5.1.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", diff --git a/packages/@aws-cdk/aws-route53/package.json b/packages/@aws-cdk/aws-route53/package.json index 82da2e16f98eb..60b155b6bcd7f 100644 --- a/packages/@aws-cdk/aws-route53/package.json +++ b/packages/@aws-cdk/aws-route53/package.json @@ -64,7 +64,7 @@ "devDependencies": { "@aws-cdk/assert": "0.0.0", "@types/nodeunit": "^0.0.31", - "aws-sdk": "^2.702.0", + "aws-sdk": "^2.703.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-sqs/package.json b/packages/@aws-cdk/aws-sqs/package.json index 9f5091f3f5afb..f2dc1fc22533f 100644 --- a/packages/@aws-cdk/aws-sqs/package.json +++ b/packages/@aws-cdk/aws-sqs/package.json @@ -65,7 +65,7 @@ "@aws-cdk/assert": "0.0.0", "@aws-cdk/aws-s3": "0.0.0", "@types/nodeunit": "^0.0.31", - "aws-sdk": "^2.702.0", + "aws-sdk": "^2.703.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/custom-resources/package.json b/packages/@aws-cdk/custom-resources/package.json index 580f3245f7646..79b0fd59e5ee6 100644 --- a/packages/@aws-cdk/custom-resources/package.json +++ b/packages/@aws-cdk/custom-resources/package.json @@ -73,7 +73,7 @@ "@types/aws-lambda": "^8.10.39", "@types/fs-extra": "^8.1.0", "@types/sinon": "^9.0.4", - "aws-sdk": "^2.702.0", + "aws-sdk": "^2.703.0", "aws-sdk-mock": "^5.1.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", diff --git a/packages/aws-cdk/package.json b/packages/aws-cdk/package.json index 00c1ef7bd8285..4fff1488d162d 100644 --- a/packages/aws-cdk/package.json +++ b/packages/aws-cdk/package.json @@ -71,7 +71,7 @@ "@aws-cdk/cx-api": "0.0.0", "@aws-cdk/region-info": "0.0.0", "archiver": "^4.0.1", - "aws-sdk": "^2.702.0", + "aws-sdk": "^2.703.0", "camelcase": "^6.0.0", "cdk-assets": "0.0.0", "colors": "^1.4.0", diff --git a/packages/cdk-assets/package.json b/packages/cdk-assets/package.json index 5b8becf96001f..859e64f9df617 100644 --- a/packages/cdk-assets/package.json +++ b/packages/cdk-assets/package.json @@ -47,7 +47,7 @@ "@aws-cdk/cloud-assembly-schema": "0.0.0", "@aws-cdk/cx-api": "0.0.0", "archiver": "^4.0.1", - "aws-sdk": "^2.702.0", + "aws-sdk": "^2.703.0", "glob": "^7.1.6", "yargs": "^15.3.1" }, diff --git a/yarn.lock b/yarn.lock index 39735396bccfe..6ec01fe2031ce 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2124,10 +2124,10 @@ aws-sdk-mock@^5.1.0: sinon "^9.0.1" traverse "^0.6.6" -aws-sdk@^2.637.0, aws-sdk@^2.702.0: - version "2.702.0" - resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.702.0.tgz#c5bb9a6caeb7d938b803095073fe981ff29e4244" - integrity sha512-FKRve3NOKeUKxFXeD6VfiIhXpIhym/yFdy7higxUObmsj2ssM/e7Iud79gUHRAKJW5fzusdtkBCAVBjotRGxew== +aws-sdk@^2.637.0, aws-sdk@^2.703.0: + version "2.703.0" + resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.703.0.tgz#b22a65094c65109ce52c206e5f46e05247c8aaf4" + integrity sha512-iMJueMVDp2fqopgpjPfejyFaxaksYYdRJ7bxzWEYSxR1UoSf6V9zgcrgkF+SgoxiKJ2rxsbPxhoPu2MV//b9xA== dependencies: buffer "4.9.2" events "1.1.1" From 9568bf7cae1e634b9736378c3660515ede5f4b17 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Wed, 24 Jun 2020 04:10:29 +0000 Subject: [PATCH 15/21] chore(deps): bump uuid from 8.1.0 to 8.2.0 (#8705) Bumps [uuid](https://github.com/uuidjs/uuid) from 8.1.0 to 8.2.0. - [Release notes](https://github.com/uuidjs/uuid/releases) - [Changelog](https://github.com/uuidjs/uuid/blob/master/CHANGELOG.md) - [Commits](https://github.com/uuidjs/uuid/compare/v8.1.0...v8.2.0) Signed-off-by: dependabot-preview[bot] Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com> --- packages/aws-cdk/package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/aws-cdk/package.json b/packages/aws-cdk/package.json index 4fff1488d162d..5895dc62b74e1 100644 --- a/packages/aws-cdk/package.json +++ b/packages/aws-cdk/package.json @@ -85,7 +85,7 @@ "semver": "^7.2.2", "source-map-support": "^0.5.19", "table": "^5.4.6", - "uuid": "^8.1.0", + "uuid": "^8.2.0", "wrap-ansi": "^7.0.0", "yaml": "^1.10.0", "yargs": "^15.3.1" diff --git a/yarn.lock b/yarn.lock index 6ec01fe2031ce..ceb77319b9efd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9742,10 +9742,10 @@ uuid@^3.0.1, uuid@^3.3.2, uuid@^3.3.3: resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== -uuid@^8.1.0: - version "8.1.0" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.1.0.tgz#6f1536eb43249f473abc6bd58ff983da1ca30d8d" - integrity sha512-CI18flHDznR0lq54xBycOVmphdCYnQLKn8abKn7PXUiKUGdEd+/l9LWNJmugXel4hXq7S+RMNl34ecyC9TntWg== +uuid@^8.2.0: + version "8.2.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.2.0.tgz#cb10dd6b118e2dada7d0cd9730ba7417c93d920e" + integrity sha512-CYpGiFTUrmI6OBMkAdjSDM0k5h8SkkiTP4WAjQgDgNB1S3Ou9VBEvr6q0Kv2H1mMk7IWfxYGpMH5sd5AvcIV2Q== v8-compile-cache@^2.0.3: version "2.1.0" From 6e6b23e329d8a1b6455210768371a5ab9de478ef Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Wed, 24 Jun 2020 12:24:47 +0200 Subject: [PATCH 16/21] fix(cli): cannot change policies or trust after initial bootstrap (#8677) Our nested stack deployment optimizer only used to look at template differences in order to be able to skip deployments. This has been enhanced over time, but stack parameters were still not included in the evaluation. The new bootstrapping experience relies heavily on parameters to configure important aspects such as policies and trust. However, due to the stack deployment skipping, you would not be able to reconfigure trust or policies after the initial deployment. Now takes parameters into account as well. Relates to #8571, relates to #6582, fixes #6581. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/aws-cdk/lib/api/deploy-stack.ts | 48 ++++++++---- .../aws-cdk/lib/api/util/cloudformation.ts | 76 ++++++++++++++++--- .../aws-cdk/test/api/deploy-stack.test.ts | 58 ++++++++++++++ .../aws-cdk/test/util/cloudformation.test.ts | 61 +++++++++++---- 4 files changed, 203 insertions(+), 40 deletions(-) diff --git a/packages/aws-cdk/lib/api/deploy-stack.ts b/packages/aws-cdk/lib/api/deploy-stack.ts index e7c7999b1a542..bf9267986bdea 100644 --- a/packages/aws-cdk/lib/api/deploy-stack.ts +++ b/packages/aws-cdk/lib/api/deploy-stack.ts @@ -10,7 +10,7 @@ import { publishAssets } from '../util/asset-publishing'; import { contentHash } from '../util/content-hash'; import { ISDK, SdkProvider } from './aws-auth'; import { ToolkitInfo } from './toolkit-info'; -import { changeSetHasNoChanges, CloudFormationStack, TemplateParameters, waitForChangeSet, waitForStack } from './util/cloudformation'; +import { changeSetHasNoChanges, CloudFormationStack, StackParameters, TemplateParameters, waitForChangeSet, waitForStack } from './util/cloudformation'; import { StackActivityMonitor } from './util/cloudformation/stack-activity-monitor'; // We need to map regions to domain suffixes, and the SDK already has a function to do this. @@ -186,7 +186,20 @@ export async function deployStack(options: DeployStackOptions): Promise { +async function canSkipDeploy( + deployStackOptions: DeployStackOptions, + cloudFormationStack: CloudFormationStack, + params: StackParameters): Promise { + const deployName = deployStackOptions.deployName || deployStackOptions.stack.stackName; debug(`${deployName}: checking if we can skip deploy`); @@ -406,6 +418,12 @@ async function canSkipDeploy(deployStackOptions: DeployStackOptions, cloudFormat return false; } + // Parameters have changed + if (params.changed) { + debug(`${deployName}: parameters have changed`); + return false; + } + // We can skip deploy return true; } diff --git a/packages/aws-cdk/lib/api/util/cloudformation.ts b/packages/aws-cdk/lib/api/util/cloudformation.ts index c0ec78e05decd..1f88754b1c405 100644 --- a/packages/aws-cdk/lib/api/util/cloudformation.ts +++ b/packages/aws-cdk/lib/api/util/cloudformation.ts @@ -9,6 +9,7 @@ export type Template = { }; interface TemplateParameter { + Type: string; Default?: any; [key: string]: any; } @@ -122,7 +123,21 @@ export class CloudFormationStack { * Empty list if the stack does not exist. */ public get parameterNames(): string[] { - return this.exists ? (this.stack!.Parameters || []).map(p => p.ParameterKey!) : []; + return Object.keys(this.parameters); + } + + /** + * Return the names and values of all current parameters to the stack + * + * Empty object if the stack does not exist. + */ + public get parameters(): Record { + if (!this.exists) { return {}; } + const ret: Record = {}; + for (const param of this.stack!.Parameters ?? []) { + ret[param.ParameterKey!] = param.ParameterValue!; + } + return ret; } /** @@ -286,23 +301,58 @@ export class TemplateParameters { } /** - * Return the set of CloudFormation parameters to pass to the CreateStack or UpdateStack API + * Calculate stack parameters to pass from the given desired parameter values + * + * Will throw if parameters without a Default value or a Previous value are not + * supplied. + */ + public toStackParameters(updates: Record): StackParameters { + return new StackParameters(this.params, updates); + } + + /** + * From the template, the given desired values and the current values, calculate the changes to the stack parameters * * Will take into account parameters already set on the template (will emit * 'UsePreviousValue: true' for those unless the value is changed), and will * throw if parameters without a Default value or a Previous value are not * supplied. */ - public makeApiParameters(updates: Record, prevParams: string[]): CloudFormation.Parameter[] { + public diff(updates: Record, previousValues: Record): StackParameters { + return new StackParameters(this.params, updates, previousValues); + } +} + +export class StackParameters { + /** + * The CloudFormation parameters to pass to the CreateStack or UpdateStack API + */ + public readonly apiParameters: CloudFormation.Parameter[] = []; + + private _changes = false; + + constructor( + private readonly params: Record, + updates: Record, + previousValues: Record = {}) { + const missingRequired = new Array(); - const ret: CloudFormation.Parameter[] = []; for (const [key, param] of Object.entries(this.params)) { + // If any of the parameters are SSM parameters, they will always lead to a change + if (param.Type.startsWith('AWS::SSM::Parameter::')) { + this._changes = true; + } if (key in updates && updates[key]) { - ret.push({ ParameterKey: key, ParameterValue: updates[key] }); - } else if (prevParams.includes(key)) { - ret.push({ ParameterKey: key, UsePreviousValue: true }); + this.apiParameters.push({ ParameterKey: key, ParameterValue: updates[key] }); + + // If the updated value is different than the current value, this will lead to a change + if (!(key in previousValues) || updates[key] !== previousValues[key]) { + this._changes = true; + } + } else if (key in previousValues) { + this.apiParameters.push({ ParameterKey: key, UsePreviousValue: true }); } else if (param.Default === undefined) { missingRequired.push(key); } @@ -318,10 +368,14 @@ export class TemplateParameters { const unknownParam = ([key, _]: [string, any]) => this.params[key] === undefined; const hasValue = ([_, value]: [string, any]) => !!value; for (const [key, value] of Object.entries(updates).filter(unknownParam).filter(hasValue)) { - ret.push({ ParameterKey: key, ParameterValue: value }); + this.apiParameters.push({ ParameterKey: key, ParameterValue: value }); } + } - return ret; - + /** + * Whether this set of parameter updates will change the actual stack values + */ + public get changed() { + return this._changes; } -} +} \ No newline at end of file diff --git a/packages/aws-cdk/test/api/deploy-stack.test.ts b/packages/aws-cdk/test/api/deploy-stack.test.ts index 3f9f33b20342d..585665b3e23d9 100644 --- a/packages/aws-cdk/test/api/deploy-stack.test.ts +++ b/packages/aws-cdk/test/api/deploy-stack.test.ts @@ -191,6 +191,64 @@ test('deploy is skipped if template did not change', async () => { expect(cfnMocks.executeChangeSet).not.toBeCalled(); }); +test('deploy is skipped if parameters are the same', async () => { + // GIVEN + givenTemplateIs(FAKE_STACK_WITH_PARAMETERS.template); + givenStackExists({ + Parameters: [ + { ParameterKey: 'HasValue', ParameterValue: 'HasValue' }, + { ParameterKey: 'HasDefault', ParameterValue: 'HasDefault' }, + { ParameterKey: 'OtherParameter', ParameterValue: 'OtherParameter' }, + ], + }); + + // WHEN + await deployStack({ + stack: FAKE_STACK_WITH_PARAMETERS, + sdk, + sdkProvider, + resolvedEnvironment: mockResolvedEnvironment(), + parameters: {}, + usePreviousParameters: true, + }); + + // THEN + expect(cfnMocks.createChangeSet).not.toHaveBeenCalled(); +}); + +test('deploy is not skipped if parameters are different', async () => { + // GIVEN + givenTemplateIs(FAKE_STACK_WITH_PARAMETERS.template); + givenStackExists({ + Parameters: [ + { ParameterKey: 'HasValue', ParameterValue: 'HasValue' }, + { ParameterKey: 'HasDefault', ParameterValue: 'HasDefault' }, + { ParameterKey: 'OtherParameter', ParameterValue: 'OtherParameter' }, + ], + }); + + // WHEN + await deployStack({ + stack: FAKE_STACK_WITH_PARAMETERS, + sdk, + sdkProvider, + resolvedEnvironment: mockResolvedEnvironment(), + parameters: { + HasValue: 'NewValue', + }, + usePreviousParameters: true, + }); + + // THEN + expect(cfnMocks.createChangeSet).toHaveBeenCalledWith(expect.objectContaining({ + Parameters: [ + { ParameterKey: 'HasValue', ParameterValue: 'NewValue' }, + { ParameterKey: 'HasDefault', UsePreviousValue: true }, + { ParameterKey: 'OtherParameter', UsePreviousValue: true }, + ], + })); +}); + test('if existing stack failed to create, it is deleted and recreated', async () => { // GIVEN givenStackExists( diff --git a/packages/aws-cdk/test/util/cloudformation.test.ts b/packages/aws-cdk/test/util/cloudformation.test.ts index 7f813c768d6e8..ab5bdbcb745c6 100644 --- a/packages/aws-cdk/test/util/cloudformation.test.ts +++ b/packages/aws-cdk/test/util/cloudformation.test.ts @@ -31,28 +31,58 @@ test('A non-existent stack pretends to have an empty template', async () => { expect(await stack.template()).toEqual({}); }); -test('given override, always use the override', () => { - for (const haveDefault of [false, true]) { - for (const havePrevious of [false, true]) { - expect(makeParams(haveDefault, havePrevious, true)).toEqual([USE_OVERRIDE]); - } - } -}); +test.each([ + [false, false], + [false, true], + [true, false], + [true, true]])('given override, always use the override (parameter has a default: %p, parameter previously supplied: %p)', + (haveDefault, havePrevious) => { + expect(makeParams(haveDefault, havePrevious, true)).toEqual({ + apiParameters: [USE_OVERRIDE], + changed: true, + }); + }); test('no default, no prev, no override => error', () => { expect(() => makeParams(false, false, false)).toThrow(/missing a value: TheParameter/); }); test('no default, yes prev, no override => use previous', () => { - expect(makeParams(false, true, false)).toEqual([USE_PREVIOUS]); + expect(makeParams(false, true, false)).toEqual({ + apiParameters: [USE_PREVIOUS], + changed: false, + }); }); test('default, no prev, no override => empty param set', () => { - expect(makeParams(true, false, false)).toEqual([]); + expect(makeParams(true, false, false)).toEqual({ + apiParameters: [], + changed: false, + }); }); test('default, prev, no override => use previous', () => { - expect(makeParams(true, true, false)).toEqual([USE_PREVIOUS]); + expect(makeParams(true, true, false)).toEqual({ + apiParameters: [USE_PREVIOUS], + changed: false, + }); +}); + +test('if a parameter is retrieved from SSM, the parameters always count as changed', () => { + const params = TemplateParameters.fromTemplate({ + Parameters: { + Foo: { + Type: 'AWS::SSM::Parameter::Name', + Default: '/Some/Key', + }, + }, + }); + + // If we don't pass a new value + expect(params.diff({}, {Foo: '/Some/Key'}).changed).toEqual(true); + + // If we do pass a new value but it's the same as the old one + expect(params.diff({Foo: '/Some/Key'}, {Foo: '/Some/Key'}).changed).toEqual(true); }); test('unknown parameter in overrides, pass it anyway', () => { @@ -61,11 +91,11 @@ test('unknown parameter in overrides, pass it anyway', () => { // just error out. But maybe we want to be warned of typos... const params = TemplateParameters.fromTemplate({ Parameters: { - Foo: { Default: 'Foo' }, + Foo: { Type: 'String', Default: 'Foo' }, }, }); - expect(params.makeApiParameters({ Bar: 'Bar' }, [])).toEqual([ + expect(params.diff({ Bar: 'Bar' }, {}).apiParameters).toEqual([ { ParameterKey: 'Bar', ParameterValue: 'Bar' }, ]); }); @@ -74,10 +104,13 @@ function makeParams(defaultValue: boolean, hasPrevValue: boolean, override: bool const params = TemplateParameters.fromTemplate({ Parameters: { [PARAM]: { + Type: 'String', Default: defaultValue ? DEFAULT : undefined, }, }, }); - const prevParams = hasPrevValue ? [PARAM] : []; - return params.makeApiParameters({ [PARAM]: override ? OVERRIDE : undefined }, prevParams); + const prevParams: Record = hasPrevValue ? {[PARAM]: 'Foo'} : {}; + const stackParams = params.diff({ [PARAM]: override ? OVERRIDE : undefined }, prevParams); + + return { apiParameters: stackParams.apiParameters, changed: stackParams.changed }; } \ No newline at end of file From 6166a70800caff8530f55a14fd8e71ca9dd4e9f8 Mon Sep 17 00:00:00 2001 From: Romain Marcadier Date: Wed, 24 Jun 2020 12:46:42 +0200 Subject: [PATCH 17/21] chore: make @aws-cdk/cdk-assets-schema/index.d.ts a module (#8708) For otherwise, an integration test that tries to import everything breaks. --- packages/@aws-cdk/cdk-assets-schema/lib/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/@aws-cdk/cdk-assets-schema/lib/index.ts b/packages/@aws-cdk/cdk-assets-schema/lib/index.ts index 860b95e527e47..c90b194d4e271 100644 --- a/packages/@aws-cdk/cdk-assets-schema/lib/index.ts +++ b/packages/@aws-cdk/cdk-assets-schema/lib/index.ts @@ -1,2 +1,4 @@ // tslint:disable-next-line: no-console console.error('error: @aws-cdk/cdk-assets-schema has been merged into @aws-cdk/cloud-assembly-schema'); + +export { }; From a7cb3b7633c433ecb0619c030914bfa497ee39bc Mon Sep 17 00:00:00 2001 From: Shiv Lakshminarayan Date: Wed, 24 Jun 2020 05:03:32 -0700 Subject: [PATCH 18/21] feat(stepfunctions-tasks): task constructs to call DynamoDB APIs (#8466) Replaces the implementation to call DynamoDb from within a task to merge state level properties as a construct. Notable changes: * APIs require an `ITable` to be supplied instead of `tableName` * `partitionKey` and `sortKey` are now represented as `key` * Rationale: aligning with the SDK for making these calls to Dynamo * `DynamoAttributeValue` is now a struct rather than a class * Rationale: the `withX` methods don't translate to every language and with a struct a fluent builder will be generated with a more idiomatic syntax for the language being used Refactored where it seemed sensible. Test expectations have not changed with the exception of the tableName being a reference. Verified the integration test. Closes #8108 BREAKING CHANGE: `Dynamo*` tasks no longer implement`IStepFunctionsTask` and have been replaced byconstructs that can be instantiated directly. See README for examples ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../aws-stepfunctions-tasks/README.md | 71 +- .../lib/dynamodb/call-dynamodb.ts | 791 ------------------ .../lib/dynamodb/delete-item.ts | 123 +++ .../lib/dynamodb/get-item.ts | 107 +++ .../lib/dynamodb/private/utils.ts | 24 + .../lib/dynamodb/put-item.ts | 121 +++ .../lib/dynamodb/shared-types.ts | 251 ++++++ .../lib/dynamodb/update-item.ts | 134 +++ .../aws-stepfunctions-tasks/lib/index.ts | 6 +- .../aws-stepfunctions-tasks/package.json | 4 +- .../test/dynamodb/call-dynamodb.test.ts | 346 -------- .../test/dynamodb/delete-item.test.ts | 65 ++ .../test/dynamodb/get-item.test.ts | 115 +++ .../integ.call-dynamodb.expected.json | 88 +- .../test/dynamodb/integ.call-dynamodb.ts | 104 +-- .../test/dynamodb/put-item.test.ts | 65 ++ .../test/dynamodb/shared-types.test.ts | 248 ++++++ .../test/dynamodb/update-item.test.ts | 67 ++ 18 files changed, 1464 insertions(+), 1266 deletions(-) delete mode 100644 packages/@aws-cdk/aws-stepfunctions-tasks/lib/dynamodb/call-dynamodb.ts create mode 100644 packages/@aws-cdk/aws-stepfunctions-tasks/lib/dynamodb/delete-item.ts create mode 100644 packages/@aws-cdk/aws-stepfunctions-tasks/lib/dynamodb/get-item.ts create mode 100644 packages/@aws-cdk/aws-stepfunctions-tasks/lib/dynamodb/private/utils.ts create mode 100644 packages/@aws-cdk/aws-stepfunctions-tasks/lib/dynamodb/put-item.ts create mode 100644 packages/@aws-cdk/aws-stepfunctions-tasks/lib/dynamodb/shared-types.ts create mode 100644 packages/@aws-cdk/aws-stepfunctions-tasks/lib/dynamodb/update-item.ts delete mode 100644 packages/@aws-cdk/aws-stepfunctions-tasks/test/dynamodb/call-dynamodb.test.ts create mode 100644 packages/@aws-cdk/aws-stepfunctions-tasks/test/dynamodb/delete-item.test.ts create mode 100644 packages/@aws-cdk/aws-stepfunctions-tasks/test/dynamodb/get-item.test.ts create mode 100644 packages/@aws-cdk/aws-stepfunctions-tasks/test/dynamodb/put-item.test.ts create mode 100644 packages/@aws-cdk/aws-stepfunctions-tasks/test/dynamodb/shared-types.test.ts create mode 100644 packages/@aws-cdk/aws-stepfunctions-tasks/test/dynamodb/update-item.test.ts diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/README.md b/packages/@aws-cdk/aws-stepfunctions-tasks/README.md index 6ed90de00c40c..907fd5f5b76a1 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/README.md +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/README.md @@ -123,14 +123,10 @@ The following example adds the item from calling DynamoDB's `getItem` API to the input and passes it to the next state. ```ts -new sfn.Task(this, 'PutItem', { - task: tasks.CallDynamoDB.getItem({ - item: { - MessageId: new tasks.DynamoAttributeValue().withS('12345'), - }, - tableName: 'my-table', - }), - resultPath: `$.Item` +new tasks.DynamoGetItem(this, 'PutItem', { + item: { MessageId: { s: '12345'} }, + tableName: 'my-table', + resultPath: `$.Item`, }); ``` @@ -251,14 +247,9 @@ Read more about calling DynamoDB APIs [here](https://docs.aws.amazon.com/step-fu The [GetItem](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_GetItem.html) operation returns a set of attributes for the item with the given primary key. ```ts -new sfn.Task(this, 'Get Item', { - task: tasks.CallDynamoDB.getItem({ - partitionKey: { - name: 'messageId', - value: new tasks.DynamoAttributeValue().withS('message-007'), - }, - tableName: 'my-table', - }), +new tasks.DynamoGetItem(this, 'Get Item', { + key: { messageId: tasks.DynamoAttributeValue.fromString('message-007') }, + table, }); ``` @@ -267,15 +258,13 @@ new sfn.Task(this, 'Get Item', { The [PutItem](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_PutItem.html) operation creates a new item, or replaces an old item with a new item. ```ts -new sfn.Task(this, 'PutItem', { - task: tasks.CallDynamoDB.putItem({ - item: { - MessageId: new tasks.DynamoAttributeValue().withS('message-007'), - Text: new tasks.DynamoAttributeValue().withS(sfn.Data.stringAt('$.bar')), - TotalCount: new tasks.DynamoAttributeValue().withN('10'), - }, - tableName: 'my-table', - }), +new tasks.DynamoPutItem(this, 'PutItem', { + item: { + MessageId: tasks.DynamoAttributeValue.fromString('message-007'), + Text: tasks.DynamoAttributeValue.fromString(sfn.Data.stringAt('$.bar')), + TotalCount: tasks.DynamoAttributeValue.fromNumber(10), + }, + table, }); ``` @@ -284,14 +273,9 @@ new sfn.Task(this, 'PutItem', { The [DeleteItem](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_DeleteItem.html) operation deletes a single item in a table by primary key. ```ts -new sfn.Task(this, 'DeleteItem', { - task: tasks.CallDynamoDB.deleteItem({ - partitionKey: { - name: 'MessageId', - value: new tasks.DynamoAttributeValue().withS('message-007'), - }, - tableName: 'my-table', - }), +new tasks.DynamoDeleteItem(this, 'DeleteItem', { + key: { MessageId: tasks.DynamoAttributeValue.fromString('message-007') }, + table, resultPath: 'DISCARD', }); ``` @@ -302,19 +286,14 @@ The [UpdateItem](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/ to the table if it does not already exist. ```ts -const updateItemTask = new sfn.Task(this, 'UpdateItem', { - task: tasks.CallDynamoDB.updateItem({ - partitionKey: { - name: 'MessageId', - value: new tasks.DynamoAttributeValue().withS('message-007'), - }, - tableName: 'my-table', - expressionAttributeValues: { - ':val': new tasks.DynamoAttributeValue().withN(sfn.Data.stringAt('$.Item.TotalCount.N')), - ':rand': new tasks.DynamoAttributeValue().withN('20'), - }, - updateExpression: 'SET TotalCount = :val + :rand', - }), +new tasks.DynamoUpdateItem(this, 'UpdateItem', { + key: { MessageId: tasks.DynamoAttributeValue.fromString('message-007') }, + table, + expressionAttributeValues: { + ':val': tasks.DynamoAttributeValue.numberFromString(sfn.Data.stringAt('$.Item.TotalCount.N')), + ':rand': tasks.DynamoAttributeValue.fromNumber(20), + }, + updateExpression: 'SET TotalCount = :val + :rand', }); ``` diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/dynamodb/call-dynamodb.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/dynamodb/call-dynamodb.ts deleted file mode 100644 index 048043ce8cd9c..0000000000000 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/dynamodb/call-dynamodb.ts +++ /dev/null @@ -1,791 +0,0 @@ -import * as iam from '@aws-cdk/aws-iam'; -import * as sfn from '@aws-cdk/aws-stepfunctions'; -import { Stack, withResolved } from '@aws-cdk/core'; -import { getResourceArn } from '../resource-arn-suffix'; - -/** - * Determines the level of detail about provisioned throughput consumption that is returned. - */ -export enum DynamoConsumedCapacity { - /** - * The response includes the aggregate ConsumedCapacity for the operation, - * together with ConsumedCapacity for each table and secondary index that was accessed - */ - INDEXES = 'INDEXES', - - /** - * The response includes only the aggregate ConsumedCapacity for the operation. - */ - TOTAL = 'TOTAL', - - /** - * No ConsumedCapacity details are included in the response. - */ - NONE = 'NONE' -} - -/** - * Determines whether item collection metrics are returned. - */ -export enum DynamoItemCollectionMetrics { - /** - * If set to SIZE, the response includes statistics about item collections, - * if any, that were modified during the operation. - */ - SIZE = 'SIZE', - - /** - * If set to NONE, no statistics are returned. - */ - NONE = 'NONE' -} - -/** - * Use ReturnValues if you want to get the item attributes as they appear before or after they are changed - */ -export enum DynamoReturnValues { - /** - * Nothing is returned - */ - NONE = 'NONE', - - /** - * Returns all of the attributes of the item - */ - ALL_OLD = 'ALL_OLD', - - /** - * Returns only the updated attributes - */ - UPDATED_OLD = 'UPDATED_OLD', - - /** - * Returns all of the attributes of the item - */ - ALL_NEW = 'ALL_NEW', - - /** - * Returns only the updated attributes - */ - UPDATED_NEW = 'UPDATED_NEW' -} - -/** - * Map of string to AttributeValue - */ -export interface DynamoAttributeValueMap { - [key: string]: DynamoAttributeValue; -} - -/** - * Class to generate AttributeValue - */ -export class DynamoAttributeValue { - private attributeValue: any = {}; - - /** - * Sets an attribute of type String. For example: "S": "Hello" - */ - public withS(value: string) { - this.attributeValue.S = value; - return this; - } - - /** - * Sets an attribute of type Number. For example: "N": "123.45" - * Numbers are sent across the network to DynamoDB as strings, - * to maximize compatibility across languages and libraries. - * However, DynamoDB treats them as number type attributes for mathematical operations. - */ - public withN(value: string) { - this.attributeValue.N = value; - return this; - } - - /** - * Sets an attribute of type Binary. For example: "B": "dGhpcyB0ZXh0IGlzIGJhc2U2NC1lbmNvZGVk" - */ - public withB(value: string) { - this.attributeValue.B = value; - return this; - } - - /** - * Sets an attribute of type String Set. For example: "SS": ["Giraffe", "Hippo" ,"Zebra"] - */ - public withSS(value: string[]) { - this.attributeValue.SS = value; - return this; - } - - /** - * Sets an attribute of type Number Set. For example: "NS": ["42.2", "-19", "7.5", "3.14"] - * Numbers are sent across the network to DynamoDB as strings, - * to maximize compatibility across languages and libraries. - * However, DynamoDB treats them as number type attributes for mathematical operations. - */ - public withNS(value: string[]) { - this.attributeValue.NS = value; - return this; - } - - /** - * Sets an attribute of type Binary Set. For example: "BS": ["U3Vubnk=", "UmFpbnk=", "U25vd3k="] - */ - public withBS(value: string[]) { - this.attributeValue.BS = value; - return this; - } - - /** - * Sets an attribute of type Map. For example: "M": {"Name": {"S": "Joe"}, "Age": {"N": "35"}} - */ - public withM(value: DynamoAttributeValueMap) { - this.attributeValue.M = transformAttributeValueMap(value); - return this; - } - - /** - * Sets an attribute of type List. For example: "L": [ {"S": "Cookies"} , {"S": "Coffee"}, {"N", "3.14159"}] - */ - public withL(value: DynamoAttributeValue[]) { - this.attributeValue.L = value.map(val => val.toObject()); - return this; - } - - /** - * Sets an attribute of type Null. For example: "NULL": true - */ - public withNULL(value: boolean) { - this.attributeValue.NULL = value; - return this; - } - - /** - * Sets an attribute of type Boolean. For example: "BOOL": true - */ - public withBOOL(value: boolean) { - this.attributeValue.BOOL = value; - return this; - } - - /** - * Return the attributeValue object - */ - public toObject() { - return this.attributeValue; - } -} - -/** - * Property for any key - */ -export interface DynamoAttribute { - /** - * The name of the attribute - */ - readonly name: string; - - /** - * The value of the attribute - */ - readonly value: DynamoAttributeValue; -} - -/** - * Class to generate projection expression - */ -export class DynamoProjectionExpression { - private expression: string[] = []; - - /** - * Adds the passed attribute to the chain - * - * @param attr Attribute name - */ - public withAttribute(attr: string): DynamoProjectionExpression { - if (this.expression.length) { - this.expression.push(`.${attr}`); - } else { - this.expression.push(attr); - } - return this; - } - - /** - * Adds the array literal access for passed index - * - * @param index array index - */ - public atIndex(index: number): DynamoProjectionExpression { - if (!this.expression.length) { - throw new Error('Expression must start with an attribute'); - } - - this.expression.push(`[${index}]`); - return this; - } - - /** - * converts and return the string expression - */ - public toString(): string { - return this.expression.join(''); - } -} - -/** - * Properties for DynamoGetItem Task - */ -export interface DynamoGetItemProps { - /** - * A attribute representing the partition key of the item to retrieve. - * - * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_GetItem.html#DDB-GetItem-request-Key - */ - readonly partitionKey: DynamoAttribute; - - /** - * The name of the table containing the requested item. - */ - readonly tableName: string; - - /** - * A attribute representing the sort key of the item to retrieve. - * - * @default - No sort key - * - * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_GetItem.html#DDB-GetItem-request-Key - */ - readonly sortKey?: DynamoAttribute; - - /** - * Determines the read consistency model: - * If set to true, then the operation uses strongly consistent reads; - * otherwise, the operation uses eventually consistent reads. - * - * @default false - */ - readonly consistentRead?: boolean; - - /** - * One or more substitution tokens for attribute names in an expression - * - * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_GetItem.html#DDB-GetItem-request-ExpressionAttributeNames - * - * @default - No expression attributes - */ - readonly expressionAttributeNames?: { [key: string]: string }; - - /** - * An array of DynamoProjectionExpression that identifies one or more attributes to retrieve from the table. - * These attributes can include scalars, sets, or elements of a JSON document. - * - * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_GetItem.html#DDB-GetItem-request-ProjectionExpression - * - * @default - No projection expression - */ - readonly projectionExpression?: DynamoProjectionExpression[]; - - /** - * Determines the level of detail about provisioned throughput consumption that is returned in the response - * - * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_GetItem.html#DDB-GetItem-request-ReturnConsumedCapacity - * - * @default DynamoConsumedCapacity.NONE - */ - readonly returnConsumedCapacity?: DynamoConsumedCapacity; -} - -/** - * Properties for DynamoPutItem Task - */ -export interface DynamoPutItemProps { - /** - * A map of attribute name/value pairs, one for each attribute. - * Only the primary key attributes are required; - * you can optionally provide other attribute name-value pairs for the item. - * - * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_PutItem.html#DDB-PutItem-request-Item - */ - readonly item: DynamoAttributeValueMap; - - /** - * The name of the table where the item should be writen . - */ - readonly tableName: string; - - /** - * A condition that must be satisfied in order for a conditional PutItem operation to succeed. - * - * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_PutItem.html#DDB-PutItem-request-ConditionExpression - * - * @default - No condition expression - */ - readonly conditionExpression?: string; - - /** - * One or more substitution tokens for attribute names in an expression - * - * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_PutItem.html#DDB-PutItem-request-ExpressionAttributeNames - * - * @default - No expression attribute names - */ - readonly expressionAttributeNames?: { [key: string]: string }; - - /** - * One or more values that can be substituted in an expression. - * - * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_PutItem.html#DDB-PutItem-request-ExpressionAttributeValues - * - * @default - No expression attribute values - */ - readonly expressionAttributeValues?: DynamoAttributeValueMap; - - /** - * Determines the level of detail about provisioned throughput consumption that is returned in the response - * - * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_PutItem.html#DDB-PutItem-request-ReturnConsumedCapacity - * - * @default DynamoConsumedCapacity.NONE - */ - readonly returnConsumedCapacity?: DynamoConsumedCapacity; - - /** - * The item collection metrics to returned in the response - * - * @see https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/LSI.html#LSI.ItemCollections - * - * @default DynamoItemCollectionMetrics.NONE - */ - readonly returnItemCollectionMetrics?: DynamoItemCollectionMetrics; - - /** - * Use ReturnValues if you want to get the item attributes as they appeared - * before they were updated with the PutItem request. - * - * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_PutItem.html#DDB-PutItem-request-ReturnValues - * - * @default DynamoReturnValues.NONE - */ - readonly returnValues?: DynamoReturnValues; -} - -/** - * Properties for DynamoDeleteItem Task - */ -export interface DynamoDeleteItemProps { - /** - * An attribute representing the partition key of the item to delete. - * - * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_DeleteItem.html#DDB-DeleteItem-request-Key - */ - readonly partitionKey: DynamoAttribute; - - /** - * The name of the table containing the requested item. - */ - readonly tableName: string; - - /** - * An attribute representing the sort key of the item to delete. - * - * @default - No sort key - * - * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_DeleteItem.html#DDB-DeleteItem-request-Key - */ - readonly sortKey?: DynamoAttribute; - - /** - * A condition that must be satisfied in order for a conditional DeleteItem to succeed. - * - * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_DeleteItem.html#DDB-DeleteItem-request-ConditionExpression - * - * @default - No condition expression - */ - readonly conditionExpression?: string; - - /** - * One or more substitution tokens for attribute names in an expression - * - * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_DeleteItem.html#DDB-DeleteItem-request-ExpressionAttributeNames - * - * @default - No expression attribute names - */ - readonly expressionAttributeNames?: { [key: string]: string }; - - /** - * One or more values that can be substituted in an expression. - * - * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_DeleteItem.html#DDB-DeleteItem-request-ExpressionAttributeValues - * - * @default - No expression attribute values - */ - readonly expressionAttributeValues?: DynamoAttributeValueMap; - - /** - * Determines the level of detail about provisioned throughput consumption that is returned in the response - * - * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_DeleteItem.html#DDB-DeleteItem-request-ReturnConsumedCapacity - * - * @default DynamoConsumedCapacity.NONE - */ - readonly returnConsumedCapacity?: DynamoConsumedCapacity; - - /** - * Determines whether item collection metrics are returned. - * If set to SIZE, the response includes statistics about item collections, if any, - * that were modified during the operation are returned in the response. - * If set to NONE (the default), no statistics are returned. - * - * @default DynamoItemCollectionMetrics.NONE - */ - readonly returnItemCollectionMetrics?: DynamoItemCollectionMetrics; - - /** - * Use ReturnValues if you want to get the item attributes as they appeared before they were deleted. - * - * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_DeleteItem.html#DDB-DeleteItem-request-ReturnValues - * - * @default DynamoReturnValues.NONE - */ - readonly returnValues?: DynamoReturnValues; -} - -/** - * Properties for DynamoUpdateItem Task - */ -export interface DynamoUpdateItemProps { - /** - * The partition key of the item to be updated. - * - * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_UpdateItem.html#DDB-UpdateItem-request-Key - */ - readonly partitionKey: DynamoAttribute; - - /** - * The name of the table containing the requested item. - */ - readonly tableName: string; - - /** - * The sort key of the item to be updated. - * - * @default - No sort key - * - * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_UpdateItem.html#DDB-UpdateItem-request-Key - */ - readonly sortKey?: DynamoAttribute; - - /** - * A condition that must be satisfied in order for a conditional DeleteItem to succeed. - * - * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_UpdateItem.html#DDB-UpdateItem-request-ConditionExpression - * - * @default - No condition expression - */ - readonly conditionExpression?: string; - - /** - * One or more substitution tokens for attribute names in an expression - * - * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_UpdateItem.html#DDB-UpdateItem-request-ExpressionAttributeNames - * - * @default - No expression attribute names - */ - readonly expressionAttributeNames?: { [key: string]: string }; - - /** - * One or more values that can be substituted in an expression. - * - * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_UpdateItem.html#DDB-UpdateItem-request-ExpressionAttributeValues - * - * @default - No expression attribute values - */ - readonly expressionAttributeValues?: DynamoAttributeValueMap; - - /** - * Determines the level of detail about provisioned throughput consumption that is returned in the response - * - * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_UpdateItem.html#DDB-UpdateItem-request-ReturnConsumedCapacity - * - * @default DynamoConsumedCapacity.NONE - */ - readonly returnConsumedCapacity?: DynamoConsumedCapacity; - - /** - * Determines whether item collection metrics are returned. - * If set to SIZE, the response includes statistics about item collections, if any, - * that were modified during the operation are returned in the response. - * If set to NONE (the default), no statistics are returned. - * - * @default DynamoItemCollectionMetrics.NONE - */ - readonly returnItemCollectionMetrics?: DynamoItemCollectionMetrics; - - /** - * Use ReturnValues if you want to get the item attributes as they appeared before they were deleted. - * - * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_UpdateItem.html#DDB-UpdateItem-request-ReturnValues - * - * @default DynamoReturnValues.NONE - */ - readonly returnValues?: DynamoReturnValues; - - /** - * An expression that defines one or more attributes to be updated, - * the action to be performed on them, and new values for them. - * - * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_UpdateItem.html#DDB-UpdateItem-request-UpdateExpression - * - * @default - No update expression - */ - readonly updateExpression?: string; -} - -/** - * A StepFunctions task to call DynamoGetItem - */ -export class DynamoGetItem implements sfn.IStepFunctionsTask { - constructor(private readonly props: DynamoGetItemProps) { - withResolved(props.tableName, validateTableName); - } - - public bind(_task: sfn.Task): sfn.StepFunctionsTaskConfig { - return { - resourceArn: getDynamoResourceArn(DynamoMethod.GET), - policyStatements: getDynamoPolicyStatements( - _task, - this.props.tableName, - DynamoMethod.GET, - ), - parameters: { - Key: configurePrimaryKey(this.props.partitionKey, this.props.sortKey), - TableName: this.props.tableName, - ConsistentRead: this.props.consistentRead ?? false, - ExpressionAttributeNames: this.props.expressionAttributeNames, - ProjectionExpression: this.configureProjectionExpression( - this.props.projectionExpression, - ), - ReturnConsumedCapacity: this.props.returnConsumedCapacity, - }, - }; - } - - private configureProjectionExpression( - expressions?: DynamoProjectionExpression[], - ): string | undefined { - return expressions - ? expressions.map(expression => expression.toString()).join(',') - : undefined; - } -} - -/** - * A StepFunctions task to call DynamoPutItem - */ -export class DynamoPutItem implements sfn.IStepFunctionsTask { - constructor(private readonly props: DynamoPutItemProps) { - withResolved(props.tableName, validateTableName); - } - - public bind(_task: sfn.Task): sfn.StepFunctionsTaskConfig { - return { - resourceArn: getDynamoResourceArn(DynamoMethod.PUT), - policyStatements: getDynamoPolicyStatements( - _task, - this.props.tableName, - DynamoMethod.PUT, - ), - parameters: { - Item: transformAttributeValueMap(this.props.item), - TableName: this.props.tableName, - ConditionExpression: this.props.conditionExpression, - ExpressionAttributeNames: this.props.expressionAttributeNames, - ExpressionAttributeValues: transformAttributeValueMap( - this.props.expressionAttributeValues, - ), - ReturnConsumedCapacity: this.props.returnConsumedCapacity, - ReturnItemCollectionMetrics: this.props.returnItemCollectionMetrics, - ReturnValues: this.props.returnValues, - }, - }; - } -} - -/** - * A StepFunctions task to call DynamoDeleteItem - */ -export class DynamoDeleteItem implements sfn.IStepFunctionsTask { - constructor(private readonly props: DynamoDeleteItemProps) { - withResolved(props.tableName, validateTableName); - } - - public bind(_task: sfn.Task): sfn.StepFunctionsTaskConfig { - return { - resourceArn: getDynamoResourceArn(DynamoMethod.DELETE), - policyStatements: getDynamoPolicyStatements( - _task, - this.props.tableName, - DynamoMethod.DELETE, - ), - parameters: { - Key: configurePrimaryKey(this.props.partitionKey, this.props.sortKey), - TableName: this.props.tableName, - ConditionExpression: this.props.conditionExpression, - ExpressionAttributeNames: this.props.expressionAttributeNames, - ExpressionAttributeValues: transformAttributeValueMap( - this.props.expressionAttributeValues, - ), - ReturnConsumedCapacity: this.props.returnConsumedCapacity, - ReturnItemCollectionMetrics: this.props.returnItemCollectionMetrics, - ReturnValues: this.props.returnValues, - }, - }; - } -} - -/** - * A StepFunctions task to call DynamoUpdateItem - */ -export class DynamoUpdateItem implements sfn.IStepFunctionsTask { - constructor(private readonly props: DynamoUpdateItemProps) { - withResolved(props.tableName, validateTableName); - } - - public bind(_task: sfn.Task): sfn.StepFunctionsTaskConfig { - return { - resourceArn: getDynamoResourceArn(DynamoMethod.UPDATE), - policyStatements: getDynamoPolicyStatements( - _task, - this.props.tableName, - DynamoMethod.UPDATE, - ), - parameters: { - Key: configurePrimaryKey(this.props.partitionKey, this.props.sortKey), - TableName: this.props.tableName, - ConditionExpression: this.props.conditionExpression, - ExpressionAttributeNames: this.props.expressionAttributeNames, - ExpressionAttributeValues: transformAttributeValueMap( - this.props.expressionAttributeValues, - ), - ReturnConsumedCapacity: this.props.returnConsumedCapacity, - ReturnItemCollectionMetrics: this.props.returnItemCollectionMetrics, - ReturnValues: this.props.returnValues, - UpdateExpression: this.props.updateExpression, - }, - }; - } -} - -/** - * A helper wrapper class to call all DynamoDB APIs - */ -export class CallDynamoDB { - /** - * Method to get DynamoGetItem task - * - * @param props DynamoGetItemProps - */ - public static getItem(props: DynamoGetItemProps) { - return new DynamoGetItem(props); - } - - /** - * Method to get DynamoPutItem task - * - * @param props DynamoPutItemProps - */ - public static putItem(props: DynamoPutItemProps) { - return new DynamoPutItem(props); - } - - /** - * Method to get DynamoDeleteItem task - * - * @param props DynamoDeleteItemProps - */ - public static deleteItem(props: DynamoDeleteItemProps) { - return new DynamoDeleteItem(props); - } - - /** - * Method to get DynamoUpdateItem task - * - * @param props DynamoUpdateItemProps - */ - public static updateItem(props: DynamoUpdateItemProps) { - return new DynamoUpdateItem(props); - } -} - -enum DynamoMethod { - GET = 'Get', - PUT = 'Put', - DELETE = 'Delete', - UPDATE = 'Update' -} - -function validateTableName(tableName: string) { - if ( - tableName.length < 3 || - tableName.length > 255 || - !new RegExp(/[a-zA-Z0-9_.-]+$/).test(tableName) - ) { - throw new Error( - `TableName should not contain alphanumeric characters and should be between 3-255 characters long. Received: ${tableName}`, - ); - } -} - -function getDynamoResourceArn(method: DynamoMethod) { - return getResourceArn( - 'dynamodb', - `${method.toLowerCase()}Item`, - sfn.ServiceIntegrationPattern.FIRE_AND_FORGET, - ); -} - -function getDynamoPolicyStatements( - task: sfn.Task, - tableName: string, - method: DynamoMethod, -) { - return [ - new iam.PolicyStatement({ - resources: [ - Stack.of(task).formatArn({ - service: 'dynamodb', - resource: 'table', - resourceName: tableName, - }), - ], - actions: [`dynamodb:${method}Item`], - }), - ]; -} - -function configurePrimaryKey( - partitionKey: DynamoAttribute, - sortKey?: DynamoAttribute, -) { - const key = { - [partitionKey.name]: partitionKey.value.toObject(), - }; - - if (sortKey) { - key[sortKey.name] = sortKey.value.toObject(); - } - - return key; -} - -function transformAttributeValueMap(attrMap?: DynamoAttributeValueMap) { - const transformedValue: any = {}; - for (const key in attrMap) { - if (key) { - transformedValue[key] = attrMap[key].toObject(); - } - } - return attrMap ? transformedValue : undefined; -} diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/dynamodb/delete-item.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/dynamodb/delete-item.ts new file mode 100644 index 0000000000000..174698241a1f8 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/dynamodb/delete-item.ts @@ -0,0 +1,123 @@ +import * as ddb from '@aws-cdk/aws-dynamodb'; +import * as iam from '@aws-cdk/aws-iam'; +import * as sfn from '@aws-cdk/aws-stepfunctions'; +import { Construct, Stack } from '@aws-cdk/core'; +import { DynamoMethod, getDynamoResourceArn, transformAttributeValueMap } from './private/utils'; +import { DynamoAttributeValue, DynamoConsumedCapacity, DynamoItemCollectionMetrics, DynamoReturnValues } from './shared-types'; + +/** + * Properties for DynamoDeleteItem Task + */ +export interface DynamoDeleteItemProps extends sfn.TaskStateBaseProps { + /** + * The name of the table containing the requested item. + */ + readonly table: ddb.ITable; + + /** + * Primary key of the item to retrieve. + * + * For the primary key, you must provide all of the attributes. + * For example, with a simple primary key, you only need to provide a value for the partition key. + * For a composite primary key, you must provide values for both the partition key and the sort key. + * + * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_GetItem.html#DDB-GetItem-request-Key + */ + readonly key: { [key: string]: DynamoAttributeValue }; + + /** + * A condition that must be satisfied in order for a conditional DeleteItem to succeed. + * + * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_DeleteItem.html#DDB-DeleteItem-request-ConditionExpression + * + * @default - No condition expression + */ + readonly conditionExpression?: string; + + /** + * One or more substitution tokens for attribute names in an expression + * + * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_DeleteItem.html#DDB-DeleteItem-request-ExpressionAttributeNames + * + * @default - No expression attribute names + */ + readonly expressionAttributeNames?: { [key: string]: string }; + + /** + * One or more values that can be substituted in an expression. + * + * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_DeleteItem.html#DDB-DeleteItem-request-ExpressionAttributeValues + * + * @default - No expression attribute values + */ + readonly expressionAttributeValues?: { [key: string]: DynamoAttributeValue }; + + /** + * Determines the level of detail about provisioned throughput consumption that is returned in the response + * + * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_DeleteItem.html#DDB-DeleteItem-request-ReturnConsumedCapacity + * + * @default DynamoConsumedCapacity.NONE + */ + readonly returnConsumedCapacity?: DynamoConsumedCapacity; + + /** + * Determines whether item collection metrics are returned. + * If set to SIZE, the response includes statistics about item collections, if any, + * that were modified during the operation are returned in the response. + * If set to NONE (the default), no statistics are returned. + * + * @default DynamoItemCollectionMetrics.NONE + */ + readonly returnItemCollectionMetrics?: DynamoItemCollectionMetrics; + + /** + * Use ReturnValues if you want to get the item attributes as they appeared before they were deleted. + * + * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_DeleteItem.html#DDB-DeleteItem-request-ReturnValues + * + * @default DynamoReturnValues.NONE + */ + readonly returnValues?: DynamoReturnValues; +} + +/** + * A StepFunctions task to call DynamoDeleteItem + */ +export class DynamoDeleteItem extends sfn.TaskStateBase { + protected readonly taskMetrics?: sfn.TaskMetricsConfig; + protected readonly taskPolicies?: iam.PolicyStatement[]; + + constructor(scope: Construct, id: string, private readonly props: DynamoDeleteItemProps) { + super(scope, id, props); + + this.taskPolicies = [ + new iam.PolicyStatement({ + resources: [ + Stack.of(this).formatArn({ + service: 'dynamodb', + resource: 'table', + resourceName: props.table.tableName, + }), + ], + actions: [`dynamodb:${DynamoMethod.DELETE}Item`], + }), + ]; + } + + protected renderTask(): any { + return { + Resource: getDynamoResourceArn(DynamoMethod.DELETE), + Parameters: sfn.FieldUtils.renderObject({ + Key: transformAttributeValueMap(this.props.key), + TableName: this.props.table.tableName, + ConditionExpression: this.props.conditionExpression, + ExpressionAttributeNames: this.props.expressionAttributeNames, + ExpressionAttributeValues: transformAttributeValueMap(this.props.expressionAttributeValues), + ReturnConsumedCapacity: this.props.returnConsumedCapacity, + ReturnItemCollectionMetrics: this.props.returnItemCollectionMetrics, + ReturnValues: this.props.returnValues, + }), + }; + } +} diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/dynamodb/get-item.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/dynamodb/get-item.ts new file mode 100644 index 0000000000000..3cef6bd994fdc --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/dynamodb/get-item.ts @@ -0,0 +1,107 @@ +import * as ddb from '@aws-cdk/aws-dynamodb'; +import * as iam from '@aws-cdk/aws-iam'; +import * as sfn from '@aws-cdk/aws-stepfunctions'; +import { Construct, Stack } from '@aws-cdk/core'; +import { DynamoMethod, getDynamoResourceArn, transformAttributeValueMap } from './private/utils'; +import { DynamoAttributeValue, DynamoConsumedCapacity, DynamoProjectionExpression } from './shared-types'; + +/** + * Properties for DynamoGetItem Task + */ +export interface DynamoGetItemProps extends sfn.TaskStateBaseProps { + /** + * The name of the table containing the requested item. + */ + readonly table: ddb.ITable; + + /** + * Primary key of the item to retrieve. + * + * For the primary key, you must provide all of the attributes. + * For example, with a simple primary key, you only need to provide a value for the partition key. + * For a composite primary key, you must provide values for both the partition key and the sort key. + * + * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_GetItem.html#DDB-GetItem-request-Key + */ + readonly key: { [key: string]: DynamoAttributeValue }; + + /** + * Determines the read consistency model: + * If set to true, then the operation uses strongly consistent reads; + * otherwise, the operation uses eventually consistent reads. + * + * @default false + */ + readonly consistentRead?: boolean; + + /** + * One or more substitution tokens for attribute names in an expression + * + * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_GetItem.html#DDB-GetItem-request-ExpressionAttributeNames + * + * @default - No expression attributes + */ + readonly expressionAttributeNames?: { [key: string]: string }; + + /** + * An array of DynamoProjectionExpression that identifies one or more attributes to retrieve from the table. + * These attributes can include scalars, sets, or elements of a JSON document. + * + * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_GetItem.html#DDB-GetItem-request-ProjectionExpression + * + * @default - No projection expression + */ + readonly projectionExpression?: DynamoProjectionExpression[]; + + /** + * Determines the level of detail about provisioned throughput consumption that is returned in the response + * + * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_GetItem.html#DDB-GetItem-request-ReturnConsumedCapacity + * + * @default DynamoConsumedCapacity.NONE + */ + readonly returnConsumedCapacity?: DynamoConsumedCapacity; +} + +/** + * A StepFunctions task to call DynamoGetItem + */ +export class DynamoGetItem extends sfn.TaskStateBase { + protected readonly taskMetrics?: sfn.TaskMetricsConfig; + protected readonly taskPolicies?: iam.PolicyStatement[]; + + constructor(scope: Construct, id: string, private readonly props: DynamoGetItemProps) { + super(scope, id, props); + + this.taskPolicies = [ + new iam.PolicyStatement({ + resources: [ + Stack.of(this).formatArn({ + service: 'dynamodb', + resource: 'table', + resourceName: props.table.tableName, + }), + ], + actions: [`dynamodb:${DynamoMethod.GET}Item`], + }), + ]; + } + + protected renderTask(): any { + return { + Resource: getDynamoResourceArn(DynamoMethod.GET), + Parameters: sfn.FieldUtils.renderObject({ + Key: transformAttributeValueMap(this.props.key), + TableName: this.props.table.tableName, + ConsistentRead: this.props.consistentRead ?? false, + ExpressionAttributeNames: this.props.expressionAttributeNames, + ProjectionExpression: this.configureProjectionExpression(this.props.projectionExpression), + ReturnConsumedCapacity: this.props.returnConsumedCapacity, + }), + }; + } + + private configureProjectionExpression(expressions?: DynamoProjectionExpression[]): string | undefined { + return expressions ? expressions.map((expression) => expression.toString()).join(',') : undefined; + } +} diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/dynamodb/private/utils.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/dynamodb/private/utils.ts new file mode 100644 index 0000000000000..f951b7ed978e1 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/dynamodb/private/utils.ts @@ -0,0 +1,24 @@ +import * as sfn from '@aws-cdk/aws-stepfunctions'; +import { integrationResourceArn } from '../../private/task-utils'; +import { DynamoAttributeValue } from '../shared-types'; + +export enum DynamoMethod { + GET = 'Get', + PUT = 'Put', + DELETE = 'Delete', + UPDATE = 'Update', +} + +export function getDynamoResourceArn(method: DynamoMethod) { + return integrationResourceArn('dynamodb', `${method.toLowerCase()}Item`, sfn.IntegrationPattern.REQUEST_RESPONSE); +} + +export function transformAttributeValueMap(attrMap?: { [key: string]: DynamoAttributeValue }) { + const transformedValue: any = {}; + for (const key in attrMap) { + if (key) { + transformedValue[key] = attrMap[key].toObject(); + } + } + return attrMap ? transformedValue : undefined; +} diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/dynamodb/put-item.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/dynamodb/put-item.ts new file mode 100644 index 0000000000000..c23842bf5d98f --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/dynamodb/put-item.ts @@ -0,0 +1,121 @@ +import * as ddb from '@aws-cdk/aws-dynamodb'; +import * as iam from '@aws-cdk/aws-iam'; +import * as sfn from '@aws-cdk/aws-stepfunctions'; +import { Construct, Stack } from '@aws-cdk/core'; +import { DynamoMethod, getDynamoResourceArn, transformAttributeValueMap } from './private/utils'; +import { DynamoAttributeValue, DynamoConsumedCapacity, DynamoItemCollectionMetrics, DynamoReturnValues } from './shared-types'; + +/** + * Properties for DynamoPutItem Task + */ +export interface DynamoPutItemProps extends sfn.TaskStateBaseProps { + /** + * A map of attribute name/value pairs, one for each attribute. + * Only the primary key attributes are required; + * you can optionally provide other attribute name-value pairs for the item. + * + * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_PutItem.html#DDB-PutItem-request-Item + */ + readonly item: { [key: string]: DynamoAttributeValue }; + + /** + * The name of the table where the item should be written . + */ + readonly table: ddb.ITable; + + /** + * A condition that must be satisfied in order for a conditional PutItem operation to succeed. + * + * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_PutItem.html#DDB-PutItem-request-ConditionExpression + * + * @default - No condition expression + */ + readonly conditionExpression?: string; + + /** + * One or more substitution tokens for attribute names in an expression + * + * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_PutItem.html#DDB-PutItem-request-ExpressionAttributeNames + * + * @default - No expression attribute names + */ + readonly expressionAttributeNames?: { [key: string]: string }; + + /** + * One or more values that can be substituted in an expression. + * + * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_PutItem.html#DDB-PutItem-request-ExpressionAttributeValues + * + * @default - No expression attribute values + */ + readonly expressionAttributeValues?: { [key: string]: DynamoAttributeValue }; + + /** + * Determines the level of detail about provisioned throughput consumption that is returned in the response + * + * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_PutItem.html#DDB-PutItem-request-ReturnConsumedCapacity + * + * @default DynamoConsumedCapacity.NONE + */ + readonly returnConsumedCapacity?: DynamoConsumedCapacity; + + /** + * The item collection metrics to returned in the response + * + * @see https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/LSI.html#LSI.ItemCollections + * + * @default DynamoItemCollectionMetrics.NONE + */ + readonly returnItemCollectionMetrics?: DynamoItemCollectionMetrics; + + /** + * Use ReturnValues if you want to get the item attributes as they appeared + * before they were updated with the PutItem request. + * + * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_PutItem.html#DDB-PutItem-request-ReturnValues + * + * @default DynamoReturnValues.NONE + */ + readonly returnValues?: DynamoReturnValues; +} + +/** + * A StepFunctions task to call DynamoPutItem + */ +export class DynamoPutItem extends sfn.TaskStateBase { + protected readonly taskMetrics?: sfn.TaskMetricsConfig; + protected readonly taskPolicies?: iam.PolicyStatement[]; + + constructor(scope: Construct, id: string, private readonly props: DynamoPutItemProps) { + super(scope, id, props); + + this.taskPolicies = [ + new iam.PolicyStatement({ + resources: [ + Stack.of(this).formatArn({ + service: 'dynamodb', + resource: 'table', + resourceName: props.table.tableName, + }), + ], + actions: [`dynamodb:${DynamoMethod.PUT}Item`], + }), + ]; + } + + protected renderTask(): any { + return { + Resource: getDynamoResourceArn(DynamoMethod.PUT), + Parameters: sfn.FieldUtils.renderObject({ + Item: transformAttributeValueMap(this.props.item), + TableName: this.props.table.tableName, + ConditionExpression: this.props.conditionExpression, + ExpressionAttributeNames: this.props.expressionAttributeNames, + ExpressionAttributeValues: transformAttributeValueMap(this.props.expressionAttributeValues), + ReturnConsumedCapacity: this.props.returnConsumedCapacity, + ReturnItemCollectionMetrics: this.props.returnItemCollectionMetrics, + ReturnValues: this.props.returnValues, + }), + }; + } +} diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/dynamodb/shared-types.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/dynamodb/shared-types.ts new file mode 100644 index 0000000000000..95a1151eb1803 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/dynamodb/shared-types.ts @@ -0,0 +1,251 @@ +import { transformAttributeValueMap } from './private/utils'; + +/** + * Determines the level of detail about provisioned throughput consumption that is returned. + */ +export enum DynamoConsumedCapacity { + /** + * The response includes the aggregate ConsumedCapacity for the operation, + * together with ConsumedCapacity for each table and secondary index that was accessed + */ + INDEXES = 'INDEXES', + + /** + * The response includes only the aggregate ConsumedCapacity for the operation. + */ + TOTAL = 'TOTAL', + + /** + * No ConsumedCapacity details are included in the response. + */ + NONE = 'NONE', +} + +/** + * Determines whether item collection metrics are returned. + */ +export enum DynamoItemCollectionMetrics { + /** + * If set to SIZE, the response includes statistics about item collections, + * if any, that were modified during the operation. + */ + SIZE = 'SIZE', + + /** + * If set to NONE, no statistics are returned. + */ + NONE = 'NONE', +} + +/** + * Use ReturnValues if you want to get the item attributes as they appear before or after they are changed + */ +export enum DynamoReturnValues { + /** + * Nothing is returned + */ + NONE = 'NONE', + + /** + * Returns all of the attributes of the item + */ + ALL_OLD = 'ALL_OLD', + + /** + * Returns only the updated attributes + */ + UPDATED_OLD = 'UPDATED_OLD', + + /** + * Returns all of the attributes of the item + */ + ALL_NEW = 'ALL_NEW', + + /** + * Returns only the updated attributes + */ + UPDATED_NEW = 'UPDATED_NEW', +} + +/** + * Class to generate projection expression + */ +export class DynamoProjectionExpression { + private expression: string[] = []; + + /** + * Adds the passed attribute to the chain + * + * @param attr Attribute name + */ + public withAttribute(attr: string): DynamoProjectionExpression { + if (this.expression.length) { + this.expression.push(`.${attr}`); + } else { + this.expression.push(attr); + } + return this; + } + + /** + * Adds the array literal access for passed index + * + * @param index array index + */ + public atIndex(index: number): DynamoProjectionExpression { + if (!this.expression.length) { + throw new Error('Expression must start with an attribute'); + } + + this.expression.push(`[${index}]`); + return this; + } + + /** + * converts and return the string expression + */ + public toString(): string { + return this.expression.join(''); + } +} + +/** + * Represents the data for an attribute. + * Each attribute value is described as a name-value pair. + * The name is the data type, and the value is the data itself. + * + * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_AttributeValue.html + */ +export class DynamoAttributeValue { + /** + * Sets an attribute of type String. For example: "S": "Hello" + * Strings may be literal values or as JsonPath + */ + public static fromString(value: string) { + return new DynamoAttributeValue({ S: value }); + } + + /** + * Sets a literal number. For example: 1234 + * Numbers are sent across the network to DynamoDB as strings, + * to maximize compatibility across languages and libraries. + * However, DynamoDB treats them as number type attributes for mathematical operations. + */ + public static fromNumber(value: number) { + return new DynamoAttributeValue({ N: value.toString() }); + } + + /** + * Sets an attribute of type Number. For example: "N": "123.45" + * Numbers are sent across the network to DynamoDB as strings, + * to maximize compatibility across languages and libraries. + * However, DynamoDB treats them as number type attributes for mathematical operations. + * + * Numbers may be expressed as literal strings or as JsonPath + */ + public static numberFromString(value: string) { + return new DynamoAttributeValue({ N: value.toString() }); + } + + /** + * Sets an attribute of type Binary. For example: "B": "dGhpcyB0ZXh0IGlzIGJhc2U2NC1lbmNvZGVk" + * + * @param value base-64 encoded string + */ + public static fromBinary(value: string) { + return new DynamoAttributeValue({ B: value }); + } + + /** + * Sets an attribute of type String Set. For example: "SS": ["Giraffe", "Hippo" ,"Zebra"] + */ + public static fromStringSet(value: string[]) { + return new DynamoAttributeValue({ SS: value }); + } + + /** + * Sets an attribute of type Number Set. For example: "NS": ["42.2", "-19", "7.5", "3.14"] + * Numbers are sent across the network to DynamoDB as strings, + * to maximize compatibility across languages and libraries. + * However, DynamoDB treats them as number type attributes for mathematical operations. + */ + public static fromNumberSet(value: number[]) { + return new DynamoAttributeValue({ NS: value.map(String) }); + } + + /** + * Sets an attribute of type Number Set. For example: "NS": ["42.2", "-19", "7.5", "3.14"] + * Numbers are sent across the network to DynamoDB as strings, + * to maximize compatibility across languages and libraries. + * However, DynamoDB treats them as number type attributes for mathematical operations. + * + * Numbers may be expressed as literal strings or as JsonPath + */ + public static numberSetFromStrings(value: string[]) { + return new DynamoAttributeValue({ NS: value }); + } + + /** + * Sets an attribute of type Binary Set. For example: "BS": ["U3Vubnk=", "UmFpbnk=", "U25vd3k="] + */ + public static fromBinarySet(value: string[]) { + return new DynamoAttributeValue({ BS: value }); + } + + /** + * Sets an attribute of type Map. For example: "M": {"Name": {"S": "Joe"}, "Age": {"N": "35"}} + */ + public static fromMap(value: { [key: string]: DynamoAttributeValue }) { + return new DynamoAttributeValue({ M: transformAttributeValueMap(value) }); + } + + /** + * Sets an attribute of type Map. For example: "M": {"Name": {"S": "Joe"}, "Age": {"N": "35"}} + * + * @param value Json path that specifies state input to be used + */ + public static mapFromJsonPath(value: string) { + if (!value.startsWith('$')) { + throw new Error("Data JSON path values must either be exactly equal to '$' or start with '$.'"); + } + return new DynamoAttributeValue({ 'M.$': value }); + } + + /** + * Sets an attribute of type List. For example: "L": [ {"S": "Cookies"} , {"S": "Coffee"}, {"N", "3.14159"}] + */ + public static fromList(value: DynamoAttributeValue[]) { + return new DynamoAttributeValue({ L: value.map((val) => val.toObject()) }); + } + + /** + * Sets an attribute of type Null. For example: "NULL": true + */ + public static fromNull(value: boolean) { + return new DynamoAttributeValue({ NULL: value }); + } + + /** + * Sets an attribute of type Boolean. For example: "BOOL": true + */ + public static fromBoolean(value: boolean) { + return new DynamoAttributeValue({ BOOL: value }); + } + + /** + * Represents the data for the attribute. Data can be + * i.e. "S": "Hello" + */ + public readonly attributeValue: any; + + private constructor(value: any) { + this.attributeValue = value; + } + + /** + * Returns the DynamoDB attribute value + */ + public toObject() { + return this.attributeValue; + } +} diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/dynamodb/update-item.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/dynamodb/update-item.ts new file mode 100644 index 0000000000000..63c424beb7b20 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/dynamodb/update-item.ts @@ -0,0 +1,134 @@ +import * as ddb from '@aws-cdk/aws-dynamodb'; +import * as iam from '@aws-cdk/aws-iam'; +import * as sfn from '@aws-cdk/aws-stepfunctions'; +import { Construct, Stack } from '@aws-cdk/core'; +import { DynamoMethod, getDynamoResourceArn, transformAttributeValueMap } from './private/utils'; +import { DynamoAttributeValue, DynamoConsumedCapacity, DynamoItemCollectionMetrics, DynamoReturnValues } from './shared-types'; + +/** + * Properties for DynamoUpdateItem Task + */ +export interface DynamoUpdateItemProps extends sfn.TaskStateBaseProps { + /** + * The name of the table containing the requested item. + */ + readonly table: ddb.ITable; + + /** + * Primary key of the item to retrieve. + * + * For the primary key, you must provide all of the attributes. + * For example, with a simple primary key, you only need to provide a value for the partition key. + * For a composite primary key, you must provide values for both the partition key and the sort key. + * + * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_GetItem.html#DDB-GetItem-request-Key + */ + readonly key: { [key: string]: DynamoAttributeValue }; + + /** + * A condition that must be satisfied in order for a conditional DeleteItem to succeed. + * + * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_UpdateItem.html#DDB-UpdateItem-request-ConditionExpression + * + * @default - No condition expression + */ + readonly conditionExpression?: string; + + /** + * One or more substitution tokens for attribute names in an expression + * + * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_UpdateItem.html#DDB-UpdateItem-request-ExpressionAttributeNames + * + * @default - No expression attribute names + */ + readonly expressionAttributeNames?: { [key: string]: string }; + + /** + * One or more values that can be substituted in an expression. + * + * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_UpdateItem.html#DDB-UpdateItem-request-ExpressionAttributeValues + * + * @default - No expression attribute values + */ + readonly expressionAttributeValues?: { [key: string]: DynamoAttributeValue }; + + /** + * Determines the level of detail about provisioned throughput consumption that is returned in the response + * + * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_UpdateItem.html#DDB-UpdateItem-request-ReturnConsumedCapacity + * + * @default DynamoConsumedCapacity.NONE + */ + readonly returnConsumedCapacity?: DynamoConsumedCapacity; + + /** + * Determines whether item collection metrics are returned. + * If set to SIZE, the response includes statistics about item collections, if any, + * that were modified during the operation are returned in the response. + * If set to NONE (the default), no statistics are returned. + * + * @default DynamoItemCollectionMetrics.NONE + */ + readonly returnItemCollectionMetrics?: DynamoItemCollectionMetrics; + + /** + * Use ReturnValues if you want to get the item attributes as they appeared before they were deleted. + * + * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_UpdateItem.html#DDB-UpdateItem-request-ReturnValues + * + * @default DynamoReturnValues.NONE + */ + readonly returnValues?: DynamoReturnValues; + + /** + * An expression that defines one or more attributes to be updated, + * the action to be performed on them, and new values for them. + * + * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_UpdateItem.html#DDB-UpdateItem-request-UpdateExpression + * + * @default - No update expression + */ + readonly updateExpression?: string; +} + +/** + * A StepFunctions task to call DynamoUpdateItem + */ +export class DynamoUpdateItem extends sfn.TaskStateBase { + protected readonly taskMetrics?: sfn.TaskMetricsConfig; + protected readonly taskPolicies?: iam.PolicyStatement[]; + + constructor(scope: Construct, id: string, private readonly props: DynamoUpdateItemProps) { + super(scope, id, props); + + this.taskPolicies = [ + new iam.PolicyStatement({ + resources: [ + Stack.of(this).formatArn({ + service: 'dynamodb', + resource: 'table', + resourceName: props.table.tableName, + }), + ], + actions: [`dynamodb:${DynamoMethod.UPDATE}Item`], + }), + ]; + } + + protected renderTask(): any { + return { + Resource: getDynamoResourceArn(DynamoMethod.UPDATE), + Parameters: sfn.FieldUtils.renderObject({ + Key: transformAttributeValueMap(this.props.key), + TableName: this.props.table.tableName, + ConditionExpression: this.props.conditionExpression, + ExpressionAttributeNames: this.props.expressionAttributeNames, + ExpressionAttributeValues: transformAttributeValueMap(this.props.expressionAttributeValues), + ReturnConsumedCapacity: this.props.returnConsumedCapacity, + ReturnItemCollectionMetrics: this.props.returnItemCollectionMetrics, + ReturnValues: this.props.returnValues, + UpdateExpression: this.props.updateExpression, + }), + }; + } +} diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/index.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/index.ts index 4dad4bf2c295c..ec24d7f518d9c 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/index.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/index.ts @@ -27,4 +27,8 @@ export * from './glue/run-glue-job-task'; export * from './glue/start-job-run'; export * from './batch/run-batch-job'; export * from './batch/submit-job'; -export * from './dynamodb/call-dynamodb'; +export * from './dynamodb/get-item'; +export * from './dynamodb/put-item'; +export * from './dynamodb/update-item'; +export * from './dynamodb/delete-item'; +export * from './dynamodb/shared-types'; diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/package.json b/packages/@aws-cdk/aws-stepfunctions-tasks/package.json index 7a8d6299c4072..f2dcbba5acc77 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/package.json +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/package.json @@ -69,8 +69,9 @@ }, "dependencies": { "@aws-cdk/assets": "0.0.0", - "@aws-cdk/aws-cloudwatch": "0.0.0", "@aws-cdk/aws-batch": "0.0.0", + "@aws-cdk/aws-cloudwatch": "0.0.0", + "@aws-cdk/aws-dynamodb": "0.0.0", "@aws-cdk/aws-ec2": "0.0.0", "@aws-cdk/aws-ecr": "0.0.0", "@aws-cdk/aws-ecr-assets": "0.0.0", @@ -91,6 +92,7 @@ "@aws-cdk/assets": "0.0.0", "@aws-cdk/aws-batch": "0.0.0", "@aws-cdk/aws-cloudwatch": "0.0.0", + "@aws-cdk/aws-dynamodb": "0.0.0", "@aws-cdk/aws-ec2": "0.0.0", "@aws-cdk/aws-ecr": "0.0.0", "@aws-cdk/aws-ecr-assets": "0.0.0", diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/dynamodb/call-dynamodb.test.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/dynamodb/call-dynamodb.test.ts deleted file mode 100644 index 13837aaff3bbb..0000000000000 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/dynamodb/call-dynamodb.test.ts +++ /dev/null @@ -1,346 +0,0 @@ -import * as sfn from '@aws-cdk/aws-stepfunctions'; -import * as cdk from '@aws-cdk/core'; -import * as tasks from '../../lib'; - -let stack: cdk.Stack; -const TABLE_NAME = 'SOME_TABLE'; - -beforeEach(() => { - // GIVEN - stack = new cdk.Stack(); -}); - -test('GetItem task', () => { - // WHEN - const task = new sfn.Task(stack, 'GetItem', { - task: tasks.CallDynamoDB.getItem({ - partitionKey: { - name: 'SOME_KEY', - value: new tasks.DynamoAttributeValue().withS('1234'), - }, - sortKey: { - name: 'OTHER_KEY', - value: new tasks.DynamoAttributeValue().withN('4321'), - }, - tableName: TABLE_NAME, - consistentRead: true, - expressionAttributeNames: { OTHER_KEY: '#OK' }, - projectionExpression: [ - new tasks.DynamoProjectionExpression() - .withAttribute('Messages') - .atIndex(1) - .withAttribute('Tags'), - new tasks.DynamoProjectionExpression().withAttribute('ID'), - ], - returnConsumedCapacity: tasks.DynamoConsumedCapacity.TOTAL, - }), - }); - - // THEN - expect(stack.resolve(task.toStateJson())).toEqual({ - Type: 'Task', - Resource: { - 'Fn::Join': [ - '', - [ - 'arn:', - { - Ref: 'AWS::Partition', - }, - ':states:::dynamodb:getItem', - ], - ], - }, - End: true, - Parameters: { - Key: { SOME_KEY: { S: '1234' }, OTHER_KEY: { N: '4321' } }, - TableName: TABLE_NAME, - ConsistentRead: true, - ExpressionAttributeNames: { OTHER_KEY: '#OK' }, - ProjectionExpression: 'Messages[1].Tags,ID', - ReturnConsumedCapacity: 'TOTAL', - }, - }); -}); - -test('PutItem task', () => { - // WHEN - const task = new sfn.Task(stack, 'PutItem', { - task: tasks.CallDynamoDB.putItem({ - item: { SOME_KEY: new tasks.DynamoAttributeValue().withS('1234') }, - tableName: TABLE_NAME, - conditionExpression: 'ForumName <> :f and Subject <> :s', - expressionAttributeNames: { OTHER_KEY: '#OK' }, - expressionAttributeValues: { - ':val': new tasks.DynamoAttributeValue().withN( - sfn.Data.stringAt('$.Item.TotalCount.N'), - ), - }, - returnConsumedCapacity: tasks.DynamoConsumedCapacity.TOTAL, - returnItemCollectionMetrics: tasks.DynamoItemCollectionMetrics.SIZE, - returnValues: tasks.DynamoReturnValues.ALL_NEW, - }), - }); - - // THEN - expect(stack.resolve(task.toStateJson())).toEqual({ - Type: 'Task', - Resource: { - 'Fn::Join': [ - '', - [ - 'arn:', - { - Ref: 'AWS::Partition', - }, - ':states:::dynamodb:putItem', - ], - ], - }, - End: true, - Parameters: { - Item: { SOME_KEY: { S: '1234' } }, - TableName: TABLE_NAME, - ConditionExpression: 'ForumName <> :f and Subject <> :s', - ExpressionAttributeNames: { OTHER_KEY: '#OK' }, - ExpressionAttributeValues: { ':val': { 'N.$': '$.Item.TotalCount.N' } }, - ReturnConsumedCapacity: 'TOTAL', - ReturnItemCollectionMetrics: 'SIZE', - ReturnValues: 'ALL_NEW', - }, - }); -}); - -test('DeleteItem task', () => { - // WHEN - const task = new sfn.Task(stack, 'DeleteItem', { - task: tasks.CallDynamoDB.deleteItem({ - partitionKey: { - name: 'SOME_KEY', - value: new tasks.DynamoAttributeValue().withS('1234'), - }, - tableName: TABLE_NAME, - conditionExpression: 'ForumName <> :f and Subject <> :s', - expressionAttributeNames: { OTHER_KEY: '#OK' }, - expressionAttributeValues: { - ':val': new tasks.DynamoAttributeValue().withN( - sfn.Data.stringAt('$.Item.TotalCount.N'), - ), - }, - returnConsumedCapacity: tasks.DynamoConsumedCapacity.TOTAL, - returnItemCollectionMetrics: tasks.DynamoItemCollectionMetrics.SIZE, - returnValues: tasks.DynamoReturnValues.ALL_NEW, - }), - }); - - // THEN - expect(stack.resolve(task.toStateJson())).toEqual({ - Type: 'Task', - Resource: { - 'Fn::Join': [ - '', - [ - 'arn:', - { - Ref: 'AWS::Partition', - }, - ':states:::dynamodb:deleteItem', - ], - ], - }, - End: true, - Parameters: { - Key: { SOME_KEY: { S: '1234' } }, - TableName: TABLE_NAME, - ConditionExpression: 'ForumName <> :f and Subject <> :s', - ExpressionAttributeNames: { OTHER_KEY: '#OK' }, - ExpressionAttributeValues: { ':val': { 'N.$': '$.Item.TotalCount.N' } }, - ReturnConsumedCapacity: 'TOTAL', - ReturnItemCollectionMetrics: 'SIZE', - ReturnValues: 'ALL_NEW', - }, - }); -}); - -test('UpdateItem task', () => { - // WHEN - const task = new sfn.Task(stack, 'UpdateItem', { - task: tasks.CallDynamoDB.updateItem({ - partitionKey: { - name: 'SOME_KEY', - value: new tasks.DynamoAttributeValue().withS('1234'), - }, - tableName: TABLE_NAME, - conditionExpression: 'ForumName <> :f and Subject <> :s', - expressionAttributeNames: { OTHER_KEY: '#OK' }, - expressionAttributeValues: { - ':val': new tasks.DynamoAttributeValue().withN( - sfn.Data.stringAt('$.Item.TotalCount.N'), - ), - }, - returnConsumedCapacity: tasks.DynamoConsumedCapacity.TOTAL, - returnItemCollectionMetrics: tasks.DynamoItemCollectionMetrics.SIZE, - returnValues: tasks.DynamoReturnValues.ALL_NEW, - updateExpression: 'SET TotalCount = TotalCount + :val', - }), - }); - - // THEN - expect(stack.resolve(task.toStateJson())).toEqual({ - Type: 'Task', - Resource: { - 'Fn::Join': [ - '', - [ - 'arn:', - { - Ref: 'AWS::Partition', - }, - ':states:::dynamodb:updateItem', - ], - ], - }, - End: true, - Parameters: { - Key: { SOME_KEY: { S: '1234' } }, - TableName: TABLE_NAME, - ConditionExpression: 'ForumName <> :f and Subject <> :s', - ExpressionAttributeNames: { OTHER_KEY: '#OK' }, - ExpressionAttributeValues: { ':val': { 'N.$': '$.Item.TotalCount.N' } }, - ReturnConsumedCapacity: 'TOTAL', - ReturnItemCollectionMetrics: 'SIZE', - ReturnValues: 'ALL_NEW', - UpdateExpression: 'SET TotalCount = TotalCount + :val', - }, - }); -}); - -test('supports tokens', () => { - // WHEN - const task = new sfn.Task(stack, 'GetItem', { - task: tasks.CallDynamoDB.getItem({ - partitionKey: { - name: 'SOME_KEY', - value: new tasks.DynamoAttributeValue().withS( - sfn.Data.stringAt('$.partitionKey'), - ), - }, - sortKey: { - name: 'OTHER_KEY', - value: new tasks.DynamoAttributeValue().withN( - sfn.Data.stringAt('$.sortKey'), - ), - }, - tableName: sfn.Data.stringAt('$.tableName'), - consistentRead: true, - expressionAttributeNames: { OTHER_KEY: sfn.Data.stringAt('$.otherKey') }, - projectionExpression: [ - new tasks.DynamoProjectionExpression() - .withAttribute('Messages') - .atIndex(1) - .withAttribute('Tags'), - new tasks.DynamoProjectionExpression().withAttribute('ID'), - ], - returnConsumedCapacity: tasks.DynamoConsumedCapacity.TOTAL, - }), - }); - - // THEN - expect(stack.resolve(task.toStateJson())).toEqual({ - Type: 'Task', - Resource: { - 'Fn::Join': [ - '', - [ - 'arn:', - { - Ref: 'AWS::Partition', - }, - ':states:::dynamodb:getItem', - ], - ], - }, - End: true, - Parameters: { - // tslint:disable:object-literal-key-quotes - Key: { - SOME_KEY: { 'S.$': '$.partitionKey' }, - OTHER_KEY: { 'N.$': '$.sortKey' }, - }, - 'TableName.$': '$.tableName', - ConsistentRead: true, - ExpressionAttributeNames: { 'OTHER_KEY.$': '$.otherKey' }, - ProjectionExpression: 'Messages[1].Tags,ID', - ReturnConsumedCapacity: 'TOTAL', - }, - }); -}); - -test('Invalid value of TableName should throw', () => { - expect(() => { - new sfn.Task(stack, 'GetItem', { - task: tasks.CallDynamoDB.getItem({ - partitionKey: { - name: 'SOME_KEY', - value: new tasks.DynamoAttributeValue().withS('1234'), - }, - tableName: 'ab', - }), - }); - }).toThrow( - /TableName should not contain alphanumeric characters and should be between 3-255 characters long. Received: ab/, - ); - - expect(() => { - new sfn.Task(stack, 'GetItem', { - task: tasks.CallDynamoDB.getItem({ - partitionKey: { - name: 'SOME_KEY', - value: new tasks.DynamoAttributeValue().withS('1234'), - }, - tableName: - 'abU93s5MTZDv6TYLk3Q3BE3Hj3AMca3NOb5ypSNZv1JZIONg7p8L8LNxuAStavPxYZKcoG36KwXktkuFHf0jJvt7SKofEqwYHmmK0tNJSkGoPe3MofnB7IWu3V48HbrqNGZqW005CMmDHESQWf40JK8qK0CSQtM8Z64zqysB7SZZazDRm7kKr062RXQKL82nvTxnKxTPfCHiG2YJEhuFdUywHCTN2Rjinl3P7TpwyIuPWyYHm6nZodRKLMmWpgUftZ', - }), - }); - }).toThrow( - /TableName should not contain alphanumeric characters and should be between 3-255 characters long. Received: abU93s5MTZDv6TYLk3Q3BE3Hj3AMca3NOb5ypSNZv1JZIONg7p8L8LNxuAStavPxYZKcoG36KwXktkuFHf0jJvt7SKofEqwYHmmK0tNJSkGoPe3MofnB7IWu3V48HbrqNGZqW005CMmDHESQWf40JK8qK0CSQtM8Z64zqysB7SZZazDRm7kKr062RXQKL82nvTxnKxTPfCHiG2YJEhuFdUywHCTN2Rjinl3P7TpwyIuPWyYHm6nZodRKLMmWpgUftZ/, - ); - - expect(() => { - new sfn.Task(stack, 'GetItem', { - task: tasks.CallDynamoDB.getItem({ - partitionKey: { - name: 'SOME_KEY', - value: new tasks.DynamoAttributeValue().withS('1234'), - }, - tableName: 'abcd@', - }), - }); - }).toThrow( - /TableName should not contain alphanumeric characters and should be between 3-255 characters long. Received: abcd@/, - ); -}); - -describe('DynamoProjectionExpression', () => { - test('should correctly configure projectionExpression', () => { - expect( - new tasks.DynamoProjectionExpression() - .withAttribute('Messages') - .atIndex(1) - .atIndex(10) - .withAttribute('Tags') - .withAttribute('Items') - .atIndex(0) - .toString(), - ).toEqual('Messages[1][10].Tags.Items[0]'); - }); - - test('should throw if expression starts with atIndex', () => { - expect(() => - new tasks.DynamoProjectionExpression() - .atIndex(1) - .withAttribute('Messages') - .toString(), - ).toThrow(/Expression must start with an attribute/); - }); -}); diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/dynamodb/delete-item.test.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/dynamodb/delete-item.test.ts new file mode 100644 index 0000000000000..031055d1d0f8c --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/dynamodb/delete-item.test.ts @@ -0,0 +1,65 @@ +import * as ddb from '@aws-cdk/aws-dynamodb'; +import * as sfn from '@aws-cdk/aws-stepfunctions'; +import * as cdk from '@aws-cdk/core'; +import * as tasks from '../../lib'; + +let stack: cdk.Stack; +let table: ddb.Table; + +beforeEach(() => { + // GIVEN + stack = new cdk.Stack(); + table = new ddb.Table(stack, 'my-table', { + tableName: 'my-table', + partitionKey: { + name: 'name', + type: ddb.AttributeType.STRING, + }, + }); +}); + +test('DeleteItem task', () => { + // WHEN + const task = new tasks.DynamoDeleteItem(stack, 'DeleteItem', { + key: { SOME_KEY: tasks.DynamoAttributeValue.fromString('1234') }, + table, + conditionExpression: 'ForumName <> :f and Subject <> :s', + expressionAttributeNames: { OTHER_KEY: '#OK' }, + expressionAttributeValues: { + ':val': tasks.DynamoAttributeValue.numberFromString(sfn.Data.stringAt('$.Item.TotalCount.N')), + }, + returnConsumedCapacity: tasks.DynamoConsumedCapacity.TOTAL, + returnItemCollectionMetrics: tasks.DynamoItemCollectionMetrics.SIZE, + returnValues: tasks.DynamoReturnValues.ALL_NEW, + }); + + // THEN + expect(stack.resolve(task.toStateJson())).toEqual({ + Type: 'Task', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':states:::dynamodb:deleteItem', + ], + ], + }, + End: true, + Parameters: { + Key: { SOME_KEY: { S: '1234' } }, + TableName: { + Ref: 'mytable0324D45C', + }, + ConditionExpression: 'ForumName <> :f and Subject <> :s', + ExpressionAttributeNames: { OTHER_KEY: '#OK' }, + ExpressionAttributeValues: { ':val': { 'N.$': '$.Item.TotalCount.N' } }, + ReturnConsumedCapacity: 'TOTAL', + ReturnItemCollectionMetrics: 'SIZE', + ReturnValues: 'ALL_NEW', + }, + }); +}); diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/dynamodb/get-item.test.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/dynamodb/get-item.test.ts new file mode 100644 index 0000000000000..b09664737a4ef --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/dynamodb/get-item.test.ts @@ -0,0 +1,115 @@ +import * as ddb from '@aws-cdk/aws-dynamodb'; +import * as sfn from '@aws-cdk/aws-stepfunctions'; +import * as cdk from '@aws-cdk/core'; +import * as tasks from '../../lib'; + +let stack: cdk.Stack; +let table: ddb.Table; + +beforeEach(() => { + // GIVEN + stack = new cdk.Stack(); + table = new ddb.Table(stack, 'my-table', { + tableName: 'my-table', + partitionKey: { + name: 'name', + type: ddb.AttributeType.STRING, + }, + }); +}); + +test('GetItem task', () => { + // WHEN + const task = new tasks.DynamoGetItem(stack, 'GetItem', { + key: { + SOME_KEY: tasks.DynamoAttributeValue.fromString('1234'), + OTHER_KEY: tasks.DynamoAttributeValue.fromNumber(4321), + }, + table, + consistentRead: true, + expressionAttributeNames: { OTHER_KEY: '#OK' }, + projectionExpression: [ + new tasks.DynamoProjectionExpression().withAttribute('Messages').atIndex(1).withAttribute('Tags'), + new tasks.DynamoProjectionExpression().withAttribute('ID'), + ], + returnConsumedCapacity: tasks.DynamoConsumedCapacity.TOTAL, + }); + + // THEN + expect(stack.resolve(task.toStateJson())).toEqual({ + Type: 'Task', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':states:::dynamodb:getItem', + ], + ], + }, + End: true, + Parameters: { + Key: { SOME_KEY: { S: '1234' }, OTHER_KEY: { N: '4321' } }, + TableName: { + Ref: 'mytable0324D45C', + }, + ConsistentRead: true, + ExpressionAttributeNames: { OTHER_KEY: '#OK' }, + ProjectionExpression: 'Messages[1].Tags,ID', + ReturnConsumedCapacity: 'TOTAL', + }, + }); +}); + +test('supports tokens', () => { + // WHEN + const task = new tasks.DynamoGetItem(stack, 'GetItem', { + key: { + SOME_KEY: tasks.DynamoAttributeValue.fromString(sfn.Data.stringAt('$.partitionKey')), + OTHER_KEY: tasks.DynamoAttributeValue.numberFromString(sfn.Data.stringAt('$.sortKey')), + }, + table, + consistentRead: true, + expressionAttributeNames: { OTHER_KEY: sfn.Data.stringAt('$.otherKey') }, + projectionExpression: [ + new tasks.DynamoProjectionExpression().withAttribute('Messages').atIndex(1).withAttribute('Tags'), + new tasks.DynamoProjectionExpression().withAttribute('ID'), + ], + returnConsumedCapacity: tasks.DynamoConsumedCapacity.TOTAL, + }); + + // THEN + expect(stack.resolve(task.toStateJson())).toEqual({ + Type: 'Task', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':states:::dynamodb:getItem', + ], + ], + }, + End: true, + Parameters: { + // tslint:disable:object-literal-key-quotes + Key: { + SOME_KEY: { 'S.$': '$.partitionKey' }, + OTHER_KEY: { 'N.$': '$.sortKey' }, + }, + TableName: { + Ref: 'mytable0324D45C', + }, + ConsistentRead: true, + ExpressionAttributeNames: { 'OTHER_KEY.$': '$.otherKey' }, + ProjectionExpression: 'Messages[1].Tags,ID', + ReturnConsumedCapacity: 'TOTAL', + }, + }); +}); diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/dynamodb/integ.call-dynamodb.expected.json b/packages/@aws-cdk/aws-stepfunctions-tasks/test/dynamodb/integ.call-dynamodb.expected.json index 5bc3b566a2d42..e4876ddef2c6a 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/dynamodb/integ.call-dynamodb.expected.json +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/dynamodb/integ.call-dynamodb.expected.json @@ -1,5 +1,29 @@ { "Resources": { + "Messages804FA4EB": { + "Type": "AWS::DynamoDB::Table", + "Properties": { + "KeySchema": [ + { + "AttributeName": "MessageId", + "KeyType": "HASH" + } + ], + "AttributeDefinitions": [ + { + "AttributeName": "MessageId", + "AttributeType": "S" + } + ], + "ProvisionedThroughput": { + "ReadCapacityUnits": 10, + "WriteCapacityUnits": 5 + }, + "TableName": "Messages" + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, "StateMachineRoleB840431D": { "Type": "AWS::IAM::Role", "Properties": { @@ -52,7 +76,10 @@ { "Ref": "AWS::AccountId" }, - ":table/Messages" + ":table/", + { + "Ref": "Messages804FA4EB" + } ] ] } @@ -76,7 +103,10 @@ { "Ref": "AWS::AccountId" }, - ":table/Messages" + ":table/", + { + "Ref": "Messages804FA4EB" + } ] ] } @@ -100,7 +130,10 @@ { "Ref": "AWS::AccountId" }, - ":table/Messages" + ":table/", + { + "Ref": "Messages804FA4EB" + } ] ] } @@ -124,7 +157,10 @@ { "Ref": "AWS::AccountId" }, - ":table/Messages" + ":table/", + { + "Ref": "Messages804FA4EB" + } ] ] } @@ -143,39 +179,59 @@ "StateMachine2E01A3A5": { "Type": "AWS::StepFunctions::StateMachine", "Properties": { + "RoleArn": { + "Fn::GetAtt": [ + "StateMachineRoleB840431D", + "Arn" + ] + }, "DefinitionString": { "Fn::Join": [ "", [ - "{\"StartAt\":\"Start\",\"States\":{\"Start\":{\"Type\":\"Pass\",\"Result\":{\"bar\":\"SomeValue\"},\"Next\":\"PutItem\"},\"PutItem\":{\"Next\":\"GetItemAfterPut\",\"Parameters\":{\"Item\":{\"MessageId\":{\"S\":\"1234\"},\"Text\":{\"S.$\":\"$.bar\"},\"TotalCount\":{\"N\":\"18\"}},\"TableName\":\"Messages\"},\"Type\":\"Task\",\"Resource\":\"arn:", + "{\"StartAt\":\"Start\",\"States\":{\"Start\":{\"Type\":\"Pass\",\"Result\":{\"bar\":\"SomeValue\"},\"Next\":\"PutItem\"},\"PutItem\":{\"Next\":\"GetItemAfterPut\",\"Type\":\"Task\",\"Resource\":\"arn:", { "Ref": "AWS::Partition" }, - ":states:::dynamodb:putItem\"},\"GetItemAfterPut\":{\"Next\":\"UpdateItem\",\"Parameters\":{\"Key\":{\"MessageId\":{\"S\":\"1234\"}},\"TableName\":\"Messages\",\"ConsistentRead\":false},\"Type\":\"Task\",\"Resource\":\"arn:", + ":states:::dynamodb:putItem\",\"Parameters\":{\"Item\":{\"MessageId\":{\"S\":\"1234\"},\"Text\":{\"S.$\":\"$.bar\"},\"TotalCount\":{\"N\":\"18\"}},\"TableName\":\"", + { + "Ref": "Messages804FA4EB" + }, + "\"}},\"GetItemAfterPut\":{\"Next\":\"UpdateItem\",\"Type\":\"Task\",\"Resource\":\"arn:", { "Ref": "AWS::Partition" }, - ":states:::dynamodb:getItem\"},\"UpdateItem\":{\"Next\":\"GetItemAfterUpdate\",\"Parameters\":{\"Key\":{\"MessageId\":{\"S\":\"1234\"}},\"TableName\":\"Messages\",\"ExpressionAttributeValues\":{\":val\":{\"N.$\":\"$.Item.TotalCount.N\"},\":rand\":{\"N\":\"24\"}},\"UpdateExpression\":\"SET TotalCount = :val + :rand\"},\"Type\":\"Task\",\"Resource\":\"arn:", + ":states:::dynamodb:getItem\",\"Parameters\":{\"Key\":{\"MessageId\":{\"S\":\"1234\"}},\"TableName\":\"", + { + "Ref": "Messages804FA4EB" + }, + "\",\"ConsistentRead\":false}},\"UpdateItem\":{\"Next\":\"GetItemAfterUpdate\",\"Type\":\"Task\",\"Resource\":\"arn:", { "Ref": "AWS::Partition" }, - ":states:::dynamodb:updateItem\"},\"GetItemAfterUpdate\":{\"Next\":\"DeleteItem\",\"Parameters\":{\"Key\":{\"MessageId\":{\"S\":\"1234\"}},\"TableName\":\"Messages\",\"ConsistentRead\":false},\"OutputPath\":\"$.Item.TotalCount.N\",\"Type\":\"Task\",\"Resource\":\"arn:", + ":states:::dynamodb:updateItem\",\"Parameters\":{\"Key\":{\"MessageId\":{\"S\":\"1234\"}},\"TableName\":\"", + { + "Ref": "Messages804FA4EB" + }, + "\",\"ExpressionAttributeValues\":{\":val\":{\"N.$\":\"$.Item.TotalCount.N\"},\":rand\":{\"N\":\"24\"}},\"UpdateExpression\":\"SET TotalCount = :val + :rand\"}},\"GetItemAfterUpdate\":{\"Next\":\"DeleteItem\",\"Type\":\"Task\",\"OutputPath\":\"$.Item.TotalCount.N\",\"Resource\":\"arn:", { "Ref": "AWS::Partition" }, - ":states:::dynamodb:getItem\"},\"DeleteItem\":{\"End\":true,\"Parameters\":{\"Key\":{\"MessageId\":{\"S\":\"1234\"}},\"TableName\":\"Messages\"},\"Type\":\"Task\",\"Resource\":\"arn:", + ":states:::dynamodb:getItem\",\"Parameters\":{\"Key\":{\"MessageId\":{\"S\":\"1234\"}},\"TableName\":\"", + { + "Ref": "Messages804FA4EB" + }, + "\",\"ConsistentRead\":false}},\"DeleteItem\":{\"End\":true,\"Type\":\"Task\",\"ResultPath\":null,\"Resource\":\"arn:", { "Ref": "AWS::Partition" }, - ":states:::dynamodb:deleteItem\",\"ResultPath\":null}}}" + ":states:::dynamodb:deleteItem\",\"Parameters\":{\"Key\":{\"MessageId\":{\"S\":\"1234\"}},\"TableName\":\"", + { + "Ref": "Messages804FA4EB" + }, + "\"}}}}" ] ] - }, - "RoleArn": { - "Fn::GetAtt": [ - "StateMachineRoleB840431D", - "Arn" - ] } }, "DependsOn": [ diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/dynamodb/integ.call-dynamodb.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/dynamodb/integ.call-dynamodb.ts index 7204abd930d46..3728a64295122 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/dynamodb/integ.call-dynamodb.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/dynamodb/integ.call-dynamodb.ts @@ -1,27 +1,16 @@ +import * as ddb from '@aws-cdk/aws-dynamodb'; import * as sfn from '@aws-cdk/aws-stepfunctions'; import * as cdk from '@aws-cdk/core'; import * as tasks from '../../lib'; /** - * Pre verification steps: - * * aws dynamodb create-table --table-name Messages --key-schema AttributeName=MessageId,KeyType=HASH \ - * * --attribute-definitions AttributeName=MessageId,AttributeType=S \ - * * --provisioned-throughput ReadCapacityUnits=10,WriteCapacityUnits=5 - */ - -/* + * * Stack verification steps: * * aws stepfunctions start-execution --state-machine-arn : should return execution arn * * - * * aws stepfunctions describe-execution --execution-arn --query 'status': should return status as SUCCEEDED - * * aws stepfunctions describe-execution --execution-arn --query 'output': should return the number 42 - */ - -/** - * Post verification steps: - * * aws dynamodb delete-table --table-name Messages + * * aws stepfunctions describe-execution --execution-arn --query 'status': should return status as SUCCEEDED + * * aws stepfunctions describe-execution --execution-arn --query 'output': should return the number 42 */ - class CallDynamoDBStack extends cdk.Stack { constructor(scope: cdk.App, id: string, props: cdk.StackProps = {}) { super(scope, id, props); @@ -31,65 +20,50 @@ class CallDynamoDBStack extends cdk.Stack { const firstNumber = 18; const secondNumber = 24; - const putItemTask = new sfn.Task(this, 'PutItem', { - task: tasks.CallDynamoDB.putItem({ - item: { - MessageId: new tasks.DynamoAttributeValue().withS(MESSAGE_ID), - Text: new tasks.DynamoAttributeValue().withS( - sfn.Data.stringAt('$.bar'), - ), - TotalCount: new tasks.DynamoAttributeValue().withN(`${firstNumber}`), - }, - tableName: TABLE_NAME, - }), + const table = new ddb.Table(this, 'Messages', { + tableName: TABLE_NAME, + partitionKey: { + name: 'MessageId', + type: ddb.AttributeType.STRING, + }, + readCapacity: 10, + writeCapacity: 5, + removalPolicy: cdk.RemovalPolicy.DESTROY, + }); + + const putItemTask = new tasks.DynamoPutItem(this, 'PutItem', { + item: { + MessageId: tasks.DynamoAttributeValue.fromString(MESSAGE_ID), + Text: tasks.DynamoAttributeValue.fromString(sfn.Data.stringAt('$.bar')), + TotalCount: tasks.DynamoAttributeValue.fromNumber(firstNumber), + }, + table, }); - const getItemTaskAfterPut = new sfn.Task(this, 'GetItemAfterPut', { - task: tasks.CallDynamoDB.getItem({ - partitionKey: { - name: 'MessageId', - value: new tasks.DynamoAttributeValue().withS(MESSAGE_ID), - }, - tableName: TABLE_NAME, - }), + const getItemTaskAfterPut = new tasks.DynamoGetItem(this, 'GetItemAfterPut', { + key: { MessageId: tasks.DynamoAttributeValue.fromString(MESSAGE_ID) }, + table, }); - const updateItemTask = new sfn.Task(this, 'UpdateItem', { - task: tasks.CallDynamoDB.updateItem({ - partitionKey: { - name: 'MessageId', - value: new tasks.DynamoAttributeValue().withS(MESSAGE_ID), - }, - tableName: TABLE_NAME, - expressionAttributeValues: { - ':val': new tasks.DynamoAttributeValue().withN( - sfn.Data.stringAt('$.Item.TotalCount.N'), - ), - ':rand': new tasks.DynamoAttributeValue().withN(`${secondNumber}`), - }, - updateExpression: 'SET TotalCount = :val + :rand', - }), + const updateItemTask = new tasks.DynamoUpdateItem(this, 'UpdateItem', { + key: { MessageId: tasks.DynamoAttributeValue.fromString(MESSAGE_ID) }, + table, + expressionAttributeValues: { + ':val': tasks.DynamoAttributeValue.numberFromString(sfn.Data.stringAt('$.Item.TotalCount.N')), + ':rand': tasks.DynamoAttributeValue.fromNumber(secondNumber), + }, + updateExpression: 'SET TotalCount = :val + :rand', }); - const getItemTaskAfterUpdate = new sfn.Task(this, 'GetItemAfterUpdate', { - task: tasks.CallDynamoDB.getItem({ - partitionKey: { - name: 'MessageId', - value: new tasks.DynamoAttributeValue().withS(MESSAGE_ID), - }, - tableName: TABLE_NAME, - }), + const getItemTaskAfterUpdate = new tasks.DynamoGetItem(this, 'GetItemAfterUpdate', { + key: { MessageId: tasks.DynamoAttributeValue.fromString(MESSAGE_ID) }, + table, outputPath: sfn.Data.stringAt('$.Item.TotalCount.N'), }); - const deleteItemTask = new sfn.Task(this, 'DeleteItem', { - task: tasks.CallDynamoDB.deleteItem({ - partitionKey: { - name: 'MessageId', - value: new tasks.DynamoAttributeValue().withS(MESSAGE_ID), - }, - tableName: TABLE_NAME, - }), + const deleteItemTask = new tasks.DynamoDeleteItem(this, 'DeleteItem', { + key: { MessageId: tasks.DynamoAttributeValue.fromString(MESSAGE_ID) }, + table, resultPath: 'DISCARD', }); diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/dynamodb/put-item.test.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/dynamodb/put-item.test.ts new file mode 100644 index 0000000000000..bfa6840abe287 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/dynamodb/put-item.test.ts @@ -0,0 +1,65 @@ +import * as ddb from '@aws-cdk/aws-dynamodb'; +import * as sfn from '@aws-cdk/aws-stepfunctions'; +import * as cdk from '@aws-cdk/core'; +import * as tasks from '../../lib'; + +let stack: cdk.Stack; +let table: ddb.Table; + +beforeEach(() => { + // GIVEN + stack = new cdk.Stack(); + table = new ddb.Table(stack, 'my-table', { + tableName: 'my-table', + partitionKey: { + name: 'name', + type: ddb.AttributeType.STRING, + }, + }); +}); + +test('PutItem task', () => { + // WHEN + const task = new tasks.DynamoPutItem(stack, 'PutItem', { + item: { SOME_KEY: tasks.DynamoAttributeValue.fromString('1234') }, + table, + conditionExpression: 'ForumName <> :f and Subject <> :s', + expressionAttributeNames: { OTHER_KEY: '#OK' }, + expressionAttributeValues: { + ':val': tasks.DynamoAttributeValue.numberFromString(sfn.Data.stringAt('$.Item.TotalCount.N')), + }, + returnConsumedCapacity: tasks.DynamoConsumedCapacity.TOTAL, + returnItemCollectionMetrics: tasks.DynamoItemCollectionMetrics.SIZE, + returnValues: tasks.DynamoReturnValues.ALL_NEW, + }); + + // THEN + expect(stack.resolve(task.toStateJson())).toEqual({ + Type: 'Task', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':states:::dynamodb:putItem', + ], + ], + }, + End: true, + Parameters: { + Item: { SOME_KEY: { S: '1234' } }, + TableName: { + Ref: 'mytable0324D45C', + }, + ConditionExpression: 'ForumName <> :f and Subject <> :s', + ExpressionAttributeNames: { OTHER_KEY: '#OK' }, + ExpressionAttributeValues: { ':val': { 'N.$': '$.Item.TotalCount.N' } }, + ReturnConsumedCapacity: 'TOTAL', + ReturnItemCollectionMetrics: 'SIZE', + ReturnValues: 'ALL_NEW', + }, + }); +}); diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/dynamodb/shared-types.test.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/dynamodb/shared-types.test.ts new file mode 100644 index 0000000000000..111be02b15027 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/dynamodb/shared-types.test.ts @@ -0,0 +1,248 @@ +import * as sfn from '@aws-cdk/aws-stepfunctions'; +import * as tasks from '../../lib'; + +describe('DynamoProjectionExpression', () => { + test('should correctly configure projectionExpression', () => { + expect( + new tasks.DynamoProjectionExpression() + .withAttribute('Messages') + .atIndex(1) + .atIndex(10) + .withAttribute('Tags') + .withAttribute('Items') + .atIndex(0) + .toString(), + ).toEqual('Messages[1][10].Tags.Items[0]'); + }); + + test('should throw if expression starts with atIndex', () => { + expect(() => new tasks.DynamoProjectionExpression().atIndex(1).withAttribute('Messages').toString()).toThrow( + /Expression must start with an attribute/, + ); + }); +}); + +describe('DynamoAttributeValue', () => { + test('from string with a string literal', () => { + // GIVEN + const s = 'my-string'; + + // WHEN + const attribute = tasks.DynamoAttributeValue.fromString(s); + + // THEN + expect(sfn.FieldUtils.renderObject(attribute)).toEqual({ + attributeValue: { + S: s, + }, + }); + }); + + test('from string with a json path', () => { + // GIVEN + const s = '$.string'; + + // WHEN + const attribute = tasks.DynamoAttributeValue.fromString(sfn.Data.stringAt(s)); + + // THEN + expect(sfn.FieldUtils.renderObject(attribute)).toEqual({ + attributeValue: { + 'S.$': s, + }, + }); + }); + + test('from number', () => { + // GIVEN + const n = 9; + + // WHEN + const attribute = tasks.DynamoAttributeValue.fromNumber(n); + + // THEN + expect(sfn.FieldUtils.renderObject(attribute)).toEqual({ + attributeValue: { + N: `${n}`, + }, + }); + }); + + test('number from string', () => { + // GIVEN + const n = '9'; + + // WHEN + const attribute = tasks.DynamoAttributeValue.numberFromString(n); + + // THEN + expect(sfn.FieldUtils.renderObject(attribute)).toEqual({ + attributeValue: { + N: n, + }, + }); + }); + + test('from binary', () => { + // GIVEN + const b = 'ejBtZ3d0ZmJicQ=='; + + // WHEN + const attribute = tasks.DynamoAttributeValue.fromBinary(b); + + // THEN + expect(sfn.FieldUtils.renderObject(attribute)).toEqual({ + attributeValue: { + B: b, + }, + }); + }); + + test('from string set', () => { + // GIVEN + const ss = ['apple', 'banana']; + + // WHEN + const attribute = tasks.DynamoAttributeValue.fromStringSet(ss); + + // THEN + expect(sfn.FieldUtils.renderObject(attribute)).toEqual({ + attributeValue: { + SS: ss, + }, + }); + }); + + test('from number set', () => { + // GIVEN + const ns = [1, 2]; + + // WHEN + const attribute = tasks.DynamoAttributeValue.fromNumberSet(ns); + + // THEN + expect(sfn.FieldUtils.renderObject(attribute)).toEqual({ + attributeValue: { + NS: ['1', '2'], + }, + }); + }); + + test('number set from strings', () => { + // GIVEN + const ns = ['1', '2']; + + // WHEN + const attribute = tasks.DynamoAttributeValue.numberSetFromStrings(ns); + + // THEN + expect(sfn.FieldUtils.renderObject(attribute)).toEqual({ + attributeValue: { + NS: ns, + }, + }); + }); + + test('from binary set', () => { + // GIVEN + const bs = ['Y2RrIGlzIGF3ZXNvbWU=', 'ejBtZ3d0ZmJicQ==']; + + // WHEN + const attribute = tasks.DynamoAttributeValue.fromBinarySet(bs); + + // THEN + expect(sfn.FieldUtils.renderObject(attribute)).toEqual({ + attributeValue: { + BS: bs, + }, + }); + }); + + test('from map', () => { + // GIVEN + const m = { cdk: tasks.DynamoAttributeValue.fromString('is-cool') }; + + // WHEN + const attribute = tasks.DynamoAttributeValue.fromMap(m); + + // THEN + expect(sfn.FieldUtils.renderObject(attribute)).toEqual({ + attributeValue: { + M: { + cdk: { S: 'is-cool' }, + }, + }, + }); + }); + + test('map from json path', () => { + // GIVEN + const m = '$.path'; + + // WHEN + const attribute = tasks.DynamoAttributeValue.mapFromJsonPath(m); + + // THEN + expect(sfn.FieldUtils.renderObject(attribute)).toEqual({ + attributeValue: { + 'M.$': m, + }, + }); + }); + + test('map from invalid json path throws', () => { + // GIVEN + const m = 'invalid'; + + // WHEN / THEN + expect(() => { + tasks.DynamoAttributeValue.mapFromJsonPath(m); + }).toThrow("Data JSON path values must either be exactly equal to '$' or start with '$.'"); + }); + + test('from list', () => { + // GIVEN + const l = [tasks.DynamoAttributeValue.fromString('a string'), tasks.DynamoAttributeValue.fromNumber(7)]; + + // WHEN + const attribute = tasks.DynamoAttributeValue.fromList(l); + + // THEN + expect(sfn.FieldUtils.renderObject(attribute)).toEqual({ + attributeValue: { + L: [ + { + S: 'a string', + }, + { + N: '7', + }, + ], + }, + }); + }); + + test('from null', () => { + // WHEN + const attribute = tasks.DynamoAttributeValue.fromNull(true); + + // THEN + expect(sfn.FieldUtils.renderObject(attribute)).toEqual({ + attributeValue: { + NULL: true, + }, + }); + }); + + test('from boolean', () => { + // WHEN + const attribute = tasks.DynamoAttributeValue.fromBoolean(true); + + // THEN + expect(sfn.FieldUtils.renderObject(attribute)).toEqual({ + attributeValue: { + BOOL: true, + }, + }); + }); +}); diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/dynamodb/update-item.test.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/dynamodb/update-item.test.ts new file mode 100644 index 0000000000000..fcd38f03f384c --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/dynamodb/update-item.test.ts @@ -0,0 +1,67 @@ +import * as ddb from '@aws-cdk/aws-dynamodb'; +import * as sfn from '@aws-cdk/aws-stepfunctions'; +import * as cdk from '@aws-cdk/core'; +import * as tasks from '../../lib'; + +let stack: cdk.Stack; +let table: ddb.Table; + +beforeEach(() => { + // GIVEN + stack = new cdk.Stack(); + table = new ddb.Table(stack, 'my-table', { + tableName: 'my-table', + partitionKey: { + name: 'name', + type: ddb.AttributeType.STRING, + }, + }); +}); + +test('UpdateItem task', () => { + // WHEN + const task = new tasks.DynamoUpdateItem(stack, 'UpdateItem', { + key: { SOME_KEY: tasks.DynamoAttributeValue.fromString('1234') }, + table, + conditionExpression: 'ForumName <> :f and Subject <> :s', + expressionAttributeNames: { OTHER_KEY: '#OK' }, + expressionAttributeValues: { + ':val': tasks.DynamoAttributeValue.numberFromString(sfn.Data.stringAt('$.Item.TotalCount.N')), + }, + returnConsumedCapacity: tasks.DynamoConsumedCapacity.TOTAL, + returnItemCollectionMetrics: tasks.DynamoItemCollectionMetrics.SIZE, + returnValues: tasks.DynamoReturnValues.ALL_NEW, + updateExpression: 'SET TotalCount = TotalCount + :val', + }); + + // THEN + expect(stack.resolve(task.toStateJson())).toEqual({ + Type: 'Task', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':states:::dynamodb:updateItem', + ], + ], + }, + End: true, + Parameters: { + Key: { SOME_KEY: { S: '1234' } }, + TableName: { + Ref: 'mytable0324D45C', + }, + ConditionExpression: 'ForumName <> :f and Subject <> :s', + ExpressionAttributeNames: { OTHER_KEY: '#OK' }, + ExpressionAttributeValues: { ':val': { 'N.$': '$.Item.TotalCount.N' } }, + ReturnConsumedCapacity: 'TOTAL', + ReturnItemCollectionMetrics: 'SIZE', + ReturnValues: 'ALL_NEW', + UpdateExpression: 'SET TotalCount = TotalCount + :val', + }, + }); +}); From d9c4f5e67c54e1a2a436978fbc28fffd92b24cd6 Mon Sep 17 00:00:00 2001 From: Adam Ruka Date: Wed, 24 Jun 2020 05:47:11 -0700 Subject: [PATCH 19/21] feat(cfn-include): support logical id overrides (#8529) Previously, we created the Tokens needed for references like `Ref` and `Fn::GetAtt` without actually referencing the created L1s in the template, just by their logical IDs. That's not strictly correct, because users of `CfnInclude` can call `overrideLogicalId()` on the CloudFormation elements retrieved from the template, and we need to make sure to also reflect that in all references to that resource. Change the code converting from CloudFormation to CDK values to actually retrieve the appropriate L1 objects when encountering expressions like `Ref` and `Fn::GetAtt`. This also gives us correctly working cross-stack references for free. Related to #7375 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../cloudformation-include/lib/cfn-include.ts | 25 +- .../test/invalid-templates.test.ts | 12 + .../invalid/bucket-policy-without-bucket.json | 3 + ...-attribute-of-a-non-existent-resource.json | 12 + .../ref-ing-a-non-existent-element.json | 12 + .../test/valid-templates.test.ts | 81 +++++ packages/@aws-cdk/core/lib/cfn-parse.ts | 301 ++++++++++-------- packages/@aws-cdk/core/lib/from-cfn.ts | 8 + packages/@aws-cdk/core/test/test.cfn-parse.ts | 30 +- tools/cfn2ts/lib/codegen.ts | 19 +- 10 files changed, 354 insertions(+), 149 deletions(-) create mode 100644 packages/@aws-cdk/cloudformation-include/test/test-templates/invalid/getting-attribute-of-a-non-existent-resource.json create mode 100644 packages/@aws-cdk/cloudformation-include/test/test-templates/invalid/ref-ing-a-non-existent-element.json diff --git a/packages/@aws-cdk/cloudformation-include/lib/cfn-include.ts b/packages/@aws-cdk/cloudformation-include/lib/cfn-include.ts index b7f712c23af56..b5cabdb9d9d01 100644 --- a/packages/@aws-cdk/cloudformation-include/lib/cfn-include.ts +++ b/packages/@aws-cdk/cloudformation-include/lib/cfn-include.ts @@ -128,7 +128,13 @@ export class CfnInclude extends core.CfnElement { } private createParameter(logicalId: string): void { - const expression = cfn_parse.FromCloudFormation.parseValue(this.template.Parameters[logicalId]); + const expression = new cfn_parse.CfnParser({ + finder: { + findResource() { throw new Error('Using GetAtt expressions in Parameter definitions is not allowed'); }, + findRefTarget() { throw new Error('Using Ref expressions in Parameter definitions is not allowed'); }, + findCondition() { throw new Error('Referring to Conditions in Parameter definitions is not allowed'); }, + }, + }).parseValue(this.template.Parameters[logicalId]); const cfnParameter = new core.CfnParameter(this, logicalId, { type: expression.Type, default: expression.Default, @@ -149,7 +155,14 @@ export class CfnInclude extends core.CfnElement { private createCondition(conditionName: string): void { // ToDo condition expressions can refer to other conditions - // will be important when implementing preserveLogicalIds=false - const expression = cfn_parse.FromCloudFormation.parseValue(this.template.Conditions[conditionName]); + const expression = new cfn_parse.CfnParser({ + finder: { + findResource() { throw new Error('Using GetAtt in Condition definitions is not allowed'); }, + findRefTarget() { throw new Error('Using Ref expressions in Condition definitions is not allowed'); }, + // ToDo handle one Condition referencing another using the { Condition: "ConditionName" } syntax + findCondition() { return undefined; }, + }, + }).parseValue(this.template.Conditions[conditionName]); const cfnCondition = new core.CfnCondition(this, conditionName, { expression, }); @@ -198,6 +211,14 @@ export class CfnInclude extends core.CfnElement { } return self.getOrCreateResource(lId); }, + + findRefTarget(elementName: string): core.CfnElement | undefined { + if (elementName in self.parameters) { + return self.parameters[elementName]; + } + + return this.findResource(elementName); + }, }; const options: core.FromCloudFormationOptions = { finder, diff --git a/packages/@aws-cdk/cloudformation-include/test/invalid-templates.test.ts b/packages/@aws-cdk/cloudformation-include/test/invalid-templates.test.ts index f6e1c2100365a..eb04ef059e15e 100644 --- a/packages/@aws-cdk/cloudformation-include/test/invalid-templates.test.ts +++ b/packages/@aws-cdk/cloudformation-include/test/invalid-templates.test.ts @@ -64,6 +64,18 @@ describe('CDK Include', () => { includeTestTemplate(stack, 'non-existent-resource-attribute.json'); }).toThrow(/The NonExistentResourceAttribute resource attribute is not supported by cloudformation-include yet/); }); + + test("throws a validation exception when encountering a Ref-erence to a template element that doesn't exist", () => { + expect(() => { + includeTestTemplate(stack, 'ref-ing-a-non-existent-element.json'); + }).toThrow(/Element used in Ref expression with logical ID: 'DoesNotExist' not found/); + }); + + test("throws a validation exception when encountering a GetAtt reference to a resource that doesn't exist", () => { + expect(() => { + includeTestTemplate(stack, 'getting-attribute-of-a-non-existent-resource.json'); + }).toThrow(/Resource used in GetAtt expression with logical ID: 'DoesNotExist' not found/); + }); }); function includeTestTemplate(scope: core.Construct, testTemplate: string): inc.CfnInclude { diff --git a/packages/@aws-cdk/cloudformation-include/test/test-templates/invalid/bucket-policy-without-bucket.json b/packages/@aws-cdk/cloudformation-include/test/test-templates/invalid/bucket-policy-without-bucket.json index c665e5f2641b7..56ab850b8736d 100644 --- a/packages/@aws-cdk/cloudformation-include/test/test-templates/invalid/bucket-policy-without-bucket.json +++ b/packages/@aws-cdk/cloudformation-include/test/test-templates/invalid/bucket-policy-without-bucket.json @@ -28,6 +28,9 @@ "Version": "2012-10-17" } } + }, + "Bucket2": { + "Type": "AWS::S3::Bucket" } } } diff --git a/packages/@aws-cdk/cloudformation-include/test/test-templates/invalid/getting-attribute-of-a-non-existent-resource.json b/packages/@aws-cdk/cloudformation-include/test/test-templates/invalid/getting-attribute-of-a-non-existent-resource.json new file mode 100644 index 0000000000000..1b575ce7cbeac --- /dev/null +++ b/packages/@aws-cdk/cloudformation-include/test/test-templates/invalid/getting-attribute-of-a-non-existent-resource.json @@ -0,0 +1,12 @@ +{ + "Resources": { + "Bucket": { + "Type": "AWS::S3::Bucket", + "Properties": { + "BucketName": { + "Fn::GetAtt": ["DoesNotExist", "SomeAttribute"] + } + } + } + } +} diff --git a/packages/@aws-cdk/cloudformation-include/test/test-templates/invalid/ref-ing-a-non-existent-element.json b/packages/@aws-cdk/cloudformation-include/test/test-templates/invalid/ref-ing-a-non-existent-element.json new file mode 100644 index 0000000000000..f62b346ed25f3 --- /dev/null +++ b/packages/@aws-cdk/cloudformation-include/test/test-templates/invalid/ref-ing-a-non-existent-element.json @@ -0,0 +1,12 @@ +{ + "Resources": { + "Bucket": { + "Type": "AWS::S3::Bucket", + "Properties": { + "BucketName": { + "Ref": "DoesNotExist" + } + } + } + } +} diff --git a/packages/@aws-cdk/cloudformation-include/test/valid-templates.test.ts b/packages/@aws-cdk/cloudformation-include/test/valid-templates.test.ts index f09331895f079..6994a77f6bff1 100644 --- a/packages/@aws-cdk/cloudformation-include/test/valid-templates.test.ts +++ b/packages/@aws-cdk/cloudformation-include/test/valid-templates.test.ts @@ -328,6 +328,87 @@ describe('CDK Include', () => { ); }); + test("correctly handles referencing the ingested template's resources across Stacks", () => { + // for cross-stack sharing to work, we need an App + const app = new core.App(); + stack = new core.Stack(app, 'MyStack'); + const cfnTemplate = includeTestTemplate(stack, 'only-empty-bucket.json'); + const cfnBucket = cfnTemplate.getResource('Bucket') as s3.CfnBucket; + + const otherStack = new core.Stack(app, 'OtherStack'); + const role = new iam.Role(otherStack, 'Role', { + assumedBy: new iam.AnyPrincipal(), + }); + role.addToPolicy(new iam.PolicyStatement({ + actions: ['s3:*'], + resources: [cfnBucket.attrArn], + })); + + expect(stack).toMatchTemplate({ + ...loadTestFileToJsObject('only-empty-bucket.json'), + "Outputs": { + "ExportsOutputFnGetAttBucketArn436138FE": { + "Value": { + "Fn::GetAtt": ["Bucket", "Arn"], + }, + "Export": { + "Name": "MyStack:ExportsOutputFnGetAttBucketArn436138FE", + }, + }, + }, + }); + + expect(otherStack).toHaveResourceLike('AWS::IAM::Policy', { + "PolicyDocument": { + "Statement": [ + { + "Action": "s3:*", + "Resource": { + "Fn::ImportValue": "MyStack:ExportsOutputFnGetAttBucketArn436138FE", + }, + }, + ], + }, + }); + }); + + test('correctly re-names references to resources in the template if their logical IDs have been changed', () => { + const cfnTemplate = includeTestTemplate(stack, 'bucket-with-encryption-key.json'); + const cfnKey = cfnTemplate.getResource('Key'); + cfnKey.overrideLogicalId('TotallyDifferentKey'); + + const originalTemplate = loadTestFileToJsObject('bucket-with-encryption-key.json'); + expect(stack).toMatchTemplate({ + "Resources": { + "Bucket": { + "Type": "AWS::S3::Bucket", + "Properties": { + "BucketEncryption": { + "ServerSideEncryptionConfiguration": [ + { + "ServerSideEncryptionByDefault": { + "KMSMasterKeyID": { + "Fn::GetAtt": ["TotallyDifferentKey", "Arn"], + }, + "SSEAlgorithm": "aws:kms", + }, + }, + ], + }, + }, + "Metadata" : { + "Object1" : "Location1", + "KeyRef": { "Ref": "TotallyDifferentKey" }, + "KeyArn": { "Fn::GetAtt": ["TotallyDifferentKey", "Arn"] }, + }, + "DeletionPolicy": "Retain", + "UpdateReplacePolicy": "Retain", + }, + "TotallyDifferentKey": originalTemplate.Resources.Key, + }, + }); + }); + test("throws an exception when encountering a Resource type it doesn't recognize", () => { expect(() => { includeTestTemplate(stack, 'non-existent-resource-type.json'); diff --git a/packages/@aws-cdk/core/lib/cfn-parse.ts b/packages/@aws-cdk/core/lib/cfn-parse.ts index e0844da24d22b..5aae33709a406 100644 --- a/packages/@aws-cdk/core/lib/cfn-parse.ts +++ b/packages/@aws-cdk/core/lib/cfn-parse.ts @@ -5,27 +5,25 @@ import { CfnCreationPolicy, CfnDeletionPolicy, CfnResourceAutoScalingCreationPolicy, CfnResourceSignal, CfnUpdatePolicy, } from './cfn-resource-policy'; import { CfnTag } from './cfn-tag'; +import { ICfnFinder } from './from-cfn'; +import { CfnReference } from './private/cfn-reference'; import { IResolvable } from './resolvable'; import { isResolvableObject, Token } from './token'; /** - * This class contains functions for translating from a pure CFN value - * (like a JS object { "Ref": "Bucket" }) - * to a form CDK understands - * (like Fn.ref('Bucket')). + * This class contains static methods called when going from + * translated values received from {@link CfnParser.parseValue} + * to the actual L1 properties - + * things like changing IResolvable to the appropriate type + * (string, string array, or number), etc. * * While this file not exported from the module * (to not make it part of the public API), - * it is directly referenced in the generated L1 code, - * so any renames of it need to be reflected in cfn2ts/codegen.ts as well. + * it is directly referenced in the generated L1 code. * * @experimental */ export class FromCloudFormation { - public static parseValue(cfnValue: any): any { - return parseCfnValueToCdkValue(cfnValue); - } - // nothing to for any but return it public static getAny(value: any) { return value; } @@ -110,11 +108,51 @@ export class FromCloudFormation { return ret; } - public static parseCreationPolicy(policy: any): CfnCreationPolicy | undefined { + public static getCfnTag(tag: any): CfnTag { + return tag == null + ? { } as any // break the type system - this should be detected at runtime by a tag validator + : { + key: tag.Key, + value: tag.Value, + }; + } +} + +/** + * The options for {@link FromCloudFormation.parseValue}. + */ +export interface ParseCfnOptions { + /** + * The finder interface used to resolve references in the template. + */ + readonly finder: ICfnFinder; +} + +/** + * This class contains methods for translating from a pure CFN value + * (like a JS object { "Ref": "Bucket" }) + * to a form CDK understands + * (like Fn.ref('Bucket')). + * + * While this file not exported from the module + * (to not make it part of the public API), + * it is directly referenced in the generated L1 code, + * so any renames of it need to be reflected in cfn2ts/codegen.ts as well. + * + * @experimental + */ +export class CfnParser { + private readonly options: ParseCfnOptions; + + constructor(options: ParseCfnOptions) { + this.options = options; + } + + public parseCreationPolicy(policy: any): CfnCreationPolicy | undefined { if (typeof policy !== 'object') { return undefined; } // change simple JS values to their CDK equivalents - policy = FromCloudFormation.parseValue(policy); + policy = this.parseValue(policy); return undefinedIfAllValuesAreEmpty({ autoScalingCreationPolicy: parseAutoScalingCreationPolicy(policy.AutoScalingCreationPolicy), @@ -139,11 +177,11 @@ export class FromCloudFormation { } } - public static parseUpdatePolicy(policy: any): CfnUpdatePolicy | undefined { + public parseUpdatePolicy(policy: any): CfnUpdatePolicy | undefined { if (typeof policy !== 'object') { return undefined; } // change simple JS values to their CDK equivalents - policy = FromCloudFormation.parseValue(policy); + policy = this.parseValue(policy); return undefinedIfAllValuesAreEmpty({ autoScalingReplacingUpdate: parseAutoScalingReplacingUpdate(policy.AutoScalingReplacingUpdate), @@ -195,7 +233,7 @@ export class FromCloudFormation { } } - public static parseDeletionPolicy(policy: any): CfnDeletionPolicy | undefined { + public parseDeletionPolicy(policy: any): CfnDeletionPolicy | undefined { switch (policy) { case null: return undefined; case undefined: return undefined; @@ -206,125 +244,128 @@ export class FromCloudFormation { } } - public static getCfnTag(tag: any): CfnTag { - return tag == null - ? { } as any // break the type system - this should be detected at runtime by a tag validator - : { - key: tag.Key, - value: tag.Value, - }; - } -} - -function parseCfnValueToCdkValue(cfnValue: any): any { - // == null captures undefined as well - if (cfnValue == null) { - return undefined; - } - // if we have any late-bound values, - // just return them - if (isResolvableObject(cfnValue)) { - return cfnValue; - } - if (Array.isArray(cfnValue)) { - return cfnValue.map(el => parseCfnValueToCdkValue(el)); - } - if (typeof cfnValue === 'object') { - // an object can be either a CFN intrinsic, or an actual object - const cfnIntrinsic = parseIfCfnIntrinsic(cfnValue); - if (cfnIntrinsic) { - return cfnIntrinsic; - } - const ret: any = {}; - for (const [key, val] of Object.entries(cfnValue)) { - ret[key] = parseCfnValueToCdkValue(val); + public parseValue(cfnValue: any): any { + // == null captures undefined as well + if (cfnValue == null) { + return undefined; } - return ret; + // if we have any late-bound values, + // just return them + if (isResolvableObject(cfnValue)) { + return cfnValue; + } + if (Array.isArray(cfnValue)) { + return cfnValue.map(el => this.parseValue(el)); + } + if (typeof cfnValue === 'object') { + // an object can be either a CFN intrinsic, or an actual object + const cfnIntrinsic = this.parseIfCfnIntrinsic(cfnValue); + if (cfnIntrinsic) { + return cfnIntrinsic; + } + const ret: any = {}; + for (const [key, val] of Object.entries(cfnValue)) { + ret[key] = this.parseValue(val); + } + return ret; + } + // in all other cases, just return the input + return cfnValue; } - // in all other cases, just return the input - return cfnValue; -} -function parseIfCfnIntrinsic(object: any): any { - const key = looksLikeCfnIntrinsic(object); - switch (key) { - case undefined: - return undefined; - case 'Ref': { - // ToDo handle translating logical IDs - return specialCaseRefs(object[key]) ?? Fn._ref(object[key]); - } - case 'Fn::GetAtt': { - // Fn::GetAtt takes a 2-element list as its argument - const value = object[key]; - // ToDo same comment here as in Ref above - return Fn.getAtt((value[0]), value[1]); - } - case 'Fn::Join': { - // Fn::Join takes a 2-element list as its argument, - // where the first element is the delimiter, - // and the second is the list of elements to join - const value = parseCfnValueToCdkValue(object[key]); - return Fn.join(value[0], value[1]); - } - case 'Fn::Cidr': { - const value = parseCfnValueToCdkValue(object[key]); - return Fn.cidr(value[0], value[1], value[2]); - } - case 'Fn::FindInMap': { - const value = parseCfnValueToCdkValue(object[key]); - return Fn.findInMap(value[0], value[1], value[2]); - } - case 'Fn::Select': { - const value = parseCfnValueToCdkValue(object[key]); - return Fn.select(value[0], value[1]); - } - case 'Fn::GetAZs': { - const value = parseCfnValueToCdkValue(object[key]); - return Fn.getAzs(value); - } - case 'Fn::ImportValue': { - const value = parseCfnValueToCdkValue(object[key]); - return Fn.importValue(value); - } - case 'Fn::Split': { - const value = parseCfnValueToCdkValue(object[key]); - return Fn.split(value[0], value[1]); - } - case 'Fn::Transform': { - const value = parseCfnValueToCdkValue(object[key]); - return Fn.transform(value.Name, value.Parameters); - } - case 'Fn::Base64': { - const value = parseCfnValueToCdkValue(object[key]); - return Fn.base64(value); - } - case 'Fn::If': { - // Fn::If takes a 3-element list as its argument - // ToDo the first argument is the name of the condition, - // so we will need to retrieve the actual object from the template - // when we handle preserveLogicalIds=false - const value = parseCfnValueToCdkValue(object[key]); - return Fn.conditionIf(value[0], value[1], value[2]); - } - case 'Fn::Equals': { - const value = parseCfnValueToCdkValue(object[key]); - return Fn.conditionEquals(value[0], value[1]); - } - case 'Fn::And': { - const value = parseCfnValueToCdkValue(object[key]); - return Fn.conditionAnd(...value); - } - case 'Fn::Not': { - const value = parseCfnValueToCdkValue(object[key]); - return Fn.conditionNot(value[0]); - } - case 'Fn::Or': { - const value = parseCfnValueToCdkValue(object[key]); - return Fn.conditionOr(...value); + private parseIfCfnIntrinsic(object: any): any { + const key = looksLikeCfnIntrinsic(object); + switch (key) { + case undefined: + return undefined; + case 'Ref': { + const refTarget = object[key]; + const specialRef = specialCaseRefs(refTarget); + if (specialRef) { + return specialRef; + } else { + const refElement = this.options.finder.findRefTarget(refTarget); + if (!refElement) { + throw new Error(`Element used in Ref expression with logical ID: '${refTarget}' not found`); + } + return CfnReference.for(refElement, 'Ref'); + } + } + case 'Fn::GetAtt': { + // Fn::GetAtt takes a 2-element list as its argument + const value = object[key]; + const target = this.options.finder.findResource(value[0]); + if (!target) { + throw new Error(`Resource used in GetAtt expression with logical ID: '${value[0]}' not found`); + } + return target.getAtt(value[1]); + } + case 'Fn::Join': { + // Fn::Join takes a 2-element list as its argument, + // where the first element is the delimiter, + // and the second is the list of elements to join + const value = this.parseValue(object[key]); + return Fn.join(value[0], value[1]); + } + case 'Fn::Cidr': { + const value = this.parseValue(object[key]); + return Fn.cidr(value[0], value[1], value[2]); + } + case 'Fn::FindInMap': { + const value = this.parseValue(object[key]); + return Fn.findInMap(value[0], value[1], value[2]); + } + case 'Fn::Select': { + const value = this.parseValue(object[key]); + return Fn.select(value[0], value[1]); + } + case 'Fn::GetAZs': { + const value = this.parseValue(object[key]); + return Fn.getAzs(value); + } + case 'Fn::ImportValue': { + const value = this.parseValue(object[key]); + return Fn.importValue(value); + } + case 'Fn::Split': { + const value = this.parseValue(object[key]); + return Fn.split(value[0], value[1]); + } + case 'Fn::Transform': { + const value = this.parseValue(object[key]); + return Fn.transform(value.Name, value.Parameters); + } + case 'Fn::Base64': { + const value = this.parseValue(object[key]); + return Fn.base64(value); + } + case 'Fn::If': { + // Fn::If takes a 3-element list as its argument + // ToDo the first argument is the name of the condition, + // so we will need to retrieve the actual object from the template + // when we handle preserveLogicalIds=false + const value = this.parseValue(object[key]); + return Fn.conditionIf(value[0], value[1], value[2]); + } + case 'Fn::Equals': { + const value = this.parseValue(object[key]); + return Fn.conditionEquals(value[0], value[1]); + } + case 'Fn::And': { + const value = this.parseValue(object[key]); + return Fn.conditionAnd(...value); + } + case 'Fn::Not': { + const value = this.parseValue(object[key]); + return Fn.conditionNot(value[0]); + } + case 'Fn::Or': { + const value = this.parseValue(object[key]); + return Fn.conditionOr(...value); + } + default: + throw new Error(`Unsupported CloudFormation function '${key}'`); } - default: - throw new Error(`Unsupported CloudFormation function '${key}'`); } } diff --git a/packages/@aws-cdk/core/lib/from-cfn.ts b/packages/@aws-cdk/core/lib/from-cfn.ts index 9d3b1544526a2..771af7775a74b 100644 --- a/packages/@aws-cdk/core/lib/from-cfn.ts +++ b/packages/@aws-cdk/core/lib/from-cfn.ts @@ -1,4 +1,5 @@ import { CfnCondition } from './cfn-condition'; +import { CfnElement } from './cfn-element'; import { CfnResource } from './cfn-resource'; /** @@ -15,6 +16,13 @@ export interface ICfnFinder { */ findCondition(conditionName: string): CfnCondition | undefined; + /** + * Returns the element referenced using a Ref expression with the given name. + * If there is no element with this name in the template, + * return undefined. + */ + findRefTarget(elementName: string): CfnElement | undefined; + /** * Returns the resource with the given logical ID in the template. * If a resource with that logical ID was not found in the template, diff --git a/packages/@aws-cdk/core/test/test.cfn-parse.ts b/packages/@aws-cdk/core/test/test.cfn-parse.ts index 47f6bdbb947cc..7000af65e231d 100644 --- a/packages/@aws-cdk/core/test/test.cfn-parse.ts +++ b/packages/@aws-cdk/core/test/test.cfn-parse.ts @@ -1,23 +1,23 @@ import { Test } from 'nodeunit'; -import { FromCloudFormation } from '../lib/cfn-parse'; +import { CfnParser } from '../lib/cfn-parse'; export = { 'FromCloudFormation class': { '#parseCreationPolicy': { 'returns undefined when given a non-object as the argument'(test: Test) { - test.equal(FromCloudFormation.parseCreationPolicy('blah'), undefined); + test.equal(parseCreationPolicy('blah'), undefined); test.done(); }, 'returns undefined when given an empty object as the argument'(test: Test) { - test.equal(FromCloudFormation.parseCreationPolicy({}), undefined); + test.equal(parseCreationPolicy({}), undefined); test.done(); }, 'returns undefined when given empty sub-objects as the argument'(test: Test) { - test.equal(FromCloudFormation.parseCreationPolicy({ + test.equal(parseCreationPolicy({ AutoScalingCreationPolicy: null, ResourceSignal: { Count: undefined, @@ -30,19 +30,19 @@ export = { '#parseUpdatePolicy': { 'returns undefined when given a non-object as the argument'(test: Test) { - test.equal(FromCloudFormation.parseUpdatePolicy('blah'), undefined); + test.equal(parseUpdatePolicy('blah'), undefined); test.done(); }, 'returns undefined when given an empty object as the argument'(test: Test) { - test.equal(FromCloudFormation.parseUpdatePolicy({}), undefined); + test.equal(parseUpdatePolicy({}), undefined); test.done(); }, 'returns undefined when given empty sub-objects as the argument'(test: Test) { - test.equal(FromCloudFormation.parseUpdatePolicy({ + test.equal(parseUpdatePolicy({ AutoScalingReplacingUpdate: null, AutoScalingRollingUpdate: { PauseTime: undefined, @@ -54,3 +54,19 @@ export = { }, }, }; + +function parseCreationPolicy(policy: any) { + return testCfnParser.parseCreationPolicy(policy); +} + +function parseUpdatePolicy(policy: any) { + return testCfnParser.parseUpdatePolicy(policy); +} + +const testCfnParser = new CfnParser({ + finder: { + findCondition() { return undefined; }, + findRefTarget() { return undefined; }, + findResource() { return undefined; }, + }, +}); diff --git a/tools/cfn2ts/lib/codegen.ts b/tools/cfn2ts/lib/codegen.ts index 6bba39e50abd5..ff87f6a178222 100644 --- a/tools/cfn2ts/lib/codegen.ts +++ b/tools/cfn2ts/lib/codegen.ts @@ -235,9 +235,12 @@ export default class CodeGenerator { this.code.openBlock(`public static fromCloudFormation(scope: ${CONSTRUCT_CLASS}, id: string, resourceAttributes: any, options: ${CORE}.FromCloudFormationOptions): ` + `${resourceName.className}`); this.code.line('resourceAttributes = resourceAttributes || {};'); + this.code.indent('const cfnParser = new cfn_parse.CfnParser({'); + this.code.line('finder: options.finder,'); + this.code.unindent('});'); if (propsType) { // translate the template properties to CDK objects - this.code.line(`const resourceProperties = ${CFN_PARSE}.FromCloudFormation.parseValue(resourceAttributes.Properties);`); + this.code.line('const resourceProperties = cfnParser.parseValue(resourceAttributes.Properties);'); // translate to props, using a (module-private) factory function this.code.line(`const props = ${genspec.fromCfnFactoryName(propsType).fqn}(resourceProperties);`); // finally, instantiate the resource class @@ -249,11 +252,11 @@ export default class CodeGenerator { // handle all non-property attributes // (retention policies, conditions, metadata, etc.) this.code.line('const cfnOptions = ret.cfnOptions;'); - this.code.line(`cfnOptions.creationPolicy = ${CFN_PARSE}.FromCloudFormation.parseCreationPolicy(resourceAttributes.CreationPolicy);`); - this.code.line(`cfnOptions.updatePolicy = ${CFN_PARSE}.FromCloudFormation.parseUpdatePolicy(resourceAttributes.UpdatePolicy);`); - this.code.line(`cfnOptions.deletionPolicy = ${CFN_PARSE}.FromCloudFormation.parseDeletionPolicy(resourceAttributes.DeletionPolicy);`); - this.code.line(`cfnOptions.updateReplacePolicy = ${CFN_PARSE}.FromCloudFormation.parseDeletionPolicy(resourceAttributes.UpdateReplacePolicy);`); - this.code.line(`cfnOptions.metadata = ${CFN_PARSE}.FromCloudFormation.parseValue(resourceAttributes.Metadata);`); + this.code.line('cfnOptions.creationPolicy = cfnParser.parseCreationPolicy(resourceAttributes.CreationPolicy);'); + this.code.line('cfnOptions.updatePolicy = cfnParser.parseUpdatePolicy(resourceAttributes.UpdatePolicy);'); + this.code.line('cfnOptions.deletionPolicy = cfnParser.parseDeletionPolicy(resourceAttributes.DeletionPolicy);'); + this.code.line('cfnOptions.updateReplacePolicy = cfnParser.parseDeletionPolicy(resourceAttributes.UpdateReplacePolicy);'); + this.code.line('cfnOptions.metadata = cfnParser.parseValue(resourceAttributes.Metadata);'); // handle DependsOn this.code.line('// handle DependsOn'); @@ -278,10 +281,6 @@ export default class CodeGenerator { this.code.line('cfnOptions.condition = condition;'); this.code.closeBlock(); - // ToDo handle: - // 1. CreationPolicy - // 2. UpdatePolicy - this.code.line('return ret;'); this.code.closeBlock(); From e512a4057b21d32432d4dc7ac14ae7caa812265d Mon Sep 17 00:00:00 2001 From: Romain Marcadier Date: Wed, 24 Jun 2020 15:05:16 +0200 Subject: [PATCH 20/21] fix(toolkit): CLI tool fails on CloudFormation Throttling (#8711) The CDK (particularly, `cdk deploy`) might crash after getting throttled by CloudFormation, after the default configured 6 retries has been reached. This changes the retry configuration of the CloudFormation client (and only that one) to allow up to 10 retries with a backoff base of 1 second. This makes the maximum back-off about 17 minutes, which I hope would be plenty enough even for the 1 TPM calls. This should allow heavily parallel deployments on the same account and region to avoid getting killed by a throttle; but will reduce the responsiveness of the progress UI. Additionaly, configured a custom logger for the SDK, which would log the SDK calls to the console when running in debug mode, allowing the users to gain visibility on more information for troubleshooting purposes. Fixes #5637 --- packages/aws-cdk/lib/api/aws-auth/sdk.ts | 25 ++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/packages/aws-cdk/lib/api/aws-auth/sdk.ts b/packages/aws-cdk/lib/api/aws-auth/sdk.ts index 239f85fef51bc..871b36c6002d3 100644 --- a/packages/aws-cdk/lib/api/aws-auth/sdk.ts +++ b/packages/aws-cdk/lib/api/aws-auth/sdk.ts @@ -42,16 +42,17 @@ export class SDK implements ISDK { private readonly config: ConfigurationOptions; /** - * Default retry options for SDK clients - * - * Biggest bottleneck is CloudFormation, with a 1tps call rate. We want to be - * a little more tenacious than the defaults, and with a little more breathing - * room between calls (defaults are {retries=3, base=100}). + * Default retry options for SDK clients. + */ + private readonly retryOptions = { maxRetries: 6, retryDelayOptions: { base: 300 } }; + + /** + * The more generous retry policy for CloudFormation, which has a 1 TPM limit on certain APIs, + * which are abundantly used for deployment tracking, ... * - * I've left this running in a tight loop for an hour and the throttle errors - * haven't escaped the retry mechanism. + * So we're allowing way more retries, but waiting a bit more. */ - private readonly retryOptions = { maxRetries: 6, retryDelayOptions: { base: 300 }}; + private readonly cloudFormationRetryOptions = { maxRetries: 10, retryDelayOptions: { base: 1_000 } }; constructor(private readonly credentials: AWS.Credentials, region: string, httpOptions: ConfigurationOptions = {}) { this.config = { @@ -59,12 +60,16 @@ export class SDK implements ISDK { ...this.retryOptions, credentials, region, + logger: { log: (...messages) => messages.forEach(m => debug('%s', m)) }, }; this.currentRegion = region; } public cloudFormation(): AWS.CloudFormation { - return wrapServiceErrorHandling(new AWS.CloudFormation(this.config)); + return wrapServiceErrorHandling(new AWS.CloudFormation({ + ...this.config, + ...this.cloudFormationRetryOptions, + })); } public ec2(): AWS.EC2 { @@ -212,4 +217,4 @@ function allChainedExceptionMessages(e: Error | undefined) { e = (e as any).originalError; } return ret.join(': '); -} \ No newline at end of file +} From e64fd55e8fe2dab4e866bbd5405e8dc4da942e46 Mon Sep 17 00:00:00 2001 From: AWS CDK Team Date: Wed, 24 Jun 2020 17:38:39 +0000 Subject: [PATCH 21/21] chore(release): 1.47.0 --- CHANGELOG.md | 24 ++++++++++++++++++++++++ lerna.json | 2 +- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7862a9041888f..28e25c02d359f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,30 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [1.47.0](https://github.com/aws/aws-cdk/compare/v1.46.0...v1.47.0) (2020-06-24) + + +### ⚠ BREAKING CHANGES + +* **stepfunctions-tasks:** `Dynamo*` tasks no longer implement`IStepFunctionsTask` and have been replaced by constructs that can be instantiated directly. See README for examples + +### Features + +* **cfn-include:** add support for retrieving parameter objects ([#8658](https://github.com/aws/aws-cdk/issues/8658)) ([52dc123](https://github.com/aws/aws-cdk/commit/52dc123ba8696abcfad99d8093e98cd39b5b104f)), closes [#8657](https://github.com/aws/aws-cdk/issues/8657) +* **cfn-include:** support logical id overrides ([#8529](https://github.com/aws/aws-cdk/issues/8529)) ([d9c4f5e](https://github.com/aws/aws-cdk/commit/d9c4f5e67c54e1a2a436978fbc28fffd92b24cd6)), closes [#7375](https://github.com/aws/aws-cdk/issues/7375) +* **cloudwatch:** CompositeAlarm ([#8498](https://github.com/aws/aws-cdk/issues/8498)) ([1e6d293](https://github.com/aws/aws-cdk/commit/1e6d293f4c445318b11bd6fe998325688a675807)) +* **efs:** access point ([#8631](https://github.com/aws/aws-cdk/issues/8631)) ([dde0ef5](https://github.com/aws/aws-cdk/commit/dde0ef52cc0cdbc40fd212f518f3cee4f30450b9)) +* **stepfunctions:** grant APIs for state machine construct ([#8486](https://github.com/aws/aws-cdk/issues/8486)) ([fe71364](https://github.com/aws/aws-cdk/commit/fe71364b6cd8274e937cc2dc9185249dcbbb9388)), closes [#5933](https://github.com/aws/aws-cdk/issues/5933) +* **stepfunctions-tasks:** task constructs to call DynamoDB APIs ([#8466](https://github.com/aws/aws-cdk/issues/8466)) ([a7cb3b7](https://github.com/aws/aws-cdk/commit/a7cb3b7633c433ecb0619c030914bfa497ee39bc)), closes [#8108](https://github.com/aws/aws-cdk/issues/8108) + + +### Bug Fixes + +* **appsync:** Not to throw an Error even if 'additionalAuthorizationModes' is undefined ([#8673](https://github.com/aws/aws-cdk/issues/8673)) ([6b5d77b](https://github.com/aws/aws-cdk/commit/6b5d77b452bccb35564d6acee118112156149eb0)), closes [#8666](https://github.com/aws/aws-cdk/issues/8666) [#8668](https://github.com/aws/aws-cdk/issues/8668) +* **cli:** cannot change policies or trust after initial bootstrap ([#8677](https://github.com/aws/aws-cdk/issues/8677)) ([6e6b23e](https://github.com/aws/aws-cdk/commit/6e6b23e329d8a1b6455210768371a5ab9de478ef)), closes [#6581](https://github.com/aws/aws-cdk/issues/6581) +* **cli:** crash on tiny reported terminal width ([#8675](https://github.com/aws/aws-cdk/issues/8675)) ([a186c24](https://github.com/aws/aws-cdk/commit/a186c24918fddc697270b794b6603add5a47e947)), closes [#8667](https://github.com/aws/aws-cdk/issues/8667) +* **toolkit:** CLI tool fails on CloudFormation Throttling ([#8711](https://github.com/aws/aws-cdk/issues/8711)) ([e512a40](https://github.com/aws/aws-cdk/commit/e512a4057b21d32432d4dc7ac14ae7caa812265d)), closes [#5637](https://github.com/aws/aws-cdk/issues/5637) + ## [1.46.0](https://github.com/aws/aws-cdk/compare/v1.45.0...v1.46.0) (2020-06-19) diff --git a/lerna.json b/lerna.json index cc8251a51f0a9..f416a842add0e 100644 --- a/lerna.json +++ b/lerna.json @@ -10,5 +10,5 @@ "tools/*" ], "rejectCycles": "true", - "version": "1.46.0" + "version": "1.47.0" }