From 82e4b4f07be202e2d6c6afa4f9ed0d9d6146f0a8 Mon Sep 17 00:00:00 2001 From: Ben Chaimberg Date: Mon, 23 Aug 2021 06:26:05 -0700 Subject: [PATCH 01/39] fix(cfnspec): changes to resource-level documentation not supported (#16170) Internal reference: V417958316 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/cfnspec/build-tools/spec-diff.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/@aws-cdk/cfnspec/build-tools/spec-diff.ts b/packages/@aws-cdk/cfnspec/build-tools/spec-diff.ts index f4aa0ee7daab3..6b5a0d6d466f9 100644 --- a/packages/@aws-cdk/cfnspec/build-tools/spec-diff.ts +++ b/packages/@aws-cdk/cfnspec/build-tools/spec-diff.ts @@ -106,6 +106,11 @@ async function main() { }); } break; + case 'Documentation': + describeChanges(resourceType, key, update.Documentation).forEach(change => { + attributeChanges.push(change); + }); + break; default: throw new Error(`Unexpected update to ${resourceType}: ${key}`); } From 677fedcc5351b8b5346970fac03e5e342f36265b Mon Sep 17 00:00:00 2001 From: Otavio Macedo Date: Mon, 23 Aug 2021 15:09:12 +0100 Subject: [PATCH 02/39] fix(ses): drop spam rule appears in the incorrect order (#16146) The auto-generated drop spam rule should come before all user-defined rules. If an email is classified as spam, it should stop the processing of all other rules. Fixes #16091 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../@aws-cdk/aws-ses/lib/receipt-rule-set.ts | 6 ++-- .../aws-ses/test/test.receipt-rule-set.ts | 35 +++++++++++++++++++ 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/packages/@aws-cdk/aws-ses/lib/receipt-rule-set.ts b/packages/@aws-cdk/aws-ses/lib/receipt-rule-set.ts index dcb3d011d4251..d8590216f2460 100644 --- a/packages/@aws-cdk/aws-ses/lib/receipt-rule-set.ts +++ b/packages/@aws-cdk/aws-ses/lib/receipt-rule-set.ts @@ -109,12 +109,12 @@ export class ReceiptRuleSet extends ReceiptRuleSetBase { this.receiptRuleSetName = resource.ref; if (props) { - const rules = props.rules || []; - rules.forEach((ruleOption, idx) => this.addRule(`Rule${idx}`, ruleOption)); - if (props.dropSpam) { this.addDropSpamRule(); } + + const rules = props.rules || []; + rules.forEach((ruleOption, idx) => this.addRule(`Rule${idx}`, ruleOption)); } } } diff --git a/packages/@aws-cdk/aws-ses/test/test.receipt-rule-set.ts b/packages/@aws-cdk/aws-ses/test/test.receipt-rule-set.ts index cf4b44f422371..458dacfcf311f 100644 --- a/packages/@aws-cdk/aws-ses/test/test.receipt-rule-set.ts +++ b/packages/@aws-cdk/aws-ses/test/test.receipt-rule-set.ts @@ -58,6 +58,41 @@ export = { test.done(); }, + 'drop spam rule should always appear first'(test: Test) { + // GIVEN + const stack = new Stack(); + + // WHEN + new ReceiptRuleSet(stack, 'RuleSet', { + dropSpam: true, + rules: [ + { + scanEnabled: true, + recipients: ['foo@example.com'], + }, + ], + }); + + // THEN + expect(stack).to(haveResource('AWS::SES::ReceiptRule', { + Rule: { + Enabled: true, + Recipients: [ + 'foo@example.com', + ], + ScanEnabled: true, + }, + // All "regular" rules should come after the drop spam rule + After: { + Ref: 'RuleSetDropSpamRule5809F51B', + }, + })); + + expect(stack).to(haveResource('AWS::Lambda::Function')); + + test.done(); + }, + 'import receipt rule set'(test: Test) { // GIVEN const stack = new Stack(); From 52f0576a072ac76bd751d766a0879edb1da87633 Mon Sep 17 00:00:00 2001 From: Nick Lynch Date: Mon, 23 Aug 2021 15:49:22 +0100 Subject: [PATCH 03/39] docs(cloudfront): fix CacheHeaderBehavior.behavior docstring (#16067) related #16002 ---- *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-cloudfront/lib/cache-policy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-cloudfront/lib/cache-policy.ts b/packages/@aws-cdk/aws-cloudfront/lib/cache-policy.ts index 0d851e8a886ef..d0dad353a8727 100644 --- a/packages/@aws-cdk/aws-cloudfront/lib/cache-policy.ts +++ b/packages/@aws-cdk/aws-cloudfront/lib/cache-policy.ts @@ -234,7 +234,7 @@ export class CacheHeaderBehavior { return new CacheHeaderBehavior('whitelist', headers); } - /** If the no headers will be passed, or an allow list of headers. */ + /** If no headers will be passed, or an allow list of headers. */ public readonly behavior: string; /** The headers for the allow/deny list, if applicable. */ public readonly headers?: string[]; From b059124b238e27751217cbdaaa01c38b00e80fc9 Mon Sep 17 00:00:00 2001 From: AWS CDK Automation <43080478+aws-cdk-automation@users.noreply.github.com> Date: Mon, 23 Aug 2021 19:19:54 +0300 Subject: [PATCH 04/39] feat(cfnspec): cloudformation spec v40.0.0 (#16183) Co-authored-by: AWS CDK Team --- packages/@aws-cdk/cfnspec/CHANGELOG.md | 83 +++++ packages/@aws-cdk/cfnspec/cfn.version | 2 +- ...0_CloudFormationResourceSpecification.json | 348 +++++++++++++++++- 3 files changed, 419 insertions(+), 14 deletions(-) diff --git a/packages/@aws-cdk/cfnspec/CHANGELOG.md b/packages/@aws-cdk/cfnspec/CHANGELOG.md index 4f7501decae02..85274a4543fbf 100644 --- a/packages/@aws-cdk/cfnspec/CHANGELOG.md +++ b/packages/@aws-cdk/cfnspec/CHANGELOG.md @@ -1,3 +1,86 @@ +# CloudFormation Resource Specification v40.0.0 + +## New Resource Types + + +## Attribute Changes + +* AWS::EC2::DHCPOptions Documentation (__changed__) + * Old: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-dhcp-options.html + * New: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-dhcpoptions.html +* AWS::EC2::DHCPOptions DhcpOptionsId (__added__) + +## Property Changes + +* AWS::AutoScaling::ScalingPolicy PredictiveScalingConfiguration (__added__) +* AWS::EC2::DHCPOptions DomainName.Documentation (__changed__) + * Old: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-dhcp-options.html#cfn-ec2-dhcpoptions-domainname + * New: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-dhcpoptions.html#cfn-ec2-dhcpoptions-domainname +* AWS::EC2::DHCPOptions DomainNameServers.Documentation (__changed__) + * Old: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-dhcp-options.html#cfn-ec2-dhcpoptions-domainnameservers + * New: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-dhcpoptions.html#cfn-ec2-dhcpoptions-domainnameservers +* AWS::EC2::DHCPOptions NetbiosNameServers.Documentation (__changed__) + * Old: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-dhcp-options.html#cfn-ec2-dhcpoptions-netbiosnameservers + * New: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-dhcpoptions.html#cfn-ec2-dhcpoptions-netbiosnameservers +* AWS::EC2::DHCPOptions NetbiosNodeType.Documentation (__changed__) + * Old: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-dhcp-options.html#cfn-ec2-dhcpoptions-netbiosnodetype + * New: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-dhcpoptions.html#cfn-ec2-dhcpoptions-netbiosnodetype +* AWS::EC2::DHCPOptions NtpServers.Documentation (__changed__) + * Old: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-dhcp-options.html#cfn-ec2-dhcpoptions-ntpservers + * New: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-dhcpoptions.html#cfn-ec2-dhcpoptions-ntpservers +* AWS::EC2::DHCPOptions Tags.Documentation (__changed__) + * Old: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-dhcp-options.html#cfn-ec2-dhcpoptions-tags + * New: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-dhcpoptions.html#cfn-ec2-dhcpoptions-tags +* AWS::Redshift::Cluster AquaConfigurationStatus (__added__) +* AWS::Redshift::Cluster AvailabilityZoneRelocation (__added__) +* AWS::Redshift::Cluster AvailabilityZoneRelocationStatus (__added__) +* AWS::Redshift::Cluster Classic (__added__) +* AWS::Redshift::Cluster DeferMaintenance (__added__) +* AWS::Redshift::Cluster DeferMaintenanceDuration (__added__) +* AWS::Redshift::Cluster DeferMaintenanceEndTime (__added__) +* AWS::Redshift::Cluster DeferMaintenanceIdentifier (__added__) +* AWS::Redshift::Cluster DeferMaintenanceStartTime (__added__) +* AWS::Redshift::Cluster DestinationRegion (__added__) +* AWS::Redshift::Cluster EnhancedVpcRouting (__added__) +* AWS::Redshift::Cluster MaintenanceTrackName (__added__) +* AWS::Redshift::Cluster ManualSnapshotRetentionPeriod (__added__) +* AWS::Redshift::Cluster ResourceAction (__added__) +* AWS::Redshift::Cluster RevisionTarget (__added__) +* AWS::Redshift::Cluster RotateEncryptionKey (__added__) +* AWS::Redshift::Cluster SnapshotCopyGrantName (__added__) +* AWS::Redshift::Cluster SnapshotCopyManual (__added__) +* AWS::Redshift::Cluster SnapshotCopyRetentionPeriod (__added__) +* AWS::Redshift::Cluster AvailabilityZone.UpdateType (__changed__) + * Old: Immutable + * New: Mutable +* AWS::Redshift::Cluster ElasticIp.UpdateType (__changed__) + * Old: Immutable + * New: Mutable +* AWS::Redshift::Cluster Encrypted.UpdateType (__changed__) + * Old: Immutable + * New: Mutable +* AWS::Redshift::Cluster KmsKeyId.UpdateType (__changed__) + * Old: Immutable + * New: Mutable +* AWS::Redshift::Cluster Port.UpdateType (__changed__) + * Old: Immutable + * New: Mutable +* AWS::SageMaker::EndpointConfig AsyncInferenceConfig (__added__) + +## Property Type Changes + +* AWS::AutoScaling::ScalingPolicy.PredictiveScalingConfiguration (__added__) +* AWS::AutoScaling::ScalingPolicy.PredictiveScalingMetricSpecification (__added__) +* AWS::AutoScaling::ScalingPolicy.PredictiveScalingPredefinedLoadMetric (__added__) +* AWS::AutoScaling::ScalingPolicy.PredictiveScalingPredefinedMetricPair (__added__) +* AWS::AutoScaling::ScalingPolicy.PredictiveScalingPredefinedScalingMetric (__added__) +* AWS::SageMaker::EndpointConfig.AsyncInferenceClientConfig (__added__) +* AWS::SageMaker::EndpointConfig.AsyncInferenceConfig (__added__) +* AWS::SageMaker::EndpointConfig.AsyncInferenceNotificationConfig (__added__) +* AWS::SageMaker::EndpointConfig.AsyncInferenceOutputConfig (__added__) +* AWS::WAFv2::WebACL.ManagedRuleGroupStatement Version (__added__) + + # CloudFormation Resource Specification v39.10.0 ## New Resource Types diff --git a/packages/@aws-cdk/cfnspec/cfn.version b/packages/@aws-cdk/cfnspec/cfn.version index 3520fac4397ab..e9340fd6f7115 100644 --- a/packages/@aws-cdk/cfnspec/cfn.version +++ b/packages/@aws-cdk/cfnspec/cfn.version @@ -1 +1 @@ -39.10.0 +40.0.0 diff --git a/packages/@aws-cdk/cfnspec/spec-source/000_CloudFormationResourceSpecification.json b/packages/@aws-cdk/cfnspec/spec-source/000_CloudFormationResourceSpecification.json index ebcff7129953c..1bb4dde7c70f4 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/000_CloudFormationResourceSpecification.json +++ b/packages/@aws-cdk/cfnspec/spec-source/000_CloudFormationResourceSpecification.json @@ -8641,6 +8641,123 @@ } } }, + "AWS::AutoScaling::ScalingPolicy.PredictiveScalingConfiguration": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-autoscaling-scalingpolicy-predictivescalingconfiguration.html", + "Properties": { + "MaxCapacityBreachBehavior": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-autoscaling-scalingpolicy-predictivescalingconfiguration.html#cfn-autoscaling-scalingpolicy-predictivescalingconfiguration-maxcapacitybreachbehavior", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "MaxCapacityBuffer": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-autoscaling-scalingpolicy-predictivescalingconfiguration.html#cfn-autoscaling-scalingpolicy-predictivescalingconfiguration-maxcapacitybuffer", + "PrimitiveType": "Integer", + "Required": false, + "UpdateType": "Mutable" + }, + "MetricSpecifications": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-autoscaling-scalingpolicy-predictivescalingconfiguration.html#cfn-autoscaling-scalingpolicy-predictivescalingconfiguration-metricspecifications", + "DuplicatesAllowed": false, + "ItemType": "PredictiveScalingMetricSpecification", + "Required": true, + "Type": "List", + "UpdateType": "Mutable" + }, + "Mode": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-autoscaling-scalingpolicy-predictivescalingconfiguration.html#cfn-autoscaling-scalingpolicy-predictivescalingconfiguration-mode", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "SchedulingBufferTime": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-autoscaling-scalingpolicy-predictivescalingconfiguration.html#cfn-autoscaling-scalingpolicy-predictivescalingconfiguration-schedulingbuffertime", + "PrimitiveType": "Integer", + "Required": false, + "UpdateType": "Mutable" + } + } + }, + "AWS::AutoScaling::ScalingPolicy.PredictiveScalingMetricSpecification": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-autoscaling-scalingpolicy-predictivescalingmetricspecification.html", + "Properties": { + "PredefinedLoadMetricSpecification": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-autoscaling-scalingpolicy-predictivescalingmetricspecification.html#cfn-autoscaling-scalingpolicy-predictivescalingmetricspecification-predefinedloadmetricspecification", + "Required": false, + "Type": "PredictiveScalingPredefinedLoadMetric", + "UpdateType": "Mutable" + }, + "PredefinedMetricPairSpecification": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-autoscaling-scalingpolicy-predictivescalingmetricspecification.html#cfn-autoscaling-scalingpolicy-predictivescalingmetricspecification-predefinedmetricpairspecification", + "Required": false, + "Type": "PredictiveScalingPredefinedMetricPair", + "UpdateType": "Mutable" + }, + "PredefinedScalingMetricSpecification": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-autoscaling-scalingpolicy-predictivescalingmetricspecification.html#cfn-autoscaling-scalingpolicy-predictivescalingmetricspecification-predefinedscalingmetricspecification", + "Required": false, + "Type": "PredictiveScalingPredefinedScalingMetric", + "UpdateType": "Mutable" + }, + "TargetValue": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-autoscaling-scalingpolicy-predictivescalingmetricspecification.html#cfn-autoscaling-scalingpolicy-predictivescalingmetricspecification-targetvalue", + "PrimitiveType": "Double", + "Required": true, + "UpdateType": "Mutable" + } + } + }, + "AWS::AutoScaling::ScalingPolicy.PredictiveScalingPredefinedLoadMetric": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-autoscaling-scalingpolicy-predictivescalingpredefinedloadmetric.html", + "Properties": { + "PredefinedMetricType": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-autoscaling-scalingpolicy-predictivescalingpredefinedloadmetric.html#cfn-autoscaling-scalingpolicy-predictivescalingpredefinedloadmetric-predefinedmetrictype", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + }, + "ResourceLabel": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-autoscaling-scalingpolicy-predictivescalingpredefinedloadmetric.html#cfn-autoscaling-scalingpolicy-predictivescalingpredefinedloadmetric-resourcelabel", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + } + } + }, + "AWS::AutoScaling::ScalingPolicy.PredictiveScalingPredefinedMetricPair": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-autoscaling-scalingpolicy-predictivescalingpredefinedmetricpair.html", + "Properties": { + "PredefinedMetricType": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-autoscaling-scalingpolicy-predictivescalingpredefinedmetricpair.html#cfn-autoscaling-scalingpolicy-predictivescalingpredefinedmetricpair-predefinedmetrictype", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + }, + "ResourceLabel": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-autoscaling-scalingpolicy-predictivescalingpredefinedmetricpair.html#cfn-autoscaling-scalingpolicy-predictivescalingpredefinedmetricpair-resourcelabel", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + } + } + }, + "AWS::AutoScaling::ScalingPolicy.PredictiveScalingPredefinedScalingMetric": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-autoscaling-scalingpolicy-predictivescalingpredefinedscalingmetric.html", + "Properties": { + "PredefinedMetricType": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-autoscaling-scalingpolicy-predictivescalingpredefinedscalingmetric.html#cfn-autoscaling-scalingpolicy-predictivescalingpredefinedscalingmetric-predefinedmetrictype", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + }, + "ResourceLabel": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-autoscaling-scalingpolicy-predictivescalingpredefinedscalingmetric.html#cfn-autoscaling-scalingpolicy-predictivescalingpredefinedscalingmetric-resourcelabel", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + } + } + }, "AWS::AutoScaling::ScalingPolicy.StepAdjustment": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-autoscaling-scalingpolicy-stepadjustments.html", "Properties": { @@ -56904,6 +57021,74 @@ } } }, + "AWS::SageMaker::EndpointConfig.AsyncInferenceClientConfig": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-endpointconfig-asyncinferenceclientconfig.html", + "Properties": { + "MaxConcurrentInvocationsPerInstance": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-endpointconfig-asyncinferenceclientconfig.html#cfn-sagemaker-endpointconfig-asyncinferenceclientconfig-maxconcurrentinvocationsperinstance", + "PrimitiveType": "Integer", + "Required": false, + "UpdateType": "Immutable" + } + } + }, + "AWS::SageMaker::EndpointConfig.AsyncInferenceConfig": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-endpointconfig-asyncinferenceconfig.html", + "Properties": { + "ClientConfig": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-endpointconfig-asyncinferenceconfig.html#cfn-sagemaker-endpointconfig-asyncinferenceconfig-clientconfig", + "Required": false, + "Type": "AsyncInferenceClientConfig", + "UpdateType": "Immutable" + }, + "OutputConfig": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-endpointconfig-asyncinferenceconfig.html#cfn-sagemaker-endpointconfig-asyncinferenceconfig-outputconfig", + "Required": true, + "Type": "AsyncInferenceOutputConfig", + "UpdateType": "Immutable" + } + } + }, + "AWS::SageMaker::EndpointConfig.AsyncInferenceNotificationConfig": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-endpointconfig-asyncinferencenotificationconfig.html", + "Properties": { + "ErrorTopic": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-endpointconfig-asyncinferencenotificationconfig.html#cfn-sagemaker-endpointconfig-asyncinferencenotificationconfig-errortopic", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, + "SuccessTopic": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-endpointconfig-asyncinferencenotificationconfig.html#cfn-sagemaker-endpointconfig-asyncinferencenotificationconfig-successtopic", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + } + } + }, + "AWS::SageMaker::EndpointConfig.AsyncInferenceOutputConfig": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-endpointconfig-asyncinferenceoutputconfig.html", + "Properties": { + "KmsKeyId": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-endpointconfig-asyncinferenceoutputconfig.html#cfn-sagemaker-endpointconfig-asyncinferenceoutputconfig-kmskeyid", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, + "NotificationConfig": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-endpointconfig-asyncinferenceoutputconfig.html#cfn-sagemaker-endpointconfig-asyncinferenceoutputconfig-notificationconfig", + "Required": false, + "Type": "AsyncInferenceNotificationConfig", + "UpdateType": "Immutable" + }, + "S3OutputPath": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-endpointconfig-asyncinferenceoutputconfig.html#cfn-sagemaker-endpointconfig-asyncinferenceoutputconfig-s3outputpath", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + } + } + }, "AWS::SageMaker::EndpointConfig.CaptureContentTypeHeader": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-endpointconfig-datacaptureconfig-capturecontenttypeheader.html", "Properties": { @@ -60972,6 +61157,12 @@ "PrimitiveType": "String", "Required": true, "UpdateType": "Mutable" + }, + "Version": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-webacl-managedrulegroupstatement.html#cfn-wafv2-webacl-managedrulegroupstatement-version", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" } } }, @@ -61705,7 +61896,7 @@ } } }, - "ResourceSpecificationVersion": "39.10.0", + "ResourceSpecificationVersion": "40.0.0", "ResourceTypes": { "AWS::ACMPCA::Certificate": { "Attributes": { @@ -66484,6 +66675,12 @@ "Required": false, "UpdateType": "Mutable" }, + "PredictiveScalingConfiguration": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-as-policy.html#cfn-autoscaling-scalingpolicy-predictivescalingconfiguration", + "Required": false, + "Type": "PredictiveScalingConfiguration", + "UpdateType": "Mutable" + }, "ScalingAdjustment": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-as-policy.html#cfn-as-scalingpolicy-scalingadjustment", "PrimitiveType": "Integer", @@ -73299,16 +73496,21 @@ } }, "AWS::EC2::DHCPOptions": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-dhcp-options.html", + "Attributes": { + "DhcpOptionsId": { + "PrimitiveType": "String" + } + }, + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-dhcpoptions.html", "Properties": { "DomainName": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-dhcp-options.html#cfn-ec2-dhcpoptions-domainname", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-dhcpoptions.html#cfn-ec2-dhcpoptions-domainname", "PrimitiveType": "String", "Required": false, "UpdateType": "Immutable" }, "DomainNameServers": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-dhcp-options.html#cfn-ec2-dhcpoptions-domainnameservers", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-dhcpoptions.html#cfn-ec2-dhcpoptions-domainnameservers", "DuplicatesAllowed": false, "PrimitiveItemType": "String", "Required": false, @@ -73316,7 +73518,7 @@ "UpdateType": "Immutable" }, "NetbiosNameServers": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-dhcp-options.html#cfn-ec2-dhcpoptions-netbiosnameservers", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-dhcpoptions.html#cfn-ec2-dhcpoptions-netbiosnameservers", "DuplicatesAllowed": false, "PrimitiveItemType": "String", "Required": false, @@ -73324,13 +73526,13 @@ "UpdateType": "Immutable" }, "NetbiosNodeType": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-dhcp-options.html#cfn-ec2-dhcpoptions-netbiosnodetype", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-dhcpoptions.html#cfn-ec2-dhcpoptions-netbiosnodetype", "PrimitiveType": "Integer", "Required": false, "UpdateType": "Immutable" }, "NtpServers": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-dhcp-options.html#cfn-ec2-dhcpoptions-ntpservers", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-dhcpoptions.html#cfn-ec2-dhcpoptions-ntpservers", "DuplicatesAllowed": true, "PrimitiveItemType": "String", "Required": false, @@ -73338,7 +73540,7 @@ "UpdateType": "Immutable" }, "Tags": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-dhcp-options.html#cfn-ec2-dhcpoptions-tags", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-dhcpoptions.html#cfn-ec2-dhcpoptions-tags", "DuplicatesAllowed": true, "ItemType": "Tag", "Required": false, @@ -93067,6 +93269,12 @@ "Required": false, "UpdateType": "Mutable" }, + "AquaConfigurationStatus": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-redshift-cluster.html#cfn-redshift-cluster-aquaconfigurationstatus", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, "AutomatedSnapshotRetentionPeriod": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-redshift-cluster.html#cfn-redshift-cluster-automatedsnapshotretentionperiod", "PrimitiveType": "Integer", @@ -93077,7 +93285,25 @@ "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-redshift-cluster.html#cfn-redshift-cluster-availabilityzone", "PrimitiveType": "String", "Required": false, - "UpdateType": "Immutable" + "UpdateType": "Mutable" + }, + "AvailabilityZoneRelocation": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-redshift-cluster.html#cfn-redshift-cluster-availabilityzonerelocation", + "PrimitiveType": "Boolean", + "Required": false, + "UpdateType": "Mutable" + }, + "AvailabilityZoneRelocationStatus": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-redshift-cluster.html#cfn-redshift-cluster-availabilityzonerelocationstatus", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "Classic": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-redshift-cluster.html#cfn-redshift-cluster-classic", + "PrimitiveType": "Boolean", + "Required": false, + "UpdateType": "Mutable" }, "ClusterIdentifier": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-redshift-cluster.html#cfn-redshift-cluster-clusteridentifier", @@ -93123,17 +93349,53 @@ "Required": true, "UpdateType": "Immutable" }, + "DeferMaintenance": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-redshift-cluster.html#cfn-redshift-cluster-defermaintenance", + "PrimitiveType": "Boolean", + "Required": false, + "UpdateType": "Mutable" + }, + "DeferMaintenanceDuration": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-redshift-cluster.html#cfn-redshift-cluster-defermaintenanceduration", + "PrimitiveType": "Integer", + "Required": false, + "UpdateType": "Mutable" + }, + "DeferMaintenanceEndTime": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-redshift-cluster.html#cfn-redshift-cluster-defermaintenanceendtime", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "DeferMaintenanceIdentifier": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-redshift-cluster.html#cfn-redshift-cluster-defermaintenanceidentifier", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "DeferMaintenanceStartTime": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-redshift-cluster.html#cfn-redshift-cluster-defermaintenancestarttime", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "DestinationRegion": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-redshift-cluster.html#cfn-redshift-cluster-destinationregion", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, "ElasticIp": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-redshift-cluster.html#cfn-redshift-cluster-elasticip", "PrimitiveType": "String", "Required": false, - "UpdateType": "Immutable" + "UpdateType": "Mutable" }, "Encrypted": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-redshift-cluster.html#cfn-redshift-cluster-encrypted", "PrimitiveType": "Boolean", "Required": false, - "UpdateType": "Immutable" + "UpdateType": "Mutable" }, "Endpoint": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-redshift-cluster.html#cfn-redshift-cluster-endpoint", @@ -93141,6 +93403,12 @@ "Type": "Endpoint", "UpdateType": "Mutable" }, + "EnhancedVpcRouting": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-redshift-cluster.html#cfn-redshift-cluster-enhancedvpcrouting", + "PrimitiveType": "Boolean", + "Required": false, + "UpdateType": "Mutable" + }, "HsmClientCertificateIdentifier": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-redshift-cluster.html#cfn-redshift-cluster-hsmclientcertificateidentifier", "PrimitiveType": "String", @@ -93165,7 +93433,7 @@ "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-redshift-cluster.html#cfn-redshift-cluster-kmskeyid", "PrimitiveType": "String", "Required": false, - "UpdateType": "Immutable" + "UpdateType": "Mutable" }, "LoggingProperties": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-redshift-cluster.html#cfn-redshift-cluster-loggingproperties", @@ -93173,6 +93441,18 @@ "Type": "LoggingProperties", "UpdateType": "Mutable" }, + "MaintenanceTrackName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-redshift-cluster.html#cfn-redshift-cluster-maintenancetrackname", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "ManualSnapshotRetentionPeriod": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-redshift-cluster.html#cfn-redshift-cluster-manualsnapshotretentionperiod", + "PrimitiveType": "Integer", + "Required": false, + "UpdateType": "Mutable" + }, "MasterUserPassword": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-redshift-cluster.html#cfn-redshift-cluster-masteruserpassword", "PrimitiveType": "String", @@ -93207,7 +93487,7 @@ "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-redshift-cluster.html#cfn-redshift-cluster-port", "PrimitiveType": "Integer", "Required": false, - "UpdateType": "Immutable" + "UpdateType": "Mutable" }, "PreferredMaintenanceWindow": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-redshift-cluster.html#cfn-redshift-cluster-preferredmaintenancewindow", @@ -93221,12 +93501,48 @@ "Required": false, "UpdateType": "Mutable" }, + "ResourceAction": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-redshift-cluster.html#cfn-redshift-cluster-resourceaction", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "RevisionTarget": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-redshift-cluster.html#cfn-redshift-cluster-revisiontarget", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "RotateEncryptionKey": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-redshift-cluster.html#cfn-redshift-cluster-rotateencryptionkey", + "PrimitiveType": "Boolean", + "Required": false, + "UpdateType": "Mutable" + }, "SnapshotClusterIdentifier": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-redshift-cluster.html#cfn-redshift-cluster-snapshotclusteridentifier", "PrimitiveType": "String", "Required": false, "UpdateType": "Immutable" }, + "SnapshotCopyGrantName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-redshift-cluster.html#cfn-redshift-cluster-snapshotcopygrantname", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "SnapshotCopyManual": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-redshift-cluster.html#cfn-redshift-cluster-snapshotcopymanual", + "PrimitiveType": "Boolean", + "Required": false, + "UpdateType": "Mutable" + }, + "SnapshotCopyRetentionPeriod": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-redshift-cluster.html#cfn-redshift-cluster-snapshotcopyretentionperiod", + "PrimitiveType": "Integer", + "Required": false, + "UpdateType": "Mutable" + }, "SnapshotIdentifier": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-redshift-cluster.html#cfn-redshift-cluster-snapshotidentifier", "PrimitiveType": "String", @@ -96592,6 +96908,12 @@ }, "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-sagemaker-endpointconfig.html", "Properties": { + "AsyncInferenceConfig": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-sagemaker-endpointconfig.html#cfn-sagemaker-endpointconfig-asyncinferenceconfig", + "Required": false, + "Type": "AsyncInferenceConfig", + "UpdateType": "Immutable" + }, "DataCaptureConfig": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-sagemaker-endpointconfig.html#cfn-sagemaker-endpointconfig-datacaptureconfig", "Required": false, From cd280a8f4f46eb50be3a25d80c00a807881832c4 Mon Sep 17 00:00:00 2001 From: "Michael S. Fischer" Date: Mon, 23 Aug 2021 10:42:53 -0700 Subject: [PATCH 05/39] feat(ecs): add support for Bottlerocket on ARM64 (#15454) Add support for Bottlerocket AMIs on ARM64 architecture, including AWS Graviton2 instances. Closes #14466 ---- *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-ecs/README.md | 11 +- packages/@aws-cdk/aws-ecs/lib/cluster.ts | 19 +- .../@aws-cdk/aws-ecs/test/cluster.test.ts | 31 +- .../integ.graviton-bottlerocket.expected.json | 874 ++++++++++++++++++ .../test/ec2/integ.graviton-bottlerocket.ts | 16 + .../test/ec2/integ.graviton.expected.json | 12 +- 6 files changed, 952 insertions(+), 11 deletions(-) create mode 100644 packages/@aws-cdk/aws-ecs/test/ec2/integ.graviton-bottlerocket.expected.json create mode 100644 packages/@aws-cdk/aws-ecs/test/ec2/integ.graviton-bottlerocket.ts diff --git a/packages/@aws-cdk/aws-ecs/README.md b/packages/@aws-cdk/aws-ecs/README.md index e3c7f900b58a2..1d8a10f4deb8c 100644 --- a/packages/@aws-cdk/aws-ecs/README.md +++ b/packages/@aws-cdk/aws-ecs/README.md @@ -137,8 +137,6 @@ If you omit the property `vpc`, the construct will create a new VPC with two AZs [Bottlerocket](https://aws.amazon.com/bottlerocket/) is a Linux-based open source operating system that is purpose-built by AWS for running containers. You can launch Amazon ECS container instances with the Bottlerocket AMI. -> **NOTICE**: The Bottlerocket AMI is in developer preview release for Amazon ECS and is subject to change. - The following example will create a capacity with self-managed Amazon EC2 capacity of 2 `c5.large` Linux instances running with `Bottlerocket` AMI. The following example adds Bottlerocket capacity to the cluster: @@ -164,7 +162,16 @@ cluster.addCapacity('graviton-cluster', { instanceType: new ec2.InstanceType('c6g.large'), machineImage: ecs.EcsOptimizedImage.amazonLinux2(ecs.AmiHardwareType.ARM), }); +``` +Bottlerocket is also supported: + +```ts +cluster.addCapacity('graviton-cluster', { + minCapacity: 2, + instanceType: new ec2.InstanceType('c6g.large'), + machineImage: ecs.MachineImageType.BOTTLEROCKET, +}); ``` ### Spot Instances diff --git a/packages/@aws-cdk/aws-ecs/lib/cluster.ts b/packages/@aws-cdk/aws-ecs/lib/cluster.ts index 1fce9b00d4500..4ea5e24b7529c 100644 --- a/packages/@aws-cdk/aws-ecs/lib/cluster.ts +++ b/packages/@aws-cdk/aws-ecs/lib/cluster.ts @@ -297,7 +297,9 @@ export class Cluster extends Resource implements ICluster { // Do 2-way defaulting here: if the machineImageType is BOTTLEROCKET, pick the right AMI. // Otherwise, determine the machineImageType from the given AMI. const machineImage = options.machineImage ?? - (options.machineImageType === MachineImageType.BOTTLEROCKET ? new BottleRocketImage() : new EcsOptimizedAmi()); + (options.machineImageType === MachineImageType.BOTTLEROCKET ? new BottleRocketImage({ + architecture: options.instanceType.architecture, + }) : new EcsOptimizedAmi()); const machineImageType = options.machineImageType ?? (isBottleRocketImage(machineImage) ? MachineImageType.BOTTLEROCKET : MachineImageType.AMAZON_LINUX_2); @@ -767,6 +769,13 @@ export interface BottleRocketImageProps { * @default - BottlerocketEcsVariant.AWS_ECS_1 */ readonly variant?: BottlerocketEcsVariant; + + /** + * The CPU architecture + * + * @default - x86_64 + */ + readonly architecture?: ec2.InstanceArchitecture; } /** @@ -779,14 +788,20 @@ export class BottleRocketImage implements ec2.IMachineImage { */ private readonly variant: string; + /** + * Instance architecture + */ + private readonly architecture: ec2.InstanceArchitecture; + /** * Constructs a new instance of the BottleRocketImage class. */ public constructor(props: BottleRocketImageProps = {}) { this.variant = props.variant ?? BottlerocketEcsVariant.AWS_ECS_1; + this.architecture = props.architecture ?? ec2.InstanceArchitecture.X86_64; // set the SSM parameter name - this.amiParameterName = `/aws/service/bottlerocket/${this.variant}/x86_64/latest/image_id`; + this.amiParameterName = `/aws/service/bottlerocket/${this.variant}/${this.architecture}/latest/image_id`; } /** diff --git a/packages/@aws-cdk/aws-ecs/test/cluster.test.ts b/packages/@aws-cdk/aws-ecs/test/cluster.test.ts index 8b6dd4626ddb6..40c74a08257f2 100644 --- a/packages/@aws-cdk/aws-ecs/test/cluster.test.ts +++ b/packages/@aws-cdk/aws-ecs/test/cluster.test.ts @@ -1682,7 +1682,36 @@ nodeunitShim({ test.done(); }, - 'cluster capacity with bottlerocket AMI, by setting the machineImage'(test: Test) { + 'correct bottlerocket AMI for ARM64 architecture'(test: Test) { + // GIVEN + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'test'); + + const cluster = new ecs.Cluster(stack, 'EcsCluster'); + cluster.addCapacity('bottlerocket-asg', { + instanceType: new ec2.InstanceType('m6g.large'), + machineImageType: ecs.MachineImageType.BOTTLEROCKET, + }); + + // THEN + expect(stack).to(haveResource('AWS::AutoScaling::LaunchConfiguration', { + ImageId: { + Ref: 'SsmParameterValueawsservicebottlerocketawsecs1arm64latestimageidC96584B6F00A464EAD1953AFF4B05118Parameter', + }, + })); + + const assembly = app.synth(); + const template = assembly.getStackByName(stack.stackName).template; + test.deepEqual(template.Parameters, { + SsmParameterValueawsservicebottlerocketawsecs1arm64latestimageidC96584B6F00A464EAD1953AFF4B05118Parameter: { + Type: 'AWS::SSM::Parameter::Value', + Default: '/aws/service/bottlerocket/aws-ecs-1/arm64/latest/image_id', + }, + }); + test.done(); + }, + + 'throws when machineImage and machineImageType both specified'(test: Test) { // GIVEN const app = new cdk.App(); const stack = new cdk.Stack(app, 'test'); diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/integ.graviton-bottlerocket.expected.json b/packages/@aws-cdk/aws-ecs/test/ec2/integ.graviton-bottlerocket.expected.json new file mode 100644 index 0000000000000..fb6c80ff66b00 --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/test/ec2/integ.graviton-bottlerocket.expected.json @@ -0,0 +1,874 @@ +{ + "Resources": { + "Vpc8378EB38": { + "Type": "AWS::EC2::VPC", + "Properties": { + "CidrBlock": "10.0.0.0/16", + "EnableDnsHostnames": true, + "EnableDnsSupport": true, + "InstanceTenancy": "default", + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc" + } + ] + } + }, + "VpcPublicSubnet1Subnet5C2D37C4": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.0.0/18", + "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": "aws-ecs-integ/Vpc/PublicSubnet1" + } + ] + } + }, + "VpcPublicSubnet1RouteTable6C95E38E": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-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": "aws-ecs-integ/Vpc/PublicSubnet1" + } + ] + } + }, + "VpcPublicSubnet1NATGateway4D7517AA": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "SubnetId": { + "Ref": "VpcPublicSubnet1Subnet5C2D37C4" + }, + "AllocationId": { + "Fn::GetAtt": [ + "VpcPublicSubnet1EIPD7E02669", + "AllocationId" + ] + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc/PublicSubnet1" + } + ] + } + }, + "VpcPublicSubnet2Subnet691E08A3": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.64.0/18", + "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": "aws-ecs-integ/Vpc/PublicSubnet2" + } + ] + } + }, + "VpcPublicSubnet2RouteTable94F7E489": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-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" + ] + }, + "VpcPublicSubnet2EIP3C605A87": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc", + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc/PublicSubnet2" + } + ] + } + }, + "VpcPublicSubnet2NATGateway9182C01D": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "SubnetId": { + "Ref": "VpcPublicSubnet2Subnet691E08A3" + }, + "AllocationId": { + "Fn::GetAtt": [ + "VpcPublicSubnet2EIP3C605A87", + "AllocationId" + ] + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc/PublicSubnet2" + } + ] + } + }, + "VpcPrivateSubnet1Subnet536B997A": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.128.0/18", + "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": "aws-ecs-integ/Vpc/PrivateSubnet1" + } + ] + } + }, + "VpcPrivateSubnet1RouteTableB2C5B500": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-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.192.0/18", + "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": "aws-ecs-integ/Vpc/PrivateSubnet2" + } + ] + } + }, + "VpcPrivateSubnet2RouteTableA678073B": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-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": "VpcPublicSubnet2NATGateway9182C01D" + } + } + }, + "VpcIGWD7BA715C": { + "Type": "AWS::EC2::InternetGateway", + "Properties": { + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc" + } + ] + } + }, + "VpcVPCGWBF912B6E": { + "Type": "AWS::EC2::VPCGatewayAttachment", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "InternetGatewayId": { + "Ref": "VpcIGWD7BA715C" + } + } + }, + "EcsCluster97242B84": { + "Type": "AWS::ECS::Cluster" + }, + "EcsClustergravitonclusterInstanceSecurityGroup0187E9BB": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "aws-ecs-integ/EcsCluster/graviton-cluster/InstanceSecurityGroup", + "SecurityGroupEgress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow all outbound traffic by default", + "IpProtocol": "-1" + } + ], + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ/EcsCluster/graviton-cluster" + } + ], + "VpcId": { + "Ref": "Vpc8378EB38" + } + } + }, + "EcsClustergravitonclusterInstanceRole0D0E0F94": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": { + "Fn::Join": [ + "", + [ + "ec2.", + { + "Ref": "AWS::URLSuffix" + } + ] + ] + } + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/AmazonSSMManagedInstanceCore" + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AmazonEC2ContainerServiceforEC2Role" + ] + ] + } + ], + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ/EcsCluster/graviton-cluster" + } + ] + } + }, + "EcsClustergravitonclusterInstanceRoleDefaultPolicyB89DB33F": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "ecs:DeregisterContainerInstance", + "ecs:RegisterContainerInstance", + "ecs:Submit*" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "EcsCluster97242B84", + "Arn" + ] + } + }, + { + "Action": [ + "ecs:Poll", + "ecs:StartTelemetrySession" + ], + "Condition": { + "ArnEquals": { + "ecs:cluster": { + "Fn::GetAtt": [ + "EcsCluster97242B84", + "Arn" + ] + } + } + }, + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": [ + "ecs:DiscoverPollEndpoint", + "ecr:GetAuthorizationToken", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "EcsClustergravitonclusterInstanceRoleDefaultPolicyB89DB33F", + "Roles": [ + { + "Ref": "EcsClustergravitonclusterInstanceRole0D0E0F94" + } + ] + } + }, + "EcsClustergravitonclusterInstanceProfileD1BBFAAE": { + "Type": "AWS::IAM::InstanceProfile", + "Properties": { + "Roles": [ + { + "Ref": "EcsClustergravitonclusterInstanceRole0D0E0F94" + } + ] + } + }, + "EcsClustergravitonclusterLaunchConfig64183B57": { + "Type": "AWS::AutoScaling::LaunchConfiguration", + "Properties": { + "ImageId": { + "Ref": "SsmParameterValueawsservicebottlerocketawsecs1arm64latestimageidC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + "InstanceType": "c6g.large", + "IamInstanceProfile": { + "Ref": "EcsClustergravitonclusterInstanceProfileD1BBFAAE" + }, + "SecurityGroups": [ + { + "Fn::GetAtt": [ + "EcsClustergravitonclusterInstanceSecurityGroup0187E9BB", + "GroupId" + ] + } + ], + "UserData": { + "Fn::Base64": { + "Fn::Join": [ + "", + [ + "\n[settings.ecs]\ncluster = \"", + { + "Ref": "EcsCluster97242B84" + }, + "\"" + ] + ] + } + } + }, + "DependsOn": [ + "EcsClustergravitonclusterInstanceRoleDefaultPolicyB89DB33F", + "EcsClustergravitonclusterInstanceRole0D0E0F94" + ] + }, + "EcsClustergravitonclusterASG869F3168": { + "Type": "AWS::AutoScaling::AutoScalingGroup", + "Properties": { + "MaxSize": "2", + "MinSize": "2", + "LaunchConfigurationName": { + "Ref": "EcsClustergravitonclusterLaunchConfig64183B57" + }, + "Tags": [ + { + "Key": "Name", + "PropagateAtLaunch": true, + "Value": "aws-ecs-integ/EcsCluster/graviton-cluster" + } + ], + "VPCZoneIdentifier": [ + { + "Ref": "VpcPrivateSubnet1Subnet536B997A" + }, + { + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" + } + ] + }, + "UpdatePolicy": { + "AutoScalingReplacingUpdate": { + "WillReplace": true + }, + "AutoScalingScheduledAction": { + "IgnoreUnmodifiedGroupSizeProperties": true + } + } + }, + "EcsClustergravitonclusterDrainECSHookFunctionServiceRole26D97764": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ], + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ/EcsCluster/graviton-cluster" + } + ] + } + }, + "EcsClustergravitonclusterDrainECSHookFunctionServiceRoleDefaultPolicy1563DC6B": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "ec2:DescribeInstances", + "ec2:DescribeInstanceAttribute", + "ec2:DescribeInstanceStatus", + "ec2:DescribeHosts" + ], + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": "autoscaling:CompleteLifecycleAction", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":autoscaling:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":autoScalingGroup:*:autoScalingGroupName/", + { + "Ref": "EcsClustergravitonclusterASG869F3168" + } + ] + ] + } + }, + { + "Action": [ + "ecs:DescribeContainerInstances", + "ecs:DescribeTasks" + ], + "Condition": { + "ArnEquals": { + "ecs:cluster": { + "Fn::GetAtt": [ + "EcsCluster97242B84", + "Arn" + ] + } + } + }, + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": [ + "ecs:ListContainerInstances", + "ecs:SubmitContainerStateChange", + "ecs:SubmitTaskStateChange" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "EcsCluster97242B84", + "Arn" + ] + } + }, + { + "Action": [ + "ecs:UpdateContainerInstancesState", + "ecs:ListTasks" + ], + "Condition": { + "ArnEquals": { + "ecs:cluster": { + "Fn::GetAtt": [ + "EcsCluster97242B84", + "Arn" + ] + } + } + }, + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "EcsClustergravitonclusterDrainECSHookFunctionServiceRoleDefaultPolicy1563DC6B", + "Roles": [ + { + "Ref": "EcsClustergravitonclusterDrainECSHookFunctionServiceRole26D97764" + } + ] + } + }, + "EcsClustergravitonclusterDrainECSHookFunctionB606E681": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "ZipFile": "import boto3, json, os, time\n\necs = boto3.client('ecs')\nautoscaling = boto3.client('autoscaling')\n\n\ndef lambda_handler(event, context):\n print(json.dumps(event))\n cluster = os.environ['CLUSTER']\n snsTopicArn = event['Records'][0]['Sns']['TopicArn']\n lifecycle_event = json.loads(event['Records'][0]['Sns']['Message'])\n instance_id = lifecycle_event.get('EC2InstanceId')\n if not instance_id:\n print('Got event without EC2InstanceId: %s', json.dumps(event))\n return\n\n instance_arn = container_instance_arn(cluster, instance_id)\n print('Instance %s has container instance ARN %s' % (lifecycle_event['EC2InstanceId'], instance_arn))\n\n if not instance_arn:\n return\n\n task_arns = container_instance_task_arns(cluster, instance_arn)\n \n if task_arns:\n print('Instance ARN %s has task ARNs %s' % (instance_arn, ', '.join(task_arns)))\n\n while has_tasks(cluster, instance_arn, task_arns):\n time.sleep(10)\n\n try:\n print('Terminating instance %s' % instance_id)\n autoscaling.complete_lifecycle_action(\n LifecycleActionResult='CONTINUE',\n **pick(lifecycle_event, 'LifecycleHookName', 'LifecycleActionToken', 'AutoScalingGroupName'))\n except Exception as e:\n # Lifecycle action may have already completed.\n print(str(e))\n\n\ndef container_instance_arn(cluster, instance_id):\n \"\"\"Turn an instance ID into a container instance ARN.\"\"\"\n arns = ecs.list_container_instances(cluster=cluster, filter='ec2InstanceId==' + instance_id)['containerInstanceArns']\n if not arns:\n return None\n return arns[0]\n\ndef container_instance_task_arns(cluster, instance_arn):\n \"\"\"Fetch tasks for a container instance ARN.\"\"\"\n arns = ecs.list_tasks(cluster=cluster, containerInstance=instance_arn)['taskArns']\n return arns\n\ndef has_tasks(cluster, instance_arn, task_arns):\n \"\"\"Return True if the instance is running tasks for the given cluster.\"\"\"\n instances = ecs.describe_container_instances(cluster=cluster, containerInstances=[instance_arn])['containerInstances']\n if not instances:\n return False\n instance = instances[0]\n\n if instance['status'] == 'ACTIVE':\n # Start draining, then try again later\n set_container_instance_to_draining(cluster, instance_arn)\n return True\n\n task_count = None\n\n if task_arns:\n # Fetch details for tasks running on the container instance\n tasks = ecs.describe_tasks(cluster=cluster, tasks=task_arns)['tasks']\n if tasks:\n # Consider any non-stopped tasks as running\n task_count = sum(task['lastStatus'] != 'STOPPED' for task in tasks) + instance['pendingTasksCount']\n \n if not task_count:\n # Fallback to instance task counts if detailed task information is unavailable\n task_count = instance['runningTasksCount'] + instance['pendingTasksCount']\n \n print('Instance %s has %s tasks' % (instance_arn, task_count))\n\n return task_count > 0\n\ndef set_container_instance_to_draining(cluster, instance_arn):\n ecs.update_container_instances_state(\n cluster=cluster,\n containerInstances=[instance_arn], status='DRAINING')\n\n\ndef pick(dct, *keys):\n \"\"\"Pick a subset of a dict.\"\"\"\n return {k: v for k, v in dct.items() if k in keys}\n" + }, + "Role": { + "Fn::GetAtt": [ + "EcsClustergravitonclusterDrainECSHookFunctionServiceRole26D97764", + "Arn" + ] + }, + "Environment": { + "Variables": { + "CLUSTER": { + "Ref": "EcsCluster97242B84" + } + } + }, + "Handler": "index.lambda_handler", + "Runtime": "python3.6", + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ/EcsCluster/graviton-cluster" + } + ], + "Timeout": 310 + }, + "DependsOn": [ + "EcsClustergravitonclusterDrainECSHookFunctionServiceRoleDefaultPolicy1563DC6B", + "EcsClustergravitonclusterDrainECSHookFunctionServiceRole26D97764" + ] + }, + "EcsClustergravitonclusterDrainECSHookFunctionAllowInvokeawsecsintegEcsClustergravitonclusterLifecycleHookDrainHookTopicF44E68AA50D91BA3": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "EcsClustergravitonclusterDrainECSHookFunctionB606E681", + "Arn" + ] + }, + "Principal": "sns.amazonaws.com", + "SourceArn": { + "Ref": "EcsClustergravitonclusterLifecycleHookDrainHookTopic0A778AAC" + } + } + }, + "EcsClustergravitonclusterDrainECSHookFunctionTopic65B3FD43": { + "Type": "AWS::SNS::Subscription", + "Properties": { + "Protocol": "lambda", + "TopicArn": { + "Ref": "EcsClustergravitonclusterLifecycleHookDrainHookTopic0A778AAC" + }, + "Endpoint": { + "Fn::GetAtt": [ + "EcsClustergravitonclusterDrainECSHookFunctionB606E681", + "Arn" + ] + } + } + }, + "EcsClustergravitonclusterLifecycleHookDrainHookRoleA16C85AD": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "autoscaling.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ/EcsCluster/graviton-cluster" + } + ] + } + }, + "EcsClustergravitonclusterLifecycleHookDrainHookRoleDefaultPolicy516A6DA7": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "sns:Publish", + "Effect": "Allow", + "Resource": { + "Ref": "EcsClustergravitonclusterLifecycleHookDrainHookTopic0A778AAC" + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "EcsClustergravitonclusterLifecycleHookDrainHookRoleDefaultPolicy516A6DA7", + "Roles": [ + { + "Ref": "EcsClustergravitonclusterLifecycleHookDrainHookRoleA16C85AD" + } + ] + } + }, + "EcsClustergravitonclusterLifecycleHookDrainHookTopic0A778AAC": { + "Type": "AWS::SNS::Topic", + "Properties": { + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ/EcsCluster/graviton-cluster" + } + ] + } + }, + "EcsClustergravitonclusterLifecycleHookDrainHookA1F91B1B": { + "Type": "AWS::AutoScaling::LifecycleHook", + "Properties": { + "AutoScalingGroupName": { + "Ref": "EcsClustergravitonclusterASG869F3168" + }, + "LifecycleTransition": "autoscaling:EC2_INSTANCE_TERMINATING", + "DefaultResult": "CONTINUE", + "HeartbeatTimeout": 300, + "NotificationTargetARN": { + "Ref": "EcsClustergravitonclusterLifecycleHookDrainHookTopic0A778AAC" + }, + "RoleARN": { + "Fn::GetAtt": [ + "EcsClustergravitonclusterLifecycleHookDrainHookRoleA16C85AD", + "Arn" + ] + } + }, + "DependsOn": [ + "EcsClustergravitonclusterLifecycleHookDrainHookRoleDefaultPolicy516A6DA7", + "EcsClustergravitonclusterLifecycleHookDrainHookRoleA16C85AD" + ] + } + }, + "Parameters": { + "SsmParameterValueawsservicebottlerocketawsecs1arm64latestimageidC96584B6F00A464EAD1953AFF4B05118Parameter": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/aws/service/bottlerocket/aws-ecs-1/arm64/latest/image_id" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/integ.graviton-bottlerocket.ts b/packages/@aws-cdk/aws-ecs/test/ec2/integ.graviton-bottlerocket.ts new file mode 100644 index 0000000000000..5b8ecdca4fd0b --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/test/ec2/integ.graviton-bottlerocket.ts @@ -0,0 +1,16 @@ +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as cdk from '@aws-cdk/core'; +import * as ecs from '../../lib'; + +const app = new cdk.App(); +const stack = new cdk.Stack(app, 'aws-ecs-integ'); + +const vpc = new ec2.Vpc(stack, 'Vpc', { maxAzs: 2 }); +const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + +cluster.addCapacity('graviton-cluster', { + minCapacity: 2, + instanceType: new ec2.InstanceType('c6g.large'), + machineImageType: ecs.MachineImageType.BOTTLEROCKET, +}); +app.synth(); diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/integ.graviton.expected.json b/packages/@aws-cdk/aws-ecs/test/ec2/integ.graviton.expected.json index 8529dea9b1171..c2b3eeecc8ddf 100644 --- a/packages/@aws-cdk/aws-ecs/test/ec2/integ.graviton.expected.json +++ b/packages/@aws-cdk/aws-ecs/test/ec2/integ.graviton.expected.json @@ -95,15 +95,15 @@ "VpcPublicSubnet1NATGateway4D7517AA": { "Type": "AWS::EC2::NatGateway", "Properties": { + "SubnetId": { + "Ref": "VpcPublicSubnet1Subnet5C2D37C4" + }, "AllocationId": { "Fn::GetAtt": [ "VpcPublicSubnet1EIPD7E02669", "AllocationId" ] }, - "SubnetId": { - "Ref": "VpcPublicSubnet1Subnet5C2D37C4" - }, "Tags": [ { "Key": "Name", @@ -192,15 +192,15 @@ "VpcPublicSubnet2NATGateway9182C01D": { "Type": "AWS::EC2::NatGateway", "Properties": { + "SubnetId": { + "Ref": "VpcPublicSubnet2Subnet691E08A3" + }, "AllocationId": { "Fn::GetAtt": [ "VpcPublicSubnet2EIP3C605A87", "AllocationId" ] }, - "SubnetId": { - "Ref": "VpcPublicSubnet2Subnet691E08A3" - }, "Tags": [ { "Key": "Name", From fdf612a45b3d3c0f8c972bd2736cb9f93e2e7057 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Tue, 24 Aug 2021 11:26:24 +0200 Subject: [PATCH 06/39] docs(pipelines): should use 'lookup' tag instead of 'deploy' tag (#15984) The code example was using the wrong condition. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/pipelines/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk/pipelines/README.md b/packages/@aws-cdk/pipelines/README.md index f17a35e3a9990..bab41842c2316 100644 --- a/packages/@aws-cdk/pipelines/README.md +++ b/packages/@aws-cdk/pipelines/README.md @@ -955,7 +955,7 @@ new CodePipeline(this, 'Pipeline', { resources: ['*'], conditions: { StringEquals: { - 'iam:ResourceTag/aws-cdk:bootstrap-role': 'deploy', + 'iam:ResourceTag/aws-cdk:bootstrap-role': 'lookup', }, }, }), From 0d0db38e3cdb557b4a641c5993068400847cc7df Mon Sep 17 00:00:00 2001 From: Yihui Han <53243835+readybuilderone@users.noreply.github.com> Date: Tue, 24 Aug 2021 18:09:03 +0800 Subject: [PATCH 07/39] fix: (aws-ec2): fix vpc endpoint incorrect issue in China region (#16139) fix:(aws-ec2): fix vpc endpoint incorrect issue in China region This PR fix the issue that can't create interface vpc endpoint for 40+ services like ecr, ec2, athena etc Closes: #9864 ---- # Considerations 1. In cn-north-1 region, there are: - 2 services whose endpoint service begin with "aws.sagemaker" prefix; - 41 services whose endpoint service begin with "cn.com.amazonaws" prefix; - 21 services whose endpoint service begin with "com.amazonaws" prefix; Details: https://gist.github.com/readybuilderone/d355f9f8f0f2b66379a10742b3c67cc7 2. in cn-northwest-1 region, there are: - 2 services whose endpoint service begin with "aws.sagemaker" prefix; - 44 services whose endpoint service begin with "cn.com.amazonaws" prefix; - 21 services whose endpoint service begin with "com.amazonaws" prefix; Details: https://gist.github.com/readybuilderone/a79f2c5e6fa02aae1699bf674b08be7c So, In cn-north-1 and cn-northwest-1, the vpc endpoint prefix is both region and service related. At first, I found the vpc endpoint prefix could be fetched via [AWS API](https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeVpcEndpoints.html) , however the javascript sdk supports only asynchronous call, currently I didn't find any pattern in CDK to make live calls in object construction. Since the exception rules won't ever change ( only be added to), and the rate of change should be small in the grand scheme, I just kept the exception prefix lists in a map as fact data. # Known Issues Since the interface vpc endpoints prefixs are not region agnostic, it requires to set the {region} in the environment to create stacks in cn-north-1, and cn-northwest-1. The methods could be found in the [offical guide](https://docs.aws.amazon.com/cdk/latest/guide/environments.html). *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-ec2/lib/vpc-endpoint.ts | 63 +++++++++- .../aws-ec2/test/vpc-endpoint.test.ts | 119 ++++++++++++++++++ 2 files changed, 181 insertions(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-ec2/lib/vpc-endpoint.ts b/packages/@aws-cdk/aws-ec2/lib/vpc-endpoint.ts index dd28d74e84ea1..69ad4aab88404 100644 --- a/packages/@aws-cdk/aws-ec2/lib/vpc-endpoint.ts +++ b/packages/@aws-cdk/aws-ec2/lib/vpc-endpoint.ts @@ -326,9 +326,70 @@ export class InterfaceVpcEndpointAwsService implements IInterfaceVpcEndpointServ const region = Lazy.uncachedString({ produce: (context) => Stack.of(context.scope).region, }); - this.name = `${prefix || 'com.amazonaws'}.${region}.${name}`; + const defaultEndpointPrefix = Lazy.uncachedString({ + produce: (context) => { + const regionName = Stack.of(context.scope).region; + return this.getDefaultEndpointPrefix(name, regionName); + }, + }); + const defaultEndpointSuffix = Lazy.uncachedString({ + produce: (context) => { + const regionName = Stack.of(context.scope).region; + return this.getDefaultEndpointSuffix(name, regionName); + }, + }); + + this.name = `${prefix || defaultEndpointPrefix}.${region}.${name}${defaultEndpointSuffix}`; this.port = port || 443; } + + /** + * Get the endpoint prefix for the service in the specified region + * because the prefix for some of the services in cn-north-1 and cn-northwest-1 are different + * + * For future maintenance, the vpc endpoint services could be fetched using AWS CLI Commmand: + * aws ec2 describe-vpc-endpoint-services + */ + private getDefaultEndpointPrefix(name: string, region: string) { + const VPC_ENDPOINT_SERVICE_EXCEPTIONS: { [region: string]: string[] } = { + 'cn-north-1': ['application-autoscaling', 'athena', 'autoscaling', 'awsconnector', 'cassandra', + 'cloudformation', 'codedeploy-commands-secure', 'databrew', 'dms', 'ebs', 'ec2', 'ecr.api', 'ecr.dkr', + 'elasticbeanstalk', 'elasticfilesystem', 'elasticfilesystem-fips', 'execute-api', 'imagebuilder', + 'iotsitewise.api', 'iotsitewise.data', 'kinesis-streams', 'lambda', 'license-manager', 'monitoring', + 'rds', 'redshift', 'redshift-data', 's3', 'sagemaker.api', 'sagemaker.featurestore-runtime', + 'sagemaker.runtime', 'servicecatalog', 'sms', 'sqs', 'states', 'sts', 'synthetics', 'transcribe', + 'transcribestreaming', 'transfer', 'xray'], + 'cn-northwest-1': ['application-autoscaling', 'athena', 'autoscaling', 'awsconnector', 'cassandra', + 'cloudformation', 'codedeploy-commands-secure', 'databrew', 'dms', 'ebs', 'ec2', 'ecr.api', 'ecr.dkr', + 'elasticbeanstalk', 'elasticfilesystem', 'elasticfilesystem-fips', 'execute-api', 'imagebuilder', + 'kinesis-streams', 'lambda', 'license-manager', 'monitoring', 'rds', 'redshift', 'redshift-data', 's3', + 'sagemaker.api', 'sagemaker.featurestore-runtime', 'sagemaker.runtime', 'servicecatalog', 'sms', 'sqs', + 'states', 'sts', 'synthetics', 'transcribe', 'transcribestreaming', 'transfer', 'workspaces', 'xray'], + }; + if (VPC_ENDPOINT_SERVICE_EXCEPTIONS[region]?.includes(name)) { + return 'cn.com.amazonaws'; + } else { + return 'com.amazonaws'; + } + } + + /** + * Get the endpoint suffix for the service in the specified region. + * In cn-north-1 and cn-northwest-1, the vpc endpoint of transcribe is: + * cn.com.amazonaws.cn-north-1.transcribe.cn + * cn.com.amazonaws.cn-northwest-1.transcribe.cn + * so suffix '.cn' should be return in these scenarios. + * + * For future maintenance, the vpc endpoint services could be fetched using AWS CLI Commmand: + * aws ec2 describe-vpc-endpoint-services + */ + private getDefaultEndpointSuffix(name: string, region: string) { + const VPC_ENDPOINT_SERVICE_EXCEPTIONS: { [region: string]: string[] } = { + 'cn-north-1': ['transcribe'], + 'cn-northwest-1': ['transcribe'], + }; + return VPC_ENDPOINT_SERVICE_EXCEPTIONS[region]?.includes(name) ? '.cn' : ''; + } } /** diff --git a/packages/@aws-cdk/aws-ec2/test/vpc-endpoint.test.ts b/packages/@aws-cdk/aws-ec2/test/vpc-endpoint.test.ts index a59dfbbca07eb..267c48a9e4145 100644 --- a/packages/@aws-cdk/aws-ec2/test/vpc-endpoint.test.ts +++ b/packages/@aws-cdk/aws-ec2/test/vpc-endpoint.test.ts @@ -607,5 +607,124 @@ nodeunitShim({ })); test.done(); }, + 'test vpc interface endpoint with cn.com.amazonaws prefix can be created correctly in cn-north-1'(test: Test) { + //GIVEN + const stack = new Stack(undefined, 'TestStack', { env: { account: '123456789012', region: 'cn-north-1' } }); + const vpc = new Vpc(stack, 'VPC'); + + //WHEN + vpc.addInterfaceEndpoint('ECR Endpoint', { + service: InterfaceVpcEndpointAwsService.ECR, + }); + + //THEN + expect(stack).to(haveResource('AWS::EC2::VPCEndpoint', { + ServiceName: 'cn.com.amazonaws.cn-north-1.ecr.api', + })); + + test.done(); + }, + 'test vpc interface endpoint with cn.com.amazonaws prefix can be created correctly in cn-northwest-1'(test: Test) { + //GIVEN + const stack = new Stack(undefined, 'TestStack', { env: { account: '123456789012', region: 'cn-northwest-1' } }); + const vpc = new Vpc(stack, 'VPC'); + + //WHEN + vpc.addInterfaceEndpoint('Lambda Endpoint', { + service: InterfaceVpcEndpointAwsService.LAMBDA, + }); + + //THEN + expect(stack).to(haveResource('AWS::EC2::VPCEndpoint', { + ServiceName: 'cn.com.amazonaws.cn-northwest-1.lambda', + })); + + test.done(); + }, + 'test vpc interface endpoint without cn.com.amazonaws prefix can be created correctly in cn-north-1'(test: Test) { + //GIVEN + const stack = new Stack(undefined, 'TestStack', { env: { account: '123456789012', region: 'cn-north-1' } }); + const vpc = new Vpc(stack, 'VPC'); + + //WHEN + vpc.addInterfaceEndpoint('ECS Endpoint', { + service: InterfaceVpcEndpointAwsService.ECS, + }); + + //THEN + expect(stack).to(haveResource('AWS::EC2::VPCEndpoint', { + ServiceName: 'com.amazonaws.cn-north-1.ecs', + })); + + test.done(); + }, + 'test vpc interface endpoint without cn.com.amazonaws prefix can be created correctly in cn-northwest-1'(test: Test) { + //GIVEN + const stack = new Stack(undefined, 'TestStack', { env: { account: '123456789012', region: 'cn-northwest-1' } }); + const vpc = new Vpc(stack, 'VPC'); + + //WHEN + vpc.addInterfaceEndpoint('Glue Endpoint', { + service: InterfaceVpcEndpointAwsService.GLUE, + }); + + //THEN + expect(stack).to(haveResource('AWS::EC2::VPCEndpoint', { + ServiceName: 'com.amazonaws.cn-northwest-1.glue', + })); + + test.done(); + }, + 'test vpc interface endpoint for transcribe can be created correctly in non-china regions'(test: Test) { + //GIVEN + const stack = new Stack(undefined, 'TestStack', { env: { account: '123456789012', region: 'us-east-1' } }); + const vpc = new Vpc(stack, 'VPC'); + + //WHEN + vpc.addInterfaceEndpoint('Transcribe Endpoint', { + service: InterfaceVpcEndpointAwsService.TRANSCRIBE, + }); + + //THEN + expect(stack).to(haveResource('AWS::EC2::VPCEndpoint', { + ServiceName: 'com.amazonaws.us-east-1.transcribe', + })); + + test.done(); + }, + 'test vpc interface endpoint for transcribe can be created correctly in cn-north-1'(test: Test) { + //GIVEN + const stack = new Stack(undefined, 'TestStack', { env: { account: '123456789012', region: 'cn-north-1' } }); + const vpc = new Vpc(stack, 'VPC'); + + //WHEN + vpc.addInterfaceEndpoint('Transcribe Endpoint', { + service: InterfaceVpcEndpointAwsService.TRANSCRIBE, + }); + + //THEN + expect(stack).to(haveResource('AWS::EC2::VPCEndpoint', { + ServiceName: 'cn.com.amazonaws.cn-north-1.transcribe.cn', + })); + + test.done(); + }, + 'test vpc interface endpoint for transcribe can be created correctly in cn-northwest-1'(test: Test) { + //GIVEN + const stack = new Stack(undefined, 'TestStack', { env: { account: '123456789012', region: 'cn-northwest-1' } }); + const vpc = new Vpc(stack, 'VPC'); + + //WHEN + vpc.addInterfaceEndpoint('Transcribe Endpoint', { + service: InterfaceVpcEndpointAwsService.TRANSCRIBE, + }); + + //THEN + expect(stack).to(haveResource('AWS::EC2::VPCEndpoint', { + ServiceName: 'cn.com.amazonaws.cn-northwest-1.transcribe.cn', + })); + + test.done(); + }, }, }); From 1be373c14350e2fade0874579f6196384dc8d3d2 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Tue, 24 Aug 2021 12:54:25 +0200 Subject: [PATCH 08/39] =?UTF-8?q?chore:=20`publishInParallel=3Dfalse`=20wi?= =?UTF-8?q?th=20tokens=20produces=20useless=20error=20m=E2=80=A6=20(#16196?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …essage When a buildspec is written to disk at synth time and the BuildSpec contains CFN references, the error produced is: ``` The "data" argument must be of type string or an instance of Buffer, TypedArray, or DataView. Received an instance of Object ``` Because it's trying to write the `{ Fn::Join }` instead of a plain string. This error message is pretty useless. Supporting the feature correctly is a lot more complicated, but at least we can detect this situation and give a more readable error message. Relates to #16164. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../lib/codepipeline/_codebuild-factory.ts | 10 +++++++- .../pipelines/test/docker-credentials.test.ts | 24 +++++++++++++++++-- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/packages/@aws-cdk/pipelines/lib/codepipeline/_codebuild-factory.ts b/packages/@aws-cdk/pipelines/lib/codepipeline/_codebuild-factory.ts index ac125aee29f0d..59abcbe8e287d 100644 --- a/packages/@aws-cdk/pipelines/lib/codepipeline/_codebuild-factory.ts +++ b/packages/@aws-cdk/pipelines/lib/codepipeline/_codebuild-factory.ts @@ -241,7 +241,15 @@ export class CodeBuildFactory implements ICodePipelineActionFactory { // Write to disk and replace with a reference const relativeSpecFile = `buildspec-${Node.of(scope).addr}-${this.constructId}.yaml`; const absSpecFile = path.join(cloudAssemblyBuildSpecDir(scope), relativeSpecFile); - fs.writeFileSync(absSpecFile, Stack.of(scope).resolve(actualBuildSpec.toBuildSpec()), { encoding: 'utf-8' }); + + // This should resolve to a pure JSON string. If it resolves to an object, it's a CFN + // expression, and we can't support that yet. Maybe someday if we think really hard about it. + const fileContents = Stack.of(scope).resolve(actualBuildSpec.toBuildSpec()); + + if (typeof fileContents !== 'string') { + throw new Error(`This BuildSpec contains CloudFormation references and is supported by publishInParallel=false: ${JSON.stringify(fileContents, undefined, 2)}`); + } + fs.writeFileSync(absSpecFile, fileContents, { encoding: 'utf-8' }); projectBuildSpec = codebuild.BuildSpec.fromSourceFilename(relativeSpecFile); } else { projectBuildSpec = actualBuildSpec; diff --git a/packages/@aws-cdk/pipelines/test/docker-credentials.test.ts b/packages/@aws-cdk/pipelines/test/docker-credentials.test.ts index de9d7efa54852..9e3559242e04c 100644 --- a/packages/@aws-cdk/pipelines/test/docker-credentials.test.ts +++ b/packages/@aws-cdk/pipelines/test/docker-credentials.test.ts @@ -6,12 +6,14 @@ import * as iam from '@aws-cdk/aws-iam'; import * as secretsmanager from '@aws-cdk/aws-secretsmanager'; import * as cdk from '@aws-cdk/core'; import * as cdkp from '../lib'; +import { ShellStep } from '../lib'; +import { DockerAssetApp, TestApp } from './testhelpers'; let app: cdk.App; let stack: cdk.Stack; beforeEach(() => { - app = new cdk.App(); + app = new TestApp(); stack = new cdk.Stack(app, 'Stack', { env: { account: '0123456789012', region: 'eu-west-1' }, }); @@ -299,9 +301,27 @@ describe('EcrDockerCredential', () => { expect(stack).not.toHaveResource('AWS::IAM::Policy'); }); - }); + // This test doesn't actually work yet. See https://github.com/aws/aws-cdk/issues/16164 + // eslint-disable-next-line jest/no-disabled-tests + test.skip('with non-parallel publishing', () => { + const pipelines = new cdkp.CodePipeline(stack, 'Pipeline', { + synth: new ShellStep('Build', { + input: cdkp.CodePipelineSource.gitHub('test/test', 'test'), + commands: ['cdk synth'], + }), + + publishAssetsInParallel: false, + dockerCredentials: [ + cdkp.DockerCredential.ecr([repo]), + ], + }); + pipelines.addStage(new DockerAssetApp(stack, 'AssetApp')); + + // Should not throw + app.synth(); + }); }); describe('dockerCredentialsInstallCommands', () => { From 9c39bcb970fc791e94d199b962cc006fca1a3320 Mon Sep 17 00:00:00 2001 From: Sam Wilson Date: Tue, 24 Aug 2021 06:56:44 -0500 Subject: [PATCH 09/39] fix(apigatewayv2): http api - disallow empty string as domain name (#16044) Currently, empty strings are allowed for custom domain name values despite other tools such as the AWS CLI not allowing empty string values. This effort brings the `DomainName` construct up to date with other tooling. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../aws-apigatewayv2/lib/common/domain-name.ts | 4 ++++ .../test/http/domain-name.test.ts | 16 ++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/common/domain-name.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/common/domain-name.ts index 94d284c7f16c6..dca1a60bd4548 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/common/domain-name.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/common/domain-name.ts @@ -84,6 +84,10 @@ export class DomainName extends Resource implements IDomainName { constructor(scope: Construct, id: string, props: DomainNameProps) { super(scope, id); + if (props.domainName === '') { + throw new Error('empty string for domainName not allowed'); + } + const domainNameProps: CfnDomainNameProps = { domainName: props.domainName, domainNameConfigurations: [ diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/http/domain-name.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/http/domain-name.test.ts index dc64fbf5bf7c9..30c981a1da1d5 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/test/http/domain-name.test.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/test/http/domain-name.test.ts @@ -29,6 +29,22 @@ describe('DomainName', () => { }); }); + test('throws when domainName is empty string', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + const t = () => { + new DomainName(stack, 'DomainName', { + domainName: '', + certificate: Certificate.fromCertificateArn(stack, 'cert', certArn), + }); + }; + + // THEN + expect(t).toThrow(/empty string for domainName not allowed/); + }); + test('import domain name correctly', () => { // GIVEN const stack = new Stack(); From 305f683e86cca221705c0138572faa38043396eb Mon Sep 17 00:00:00 2001 From: Giedrius Kaskonas <57504623+gkaskonas@users.noreply.github.com> Date: Tue, 24 Aug 2021 15:47:49 +0100 Subject: [PATCH 10/39] feat(lambda): nodejs14.x supports inline code (#16131) Cloudformation now supports Inline code for NodeJS14 runtime. Updating CDK to reflect that ---- *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-lambda/lib/runtime.ts | 2 +- .../integ.runtime.inlinecode.expected.json | 55 +++++++++++++++++++ .../test/integ.runtime.inlinecode.ts | 7 +++ 3 files changed, 63 insertions(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-lambda/lib/runtime.ts b/packages/@aws-cdk/aws-lambda/lib/runtime.ts index e354e5862cc6e..ae26ca066e1c9 100644 --- a/packages/@aws-cdk/aws-lambda/lib/runtime.ts +++ b/packages/@aws-cdk/aws-lambda/lib/runtime.ts @@ -77,7 +77,7 @@ export class Runtime { /** * The NodeJS 14.x runtime (nodejs14.x) */ - public static readonly NODEJS_14_X = new Runtime('nodejs14.x', RuntimeFamily.NODEJS, { supportsInlineCode: false }); + public static readonly NODEJS_14_X = new Runtime('nodejs14.x', RuntimeFamily.NODEJS, { supportsInlineCode: true }); /** * The Python 2.7 runtime (python2.7) diff --git a/packages/@aws-cdk/aws-lambda/test/integ.runtime.inlinecode.expected.json b/packages/@aws-cdk/aws-lambda/test/integ.runtime.inlinecode.expected.json index 30d39828cc39d..59961a0755b84 100644 --- a/packages/@aws-cdk/aws-lambda/test/integ.runtime.inlinecode.expected.json +++ b/packages/@aws-cdk/aws-lambda/test/integ.runtime.inlinecode.expected.json @@ -299,6 +299,56 @@ "DependsOn": [ "PYTHON38ServiceRole3EA86BBE" ] + }, + "NODEJS14XServiceRole4523ECDB": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + } + }, + "NODEJS14X930214A3": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "ZipFile": "exports.handler = async function(event) { return \"success\" }" + }, + "Role": { + "Fn::GetAtt": [ + "NODEJS14XServiceRole4523ECDB", + "Arn" + ] + }, + "Handler": "index.handler", + "Runtime": "nodejs14.x" + }, + "DependsOn": [ + "NODEJS14XServiceRole4523ECDB" + ] } }, "Outputs": { @@ -331,6 +381,11 @@ "Value": { "Ref": "PYTHON38A180AE47" } + }, + "NODEJS14XfunctionName": { + "Value": { + "Ref": "NODEJS14X930214A3" + } } } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-lambda/test/integ.runtime.inlinecode.ts b/packages/@aws-cdk/aws-lambda/test/integ.runtime.inlinecode.ts index 56f5bd27f7746..3bde19e5cd22b 100644 --- a/packages/@aws-cdk/aws-lambda/test/integ.runtime.inlinecode.ts +++ b/packages/@aws-cdk/aws-lambda/test/integ.runtime.inlinecode.ts @@ -57,4 +57,11 @@ const python38 = new Function(stack, 'PYTHON_3_8', { }); new CfnOutput(stack, 'PYTHON_3_8-functionName', { value: python38.functionName }); +const node14xfn = new Function(stack, 'NODEJS_14_X', { + code: new InlineCode('exports.handler = async function(event) { return "success" }'), + handler: 'index.handler', + runtime: Runtime.NODEJS_14_X, +}); +new CfnOutput(stack, 'NODEJS_14_X-functionName', { value: node14xfn.functionName }); + app.synth(); From 64019bbf090e156261feb626a5a4bd7ff4f26545 Mon Sep 17 00:00:00 2001 From: Emil Rowland Date: Tue, 24 Aug 2021 17:28:27 +0200 Subject: [PATCH 11/39] feat(cognito): user pools - device tracking (#16055) fixes #15013 This pull request adds the support of device configuration for Cognito user pools. [DeviceConfiguration](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cognito-userpool-deviceconfiguration.html) ---- *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-cognito/README.md | 18 +++++++++++++ .../@aws-cdk/aws-cognito/lib/user-pool.ts | 27 +++++++++++++++++++ .../aws-cognito/test/user-pool.test.ts | 21 +++++++++++++++ 3 files changed, 66 insertions(+) diff --git a/packages/@aws-cdk/aws-cognito/README.md b/packages/@aws-cdk/aws-cognito/README.md index bdb93fc24d566..b015607589652 100644 --- a/packages/@aws-cdk/aws-cognito/README.md +++ b/packages/@aws-cdk/aws-cognito/README.md @@ -45,6 +45,7 @@ This module is part of the [AWS Cloud Development Kit](https://github.com/aws/aw - [Multi-factor Authentication](#multi-factor-authentication-mfa) - [Account Recovery Settings](#account-recovery-settings) - [Emails](#emails) + - [Device Tracking](#device-tracking) - [Lambda Triggers](#lambda-triggers) - [Trigger Permissions](#trigger-permissions) - [Import](#importing-user-pools) @@ -337,6 +338,23 @@ layer](https://docs.aws.amazon.com/cdk/latest/guide/cfn_layer.html) to configure If an email address contains non-ASCII characters, it will be encoded using the [punycode encoding](https://en.wikipedia.org/wiki/Punycode) when generating the template for Cloudformation. +### Device Tracking + +User pools can be configured to track devices that users have logged in to. +Read more at [Device Tracking](https://docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-user-pools-device-tracking.html) + +```ts +new cognito.UserPool(this, 'myuserpool', { + // ... + deviceTracking: { + challengeRequiredOnNewDevice: true, + deviceOnlyRememberedOnUserPrompt: true, + }, +}); +``` + +The default is to not track devices. + ### Lambda Triggers User pools can be configured such that AWS Lambda functions can be triggered when certain user operations or actions diff --git a/packages/@aws-cdk/aws-cognito/lib/user-pool.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool.ts index 9f598761ee843..6277ee0f467ee 100644 --- a/packages/@aws-cdk/aws-cognito/lib/user-pool.ts +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool.ts @@ -429,6 +429,26 @@ export enum AccountRecovery { NONE, } +/** + * Device tracking settings + * @see https://docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-user-pools-device-tracking.html + */ +export interface DeviceTracking { + /** + * Indicates whether a challenge is required on a new device. Only applicable to a new device. + * @see https://docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-user-pools-device-tracking.html + * @default false + */ + readonly challengeRequiredOnNewDevice: boolean; + + /** + * If true, a device is only remembered on user prompt. + * @see https://docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-user-pools-device-tracking.html + * @default false + */ + readonly deviceOnlyRememberedOnUserPrompt: boolean; +} + /** * Props for the UserPool construct */ @@ -581,6 +601,12 @@ export interface UserPoolProps { * @default RemovalPolicy.RETAIN */ readonly removalPolicy?: RemovalPolicy; + + /** + * Device tracking settings + * @default - see defaults on each property of DeviceTracking. + */ + readonly deviceTracking?: DeviceTracking; } /** @@ -787,6 +813,7 @@ export class UserPool extends UserPoolBase { caseSensitive: props.signInCaseSensitive, }), accountRecoverySetting: this.accountRecovery(props), + deviceConfiguration: props.deviceTracking, }); userPool.applyRemovalPolicy(props.removalPolicy); diff --git a/packages/@aws-cdk/aws-cognito/test/user-pool.test.ts b/packages/@aws-cdk/aws-cognito/test/user-pool.test.ts index 75ed3dcfa1a2d..0c61312222355 100644 --- a/packages/@aws-cdk/aws-cognito/test/user-pool.test.ts +++ b/packages/@aws-cdk/aws-cognito/test/user-pool.test.ts @@ -1390,6 +1390,27 @@ describe('User Pool', () => { }); }); +test('device tracking is configured correctly', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + new UserPool(stack, 'Pool', { + deviceTracking: { + challengeRequiredOnNewDevice: true, + deviceOnlyRememberedOnUserPrompt: true, + }, + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::Cognito::UserPool', { + DeviceConfiguration: { + ChallengeRequiredOnNewDevice: true, + DeviceOnlyRememberedOnUserPrompt: true, + }, + }); +}); + function fooFunction(scope: Construct, name: string): lambda.IFunction { return new lambda.Function(scope, name, { From 90f95e10f4dd9e4992731d6262dcfc767b65ab3f Mon Sep 17 00:00:00 2001 From: Niranjan Jayakar Date: Tue, 24 Aug 2021 17:08:35 +0100 Subject: [PATCH 12/39] feat(assertions): queries and assertions against the Outputs and Mappings sections (#15892) Introduce APIs `hasOutput()`, `findOutputs()`, `hasMapping()` and `findMappings()` to assert the `Mappings` and `Outputs` section of the CloudFormation template. Also, refactored the implementation of `hasResource()` to increase re-usability of its implementation across these new APIs. Migrated the modules `aws-kinesisfirehose` and `aws-neptune` that use these new APIs. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/assertions/README.md | 18 +- packages/@aws-cdk/assertions/lib/match.ts | 12 +- packages/@aws-cdk/assertions/lib/matcher.ts | 8 + .../assertions/lib/private/mappings.ts | 31 ++++ .../assertions/lib/private/outputs.ts | 31 ++++ .../assertions/lib/private/resource.ts | 82 --------- .../assertions/lib/private/resources.ts | 42 +++++ .../assertions/lib/private/section.ts | 58 ++++++ packages/@aws-cdk/assertions/lib/template.ts | 50 +++++- .../assertions/test/private/section.test.ts | 43 +++++ .../@aws-cdk/assertions/test/template.test.ts | 166 +++++++++++++++++- .../@aws-cdk/aws-kinesisfirehose/package.json | 2 +- .../test/delivery-stream.test.ts | 153 +++++++--------- packages/@aws-cdk/aws-neptune/package.json | 4 +- .../@aws-cdk/aws-neptune/test/cluster.test.ts | 49 +++--- .../aws-neptune/test/instance.test.ts | 30 ++-- .../aws-neptune/test/parameter-group.test.ts | 10 +- .../aws-neptune/test/subnet-group.test.ts | 10 +- 18 files changed, 566 insertions(+), 233 deletions(-) create mode 100644 packages/@aws-cdk/assertions/lib/private/mappings.ts create mode 100644 packages/@aws-cdk/assertions/lib/private/outputs.ts delete mode 100644 packages/@aws-cdk/assertions/lib/private/resource.ts create mode 100644 packages/@aws-cdk/assertions/lib/private/resources.ts create mode 100644 packages/@aws-cdk/assertions/lib/private/section.ts create mode 100644 packages/@aws-cdk/assertions/test/private/section.test.ts diff --git a/packages/@aws-cdk/assertions/README.md b/packages/@aws-cdk/assertions/README.md index bfeb893417c31..67109b1156964 100644 --- a/packages/@aws-cdk/assertions/README.md +++ b/packages/@aws-cdk/assertions/README.md @@ -107,11 +107,23 @@ By default, the `hasResource()` and `hasResourceProperties()` APIs perform deep partial object matching. This behavior can be configured using matchers. See subsequent section on [special matchers](#special-matchers). +## Other Sections + +Similar to the `hasResource()` and `findResources()`, we have equivalent methods +to check and find other sections of the CloudFormation resources. + +* Outputs - `hasOutput()` and `findOutputs()` +* Mapping - `hasMapping()` and `findMappings()` + +All of the defaults and behaviour documented for `hasResource()` and +`findResources()` apply to these methods. + ## Special Matchers -The expectation provided to the `hasResourceXXX()` methods, besides carrying -literal values, as seen in the above examples, can also have special matchers -encoded. +The expectation provided to the `hasXXX()` and `findXXX()` methods, besides +carrying literal values, as seen in the above examples, also accept special +matchers. + They are available as part of the `Match` class. ### Object Matchers diff --git a/packages/@aws-cdk/assertions/lib/match.ts b/packages/@aws-cdk/assertions/lib/match.ts index 0272f344efcbc..2a88bada59bac 100644 --- a/packages/@aws-cdk/assertions/lib/match.ts +++ b/packages/@aws-cdk/assertions/lib/match.ts @@ -99,7 +99,7 @@ class LiteralMatch extends Matcher { return new ObjectMatch(this.name, this.pattern, { partial: this.partialObjects }).test(actual); } - const result = new MatchResult(); + const result = new MatchResult(actual); if (typeof this.pattern !== typeof actual) { result.push(this, [], `Expected type ${typeof this.pattern} but received ${getType(actual)}`); return result; @@ -152,16 +152,16 @@ class ArrayMatch extends Matcher { public test(actual: any): MatchResult { if (!Array.isArray(actual)) { - return new MatchResult().push(this, [], `Expected type array but received ${getType(actual)}`); + return new MatchResult(actual).push(this, [], `Expected type array but received ${getType(actual)}`); } if (!this.partial && this.pattern.length !== actual.length) { - return new MatchResult().push(this, [], `Expected array of length ${this.pattern.length} but received ${actual.length}`); + return new MatchResult(actual).push(this, [], `Expected array of length ${this.pattern.length} but received ${actual.length}`); } let patternIdx = 0; let actualIdx = 0; - const result = new MatchResult(); + const result = new MatchResult(actual); while (patternIdx < this.pattern.length && actualIdx < actual.length) { const patternElement = this.pattern[patternIdx]; const matcher = Matcher.isMatcher(patternElement) ? patternElement : new LiteralMatch(this.name, patternElement); @@ -220,10 +220,10 @@ class ObjectMatch extends Matcher { public test(actual: any): MatchResult { if (typeof actual !== 'object' || Array.isArray(actual)) { - return new MatchResult().push(this, [], `Expected type object but received ${getType(actual)}`); + return new MatchResult(actual).push(this, [], `Expected type object but received ${getType(actual)}`); } - const result = new MatchResult(); + const result = new MatchResult(actual); if (!this.partial) { for (const a of Object.keys(actual)) { if (!(a in this.pattern)) { diff --git a/packages/@aws-cdk/assertions/lib/matcher.ts b/packages/@aws-cdk/assertions/lib/matcher.ts index 0495b72d25c5a..a3263d6f00829 100644 --- a/packages/@aws-cdk/assertions/lib/matcher.ts +++ b/packages/@aws-cdk/assertions/lib/matcher.ts @@ -27,8 +27,16 @@ export abstract class Matcher { * The result of `Match.test()`. */ export class MatchResult { + /** + * The target for which this result was generated. + */ + public readonly target: any; private readonly failures: MatchFailure[] = []; + constructor(target: any) { + this.target = target; + } + /** * Push a new failure into this result at a specific path. * If the failure occurred at root of the match tree, set the path to an empty list. diff --git a/packages/@aws-cdk/assertions/lib/private/mappings.ts b/packages/@aws-cdk/assertions/lib/private/mappings.ts new file mode 100644 index 0000000000000..0def435cc0e1d --- /dev/null +++ b/packages/@aws-cdk/assertions/lib/private/mappings.ts @@ -0,0 +1,31 @@ +import { StackInspector } from '../vendored/assert'; +import { formatFailure, matchSection } from './section'; + +export function findMappings(inspector: StackInspector, props: any = {}): { [key: string]: any }[] { + const section: { [key: string] : {} } = inspector.value.Mappings; + const result = matchSection(section, props); + + if (!result.match) { + return []; + } + + return result.matches; +} + +export function hasMapping(inspector: StackInspector, props: any): string | void { + const section: { [key: string]: {} } = inspector.value.Mappings; + const result = matchSection(section, props); + + if (result.match) { + return; + } + + if (result.closestResult === undefined) { + return 'No mappings found in the template'; + } + + return [ + `Template has ${result.analyzedCount} mappings, but none match as expected.`, + formatFailure(result.closestResult), + ].join('\n'); +} \ No newline at end of file diff --git a/packages/@aws-cdk/assertions/lib/private/outputs.ts b/packages/@aws-cdk/assertions/lib/private/outputs.ts new file mode 100644 index 0000000000000..46e5a6cb1d52b --- /dev/null +++ b/packages/@aws-cdk/assertions/lib/private/outputs.ts @@ -0,0 +1,31 @@ +import { StackInspector } from '../vendored/assert'; +import { formatFailure, matchSection } from './section'; + +export function findOutputs(inspector: StackInspector, props: any = {}): { [key: string]: any }[] { + const section: { [key: string] : {} } = inspector.value.Outputs; + const result = matchSection(section, props); + + if (!result.match) { + return []; + } + + return result.matches; +} + +export function hasOutput(inspector: StackInspector, props: any): string | void { + const section: { [key: string]: {} } = inspector.value.Outputs; + const result = matchSection(section, props); + + if (result.match) { + return; + } + + if (result.closestResult === undefined) { + return 'No outputs found in the template'; + } + + return [ + `Template has ${result.analyzedCount} outputs, but none match as expected.`, + formatFailure(result.closestResult), + ].join('\n'); +} \ No newline at end of file diff --git a/packages/@aws-cdk/assertions/lib/private/resource.ts b/packages/@aws-cdk/assertions/lib/private/resource.ts deleted file mode 100644 index 22a2b01734b70..0000000000000 --- a/packages/@aws-cdk/assertions/lib/private/resource.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { Match } from '../match'; -import { Matcher, MatchResult } from '../matcher'; -import { StackInspector } from '../vendored/assert'; - -export function findResources(inspector: StackInspector, type: string, props: any = {}): { [key: string]: any }[] { - const matcher = Matcher.isMatcher(props) ? props : Match.objectLike(props); - let results: { [key: string]: any }[] = []; - - eachResourceWithType(inspector, type, (resource) => { - const result = matcher.test(resource); - if (!result.hasFailed()) { - results.push(resource); - } - }); - - return results; -} - -export function hasResource(inspector: StackInspector, type: string, props: any): string | void { - const matcher = Matcher.isMatcher(props) ? props : Match.objectLike(props); - let closestResult: MatchResult | undefined = undefined; - let closestResource: { [key: string]: any } | undefined = undefined; - let count: number = 0; - - let match = false; - eachResourceWithType(inspector, type, (resource) => { - if (match) { return; } - count++; - const result = matcher.test(resource); - if (!result.hasFailed()) { - match = true; - } - if (closestResult === undefined || closestResult.failCount > result.failCount) { - closestResult = result; - closestResource = resource; - } - }); - - if (match) { - return; - } - - if (closestResult === undefined) { - return `No resource with type ${type} found`; - } - - if (!closestResource) { - throw new Error('unexpected: closestResult is null'); - } - - return [ - `${count} resources with type ${type} found, but none match as expected.`, - formatMessage(closestResult, closestResource), - ].join('\n'); -} - -function eachResourceWithType( - inspector: StackInspector, - type: string, - cb: (resource: {[key: string]: any}) => void): void { - - for (const logicalId of Object.keys(inspector.value.Resources ?? {})) { - const resource: { [key: string]: any } = inspector.value.Resources[logicalId]; - if (resource.Type === type) { - cb(resource); - } - } -} - -function formatMessage(closestResult: MatchResult, closestResource: {}): string { - return [ - 'The closest result is:', - reindent(JSON.stringify(closestResource, undefined, 2)), - 'with the following mismatches:', - ...closestResult.toHumanStrings().map(s => `\t${s}`), - ].join('\n'); -} - -function reindent(x: string, indent: number = 2): string { - const pad = ' '.repeat(indent); - return pad + x.split('\n').join(`\n${pad}`); -} \ No newline at end of file diff --git a/packages/@aws-cdk/assertions/lib/private/resources.ts b/packages/@aws-cdk/assertions/lib/private/resources.ts new file mode 100644 index 0000000000000..31286a8a6f1a4 --- /dev/null +++ b/packages/@aws-cdk/assertions/lib/private/resources.ts @@ -0,0 +1,42 @@ +import { StackInspector } from '../vendored/assert'; +import { formatFailure, matchSection } from './section'; + +// Partial type for CloudFormation Resource +type Resource = { + Type: string; +} + +export function findResources(inspector: StackInspector, type: string, props: any = {}): { [key: string]: any }[] { + const section: { [key: string] : Resource } = inspector.value.Resources; + const result = matchSection(filterType(section, type), props); + + if (!result.match) { + return []; + } + + return result.matches; +} + +export function hasResource(inspector: StackInspector, type: string, props: any): string | void { + const section: { [key: string]: Resource } = inspector.value.Resources; + const result = matchSection(filterType(section, type), props); + + if (result.match) { + return; + } + + if (result.closestResult === undefined) { + return `No resource with type ${type} found`; + } + + return [ + `Template has ${result.analyzedCount} resources with type ${type}, but none match as expected.`, + formatFailure(result.closestResult), + ].join('\n'); +} + +function filterType(section: { [key: string]: Resource }, type: string): { [key: string]: Resource } { + return Object.entries(section ?? {}) + .filter(([_, v]) => v.Type === type) + .reduce((agg, [k, v]) => { return { ...agg, [k]: v }; }, {}); +} \ No newline at end of file diff --git a/packages/@aws-cdk/assertions/lib/private/section.ts b/packages/@aws-cdk/assertions/lib/private/section.ts new file mode 100644 index 0000000000000..d8f0123de20d6 --- /dev/null +++ b/packages/@aws-cdk/assertions/lib/private/section.ts @@ -0,0 +1,58 @@ +import { Match } from '../match'; +import { Matcher, MatchResult } from '../matcher'; + +export type MatchSuccess = { match: true, matches: any[] }; +export type MatchFailure = { match: false, closestResult?: MatchResult, analyzedCount: number }; + +export function matchSection(section: any, props: any): MatchSuccess | MatchFailure { + const matcher = Matcher.isMatcher(props) ? props : Match.objectLike(props); + let closestResult: MatchResult | undefined = undefined; + let matching: any[] = []; + let count = 0; + + eachEntryInSection( + section, + + (entry) => { + const result = matcher.test(entry); + if (!result.hasFailed()) { + matching.push(entry); + } else { + count++; + if (closestResult === undefined || closestResult.failCount > result.failCount) { + closestResult = result; + } + } + }, + ); + + if (matching.length > 0) { + return { match: true, matches: matching }; + } else { + return { match: false, closestResult, analyzedCount: count }; + } +} + +function eachEntryInSection( + section: any, + cb: (entry: {[key: string]: any}) => void): void { + + for (const logicalId of Object.keys(section ?? {})) { + const resource: { [key: string]: any } = section[logicalId]; + cb(resource); + } +} + +export function formatFailure(closestResult: MatchResult): string { + return [ + 'The closest result is:', + leftPad(JSON.stringify(closestResult.target, undefined, 2)), + 'with the following mismatches:', + ...closestResult.toHumanStrings().map(s => `\t${s}`), + ].join('\n'); +} + +function leftPad(x: string, indent: number = 2): string { + const pad = ' '.repeat(indent); + return pad + x.split('\n').join(`\n${pad}`); +} \ No newline at end of file diff --git a/packages/@aws-cdk/assertions/lib/template.ts b/packages/@aws-cdk/assertions/lib/template.ts index 480290bc45b04..848c46bcc295a 100644 --- a/packages/@aws-cdk/assertions/lib/template.ts +++ b/packages/@aws-cdk/assertions/lib/template.ts @@ -1,7 +1,9 @@ import { Stack, Stage } from '@aws-cdk/core'; import { Match } from './match'; import { Matcher } from './matcher'; -import { findResources, hasResource } from './private/resource'; +import { findMappings, hasMapping } from './private/mappings'; +import { findOutputs, hasOutput } from './private/outputs'; +import { findResources, hasResource } from './private/resources'; import * as assert from './vendored/assert'; /** @@ -103,6 +105,52 @@ export class Template { return findResources(this.inspector, type, props); } + /** + * Assert that an Output with the given properties exists in the CloudFormation template. + * By default, performs partial matching on the resource, via the `Match.objectLike()`. + * To configure different behavour, use other matchers in the `Match` class. + * @param props the output as should be expected in the template. + */ + public hasOutput(props: any): void { + const matchError = hasOutput(this.inspector, props); + if (matchError) { + throw new Error(matchError); + } + } + + /** + * Get the set of matching Outputs that match the given properties in the CloudFormation template. + * @param props by default, matches all Outputs in the template. + * When a literal object is provided, performs a partial match via `Match.objectLike()`. + * Use the `Match` APIs to configure a different behaviour. + */ + public findOutputs(props: any = {}): { [key: string]: any }[] { + return findOutputs(this.inspector, props); + } + + /** + * Assert that a Mapping with the given properties exists in the CloudFormation template. + * By default, performs partial matching on the resource, via the `Match.objectLike()`. + * To configure different behavour, use other matchers in the `Match` class. + * @param props the output as should be expected in the template. + */ + public hasMapping(props: any): void { + const matchError = hasMapping(this.inspector, props); + if (matchError) { + throw new Error(matchError); + } + } + + /** + * Get the set of matching Mappings that match the given properties in the CloudFormation template. + * @param props by default, matches all Mappings in the template. + * When a literal object is provided, performs a partial match via `Match.objectLike()`. + * Use the `Match` APIs to configure a different behaviour. + */ + public findMappings(props: any = {}): { [key: string]: any }[] { + return findMappings(this.inspector, props); + } + /** * Assert that the CloudFormation template matches the given value * @param expected the expected CloudFormation template as key-value pairs. diff --git a/packages/@aws-cdk/assertions/test/private/section.test.ts b/packages/@aws-cdk/assertions/test/private/section.test.ts new file mode 100644 index 0000000000000..20cc2d5d14e0b --- /dev/null +++ b/packages/@aws-cdk/assertions/test/private/section.test.ts @@ -0,0 +1,43 @@ +import { Match } from '../../lib'; +import { MatchFailure, matchSection, MatchSuccess } from '../../lib/private/section'; + +describe('section', () => { + describe('matchSection', () => { + test('success', () => { + // GIVEN + const matcher = Match.objectLike({ foo: 'bar' }); + const section = { + Entry1: { foo: 'bar' }, + Entry2: { foo: 'bar', baz: 'qux' }, + Entry3: { fred: 'waldo' }, + }; + + // WHEN + const result = matchSection(section, matcher); + + // THEN + expect(result.match).toEqual(true); + const success = result as MatchSuccess; + expect(success.matches.length).toEqual(2); + }); + + test('failure', () => { + // GIVEN + const matcher = Match.objectLike({ foo: 'bar' }); + const section = { + Entry1: { foo: 'qux' }, + Entry3: { fred: 'waldo' }, + }; + + // WHEN + const result = matchSection(section, matcher); + + // THEN + expect(result.match).toEqual(false); + const success = result as MatchFailure; + expect(success.analyzedCount).toEqual(2); + expect(success.closestResult).toBeDefined(); + expect(success.closestResult?.target).toEqual({ foo: 'qux' }); + }); + }); +}); \ No newline at end of file diff --git a/packages/@aws-cdk/assertions/test/template.test.ts b/packages/@aws-cdk/assertions/test/template.test.ts index fcbc199f1f2da..50fb60a1a27f7 100644 --- a/packages/@aws-cdk/assertions/test/template.test.ts +++ b/packages/@aws-cdk/assertions/test/template.test.ts @@ -1,4 +1,4 @@ -import { App, CfnResource, Stack } from '@aws-cdk/core'; +import { App, CfnMapping, CfnOutput, CfnResource, Stack } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { Match, Template } from '../lib'; @@ -330,6 +330,170 @@ describe('Template', () => { expect(inspect.findResources('Foo::Bar').length).toEqual(2); }); }); + + describe('hasOutput', () => { + test('matching', () => { + const stack = new Stack(); + new CfnOutput(stack, 'Foo', { + value: 'Bar', + }); + new CfnOutput(stack, 'Fred', { + value: 'Waldo', + }); + + const inspect = Template.fromStack(stack); + expect(() => inspect.hasOutput({ Value: 'Bar' })).not.toThrow(); + }); + + test('not matching', (done) => { + const stack = new Stack(); + new CfnOutput(stack, 'Foo', { + value: 'Bar', + exportName: 'ExportBar', + }); + new CfnOutput(stack, 'Fred', { + value: 'Waldo', + }); + + const inspect = Template.fromStack(stack); + expectToThrow( + () => inspect.hasOutput({ + Value: 'Bar', + Export: { Name: 'ExportBaz' }, + }), + [ + /2 outputs/, + /Expected ExportBaz but received ExportBar/, + ], + done, + ); + done(); + }); + }); + + describe('findOutputs', () => { + test('matching', () => { + const stack = new Stack(); + new CfnOutput(stack, 'Foo', { + value: 'Fred', + description: 'FooFred', + }); + new CfnOutput(stack, 'Bar', { + value: 'Fred', + description: 'BarFred', + }); + new CfnOutput(stack, 'Baz', { + value: 'Waldo', + description: 'BazWaldo', + }); + + const inspect = Template.fromStack(stack); + const result = inspect.findOutputs({ Value: 'Fred' }); + expect(result).toEqual([ + { Value: 'Fred', Description: 'FooFred' }, + { Value: 'Fred', Description: 'BarFred' }, + ]); + }); + + test('not matching', () => { + const stack = new Stack(); + new CfnOutput(stack, 'Foo', { + value: 'Fred', + }); + + const inspect = Template.fromStack(stack); + const result = inspect.findOutputs({ Value: 'Waldo' }); + expect(result.length).toEqual(0); + }); + }); + + describe('hasMapping', () => { + test('matching', () => { + const stack = new Stack(); + new CfnMapping(stack, 'Foo', { + mapping: { + Foo: { Bar: 'Lightning', Fred: 'Waldo' }, + Baz: { Bar: 'Qux' }, + }, + }); + new CfnMapping(stack, 'Fred', { + mapping: { + Foo: { Bar: 'Lightning' }, + }, + }); + + const inspect = Template.fromStack(stack); + expect(() => inspect.hasMapping({ Foo: { Bar: 'Lightning' } })).not.toThrow(); + }); + + test('not matching', (done) => { + const stack = new Stack(); + new CfnMapping(stack, 'Foo', { + mapping: { + Foo: { Bar: 'Fred', Baz: 'Waldo' }, + Qux: { Bar: 'Fred' }, + }, + }); + new CfnMapping(stack, 'Fred', { + mapping: { + Foo: { Baz: 'Baz' }, + }, + }); + + const inspect = Template.fromStack(stack); + expectToThrow( + () => inspect.hasMapping({ + Foo: { Bar: 'Qux' }, + }), + [ + /2 mappings/, + /Expected Qux but received Fred/, + ], + done, + ); + done(); + }); + }); + + describe('findMappings', () => { + test('matching', () => { + const stack = new Stack(); + new CfnMapping(stack, 'Foo', { + mapping: { + Foo: { Bar: 'Lightning', Fred: 'Waldo' }, + Baz: { Bar: 'Qux' }, + }, + }); + new CfnMapping(stack, 'Fred', { + mapping: { + Foo: { Bar: 'Lightning' }, + }, + }); + + const inspect = Template.fromStack(stack); + const result = inspect.findMappings({ Foo: { Bar: 'Lightning' } }); + expect(result).toEqual([ + { + Foo: { Bar: 'Lightning', Fred: 'Waldo' }, + Baz: { Bar: 'Qux' }, + }, + { Foo: { Bar: 'Lightning' } }, + ]); + }); + + test('not matching', () => { + const stack = new Stack(); + new CfnMapping(stack, 'Foo', { + mapping: { + Foo: { Bar: 'Fred', Baz: 'Waldo' }, + }, + }); + + const inspect = Template.fromStack(stack); + const result = inspect.findMappings({ Foo: { Bar: 'Waldo' } }); + expect(result.length).toEqual(0); + }); + }); }); function expectToThrow(fn: () => void, msgs: (RegExp | string)[], done: jest.DoneCallback): void { diff --git a/packages/@aws-cdk/aws-kinesisfirehose/package.json b/packages/@aws-cdk/aws-kinesisfirehose/package.json index 58c5568c8c299..fe73affa84c97 100644 --- a/packages/@aws-cdk/aws-kinesisfirehose/package.json +++ b/packages/@aws-cdk/aws-kinesisfirehose/package.json @@ -73,7 +73,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@aws-cdk/assert-internal": "0.0.0", + "@aws-cdk/assertions": "0.0.0", "@aws-cdk/aws-s3": "0.0.0", "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", diff --git a/packages/@aws-cdk/aws-kinesisfirehose/test/delivery-stream.test.ts b/packages/@aws-cdk/aws-kinesisfirehose/test/delivery-stream.test.ts index 345e7feb0774a..7c36a29e379b5 100644 --- a/packages/@aws-cdk/aws-kinesisfirehose/test/delivery-stream.test.ts +++ b/packages/@aws-cdk/aws-kinesisfirehose/test/delivery-stream.test.ts @@ -1,5 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; -import { ABSENT, ResourcePart, SynthUtils, anything, arrayWith } from '@aws-cdk/assert-internal'; +import { Match, Template } from '@aws-cdk/assertions'; import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as iam from '@aws-cdk/aws-iam'; @@ -47,11 +46,11 @@ describe('delivery stream', () => { destinations: [mockS3Destination], }); - expect(stack).toHaveResource('AWS::KinesisFirehose::DeliveryStream', { - DeliveryStreamEncryptionConfigurationInput: ABSENT, - DeliveryStreamName: ABSENT, + Template.fromStack(stack).hasResourceProperties('AWS::KinesisFirehose::DeliveryStream', { + DeliveryStreamEncryptionConfigurationInput: Match.absentProperty(), + DeliveryStreamName: Match.absentProperty(), DeliveryStreamType: 'DirectPut', - KinesisStreamSourceConfiguration: ABSENT, + KinesisStreamSourceConfiguration: Match.absentProperty(), ExtendedS3DestinationConfiguration: { BucketARN: bucketArn, RoleARN: roleArn, @@ -77,14 +76,14 @@ describe('delivery stream', () => { destinations: [mockS3Destination], }); - expect(stack).toHaveResourceLike('AWS::IAM::Role', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { AssumeRolePolicyDocument: { Statement: [ - { + Match.objectLike({ Principal: { Service: 'firehose.amazonaws.com', }, - }, + }), ], }, }); @@ -99,32 +98,33 @@ describe('delivery stream', () => { role: deliveryStreamRole, }); - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { - Action: arrayWith( - 'kinesis:DescribeStream', + Effect: 'Allow', + Action: Match.arrayWith([ 'kinesis:GetRecords', 'kinesis:GetShardIterator', 'kinesis:ListShards', - ), + 'kinesis:DescribeStream', + ]), Resource: stack.resolve(sourceStream.streamArn), }, ], }, Roles: [stack.resolve(deliveryStreamRole.roleName)], }); - expect(stack).toHaveResource('AWS::KinesisFirehose::DeliveryStream', { + Template.fromStack(stack).hasResourceProperties('AWS::KinesisFirehose::DeliveryStream', { DeliveryStreamType: 'KinesisStreamAsSource', KinesisStreamSourceConfiguration: { KinesisStreamARN: stack.resolve(sourceStream.streamArn), RoleARN: stack.resolve(deliveryStreamRole.roleArn), }, }); - expect(stack).toHaveResourceLike('AWS::KinesisFirehose::DeliveryStream', { - DependsOn: arrayWith('DeliveryStreamRoleDefaultPolicy2759968B'), - }, ResourcePart.CompleteDefinition); + Template.fromStack(stack).hasResource('AWS::KinesisFirehose::DeliveryStream', { + DependsOn: Match.arrayWith(['DeliveryStreamRoleDefaultPolicy2759968B']), + }); }); test('requesting customer-owned encryption creates key and configuration', () => { @@ -134,21 +134,21 @@ describe('delivery stream', () => { role: deliveryStreamRole, }); - expect(stack).toHaveResource('AWS::KMS::Key'); - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).resourceCountIs('AWS::KMS::Key', 1); + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ - { - Action: arrayWith( - 'kms:Encrypt', + Match.objectLike({ + Action: Match.arrayWith([ 'kms:Decrypt', - ), - }, + 'kms:Encrypt', + ]), + }), ], }, Roles: [stack.resolve(deliveryStreamRole.roleName)], }); - expect(stack).toHaveResourceLike('AWS::KinesisFirehose::DeliveryStream', { + Template.fromStack(stack).hasResourceProperties('AWS::KinesisFirehose::DeliveryStream', { DeliveryStreamEncryptionConfigurationInput: { KeyARN: { 'Fn::GetAtt': [ @@ -170,22 +170,22 @@ describe('delivery stream', () => { role: deliveryStreamRole, }); - expect(stack).toHaveResource('AWS::KMS::Key'); - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).resourceCountIs('AWS::KMS::Key', 1); + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ - { - Action: arrayWith( - 'kms:Encrypt', + Match.objectLike({ + Action: Match.arrayWith([ 'kms:Decrypt', - ), + 'kms:Encrypt', + ]), Resource: stack.resolve(key.keyArn), - }, + }), ], }, Roles: [stack.resolve(deliveryStreamRole.roleName)], }); - expect(stack).toHaveResourceLike('AWS::KinesisFirehose::DeliveryStream', { + Template.fromStack(stack).hasResourceProperties('AWS::KinesisFirehose::DeliveryStream', { DeliveryStreamEncryptionConfigurationInput: { KeyARN: stack.resolve(key.keyArn), KeyType: 'CUSTOMER_MANAGED_CMK', @@ -200,24 +200,12 @@ describe('delivery stream', () => { role: deliveryStreamRole, }); - expect(stack).not.toHaveResource('AWS::KMS::Key'); - expect(stack).not.toHaveResourceLike('AWS::IAM::Policy', { - PolicyDocument: { - Statement: [ - { - Action: arrayWith( - 'kms:Encrypt', - 'kms:Decrypt', - ), - }, - ], - }, - Roles: [stack.resolve(deliveryStreamRole.roleName)], - }); - expect(stack).toHaveResourceLike('AWS::KinesisFirehose::DeliveryStream', { + Template.fromStack(stack).resourceCountIs('AWS::KMS::Key', 0); + Template.fromStack(stack).resourceCountIs('AWS::IAM::Policy', 0); + Template.fromStack(stack).hasResourceProperties('AWS::KinesisFirehose::DeliveryStream', { DeliveryStreamType: 'DirectPut', DeliveryStreamEncryptionConfigurationInput: { - KeyARN: ABSENT, + KeyARN: Match.absentProperty(), KeyType: 'AWS_OWNED_CMK', }, }); @@ -230,23 +218,11 @@ describe('delivery stream', () => { role: deliveryStreamRole, }); - expect(stack).not.toHaveResource('AWS::KMS::Key'); - expect(stack).not.toHaveResourceLike('AWS::IAM::Policy', { - PolicyDocument: { - Statement: [ - { - Action: arrayWith( - 'kms:Encrypt', - 'kms:Decrypt', - ), - }, - ], - }, - Roles: [stack.resolve(deliveryStreamRole.roleName)], - }); - expect(stack).toHaveResourceLike('AWS::KinesisFirehose::DeliveryStream', { + Template.fromStack(stack).resourceCountIs('AWS::KMS::Key', 0); + Template.fromStack(stack).resourceCountIs('AWS::IAM::Policy', 0); + Template.fromStack(stack).hasResourceProperties('AWS::KinesisFirehose::DeliveryStream', { DeliveryStreamType: 'DirectPut', - DeliveryStreamEncryptionConfigurationInput: ABSENT, + DeliveryStreamEncryptionConfigurationInput: Match.absentProperty(), }); }); @@ -300,13 +276,13 @@ describe('delivery stream', () => { deliveryStream.grant(role, 'firehose:PutRecord'); - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ - { + Match.objectLike({ Action: 'firehose:PutRecord', Resource: stack.resolve(deliveryStream.deliveryStreamArn), - }, + }), ], }, Roles: [stack.resolve(role.roleName)], @@ -323,16 +299,16 @@ describe('delivery stream', () => { deliveryStream.grantPutRecords(role); - expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ - { + Match.objectLike({ Action: [ 'firehose:PutRecord', 'firehose:PutRecordBatch', ], Resource: stack.resolve(deliveryStream.deliveryStreamArn), - }, + }), ], }, Roles: [stack.resolve(role.roleName)], @@ -346,12 +322,12 @@ describe('delivery stream', () => { destinations: [mockS3Destination], }); - expect(stack).toHaveResourceLike('AWS::KinesisFirehose::DeliveryStream', { + Template.fromStack(stack).hasResource('AWS::KinesisFirehose::DeliveryStream', { DependsOn: [dependableId], - }, ResourcePart.CompleteDefinition); - expect(stack).toHaveResourceLike('AWS::IAM::Role', { - DependsOn: ABSENT, - }, ResourcePart.CompleteDefinition); + }); + Template.fromStack(stack).hasResource('AWS::IAM::Role', { + DependsOn: Match.absentProperty(), + }); }); test('supplying 0 or multiple destinations throws', () => { @@ -472,19 +448,18 @@ describe('delivery stream', () => { securityGroup.connections.allowFrom(deliveryStream, ec2.Port.allTcp()); - expect(stack).toHaveResourceLike('AWS::EC2::SecurityGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroup', { SecurityGroupIngress: [ - { + Match.objectLike({ CidrIp: { - 'Fn::FindInMap': [ - anything(), + 'Fn::FindInMap': Match.arrayWith([ { Ref: 'AWS::Region', }, 'FirehoseCidrBlock', - ], + ]), }, - }, + }), ], }); }); @@ -499,11 +474,11 @@ describe('delivery stream', () => { securityGroup.connections.allowFrom(deliveryStream, ec2.Port.allTcp()); - expect(stack).toHaveResourceLike('AWS::EC2::SecurityGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroup', { SecurityGroupIngress: [ - { + Match.objectLike({ CidrIp: '13.57.135.192/27', - }, + }), ], }); }); @@ -516,7 +491,11 @@ describe('delivery stream', () => { destinations: [mockS3Destination], }); - expect(Object.keys(SynthUtils.toCloudFormation(stack).Mappings).length).toBe(1); + Template.fromStack(stack).hasMapping({ + 'af-south-1': { + FirehoseCidrBlock: '13.244.121.224/27', + }, + }); }); test('can add tags', () => { @@ -526,7 +505,7 @@ describe('delivery stream', () => { cdk.Tags.of(deliveryStream).add('tagKey', 'tagValue'); - expect(stack).toHaveResource('AWS::KinesisFirehose::DeliveryStream', { + Template.fromStack(stack).hasResourceProperties('AWS::KinesisFirehose::DeliveryStream', { Tags: [ { Key: 'tagKey', diff --git a/packages/@aws-cdk/aws-neptune/package.json b/packages/@aws-cdk/aws-neptune/package.json index dfc50d27ac70d..b3a429fb6642a 100644 --- a/packages/@aws-cdk/aws-neptune/package.json +++ b/packages/@aws-cdk/aws-neptune/package.json @@ -74,12 +74,12 @@ }, "license": "Apache-2.0", "devDependencies": { + "@aws-cdk/assertions": "0.0.0", "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", - "pkglint": "0.0.0", - "@aws-cdk/assert-internal": "0.0.0" + "pkglint": "0.0.0" }, "dependencies": { "@aws-cdk/aws-ec2": "0.0.0", diff --git a/packages/@aws-cdk/aws-neptune/test/cluster.test.ts b/packages/@aws-cdk/aws-neptune/test/cluster.test.ts index 24e61c8be34a1..74f4770a8a74c 100644 --- a/packages/@aws-cdk/aws-neptune/test/cluster.test.ts +++ b/packages/@aws-cdk/aws-neptune/test/cluster.test.ts @@ -1,5 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; -import { ABSENT, ResourcePart } from '@aws-cdk/assert-internal'; +import { Match, Template } from '@aws-cdk/assertions'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as iam from '@aws-cdk/aws-iam'; import * as kms from '@aws-cdk/aws-kms'; @@ -21,7 +20,7 @@ describe('DatabaseCluster', () => { }); // THEN - expect(stack).toHaveResource('AWS::Neptune::DBCluster', { + Template.fromStack(stack).hasResource('AWS::Neptune::DBCluster', { Properties: { DBSubnetGroupName: { Ref: 'DatabaseSubnets3C9252C9' }, VpcSecurityGroupIds: [{ 'Fn::GetAtt': ['DatabaseSecurityGroup5C91FDCB', 'GroupId'] }], @@ -29,14 +28,14 @@ describe('DatabaseCluster', () => { }, DeletionPolicy: 'Retain', UpdateReplacePolicy: 'Retain', - }, ResourcePart.CompleteDefinition); + }); - expect(stack).toHaveResource('AWS::Neptune::DBInstance', { + Template.fromStack(stack).hasResource('AWS::Neptune::DBInstance', { DeletionPolicy: 'Retain', UpdateReplacePolicy: 'Retain', - }, ResourcePart.CompleteDefinition); + }); - expect(stack).toHaveResource('AWS::Neptune::DBSubnetGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::Neptune::DBSubnetGroup', { SubnetIds: [ { Ref: 'VPCPrivateSubnet1Subnet8BCA10E0' }, { Ref: 'VPCPrivateSubnet2SubnetCFCDAA7A' }, @@ -58,7 +57,7 @@ describe('DatabaseCluster', () => { }); // THEN - expect(stack).toHaveResource('AWS::Neptune::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::Neptune::DBCluster', { DBSubnetGroupName: { Ref: 'DatabaseSubnets3C9252C9' }, VpcSecurityGroupIds: [{ 'Fn::GetAtt': ['DatabaseSecurityGroup5C91FDCB', 'GroupId'] }], }); @@ -112,7 +111,7 @@ describe('DatabaseCluster', () => { }); // THEN - expect(stack).toHaveResource('AWS::Neptune::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::Neptune::DBCluster', { EngineVersion: '1.0.4.1', DBSubnetGroupName: { Ref: 'DatabaseSubnets3C9252C9' }, VpcSecurityGroupIds: [{ 'Fn::GetAtt': ['DatabaseSecurityGroup5C91FDCB', 'GroupId'] }], @@ -136,7 +135,7 @@ describe('DatabaseCluster', () => { }); // THEN - expect(stack).toHaveResource('AWS::Neptune::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::Neptune::DBCluster', { DBSubnetGroupName: { Ref: 'DatabaseSubnets3C9252C9' }, VpcSecurityGroupIds: ['SecurityGroupId12345'], }); @@ -161,7 +160,7 @@ describe('DatabaseCluster', () => { }); // THEN - expect(stack).toHaveResource('AWS::Neptune::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::Neptune::DBCluster', { DBClusterParameterGroupName: { Ref: 'ParamsA8366201' }, }); }); @@ -184,7 +183,7 @@ describe('DatabaseCluster', () => { }); // THEN - expect(stack).toHaveResource('AWS::Neptune::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::Neptune::DBCluster', { AssociatedRoles: [ { RoleArn: { @@ -213,7 +212,7 @@ describe('DatabaseCluster', () => { }); // THEN - expect(stack).toHaveResource('AWS::Neptune::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::Neptune::DBCluster', { DBClusterParameterGroupName: 'ParamGroupName', }); }); @@ -231,7 +230,7 @@ describe('DatabaseCluster', () => { }); // THEN - expect(stack).toHaveResource('AWS::Neptune::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::Neptune::DBCluster', { KmsKeyId: { 'Fn::GetAtt': [ 'Key961B73FD', @@ -254,7 +253,7 @@ describe('DatabaseCluster', () => { }); // THEN - expect(stack).toHaveResource('AWS::Neptune::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::Neptune::DBCluster', { StorageEncrypted: true, }); }); @@ -307,7 +306,7 @@ describe('DatabaseCluster', () => { }); // THEN - expect(stack).toHaveResource('AWS::Neptune::DBInstance', { + Template.fromStack(stack).hasResourceProperties('AWS::Neptune::DBInstance', { DBInstanceIdentifier: `${instanceIdentifierBase}1`, }); }); @@ -326,7 +325,7 @@ describe('DatabaseCluster', () => { }); // THEN - expect(stack).toHaveResource('AWS::Neptune::DBInstance', { + Template.fromStack(stack).hasResourceProperties('AWS::Neptune::DBInstance', { DBInstanceIdentifier: `${clusterIdentifier}instance1`, }); }); @@ -373,7 +372,7 @@ describe('DatabaseCluster', () => { cluster.connections.allowToAnyIpv4(ec2.Port.tcp(443)); // THEN - expect(stack).toHaveResource('AWS::EC2::SecurityGroupEgress', { + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroupEgress', { GroupId: 'sg-123456789', }); }); @@ -391,7 +390,7 @@ describe('DatabaseCluster', () => { }); // THEN - expect(stack).toHaveResource('AWS::Neptune::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::Neptune::DBCluster', { BackupRetentionPeriod: 20, }); }); @@ -410,7 +409,7 @@ describe('DatabaseCluster', () => { }); // THEN - expect(stack).toHaveResource('AWS::Neptune::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::Neptune::DBCluster', { BackupRetentionPeriod: 20, PreferredBackupWindow: '07:34-08:04', }); @@ -429,7 +428,7 @@ describe('DatabaseCluster', () => { }); // THEN - expect(stack).toHaveResource('AWS::Neptune::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::Neptune::DBCluster', { PreferredMaintenanceWindow: '07:34-08:04', }); }); @@ -446,8 +445,8 @@ describe('DatabaseCluster', () => { }); // THEN - expect(stack).toHaveResourceLike('AWS::Neptune::DBCluster', { - IamAuthEnabled: ABSENT, + Template.fromStack(stack).hasResourceProperties('AWS::Neptune::DBCluster', { + IamAuthEnabled: Match.absentProperty(), }); }); @@ -467,10 +466,10 @@ describe('DatabaseCluster', () => { cluster.grantConnect(role); // THEN - expect(stack).toHaveResourceLike('AWS::Neptune::DBCluster', { + Template.fromStack(stack).hasResourceProperties('AWS::Neptune::DBCluster', { IamAuthEnabled: true, }); - expect(stack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [{ Effect: 'Allow', diff --git a/packages/@aws-cdk/aws-neptune/test/instance.test.ts b/packages/@aws-cdk/aws-neptune/test/instance.test.ts index dff005199ffc7..38a6981ab2a78 100644 --- a/packages/@aws-cdk/aws-neptune/test/instance.test.ts +++ b/packages/@aws-cdk/aws-neptune/test/instance.test.ts @@ -1,4 +1,4 @@ -import { expect as expectCDK, haveOutput, haveResource, ResourcePart } from '@aws-cdk/assert-internal'; +import { Template } from '@aws-cdk/assertions'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as cdk from '@aws-cdk/core'; import * as constructs from 'constructs'; @@ -17,14 +17,14 @@ describe('DatabaseInstance', () => { }); // THEN - expectCDK(stack).to(haveResource('AWS::Neptune::DBInstance', { + Template.fromStack(stack).hasResource('AWS::Neptune::DBInstance', { Properties: { DBClusterIdentifier: { Ref: 'DatabaseB269D8BB' }, DBInstanceClass: 'db.r5.large', }, DeletionPolicy: 'Retain', UpdateReplacePolicy: 'Retain', - }, ResourcePart.CompleteDefinition)); + }); }); test('check that the endpoint works', () => { @@ -43,9 +43,9 @@ describe('DatabaseInstance', () => { }); // THEN - expectCDK(stack).to(haveOutput({ - exportName, - outputValue: { + Template.fromStack(stack).hasOutput({ + Export: { Name: exportName }, + Value: { 'Fn::Join': [ '', [ @@ -55,7 +55,7 @@ describe('DatabaseInstance', () => { ], ], }, - })); + }); }); test('check importing works as expected', () => { @@ -78,10 +78,10 @@ describe('DatabaseInstance', () => { }); // THEN - expectCDK(stack).to(haveOutput({ - exportName: endpointExportName, - outputValue: `${instanceEndpointAddress}:${port}`, - })); + Template.fromStack(stack).hasOutput({ + Export: { Name: endpointExportName }, + Value: `${instanceEndpointAddress}:${port}`, + }); }); test('instance with parameter group', () => { @@ -102,9 +102,9 @@ describe('DatabaseInstance', () => { }); // THEN - expectCDK(stack).to(haveResource('AWS::Neptune::DBInstance', { + Template.fromStack(stack).hasResourceProperties('AWS::Neptune::DBInstance', { DBParameterGroupName: { Ref: 'ParamsA8366201' }, - })); + }); }); test('instance type from CfnParameter', () => { @@ -130,11 +130,11 @@ describe('DatabaseInstance', () => { }); // THEN - expectCDK(stack).to(haveResource('AWS::Neptune::DBInstance', { + Template.fromStack(stack).hasResourceProperties('AWS::Neptune::DBInstance', { DBInstanceClass: { Ref: 'NeptuneInstaneType', }, - })); + }); }); test('instance type from string throws if missing db prefix', () => { diff --git a/packages/@aws-cdk/aws-neptune/test/parameter-group.test.ts b/packages/@aws-cdk/aws-neptune/test/parameter-group.test.ts index 3267f70aed062..57d71b1767734 100644 --- a/packages/@aws-cdk/aws-neptune/test/parameter-group.test.ts +++ b/packages/@aws-cdk/aws-neptune/test/parameter-group.test.ts @@ -1,4 +1,4 @@ -import { expect, haveResource } from '@aws-cdk/assert-internal'; +import { Template } from '@aws-cdk/assertions'; import { Stack } from '@aws-cdk/core'; import { ClusterParameterGroup, ParameterGroup } from '../lib'; @@ -17,12 +17,12 @@ describe('ClusterParameterGroup', () => { }); // THEN - expect(stack).to(haveResource('AWS::Neptune::DBClusterParameterGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::Neptune::DBClusterParameterGroup', { Description: 'desc', Parameters: { key: 'value', }, - })); + }); }); @@ -39,12 +39,12 @@ describe('ClusterParameterGroup', () => { }); // THEN - expect(stack).to(haveResource('AWS::Neptune::DBParameterGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::Neptune::DBParameterGroup', { Description: 'desc', Parameters: { key: 'value', }, - })); + }); }); }); diff --git a/packages/@aws-cdk/aws-neptune/test/subnet-group.test.ts b/packages/@aws-cdk/aws-neptune/test/subnet-group.test.ts index 675b2cf89a920..5e736d212a12e 100644 --- a/packages/@aws-cdk/aws-neptune/test/subnet-group.test.ts +++ b/packages/@aws-cdk/aws-neptune/test/subnet-group.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { Template } from '@aws-cdk/assertions'; import * as ec2 from '@aws-cdk/aws-ec2'; import { Stack } from '@aws-cdk/core'; import { SubnetGroup } from '../lib'; @@ -17,7 +17,7 @@ test('creates a subnet group from minimal properties', () => { vpc, }); - expect(stack).toHaveResource('AWS::Neptune::DBSubnetGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::Neptune::DBSubnetGroup', { DBSubnetGroupDescription: 'MyGroup', SubnetIds: [ { Ref: 'VPCPrivateSubnet1Subnet8BCA10E0' }, @@ -34,7 +34,7 @@ test('creates a subnet group from all properties', () => { vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE }, }); - expect(stack).toHaveResource('AWS::Neptune::DBSubnetGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::Neptune::DBSubnetGroup', { DBSubnetGroupDescription: 'My Shared Group', DBSubnetGroupName: 'SharedGroup', SubnetIds: [ @@ -51,7 +51,7 @@ describe('subnet selection', () => { vpc, }); - expect(stack).toHaveResource('AWS::Neptune::DBSubnetGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::Neptune::DBSubnetGroup', { DBSubnetGroupDescription: 'MyGroup', SubnetIds: [ { Ref: 'VPCPrivateSubnet1Subnet8BCA10E0' }, @@ -67,7 +67,7 @@ describe('subnet selection', () => { vpcSubnets: { subnetType: ec2.SubnetType.PUBLIC }, }); - expect(stack).toHaveResource('AWS::Neptune::DBSubnetGroup', { + Template.fromStack(stack).hasResourceProperties('AWS::Neptune::DBSubnetGroup', { DBSubnetGroupDescription: 'MyGroup', SubnetIds: [ { Ref: 'VPCPublicSubnet1SubnetB4246D30' }, From c06b161636425801de67f2f95166359ff0dd5dd6 Mon Sep 17 00:00:00 2001 From: Madeline Kusters <80541297+madeline-k@users.noreply.github.com> Date: Tue, 24 Aug 2021 09:48:31 -0700 Subject: [PATCH 13/39] chore(cli): Add node 10 deprecation warning (#16205) Testing: Downgraded to node 10.24.1 on my laptop, and saw the warning banner "Node v10.24.1 has reached end-of-life and will no longer be supported in new releases after 2021-09-01..." when running cli commands on a CDK app linked to this repo. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/aws-cdk/bin/cdk.ts | 1 + packages/aws-cdk/package.json | 1 + yarn.lock | 16 ++++++++++++++++ 3 files changed, 18 insertions(+) diff --git a/packages/aws-cdk/bin/cdk.ts b/packages/aws-cdk/bin/cdk.ts index 1103b6354bf67..29f217360355f 100644 --- a/packages/aws-cdk/bin/cdk.ts +++ b/packages/aws-cdk/bin/cdk.ts @@ -1,6 +1,7 @@ #!/usr/bin/env node import 'source-map-support/register'; import * as cxapi from '@aws-cdk/cx-api'; +import '@jsii/check-node/run'; import * as colors from 'colors/safe'; import * as yargs from 'yargs'; diff --git a/packages/aws-cdk/package.json b/packages/aws-cdk/package.json index 4485c964c105e..497afb2e2ec92 100644 --- a/packages/aws-cdk/package.json +++ b/packages/aws-cdk/package.json @@ -73,6 +73,7 @@ "@aws-cdk/cloudformation-diff": "0.0.0", "@aws-cdk/cx-api": "0.0.0", "@aws-cdk/region-info": "0.0.0", + "@jsii/check-node": "1.33.0", "archiver": "^5.3.0", "aws-sdk": "^2.848.0", "camelcase": "^6.2.0", diff --git a/yarn.lock b/yarn.lock index 431e3492b2be6..dc35a18bf0134 100644 --- a/yarn.lock +++ b/yarn.lock @@ -557,6 +557,14 @@ "@types/yargs" "^15.0.0" chalk "^4.0.0" +"@jsii/check-node@1.33.0": + version "1.33.0" + resolved "https://registry.yarnpkg.com/@jsii/check-node/-/check-node-1.33.0.tgz#55d75cbef1c84e2012c67ab8d6de63f773be4a9b" + integrity sha512-Bajxa09dhkuQ8bM1ve6qtm2oFNhW9/+GaKRh4Deewsk/G86ovLXI/rRS6TfCsSw4E0TGPFWzWy0tBeJuEDo7sw== + dependencies: + chalk "^4.1.2" + semver "^7.3.5" + "@jsii/spec@^1.31.0": version "1.31.0" resolved "https://registry.yarnpkg.com/@jsii/spec/-/spec-1.31.0.tgz#9298dc163fdae0bab4006b817592235a29922871" @@ -2686,6 +2694,14 @@ chalk@^4.0.0, chalk@^4.1.0: ansi-styles "^4.1.0" supports-color "^7.1.0" +chalk@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + char-regex@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf" From a1a65bc9a38b06ec51dff462e52b1beb8d421a56 Mon Sep 17 00:00:00 2001 From: Julian Michel Date: Tue, 24 Aug 2021 19:37:21 +0200 Subject: [PATCH 14/39] fix(sqs): unable to import a FIFO queue when the queue ARN is a token (#15976) The fifo property of a SQS queue is determined based on the queue name. The suffix `.fifo` indicates that it is a fifo queue. When the queue is imported from a token, the name is not known on synthesis time. Therefore, the name of the queue can't be used to initialize the queue as a FIFO queue. In some constructs, CDK checks during synthesis if the given queue is a FIFIO queue. Because of that, an additional option is necessary to specify a FIFO queue. A new boolean property `fifo` is introduced which can be used to specify a FIFO queue that is imported from a token. The property is optional and is only necessary when a FIFO queue should be imported from a token. Closes #12466. ---- *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-sqs/lib/queue-base.ts | 9 +++ packages/@aws-cdk/aws-sqs/lib/queue.ts | 25 ++++++- packages/@aws-cdk/aws-sqs/test/test.sqs.ts | 83 ++++++++++++++++++++- 3 files changed, 113 insertions(+), 4 deletions(-) diff --git a/packages/@aws-cdk/aws-sqs/lib/queue-base.ts b/packages/@aws-cdk/aws-sqs/lib/queue-base.ts index 162c149aec2ad..23cf09218cc58 100644 --- a/packages/@aws-cdk/aws-sqs/lib/queue-base.ts +++ b/packages/@aws-cdk/aws-sqs/lib/queue-base.ts @@ -275,4 +275,13 @@ export interface QueueAttributes { * @default - None */ readonly keyArn?: string; + + /** + * Whether this queue is an Amazon SQS FIFO queue. If false, this is a standard queue. + * + * In case of a FIFO queue which is imported from a token, this value has to be explicitly set to true. + * + * @default - if fifo is not specified, the property will be determined based on the queue name (not possible for FIFO queues imported from a token) + */ + readonly fifo?: boolean; } diff --git a/packages/@aws-cdk/aws-sqs/lib/queue.ts b/packages/@aws-cdk/aws-sqs/lib/queue.ts index 414ddaaf8a919..db2992630aa63 100644 --- a/packages/@aws-cdk/aws-sqs/lib/queue.ts +++ b/packages/@aws-cdk/aws-sqs/lib/queue.ts @@ -1,5 +1,5 @@ import * as kms from '@aws-cdk/aws-kms'; -import { Duration, RemovalPolicy, Stack, Token } from '@aws-cdk/core'; +import { Duration, RemovalPolicy, Stack, Token, ArnFormat } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { IQueue, QueueAttributes, QueueBase } from './queue-base'; import { CfnQueue } from './sqs.generated'; @@ -257,7 +257,7 @@ export class Queue extends QueueBase { */ public static fromQueueAttributes(scope: Construct, id: string, attrs: QueueAttributes): IQueue { const stack = Stack.of(scope); - const parsedArn = stack.parseArn(attrs.queueArn); + const parsedArn = stack.splitArn(attrs.queueArn, ArnFormat.NO_RESOURCE_NAME); const queueName = attrs.queueName || parsedArn.resource; const queueUrl = attrs.queueUrl || `https://sqs.${parsedArn.region}.${stack.urlSuffix}/${parsedArn.account}/${queueName}`; @@ -268,9 +268,28 @@ export class Queue extends QueueBase { public readonly encryptionMasterKey = attrs.keyArn ? kms.Key.fromKeyArn(this, 'Key', attrs.keyArn) : undefined; - public readonly fifo = queueName.endsWith('.fifo') ? true : false; + public readonly fifo: boolean = this.determineFifo();; protected readonly autoCreatePolicy = false; + + /** + * Determine fifo flag based on queueName and fifo attribute + */ + private determineFifo(): boolean { + if (Token.isUnresolved(this.queueArn)) { + return attrs.fifo || false; + } else { + if (typeof attrs.fifo !== 'undefined') { + if (attrs.fifo && !queueName.endsWith('.fifo')) { + throw new Error("FIFO queue names must end in '.fifo'"); + } + if (!attrs.fifo && queueName.endsWith('.fifo')) { + throw new Error("Non-FIFO queue name may not end in '.fifo'"); + } + } + return queueName.endsWith('.fifo') ? true : false; + } + } } return new Import(scope, id); diff --git a/packages/@aws-cdk/aws-sqs/test/test.sqs.ts b/packages/@aws-cdk/aws-sqs/test/test.sqs.ts index da8637ba5d4f5..ae334e5fcd4d3 100644 --- a/packages/@aws-cdk/aws-sqs/test/test.sqs.ts +++ b/packages/@aws-cdk/aws-sqs/test/test.sqs.ts @@ -1,7 +1,7 @@ import { expect, haveResource, ResourcePart } from '@aws-cdk/assert-internal'; import * as iam from '@aws-cdk/aws-iam'; import * as kms from '@aws-cdk/aws-kms'; -import { CfnParameter, Duration, Stack, App } from '@aws-cdk/core'; +import { CfnParameter, Duration, Stack, App, Token } from '@aws-cdk/core'; import { Test } from 'nodeunit'; import * as sqs from '../lib'; @@ -191,6 +191,87 @@ export = { test.done(); }, + 'import queueArn from token, fifo and standard queues can be defined'(test: Test) { + // GIVEN + const stack = new Stack(); + + // WHEN + const stdQueue1 = sqs.Queue.fromQueueAttributes(stack, 'StdQueue1', { + queueArn: Token.asString({ Ref: 'ARN' }), + }); + const stdQueue2 = sqs.Queue.fromQueueAttributes(stack, 'StdQueue2', { + queueArn: Token.asString({ Ref: 'ARN' }), + fifo: false, + }); + const fifoQueue = sqs.Queue.fromQueueAttributes(stack, 'FifoQueue', { + queueArn: Token.asString({ Ref: 'ARN' }), + fifo: true, + }); + + // THEN + test.deepEqual(stdQueue1.fifo, false); + test.deepEqual(stdQueue2.fifo, false); + test.deepEqual(fifoQueue.fifo, true); + test.done(); + }, + + 'import queueArn from token, check attributes'(test: Test) { + // GIVEN + const stack = new Stack(); + + // WHEN + const stdQueue1 = sqs.Queue.fromQueueArn(stack, 'StdQueue', Token.asString({ Ref: 'ARN' })); + + // THEN + test.deepEqual(stack.resolve(stdQueue1.queueArn), { + Ref: 'ARN', + }); + test.deepEqual(stack.resolve(stdQueue1.queueName), { + 'Fn::Select': [5, { 'Fn::Split': [':', { Ref: 'ARN' }] }], + }); + test.deepEqual(stack.resolve(stdQueue1.queueUrl), { + 'Fn::Join': + ['', + ['https://sqs.', + { 'Fn::Select': [3, { 'Fn::Split': [':', { Ref: 'ARN' }] }] }, + '.', + { Ref: 'AWS::URLSuffix' }, + '/', + { 'Fn::Select': [4, { 'Fn::Split': [':', { Ref: 'ARN' }] }] }, + '/', + { 'Fn::Select': [5, { 'Fn::Split': [':', { Ref: 'ARN' }] }] }]], + }); + test.deepEqual(stdQueue1.fifo, false); + test.done(); + }, + + 'fails if fifo flag is set for non fifo queue name'(test: Test) { + // GIVEN + const app = new App(); + const stack = new Stack(app, 'my-stack'); + + // THEN + test.throws(() => sqs.Queue.fromQueueAttributes(stack, 'ImportedStdQueue', { + queueArn: 'arn:aws:sqs:us-west-2:123456789012:queue1', + fifo: true, + }), /FIFO queue names must end in '.fifo'/); + test.done(); + }, + + + 'fails if fifo flag is false for fifo queue name'(test: Test) { + // GIVEN + const app = new App(); + const stack = new Stack(app, 'my-stack'); + + // THEN + test.throws(() => sqs.Queue.fromQueueAttributes(stack, 'ImportedFifoQueue', { + queueArn: 'arn:aws:sqs:us-west-2:123456789012:queue1.fifo', + fifo: false, + }), /Non-FIFO queue name may not end in '.fifo'/); + test.done(); + }, + 'importing works correctly for cross region queue'(test: Test) { // GIVEN const stack = new Stack(undefined, 'Stack', { env: { region: 'us-east-1' } }); From eb54cd416a48708898e30986058491e21125b2f7 Mon Sep 17 00:00:00 2001 From: Julian Michel Date: Tue, 24 Aug 2021 20:17:15 +0200 Subject: [PATCH 15/39] fix(ssm): StringParameter.fromStringParameterAttributes cannot accept version as a numeric Token (#16048) In `StringParameter.fromStringParameterAttributes()` and `StringParameter.fromSecureStringParameterAttributes()` the version of a parameter can be specified using data type number. If a token value (e.g. CloudFormation Parameter) was used here, the token was not resolved correctly. The conversion of property `version` was corrected by using method `Tokenization.stringifyNumber()`. Fixes #11913. ---- *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-ssm/lib/parameter.ts | 5 +- ...g.parameter-store-string.lit.expected.json | 18 +++++ .../test/integ.parameter-store-string.lit.ts | 20 ++++++ .../@aws-cdk/aws-ssm/test/test.parameter.ts | 68 +++++++++++++++++++ 4 files changed, 109 insertions(+), 2 deletions(-) diff --git a/packages/@aws-cdk/aws-ssm/lib/parameter.ts b/packages/@aws-cdk/aws-ssm/lib/parameter.ts index 416454c1ef8cc..df30c8197cf91 100644 --- a/packages/@aws-cdk/aws-ssm/lib/parameter.ts +++ b/packages/@aws-cdk/aws-ssm/lib/parameter.ts @@ -4,6 +4,7 @@ import * as cxschema from '@aws-cdk/cloud-assembly-schema'; import { CfnDynamicReference, CfnDynamicReferenceService, CfnParameter, Construct as CompatConstruct, ContextProvider, Fn, IResource, Resource, Stack, Token, + Tokenization, } from '@aws-cdk/core'; import { Construct } from 'constructs'; import * as ssm from './ssm.generated'; @@ -324,7 +325,7 @@ export class StringParameter extends ParameterBase implements IStringParameter { const type = attrs.type || ParameterType.STRING; const stringValue = attrs.version - ? new CfnDynamicReference(CfnDynamicReferenceService.SSM, `${attrs.parameterName}:${attrs.version}`).toString() + ? new CfnDynamicReference(CfnDynamicReferenceService.SSM, `${attrs.parameterName}:${Tokenization.stringifyNumber(attrs.version)}`).toString() : new CfnParameter(scope as CompatConstruct, `${id}.Parameter`, { type: `AWS::SSM::Parameter::Value<${type}>`, default: attrs.parameterName }).valueAsString; class Import extends ParameterBase { @@ -341,7 +342,7 @@ export class StringParameter extends ParameterBase implements IStringParameter { * Imports a secure string parameter from the SSM parameter store. */ public static fromSecureStringParameterAttributes(scope: Construct, id: string, attrs: SecureStringParameterAttributes): IStringParameter { - const stringValue = new CfnDynamicReference(CfnDynamicReferenceService.SSM_SECURE, `${attrs.parameterName}:${attrs.version}`).toString(); + const stringValue = new CfnDynamicReference(CfnDynamicReferenceService.SSM_SECURE, `${attrs.parameterName}:${Tokenization.stringifyNumber(attrs.version)}`).toString(); class Import extends ParameterBase { public readonly parameterName = attrs.parameterName; diff --git a/packages/@aws-cdk/aws-ssm/test/integ.parameter-store-string.lit.expected.json b/packages/@aws-cdk/aws-ssm/test/integ.parameter-store-string.lit.expected.json index 1d3d7baba28e0..074f6694ac41b 100644 --- a/packages/@aws-cdk/aws-ssm/test/integ.parameter-store-string.lit.expected.json +++ b/packages/@aws-cdk/aws-ssm/test/integ.parameter-store-string.lit.expected.json @@ -13,6 +13,10 @@ }, { "Parameters": { + "MyParameterVersion": { + "Type": "Number", + "Default": 1 + }, "MyValueParameter": { "Type": "AWS::SSM::Parameter::Value", "Default": "/My/Public/Parameter" @@ -28,6 +32,20 @@ "Value": { "Ref": "MyValueParameter" } + }, + "TheValueVersionFromToken": { + "Value": { + "Fn::Join": [ + "", + [ + "{{resolve:ssm:/My/Public/Parameter:", + { + "Ref": "MyParameterVersion" + }, + "}}" + ] + ] + } } } } diff --git a/packages/@aws-cdk/aws-ssm/test/integ.parameter-store-string.lit.ts b/packages/@aws-cdk/aws-ssm/test/integ.parameter-store-string.lit.ts index 7c4604fa79c9e..09e0f6f17f01f 100644 --- a/packages/@aws-cdk/aws-ssm/test/integ.parameter-store-string.lit.ts +++ b/packages/@aws-cdk/aws-ssm/test/integ.parameter-store-string.lit.ts @@ -17,6 +17,13 @@ class UsingStack extends cdk.Stack { constructor(scope: cdk.App, id: string) { super(scope, id); + // Parameter that contains version number, will be used to pass + // version value from token. + const parameterVersion = new cdk.CfnParameter(this, 'MyParameterVersion', { + type: 'Number', + default: 1, + }).valueAsNumber; + /// !show // Retrieve the latest value of the non-secret parameter // with name "/My/String/Parameter". @@ -24,6 +31,11 @@ class UsingStack extends cdk.Stack { parameterName: '/My/Public/Parameter', // 'version' can be specified but is optional. }).stringValue; + const stringValueVersionFromToken = ssm.StringParameter.fromStringParameterAttributes(this, 'MyValueVersionFromToken', { + parameterName: '/My/Public/Parameter', + // parameter version from token + version: parameterVersion, + }).stringValue; // Retrieve a specific version of the secret (SecureString) parameter. // 'version' is always required. @@ -31,13 +43,21 @@ class UsingStack extends cdk.Stack { parameterName: '/My/Secret/Parameter', version: 5, }); + const secretValueVersionFromToken = ssm.StringParameter.fromSecureStringParameterAttributes(this, 'MySecureValueVersionFromToken', { + parameterName: '/My/Secret/Parameter', + // parameter version from token + version: parameterVersion, + }); + /// !hide new cdk.CfnResource(this, 'Dummy', { type: 'AWS::SNS::Topic' }); new cdk.CfnOutput(this, 'TheValue', { value: stringValue }); + new cdk.CfnOutput(this, 'TheValueVersionFromToken', { value: stringValueVersionFromToken }); // Cannot be provisioned so cannot be actually used Array.isArray(secretValue); + Array.isArray(secretValueVersionFromToken); } } diff --git a/packages/@aws-cdk/aws-ssm/test/test.parameter.ts b/packages/@aws-cdk/aws-ssm/test/test.parameter.ts index 8ce26dd03a0c5..83b68eda4a508 100644 --- a/packages/@aws-cdk/aws-ssm/test/test.parameter.ts +++ b/packages/@aws-cdk/aws-ssm/test/test.parameter.ts @@ -344,6 +344,40 @@ export = { test.done(); }, + 'StringParameter.fromStringParameterAttributes with version from token'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const param = ssm.StringParameter.fromStringParameterAttributes(stack, 'MyParamName', { + parameterName: 'MyParamName', + version: cdk.Token.asNumber({ Ref: 'version' }), + }); + + // THEN + test.deepEqual(stack.resolve(param.parameterArn), { + 'Fn::Join': ['', [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':ssm:', + { Ref: 'AWS::Region' }, + ':', + { Ref: 'AWS::AccountId' }, + ':parameter/MyParamName', + ]], + }); + test.deepEqual(stack.resolve(param.parameterName), 'MyParamName'); + test.deepEqual(stack.resolve(param.parameterType), 'String'); + test.deepEqual(stack.resolve(param.stringValue), { + 'Fn::Join': ['', [ + '{{resolve:ssm:MyParamName:', + { Ref: 'version' }, + '}}', + ]], + }); + test.done(); + }, + 'StringParameter.fromSecureStringParameterAttributes'(test: Test) { // GIVEN const stack = new cdk.Stack(); @@ -372,6 +406,40 @@ export = { test.done(); }, + 'StringParameter.fromSecureStringParameterAttributes with version from token'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const param = ssm.StringParameter.fromSecureStringParameterAttributes(stack, 'MyParamName', { + parameterName: 'MyParamName', + version: cdk.Token.asNumber({ Ref: 'version' }), + }); + + // THEN + test.deepEqual(stack.resolve(param.parameterArn), { + 'Fn::Join': ['', [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':ssm:', + { Ref: 'AWS::Region' }, + ':', + { Ref: 'AWS::AccountId' }, + ':parameter/MyParamName', + ]], + }); + test.deepEqual(stack.resolve(param.parameterName), 'MyParamName'); + test.deepEqual(stack.resolve(param.parameterType), 'SecureString'); + test.deepEqual(stack.resolve(param.stringValue), { + 'Fn::Join': ['', [ + '{{resolve:ssm-secure:MyParamName:', + { Ref: 'version' }, + '}}', + ]], + }); + test.done(); + }, + 'StringParameter.fromSecureStringParameterAttributes with encryption key creates the correct policy for grantRead'(test: Test) { // GIVEN const stack = new cdk.Stack(); From 0f7beb29be18d809052f4d46e415a0394c9299ab Mon Sep 17 00:00:00 2001 From: Colin Ihrig Date: Tue, 24 Aug 2021 14:59:05 -0400 Subject: [PATCH 16/39] feat(docdb): cluster - deletion protection (#15216) This commit adds support for `deletionProtection` on docdb `DatabaseCluster`s. Fixes: https://github.com/aws/aws-cdk/issues/15170 ---- *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-docdb/README.md | 18 ++++++++++++++++ packages/@aws-cdk/aws-docdb/lib/cluster.ts | 11 ++++++++++ .../@aws-cdk/aws-docdb/test/cluster.test.ts | 21 +++++++++++++++++++ 3 files changed, 50 insertions(+) diff --git a/packages/@aws-cdk/aws-docdb/README.md b/packages/@aws-cdk/aws-docdb/README.md index fb1c4eeb270ee..6f7ed89e28e11 100644 --- a/packages/@aws-cdk/aws-docdb/README.md +++ b/packages/@aws-cdk/aws-docdb/README.md @@ -60,6 +60,24 @@ const securityGroup = new ec2.SecurityGroup(stack, 'SecurityGroup', { cluster.addSecurityGroups(securityGroup); ``` +## Deletion protection + +Deletion protection can be enabled on an Amazon DocumentDB cluster to prevent accidental deletion of the cluster: + +```ts +const cluster = new DatabaseCluster(this, 'Database', { + masterUser: { + username: 'myuser' + }, + instanceType: ec2.InstanceType.of(ec2.InstanceClass.R5, ec2.InstanceSize.LARGE), + vpcSubnets: { + subnetType: ec2.SubnetType.PUBLIC, + }, + vpc, + deletionProtection: true // Enable deletion protection. +}); +``` + ## Rotating credentials When the master password is generated and stored in AWS Secrets Manager, it can be rotated automatically: diff --git a/packages/@aws-cdk/aws-docdb/lib/cluster.ts b/packages/@aws-cdk/aws-docdb/lib/cluster.ts index f90e976db8798..56b5a724dd439 100644 --- a/packages/@aws-cdk/aws-docdb/lib/cluster.ts +++ b/packages/@aws-cdk/aws-docdb/lib/cluster.ts @@ -136,6 +136,16 @@ export interface DatabaseClusterProps { * @default - Retain cluster. */ readonly removalPolicy?: RemovalPolicy + + /** + * Specifies whether this cluster can be deleted. If deletionProtection is + * enabled, the cluster cannot be deleted unless it is modified and + * deletionProtection is disabled. deletionProtection protects clusters from + * being accidentally deleted. + * + * @default - false + */ + readonly deletionProtection?: boolean; } /** @@ -361,6 +371,7 @@ export class DatabaseCluster extends DatabaseClusterBase { port: props.port, vpcSecurityGroupIds: [this.securityGroupId], dbClusterParameterGroupName: props.parameterGroup?.parameterGroupName, + deletionProtection: props.deletionProtection, // Admin masterUsername: secret ? secret.secretValueFromJson('username').toString() : props.masterUser.username, masterUserPassword: secret diff --git a/packages/@aws-cdk/aws-docdb/test/cluster.test.ts b/packages/@aws-cdk/aws-docdb/test/cluster.test.ts index beb564ed738e5..cb1f8653509df 100644 --- a/packages/@aws-cdk/aws-docdb/test/cluster.test.ts +++ b/packages/@aws-cdk/aws-docdb/test/cluster.test.ts @@ -167,6 +167,27 @@ describe('DatabaseCluster', () => { })); }); + test('can configure cluster deletion protection', () => { + // GIVEN + const stack = testStack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + + // WHEN + new DatabaseCluster(stack, 'Database', { + masterUser: { + username: 'admin', + }, + instanceType: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.SMALL), + vpc, + deletionProtection: true, + }); + + // THEN + expectCDK(stack).to(haveResource('AWS::DocDB::DBCluster', { + DeletionProtection: true, + })); + }); + test('cluster with parameter group', () => { // GIVEN const stack = testStack(); From cdee1af03c34a1c08988e672bae6edc2538a8877 Mon Sep 17 00:00:00 2001 From: Madeline Kusters <80541297+madeline-k@users.noreply.github.com> Date: Tue, 24 Aug 2021 13:12:06 -0700 Subject: [PATCH 17/39] fix(resourcegroups): ResourceGroup not using TagType.STANDARD, causes deploy failure (#16211) This PR removes a patch to the cloudformation spec that is no longer necessary. Without the patch, the correct tag type of STANDARD is generated for CfnResourceGroup. PR where the patch was introduced: https://github.com/aws/aws-cdk/pull/6995 Closes #12986 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../530_ResourceGroups_Tags_patch.json | 24 ------------------- 1 file changed, 24 deletions(-) delete mode 100644 packages/@aws-cdk/cfnspec/spec-source/530_ResourceGroups_Tags_patch.json diff --git a/packages/@aws-cdk/cfnspec/spec-source/530_ResourceGroups_Tags_patch.json b/packages/@aws-cdk/cfnspec/spec-source/530_ResourceGroups_Tags_patch.json deleted file mode 100644 index da2d3c603ddbd..0000000000000 --- a/packages/@aws-cdk/cfnspec/spec-source/530_ResourceGroups_Tags_patch.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "ResourceTypes": { - "AWS::ResourceGroups::Group": { - "patch": { - "description": "Sets AWS::ResourceGroups::Group#Tags to Json", - "operations": [ - { - "op": "remove", - "path": "/Properties/Tags/ItemType" - }, - { - "op": "remove", - "path": "/Properties/Tags/Type" - }, - { - "op": "add", - "path": "/Properties/Tags/PrimitiveType", - "value": "Json" - } - ] - } - } - } -} From 27a37ac26172301178a87fda93370b1554f5d513 Mon Sep 17 00:00:00 2001 From: Eli Polonsky Date: Tue, 24 Aug 2021 16:17:26 -0700 Subject: [PATCH 18/39] chore(integ): node check breaks cli integ tests (#16216) The Node runtime check performed by jsii and introduced in this [PR](https://github.com/aws/aws-cdk/pull/16205) breaks the CLI integration tests since it changes the output of the `synth` command. This PR makes it so the test asserts on the content of the template rather than the output of the CLI. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../aws-cdk/test/integ/cli/cli.integtest.ts | 47 ++++++++++++------- packages/aws-cdk/test/integ/helpers/cdk.ts | 6 +++ 2 files changed, 36 insertions(+), 17 deletions(-) diff --git a/packages/aws-cdk/test/integ/cli/cli.integtest.ts b/packages/aws-cdk/test/integ/cli/cli.integtest.ts index 957d04bf09fd9..873e3a1787ee5 100644 --- a/packages/aws-cdk/test/integ/cli/cli.integtest.ts +++ b/packages/aws-cdk/test/integ/cli/cli.integtest.ts @@ -39,23 +39,36 @@ integTest('Termination protection', withDefaultFixture(async (fixture) => { })); integTest('cdk synth', withDefaultFixture(async (fixture) => { - await expect(fixture.cdk(['synth', fixture.fullStackName('test-1')], { verbose: false })).resolves.toEqual( - `Resources: - topic69831491: - Type: AWS::SNS::Topic - Metadata: - aws:cdk:path: ${fixture.stackNamePrefix}-test-1/topic/Resource`); - - await expect(fixture.cdk(['synth', fixture.fullStackName('test-2')], { verbose: false })).resolves.toEqual( - `Resources: - topic152D84A37: - Type: AWS::SNS::Topic - Metadata: - aws:cdk:path: ${fixture.stackNamePrefix}-test-2/topic1/Resource - topic2A4FB547F: - Type: AWS::SNS::Topic - Metadata: - aws:cdk:path: ${fixture.stackNamePrefix}-test-2/topic2/Resource`); + await fixture.cdk(['synth', fixture.fullStackName('test-1')]); + expect(fixture.template('test-1')).toEqual({ + Resources: { + topic69831491: { + Type: 'AWS::SNS::Topic', + Metadata: { + 'aws:cdk:path': `${fixture.stackNamePrefix}-test-1/topic/Resource`, + }, + }, + }, + }); + + await fixture.cdk(['synth', fixture.fullStackName('test-2')], { verbose: false }); + expect(fixture.template('test-2')).toEqual({ + Resources: { + topic152D84A37: { + Type: 'AWS::SNS::Topic', + Metadata: { + 'aws:cdk:path': `${fixture.stackNamePrefix}-test-2/topic1/Resource`, + }, + }, + topic2A4FB547F: { + Type: 'AWS::SNS::Topic', + Metadata: { + 'aws:cdk:path': `${fixture.stackNamePrefix}-test-2/topic2/Resource`, + }, + }, + }, + }); + })); integTest('ssm parameter provider error', withDefaultFixture(async (fixture) => { diff --git a/packages/aws-cdk/test/integ/helpers/cdk.ts b/packages/aws-cdk/test/integ/helpers/cdk.ts index 8bf256140d13b..4a09f43063600 100644 --- a/packages/aws-cdk/test/integ/helpers/cdk.ts +++ b/packages/aws-cdk/test/integ/helpers/cdk.ts @@ -382,6 +382,12 @@ export class TestFixture { }); } + public template(stackName: string): any { + const fullStackName = this.fullStackName(stackName); + const templatePath = path.join(this.integTestDir, 'cdk.out', `${fullStackName}.template.json`); + return JSON.parse(fs.readFileSync(templatePath, { encoding: 'utf-8' }).toString()); + } + public get bootstrapStackName() { return this.fullStackName('bootstrap-stack'); } From e547ba0d1491af0abe703132fa06fe786ffd7070 Mon Sep 17 00:00:00 2001 From: kaizen3031593 <36202692+kaizen3031593@users.noreply.github.com> Date: Tue, 24 Aug 2021 20:16:53 -0400 Subject: [PATCH 19/39] feat(cloudwatch): add support for cross-account alarms (#16007) Allows `accountId` to be specified in a CloudWatch Alarm. This opens up the ability to create cross-account alarms, which was just [announced](https://aws.amazon.com/about-aws/whats-new/2021/08/announcing-amazon-cloudwatch-cross-account-alarms/). closes #15959. ---- *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 | 4 ++ packages/@aws-cdk/aws-cloudwatch/lib/alarm.ts | 11 +-- .../test/cross-environment.test.ts | 67 ++++++++++++++++++- 3 files changed, 76 insertions(+), 6 deletions(-) diff --git a/packages/@aws-cdk/aws-cloudwatch/README.md b/packages/@aws-cdk/aws-cloudwatch/README.md index ff1f1997489f4..bdcf58e61024f 100644 --- a/packages/@aws-cdk/aws-cloudwatch/README.md +++ b/packages/@aws-cdk/aws-cloudwatch/README.md @@ -28,6 +28,8 @@ represents the amount of errors reported by that Lambda function: const errors = fn.metricErrors(); ``` +`Metric` objects can be account and region aware. You can specify `account` and `region` as properties of the metric, or use the `metric.attachTo(Construct)` method. `metric.attachTo()` will automatically copy the `region` and `account` fields of the `Construct`, which can come from anywhere in the Construct tree. + You can also instantiate `Metric` objects to reference any [published metric](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/aws-services-cloudwatch-metrics.html) that's not exposed using a convenience method on the CDK construct. @@ -160,6 +162,8 @@ The most important properties to set while creating an Alarms are: - `evaluationPeriods`: how many consecutive periods the metric has to be breaching the the threshold for the alarm to trigger. +To create a cross-account alarm, make sure you have enabled [cross-account functionality](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/Cross-Account-Cross-Region.html) in CloudWatch. Then, set the `account` property in the `Metric` object either manually or via the `metric.attachTo()` method. + ### Alarm Actions To add actions to an alarm, use the integration classes from the diff --git a/packages/@aws-cdk/aws-cloudwatch/lib/alarm.ts b/packages/@aws-cdk/aws-cloudwatch/lib/alarm.ts index 6b102d567125f..41928d0af7193 100644 --- a/packages/@aws-cdk/aws-cloudwatch/lib/alarm.ts +++ b/packages/@aws-cdk/aws-cloudwatch/lib/alarm.ts @@ -257,7 +257,10 @@ export class Alarm extends AlarmBase { return dispatchMetric(metric, { withStat(stat, conf) { self.validateMetricStat(stat, metric); - if (conf.renderingProperties?.label == undefined) { + const canRenderAsLegacyMetric = conf.renderingProperties?.label == undefined && + (stat.account == undefined || Stack.of(self).account == stat.account); + // Do this to disturb existing templates as little as possible + if (canRenderAsLegacyMetric) { return dropUndefined({ dimensions: stat.dimensions, namespace: stat.namespace, @@ -283,6 +286,7 @@ export class Alarm extends AlarmBase { unit: stat.unitFilter, }, id: 'm1', + accountId: stat.account, label: conf.renderingProperties?.label, returnData: true, } as CfnAlarm.MetricDataQueryProperty, @@ -344,7 +348,7 @@ export class Alarm extends AlarmBase { } /** - * Validate that if a region and account are in the given stat config, they match the Alarm + * Validate that if a region is in the given stat config, they match the Alarm */ private validateMetricStat(stat: MetricStatConfig, metric: IMetric) { const stack = Stack.of(this); @@ -352,9 +356,6 @@ export class Alarm extends AlarmBase { if (definitelyDifferent(stat.region, stack.region)) { throw new Error(`Cannot create an Alarm in region '${stack.region}' based on metric '${metric}' in '${stat.region}'`); } - if (definitelyDifferent(stat.account, stack.account)) { - throw new Error(`Cannot create an Alarm in account '${stack.account}' based on metric '${metric}' in '${stat.account}'`); - } } } diff --git a/packages/@aws-cdk/aws-cloudwatch/test/cross-environment.test.ts b/packages/@aws-cdk/aws-cloudwatch/test/cross-environment.test.ts index 7c7ed7d04bf6f..f17391591a8c5 100644 --- a/packages/@aws-cdk/aws-cloudwatch/test/cross-environment.test.ts +++ b/packages/@aws-cdk/aws-cloudwatch/test/cross-environment.test.ts @@ -6,11 +6,12 @@ const a = new Metric({ namespace: 'Test', metricName: 'ACount' }); let stack1: Stack; let stack2: Stack; +let stack3: Stack; describe('cross environment', () => { beforeEach(() => { stack1 = new Stack(undefined, undefined, { env: { region: 'pluto', account: '1234' } }); stack2 = new Stack(undefined, undefined, { env: { region: 'mars', account: '5678' } }); - + stack3 = new Stack(undefined, undefined, { env: { region: 'pluto', account: '0000' } }); }); describe('in graphs', () => { @@ -112,6 +113,70 @@ describe('cross environment', () => { }); + + test('metric attached to stack3 will render in stack1', () => { + //Cross-account metrics are supported in Alarms + + // GIVEN + new Alarm(stack1, 'Alarm', { + threshold: 1, + evaluationPeriods: 1, + metric: a.attachTo(stack3), + }); + + // THEN + Template.fromStack(stack1).hasResourceProperties('AWS::CloudWatch::Alarm', { + Metrics: [ + { + AccountId: '0000', + Id: 'm1', + MetricStat: { + Metric: { + MetricName: 'ACount', + Namespace: 'Test', + }, + Period: 300, + Stat: 'Average', + }, + ReturnData: true, + }, + ], + }); + }); + + test('metric can render in a different account', () => { + // GIVEN + const b = new Metric({ + namespace: 'Test', + metricName: 'ACount', + account: '0000', + }); + + new Alarm(stack1, 'Alarm', { + threshold: 1, + evaluationPeriods: 1, + metric: b, + }); + + // THEN + Template.fromStack(stack1).hasResourceProperties('AWS::CloudWatch::Alarm', { + Metrics: [ + { + AccountId: '0000', + Id: 'm1', + MetricStat: { + Metric: { + MetricName: 'ACount', + Namespace: 'Test', + }, + Period: 300, + Stat: 'Average', + }, + ReturnData: true, + }, + ], + }); + }); }); }); From e678f104df4fb0377c6ad5c8abc4132433363871 Mon Sep 17 00:00:00 2001 From: Ben Chaimberg Date: Wed, 25 Aug 2021 05:51:34 -0700 Subject: [PATCH 20/39] revert: temporarily transfer @skinny85 module ownership (#16206) This reverts commit 77dfa2f8546d03671b3304d2910a9c7e387ff6a8. --- .github/workflows/issue-label-assign.yml | 116 +++++++++++------------ 1 file changed, 58 insertions(+), 58 deletions(-) diff --git a/.github/workflows/issue-label-assign.yml b/.github/workflows/issue-label-assign.yml index 618de4693571f..64e3daf97887b 100644 --- a/.github/workflows/issue-label-assign.yml +++ b/.github/workflows/issue-label-assign.yml @@ -21,23 +21,23 @@ jobs: [ {"keywords":["(cli)","(command line)"],"labels":["package/tools"],"assignees":["rix0rrr"]}, {"keywords":["(@aws-cdk/alexa-ask)","(alexa-ask)","(alexa ask)"],"labels":["@aws-cdk/alexa-ask"],"assignees":["madeline-k"]}, - {"keywords":["(@aws-cdk/app-delivery)","(app-delivery)","(app delivery)"],"labels":["@aws-cdk/app-delivery"],"assignees":["madeline-k"]}, + {"keywords":["(@aws-cdk/app-delivery)","(app-delivery)","(app delivery)"],"labels":["@aws-cdk/app-delivery"],"assignees":["skinny85"]}, {"keywords":["(@aws-cdk/assert)","(assert)"],"labels":["@aws-cdk/assert"],"assignees":["nija-at"]}, {"keywords":["(@aws-cdk/assets)","(assets)"],"labels":["@aws-cdk/assets"],"assignees":["eladb"]}, - {"keywords":["(@aws-cdk/aws-accessanalyzer)","(aws-accessanalyzer)","(accessanalyzer)","(access analyzer)"],"labels":["@aws-cdk/aws-accessanalyzer"],"assignees":["BenChaimberg"]}, - {"keywords":["(@aws-cdk/aws-acmpca)","(aws-acmpca)","(acmpca)"],"labels":["@aws-cdk/aws-acmpca"],"assignees":["BenChaimberg"]}, + {"keywords":["(@aws-cdk/aws-accessanalyzer)","(aws-accessanalyzer)","(accessanalyzer)","(access analyzer)"],"labels":["@aws-cdk/aws-accessanalyzer"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-acmpca)","(aws-acmpca)","(acmpca)"],"labels":["@aws-cdk/aws-acmpca"],"assignees":["skinny85"]}, {"keywords":["(@aws-cdk/aws-amazonmq)","(aws-amazonmq)","(amazonmq)","(amazon mq)","(amazon-mq)"],"labels":["@aws-cdk/aws-amazonmq"],"assignees":["otaviomacedo"]}, - {"keywords":["(@aws-cdk/aws-amplify)","(aws-amplify)","(amplify)"],"labels":["@aws-cdk/aws-amplify"],"assignees":["madeline-k"]}, + {"keywords":["(@aws-cdk/aws-amplify)","(aws-amplify)","(amplify)"],"labels":["@aws-cdk/aws-amplify"],"assignees":["skinny85"]}, {"keywords":["(@aws-cdk/aws-apigateway)","(aws-apigateway)","(apigateway)","(api gateway)","(api-gateway)"],"labels":["@aws-cdk/aws-apigateway"],"assignees":["nija-at"]}, {"keywords":["(@aws-cdk/aws-apigatewayv2)","(aws-apigatewayv2)","(apigatewayv2)","(apigateway v2)","(api-gateway-v2)"],"labels":["@aws-cdk/aws-apigatewayv2"],"assignees":["nija-at"]}, {"keywords":["(@aws-cdk/aws-apigatewayv2-authorizers)","(aws-apigatewayv2-authorizers)","(apigatewayv2-authorizers)"],"labels":["@aws-cdk/aws-apigatewayv2-authorizers"],"assignees":["nija-at"]}, {"keywords":["(@aws-cdk/aws-apigatewayv2-integrations)","(aws-apigatewayv2-integrations)","(apigatewayv2-integrations)","(apigateway v2 integrations)","(api-gateway-v2-integrations)"],"labels":["@aws-cdk/aws-apigatewayv2-integrations"],"assignees":["nija-at"]}, {"keywords":["(@aws-cdk/aws-appconfig)","(aws-appconfig)","(appconfig)","(app config)","(app-config)"],"labels":["@aws-cdk/aws-appconfig"],"assignees":["rix0rrr"]}, - {"keywords":["(@aws-cdk/aws-appflow)","(aws-appflow)","(appflow)","(app flow)","(app-flow)"],"labels":["@aws-cdk/aws-appflow"],"assignees":["BenChaimberg"]}, - {"keywords":["(@aws-cdk/aws-appintegrations)","(aws-appintegrations)","(appintegrations)"],"labels":["@aws-cdk/aws-appintegrations"],"assignees":["BenChaimberg"]}, + {"keywords":["(@aws-cdk/aws-appflow)","(aws-appflow)","(appflow)","(app flow)","(app-flow)"],"labels":["@aws-cdk/aws-appflow"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-appintegrations)","(aws-appintegrations)","(appintegrations)"],"labels":["@aws-cdk/aws-appintegrations"],"assignees":["skinny85"]}, {"keywords":["(@aws-cdk/aws-applicationautoscaling)","(aws-applicationautoscaling)","(applicationautoscaling)","(application autoscaling)","(application-autoscaling)"],"labels":["@aws-cdk/aws-applicationautoscaling"],"assignees":["comcalvi"]}, {"keywords":["(@aws-cdk/aws-applicationinsights)","(aws-applicationinsights)","(applicationinsights)","(application insights)","(application-insights)"],"labels":["@aws-cdk/aws-applicationinsights"],"assignees":["njlynch"]}, - {"keywords":["(@aws-cdk/aws-appmesh)","(aws-appmesh)","(appmesh)","(app mesh)","(app-mesh)"],"labels":["@aws-cdk/aws-appmesh"],"assignees":["BenChaimberg"]}, + {"keywords":["(@aws-cdk/aws-appmesh)","(aws-appmesh)","(appmesh)","(app mesh)","(app-mesh)"],"labels":["@aws-cdk/aws-appmesh"],"assignees":["skinny85"]}, {"keywords":["(@aws-cdk/aws-appstream)","(aws-appstream)","(appstream)","(app stream)","(app-stream)"],"labels":["@aws-cdk/aws-appstream"],"assignees":["madeline-k"]}, {"keywords":["(@aws-cdk/aws-appsync)","(aws-appsync)","(appsync)","(app sync)","(app-sync)"],"labels":["@aws-cdk/aws-appsync"],"assignees":["otaviomacedo"]}, {"keywords":["(@aws-cdk/aws-athena)","(aws-athena)","(athena)"],"labels":["@aws-cdk/aws-athena"],"assignees":["BenChaimberg"]}, @@ -54,7 +54,7 @@ jobs: {"keywords":["(@aws-cdk/aws-ce)","(aws-ce)","(ce)"],"labels":["@aws-cdk/aws-ce"],"assignees":["njlynch"]}, {"keywords":["(@aws-cdk/aws-certificatemanager)","(aws-certificatemanager)","(certificatemanager)","(certificate manager)","(certificate-manager)","(acm)"],"labels":["@aws-cdk/aws-certificatemanager"],"assignees":["njlynch"]}, {"keywords":["(@aws-cdk/aws-chatbot)","(aws-chatbot)","(chatbot)"],"labels":["@aws-cdk/aws-chatbot"],"assignees":["BenChaimberg"]}, - {"keywords":["(@aws-cdk/aws-cloud9)","(aws-cloud9)","(cloud9)","(cloud 9)"],"labels":["@aws-cdk/aws-cloud9"],"assignees":["BenChaimberg"]}, + {"keywords":["(@aws-cdk/aws-cloud9)","(aws-cloud9)","(cloud9)","(cloud 9)"],"labels":["@aws-cdk/aws-cloud9"],"assignees":["skinny85"]}, {"keywords":["(@aws-cdk/aws-cloudformation)","(aws-cloudformation)","(cloudformation)","(cloud formation)"],"labels":["@aws-cdk/aws-cloudformation"],"assignees":["rix0rrr"]}, {"keywords":["(@aws-cdk/aws-cloudfront)","(aws-cloudfront)","(cloudfront)","(cloud front)"],"labels":["@aws-cdk/aws-cloudfront"],"assignees":["njlynch"]}, {"keywords":["(@aws-cdk/aws-cloudfront-origins)","(aws-cloudfront-origins)","(cloudfront-origins)","(cloudfront origins)"],"labels":["@aws-cdk/aws-cloudfront-origins"],"assignees":["njlynch"]}, @@ -62,31 +62,31 @@ jobs: {"keywords":["(@aws-cdk/aws-cloudwatch)","(aws-cloudwatch)","(cloudwatch)","(cloud watch)"],"labels":["@aws-cdk/aws-cloudwatch"],"assignees":["madeline-k"]}, {"keywords":["(@aws-cdk/aws-cloudwatch-actions)","(aws-cloudwatch-actions)","(cloudwatch-actions)","(cloudwatch actions)"],"labels":["@aws-cdk/aws-cloudwatch-actions"],"assignees":["madeline-k"]}, {"keywords":["(@aws-cdk/aws-codeartifact)","(aws-codeartifact)","(codeartifact)","(code artifact)","(code-artifact)"],"labels":["@aws-cdk/aws-codeartifact"],"assignees":["njlynch"]}, - {"keywords":["(@aws-cdk/aws-codebuild)","(aws-codebuild)","(codebuild)","(code build)","(code-build)"],"labels":["@aws-cdk/aws-codebuild"],"assignees":["BenChaimberg"]}, - {"keywords":["(@aws-cdk/aws-codecommit)","(aws-codecommit)","(codecommit)","(code commit)", "(code-commit)"],"labels":["@aws-cdk/aws-codecommit"],"assignees":["BenChaimberg"]}, - {"keywords":["(@aws-cdk/aws-codedeploy)","(aws-codedeploy)","(codedeploy)","(code deploy)","(code-deploy)"],"labels":["@aws-cdk/aws-codedeploy"],"assignees":["BenChaimberg"]}, - {"keywords":["(@aws-cdk/aws-codeguruprofiler)","(aws-codeguruprofiler)","(codeguruprofiler)","(codeguru profiler)","(codeguru-profiler)"],"labels":["@aws-cdk/aws-codeguruprofiler"],"assignees":["BenChaimberg"]}, - {"keywords":["(@aws-cdk/aws-codegurureviewer)","(aws-codegurureviewer)","(codegurureviewer)","(codeguru reviewer)","(codeguru-reviewer)"],"labels":["@aws-cdk/aws-codegurureviewer"],"assignees":["BenChaimberg"]}, - {"keywords":["(@aws-cdk/aws-codepipeline)","(aws-codepipeline)","(codepipeline)","(code pipeline)","(code-pipeline)"],"labels":["@aws-cdk/aws-codepipeline"],"assignees":["BenChaimberg"]}, - {"keywords":["(@aws-cdk/aws-codepipeline-actions)","(aws-codepipeline-actions)","(codepipeline-actions)","(codepipeline actions)"],"labels":["@aws-cdk/aws-codepipeline-actions"],"assignees":["BenChaimberg"]}, - {"keywords":["(@aws-cdk/aws-codestar)","(aws-codestar)","(codestar)"],"labels":["@aws-cdk/aws-codestar"],"assignees":["BenChaimberg"]}, - {"keywords":["(@aws-cdk/aws-codestarconnections)","(aws-codestarconnections)","(codestarconnections)","(codestar connections)","(codestar-connections)"],"labels":["@aws-cdk/aws-codestarconnections"],"assignees":["BenChaimberg"]}, - {"keywords":["(@aws-cdk/aws-codestarnotifications)","(aws-codestarnotifications)","(codestarnotifications)","(codestar notifications)","(codestar-notifications)"],"labels":["@aws-cdk/aws-codestarnotifications"],"assignees":["BenChaimberg"]}, + {"keywords":["(@aws-cdk/aws-codebuild)","(aws-codebuild)","(codebuild)","(code build)","(code-build)"],"labels":["@aws-cdk/aws-codebuild"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-codecommit)","(aws-codecommit)","(codecommit)","(code commit)", "(code-commit)"],"labels":["@aws-cdk/aws-codecommit"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-codedeploy)","(aws-codedeploy)","(codedeploy)","(code deploy)","(code-deploy)"],"labels":["@aws-cdk/aws-codedeploy"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-codeguruprofiler)","(aws-codeguruprofiler)","(codeguruprofiler)","(codeguru profiler)","(codeguru-profiler)"],"labels":["@aws-cdk/aws-codeguruprofiler"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-codegurureviewer)","(aws-codegurureviewer)","(codegurureviewer)","(codeguru reviewer)","(codeguru-reviewer)"],"labels":["@aws-cdk/aws-codegurureviewer"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-codepipeline)","(aws-codepipeline)","(codepipeline)","(code pipeline)","(code-pipeline)"],"labels":["@aws-cdk/aws-codepipeline"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-codepipeline-actions)","(aws-codepipeline-actions)","(codepipeline-actions)","(codepipeline actions)"],"labels":["@aws-cdk/aws-codepipeline-actions"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-codestar)","(aws-codestar)","(codestar)"],"labels":["@aws-cdk/aws-codestar"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-codestarconnections)","(aws-codestarconnections)","(codestarconnections)","(codestar connections)","(codestar-connections)"],"labels":["@aws-cdk/aws-codestarconnections"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-codestarnotifications)","(aws-codestarnotifications)","(codestarnotifications)","(codestar notifications)","(codestar-notifications)"],"labels":["@aws-cdk/aws-codestarnotifications"],"assignees":["skinny85"]}, {"keywords":["(@aws-cdk/aws-cognito)","(aws-cognito)","(cognito)"],"labels":["@aws-cdk/aws-cognito"],"assignees":["nija-at"]}, {"keywords":["(@aws-cdk/aws-config)","(aws-config)","(config)"],"labels":["@aws-cdk/aws-config"],"assignees":["rix0rrr"]}, {"keywords":["(@aws-cdk/aws-customerprofiles)","(aws-customerprofiles)","(customerprofiles)"],"labels":["@aws-cdk/aws-customerprofiles"],"assignees":["otaviomacedo"]}, {"keywords":["(@aws-cdk/aws-databrew)","(aws-databrew)","(databrew)"],"labels":["@aws-cdk/aws-databrew"],"assignees":["BenChaimberg"]}, {"keywords":["(@aws-cdk/aws-datapipeline)","(aws-datapipeline)","(datapipeline)","(data pipeline)","(data-pipeline)"],"labels":["@aws-cdk/aws-datapipeline"],"assignees":["BenChaimberg"]}, {"keywords":["(@aws-cdk/aws-datasync)","(aws-datasync)","(datasync)"],"labels":["@aws-cdk/aws-datasync"],"assignees":["BenChaimberg"]}, - {"keywords":["(@aws-cdk/aws-dax)","(aws-dax)","(dax)"],"labels":["@aws-cdk/aws-dax"],"assignees":["BenChaimberg"]}, - {"keywords":["(@aws-cdk/aws-detective)","(aws-detective)","(detective)"],"labels":["@aws-cdk/aws-detective"],"assignees":["BenChaimberg"]}, + {"keywords":["(@aws-cdk/aws-dax)","(aws-dax)","(dax)"],"labels":["@aws-cdk/aws-dax"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-detective)","(aws-detective)","(detective)"],"labels":["@aws-cdk/aws-detective"],"assignees":["skinny85"]}, {"keywords":["(@aws-cdk/aws-devopsguru)","(aws-devopsguru)","(devopsguru)"],"labels":["@aws-cdk/aws-devopsguru"],"assignees":["nija-at"]}, {"keywords":["(@aws-cdk/aws-directoryservice)","(aws-directoryservice)","(directoryservice)","(directory service)","(directory-service)"],"labels":["@aws-cdk/aws-directoryservice"],"assignees":["rix0rrr"]}, {"keywords":["(@aws-cdk/aws-dlm)","(aws-dlm)","(dlm)"],"labels":["@aws-cdk/aws-dlm"],"assignees":["njlynch"]}, {"keywords":["(@aws-cdk/aws-dms)","(aws-dms)","(dms)"],"labels":["@aws-cdk/aws-dms"],"assignees":["njlynch"]}, - {"keywords":["(@aws-cdk/aws-docdb)","(aws-docdb)","(docdb)","(doc db)","(doc-db)"],"labels":["@aws-cdk/aws-docdb"],"assignees":["madeline-k"]}, - {"keywords":["(@aws-cdk/aws-dynamodb)","(aws-dynamodb)","(dynamodb)","(dynamo db)","(dynamo-db)"],"labels":["@aws-cdk/aws-dynamodb"],"assignees":["madeline-k"]}, - {"keywords":["(@aws-cdk/aws-dynamodb-global)","(aws-dynamodb-global)","(dynamodb-global)","(dynamodb global)"],"labels":["@aws-cdk/aws-dynamodb-global"],"assignees":["madeline-k"]}, + {"keywords":["(@aws-cdk/aws-docdb)","(aws-docdb)","(docdb)","(doc db)","(doc-db)"],"labels":["@aws-cdk/aws-docdb"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-dynamodb)","(aws-dynamodb)","(dynamodb)","(dynamo db)","(dynamo-db)"],"labels":["@aws-cdk/aws-dynamodb"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-dynamodb-global)","(aws-dynamodb-global)","(dynamodb-global)","(dynamodb global)"],"labels":["@aws-cdk/aws-dynamodb-global"],"assignees":["skinny85"]}, {"keywords":["(@aws-cdk/aws-ec2)","(aws-ec2)","(ec2)","(vpc)"],"labels":["@aws-cdk/aws-ec2"],"assignees":["njlynch"]}, {"keywords":["(@aws-cdk/aws-ecr)","(aws-ecr)","(ecr)"],"labels":["@aws-cdk/aws-ecr"],"assignees":["madeline-k"]}, {"keywords":["(@aws-cdk/aws-ecr-assets)","(aws-ecr-assets)","(ecr-assets)","(ecr assets)","(ecrassets)"],"labels":["@aws-cdk/aws-ecr-assets"],"assignees":["eladb"]}, @@ -96,7 +96,7 @@ jobs: {"keywords":["(@aws-cdk/aws-eks)","(aws-eks)","(eks)"],"labels":["@aws-cdk/aws-eks"],"assignees":["otaviomacedo"]}, {"keywords":["(@aws-cdk/aws-eks-legacy)","(aws-eks-legacy)","(eks-legacy)"],"labels":["@aws-cdk/aws-eks-legacy"],"assignees":["otaviomacedo"]}, {"keywords":["(@aws-cdk/aws-elasticache)","(aws-elasticache)","(elasticache)","(elastic cache)","(elastic-cache)"],"labels":["@aws-cdk/aws-elasticache"],"assignees":["otaviomacedo"]}, - {"keywords":["(@aws-cdk/aws-elasticbeanstalk)","(aws-elasticbeanstalk)","(elasticbeanstalk)","(elastic beanstalk)","(elastic-beanstalk)"],"labels":["@aws-cdk/aws-elasticbeanstalk"],"assignees":["madeline-k"]}, + {"keywords":["(@aws-cdk/aws-elasticbeanstalk)","(aws-elasticbeanstalk)","(elasticbeanstalk)","(elastic beanstalk)","(elastic-beanstalk)"],"labels":["@aws-cdk/aws-elasticbeanstalk"],"assignees":["skinny85"]}, {"keywords":["(@aws-cdk/aws-elasticloadbalancing)","(aws-elasticloadbalancing)","(elasticloadbalancing)","(elastic loadbalancing)","(elastic-loadbalancing)","(elb)"],"labels":["@aws-cdk/aws-elasticloadbalancing"],"assignees":["njlynch"]}, {"keywords":["(@aws-cdk/aws-elasticloadbalancingv2)","(aws-elasticloadbalancingv2)","(elasticloadbalancingv2)","(elastic-loadbalancing-v2)","(elbv2)","(elb v2)"],"labels":["@aws-cdk/aws-elasticloadbalancingv2"],"assignees":["njlynch"]}, {"keywords":["(@aws-cdk/aws-elasticloadbalancingv2-actions)","(aws-elasticloadbalancingv2-actions)","(elasticloadbalancingv2-actions)"],"labels":["@aws-cdk/aws-elasticloadbalancingv2-actions"],"assignees":["njlynch"]}, @@ -106,7 +106,7 @@ jobs: {"keywords":["(@aws-cdk/aws-emrcontainers)","(aws-emrcontainers)","(emrcontainers)"],"labels":["@aws-cdk/aws-emrcontainers"],"assignees":["BenChaimberg"]}, {"keywords":["(@aws-cdk/aws-events)","(aws-events)","(events)","(eventbridge)","(event-bridge)"],"labels":["@aws-cdk/aws-events"],"assignees":["rix0rrr"]}, {"keywords":["(@aws-cdk/aws-events-targets)","(aws-events-targets)","(events-targets)","(events targets)"],"labels":["@aws-cdk/aws-events-targets"],"assignees":["rix0rrr"]}, - {"keywords":["(@aws-cdk/aws-eventschemas)","(aws-eventschemas)","(eventschemas)","(event schemas)"],"labels":["@aws-cdk/aws-eventschemas"],"assignees":["BenChaimberg"]}, + {"keywords":["(@aws-cdk/aws-eventschemas)","(aws-eventschemas)","(eventschemas)","(event schemas)"],"labels":["@aws-cdk/aws-eventschemas"],"assignees":["skinny85"]}, {"keywords":["(@aws-cdk/aws-finspace)","(aws-finspace)","(finspace)"],"labels":["@aws-cdk/aws-finspace"],"assignees":["njlynch"]}, {"keywords":["(@aws-cdk/aws-fis)","(aws-fis)","(fis)"],"labels":["@aws-cdk/aws-fis"],"assignees":["njlynch"]}, {"keywords":["(@aws-cdk/aws-fms)","(aws-fms)","(fms)"],"labels":["@aws-cdk/aws-fms"],"assignees":["rix0rrr"]}, @@ -116,23 +116,23 @@ jobs: {"keywords":["(@aws-cdk/aws-globalaccelerator)","(aws-globalaccelerator)","(globalaccelerator)","(global accelerator)","(global-accelerator)"],"labels":["@aws-cdk/aws-globalaccelerator"],"assignees":["rix0rrr"]}, {"keywords":["(@aws-cdk/aws-globalaccelerator-endpoints)","(aws-globalaccelerator-endpoints)","(globalaccelerator-endpoints)"],"labels":["@aws-cdk/aws-globalaccelerator-endpoints"],"assignees":["rix0rrr"]}, {"keywords":["(@aws-cdk/aws-glue)","(aws-glue)","(glue)"],"labels":["@aws-cdk/aws-glue"],"assignees":["BenChaimberg"]}, - {"keywords":["(@aws-cdk/aws-greengrass)","(aws-greengrass)","(greengrass)","(green grass)","(green-grass)"],"labels":["@aws-cdk/aws-greengrass"],"assignees":["BenChaimberg"]}, - {"keywords":["(@aws-cdk/aws-greengrassv2)","(aws-greengrassv2)","(greengrassv2)"],"labels":["@aws-cdk/aws-greengrassv2"],"assignees":["BenChaimberg"]}, + {"keywords":["(@aws-cdk/aws-greengrass)","(aws-greengrass)","(greengrass)","(green grass)","(green-grass)"],"labels":["@aws-cdk/aws-greengrass"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-greengrassv2)","(aws-greengrassv2)","(greengrassv2)"],"labels":["@aws-cdk/aws-greengrassv2"],"assignees":["skinny85"]}, {"keywords":["(@aws-cdk/aws-groundstation)","(aws-groundstation)","(groundstation)"],"labels":["@aws-cdk/aws-groundstation"],"assignees":["rix0rrr"]}, {"keywords":["(@aws-cdk/aws-guardduty)","(aws-guardduty)","(guardduty)","(guard duty)","(guard-duty)"],"labels":["@aws-cdk/aws-guardduty"],"assignees":["rix0rrr"]}, {"keywords":["(@aws-cdk/aws-iam)","(aws-iam)","(iam)"],"labels":["@aws-cdk/aws-iam"],"assignees":["rix0rrr"]}, - {"keywords":["(@aws-cdk/aws-imagebuilder)","(aws-imagebuilder)","(imagebuilder)","(image builder)","(image-builder)"],"labels":["@aws-cdk/aws-imagebuilder"],"assignees":["BenChaimberg"]}, - {"keywords":["(@aws-cdk/aws-inspector)","(aws-inspector)","(inspector)"],"labels":["@aws-cdk/aws-inspector"],"assignees":["BenChaimberg"]}, - {"keywords":["(@aws-cdk/aws-iot)","(aws-iot)","(iot)"],"labels":["@aws-cdk/aws-iot"],"assignees":["BenChaimberg"]}, - {"keywords":["(@aws-cdk/aws-iot1click)","(aws-iot1click)","(iot1click)","(iot 1click)"],"labels":["@aws-cdk/aws-iot1click"],"assignees":["BenChaimberg"]}, - {"keywords":["(@aws-cdk/aws-iotanalytics)","(aws-iotanalytics)","(iotanalytics)","(iot analytics)","(iot-analytics)"],"labels":["@aws-cdk/aws-iotanalytics"],"assignees":["BenChaimberg"]}, - {"keywords":["(@aws-cdk/aws-iotevents)","(aws-iotevents)","(iotevents)","(iot events)","(iot-events)"],"labels":["@aws-cdk/aws-iotevents"],"assignees":["BenChaimberg"]}, - {"keywords":["(@aws-cdk/aws-iotfleethub)","(aws-iotfleethub)","(iotfleethub)"],"labels":["@aws-cdk/aws-iotfleethub"],"assignees":["BenChaimberg"]}, - {"keywords":["(@aws-cdk/aws-iotsitewise)","(aws-iotsitewise)","(iotsitewise)","(iot sitewise)","(iot-sitewise)","(iot-site-wise)","(iot site wise)"],"labels":["@aws-cdk/aws-iotsitewise"],"assignees":["BenChaimberg"]}, - {"keywords":["(@aws-cdk/aws-iotthingsgraph)","(aws-iotthingsgraph)","(iotthingsgraph)","(iot things graph)","(iot-things-graph)"],"labels":["@aws-cdk/aws-iotthingsgraph"],"assignees":["BenChaimberg"]}, - {"keywords":["(@aws-cdk/aws-iotwireless)","(aws-iotwireless)","(iotwireless)"],"labels":["@aws-cdk/aws-iotwireless"],"assignees":["BenChaimberg"]}, - {"keywords":["(@aws-cdk/aws-ivs)","(aws-ivs)","(Interactive Video Service)","(ivs)"],"labels":["@aws-cdk/aws-ivs"],"assignees":["BenChaimberg"]}, - {"keywords":["(@aws-cdk/aws-kendra)","(aws-kendra)","(kendra)"],"labels":["@aws-cdk/aws-kendra"],"assignees":["BenChaimberg"]}, + {"keywords":["(@aws-cdk/aws-imagebuilder)","(aws-imagebuilder)","(imagebuilder)","(image builder)","(image-builder)"],"labels":["@aws-cdk/aws-imagebuilder"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-inspector)","(aws-inspector)","(inspector)"],"labels":["@aws-cdk/aws-inspector"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-iot)","(aws-iot)","(iot)"],"labels":["@aws-cdk/aws-iot"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-iot1click)","(aws-iot1click)","(iot1click)","(iot 1click)"],"labels":["@aws-cdk/aws-iot1click"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-iotanalytics)","(aws-iotanalytics)","(iotanalytics)","(iot analytics)","(iot-analytics)"],"labels":["@aws-cdk/aws-iotanalytics"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-iotevents)","(aws-iotevents)","(iotevents)","(iot events)","(iot-events)"],"labels":["@aws-cdk/aws-iotevents"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-iotfleethub)","(aws-iotfleethub)","(iotfleethub)"],"labels":["@aws-cdk/aws-iotfleethub"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-iotsitewise)","(aws-iotsitewise)","(iotsitewise)","(iot sitewise)","(iot-sitewise)","(iot-site-wise)","(iot site wise)"],"labels":["@aws-cdk/aws-iotsitewise"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-iotthingsgraph)","(aws-iotthingsgraph)","(iotthingsgraph)","(iot things graph)","(iot-things-graph)"],"labels":["@aws-cdk/aws-iotthingsgraph"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-iotwireless)","(aws-iotwireless)","(iotwireless)"],"labels":["@aws-cdk/aws-iotwireless"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-ivs)","(aws-ivs)","(Interactive Video Service)","(ivs)"],"labels":["@aws-cdk/aws-ivs"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-kendra)","(aws-kendra)","(kendra)"],"labels":["@aws-cdk/aws-kendra"],"assignees":["skinny85"]}, {"keywords":["(@aws-cdk/aws-kinesis)","(aws-kinesis)","(kinesis)"],"labels":["@aws-cdk/aws-kinesis"],"assignees":["otaviomacedo"]}, {"keywords":["(@aws-cdk/aws-kinesisanalytics)","(aws-kinesisanalytics)","(kinesisanalytics)","(kinesis analytics)","(kinesis-analytics)"],"labels":["@aws-cdk/aws-kinesisanalytics"],"assignees":["otaviomacedo"]}, {"keywords":["(@aws-cdk/aws-kinesisanalytics-flink)","(aws-kinesisanalytics-flink)","(kinesisanalytics-flink)"],"labels":["@aws-cdk/aws-kinesisanalytics-flink"],"assignees":["otaviomacedo"]}, @@ -151,27 +151,27 @@ jobs: {"keywords":["(@aws-cdk/aws-lookoutmetrics)","(aws-lookoutmetrics)","(lookoutmetrics)"],"labels":["@aws-cdk/aws-lookoutmetrics"],"assignees":["BenChaimberg"]}, {"keywords":["(@aws-cdk/aws-lookoutvision)","(aws-lookoutvision)","(lookoutvision)"],"labels":["@aws-cdk/aws-lookoutvision"],"assignees":["BenChaimberg"]}, {"keywords":["(@aws-cdk/aws-macie)","(aws-macie)","(macie)"],"labels":["@aws-cdk/aws-macie"],"assignees":["njlynch"]}, - {"keywords":["(@aws-cdk/aws-managedblockchain)","(aws-managedblockchain)","(managedblockchain)","(managed blockchain)","(managed-blockchain)"],"labels":["@aws-cdk/aws-managedblockchain"],"assignees":["BenChaimberg"]}, - {"keywords":["(@aws-cdk/aws-mediaconnect)","(aws-mediaconnect)","(mediaconnect)"],"labels":["@aws-cdk/aws-mediaconnect"],"assignees":["BenChaimberg"]}, - {"keywords":["(@aws-cdk/aws-mediaconvert)","(aws-mediaconvert)","(mediaconvert)","(media convert)","(media-convert)"],"labels":["@aws-cdk/aws-mediaconvert"],"assignees":["BenChaimberg"]}, - {"keywords":["(@aws-cdk/aws-medialive)","(aws-medialive)","(medialive)","(media live)","(media-live)"],"labels":["@aws-cdk/aws-medialive"],"assignees":["BenChaimberg"]}, - {"keywords":["(@aws-cdk/aws-mediastore)","(aws-mediastore)","(mediastore)","(media store)","(media-store)"],"labels":["@aws-cdk/aws-mediastore"],"assignees":["BenChaimberg"]}, - {"keywords":["(@aws-cdk/aws-mediapackage)","(aws-mediapackage)","(mediapackage)","(media package)","(media-package)"],"labels":["@aws-cdk/aws-mediapackage"],"assignees":["BenChaimberg"]}, + {"keywords":["(@aws-cdk/aws-managedblockchain)","(aws-managedblockchain)","(managedblockchain)","(managed blockchain)","(managed-blockchain)"],"labels":["@aws-cdk/aws-managedblockchain"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-mediaconnect)","(aws-mediaconnect)","(mediaconnect)"],"labels":["@aws-cdk/aws-mediaconnect"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-mediaconvert)","(aws-mediaconvert)","(mediaconvert)","(media convert)","(media-convert)"],"labels":["@aws-cdk/aws-mediaconvert"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-medialive)","(aws-medialive)","(medialive)","(media live)","(media-live)"],"labels":["@aws-cdk/aws-medialive"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-mediastore)","(aws-mediastore)","(mediastore)","(media store)","(media-store)"],"labels":["@aws-cdk/aws-mediastore"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-mediapackage)","(aws-mediapackage)","(mediapackage)","(media package)","(media-package)"],"labels":["@aws-cdk/aws-mediapackage"],"assignees":["skinny85"]}, {"keywords":["(@aws-cdk/aws-msk)","(aws-msk)","(msk)"],"labels":["@aws-cdk/aws-msk"],"assignees":["otaviomacedo"]}, {"keywords":["(@aws-cdk/aws-mwaa)","(aws-mwaa)","(mwaa)"],"labels":["@aws-cdk/aws-mwaa"],"assignees":["rix0rrr"]}, {"keywords":["(@aws-cdk/aws-neptune)","(aws-neptune)","(neptune)"],"labels":["@aws-cdk/aws-neptune"],"assignees":["njlynch"]}, - {"keywords":["(@aws-cdk/aws-networkfirewall)","(aws-networkfirewall)","(networkfirewall)"],"labels":["@aws-cdk/aws-networkfirewall"],"assignees":["BenChaimberg"]}, - {"keywords":["(@aws-cdk/aws-networkmanager)","(aws-networkmanager)","(networkmanager)","(network manager)","(network-manager)"],"labels":["@aws-cdk/aws-networkmanager"],"assignees":["BenChaimberg"]}, + {"keywords":["(@aws-cdk/aws-networkfirewall)","(aws-networkfirewall)","(networkfirewall)"],"labels":["@aws-cdk/aws-networkfirewall"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-networkmanager)","(aws-networkmanager)","(networkmanager)","(network manager)","(network-manager)"],"labels":["@aws-cdk/aws-networkmanager"],"assignees":["skinny85"]}, {"keywords":["(@aws-cdk/aws-nimblestudio)","(aws-nimblestudio)","(nimblestudio)"],"labels":["@aws-cdk/aws-nimblestudio"],"assignees":["madeline-k"]}, {"keywords":["(@aws-cdk/aws-opsworks)","(aws-opsworks)","(opsworks)","(ops works)","(ops-works)"],"labels":["@aws-cdk/aws-opsworks"],"assignees":["madeline-k"]}, {"keywords":["(@aws-cdk/aws-opsworkscm)","(aws-opsworkscm)","(opsworkscm)","(opsworks cm)","(opsworks-cm)"],"labels":["@aws-cdk/aws-opsworkscm"],"assignees":["madeline-k"]}, {"keywords":["(@aws-cdk/aws-personalize)","(aws-personalize)","(personalize)"],"labels":["@aws-cdk/aws-personalize"],"assignees":["njlynch"]}, {"keywords":["(@aws-cdk/aws-pinpoint)","(aws-pinpoint)","(pinpoint)"],"labels":["@aws-cdk/aws-pinpoint"],"assignees":["otaviomacedo"]}, {"keywords":["(@aws-cdk/aws-pinpointemail)","(aws-pinpointemail)","(pinpointemail)","(pinpoint email)","(pinpoint-email)"],"labels":["@aws-cdk/aws-pinpointemail"],"assignees":["otaviomacedo"]}, - {"keywords":["(@aws-cdk/aws-qldb)","(aws-qldb)","(qldb)"],"labels":["@aws-cdk/aws-qldb"],"assignees":["BenChaimberg"]}, + {"keywords":["(@aws-cdk/aws-qldb)","(aws-qldb)","(qldb)"],"labels":["@aws-cdk/aws-qldb"],"assignees":["skinny85"]}, {"keywords":["(@aws-cdk/aws-quicksight)","(aws-quicksight)","(quicksight)"],"labels":["@aws-cdk/aws-quicksight"],"assignees":["BenChaimberg"]}, {"keywords":["(@aws-cdk/aws-ram)","(aws-ram)","(ram)"],"labels":["@aws-cdk/aws-ram"],"assignees":["madeline-k"]}, - {"keywords":["(@aws-cdk/aws-rds)","(aws-rds)","(rds)"],"labels":["@aws-cdk/aws-rds"],"assignees":["BenChaimberg"]}, + {"keywords":["(@aws-cdk/aws-rds)","(aws-rds)","(rds)"],"labels":["@aws-cdk/aws-rds"],"assignees":["skinny85"]}, {"keywords":["(@aws-cdk/aws-redshift)","(aws-redshift)","(redshift)","(red shift)","(red-shift)"],"labels":["@aws-cdk/aws-redshift"],"assignees":["njlynch"]}, {"keywords":["(@aws-cdk/aws-resourcegroups)","(aws-resourcegroups)","(resourcegroups)","(resource groups)","(resource-groups)"],"labels":["@aws-cdk/aws-resourcegroups"],"assignees":["madeline-k"]}, {"keywords":["(@aws-cdk/aws-robomaker)","(aws-robomaker)","(robomaker)","(robo maker)","(robo-maker)"],"labels":["@aws-cdk/aws-robomaker"],"assignees":["njlynch"]}, @@ -189,9 +189,9 @@ jobs: {"keywords":["(@aws-cdk/aws-sam)","(aws-sam)","(sam)"],"labels":["@aws-cdk/aws-sam"],"assignees":["njlynch"]}, {"keywords":["(@aws-cdk/aws-sdb)","(aws-sdb)","(sdb)"],"labels":["@aws-cdk/aws-sdb"],"assignees":["njlynch"]}, {"keywords":["(@aws-cdk/aws-secretsmanager)","(aws-secretsmanager)","(secretsmanager)","(secrets manager)","(secrets-manager)"],"labels":["@aws-cdk/aws-secretsmanager"],"assignees":["njlynch"]}, - {"keywords":["(@aws-cdk/aws-securityhub)","(aws-securityhub)","(securityhub)","(security hub)","(security-hub)"],"labels":["@aws-cdk/aws-securityhub"],"assignees":["BenChaimberg"]}, - {"keywords":["(@aws-cdk/aws-servicecatalog)","(aws-servicecatalog)","(servicecatalog)","(service catalog)","(service-catalog)"],"labels":["@aws-cdk/aws-servicecatalog"],"assignees":["BenChaimberg"]}, - {"keywords":["(@aws-cdk/aws-servicecatalogappregistry)","(aws-servicecatalogappregistry)","(servicecatalogappregistry)"],"labels":["@aws-cdk/aws-servicecatalogappregistry"],"assignees":["BenChaimberg"]}, + {"keywords":["(@aws-cdk/aws-securityhub)","(aws-securityhub)","(securityhub)","(security hub)","(security-hub)"],"labels":["@aws-cdk/aws-securityhub"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-servicecatalog)","(aws-servicecatalog)","(servicecatalog)","(service catalog)","(service-catalog)"],"labels":["@aws-cdk/aws-servicecatalog"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-servicecatalogappregistry)","(aws-servicecatalogappregistry)","(servicecatalogappregistry)"],"labels":["@aws-cdk/aws-servicecatalogappregistry"],"assignees":["skinny85"]}, {"keywords":["(@aws-cdk/aws-servicediscovery)","(aws-servicediscovery)","(servicediscovery)","(service discovery)","(service-discovery)"],"labels":["@aws-cdk/aws-servicediscovery"],"assignees":["madeline-k"]}, {"keywords":["(@aws-cdk/aws-ses)","(aws-ses)","(ses)"],"labels":["@aws-cdk/aws-ses"],"assignees":["otaviomacedo"]}, {"keywords":["(@aws-cdk/aws-ses-actions)","(aws-ses-actions)","(ses-actions)","(ses actions)"],"labels":["@aws-cdk/aws-ses-actions"],"assignees":["otaviomacedo"]}, @@ -200,11 +200,11 @@ jobs: {"keywords":["(@aws-cdk/aws-sns-subscriptions)","(aws-sns-subscriptions)","(sns-subscriptions)","(sns subscriptions)"],"labels":["@aws-cdk/aws-sns-subscriptions"],"assignees":["njlynch"]}, {"keywords":["(@aws-cdk/aws-sqs)","(aws-sqs)","(sqs)"],"labels":["@aws-cdk/aws-sqs"],"assignees":["njlynch"]}, {"keywords":["(@aws-cdk/aws-ssm)","(aws-ssm)","(ssm)"],"labels":["@aws-cdk/aws-ssm"],"assignees":["njlynch"]}, - {"keywords":["(@aws-cdk/aws-sso)","(aws-sso)","(sso)"],"labels":["@aws-cdk/aws-sso"],"assignees":["BenChaimberg"]}, + {"keywords":["(@aws-cdk/aws-sso)","(aws-sso)","(sso)"],"labels":["@aws-cdk/aws-sso"],"assignees":["skinny85"]}, {"keywords":["(@aws-cdk/aws-stepfunctions)","(aws-stepfunctions)","(stepfunctions)","(step functions)","(step-functions)"],"labels":["@aws-cdk/aws-stepfunctions"],"assignees":["BenChaimberg"]}, {"keywords":["(@aws-cdk/aws-stepfunctions-tasks)","(aws-stepfunctions-tasks)","(stepfunctions-tasks)","(stepfunctions tasks)"],"labels":["@aws-cdk/aws-stepfunctions-tasks"],"assignees":["BenChaimberg"]}, {"keywords":["(@aws-cdk/aws-synthetics)","(aws-synthetics)","(synthetics)"],"labels":["@aws-cdk/aws-synthetics"],"assignees":["BenChaimberg"]}, - {"keywords":["(@aws-cdk/aws-timestream)","(aws-timestream)","(timestream)"],"labels":["@aws-cdk/aws-timestream"],"assignees":["BenChaimberg"]}, + {"keywords":["(@aws-cdk/aws-timestream)","(aws-timestream)","(timestream)"],"labels":["@aws-cdk/aws-timestream"],"assignees":["skinny85"]}, {"keywords":["(@aws-cdk/aws-transfer)","(aws-transfer)","(transfer)"],"labels":["@aws-cdk/aws-transfer"],"assignees":["otaviomacedo"]}, {"keywords":["(@aws-cdk/aws-waf)","(aws-waf)","(waf)"],"labels":["@aws-cdk/aws-waf"],"assignees":["njlynch"]}, {"keywords":["(@aws-cdk/aws-wafregional)","(aws-wafregional)","(wafregional)","(waf regional)","(waf-regional)"],"labels":["@aws-cdk/aws-wafregional"],"assignees":["njlynch"]}, @@ -213,16 +213,16 @@ jobs: {"keywords":["(@aws-cdk/aws-xray)","(aws-xray)","(xray)"],"labels":["@aws-cdk/aws-xray"],"assignees":["nija-at"]}, {"keywords":["(@aws-cdk/cfnspec)","(cfnspec)","(cfn spec)","(cfn-spec)"],"labels":["@aws-cdk/cfnspec"],"assignees":["rix0rrr"]}, {"keywords":["(@aws-cdk/cloud-assembly-schema)","(cloud-assembly-schema)","(cloud assembly schema)"],"labels":["@aws-cdk/cloud-assembly-schema"],"assignees":["rix0rrr"]}, - {"keywords":["(@aws-cdk/cloudformation-diff)","(cloudformation-diff)","(cloudformation diff)","(cfn diff)"],"labels":["@aws-cdk/cloudformation-diff"],"assignees":["BenChaimberg"]}, - {"keywords":["(@aws-cdk/cloudformation-include)","(cloudformation-include)","(cloudformation include)","(cfn include)","(cfn-include)"],"labels":["@aws-cdk/cloudformation-include"],"assignees":["BenChaimberg"]}, + {"keywords":["(@aws-cdk/cloudformation-diff)","(cloudformation-diff)","(cloudformation diff)","(cfn diff)"],"labels":["@aws-cdk/cloudformation-diff"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/cloudformation-include)","(cloudformation-include)","(cloudformation include)","(cfn include)","(cfn-include)"],"labels":["@aws-cdk/cloudformation-include"],"assignees":["skinny85"]}, {"keywords":["(@aws-cdk/core)","(core)"],"labels":["@aws-cdk/core"],"assignees":["rix0rrr"]}, {"keywords":["(@aws-cdk/custom-resources)","(custom-resources)","(custom resources)"],"labels":["@aws-cdk/custom-resources"],"assignees":["rix0rrr"]}, {"keywords":["(@aws-cdk/cx-api)","(cx-api)","(cx api)"],"labels":["@aws-cdk/cx-api"],"assignees":["rix0rrr"]}, {"keywords":["(@aws-cdk/aws-lambda-layer-awscli)","(aws-lambda-layer-awscli)","(lambda-layer-awscli)"],"labels":["@aws-cdk/aws-lambda-layer-awscli"],"assignees":["rix0rrr"]}, {"keywords":["(@aws-cdk/aws-lambda-layer-kubectl)","(aws-lambda-layer-kubectl)","(lambda-layer-kubectl)"],"labels":["@aws-cdk/aws-lambda-layer-kubectl"],"assignees":["eladb"]}, {"keywords":["(@aws-cdk/pipelines)","(pipelines)","(cdk pipelines)","(cdk-pipelines)"],"labels":["@aws-cdk/pipelines"],"assignees":["rix0rrr"]}, - {"keywords":["(@aws-cdk/region-info)","(region-info)","(region info)"],"labels":["@aws-cdk/region-info"],"assignees":["BenChaimberg"]}, + {"keywords":["(@aws-cdk/region-info)","(region-info)","(region info)"],"labels":["@aws-cdk/region-info"],"assignees":["skinny85"]}, {"keywords":["(aws-cdk-lib)","(cdk-v2)", "(v2)", "(ubergen)"],"labels":["aws-cdk-lib"],"assignees":["nija-at"]}, {"keywords":["(monocdk)","(monocdk-experiment)"],"labels":["monocdk"],"assignees":["nija-at"]}, - {"keywords":["(@aws-cdk/yaml-cfn)","(aws-yaml-cfn)","(yaml-cfn)"],"labels":["@aws-cdk/aws-yaml-cfn"],"assignees":["BenChaimberg"]} + {"keywords":["(@aws-cdk/yaml-cfn)","(aws-yaml-cfn)","(yaml-cfn)"],"labels":["@aws-cdk/aws-yaml-cfn"],"assignees":["skinny85"]} ] From b1d69d7b06cd2a2ae8f578e217bdf7fef50a0163 Mon Sep 17 00:00:00 2001 From: Otavio Macedo Date: Wed, 25 Aug 2021 17:53:28 +0100 Subject: [PATCH 21/39] fix(s3): bucket is not emptied before update when the name changes (#16203) Changing the bucket name leads CloudFormation to try to delete the bucket and create a new one with the new name. If the bucket is not empty, the deployment will fail. With this change, the custom resource will clear the old bucket when it detects that there has been a name change. NB: this custom resource is created only when `autoDeleteObjects: true` is passed to the bucket. Fixes #14011. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../test/integ.s3-bucket.lit.expected.json | 18 +-- .../lib/auto-delete-objects-handler/index.ts | 24 +++- .../test/auto-delete-objects-handler.test.ts | 103 +++++++++++++++++- ...g.bucket-auto-delete-objects.expected.json | 18 +-- 4 files changed, 139 insertions(+), 24 deletions(-) diff --git a/packages/@aws-cdk/aws-kinesisfirehose-destinations/test/integ.s3-bucket.lit.expected.json b/packages/@aws-cdk/aws-kinesisfirehose-destinations/test/integ.s3-bucket.lit.expected.json index 2a85e28a6984d..ba6e2aecacda8 100644 --- a/packages/@aws-cdk/aws-kinesisfirehose-destinations/test/integ.s3-bucket.lit.expected.json +++ b/packages/@aws-cdk/aws-kinesisfirehose-destinations/test/integ.s3-bucket.lit.expected.json @@ -102,7 +102,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameters1a8becf42c48697a059094af1e94aa6bc6df0512d30433db8c22618ca02dfca1S3BucketF01ADF6B" + "Ref": "AssetParametersfe5df38824187483182e1459db47adfae2a515aa4caedd437fc4033a0c5b3de9S3BucketD715D8B0" }, "S3Key": { "Fn::Join": [ @@ -115,7 +115,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters1a8becf42c48697a059094af1e94aa6bc6df0512d30433db8c22618ca02dfca1S3VersionKey6FC34F51" + "Ref": "AssetParametersfe5df38824187483182e1459db47adfae2a515aa4caedd437fc4033a0c5b3de9S3VersionKey6E76822C" } ] } @@ -128,7 +128,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters1a8becf42c48697a059094af1e94aa6bc6df0512d30433db8c22618ca02dfca1S3VersionKey6FC34F51" + "Ref": "AssetParametersfe5df38824187483182e1459db47adfae2a515aa4caedd437fc4033a0c5b3de9S3VersionKey6E76822C" } ] } @@ -751,17 +751,17 @@ } }, "Parameters": { - "AssetParameters1a8becf42c48697a059094af1e94aa6bc6df0512d30433db8c22618ca02dfca1S3BucketF01ADF6B": { + "AssetParametersfe5df38824187483182e1459db47adfae2a515aa4caedd437fc4033a0c5b3de9S3BucketD715D8B0": { "Type": "String", - "Description": "S3 bucket for asset \"1a8becf42c48697a059094af1e94aa6bc6df0512d30433db8c22618ca02dfca1\"" + "Description": "S3 bucket for asset \"fe5df38824187483182e1459db47adfae2a515aa4caedd437fc4033a0c5b3de9\"" }, - "AssetParameters1a8becf42c48697a059094af1e94aa6bc6df0512d30433db8c22618ca02dfca1S3VersionKey6FC34F51": { + "AssetParametersfe5df38824187483182e1459db47adfae2a515aa4caedd437fc4033a0c5b3de9S3VersionKey6E76822C": { "Type": "String", - "Description": "S3 key for asset version \"1a8becf42c48697a059094af1e94aa6bc6df0512d30433db8c22618ca02dfca1\"" + "Description": "S3 key for asset version \"fe5df38824187483182e1459db47adfae2a515aa4caedd437fc4033a0c5b3de9\"" }, - "AssetParameters1a8becf42c48697a059094af1e94aa6bc6df0512d30433db8c22618ca02dfca1ArtifactHash9ECACDFD": { + "AssetParametersfe5df38824187483182e1459db47adfae2a515aa4caedd437fc4033a0c5b3de9ArtifactHash9AE3702B": { "Type": "String", - "Description": "Artifact hash for asset \"1a8becf42c48697a059094af1e94aa6bc6df0512d30433db8c22618ca02dfca1\"" + "Description": "Artifact hash for asset \"fe5df38824187483182e1459db47adfae2a515aa4caedd437fc4033a0c5b3de9\"" }, "AssetParameters5ee078f2a1957fe672d6cfd84faf49e07b8460758b5cd2669b3df1212a14cd19S3BucketFEDDFB43": { "Type": "String", diff --git a/packages/@aws-cdk/aws-s3/lib/auto-delete-objects-handler/index.ts b/packages/@aws-cdk/aws-s3/lib/auto-delete-objects-handler/index.ts index 5dd144b446e8e..f431aacf3fca2 100644 --- a/packages/@aws-cdk/aws-s3/lib/auto-delete-objects-handler/index.ts +++ b/packages/@aws-cdk/aws-s3/lib/auto-delete-objects-handler/index.ts @@ -6,10 +6,25 @@ const s3 = new S3(); export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent) { switch (event.RequestType) { case 'Create': - case 'Update': return; + case 'Update': + return onUpdate(event); case 'Delete': - return onDelete(event); + return onDelete(event.ResourceProperties?.BucketName); + } +} + +async function onUpdate(event: AWSLambda.CloudFormationCustomResourceEvent) { + const updateEvent = event as AWSLambda.CloudFormationCustomResourceUpdateEvent; + const oldBucketName = updateEvent.OldResourceProperties?.BucketName; + const newBucketName = updateEvent.ResourceProperties?.BucketName; + const bucketNameHasChanged = newBucketName != null && oldBucketName != null && newBucketName !== oldBucketName; + + /* If the name of the bucket has changed, CloudFormation will try to delete the bucket + and create a new one with the new name. So we have to delete the contents of the + bucket so that this operation does not fail. */ + if (bucketNameHasChanged) { + return onDelete(oldBucketName); } } @@ -23,7 +38,7 @@ async function emptyBucket(bucketName: string) { const contents = [...listedObjects.Versions ?? [], ...listedObjects.DeleteMarkers ?? []]; if (contents.length === 0) { return; - }; + } const records = contents.map((record: any) => ({ Key: record.Key, VersionId: record.VersionId })); await s3.deleteObjects({ Bucket: bucketName, Delete: { Objects: records } }).promise(); @@ -33,8 +48,7 @@ async function emptyBucket(bucketName: string) { } } -async function onDelete(deleteEvent: AWSLambda.CloudFormationCustomResourceDeleteEvent) { - const bucketName = deleteEvent.ResourceProperties?.BucketName; +async function onDelete(bucketName?: string) { if (!bucketName) { throw new Error('No BucketName was provided.'); } diff --git a/packages/@aws-cdk/aws-s3/test/auto-delete-objects-handler.test.ts b/packages/@aws-cdk/aws-s3/test/auto-delete-objects-handler.test.ts index e5a7072441974..b09b9e5e4f5b3 100644 --- a/packages/@aws-cdk/aws-s3/test/auto-delete-objects-handler.test.ts +++ b/packages/@aws-cdk/aws-s3/test/auto-delete-objects-handler.test.ts @@ -37,7 +37,51 @@ test('does nothing on create event', async () => { expect(mockS3Client.deleteObjects).toHaveBeenCalledTimes(0); }); -test('does nothing on update event', async () => { +test('does nothing on update event when everything remains the same', async () => { + // GIVEN + const event: Partial = { + RequestType: 'Update', + ResourceProperties: { + ServiceToken: 'Foo', + BucketName: 'MyBucket', + }, + OldResourceProperties: { + ServiceToken: 'Foo', + BucketName: 'MyBucket', + }, + }; + + // WHEN + await invokeHandler(event); + + // THEN + expect(mockS3Client.listObjectVersions).toHaveBeenCalledTimes(0); + expect(mockS3Client.deleteObjects).toHaveBeenCalledTimes(0); +}); + +test('does nothing on update event when the bucket name remains the same but the service token changes', async () => { + // GIVEN + const event: Partial = { + RequestType: 'Update', + ResourceProperties: { + ServiceToken: 'Foo', + BucketName: 'MyBucket', + }, + OldResourceProperties: { + ServiceToken: 'Bar', + BucketName: 'MyBucket', + }, + }; + + // WHEN + await invokeHandler(event); + + // THEN + expect(mockS3Client.listObjectVersions).toHaveBeenCalledTimes(0); + expect(mockS3Client.deleteObjects).toHaveBeenCalledTimes(0); +}); + +test('does nothing on update event when the old resource properties are absent', async () => { // GIVEN const event: Partial = { RequestType: 'Update', @@ -55,6 +99,63 @@ test('does nothing on update event', async () => { expect(mockS3Client.deleteObjects).toHaveBeenCalledTimes(0); }); +test('does nothing on update event when the new resource properties are absent', async () => { + // GIVEN + const event: Partial = { + RequestType: 'Update', + OldResourceProperties: { + ServiceToken: 'Foo', + BucketName: 'MyBucket', + }, + }; + + // WHEN + await invokeHandler(event); + + // THEN + expect(mockS3Client.listObjectVersions).toHaveBeenCalledTimes(0); + expect(mockS3Client.deleteObjects).toHaveBeenCalledTimes(0); +}); + +test('deletes all objects when the name changes on update event', async () => { + // GIVEN + mockS3Client.promise.mockResolvedValue({ // listObjectVersions() call + Versions: [ + { Key: 'Key1', VersionId: 'VersionId1' }, + { Key: 'Key2', VersionId: 'VersionId2' }, + ], + }); + + const event: Partial = { + RequestType: 'Update', + OldResourceProperties: { + ServiceToken: 'Foo', + BucketName: 'MyBucket', + }, + ResourceProperties: { + ServiceToken: 'Foo', + BucketName: 'MyBucket-renamed', + }, + }; + + // WHEN + await invokeHandler(event); + + // THEN + expect(mockS3Client.listObjectVersions).toHaveBeenCalledTimes(1); + expect(mockS3Client.listObjectVersions).toHaveBeenCalledWith({ Bucket: 'MyBucket' }); + expect(mockS3Client.deleteObjects).toHaveBeenCalledTimes(1); + expect(mockS3Client.deleteObjects).toHaveBeenCalledWith({ + Bucket: 'MyBucket', + Delete: { + Objects: [ + { Key: 'Key1', VersionId: 'VersionId1' }, + { Key: 'Key2', VersionId: 'VersionId2' }, + ], + }, + }); +}); + test('deletes no objects on delete event when bucket has no objects', async () => { // GIVEN mockS3Client.promise.mockResolvedValue({ Versions: [] }); // listObjectVersions() call diff --git a/packages/@aws-cdk/aws-s3/test/integ.bucket-auto-delete-objects.expected.json b/packages/@aws-cdk/aws-s3/test/integ.bucket-auto-delete-objects.expected.json index 107132c1cd2dd..6623d9d3e7a8c 100644 --- a/packages/@aws-cdk/aws-s3/test/integ.bucket-auto-delete-objects.expected.json +++ b/packages/@aws-cdk/aws-s3/test/integ.bucket-auto-delete-objects.expected.json @@ -102,7 +102,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameters1a8becf42c48697a059094af1e94aa6bc6df0512d30433db8c22618ca02dfca1S3BucketF01ADF6B" + "Ref": "AssetParametersfe5df38824187483182e1459db47adfae2a515aa4caedd437fc4033a0c5b3de9S3BucketD715D8B0" }, "S3Key": { "Fn::Join": [ @@ -115,7 +115,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters1a8becf42c48697a059094af1e94aa6bc6df0512d30433db8c22618ca02dfca1S3VersionKey6FC34F51" + "Ref": "AssetParametersfe5df38824187483182e1459db47adfae2a515aa4caedd437fc4033a0c5b3de9S3VersionKey6E76822C" } ] } @@ -128,7 +128,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters1a8becf42c48697a059094af1e94aa6bc6df0512d30433db8c22618ca02dfca1S3VersionKey6FC34F51" + "Ref": "AssetParametersfe5df38824187483182e1459db47adfae2a515aa4caedd437fc4033a0c5b3de9S3VersionKey6E76822C" } ] } @@ -289,17 +289,17 @@ } }, "Parameters": { - "AssetParameters1a8becf42c48697a059094af1e94aa6bc6df0512d30433db8c22618ca02dfca1S3BucketF01ADF6B": { + "AssetParametersfe5df38824187483182e1459db47adfae2a515aa4caedd437fc4033a0c5b3de9S3BucketD715D8B0": { "Type": "String", - "Description": "S3 bucket for asset \"1a8becf42c48697a059094af1e94aa6bc6df0512d30433db8c22618ca02dfca1\"" + "Description": "S3 bucket for asset \"fe5df38824187483182e1459db47adfae2a515aa4caedd437fc4033a0c5b3de9\"" }, - "AssetParameters1a8becf42c48697a059094af1e94aa6bc6df0512d30433db8c22618ca02dfca1S3VersionKey6FC34F51": { + "AssetParametersfe5df38824187483182e1459db47adfae2a515aa4caedd437fc4033a0c5b3de9S3VersionKey6E76822C": { "Type": "String", - "Description": "S3 key for asset version \"1a8becf42c48697a059094af1e94aa6bc6df0512d30433db8c22618ca02dfca1\"" + "Description": "S3 key for asset version \"fe5df38824187483182e1459db47adfae2a515aa4caedd437fc4033a0c5b3de9\"" }, - "AssetParameters1a8becf42c48697a059094af1e94aa6bc6df0512d30433db8c22618ca02dfca1ArtifactHash9ECACDFD": { + "AssetParametersfe5df38824187483182e1459db47adfae2a515aa4caedd437fc4033a0c5b3de9ArtifactHash9AE3702B": { "Type": "String", - "Description": "Artifact hash for asset \"1a8becf42c48697a059094af1e94aa6bc6df0512d30433db8c22618ca02dfca1\"" + "Description": "Artifact hash for asset \"fe5df38824187483182e1459db47adfae2a515aa4caedd437fc4033a0c5b3de9\"" }, "AssetParametersf7ee44e9b6217d201200d9abd42c6493b0b11e86be8a7f36163c3ea049c54653S3BucketDB5FAF47": { "Type": "String", From 5812340bfaebfef2bc41d5e7fbd0d45af92e0e49 Mon Sep 17 00:00:00 2001 From: Eli Polonsky Date: Wed, 25 Aug 2021 11:05:24 -0700 Subject: [PATCH 22/39] chore(integ): fix regression suite failures (#16226) Follow up to https://github.com/aws/aws-cdk/pull/16216 We need to apply a patch for the regression suite to pass because its running the older tests which still make the wrong assertion. See `NOTES.md` for more details. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../cli-regression-patches/v1.119.0/NOTES.md | 5 + .../v1.119.0/cli.integtest.js | 659 ++++++++++++++++++ 2 files changed, 664 insertions(+) create mode 100644 packages/aws-cdk/test/integ/cli-regression-patches/v1.119.0/NOTES.md create mode 100644 packages/aws-cdk/test/integ/cli-regression-patches/v1.119.0/cli.integtest.js diff --git a/packages/aws-cdk/test/integ/cli-regression-patches/v1.119.0/NOTES.md b/packages/aws-cdk/test/integ/cli-regression-patches/v1.119.0/NOTES.md new file mode 100644 index 0000000000000..5ca96b632f75a --- /dev/null +++ b/packages/aws-cdk/test/integ/cli-regression-patches/v1.119.0/NOTES.md @@ -0,0 +1,5 @@ +This [PR](https://github.com/aws/aws-cdk/pull/16205) added a node version check to our CLI courtesy of [`@jsii/check-node/run`](https://github.com/aws/jsii/tree/main/packages/%40jsii/check-node). + +This check now causes the CLI to print a deprecation warning that changes the output of the `synth` command. We don't consider this a breaking change since we have no guarantess for CLI output, but it did break some our integ tests (namely `cdk synth`) that used to rely on a specific output. + +This patch brings the [fix](https://github.com/aws/aws-cdk/pull/16216) into the regression suite. \ No newline at end of file diff --git a/packages/aws-cdk/test/integ/cli-regression-patches/v1.119.0/cli.integtest.js b/packages/aws-cdk/test/integ/cli-regression-patches/v1.119.0/cli.integtest.js new file mode 100644 index 0000000000000..b8009dcaab00e --- /dev/null +++ b/packages/aws-cdk/test/integ/cli-regression-patches/v1.119.0/cli.integtest.js @@ -0,0 +1,659 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +const fs_1 = require("fs"); +const os = require("os"); +const path = require("path"); +const aws_1 = require("../helpers/aws"); +const cdk_1 = require("../helpers/cdk"); +const test_helpers_1 = require("../helpers/test-helpers"); +jest.setTimeout(600 * 1000); +test_helpers_1.integTest('VPC Lookup', cdk_1.withDefaultFixture(async (fixture) => { + fixture.log('Making sure we are clean before starting.'); + await fixture.cdkDestroy('define-vpc', { modEnv: { ENABLE_VPC_TESTING: 'DEFINE' } }); + fixture.log('Setting up: creating a VPC with known tags'); + await fixture.cdkDeploy('define-vpc', { modEnv: { ENABLE_VPC_TESTING: 'DEFINE' } }); + fixture.log('Setup complete!'); + fixture.log('Verifying we can now import that VPC'); + await fixture.cdkDeploy('import-vpc', { modEnv: { ENABLE_VPC_TESTING: 'IMPORT' } }); +})); +test_helpers_1.integTest('Two ways of shoing the version', cdk_1.withDefaultFixture(async (fixture) => { + const version1 = await fixture.cdk(['version'], { verbose: false }); + const version2 = await fixture.cdk(['--version'], { verbose: false }); + expect(version1).toEqual(version2); +})); +test_helpers_1.integTest('Termination protection', cdk_1.withDefaultFixture(async (fixture) => { + const stackName = 'termination-protection'; + await fixture.cdkDeploy(stackName); + // Try a destroy that should fail + await expect(fixture.cdkDestroy(stackName)).rejects.toThrow('exited with error'); + // Can update termination protection even though the change set doesn't contain changes + await fixture.cdkDeploy(stackName, { modEnv: { TERMINATION_PROTECTION: 'FALSE' } }); + await fixture.cdkDestroy(stackName); +})); +test_helpers_1.integTest('cdk synth', cdk_1.withDefaultFixture(async (fixture) => { + await fixture.cdk(['synth', fixture.fullStackName('test-1')]); + const template1 = await readTemplate(fixture, 'test-1'); + expect(template1).toEqual({ + Resources: { + topic69831491: { + Type: 'AWS::SNS::Topic', + Metadata: { + 'aws:cdk:path': `${fixture.stackNamePrefix}-test-1/topic/Resource`, + }, + }, + }, + }); + await fixture.cdk(['synth', fixture.fullStackName('test-2')], { verbose: false }); + const template2 = await readTemplate(fixture, 'test-2'); + expect(template2).toEqual({ + Resources: { + topic152D84A37: { + Type: 'AWS::SNS::Topic', + Metadata: { + 'aws:cdk:path': `${fixture.stackNamePrefix}-test-2/topic1/Resource`, + }, + }, + topic2A4FB547F: { + Type: 'AWS::SNS::Topic', + Metadata: { + 'aws:cdk:path': `${fixture.stackNamePrefix}-test-2/topic2/Resource`, + }, + }, + }, + }); +})); +test_helpers_1.integTest('ssm parameter provider error', cdk_1.withDefaultFixture(async (fixture) => { + await expect(fixture.cdk(['synth', + fixture.fullStackName('missing-ssm-parameter'), + '-c', 'test:ssm-parameter-name=/does/not/exist'], { + allowErrExit: true, + })).resolves.toContain('SSM parameter not available in account'); +})); +test_helpers_1.integTest('automatic ordering', cdk_1.withDefaultFixture(async (fixture) => { + // Deploy the consuming stack which will include the producing stack + await fixture.cdkDeploy('order-consuming'); + // Destroy the providing stack which will include the consuming stack + await fixture.cdkDestroy('order-providing'); +})); +test_helpers_1.integTest('context setting', cdk_1.withDefaultFixture(async (fixture) => { + await fs_1.promises.writeFile(path.join(fixture.integTestDir, 'cdk.context.json'), JSON.stringify({ + contextkey: 'this is the context value', + })); + try { + await expect(fixture.cdk(['context'])).resolves.toContain('this is the context value'); + // Test that deleting the contextkey works + await fixture.cdk(['context', '--reset', 'contextkey']); + await expect(fixture.cdk(['context'])).resolves.not.toContain('this is the context value'); + // Test that forced delete of the context key does not throw + await fixture.cdk(['context', '-f', '--reset', 'contextkey']); + } + finally { + await fs_1.promises.unlink(path.join(fixture.integTestDir, 'cdk.context.json')); + } +})); +test_helpers_1.integTest('context in stage propagates to top', cdk_1.withDefaultFixture(async (fixture) => { + await expect(fixture.cdkSynth({ + // This will make it error to prove that the context bubbles up, and also that we can fail on command + options: ['--no-lookups'], + modEnv: { + INTEG_STACK_SET: 'stage-using-context', + }, + allowErrExit: true, + })).resolves.toContain('Context lookups have been disabled'); +})); +test_helpers_1.integTest('deploy', cdk_1.withDefaultFixture(async (fixture) => { + var _a; + const stackArn = await fixture.cdkDeploy('test-2', { captureStderr: false }); + // verify the number of resources in the stack + const response = await fixture.aws.cloudFormation('describeStackResources', { + StackName: stackArn, + }); + expect((_a = response.StackResources) === null || _a === void 0 ? void 0 : _a.length).toEqual(2); +})); +test_helpers_1.integTest('deploy all', cdk_1.withDefaultFixture(async (fixture) => { + const arns = await fixture.cdkDeploy('test-*', { captureStderr: false }); + // verify that we only deployed a single stack (there's a single ARN in the output) + expect(arns.split('\n').length).toEqual(2); +})); +test_helpers_1.integTest('nested stack with parameters', cdk_1.withDefaultFixture(async (fixture) => { + var _a; + // STACK_NAME_PREFIX is used in MyTopicParam to allow multiple instances + // of this test to run in parallel, othewise they will attempt to create the same SNS topic. + const stackArn = await fixture.cdkDeploy('with-nested-stack-using-parameters', { + options: ['--parameters', `MyTopicParam=${fixture.stackNamePrefix}ThereIsNoSpoon`], + captureStderr: false, + }); + // verify that we only deployed a single stack (there's a single ARN in the output) + expect(stackArn.split('\n').length).toEqual(1); + // verify the number of resources in the stack + const response = await fixture.aws.cloudFormation('describeStackResources', { + StackName: stackArn, + }); + expect((_a = response.StackResources) === null || _a === void 0 ? void 0 : _a.length).toEqual(1); +})); +test_helpers_1.integTest('deploy without execute a named change set', cdk_1.withDefaultFixture(async (fixture) => { + var _a; + const changeSetName = 'custom-change-set-name'; + const stackArn = await fixture.cdkDeploy('test-2', { + options: ['--no-execute', '--change-set-name', changeSetName], + captureStderr: false, + }); + // verify that we only deployed a single stack (there's a single ARN in the output) + expect(stackArn.split('\n').length).toEqual(1); + const response = await fixture.aws.cloudFormation('describeStacks', { + StackName: stackArn, + }); + expect((_a = response.Stacks) === null || _a === void 0 ? void 0 : _a[0].StackStatus).toEqual('REVIEW_IN_PROGRESS'); + //verify a change set was created with the provided name + const changeSetResponse = await fixture.aws.cloudFormation('listChangeSets', { + StackName: stackArn, + }); + const changeSets = changeSetResponse.Summaries || []; + expect(changeSets.length).toEqual(1); + expect(changeSets[0].ChangeSetName).toEqual(changeSetName); + expect(changeSets[0].Status).toEqual('CREATE_COMPLETE'); +})); +test_helpers_1.integTest('security related changes without a CLI are expected to fail', cdk_1.withDefaultFixture(async (fixture) => { + // redirect /dev/null to stdin, which means there will not be tty attached + // since this stack includes security-related changes, the deployment should + // immediately fail because we can't confirm the changes + const stackName = 'iam-test'; + await expect(fixture.cdkDeploy(stackName, { + options: ['<', '/dev/null'], + neverRequireApproval: false, + })).rejects.toThrow('exited with error'); + // Ensure stack was not deployed + await expect(fixture.aws.cloudFormation('describeStacks', { + StackName: fixture.fullStackName(stackName), + })).rejects.toThrow('does not exist'); +})); +test_helpers_1.integTest('deploy wildcard with outputs', cdk_1.withDefaultFixture(async (fixture) => { + const outputsFile = path.join(fixture.integTestDir, 'outputs', 'outputs.json'); + await fs_1.promises.mkdir(path.dirname(outputsFile), { recursive: true }); + await fixture.cdkDeploy(['outputs-test-*'], { + options: ['--outputs-file', outputsFile], + }); + const outputs = JSON.parse((await fs_1.promises.readFile(outputsFile, { encoding: 'utf-8' })).toString()); + expect(outputs).toEqual({ + [`${fixture.stackNamePrefix}-outputs-test-1`]: { + TopicName: `${fixture.stackNamePrefix}-outputs-test-1MyTopic`, + }, + [`${fixture.stackNamePrefix}-outputs-test-2`]: { + TopicName: `${fixture.stackNamePrefix}-outputs-test-2MyOtherTopic`, + }, + }); +})); +test_helpers_1.integTest('deploy with parameters', cdk_1.withDefaultFixture(async (fixture) => { + var _a; + const stackArn = await fixture.cdkDeploy('param-test-1', { + options: [ + '--parameters', `TopicNameParam=${fixture.stackNamePrefix}bazinga`, + ], + captureStderr: false, + }); + const response = await fixture.aws.cloudFormation('describeStacks', { + StackName: stackArn, + }); + expect((_a = response.Stacks) === null || _a === void 0 ? void 0 : _a[0].Parameters).toEqual([ + { + ParameterKey: 'TopicNameParam', + ParameterValue: `${fixture.stackNamePrefix}bazinga`, + }, + ]); +})); +test_helpers_1.integTest('update to stack in ROLLBACK_COMPLETE state will delete stack and create a new one', cdk_1.withDefaultFixture(async (fixture) => { + var _a, _b, _c, _d; + // GIVEN + await expect(fixture.cdkDeploy('param-test-1', { + options: [ + '--parameters', `TopicNameParam=${fixture.stackNamePrefix}@aww`, + ], + captureStderr: false, + })).rejects.toThrow('exited with error'); + const response = await fixture.aws.cloudFormation('describeStacks', { + StackName: fixture.fullStackName('param-test-1'), + }); + const stackArn = (_a = response.Stacks) === null || _a === void 0 ? void 0 : _a[0].StackId; + expect((_b = response.Stacks) === null || _b === void 0 ? void 0 : _b[0].StackStatus).toEqual('ROLLBACK_COMPLETE'); + // WHEN + const newStackArn = await fixture.cdkDeploy('param-test-1', { + options: [ + '--parameters', `TopicNameParam=${fixture.stackNamePrefix}allgood`, + ], + captureStderr: false, + }); + const newStackResponse = await fixture.aws.cloudFormation('describeStacks', { + StackName: newStackArn, + }); + // THEN + expect(stackArn).not.toEqual(newStackArn); // new stack was created + expect((_c = newStackResponse.Stacks) === null || _c === void 0 ? void 0 : _c[0].StackStatus).toEqual('CREATE_COMPLETE'); + expect((_d = newStackResponse.Stacks) === null || _d === void 0 ? void 0 : _d[0].Parameters).toEqual([ + { + ParameterKey: 'TopicNameParam', + ParameterValue: `${fixture.stackNamePrefix}allgood`, + }, + ]); +})); +test_helpers_1.integTest('stack in UPDATE_ROLLBACK_COMPLETE state can be updated', cdk_1.withDefaultFixture(async (fixture) => { + var _a, _b, _c, _d; + // GIVEN + const stackArn = await fixture.cdkDeploy('param-test-1', { + options: [ + '--parameters', `TopicNameParam=${fixture.stackNamePrefix}nice`, + ], + captureStderr: false, + }); + let response = await fixture.aws.cloudFormation('describeStacks', { + StackName: stackArn, + }); + expect((_a = response.Stacks) === null || _a === void 0 ? void 0 : _a[0].StackStatus).toEqual('CREATE_COMPLETE'); + // bad parameter name with @ will put stack into UPDATE_ROLLBACK_COMPLETE + await expect(fixture.cdkDeploy('param-test-1', { + options: [ + '--parameters', `TopicNameParam=${fixture.stackNamePrefix}@aww`, + ], + captureStderr: false, + })).rejects.toThrow('exited with error'); + ; + response = await fixture.aws.cloudFormation('describeStacks', { + StackName: stackArn, + }); + expect((_b = response.Stacks) === null || _b === void 0 ? void 0 : _b[0].StackStatus).toEqual('UPDATE_ROLLBACK_COMPLETE'); + // WHEN + await fixture.cdkDeploy('param-test-1', { + options: [ + '--parameters', `TopicNameParam=${fixture.stackNamePrefix}allgood`, + ], + captureStderr: false, + }); + response = await fixture.aws.cloudFormation('describeStacks', { + StackName: stackArn, + }); + // THEN + expect((_c = response.Stacks) === null || _c === void 0 ? void 0 : _c[0].StackStatus).toEqual('UPDATE_COMPLETE'); + expect((_d = response.Stacks) === null || _d === void 0 ? void 0 : _d[0].Parameters).toEqual([ + { + ParameterKey: 'TopicNameParam', + ParameterValue: `${fixture.stackNamePrefix}allgood`, + }, + ]); +})); +test_helpers_1.integTest('deploy with wildcard and parameters', cdk_1.withDefaultFixture(async (fixture) => { + await fixture.cdkDeploy('param-test-*', { + options: [ + '--parameters', `${fixture.stackNamePrefix}-param-test-1:TopicNameParam=${fixture.stackNamePrefix}bazinga`, + '--parameters', `${fixture.stackNamePrefix}-param-test-2:OtherTopicNameParam=${fixture.stackNamePrefix}ThatsMySpot`, + '--parameters', `${fixture.stackNamePrefix}-param-test-3:DisplayNameParam=${fixture.stackNamePrefix}HeyThere`, + '--parameters', `${fixture.stackNamePrefix}-param-test-3:OtherDisplayNameParam=${fixture.stackNamePrefix}AnotherOne`, + ], + }); +})); +test_helpers_1.integTest('deploy with parameters multi', cdk_1.withDefaultFixture(async (fixture) => { + var _a; + const paramVal1 = `${fixture.stackNamePrefix}bazinga`; + const paramVal2 = `${fixture.stackNamePrefix}=jagshemash`; + const stackArn = await fixture.cdkDeploy('param-test-3', { + options: [ + '--parameters', `DisplayNameParam=${paramVal1}`, + '--parameters', `OtherDisplayNameParam=${paramVal2}`, + ], + captureStderr: false, + }); + const response = await fixture.aws.cloudFormation('describeStacks', { + StackName: stackArn, + }); + expect((_a = response.Stacks) === null || _a === void 0 ? void 0 : _a[0].Parameters).toEqual([ + { + ParameterKey: 'DisplayNameParam', + ParameterValue: paramVal1, + }, + { + ParameterKey: 'OtherDisplayNameParam', + ParameterValue: paramVal2, + }, + ]); +})); +test_helpers_1.integTest('deploy with notification ARN', cdk_1.withDefaultFixture(async (fixture) => { + var _a; + const topicName = `${fixture.stackNamePrefix}-test-topic`; + const response = await fixture.aws.sns('createTopic', { Name: topicName }); + const topicArn = response.TopicArn; + try { + await fixture.cdkDeploy('test-2', { + options: ['--notification-arns', topicArn], + }); + // verify that the stack we deployed has our notification ARN + const describeResponse = await fixture.aws.cloudFormation('describeStacks', { + StackName: fixture.fullStackName('test-2'), + }); + expect((_a = describeResponse.Stacks) === null || _a === void 0 ? void 0 : _a[0].NotificationARNs).toEqual([topicArn]); + } + finally { + await fixture.aws.sns('deleteTopic', { + TopicArn: topicArn, + }); + } +})); +test_helpers_1.integTest('deploy with role', cdk_1.withDefaultFixture(async (fixture) => { + const roleName = `${fixture.stackNamePrefix}-test-role`; + await deleteRole(); + const createResponse = await fixture.aws.iam('createRole', { + RoleName: roleName, + AssumeRolePolicyDocument: JSON.stringify({ + Version: '2012-10-17', + Statement: [{ + Action: 'sts:AssumeRole', + Principal: { Service: 'cloudformation.amazonaws.com' }, + Effect: 'Allow', + }, { + Action: 'sts:AssumeRole', + Principal: { AWS: (await fixture.aws.sts('getCallerIdentity', {})).Arn }, + Effect: 'Allow', + }], + }), + }); + const roleArn = createResponse.Role.Arn; + try { + await fixture.aws.iam('putRolePolicy', { + RoleName: roleName, + PolicyName: 'DefaultPolicy', + PolicyDocument: JSON.stringify({ + Version: '2012-10-17', + Statement: [{ + Action: '*', + Resource: '*', + Effect: 'Allow', + }], + }), + }); + await aws_1.retry(fixture.output, 'Trying to assume fresh role', aws_1.retry.forSeconds(300), async () => { + await fixture.aws.sts('assumeRole', { + RoleArn: roleArn, + RoleSessionName: 'testing', + }); + }); + // In principle, the role has replicated from 'us-east-1' to wherever we're testing. + // Give it a little more sleep to make sure CloudFormation is not hitting a box + // that doesn't have it yet. + await aws_1.sleep(5000); + await fixture.cdkDeploy('test-2', { + options: ['--role-arn', roleArn], + }); + // Immediately delete the stack again before we delete the role. + // + // Since roles are sticky, if we delete the role before the stack, subsequent DeleteStack + // operations will fail when CloudFormation tries to assume the role that's already gone. + await fixture.cdkDestroy('test-2'); + } + finally { + await deleteRole(); + } + async function deleteRole() { + try { + for (const policyName of (await fixture.aws.iam('listRolePolicies', { RoleName: roleName })).PolicyNames) { + await fixture.aws.iam('deleteRolePolicy', { + RoleName: roleName, + PolicyName: policyName, + }); + } + await fixture.aws.iam('deleteRole', { RoleName: roleName }); + } + catch (e) { + if (e.message.indexOf('cannot be found') > -1) { + return; + } + throw e; + } + } +})); +test_helpers_1.integTest('cdk diff', cdk_1.withDefaultFixture(async (fixture) => { + const diff1 = await fixture.cdk(['diff', fixture.fullStackName('test-1')]); + expect(diff1).toContain('AWS::SNS::Topic'); + const diff2 = await fixture.cdk(['diff', fixture.fullStackName('test-2')]); + expect(diff2).toContain('AWS::SNS::Topic'); + // We can make it fail by passing --fail + await expect(fixture.cdk(['diff', '--fail', fixture.fullStackName('test-1')])) + .rejects.toThrow('exited with error'); +})); +test_helpers_1.integTest('cdk diff --fail on multiple stacks exits with error if any of the stacks contains a diff', cdk_1.withDefaultFixture(async (fixture) => { + // GIVEN + const diff1 = await fixture.cdk(['diff', fixture.fullStackName('test-1')]); + expect(diff1).toContain('AWS::SNS::Topic'); + await fixture.cdkDeploy('test-2'); + const diff2 = await fixture.cdk(['diff', fixture.fullStackName('test-2')]); + expect(diff2).toContain('There were no differences'); + // WHEN / THEN + await expect(fixture.cdk(['diff', '--fail', fixture.fullStackName('test-1'), fixture.fullStackName('test-2')])).rejects.toThrow('exited with error'); +})); +test_helpers_1.integTest('cdk diff --fail with multiple stack exits with if any of the stacks contains a diff', cdk_1.withDefaultFixture(async (fixture) => { + // GIVEN + await fixture.cdkDeploy('test-1'); + const diff1 = await fixture.cdk(['diff', fixture.fullStackName('test-1')]); + expect(diff1).toContain('There were no differences'); + const diff2 = await fixture.cdk(['diff', fixture.fullStackName('test-2')]); + expect(diff2).toContain('AWS::SNS::Topic'); + // WHEN / THEN + await expect(fixture.cdk(['diff', '--fail', fixture.fullStackName('test-1'), fixture.fullStackName('test-2')])).rejects.toThrow('exited with error'); +})); +test_helpers_1.integTest('cdk diff --security-only --fail exits when security changes are present', cdk_1.withDefaultFixture(async (fixture) => { + const stackName = 'iam-test'; + await expect(fixture.cdk(['diff', '--security-only', '--fail', fixture.fullStackName(stackName)])).rejects.toThrow('exited with error'); +})); +test_helpers_1.integTest('deploy stack with docker asset', cdk_1.withDefaultFixture(async (fixture) => { + await fixture.cdkDeploy('docker'); +})); +test_helpers_1.integTest('deploy and test stack with lambda asset', cdk_1.withDefaultFixture(async (fixture) => { + var _a, _b; + const stackArn = await fixture.cdkDeploy('lambda', { captureStderr: false }); + const response = await fixture.aws.cloudFormation('describeStacks', { + StackName: stackArn, + }); + const lambdaArn = (_b = (_a = response.Stacks) === null || _a === void 0 ? void 0 : _a[0].Outputs) === null || _b === void 0 ? void 0 : _b[0].OutputValue; + if (lambdaArn === undefined) { + throw new Error('Stack did not have expected Lambda ARN output'); + } + const output = await fixture.aws.lambda('invoke', { + FunctionName: lambdaArn, + }); + expect(JSON.stringify(output.Payload)).toContain('dear asset'); +})); +test_helpers_1.integTest('cdk ls', cdk_1.withDefaultFixture(async (fixture) => { + const listing = await fixture.cdk(['ls'], { captureStderr: false }); + const expectedStacks = [ + 'conditional-resource', + 'docker', + 'docker-with-custom-file', + 'failed', + 'iam-test', + 'lambda', + 'missing-ssm-parameter', + 'order-providing', + 'outputs-test-1', + 'outputs-test-2', + 'param-test-1', + 'param-test-2', + 'param-test-3', + 'termination-protection', + 'test-1', + 'test-2', + 'with-nested-stack', + 'with-nested-stack-using-parameters', + 'order-consuming', + ]; + for (const stack of expectedStacks) { + expect(listing).toContain(fixture.fullStackName(stack)); + } +})); +test_helpers_1.integTest('synthing a stage with errors leads to failure', cdk_1.withDefaultFixture(async (fixture) => { + const output = await fixture.cdk(['synth'], { + allowErrExit: true, + modEnv: { + INTEG_STACK_SET: 'stage-with-errors', + }, + }); + expect(output).toContain('This is an error'); +})); +test_helpers_1.integTest('synthing a stage with errors can be suppressed', cdk_1.withDefaultFixture(async (fixture) => { + await fixture.cdk(['synth', '--no-validation'], { + modEnv: { + INTEG_STACK_SET: 'stage-with-errors', + }, + }); +})); +test_helpers_1.integTest('deploy stack without resource', cdk_1.withDefaultFixture(async (fixture) => { + // Deploy the stack without resources + await fixture.cdkDeploy('conditional-resource', { modEnv: { NO_RESOURCE: 'TRUE' } }); + // This should have succeeded but not deployed the stack. + await expect(fixture.aws.cloudFormation('describeStacks', { StackName: fixture.fullStackName('conditional-resource') })) + .rejects.toThrow('conditional-resource does not exist'); + // Deploy the stack with resources + await fixture.cdkDeploy('conditional-resource'); + // Then again WITHOUT resources (this should destroy the stack) + await fixture.cdkDeploy('conditional-resource', { modEnv: { NO_RESOURCE: 'TRUE' } }); + await expect(fixture.aws.cloudFormation('describeStacks', { StackName: fixture.fullStackName('conditional-resource') })) + .rejects.toThrow('conditional-resource does not exist'); +})); +test_helpers_1.integTest('IAM diff', cdk_1.withDefaultFixture(async (fixture) => { + const output = await fixture.cdk(['diff', fixture.fullStackName('iam-test')]); + // Roughly check for a table like this: + // + // ┌───┬─────────────────┬────────┬────────────────┬────────────────────────────-──┬───────────┐ + // │ │ Resource │ Effect │ Action │ Principal │ Condition │ + // ├───┼─────────────────┼────────┼────────────────┼───────────────────────────────┼───────────┤ + // │ + │ ${SomeRole.Arn} │ Allow │ sts:AssumeRole │ Service:ec2.amazonaws.com │ │ + // └───┴─────────────────┴────────┴────────────────┴───────────────────────────────┴───────────┘ + expect(output).toContain('${SomeRole.Arn}'); + expect(output).toContain('sts:AssumeRole'); + expect(output).toContain('ec2.amazonaws.com'); +})); +test_helpers_1.integTest('fast deploy', cdk_1.withDefaultFixture(async (fixture) => { + // we are using a stack with a nested stack because CFN will always attempt to + // update a nested stack, which will allow us to verify that updates are actually + // skipped unless --force is specified. + const stackArn = await fixture.cdkDeploy('with-nested-stack', { captureStderr: false }); + const changeSet1 = await getLatestChangeSet(); + // Deploy the same stack again, there should be no new change set created + await fixture.cdkDeploy('with-nested-stack'); + const changeSet2 = await getLatestChangeSet(); + expect(changeSet2.ChangeSetId).toEqual(changeSet1.ChangeSetId); + // Deploy the stack again with --force, now we should create a changeset + await fixture.cdkDeploy('with-nested-stack', { options: ['--force'] }); + const changeSet3 = await getLatestChangeSet(); + expect(changeSet3.ChangeSetId).not.toEqual(changeSet2.ChangeSetId); + // Deploy the stack again with tags, expected to create a new changeset + // even though the resources didn't change. + await fixture.cdkDeploy('with-nested-stack', { options: ['--tags', 'key=value'] }); + const changeSet4 = await getLatestChangeSet(); + expect(changeSet4.ChangeSetId).not.toEqual(changeSet3.ChangeSetId); + async function getLatestChangeSet() { + var _a, _b, _c; + const response = await fixture.aws.cloudFormation('describeStacks', { StackName: stackArn }); + if (!((_a = response.Stacks) === null || _a === void 0 ? void 0 : _a[0])) { + throw new Error('Did not get a ChangeSet at all'); + } + fixture.log(`Found Change Set ${(_b = response.Stacks) === null || _b === void 0 ? void 0 : _b[0].ChangeSetId}`); + return (_c = response.Stacks) === null || _c === void 0 ? void 0 : _c[0]; + } +})); +test_helpers_1.integTest('failed deploy does not hang', cdk_1.withDefaultFixture(async (fixture) => { + // this will hang if we introduce https://github.com/aws/aws-cdk/issues/6403 again. + await expect(fixture.cdkDeploy('failed')).rejects.toThrow('exited with error'); +})); +test_helpers_1.integTest('can still load old assemblies', cdk_1.withDefaultFixture(async (fixture) => { + const cxAsmDir = path.join(os.tmpdir(), 'cdk-integ-cx'); + const testAssembliesDirectory = path.join(__dirname, 'cloud-assemblies'); + for (const asmdir of await listChildDirs(testAssembliesDirectory)) { + fixture.log(`ASSEMBLY ${asmdir}`); + await cdk_1.cloneDirectory(asmdir, cxAsmDir); + // Some files in the asm directory that have a .js extension are + // actually treated as templates. Evaluate them using NodeJS. + const templates = await listChildren(cxAsmDir, fullPath => Promise.resolve(fullPath.endsWith('.js'))); + for (const template of templates) { + const targetName = template.replace(/.js$/, ''); + await cdk_1.shell([process.execPath, template, '>', targetName], { + cwd: cxAsmDir, + output: fixture.output, + modEnv: { + TEST_ACCOUNT: await fixture.aws.account(), + TEST_REGION: fixture.aws.region, + }, + }); + } + // Use this directory as a Cloud Assembly + const output = await fixture.cdk([ + '--app', cxAsmDir, + '-v', + 'synth', + ]); + // Assert that there was no providerError in CDK's stderr + // Because we rely on the app/framework to actually error in case the + // provider fails, we inspect the logs here. + expect(output).not.toContain('$providerError'); + } +})); +test_helpers_1.integTest('generating and loading assembly', cdk_1.withDefaultFixture(async (fixture) => { + const asmOutputDir = `${fixture.integTestDir}-cdk-integ-asm`; + await fixture.shell(['rm', '-rf', asmOutputDir]); + // Synthesize a Cloud Assembly tothe default directory (cdk.out) and a specific directory. + await fixture.cdk(['synth']); + await fixture.cdk(['synth', '--output', asmOutputDir]); + // cdk.out in the current directory and the indicated --output should be the same + await fixture.shell(['diff', 'cdk.out', asmOutputDir]); + // Check that we can 'ls' the synthesized asm. + // Change to some random directory to make sure we're not accidentally loading cdk.json + const list = await fixture.cdk(['--app', asmOutputDir, 'ls'], { cwd: os.tmpdir() }); + // Same stacks we know are in the app + expect(list).toContain(`${fixture.stackNamePrefix}-lambda`); + expect(list).toContain(`${fixture.stackNamePrefix}-test-1`); + expect(list).toContain(`${fixture.stackNamePrefix}-test-2`); + // Check that we can use '.' and just synth ,the generated asm + const stackTemplate = await fixture.cdk(['--app', '.', 'synth', fixture.fullStackName('test-2')], { + cwd: asmOutputDir, + }); + expect(stackTemplate).toContain('topic152D84A37'); + // Deploy a Lambda from the copied asm + await fixture.cdkDeploy('lambda', { options: ['-a', '.'], cwd: asmOutputDir }); + // Remove (rename) the original custom docker file that was used during synth. + // this verifies that the assemly has a copy of it and that the manifest uses + // relative paths to reference to it. + const customDockerFile = path.join(fixture.integTestDir, 'docker', 'Dockerfile.Custom'); + await fs_1.promises.rename(customDockerFile, `${customDockerFile}~`); + try { + // deploy a docker image with custom file without synth (uses assets) + await fixture.cdkDeploy('docker-with-custom-file', { options: ['-a', '.'], cwd: asmOutputDir }); + } + finally { + // Rename back to restore fixture to original state + await fs_1.promises.rename(`${customDockerFile}~`, customDockerFile); + } +})); +test_helpers_1.integTest('templates on disk contain metadata resource, also in nested assemblies', cdk_1.withDefaultFixture(async (fixture) => { + // Synth first, and switch on version reporting because cdk.json is disabling it + await fixture.cdk(['synth', '--version-reporting=true']); + // Load template from disk from root assembly + const templateContents = await fixture.shell(['cat', 'cdk.out/*-lambda.template.json']); + expect(JSON.parse(templateContents).Resources.CDKMetadata).toBeTruthy(); + // Load template from nested assembly + const nestedTemplateContents = await fixture.shell(['cat', 'cdk.out/assembly-*-stage/*-stage-StackInStage.template.json']); + expect(JSON.parse(nestedTemplateContents).Resources.CDKMetadata).toBeTruthy(); +})); +async function listChildren(parent, pred) { + const ret = new Array(); + for (const child of await fs_1.promises.readdir(parent, { encoding: 'utf-8' })) { + const fullPath = path.join(parent, child.toString()); + if (await pred(fullPath)) { + ret.push(fullPath); + } + } + return ret; +} +async function listChildDirs(parent) { + return listChildren(parent, async (fullPath) => (await fs_1.promises.stat(fullPath)).isDirectory()); +} +async function readTemplate(fixture, stackName) { + const fullStackName = fixture.fullStackName(stackName); + const templatePath = path.join(fixture.integTestDir, 'cdk.out', `${fullStackName}.template.json`); + return JSON.parse((await fs_1.promises.readFile(templatePath, { encoding: 'utf-8' })).toString()); +} +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2xpLmludGVndGVzdC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbImNsaS5pbnRlZ3Rlc3QudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7QUFBQSwyQkFBb0M7QUFDcEMseUJBQXlCO0FBQ3pCLDZCQUE2QjtBQUM3Qix3Q0FBOEM7QUFDOUMsd0NBQXdGO0FBQ3hGLDBEQUFvRDtBQUVwRCxJQUFJLENBQUMsVUFBVSxDQUFDLEdBQUcsR0FBRyxJQUFJLENBQUMsQ0FBQztBQUU1Qix3QkFBUyxDQUFDLFlBQVksRUFBRSx3QkFBa0IsQ0FBQyxLQUFLLEVBQUUsT0FBTyxFQUFFLEVBQUU7SUFDM0QsT0FBTyxDQUFDLEdBQUcsQ0FBQywyQ0FBMkMsQ0FBQyxDQUFDO0lBQ3pELE1BQU0sT0FBTyxDQUFDLFVBQVUsQ0FBQyxZQUFZLEVBQUUsRUFBRSxNQUFNLEVBQUUsRUFBRSxrQkFBa0IsRUFBRSxRQUFRLEVBQUUsRUFBRSxDQUFDLENBQUM7SUFFckYsT0FBTyxDQUFDLEdBQUcsQ0FBQyw0Q0FBNEMsQ0FBQyxDQUFDO0lBQzFELE1BQU0sT0FBTyxDQUFDLFNBQVMsQ0FBQyxZQUFZLEVBQUUsRUFBRSxNQUFNLEVBQUUsRUFBRSxrQkFBa0IsRUFBRSxRQUFRLEVBQUUsRUFBRSxDQUFDLENBQUM7SUFDcEYsT0FBTyxDQUFDLEdBQUcsQ0FBQyxpQkFBaUIsQ0FBQyxDQUFDO0lBRS9CLE9BQU8sQ0FBQyxHQUFHLENBQUMsc0NBQXNDLENBQUMsQ0FBQztJQUNwRCxNQUFNLE9BQU8sQ0FBQyxTQUFTLENBQUMsWUFBWSxFQUFFLEVBQUUsTUFBTSxFQUFFLEVBQUUsa0JBQWtCLEVBQUUsUUFBUSxFQUFFLEVBQUUsQ0FBQyxDQUFDO0FBQ3RGLENBQUMsQ0FBQyxDQUFDLENBQUM7QUFFSix3QkFBUyxDQUFDLGdDQUFnQyxFQUFFLHdCQUFrQixDQUFDLEtBQUssRUFBRSxPQUFPLEVBQUUsRUFBRTtJQUMvRSxNQUFNLFFBQVEsR0FBRyxNQUFNLE9BQU8sQ0FBQyxHQUFH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YUFBYSxDQUFDLFNBQVMsQ0FBQyxDQUFDO0lBQ3ZELE1BQU0sWUFBWSxHQUFHLElBQUksQ0FBQyxJQUFJLENBQUMsT0FBTyxDQUFDLFlBQVksRUFBRSxTQUFTLEVBQUUsR0FBRyxhQUFhLGdCQUFnQixDQUFDLENBQUM7SUFDbEcsT0FBTyxJQUFJLENBQUMsS0FBSyxDQUFDLENBQUMsTUFBTSxhQUFFLENBQUMsUUFBUSxDQUFDLFlBQVksRUFBRSxFQUFFLFFBQVEsRUFBRSxPQUFPLEVBQUUsQ0FBQyxDQUFDLENBQUMsUUFBUSxFQUFFLENBQUMsQ0FBQztBQUN6RixDQUFDIiwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IHsgcHJvbWlzZXMgYXMgZnMgfSBmcm9tICdmcyc7XG5pbXBvcnQgKiBhcyBvcyBmcm9tICdvcyc7XG5pbXBvcnQgKiBhcyBwYXRoIGZyb20gJ3BhdGgnO1xuaW1wb3J0IHsgcmV0cnksIHNsZWVwIH0gZnJvbSAnLi4vaGVscGVycy9hd3MnO1xuaW1wb3J0IHsgY2xvbmVEaXJlY3RvcnksIHNoZWxsLCB3aXRoRGVmYXVsdEZpeHR1cmUsIFRlc3RGaXh0dXJlIH0gZnJvbSAnLi4vaGVscGVycy9jZGsnO1xuaW1wb3J0IHsgaW50ZWdUZXN0IH0gZnJvbSAnLi4vaGVscGVycy90ZXN0LWhlbHBlcnMnO1xuXG5qZXN0LnNldFRpbWVvdXQoNjAwICogMTAwMCk7XG5cbmludGVnVGVzdCgnVlBDIExvb2t1cCcsIHdpdGhEZWZhdWx0Rml4dHVyZShhc3luYyAoZml4dHVyZSkgPT4ge1xuICBmaXh0dXJlLmxvZygnTWFraW5nIHN1cmUgd2UgYXJlIGNsZWFuIGJlZm9yZSBzdGFydGluZy4nKTtcbiAgYXdhaXQgZml4dHVyZS5jZGtEZXN0cm95KCdkZWZpbmUtdnBjJywgeyBtb2RFbnY6IHsgRU5BQkxFX1ZQQ19URVNUSU5HOiAnREVGSU5FJyB9IH0pO1xuXG4gIGZpeHR1cmUubG9nKCdTZXR0aW5nIHVwOiBjcmVhdGluZyBhIFZQQyB3aXRoIGtub3duIHRhZ3MnKTtcbiAgYXdhaXQgZml4dHVyZS5jZGtEZXBsb3koJ2RlZmluZS12cGMnLCB7IG1vZEVudjogeyBFTkFCTEVfVlBDX1RFU1RJTkc6ICdERUZJTkUnIH0gfSk7XG4gIGZpeHR1cmUubG9nKCdTZXR1cCBjb21wbGV0ZSEnKTtcblxuICBmaXh0dXJlLmxvZygnVmVyaWZ5aW5nIHdlIGNhbiBub3cgaW1wb3J0IHRoYXQgVlBDJyk7XG4gIGF3YWl0IGZpeHR1cmUuY2RrRGVwbG95KCdpbXBvcnQtdnBjJywgeyBtb2RFbnY6IHsgRU5BQkxFX1ZQQ19URVNUSU5HOiAnSU1QT1JUJyB9IH0pO1xufSkpO1xuXG5pbnRlZ1Rlc3QoJ1R3byB3YXlzIG9mIHNob2luZyB0aGUgdmVyc2lvbicsIHdpdGhEZWZhdWx0Rml4dHVyZShhc3luYyAoZml4dHVyZSkgPT4ge1xuICBjb25zdCB2ZXJzaW9uMSA9IGF3YWl0IGZpeHR1cmUuY2RrKFsndmVyc2lvbiddLCB7IHZlcmJvc2U6IGZhbHNlIH0pO1xuICBjb25zdCB2ZXJzaW9uMiA9IGF3YWl0IGZpeHR1cmUuY2RrKFsnLS12ZXJzaW9uJ10sIHsgdmVyYm9zZTogZmFsc2UgfSk7XG5cbiAgZXhwZWN0KHZlcnNpb24xKS50b0VxdWFsKHZlcnNpb24yKTtcbn0pKTtcblxuaW50ZWdUZXN0KCdUZXJtaW5hdGlvbiBwcm90ZWN0aW9uJywgd2l0aERlZmF1bHRGaXh0dXJlKGFzeW5jIChmaXh0dXJlKSA9PiB7XG4gIGNvbnN0IHN0YWNrTmFtZSA9ICd0ZXJtaW5hdGlvbi1wcm90ZWN0aW9uJztcbiAgYXdhaXQgZml4dHVyZS5jZGtEZXBsb3koc3RhY2tOYW1lKTtcblxuICAvLyBUcnkgYSBkZXN0cm95IHRoYXQgc2hvdWxkIGZhaWxcbiAgYXdhaXQgZXhwZWN0KGZpeHR1cmUuY2RrRGVzdHJveShzdGFja05hbWUpKS5yZWplY3RzLnRvVGhyb3coJ2V4aXRlZCB3aXRoIGVycm9yJyk7XG5cbiAgLy8gQ2FuIHVwZGF0ZSB0ZXJtaW5hdGlvbiBwcm90ZWN0aW9uIGV2ZW4gdGhvdWdoIHRoZSBjaGFuZ2Ugc2V0IGRvZXNuJ3QgY29udGFpbiBjaGFuZ2VzXG4gIGF3YWl0IGZpeHR1cmUuY2RrRGVwbG95KHN0YWNrTmFtZSwgeyBtb2RFbnY6IHsgVEVSTUlOQVRJT05fUFJPVEVDVElPTjogJ0ZBTFNFJyB9IH0pO1xuICBhd2FpdCBmaXh0dXJlLmNka0Rlc3Ryb3koc3RhY2tOYW1lKTtcbn0pKTtcblxuaW50ZWdUZXN0KCdjZGsgc3ludGgnLCB3aXRoRGVmYXVsdEZpeHR1cmUoYXN5bmMgKGZpeHR1cmUpID0+IHtcbiAgYXdhaXQgZml4dHVyZS5jZGsoWydzeW50aCcsIGZpeHR1cmUuZnVsbFN0YWNrTmFtZSgndGVzdC0xJyldKTtcbiAgY29uc3QgdGVtcGxhdGUxID0gYXdhaXQgcmVhZFRlbXBsYXRlKGZpeHR1cmUsICd0ZXN0LTEnKTtcbiAgZXhwZWN0KHRlbXBsYXRlMSkudG9FcXVhbCh7XG4gICAgUmVzb3VyY2VzOiB7XG4gICAgICB0b3BpYzY5ODMxNDkxOiB7XG4gICAgICAgIFR5cGU6ICdBV1M6OlNOUzo6VG9waWMnLFxuICAgICAgICBNZXRhZGF0YToge1xuICAgICAgICAgICdhd3M6Y2RrOnBhdGgnOiBgJHtmaXh0dXJlLnN0YWNrTmFtZVByZWZpeH0tdGVzdC0xL3RvcGljL1Jlc291cmNlYCxcbiAgICAgICAgfSxcbiAgICAgIH0sXG4gICAgfSxcbiAgfSk7XG5cbiAgYXdhaXQgZml4dHVyZS5jZGsoWydzeW50aCcsIGZpeHR1cmUuZnVsbFN0YWNrTmFtZSgndGVzdC0yJyldLCB7IHZlcmJvc2U6IGZhbHNlIH0pO1xuICBjb25zdCB0ZW1wbGF0ZTIgPSBhd2FpdCByZWFkVGVtcGxhdGUoZml4dHVyZSwgJ3Rlc3QtMicpO1xuICBleHBlY3QodGVtcGxhdGUyKS50b0VxdWFsKHtcbiAgICBSZXNvdXJjZXM6IHtcbiAgICAgIHRvcGljMTUyRDg0QTM3OiB7XG4gICAgICAgIFR5cGU6ICdBV1M6OlNOUzo6VG9waWMnLFxuICAgICAgICBNZXRhZGF0YToge1xuICAgICAgICAgICdhd3M6Y2RrOnBhdGgnOiBgJHtmaXh0dXJlLnN0YWNrTmFtZVByZWZpeH0tdGVzdC0yL3RvcGljMS9SZXNvdXJjZWAsXG4gICAgICAgIH0sXG4gICAgICB9LFxuICAgICAgdG9waWMyQTRGQjU0N0Y6IHtcbiAgICAgICAgVHlwZTogJ0FXUzo6U05TOjpUb3BpYycsXG4gICAgICAgIE1ldGFkYXRhOiB7XG4gICAgICAgICAgJ2F3czpjZGs6cGF0aCc6IGAke2ZpeHR1cmUuc3RhY2tOYW1lUHJlZml4fS10ZXN0LTIvdG9waWMyL1Jlc291cmNlYCxcbiAgICAgICAgfSxcbiAgICAgIH0sXG4gICAgfSxcbiAgfSk7XG5cbn0pKTtcblxuaW50ZWdUZXN0KCdzc20gcGFyYW1ldGVyIHByb3ZpZGVyIGVycm9yJywgd2l0aERlZmF1bHRGaXh0dXJlKGFzeW5jIChmaXh0dXJlKSA9PiB7XG4gIGF3YWl0IGV4cGVjdChmaXh0dXJlLmNkayhbJ3N5bnRoJyxcbiAgICBmaXh0dXJlLmZ1bGxTdGFja05hbWUoJ21pc3Npbmctc3NtLXBhcmFtZXRlcicpLFxuICAgICctYycsICd0ZXN0OnNzbS1wYXJhbWV0ZXItbmFtZT0vZG9lcy9ub3QvZXhpc3QnXSwge1xuICAgIGFsbG93RXJyRXhpdDogdHJ1ZSxcbiAgfSkpLnJlc29sdmVzLnRvQ29udGFpbignU1NNIHBhcmFtZXRlciBub3QgYXZhaWxhYmxlIGluIGFjY291bnQnKTtcbn0pKTtcblxuaW50ZWdUZXN0KCdhdXRvbWF0aWMgb3JkZXJpbmcnLCB3aXRoRGVmYXVsdEZpeHR1cmUoYXN5bmMgKGZpeHR1cmUpID0+IHtcbiAgLy8gRGVwbG95IHRoZSBjb25zdW1pbmcgc3RhY2sgd2hpY2ggd2lsbCBpbmNsdWRlIHRoZSBwcm9kdWNpbmcgc3RhY2tcbiAgYXdhaXQgZml4dHVyZS5jZGtEZXBsb3koJ29yZGVyLWNvbnN1bWluZycpO1xuXG4gIC8vIERlc3Ryb3kgdGhlIHByb3ZpZGluZyBzdGFjayB3aGljaCB3aWxsIGluY2x1ZGUgdGhlIGNvbnN1bWluZyBzdGFja1xuICBhd2FpdCBmaXh0dXJlLmNka0Rlc3Ryb3koJ29yZGVyLXByb3ZpZGluZycpO1xufSkpO1xuXG5pbnRlZ1Rlc3QoJ2NvbnRleHQgc2V0dGluZycsIHdpdGhEZWZhdWx0Rml4dHVyZShhc3luYyAoZml4dHVyZSkgPT4ge1xuICBhd2FpdCBmcy53cml0ZUZpbGUocGF0aC5qb2luKGZpeHR1cmUuaW50ZWdUZXN0RGlyLCAnY2RrLmNvbnRleHQuanNvbicpLCBKU09OLnN0cmluZ2lmeSh7XG4gICAgY29udGV4dGtleTogJ3RoaXMgaXMgdGhlIGNvbnRleHQgdmFsdWUnLFxuICB9KSk7XG4gIHRyeSB7XG4gICAgYXdhaXQgZXhwZWN0KGZpeHR1cmUuY2RrKFsnY29udGV4dCddKSkucmVzb2x2ZXMudG9Db250YWluKCd0aGlzIGlzIHRoZSBjb250ZXh0IHZhbHVlJyk7XG5cbiAgICAvLyBUZXN0IHRoYXQgZGVsZXRpbmcgdGhlIGNvbnRleHRrZXkgd29ya3NcbiAgICBhd2FpdCBmaXh0dXJlLmNkayhbJ2NvbnRleHQnLCAnLS1yZXNldCcsICdjb250ZXh0a2V5J10pO1xuICAgIGF3YWl0IGV4cGVjdChmaXh0dXJlLmNkayhbJ2NvbnRleHQnXSkpLnJlc29sdmVzLm5vdC50b0NvbnRhaW4oJ3RoaXMgaXMgdGhlIGNvbnRleHQgdmFsdWUnKTtcblxuICAgIC8vIFRlc3QgdGhhdCBmb3JjZWQgZGVsZXRlIG9mIHRoZSBjb250ZXh0IGtleSBkb2VzIG5vdCB0aHJvd1xuICAgIGF3YWl0IGZpeHR1cmUuY2RrKFsnY29udGV4dCcsICctZicsICctLXJlc2V0JywgJ2NvbnRleHRrZXknXSk7XG5cbiAgfSBmaW5hbGx5IHtcbiAgICBhd2FpdCBmcy51bmxpbmsocGF0aC5qb2luKGZpeHR1cmUuaW50ZWdUZXN0RGlyLCAnY2RrLmNvbnRleHQuanNvbicpKTtcbiAgfVxufSkpO1xuXG5pbnRlZ1Rlc3QoJ2NvbnRleHQgaW4gc3RhZ2UgcHJvcGFnYXRlcyB0byB0b3AnLCB3aXRoRGVmYXVsdEZpeHR1cmUoYXN5bmMgKGZpeHR1cmUpID0+IHtcbiAgYXdhaXQgZXhwZWN0KGZpeHR1cmUuY2RrU3ludGgoe1xuICAgIC8vIFRoaXMgd2lsbCBtYWtlIGl0IGVycm9yIHRvIHByb3ZlIHRoYXQgdGhlIGNvbnRleHQgYnViYmxlcyB1cCwgYW5kIGFsc28gdGhhdCB3ZSBjYW4gZmFpbCBvbiBjb21tYW5kXG4gICAgb3B0aW9uczogWyctLW5vLWxvb2t1cHMnXSxcbiAgICBtb2RFbnY6IHtcbiAgICAgIElOVEVHX1NUQUNLX1NFVDogJ3N0YWdlLXVzaW5nLWNvbnRleHQnLFxuICAgIH0sXG4gICAgYWxsb3dFcnJFeGl0OiB0cnVlLFxuICB9KSkucmVzb2x2ZXMudG9Db250YWluKCdDb250ZXh0IGxvb2t1cHMgaGF2ZSBiZWVuIGRpc2FibGVkJyk7XG59KSk7XG5cbmludGVnVGVzdCgnZGVwbG95Jywgd2l0aERlZmF1bHRGaXh0dXJlKGFzeW5jIChmaXh0dXJlKSA9PiB7XG4gIGNvbnN0IHN0YWNrQXJuID0gYXdhaXQgZml4dHVyZS5jZGtEZXBsb3koJ3Rlc3QtMicsIHsgY2FwdHVyZVN0ZGVycjogZmFsc2UgfSk7XG5cbiAgLy8gdmVyaWZ5IHRoZSBudW1iZXIgb2YgcmVzb3VyY2VzIGluIHRoZSBzdGFja1xuICBjb25zdCByZXNwb25zZSA9IGF3YWl0IGZpeHR1cmUuYXdzLmNsb3VkRm9ybWF0aW9uKCdkZXNjcmliZVN0YWNrUmVzb3VyY2VzJywge1xuICAgIFN0YWNrTmFtZTogc3RhY2tBcm4sXG4gIH0pO1xuICBleHBlY3QocmVzcG9uc2UuU3RhY2tSZXNvdXJjZXM/Lmxlbmd0aCkudG9FcXVhbCgyKTtcbn0pKTtcblxuaW50ZWdUZXN0KCdkZXBsb3kgYWxsJywgd2l0aERlZmF1bHRGaXh0dXJlKGFzeW5jIChmaXh0dXJlKSA9PiB7XG4gIGNvbnN0IGFybnMgPSBhd2FpdCBmaXh0dXJlLmNka0RlcGxveSgndGVzdC0qJywgeyBjYXB0dXJlU3RkZXJyOiBmYWxzZSB9KTtcblxuICAvLyB2ZXJpZnkgdGhhdCB3ZSBvbmx5IGRlcGxveWVkIGEgc2luZ2xlIHN0YWNrICh0aGVyZSdzIGEgc2luZ2xlIEFSTiBpbiB0aGUgb3V0cHV0KVxuICBleHBlY3QoYXJucy5zcGxpdCgnXFxuJykubGVuZ3RoKS50b0VxdWFsKDIpO1xufSkpO1xuXG5pbnRlZ1Rlc3QoJ25lc3RlZCBzdGFjayB3aXRoIHBhcmFtZXRlcnMnLCB3aXRoRGVmYXVsdEZpeHR1cmUoYXN5bmMgKGZpeHR1cmUpID0+IHtcbiAgLy8gU1RBQ0tfTkFNRV9QUkVGSVggaXMgdXNlZCBpbiBNeVRvcGljUGFyYW0gdG8gYWxsb3cgbXVsdGlwbGUgaW5zdGFuY2VzXG4gIC8vIG9mIHRoaXMgdGVzdCB0byBydW4gaW4gcGFyYWxsZWwsIG90aGV3aXNlIHRoZXkgd2lsbCBhdHRlbXB0IHRvIGNyZWF0ZSB0aGUgc2FtZSBTTlMgdG9waWMuXG4gIGNvbnN0IHN0YWNrQXJuID0gYXdhaXQgZml4dHVyZS5jZGtEZXBsb3koJ3dpdGgtbmVzdGVkLXN0YWNrLXVzaW5nLXBhcmFtZXRlcnMnLCB7XG4gICAgb3B0aW9uczogWyctLXBhcmFtZXRlcnMnLCBgTXlUb3BpY1BhcmFtPSR7Zml4dHVyZS5zdGFja05hbWVQcmVmaXh9VGhlcmVJc05vU3Bvb25gXSxcbiAgICBjYXB0dXJlU3RkZXJyOiBmYWxzZSxcbiAgfSk7XG5cbiAgLy8gdmVyaWZ5IHRoYXQgd2Ugb25seSBkZXBsb3llZCBhIHNpbmdsZSBzdGFjayAodGhlcmUncyBhIHNpbmdsZSBBUk4gaW4gdGhlIG91dHB1dClcbiAgZXhwZWN0KHN0YWNrQXJuLnNwbGl0KCdcXG4nKS5sZW5ndGgpLnRvRXF1YWwoMSk7XG5cbiAgLy8gdmVyaWZ5IHRoZSBudW1iZXIgb2YgcmVzb3VyY2VzIGluIHRoZSBzdGFja1xuICBjb25zdCByZXNwb25zZSA9IGF3YWl0IGZpeHR1cmUuYXdzLmNsb3VkRm9ybWF0aW9uKCdkZXNjcmliZVN0YWNrUmVzb3VyY2VzJywge1xuICAgIFN0YWNrTmFtZTogc3RhY2tBcm4sXG4gIH0pO1xuICBleHBlY3QocmVzcG9uc2UuU3RhY2tSZXNvdXJjZXM/Lmxlbmd0aCkudG9FcXVhbCgxKTtcbn0pKTtcblxuaW50ZWdUZXN0KCdkZXBsb3kgd2l0aG91dCBleGVjdXRlIGEgbmFtZWQgY2hhbmdlIHNldCcsIHdpdGhEZWZhdWx0Rml4dHVyZShhc3luYyAoZml4dHVyZSkgPT4ge1xuICBjb25zdCBjaGFuZ2VTZXROYW1lID0gJ2N1c3RvbS1jaGFuZ2Utc2V0LW5hbWUnO1xuICBjb25zdCBzdGFja0FybiA9IGF3YWl0IGZpeHR1cmUuY2RrRGVwbG95KCd0ZXN0LTInLCB7XG4gICAgb3B0aW9uczogWyctLW5vLWV4ZWN1dGUnLCAnLS1jaGFuZ2Utc2V0LW5hbWUnLCBjaGFuZ2VTZXROYW1lXSxcbiAgICBjYXB0dXJlU3RkZXJyOiBmYWxzZSxcbiAgfSk7XG4gIC8vIHZlcmlmeSB0aGF0IHdlIG9ubHkgZGVwbG95ZWQgYSBzaW5nbGUgc3RhY2sgKHRoZXJlJ3MgYSBzaW5nbGUgQVJOIGluIHRoZSBvdXRwdXQpXG4gIGV4cGVjdChzdGFja0Fybi5zcGxpdCgnXFxuJykubGVuZ3RoKS50b0VxdWFsKDEpO1xuXG4gIGNvbnN0IHJlc3BvbnNlID0gYXdhaXQgZml4dHVyZS5hd3MuY2xvdWRGb3JtYXRpb24oJ2Rlc2NyaWJlU3RhY2tzJywge1xuICAgIFN0YWNrTmFtZTogc3RhY2tBcm4sXG4gIH0pO1xuICBleHBlY3QocmVzcG9uc2UuU3RhY2tzPy5bMF0uU3RhY2tTdGF0dXMpLnRvRXF1YWwoJ1JFVklFV19JTl9QUk9HUkVTUycpO1xuXG4gIC8vdmVyaWZ5IGEgY2hhbmdlIHNldCB3YXMgY3JlYXRlZCB3aXRoIHRoZSBwcm92aWRlZCBuYW1lXG4gIGNvbnN0IGNoYW5nZVNldFJlc3BvbnNlID0gYXdhaXQgZml4dHVyZS5hd3MuY2xvdWRGb3JtYXRpb24oJ2xpc3RDaGFuZ2VTZXRzJywge1xuICAgIFN0YWNrTmFtZTogc3RhY2tBcm4sXG4gIH0pO1xuICBjb25zdCBjaGFuZ2VTZXRzID0gY2hhbmdlU2V0UmVzcG9uc2UuU3VtbWFyaWVzIHx8IFtdO1xuICBleHBlY3QoY2hhbmdlU2V0cy5sZW5ndGgpLnRvRXF1YWwoMSk7XG4gIGV4cGVjdChjaGFuZ2VTZXRzWzBdLkNoYW5nZVNldE5hbWUpLnRvRXF1YWwoY2hhbmdlU2V0TmFtZSk7XG4gIGV4cGVjdChjaGFuZ2VTZXRzWzBdLlN0YXR1cykudG9FcXVhbCgnQ1JFQVRFX0NPTVBMRVRFJyk7XG59KSk7XG5cbmludGVnVGVzdCgnc2VjdXJpdHkgcmVsYXRlZCBjaGFuZ2VzIHdpdGhvdXQgYSBDTEkgYXJlIGV4cGVjdGVkIHRvIGZhaWwnLCB3aXRoRGVmYXVsdEZpeHR1cmUoYXN5bmMgKGZpeHR1cmUpID0+IHtcbiAgLy8gcmVkaXJlY3QgL2Rldi9udWxsIHRvIHN0ZGluLCB3aGljaCBtZWFucyB0aGVyZSB3aWxsIG5vdCBiZSB0dHkgYXR0YWNoZWRcbiAgLy8gc2luY2UgdGhpcyBzdGFjayBpbmNsdWRlcyBzZWN1cml0eS1yZWxhdGVkIGNoYW5nZXMsIHRoZSBkZXBsb3ltZW50IHNob3VsZFxuICAvLyBpbW1lZGlhdGVseSBmYWlsIGJlY2F1c2Ugd2UgY2FuJ3QgY29uZmlybSB0aGUgY2hhbmdlc1xuICBjb25zdCBzdGFja05hbWUgPSAnaWFtLXRlc3QnO1xuICBhd2FpdCBleHBlY3QoZml4dHVyZS5jZGtEZXBsb3koc3RhY2tOYW1lLCB7XG4gICAgb3B0aW9uczogWyc8JywgJy9kZXYvbnVsbCddLCAvLyBINHgsIHRoaXMgb25seSB3b3JrcyBiZWNhdXNlIEkgaGFwcGVuIHRvIGtub3cgd2UgcGFzcyBzaGVsbDogdHJ1ZS5cbiAgICBuZXZlclJlcXVpcmVBcHByb3ZhbDogZmFsc2UsXG4gIH0pKS5yZWplY3RzLnRvVGhyb3coJ2V4aXRlZCB3aXRoIGVycm9yJyk7XG5cbiAgLy8gRW5zdXJlIHN0YWNrIHdhcyBub3QgZGVwbG95ZWRcbiAgYXdhaXQgZXhwZWN0KGZpeHR1cmUuYXdzLmNsb3VkRm9ybWF0aW9uKCdkZXNjcmliZVN0YWNrcycsIHtcbiAgICBTdGFja05hbWU6IGZpeHR1cmUuZnVsbFN0YWNrTmFtZShzdGFja05hbWUpLFxuICB9KSkucmVqZWN0cy50b1Rocm93KCdkb2VzIG5vdCBleGlzdCcpO1xufSkpO1xuXG5pbnRlZ1Rlc3QoJ2RlcGxveSB3aWxkY2FyZCB3aXRoIG91dHB1dHMnLCB3aXRoRGVmYXVsdEZpeHR1cmUoYXN5bmMgKGZpeHR1cmUpID0+IHtcbiAgY29uc3Qgb3V0cHV0c0ZpbGUgPSBwYXRoLmpvaW4oZml4dHVyZS5pbnRlZ1Rlc3REaXIsICdvdXRwdXRzJywgJ291dHB1dHMuanNvbicpO1xuICBhd2FpdCBmcy5ta2RpcihwYXRoLmRpcm5hbWUob3V0cHV0c0ZpbGUpLCB7IHJlY3Vyc2l2ZTogdHJ1ZSB9KTtcblxuICBhd2FpdCBmaXh0dXJlLmNka0RlcGxveShbJ291dHB1dHMtdGVzdC0qJ10sIHtcbiAgICBvcHRpb25zOiBbJy0tb3V0cHV0cy1maWxlJywgb3V0cHV0c0ZpbGVdLFxuICB9KTtcblxuICBjb25zdCBvdXRwdXRzID0gSlNPTi5wYXJzZSgoYXdhaXQgZnMucmVhZEZpbGUob3V0cHV0c0ZpbGUsIHsgZW5jb2Rpbmc6ICd1dGYtOCcgfSkpLnRvU3RyaW5nKCkpO1xuICBleHBlY3Qob3V0cHV0cykudG9FcXVhbCh7XG4gICAgW2Ake2ZpeHR1cmUuc3RhY2tOYW1lUHJlZml4fS1vdXRwdXRzLXRlc3QtMWBdOiB7XG4gICAgICBUb3BpY05hbWU6IGAke2ZpeHR1cmUuc3RhY2tOYW1lUHJlZml4fS1vdXRwdXRzLXRlc3QtMU15VG9waWNgLFxuICAgIH0sXG4gICAgW2Ake2ZpeHR1cmUuc3RhY2tOYW1lUHJlZml4fS1vdXRwdXRzLXRlc3QtMmBdOiB7XG4gICAgICBUb3BpY05hbWU6IGAke2ZpeHR1cmUuc3RhY2tOYW1lUHJlZml4fS1vdXRwdXRzLXRlc3QtMk15T3RoZXJUb3BpY2AsXG4gICAgfSxcbiAgfSk7XG59KSk7XG5cbmludGVnVGVzdCgnZGVwbG95IHdpdGggcGFyYW1ldGVycycsIHdpdGhEZWZhdWx0Rml4dHVyZShhc3luYyAoZml4dHVyZSkgPT4ge1xuICBjb25zdCBzdGFja0FybiA9IGF3YWl0IGZpeHR1cmUuY2RrRGVwbG95KCdwYXJhbS10ZXN0LTEnLCB7XG4gICAgb3B0aW9uczogW1xuICAgICAgJy0tcGFyYW1ldGVycycsIGBUb3BpY05hbWVQYXJhbT0ke2ZpeHR1cmUuc3RhY2tOYW1lUHJlZml4fWJhemluZ2FgLFxuICAgIF0sXG4gICAgY2FwdHVyZVN0ZGVycjogZmFsc2UsXG4gIH0pO1xuXG4gIGNvbnN0IHJlc3BvbnNlID0gYXdhaXQgZml4dHVyZS5hd3MuY2xvdWRGb3JtYXRpb24oJ2Rlc2NyaWJlU3RhY2tzJywge1xuICAgIFN0YWNrTmFtZTogc3RhY2tBcm4sXG4gIH0pO1xuXG4gIGV4cGVjdChyZXNwb25zZS5TdGFja3M/LlswXS5QYXJhbWV0ZXJzKS50b0VxdWFsKFtcbiAgICB7XG4gICAgICBQYXJhbWV0ZXJLZXk6ICdUb3BpY05hbWVQYXJhbScsXG4gICAgICBQYXJhbWV0ZXJWYWx1ZTogYCR7Zml4dHVyZS5zdGFja05hbWVQcmVmaXh9YmF6aW5nYWAsXG4gICAgfSxcbiAgXSk7XG59KSk7XG5cbmludGVnVGVzdCgndXBkYXRlIHRvIHN0YWNrIGluIFJPTExCQUNLX0NPTVBMRVRFIHN0YXRlIHdpbGwgZGVsZXRlIHN0YWNrIGFuZCBjcmVhdGUgYSBuZXcgb25lJywgd2l0aERlZmF1bHRGaXh0dXJlKGFzeW5jIChmaXh0dXJlKSA9PiB7XG4gIC8vIEdJVkVOXG4gIGF3YWl0IGV4cGVjdChmaXh0dXJlLmNka0RlcGxveSgncGFyYW0tdGVzdC0xJywge1xuICAgIG9wdGlvbnM6IFtcbiAgICAgICctLXBhcmFtZXRlcnMnLCBgVG9waWNOYW1lUGFyYW09JHtmaXh0dXJlLnN0YWNrTmFtZVByZWZpeH1AYXd3YCxcbiAgICBdLFxuICAgIGNhcHR1cmVTdGRlcnI6IGZhbHNlLFxuICB9KSkucmVqZWN0cy50b1Rocm93KCdleGl0ZWQgd2l0aCBlcnJvcicpO1xuXG4gIGNvbnN0IHJlc3BvbnNlID0gYXdhaXQgZml4dHVyZS5hd3MuY2xvdWRGb3JtYXRpb24oJ2Rlc2NyaWJlU3RhY2tzJywge1xuICAgIFN0YWNrTmFtZTogZml4dHVyZS5mdWxsU3RhY2tOYW1lKCdwYXJhbS10ZXN0LTEnKSxcbiAgfSk7XG5cbiAgY29uc3Qgc3RhY2tBcm4gPSByZXNwb25zZS5TdGFja3M/LlswXS5TdGFja0lkO1xuICBleHBlY3QocmVzcG9uc2UuU3RhY2tzPy5bMF0uU3RhY2tTdGF0dXMpLnRvRXF1YWwoJ1JPTExCQUNLX0NPTVBMRVRFJyk7XG5cbiAgLy8gV0hFTlxuICBjb25zdCBuZXdTdGFja0FybiA9IGF3YWl0IGZpeHR1cmUuY2RrRGVwbG95KCdwYXJhbS10ZXN0LTEnLCB7XG4gICAgb3B0aW9uczogW1xuICAgICAgJy0tcGFyYW1ldGVycycsIGBUb3BpY05hbWVQYXJhbT0ke2ZpeHR1cmUuc3RhY2tOYW1lUHJlZml4fWFsbGdvb2RgLFxuICAgIF0sXG4gICAgY2FwdHVyZVN0ZGVycjogZmFsc2UsXG4gIH0pO1xuXG4gIGNvbnN0IG5ld1N0YWNrUmVzcG9uc2UgPSBhd2FpdCBmaXh0dXJlLmF3cy5jbG91ZEZvcm1hdGlvbignZGVzY3JpYmVTdGFja3MnLCB7XG4gICAgU3RhY2tOYW1lOiBuZXdTdGFja0FybixcbiAgfSk7XG5cbiAgLy8gVEhFTlxuICBleHBlY3QgKHN0YWNrQXJuKS5ub3QudG9FcXVhbChuZXdTdGFja0Fybik7IC8vIG5ldyBzdGFjayB3YXMgY3JlYXRlZFxuICBleHBlY3QobmV3U3RhY2tSZXNwb25zZS5TdGFja3M/LlswXS5TdGFja1N0YXR1cykudG9FcXVhbCgnQ1JFQVRFX0NPTVBMRVRFJyk7XG4gIGV4cGVjdChuZXdTdGFja1Jlc3BvbnNlLlN0YWNrcz8uWzBdLlBhcmFtZXRlcnMpLnRvRXF1YWwoW1xuICAgIHtcbiAgICAgIFBhcmFtZXRlcktleTogJ1RvcGljTmFtZVBhcmFtJyxcbiAgICAgIFBhcmFtZXRlclZhbHVlOiBgJHtmaXh0dXJlLnN0YWNrTmFtZVByZWZpeH1hbGxnb29kYCxcbiAgICB9LFxuICBdKTtcbn0pKTtcblxuaW50ZWdUZXN0KCdzdGFjayBpbiBVUERBVEVfUk9MTEJBQ0tfQ09NUExFVEUgc3RhdGUgY2FuIGJlIHVwZGF0ZWQnLCB3aXRoRGVmYXVsdEZpeHR1cmUoYXN5bmMgKGZpeHR1cmUpID0+IHtcbiAgLy8gR0lWRU5cbiAgY29uc3Qgc3RhY2tBcm4gPSBhd2FpdCBmaXh0dXJlLmNka0RlcGxveSgncGFyYW0tdGVzdC0xJywge1xuICAgIG9wdGlvbnM6IFtcbiAgICAgICctLXBhcmFtZXRlcnMnLCBgVG9waWNOYW1lUGFyYW09JHtmaXh0dXJlLnN0YWNrTmFtZVByZWZpeH1uaWNlYCxcbiAgICBdLFxuICAgIGNhcHR1cmVTdGRlcnI6IGZhbHNlLFxuICB9KTtcblxuICBsZXQgcmVzcG9uc2UgPSBhd2FpdCBmaXh0dXJlLmF3cy5jbG91ZEZvcm1hdGlvbignZGVzY3JpYmVTdGFja3MnLCB7XG4gICAgU3RhY2tOYW1lOiBzdGFja0FybixcbiAgfSk7XG5cbiAgZXhwZWN0KHJlc3BvbnNlLlN0YWNrcz8uWzBdLlN0YWNrU3RhdHVzKS50b0VxdWFsKCdDUkVBVEVfQ09NUExFVEUnKTtcblxuICAvLyBiYWQgcGFyYW1ldGVyIG5hbWUgd2l0aCBAIHdpbGwgcHV0IHN0YWNrIGludG8gVVBEQVRFX1JPTExCQUNLX0NPTVBMRVRFXG4gIGF3YWl0IGV4cGVjdChmaXh0dXJlLmNka0RlcGxveSgncGFyYW0tdGVzdC0xJywge1xuICAgIG9wdGlvbnM6IFtcbiAgICAgICctLXBhcmFtZXRlcnMnLCBgVG9waWNOYW1lUGFyYW09JHtmaXh0dXJlLnN0YWNrTmFtZVByZWZpeH1AYXd3YCxcbiAgICBdLFxuICAgIGNhcHR1cmVTdGRlcnI6IGZhbHNlLFxuICB9KSkucmVqZWN0cy50b1Rocm93KCdleGl0ZWQgd2l0aCBlcnJvcicpOztcblxuICByZXNwb25zZSA9IGF3YWl0IGZpeHR1cmUuYXdzLmNsb3VkRm9ybWF0aW9uKCdkZXNjcmliZVN0YWNrcycsIHtcbiAgICBTdGFja05hbWU6IHN0YWNrQXJuLFxuICB9KTtcblxuICBleHBlY3QocmVzcG9uc2UuU3RhY2tzPy5bMF0uU3RhY2tTdGF0dXMpLnRvRXF1YWwoJ1VQREFURV9ST0xMQkFDS19DT01QTEVURScpO1xuXG4gIC8vIFdIRU5cbiAgYXdhaXQgZml4dHVyZS5jZGtEZXBsb3koJ3BhcmFtLXRlc3QtMScsIHtcbiAgICBvcHRpb25zOiBbXG4gICAgICAnLS1wYXJhbWV0ZXJzJywgYFRvcGljTmFtZVBhcmFtPSR7Zml4dHVyZS5zdGFja05hbWVQcmVmaXh9YWxsZ29vZGAsXG4gICAgXSxcbiAgICBjYXB0dXJlU3RkZXJyOiBmYWxzZSxcbiAgfSk7XG5cbiAgcmVzcG9uc2UgPSBhd2FpdCBmaXh0dXJlLmF3cy5jbG91ZEZvcm1hdGlvbignZGVzY3JpYmVTdGFja3MnLCB7XG4gICAgU3RhY2tOYW1lOiBzdGFja0FybixcbiAgfSk7XG5cbiAgLy8gVEhFTlxuICBleHBlY3QocmVzcG9uc2UuU3RhY2tzPy5bMF0uU3RhY2tTdGF0dXMpLnRvRXF1YWwoJ1VQREFURV9DT01QTEVURScpO1xuICBleHBlY3QocmVzcG9uc2UuU3RhY2tzPy5bMF0uUGFyYW1ldGVycykudG9FcXVhbChbXG4gICAge1xuICAgICAgUGFyYW1ldGVyS2V5OiAnVG9waWNOYW1lUGFyYW0nLFxuICAgICAgUGFyYW1ldGVyVmFsdWU6IGAke2ZpeHR1cmUuc3RhY2tOYW1lUHJlZml4fWFsbGdvb2RgLFxuICAgIH0sXG4gIF0pO1xufSkpO1xuXG5pbnRlZ1Rlc3QoJ2RlcGxveSB3aXRoIHdpbGRjYXJkIGFuZCBwYXJhbWV0ZXJzJywgd2l0aERlZmF1bHRGaXh0dXJlKGFzeW5jIChmaXh0dXJlKSA9PiB7XG4gIGF3YWl0IGZpeHR1cmUuY2RrRGVwbG95KCdwYXJhbS10ZXN0LSonLCB7XG4gICAgb3B0aW9uczogW1xuICAgICAgJy0tcGFyYW1ldGVycycsIGAke2ZpeHR1cmUuc3RhY2tOYW1lUHJlZml4fS1wYXJhbS10ZXN0LTE6VG9waWNOYW1lUGFyYW09JHtmaXh0dXJlLnN0YWNrTmFtZVByZWZpeH1iYXppbmdhYCxcbiAgICAgICctLXBhcmFtZXRlcnMnLCBgJHtmaXh0dXJlLnN0YWNrTmFtZVByZWZpeH0tcGFyYW0tdGVzdC0yOk90aGVyVG9waWNOYW1lUGFyYW09JHtmaXh0dXJlLnN0YWNrTmFtZVByZWZpeH1UaGF0c015U3BvdGAsXG4gICAgICAnLS1wYXJhbWV0ZXJzJywgYCR7Zml4dHVyZS5zdGFja05hbWVQcmVmaXh9LXBhcmFtLXRlc3QtMzpEaXNwbGF5TmFtZVBhcmFtPSR7Zml4dHVyZS5zdGFja05hbWVQcmVmaXh9SGV5VGhlcmVgLFxuICAgICAgJy0tcGFyYW1ldGVycycsIGAke2ZpeHR1cmUuc3RhY2tOYW1lUHJlZml4fS1wYXJhbS10ZXN0LTM6T3RoZXJEaXNwbGF5TmFtZVBhcmFtPSR7Zml4dHVyZS5zdGFja05hbWVQcmVmaXh9QW5vdGhlck9uZWAsXG4gICAgXSxcbiAgfSk7XG59KSk7XG5cbmludGVnVGVzdCgnZGVwbG95IHdpdGggcGFyYW1ldGVycyBtdWx0aScsIHdpdGhEZWZhdWx0Rml4dHVyZShhc3luYyAoZml4dHVyZSkgPT4ge1xuICBjb25zdCBwYXJhbVZhbDEgPSBgJHtmaXh0dXJlLnN0YWNrTmFtZVByZWZpeH1iYXppbmdhYDtcbiAgY29uc3QgcGFyYW1WYWwyID0gYCR7Zml4dHVyZS5zdGFja05hbWVQcmVmaXh9PWphZ3NoZW1hc2hgO1xuXG4gIGNvbnN0IHN0YWNrQXJuID0gYXdhaXQgZml4dHVyZS5jZGtEZXBsb3koJ3BhcmFtLXRlc3QtMycsIHtcbiAgICBvcHRpb25zOiBbXG4gICAgICAnLS1wYXJhbWV0ZXJzJywgYERpc3BsYXlOYW1lUGFyYW09JHtwYXJhbVZhbDF9YCxcbiAgICAgICctLXBhcmFtZXRlcnMnLCBgT3RoZXJEaXNwbGF5TmFtZVBhcmFtPSR7cGFyYW1WYWwyfWAsXG4gICAgXSxcbiAgICBjYXB0dXJlU3RkZXJyOiBmYWxzZSxcbiAgfSk7XG5cbiAgY29uc3QgcmVzcG9uc2UgPSBhd2FpdCBmaXh0dXJlLmF3cy5jbG91ZEZvcm1hdGlvbignZGVzY3JpYmVTdGFja3MnLCB7XG4gICAgU3RhY2tOYW1lOiBzdGFja0FybixcbiAgfSk7XG5cbiAgZXhwZWN0KHJlc3BvbnNlLlN0YWNrcz8uWzBdLlBhcmFtZXRlcnMpLnRvRXF1YWwoW1xuICAgIHtcbiAgICAgIFBhcmFtZXRlcktleTogJ0Rpc3BsYXlOYW1lUGFyYW0nLFxuICAgICAgUGFyYW1ldGVyVmFsdWU6IHBhcmFtVmFsMSxcbiAgICB9LFxuICAgIHtcbiAgICAgIFBhcmFtZXRlcktleTogJ090aGVyRGlzcGxheU5hbWVQYXJhbScsXG4gICAgICBQYXJhbWV0ZXJWYWx1ZTogcGFyYW1WYWwyLFxuICAgIH0sXG4gIF0pO1xufSkpO1xuXG5pbnRlZ1Rlc3QoJ2RlcGxveSB3aXRoIG5vdGlmaWNhdGlvbiBBUk4nLCB3aXRoRGVmYXVsdEZpeHR1cmUoYXN5bmMgKGZpeHR1cmUpID0+IHtcbiAgY29uc3QgdG9waWNOYW1lID0gYCR7Zml4dHVyZS5zdGFja05hbWVQcmVmaXh9LXRlc3QtdG9waWNgO1xuXG4gIGNvbnN0IHJlc3BvbnNlID0gYXdhaXQgZml4dHVyZS5hd3Muc25zKCdjcmVhdGVUb3BpYycsIHsgTmFtZTogdG9waWNOYW1lIH0pO1xuICBjb25zdCB0b3BpY0FybiA9IHJlc3BvbnNlLlRvcGljQXJuITtcbiAgdHJ5IHtcbiAgICBhd2FpdCBmaXh0dXJlLmNka0RlcGxveSgndGVzdC0yJywge1xuICAgICAgb3B0aW9uczogWyctLW5vdGlmaWNhdGlvbi1hcm5zJywgdG9waWNBcm5dLFxuICAgIH0pO1xuXG4gICAgLy8gdmVyaWZ5IHRoYXQgdGhlIHN0YWNrIHdlIGRlcGxveWVkIGhhcyBvdXIgbm90aWZpY2F0aW9uIEFSTlxuICAgIGNvbnN0IGRlc2NyaWJlUmVzcG9uc2UgPSBhd2FpdCBmaXh0dXJlLmF3cy5jbG91ZEZvcm1hdGlvbignZGVzY3JpYmVTdGFja3MnLCB7XG4gICAgICBTdGFja05hbWU6IGZpeHR1cmUuZnVsbFN0YWNrTmFtZSgndGVzdC0yJyksXG4gICAgfSk7XG4gICAgZXhwZWN0KGRlc2NyaWJlUmVzcG9uc2UuU3RhY2tzPy5bMF0uTm90aWZpY2F0aW9uQVJOcykudG9FcXVhbChbdG9waWNBcm5dKTtcbiAgfSBmaW5hbGx5IHtcbiAgICBhd2FpdCBmaXh0dXJlLmF3cy5zbnMoJ2RlbGV0ZVRvcGljJywge1xuICAgICAgVG9waWNBcm46IHRvcGljQXJuLFxuICAgIH0pO1xuICB9XG59KSk7XG5cbmludGVnVGVzdCgnZGVwbG95IHdpdGggcm9sZScsIHdpdGhEZWZhdWx0Rml4dHVyZShhc3luYyAoZml4dHVyZSkgPT4ge1xuICBjb25zdCByb2xlTmFtZSA9IGAke2ZpeHR1cmUuc3RhY2tOYW1lUHJlZml4fS10ZXN0LXJvbGVgO1xuXG4gIGF3YWl0IGRlbGV0ZVJvbGUoKTtcblxuICBjb25zdCBjcmVhdGVSZXNwb25zZSA9IGF3YWl0IGZpeHR1cmUuYXdzLmlhbSgnY3JlYXRlUm9sZScsIHtcbiAgICBSb2xlTmFtZTogcm9sZU5hbWUsXG4gICAgQXNzdW1lUm9sZVBvbGljeURvY3VtZW50OiBKU09OLnN0cmluZ2lmeSh7XG4gICAgICBWZXJzaW9uOiAnMjAxMi0xMC0xNycsXG4gICAgICBTdGF0ZW1lbnQ6IFt7XG4gICAgICAgIEFjdGlvbjogJ3N0czpBc3N1bWVSb2xlJyxcbiAgICAgICAgUHJpbmNpcGFsOiB7IFNlcnZpY2U6ICdjbG91ZGZvcm1hdGlvbi5hbWF6b25hd3MuY29tJyB9LFxuICAgICAgICBFZmZlY3Q6ICdBbGxvdycsXG4gICAgICB9LCB7XG4gICAgICAgIEFjdGlvbjogJ3N0czpBc3N1bWVSb2xlJyxcbiAgICAgICAgUHJpbmNpcGFsOiB7IEFXUzogKGF3YWl0IGZpeHR1cmUuYXdzLnN0cygnZ2V0Q2FsbGVySWRlbnRpdHknLCB7fSkpLkFybiB9LFxuICAgICAgICBFZmZlY3Q6ICdBbGxvdycsXG4gICAgICB9XSxcbiAgICB9KSxcbiAgfSk7XG4gIGNvbnN0IHJvbGVBcm4gPSBjcmVhdGVSZXNwb25zZS5Sb2xlLkFybjtcbiAgdHJ5IHtcbiAgICBhd2FpdCBmaXh0dXJlLmF3cy5pYW0oJ3B1dFJvbGVQb2xpY3knLCB7XG4gICAgICBSb2xlTmFtZTogcm9sZU5hbWUsXG4gICAgICBQb2xpY3lOYW1lOiAnRGVmYXVsdFBvbGljeScsXG4gICAgICBQb2xpY3lEb2N1bWVudDogSlNPTi5zdHJpbmdpZnkoe1xuICAgICAgICBWZXJzaW9uOiAnMjAxMi0xMC0xNycsXG4gICAgICAgIFN0YXRlbWVudDogW3tcbiAgICAgICAgICBBY3Rpb246ICcqJyxcbiAgICAgICAgICBSZXNvdXJjZTogJyonLFxuICAgICAgICAgIEVmZmVjdDogJ0FsbG93JyxcbiAgICAgICAgfV0sXG4gICAgICB9KSxcbiAgICB9KTtcblxuICAgIGF3YWl0IHJldHJ5KGZpeHR1cmUub3V0cHV0LCAnVHJ5aW5nIHRvIGFzc3VtZSBmcmVzaCByb2xlJywgcmV0cnkuZm9yU2Vjb25kcygzMDApLCBhc3luYyAoKSA9PiB7XG4gICAgICBhd2FpdCBmaXh0dXJlLmF3cy5zdHMoJ2Fzc3VtZVJvbGUnLCB7XG4gICAgICAgIFJvbGVBcm46IHJvbGVBcm4sXG4gICAgICAgIFJvbGVTZXNzaW9uTmFtZTogJ3Rlc3RpbmcnLFxuICAgICAgfSk7XG4gICAgfSk7XG5cbiAgICAvLyBJbiBwcmluY2lwbGUsIHRoZSByb2xlIGhhcyByZXBsaWNhdGVkIGZyb20gJ3VzLWVhc3QtMScgdG8gd2hlcmV2ZXIgd2UncmUgdGVzdGluZy5cbiAgICAvLyBHaXZlIGl0IGEgbGl0dGxlIG1vcmUgc2xlZXAgdG8gbWFrZSBzdXJlIENsb3VkRm9ybWF0aW9uIGlzIG5vdCBoaXR0aW5nIGEgYm94XG4gICAgLy8gdGhhdCBkb2Vzbid0IGhhdmUgaXQgeWV0LlxuICAgIGF3YWl0IHNsZWVwKDUwMDApO1xuXG4gICAgYXdhaXQgZml4dHVyZS5jZGtEZXBsb3koJ3Rlc3QtMicsIHtcbiAgICAgIG9wdGlvbnM6IFsnLS1yb2xlLWFybicsIHJvbGVBcm5dLFxuICAgIH0pO1xuXG4gICAgLy8gSW1tZWRpYXRlbHkgZGVsZXRlIHRoZSBzdGFjayBhZ2FpbiBiZWZvcmUgd2UgZGVsZXRlIHRoZSByb2xlLlxuICAgIC8vXG4gICAgLy8gU2luY2Ugcm9sZXMgYXJlIHN0aWNreSwgaWYgd2UgZGVsZXRlIHRoZSByb2xlIGJlZm9yZSB0aGUgc3RhY2ssIHN1YnNlcXVlbnQgRGVsZXRlU3RhY2tcbiAgICAvLyBvcGVyYXRpb25zIHdpbGwgZmFpbCB3aGVuIENsb3VkRm9ybWF0aW9uIHRyaWVzIHRvIGFzc3VtZSB0aGUgcm9sZSB0aGF0J3MgYWxyZWFkeSBnb25lLlxuICAgIGF3YWl0IGZpeHR1cmUuY2RrRGVzdHJveSgndGVzdC0yJyk7XG5cbiAgfSBmaW5hbGx5IHtcbiAgICBhd2FpdCBkZWxldGVSb2xlKCk7XG4gIH1cblxuICBhc3luYyBmdW5jdGlvbiBkZWxldGVSb2xlKCkge1xuICAgIHRyeSB7XG4gICAgICBmb3IgKGNvbnN0IHBvbGljeU5hbWUgb2YgKGF3YWl0IGZpeHR1cmUuYXdzLmlhbSgnbGlzdFJvbGVQb2xpY2llcycsIHsgUm9sZU5hbWU6IHJvbGVOYW1lIH0pKS5Qb2xpY3lOYW1lcykge1xuICAgICAgICBhd2FpdCBmaXh0dXJlLmF3cy5pYW0oJ2RlbGV0ZVJvbGVQb2xpY3knLCB7XG4gICAgICAgICAgUm9sZU5hbWU6IHJvbGVOYW1lLFxuICAgICAgICAgIFBvbGljeU5hbWU6IHBvbGljeU5hbWUsXG4gICAgICAgIH0pO1xuICAgICAgfVxuICAgICAgYXdhaXQgZml4dHVyZS5hd3MuaWFtKCdkZWxldGVSb2xlJywgeyBSb2xlTmFtZTogcm9sZU5hbWUgfSk7XG4gICAgfSBjYXRjaCAoZSkge1xuICAgICAgaWYgKGUubWVzc2FnZS5pbmRleE9mKCdjYW5ub3QgYmUgZm91bmQnKSA+IC0xKSB7IHJldHVybjsgfVxuICAgICAgdGhyb3cgZTtcbiAgICB9XG4gIH1cbn0pKTtcblxuaW50ZWdUZXN0KCdjZGsgZGlmZicsIHdpdGhEZWZhdWx0Rml4dHVyZShhc3luYyAoZml4dHVyZSkgPT4ge1xuICBjb25zdCBkaWZmMSA9IGF3YWl0IGZpeHR1cmUuY2RrKFsnZGlmZicsIGZpeHR1cmUuZnVsbFN0YWNrTmFtZSgndGVzdC0xJyldKTtcbiAgZXhwZWN0KGRpZmYxKS50b0NvbnRhaW4oJ0FXUzo6U05TOjpUb3BpYycpO1xuXG4gIGNvbnN0IGRpZmYyID0gYXdhaXQgZml4dHVyZS5jZGsoWydkaWZmJywgZml4dHVyZS5mdWxsU3RhY2tOYW1lKCd0ZXN0LTInKV0pO1xuICBleHBlY3QoZGlmZjIpLnRvQ29udGFpbignQVdTOjpTTlM6OlRvcGljJyk7XG5cbiAgLy8gV2UgY2FuIG1ha2UgaXQgZmFpbCBieSBwYXNzaW5nIC0tZmFpbFxuICBhd2FpdCBleHBlY3QoZml4dHVyZS5jZGsoWydkaWZmJywgJy0tZmFpbCcsIGZpeHR1cmUuZnVsbFN0YWNrTmFtZSgndGVzdC0xJyldKSlcbiAgICAucmVqZWN0cy50b1Rocm93KCdleGl0ZWQgd2l0aCBlcnJvcicpO1xufSkpO1xuXG5pbnRlZ1Rlc3QoJ2NkayBkaWZmIC0tZmFpbCBvbiBtdWx0aXBsZSBzdGFja3MgZXhpdHMgd2l0aCBlcnJvciBpZiBhbnkgb2YgdGhlIHN0YWNrcyBjb250YWlucyBhIGRpZmYnLCB3aXRoRGVmYXVsdEZpeHR1cmUoYXN5bmMgKGZpeHR1cmUpID0+IHtcbiAgLy8gR0lWRU5cbiAgY29uc3QgZGlmZjEgPSBhd2FpdCBmaXh0dXJlLmNkayhbJ2RpZmYnLCBmaXh0dXJlLmZ1bGxTdGFja05hbWUoJ3Rlc3QtMScpXSk7XG4gIGV4cGVjdChkaWZmMSkudG9Db250YWluKCdBV1M6OlNOUzo6VG9waWMnKTtcblxuICBhd2FpdCBmaXh0dXJlLmNka0RlcGxveSgndGVzdC0yJyk7XG4gIGNvbnN0IGRpZmYyID0gYXdhaXQgZml4dHVyZS5jZGsoWydkaWZmJywgZml4dHVyZS5mdWxsU3RhY2tOYW1lKCd0ZXN0LTInKV0pO1xuICBleHBlY3QoZGlmZjIpLnRvQ29udGFpbignVGhlcmUgd2VyZSBubyBkaWZmZXJlbmNlcycpO1xuXG4gIC8vIFdIRU4gLyBUSEVOXG4gIGF3YWl0IGV4cGVjdChmaXh0dXJlLmNkayhbJ2RpZmYnLCAnLS1mYWlsJywgZml4dHVyZS5mdWxsU3RhY2tOYW1lKCd0ZXN0LTEnKSwgZml4dHVyZS5mdWxsU3RhY2tOYW1lKCd0ZXN0LTInKV0pKS5yZWplY3RzLnRvVGhyb3coJ2V4aXRlZCB3aXRoIGVycm9yJyk7XG59KSk7XG5cbmludGVnVGVzdCgnY2RrIGRpZmYgLS1mYWlsIHdpdGggbXVsdGlwbGUgc3RhY2sgZXhpdHMgd2l0aCBpZiBhbnkgb2YgdGhlIHN0YWNrcyBjb250YWlucyBhIGRpZmYnLCB3aXRoRGVmYXVsdEZpeHR1cmUoYXN5bmMgKGZpeHR1cmUpID0+IHtcbiAgLy8gR0lWRU5cbiAgYXdhaXQgZml4dHVyZS5jZGtEZXBsb3koJ3Rlc3QtMScpO1xuICBjb25zdCBkaWZmMSA9IGF3YWl0IGZpeHR1cmUuY2RrKFsnZGlmZicsIGZpeHR1cmUuZnVsbFN0YWNrTmFtZSgndGVzdC0xJyldKTtcbiAgZXhwZWN0KGRpZmYxKS50b0NvbnRhaW4oJ1RoZXJlIHdlcmUgbm8gZGlmZmVyZW5jZXMnKTtcblxuICBjb25zdCBkaWZmMiA9IGF3YWl0IGZpeHR1cmUuY2RrKFsnZGlmZicsIGZpeHR1cmUuZnVsbFN0YWNrTmFtZSgndGVzdC0yJyldKTtcbiAgZXhwZWN0KGRpZmYyKS50b0NvbnRhaW4oJ0FXUzo6U05TOjpUb3BpYycpO1xuXG4gIC8vIFdIRU4gLyBUSEVOXG4gIGF3YWl0IGV4cGVjdChmaXh0dXJlLmNkayhbJ2RpZmYnLCAnLS1mYWlsJywgZml4dHVyZS5mdWxsU3RhY2tOYW1lKCd0ZXN0LTEnKSwgZml4dHVyZS5mdWxsU3RhY2tOYW1lKCd0ZXN0LTInKV0pKS5yZWplY3RzLnRvVGhyb3coJ2V4aXRlZCB3aXRoIGVycm9yJyk7XG59KSk7XG5cbmludGVnVGVzdCgnY2RrIGRpZmYgLS1zZWN1cml0eS1vbmx5IC0tZmFpbCBleGl0cyB3aGVuIHNlY3VyaXR5IGNoYW5nZXMgYXJlIHByZXNlbnQnLCB3aXRoRGVmYXVsdEZpeHR1cmUoYXN5bmMgKGZpeHR1cmUpID0+IHtcbiAgY29uc3Qgc3RhY2tOYW1lID0gJ2lhbS10ZXN0JztcbiAgYXdhaXQgZXhwZWN0KGZpeHR1cmUuY2RrKFsnZGlmZicsICctLXNlY3VyaXR5LW9ubHknLCAnLS1mYWlsJywgZml4dHVyZS5mdWxsU3RhY2tOYW1lKHN0YWNrTmFtZSldKSkucmVqZWN0cy50b1Rocm93KCdleGl0ZWQgd2l0aCBlcnJvcicpO1xufSkpO1xuXG5pbnRlZ1Rlc3QoJ2RlcGxveSBzdGFjayB3aXRoIGRvY2tlciBhc3NldCcsIHdpdGhEZWZhdWx0Rml4dHVyZShhc3luYyAoZml4dHVyZSkgPT4ge1xuICBhd2FpdCBmaXh0dXJlLmNka0RlcGxveSgnZG9ja2VyJyk7XG59KSk7XG5cbmludGVnVGVzdCgnZGVwbG95IGFuZCB0ZXN0IHN0YWNrIHdpdGggbGFtYmRhIGFzc2V0Jywgd2l0aERlZmF1bHRGaXh0dXJlKGFzeW5jIChmaXh0dXJlKSA9PiB7XG4gIGNvbnN0IHN0YWNrQXJuID0gYXdhaXQgZml4dHVyZS5jZGtEZXBsb3koJ2xhbWJkYScsIHsgY2FwdHVyZVN0ZGVycjogZmFsc2UgfSk7XG5cbiAgY29uc3QgcmVzcG9uc2UgPSBhd2FpdCBmaXh0dXJlLmF3cy5jbG91ZEZvcm1hdGlvbignZGVzY3JpYmVTdGFja3MnLCB7XG4gICAgU3RhY2tOYW1lOiBzdGFja0FybixcbiAgfSk7XG4gIGNvbnN0IGxhbWJkYUFybiA9IHJlc3BvbnNlLlN0YWNrcz8uWzBdLk91dHB1dHM/LlswXS5PdXRwdXRWYWx1ZTtcbiAgaWYgKGxhbWJkYUFybiA9PT0gdW5kZWZpbmVkKSB7XG4gICAgdGhyb3cgbmV3IEVycm9yKCdTdGFjayBkaWQgbm90IGhhdmUgZXhwZWN0ZWQgTGFtYmRhIEFSTiBvdXRwdXQnKTtcbiAgfVxuXG4gIGNvbnN0IG91dHB1dCA9IGF3YWl0IGZpeHR1cmUuYXdzLmxhbWJkYSgnaW52b2tlJywge1xuICAgIEZ1bmN0aW9uTmFtZTogbGFtYmRhQXJuLFxuICB9KTtcblxuICBleHBlY3QoSlNPTi5zdHJpbmdpZnkob3V0cHV0LlBheWxvYWQpKS50b0NvbnRhaW4oJ2RlYXIgYXNzZXQnKTtcbn0pKTtcblxuaW50ZWdUZXN0KCdjZGsgbHMnLCB3aXRoRGVmYXVsdEZpeHR1cmUoYXN5bmMgKGZpeHR1cmUpID0+IHtcbiAgY29uc3QgbGlzdGluZyA9IGF3YWl0IGZpeHR1cmUuY2RrKFsnbHMnXSwgeyBjYXB0dXJlU3RkZXJyOiBmYWxzZSB9KTtcblxuICBjb25zdCBleHBlY3RlZFN0YWNrcyA9IFtcbiAgICAnY29uZGl0aW9uYWwtcmVzb3VyY2UnLFxuICAgICdkb2NrZXInLFxuICAgICdkb2NrZXItd2l0aC1jdXN0b20tZmlsZScsXG4gICAgJ2ZhaWxlZCcsXG4gICAgJ2lhbS10ZXN0JyxcbiAgICAnbGFtYmRhJyxcbiAgICAnbWlzc2luZy1zc20tcGFyYW1ldGVyJyxcbiAgICAnb3JkZXItcHJvdmlkaW5nJyxcbiAgICAnb3V0cHV0cy10ZXN0LTEnLFxuICAgICdvdXRwdXRzLXRlc3QtMicsXG4gICAgJ3BhcmFtLXRlc3QtMScsXG4gICAgJ3BhcmFtLXRlc3QtMicsXG4gICAgJ3BhcmFtLXRlc3QtMycsXG4gICAgJ3Rlcm1pbmF0aW9uLXByb3RlY3Rpb24nLFxuICAgICd0ZXN0LTEnLFxuICAgICd0ZXN0LTInLFxuICAgICd3aXRoLW5lc3RlZC1zdGFjaycsXG4gICAgJ3dpdGgtbmVzdGVkLXN0YWNrLXVzaW5nLXBhcmFtZXRlcnMnLFxuICAgICdvcmRlci1jb25zdW1pbmcnLFxuICBdO1xuXG4gIGZvciAoY29uc3Qgc3RhY2sgb2YgZXhwZWN0ZWRTdGFja3MpIHtcbiAgICBleHBlY3QobGlzdGluZykudG9Db250YWluKGZpeHR1cmUuZnVsbFN0YWNrTmFtZShzdGFjaykpO1xuICB9XG59KSk7XG5cbmludGVnVGVzdCgnc3ludGhpbmcgYSBzdGFnZSB3aXRoIGVycm9ycyBsZWFkcyB0byBmYWlsdXJlJywgd2l0aERlZmF1bHRGaXh0dXJlKGFzeW5jIChmaXh0dXJlKSA9PiB7XG4gIGNvbnN0IG91dHB1dCA9IGF3YWl0IGZpeHR1cmUuY2RrKFsnc3ludGgnXSwge1xuICAgIGFsbG93RXJyRXhpdDogdHJ1ZSxcbiAgICBtb2RFbnY6IHtcbiAgICAgIElOVEVHX1NUQUNLX1NFVDogJ3N0YWdlLXdpdGgtZXJyb3JzJyxcbiAgICB9LFxuICB9KTtcblxuICBleHBlY3Qob3V0cHV0KS50b0NvbnRhaW4oJ1RoaXMgaXMgYW4gZXJyb3InKTtcbn0pKTtcblxuaW50ZWdUZXN0KCdzeW50aGluZyBhIHN0YWdlIHdpdGggZXJyb3JzIGNhbiBiZSBzdXBwcmVzc2VkJywgd2l0aERlZmF1bHRGaXh0dXJlKGFzeW5jIChmaXh0dXJlKSA9PiB7XG4gIGF3YWl0IGZpeHR1cmUuY2RrKFsnc3ludGgnLCAnLS1uby12YWxpZGF0aW9uJ10sIHtcbiAgICBtb2RFbnY6IHtcbiAgICAgIElOVEVHX1NUQUNLX1NFVDogJ3N0YWdlLXdpdGgtZXJyb3JzJyxcbiAgICB9LFxuICB9KTtcbn0pKTtcblxuaW50ZWdUZXN0KCdkZXBsb3kgc3RhY2sgd2l0aG91dCByZXNvdXJjZScsIHdpdGhEZWZhdWx0Rml4dHVyZShhc3luYyAoZml4dHVyZSkgPT4ge1xuICAvLyBEZXBsb3kgdGhlIHN0YWNrIHdpdGhvdXQgcmVzb3VyY2VzXG4gIGF3YWl0IGZpeHR1cmUuY2RrRGVwbG95KCdjb25kaXRpb25hbC1yZXNvdXJjZScsIHsgbW9kRW52OiB7IE5PX1JFU09VUkNFOiAnVFJVRScgfSB9KTtcblxuICAvLyBUaGlzIHNob3VsZCBoYXZlIHN1Y2NlZWRlZCBidXQgbm90IGRlcGxveWVkIHRoZSBzdGFjay5cbiAgYXdhaXQgZXhwZWN0KGZpeHR1cmUuYXdzLmNsb3VkRm9ybWF0aW9uKCdkZXNjcmliZVN0YWNrcycsIHsgU3RhY2tOYW1lOiBmaXh0dXJlLmZ1bGxTdGFja05hbWUoJ2NvbmRpdGlvbmFsLXJlc291cmNlJykgfSkpXG4gICAgLnJlamVjdHMudG9UaHJvdygnY29uZGl0aW9uYWwtcmVzb3VyY2UgZG9lcyBub3QgZXhpc3QnKTtcblxuICAvLyBEZXBsb3kgdGhlIHN0YWNrIHdpdGggcmVzb3VyY2VzXG4gIGF3YWl0IGZpeHR1cmUuY2RrRGVwbG95KCdjb25kaXRpb25hbC1yZXNvdXJjZScpO1xuXG4gIC8vIFRoZW4gYWdhaW4gV0lUSE9VVCByZXNvdXJjZXMgKHRoaXMgc2hvdWxkIGRlc3Ryb3kgdGhlIHN0YWNrKVxuICBhd2FpdCBmaXh0dXJlLmNka0RlcGxveSgnY29uZGl0aW9uYWwtcmVzb3VyY2UnLCB7IG1vZEVudjogeyBOT19SRVNPVVJDRTogJ1RSVUUnIH0gfSk7XG5cbiAgYXdhaXQgZXhwZWN0KGZpeHR1cmUuYXdzLmNsb3VkRm9ybWF0aW9uKCdkZXNjcmliZVN0YWNrcycsIHsgU3RhY2tOYW1lOiBmaXh0dXJlLmZ1bGxTdGFja05hbWUoJ2NvbmRpdGlvbmFsLXJlc291cmNlJykgfSkpXG4gICAgLnJlamVjdHMudG9UaHJvdygnY29uZGl0aW9uYWwtcmVzb3VyY2UgZG9lcyBub3QgZXhpc3QnKTtcbn0pKTtcblxuaW50ZWdUZXN0KCdJQU0gZGlmZicsIHdpdGhEZWZhdWx0Rml4dHVyZShhc3luYyAoZml4dHVyZSkgPT4ge1xuICBjb25zdCBvdXRwdXQgPSBhd2FpdCBmaXh0dXJlLmNkayhbJ2RpZmYnLCBmaXh0dXJlLmZ1bGxTdGFja05hbWUoJ2lhbS10ZXN0JyldKTtcblxuICAvLyBSb3VnaGx5IGNoZWNrIGZvciBhIHRhYmxlIGxpa2UgdGhpczpcbiAgLy9cbiAgLy8g4pSM4pSA4pSA4pSA4pSs4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSs4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSs4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSs4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSALeKUgOKUgOKUrOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUkFxuICAvLyDilIIgICDilIIgUmVzb3VyY2UgICAgICAgIOKUgiBFZmZlY3Qg4pSCIEFjdGlvbiAgICAgICAgIOKUgiBQcmluY2lwYWwgICAgICAgICAgICAgICAgICAgICDilIIgQ29uZGl0aW9uIOKUglxuICAvLyDilJzilIDilIDilIDilLzilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilLzilIDilIDilIDilIDilIDilIDilIDilIDilLzilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilLzilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilLzilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilKRcbiAgLy8g4pSCICsg4pSCICR7U29tZVJvbGUuQXJufSDilIIgQWxsb3cgIOKUgiBzdHM6QXNzdW1lUm9sZSDilIIgU2VydmljZTplYzIuYW1hem9uYXdzLmNvbSAgICAg4pSCICAgICAgICAgICDilIJcbiAgLy8g4pSU4pSA4pSA4pSA4pS04pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pS04pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pS04pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pS04pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pS04pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSYXG5cbiAgZXhwZWN0KG91dHB1dCkudG9Db250YWluKCcke1NvbWVSb2xlLkFybn0nKTtcbiAgZXhwZWN0KG91dHB1dCkudG9Db250YWluKCdzdHM6QXNzdW1lUm9sZScpO1xuICBleHBlY3Qob3V0cHV0KS50b0NvbnRhaW4oJ2VjMi5hbWF6b25hd3MuY29tJyk7XG59KSk7XG5cbmludGVnVGVzdCgnZmFzdCBkZXBsb3knLCB3aXRoRGVmYXVsdEZpeHR1cmUoYXN5bmMgKGZpeHR1cmUpID0+IHtcbiAgLy8gd2UgYXJlIHVzaW5nIGEgc3RhY2sgd2l0aCBhIG5lc3RlZCBzdGFjayBiZWNhdXNlIENGTiB3aWxsIGFsd2F5cyBhdHRlbXB0IHRvXG4gIC8vIHVwZGF0ZSBhIG5lc3RlZCBzdGFjaywgd2hpY2ggd2lsbCBhbGxvdyB1cyB0byB2ZXJpZnkgdGhhdCB1cGRhdGVzIGFyZSBhY3R1YWxseVxuICAvLyBza2lwcGVkIHVubGVzcyAtLWZvcmNlIGlzIHNwZWNpZmllZC5cbiAgY29uc3Qgc3RhY2tBcm4gPSBhd2FpdCBmaXh0dXJlLmNka0RlcGxveSgnd2l0aC1uZXN0ZWQtc3RhY2snLCB7IGNhcHR1cmVTdGRlcnI6IGZhbHNlIH0pO1xuICBjb25zdCBjaGFuZ2VTZXQxID0gYXdhaXQgZ2V0TGF0ZXN0Q2hhbmdlU2V0KCk7XG5cbiAgLy8gRGVwbG95IHRoZSBzYW1lIHN0YWNrIGFnYWluLCB0aGVyZSBzaG91bGQgYmUgbm8gbmV3IGNoYW5nZSBzZXQgY3JlYXRlZFxuICBhd2FpdCBmaXh0dXJlLmNka0RlcGxveSgnd2l0aC1uZXN0ZWQtc3RhY2snKTtcbiAgY29uc3QgY2hhbmdlU2V0MiA9IGF3YWl0IGdldExhdGVzdENoYW5nZVNldCgpO1xuICBleHBlY3QoY2hhbmdlU2V0Mi5DaGFuZ2VTZXRJZCkudG9FcXVhbChjaGFuZ2VTZXQxLkNoYW5nZVNldElkKTtcblxuICAvLyBEZXBsb3kgdGhlIHN0YWNrIGFnYWluIHdpdGggLS1mb3JjZSwgbm93IHdlIHNob3VsZCBjcmVhdGUgYSBjaGFuZ2VzZXRcbiAgYXdhaXQgZml4dHVyZS5jZGtEZXBsb3koJ3dpdGgtbmVzdGVkLXN0YWNrJywgeyBvcHRpb25zOiBbJy0tZm9yY2UnXSB9KTtcbiAgY29uc3QgY2hhbmdlU2V0MyA9IGF3YWl0IGdldExhdGVzdENoYW5nZVNldCgpO1xuICBleHBlY3QoY2hhbmdlU2V0My5DaGFuZ2VTZXRJZCkubm90LnRvRXF1YWwoY2hhbmdlU2V0Mi5DaGFuZ2VTZXRJZCk7XG5cbiAgLy8gRGVwbG95IHRoZSBzdGFjayBhZ2FpbiB3aXRoIHRhZ3MsIGV4cGVjdGVkIHRvIGNyZWF0ZSBhIG5ldyBjaGFuZ2VzZXRcbiAgLy8gZXZlbiB0aG91Z2ggdGhlIHJlc291cmNlcyBkaWRuJ3QgY2hhbmdlLlxuICBhd2FpdCBmaXh0dXJlLmNka0RlcGxveSgnd2l0aC1uZXN0ZWQtc3RhY2snLCB7IG9wdGlvbnM6IFsnLS10YWdzJywgJ2tleT12YWx1ZSddIH0pO1xuICBjb25zdCBjaGFuZ2VTZXQ0ID0gYXdhaXQgZ2V0TGF0ZXN0Q2hhbmdlU2V0KCk7XG4gIGV4cGVjdChjaGFuZ2VTZXQ0LkNoYW5nZVNldElkKS5ub3QudG9FcXVhbChjaGFuZ2VTZXQzLkNoYW5nZVNldElkKTtcblxuICBhc3luYyBmdW5jdGlvbiBnZXRMYXRlc3RDaGFuZ2VTZXQoKSB7XG4gICAgY29uc3QgcmVzcG9uc2UgPSBhd2FpdCBmaXh0dXJlLmF3cy5jbG91ZEZvcm1hdGlvbignZGVzY3JpYmVTdGFja3MnLCB7IFN0YWNrTmFtZTogc3RhY2tBcm4gfSk7XG4gICAgaWYgKCFyZXNwb25zZS5TdGFja3M/LlswXSkgeyB0aHJvdyBuZXcgRXJyb3IoJ0RpZCBub3QgZ2V0IGEgQ2hhbmdlU2V0IGF0IGFsbCcpOyB9XG4gICAgZml4dHVyZS5sb2coYEZvdW5kIENoYW5nZSBTZXQgJHtyZXNwb25zZS5TdGFja3M/LlswXS5DaGFuZ2VTZXRJZH1gKTtcbiAgICByZXR1cm4gcmVzcG9uc2UuU3RhY2tzPy5bMF07XG4gIH1cbn0pKTtcblxuaW50ZWdUZXN0KCdmYWlsZWQgZGVwbG95IGRvZXMgbm90IGhhbmcnLCB3aXRoRGVmYXVsdEZpeHR1cmUoYXN5bmMgKGZpeHR1cmUpID0+IHtcbiAgLy8gdGhpcyB3aWxsIGhhbmcgaWYgd2UgaW50cm9kdWNlIGh0dHBzOi8vZ2l0aHViLmNvbS9hd3MvYXdzLWNkay9pc3N1ZXMvNjQwMyBhZ2Fpbi5cbiAgYXdhaXQgZXhwZWN0KGZpeHR1cmUuY2RrRGVwbG95KCdmYWlsZWQnKSkucmVqZWN0cy50b1Rocm93KCdleGl0ZWQgd2l0aCBlcnJvcicpO1xufSkpO1xuXG5pbnRlZ1Rlc3QoJ2NhbiBzdGlsbCBsb2FkIG9sZCBhc3NlbWJsaWVzJywgd2l0aERlZmF1bHRGaXh0dXJlKGFzeW5jIChmaXh0dXJlKSA9PiB7XG4gIGNvbnN0IGN4QXNtRGlyID0gcGF0aC5qb2luKG9zLnRtcGRpcigpLCAnY2RrLWludGVnLWN4Jyk7XG5cbiAgY29uc3QgdGVzdEFzc2VtYmxpZXNEaXJlY3RvcnkgPSBwYXRoLmpvaW4oX19kaXJuYW1lLCAnY2xvdWQtYXNzZW1ibGllcycpO1xuICBmb3IgKGNvbnN0IGFzbWRpciBvZiBhd2FpdCBsaXN0Q2hpbGREaXJzKHRlc3RBc3NlbWJsaWVzRGlyZWN0b3J5KSkge1xuICAgIGZpeHR1cmUubG9nKGBBU1NFTUJMWSAke2FzbWRpcn1gKTtcbiAgICBhd2FpdCBjbG9uZURpcmVjdG9yeShhc21kaXIsIGN4QXNtRGlyKTtcblxuICAgIC8vIFNvbWUgZmlsZXMgaW4gdGhlIGFzbSBkaXJlY3RvcnkgdGhhdCBoYXZlIGEgLmpzIGV4dGVuc2lvbiBhcmVcbiAgICAvLyBhY3R1YWxseSB0cmVhdGVkIGFzIHRlbXBsYXRlcy4gRXZhbHVhdGUgdGhlbSB1c2luZyBOb2RlSlMuXG4gICAgY29uc3QgdGVtcGxhdGVzID0gYXdhaXQgbGlzdENoaWxkcmVuKGN4QXNtRGlyLCBmdWxsUGF0aCA9PiBQcm9taXNlLnJlc29sdmUoZnVsbFBhdGguZW5kc1dpdGgoJy5qcycpKSk7XG4gICAgZm9yIChjb25zdCB0ZW1wbGF0ZSBvZiB0ZW1wbGF0ZXMpIHtcbiAgICAgIGNvbnN0IHRhcmdldE5hbWUgPSB0ZW1wbGF0ZS5yZXBsYWNlKC8uanMkLywgJycpO1xuICAgICAgYXdhaXQgc2hlbGwoW3Byb2Nlc3MuZXhlY1BhdGgsIHRlbXBsYXRlLCAnPicsIHRhcmdldE5hbWVdLCB7XG4gICAgICAgIGN3ZDogY3hBc21EaXIsXG4gICAgICAgIG91dHB1dDogZml4dHVyZS5vdXRwdXQsXG4gICAgICAgIG1vZEVudjoge1xuICAgICAgICAgIFRFU1RfQUNDT1VOVDogYXdhaXQgZml4dHVyZS5hd3MuYWNjb3VudCgpLFxuICAgICAgICAgIFRFU1RfUkVHSU9OOiBmaXh0dXJlLmF3cy5yZWdpb24sXG4gICAgICAgIH0sXG4gICAgICB9KTtcbiAgICB9XG5cbiAgICAvLyBVc2UgdGhpcyBkaXJlY3RvcnkgYXMgYSBDbG91ZCBBc3NlbWJseVxuICAgIGNvbnN0IG91dHB1dCA9IGF3YWl0IGZpeHR1cmUuY2RrKFtcbiAgICAgICctLWFwcCcsIGN4QXNtRGlyLFxuICAgICAgJy12JyxcbiAgICAgICdzeW50aCcsXG4gICAgXSk7XG5cbiAgICAvLyBBc3NlcnQgdGhhdCB0aGVyZSB3YXMgbm8gcHJvdmlkZXJFcnJvciBpbiBDREsncyBzdGRlcnJcbiAgICAvLyBCZWNhdXNlIHdlIHJlbHkgb24gdGhlIGFwcC9mcmFtZXdvcmsgdG8gYWN0dWFsbHkgZXJyb3IgaW4gY2FzZSB0aGVcbiAgICAvLyBwcm92aWRlciBmYWlscywgd2UgaW5zcGVjdCB0aGUgbG9ncyBoZXJlLlxuICAgIGV4cGVjdChvdXRwdXQpLm5vdC50b0NvbnRhaW4oJyRwcm92aWRlckVycm9yJyk7XG4gIH1cbn0pKTtcblxuaW50ZWdUZXN0KCdnZW5lcmF0aW5nIGFuZCBsb2FkaW5nIGFzc2VtYmx5Jywgd2l0aERlZmF1bHRGaXh0dXJlKGFzeW5jIChmaXh0dXJlKSA9PiB7XG4gIGNvbnN0IGFzbU91dHB1dERpciA9IGAke2ZpeHR1cmUuaW50ZWdUZXN0RGlyfS1jZGstaW50ZWctYXNtYDtcbiAgYXdhaXQgZml4dHVyZS5zaGVsbChbJ3JtJywgJy1yZicsIGFzbU91dHB1dERpcl0pO1xuXG4gIC8vIFN5bnRoZXNpemUgYSBDbG91ZCBBc3NlbWJseSB0b3RoZSBkZWZhdWx0IGRpcmVjdG9yeSAoY2RrLm91dCkgYW5kIGEgc3BlY2lmaWMgZGlyZWN0b3J5LlxuICBhd2FpdCBmaXh0dXJlLmNkayhbJ3N5bnRoJ10pO1xuICBhd2FpdCBmaXh0dXJlLmNkayhbJ3N5bnRoJywgJy0tb3V0cHV0JywgYXNtT3V0cHV0RGlyXSk7XG5cbiAgLy8gY2RrLm91dCBpbiB0aGUgY3VycmVudCBkaXJlY3RvcnkgYW5kIHRoZSBpbmRpY2F0ZWQgLS1vdXRwdXQgc2hvdWxkIGJlIHRoZSBzYW1lXG4gIGF3YWl0IGZpeHR1cmUuc2hlbGwoWydkaWZmJywgJ2Nkay5vdXQnLCBhc21PdXRwdXREaXJdKTtcblxuICAvLyBDaGVjayB0aGF0IHdlIGNhbiAnbHMnIHRoZSBzeW50aGVzaXplZCBhc20uXG4gIC8vIENoYW5nZSB0byBzb21lIHJhbmRvbSBkaXJlY3RvcnkgdG8gbWFrZSBzdXJlIHdlJ3JlIG5vdCBhY2NpZGVudGFsbHkgbG9hZGluZyBjZGsuanNvblxuICBjb25zdCBsaXN0ID0gYXdhaXQgZml4dHVyZS5jZGsoWyctLWFwcCcsIGFzbU91dHB1dERpciwgJ2xzJ10sIHsgY3dkOiBvcy50bXBkaXIoKSB9KTtcbiAgLy8gU2FtZSBzdGFja3Mgd2Uga25vdyBhcmUgaW4gdGhlIGFwcFxuICBleHBlY3QobGlzdCkudG9Db250YWluKGAke2ZpeHR1cmUuc3RhY2tOYW1lUHJlZml4fS1sYW1iZGFgKTtcbiAgZXhwZWN0KGxpc3QpLnRvQ29udGFpbihgJHtmaXh0dXJlLnN0YWNrTmFtZVByZWZpeH0tdGVzdC0xYCk7XG4gIGV4cGVjdChsaXN0KS50b0NvbnRhaW4oYCR7Zml4dHVyZS5zdGFja05hbWVQcmVmaXh9LXRlc3QtMmApO1xuXG4gIC8vIENoZWNrIHRoYXQgd2UgY2FuIHVzZSAnLicgYW5kIGp1c3Qgc3ludGggLHRoZSBnZW5lcmF0ZWQgYXNtXG4gIGNvbnN0IHN0YWNrVGVtcGxhdGUgPSBhd2FpdCBmaXh0dXJlLmNkayhbJy0tYXBwJywgJy4nLCAnc3ludGgnLCBmaXh0dXJlLmZ1bGxTdGFja05hbWUoJ3Rlc3QtMicpXSwge1xuICAgIGN3ZDogYXNtT3V0cHV0RGlyLFxuICB9KTtcbiAgZXhwZWN0KHN0YWNrVGVtcGxhdGUpLnRvQ29udGFpbigndG9waWMxNTJEODRBMzcnKTtcblxuICAvLyBEZXBsb3kgYSBMYW1iZGEgZnJvbSB0aGUgY29waWVkIGFzbVxuICBhd2FpdCBmaXh0dXJlLmNka0RlcGxveSgnbGFtYmRhJywgeyBvcHRpb25zOiBbJy1hJywgJy4nXSwgY3dkOiBhc21PdXRwdXREaXIgfSk7XG5cbiAgLy8gUmVtb3ZlIChyZW5hbWUpIHRoZSBvcmlnaW5hbCBjdXN0b20gZG9ja2VyIGZpbGUgdGhhdCB3YXMgdXNlZCBkdXJpbmcgc3ludGguXG4gIC8vIHRoaXMgdmVyaWZpZXMgdGhhdCB0aGUgYXNzZW1seSBoYXMgYSBjb3B5IG9mIGl0IGFuZCB0aGF0IHRoZSBtYW5pZmVzdCB1c2VzXG4gIC8vIHJlbGF0aXZlIHBhdGhzIHRvIHJlZmVyZW5jZSB0byBpdC5cbiAgY29uc3QgY3VzdG9tRG9ja2VyRmlsZSA9IHBhdGguam9pbihmaXh0dXJlLmludGVnVGVzdERpciwgJ2RvY2tlcicsICdEb2NrZXJmaWxlLkN1c3RvbScpO1xuICBhd2FpdCBmcy5yZW5hbWUoY3VzdG9tRG9ja2VyRmlsZSwgYCR7Y3VzdG9tRG9ja2VyRmlsZX1+YCk7XG4gIHRyeSB7XG5cbiAgICAvLyBkZXBsb3kgYSBkb2NrZXIgaW1hZ2Ugd2l0aCBjdXN0b20gZmlsZSB3aXRob3V0IHN5bnRoICh1c2VzIGFzc2V0cylcbiAgICBhd2FpdCBmaXh0dXJlLmNka0RlcGxveSgnZG9ja2VyLXdpdGgtY3VzdG9tLWZpbGUnLCB7IG9wdGlvbnM6IFsnLWEnLCAnLiddLCBjd2Q6IGFzbU91dHB1dERpciB9KTtcblxuICB9IGZpbmFsbHkge1xuICAgIC8vIFJlbmFtZSBiYWNrIHRvIHJlc3RvcmUgZml4dHVyZSB0byBvcmlnaW5hbCBzdGF0ZVxuICAgIGF3YWl0IGZzLnJlbmFtZShgJHtjdXN0b21Eb2NrZXJGaWxlfX5gLCBjdXN0b21Eb2NrZXJGaWxlKTtcbiAgfVxufSkpO1xuXG5pbnRlZ1Rlc3QoJ3RlbXBsYXRlcyBvbiBkaXNrIGNvbnRhaW4gbWV0YWRhdGEgcmVzb3VyY2UsIGFsc28gaW4gbmVzdGVkIGFzc2VtYmxpZXMnLCB3aXRoRGVmYXVsdEZpeHR1cmUoYXN5bmMgKGZpeHR1cmUpID0+IHtcbiAgLy8gU3ludGggZmlyc3QsIGFuZCBzd2l0Y2ggb24gdmVyc2lvbiByZXBvcnRpbmcgYmVjYXVzZSBjZGsuanNvbiBpcyBkaXNhYmxpbmcgaXRcbiAgYXdhaXQgZml4dHVyZS5jZGsoWydzeW50aCcsICctLXZlcnNpb24tcmVwb3J0aW5nPXRydWUnXSk7XG5cbiAgLy8gTG9hZCB0ZW1wbGF0ZSBmcm9tIGRpc2sgZnJvbSByb290IGFzc2VtYmx5XG4gIGNvbnN0IHRlbXBsYXRlQ29udGVudHMgPSBhd2FpdCBmaXh0dXJlLnNoZWxsKFsnY2F0JywgJ2Nkay5vdXQvKi1sYW1iZGEudGVtcGxhdGUuanNvbiddKTtcblxuICBleHBlY3QoSlNPTi5wYXJzZSh0ZW1wbGF0ZUNvbnRlbnRzKS5SZXNvdXJjZXMuQ0RLTWV0YWRhdGEpLnRvQmVUcnV0aHkoKTtcblxuICAvLyBMb2FkIHRlbXBsYXRlIGZyb20gbmVzdGVkIGFzc2VtYmx5XG4gIGNvbnN0IG5lc3RlZFRlbXBsYXRlQ29udGVudHMgPSBhd2FpdCBmaXh0dXJlLnNoZWxsKFsnY2F0JywgJ2Nkay5vdXQvYXNzZW1ibHktKi1zdGFnZS8qLXN0YWdlLVN0YWNrSW5TdGFnZS50ZW1wbGF0ZS5qc29uJ10pO1xuXG4gIGV4cGVjdChKU09OLnBhcnNlKG5lc3RlZFRlbXBsYXRlQ29udGVudHMpLlJlc291cmNlcy5DREtNZXRhZGF0YSkudG9CZVRydXRoeSgpO1xufSkpO1xuXG5hc3luYyBmdW5jdGlvbiBsaXN0Q2hpbGRyZW4ocGFyZW50OiBzdHJpbmcsIHByZWQ6ICh4OiBzdHJpbmcpID0+IFByb21pc2U8Ym9vbGVhbj4pIHtcbiAgY29uc3QgcmV0ID0gbmV3IEFycmF5PHN0cmluZz4oKTtcbiAgZm9yIChjb25zdCBjaGlsZCBvZiBhd2FpdCBmcy5yZWFkZGlyKHBhcmVudCwgeyBlbmNvZGluZzogJ3V0Zi04JyB9KSkge1xuICAgIGNvbnN0IGZ1bGxQYXRoID0gcGF0aC5qb2luKHBhcmVudCwgY2hpbGQudG9TdHJpbmcoKSk7XG4gICAgaWYgKGF3YWl0IHByZWQoZnVsbFBhdGgpKSB7XG4gICAgICByZXQucHVzaChmdWxsUGF0aCk7XG4gICAgfVxuICB9XG4gIHJldHVybiByZXQ7XG59XG5cbmFzeW5jIGZ1bmN0aW9uIGxpc3RDaGlsZERpcnMocGFyZW50OiBzdHJpbmcpIHtcbiAgcmV0dXJuIGxpc3RDaGlsZHJlbihwYXJlbnQsIGFzeW5jIChmdWxsUGF0aDogc3RyaW5nKSA9PiAoYXdhaXQgZnMuc3RhdChmdWxsUGF0aCkpLmlzRGlyZWN0b3J5KCkpO1xufVxuXG5hc3luYyBmdW5jdGlvbiByZWFkVGVtcGxhdGUoZml4dHVyZTogVGVzdEZpeHR1cmUsIHN0YWNrTmFtZTogc3RyaW5nKTogUHJvbWlzZTxhbnk+IHtcbiAgY29uc3QgZnVsbFN0YWNrTmFtZSA9IGZpeHR1cmUuZnVsbFN0YWNrTmFtZShzdGFja05hbWUpO1xuICBjb25zdCB0ZW1wbGF0ZVBhdGggPSBwYXRoLmpvaW4oZml4dHVyZS5pbnRlZ1Rlc3REaXIsICdjZGsub3V0JywgYCR7ZnVsbFN0YWNrTmFtZX0udGVtcGxhdGUuanNvbmApO1xuICByZXR1cm4gSlNPTi5wYXJzZSgoYXdhaXQgZnMucmVhZEZpbGUodGVtcGxhdGVQYXRoLCB7IGVuY29kaW5nOiAndXRmLTgnIH0pKS50b1N0cmluZygpKTtcbn1cbiJdfQ== \ No newline at end of file From ae34d4a69a5073d8f0175b5282fa8bf92139fab5 Mon Sep 17 00:00:00 2001 From: Ken Dirschl Date: Wed, 25 Aug 2021 16:46:09 -0400 Subject: [PATCH 23/39] feat(codecommit): make Repository a source for CodeStar Notifications (#15739) ---- Fixes #15653 Notification rule can be created for CodeCommit Repository events using @aws-cdk/aws-codestarnotifications *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-codecommit/README.md | 13 + .../@aws-cdk/aws-codecommit/lib/repository.ts | 266 +++++++++++++++++- packages/@aws-cdk/aws-codecommit/package.json | 2 + ...nteg.repository-notification.expected.json | 87 ++++++ .../test/integ.repository-notification.ts | 19 ++ .../test/test.notification-rule.ts | 66 +++++ .../lib/notification-rule.ts | 2 +- .../aws-codestarnotifications/test/helpers.ts | 11 + .../test/notification-rule.test.ts | 21 ++ 9 files changed, 476 insertions(+), 11 deletions(-) create mode 100644 packages/@aws-cdk/aws-codecommit/test/integ.repository-notification.expected.json create mode 100644 packages/@aws-cdk/aws-codecommit/test/integ.repository-notification.ts create mode 100644 packages/@aws-cdk/aws-codecommit/test/test.notification-rule.ts diff --git a/packages/@aws-cdk/aws-codecommit/README.md b/packages/@aws-cdk/aws-codecommit/README.md index 3d4ef1c3ba46c..7dd08eef3bb82 100644 --- a/packages/@aws-cdk/aws-codecommit/README.md +++ b/packages/@aws-cdk/aws-codecommit/README.md @@ -55,3 +55,16 @@ const rule = repo.onCommentOnPullRequest('CommentOnPullRequest', { target: new targets.SnsTopic(myTopic), }); ``` + +## CodeStar Notifications + +To define CodeStar Notification rules for Repositories, use one of the `notifyOnXxx()` methods. +They are very similar to `onXxx()` methods for CloudWatch events: + +```ts +const target = new chatbot.SlackChannelConfiguration(stack, 'MySlackChannel', { + slackChannelConfigurationName: 'YOUR_CHANNEL_NAME', + slackWorkspaceId: 'YOUR_SLACK_WORKSPACE_ID', + slackChannelId: 'YOUR_SLACK_CHANNEL_ID', +}); +const rule = repository.notifyOnPullRequestCreated('NotifyOnPullRequestCreated', target); diff --git a/packages/@aws-cdk/aws-codecommit/lib/repository.ts b/packages/@aws-cdk/aws-codecommit/lib/repository.ts index 495be2f981d86..e55053c0249be 100644 --- a/packages/@aws-cdk/aws-codecommit/lib/repository.ts +++ b/packages/@aws-cdk/aws-codecommit/lib/repository.ts @@ -1,10 +1,23 @@ +import * as notifications from '@aws-cdk/aws-codestarnotifications'; import * as events from '@aws-cdk/aws-events'; import * as iam from '@aws-cdk/aws-iam'; import { IResource, Lazy, Resource, Stack } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { CfnRepository } from './codecommit.generated'; -export interface IRepository extends IResource { +/** + * Additional options to pass to the notification rule. + */ +export interface RepositoryNotifyOnOptions extends notifications.NotificationRuleOptions { + /** + * A list of event types associated with this notification rule for CodeCommit repositories. + * For a complete list of event types and IDs, see Notification concepts in the Developer Tools Console User Guide. + * @see https://docs.aws.amazon.com/dtconsole/latest/userguide/concepts.html#concepts-api + */ + readonly events: RepositoryNotificationEvents[]; +} + +export interface IRepository extends IResource, notifications.INotificationRuleSource { /** * The ARN of this Repository. * @attribute @@ -18,19 +31,19 @@ export interface IRepository extends IResource { readonly repositoryName: string; /** - * The HTTP clone URL + * The HTTP clone URL. * @attribute */ readonly repositoryCloneUrlHttp: string; /** - * The SSH clone URL + * The SSH clone URL. * @attribute */ readonly repositoryCloneUrlSsh: string; /** - * The HTTPS (GRC) clone URL + * The HTTPS (GRC) clone URL. * * HTTPS (GRC) is the protocol to use with git-remote-codecommit (GRC). * @@ -92,7 +105,7 @@ export interface IRepository extends IResource { onCommit(id: string, options?: OnCommitOptions): events.Rule; /** - * Grant the given principal identity permissions to perform the actions on this repository + * Grant the given principal identity permissions to perform the actions on this repository. */ grant(grantee: iam.IGrantable, ...actions: string[]): iam.Grant; @@ -110,13 +123,90 @@ export interface IRepository extends IResource { * Grant the given identity permissions to read this repository. */ grantRead(grantee: iam.IGrantable): iam.Grant; + + /** + * Defines a CodeStar Notification rule triggered when the project + * events specified by you are emitted. Similar to `onEvent` API. + * + * You can also use the methods to define rules for the specific event emitted. + * eg: `notifyOnPullRequstCreated`. + * + * @returns CodeStar Notifications rule associated with this repository. + */ + notifyOn( + id: string, + target: notifications.INotificationRuleTarget, + options: RepositoryNotifyOnOptions, + ): notifications.INotificationRule; + + /** + * Defines a CodeStar Notification rule which triggers when a comment is made on a pull request. + */ + notifyOnPullRequestComment( + id: string, + target: notifications.INotificationRuleTarget, + options?: notifications.NotificationRuleOptions, + ): notifications.INotificationRule; + + /** + * Defines a CodeStar Notification rule which triggers when an approval status is changed. + */ + notifyOnApprovalStatusChanged( + id: string, + target: notifications.INotificationRuleTarget, + options?: notifications.NotificationRuleOptions, + ): notifications.INotificationRule; + + /** + * Defines a CodeStar Notification rule which triggers when an approval rule is overridden. + */ + notifyOnApprovalRuleOverridden( + id: string, + target: notifications.INotificationRuleTarget, + options?: notifications.NotificationRuleOptions, + ): notifications.INotificationRule; + + /** + * Defines a CodeStar Notification rule which triggers when a pull request is created. + */ + notifyOnPullRequestCreated( + id: string, + target: notifications.INotificationRuleTarget, + options?: notifications.NotificationRuleOptions, + ): notifications.INotificationRule; + + /** + * Defines a CodeStar Notification rule which triggers when a pull request is merged. + */ + notifiyOnPullRequestMerged( + id: string, + target: notifications.INotificationRuleTarget, + options?: notifications.NotificationRuleOptions, + ): notifications.INotificationRule; + + /** + * Defines a CodeStar Notification rule which triggers when a new branch or tag is created. + */ + notifyOnBranchOrTagCreated( + id: string, + target: notifications.INotificationRuleTarget, + options?: notifications.NotificationRuleOptions, + ): notifications.INotificationRule; + + /** + * Defines a CodeStar Notification rule which triggers when a branch or tag is deleted. + */ + notifyOnBranchOrTagDeleted( + id: string, + target: notifications.INotificationRuleTarget, + options?: notifications.NotificationRuleOptions, + ): notifications.INotificationRule; } /** - * Options for the onCommit() method + * Options for the onCommit() method. */ export interface OnCommitOptions extends events.OnEventOptions { - /** * The branch to monitor. * @@ -268,6 +358,101 @@ abstract class RepositoryBase extends Resource implements IRepository { 'codecommit:Describe*', ); } + + public notifyOn( + id: string, + target: notifications.INotificationRuleTarget, + options: RepositoryNotifyOnOptions, + ): notifications.INotificationRule { + return new notifications.NotificationRule(this, id, { + ...options, + source: this, + targets: [target], + }); + } + + public notifyOnPullRequestComment( + id: string, + target: notifications.INotificationRuleTarget, + options?: notifications.NotificationRuleOptions, + ): notifications.INotificationRule { + return this.notifyOn(id, target, { + ...options, + events: [RepositoryNotificationEvents.PULL_REQUEST_COMMENT], + }); + } + + public notifyOnApprovalStatusChanged( + id: string, + target: notifications.INotificationRuleTarget, + options?: notifications.NotificationRuleOptions, + ): notifications.INotificationRule { + return this.notifyOn(id, target, { + ...options, + events: [RepositoryNotificationEvents.APPROVAL_STATUS_CHANGED], + }); + } + + public notifyOnApprovalRuleOverridden( + id: string, + target: notifications.INotificationRuleTarget, + options?: notifications.NotificationRuleOptions, + ): notifications.INotificationRule { + return this.notifyOn(id, target, { + ...options, + events: [RepositoryNotificationEvents.APPROVAL_RULE_OVERRIDDEN], + }); + } + + public notifyOnPullRequestCreated( + id: string, + target: notifications.INotificationRuleTarget, + options?: notifications.NotificationRuleOptions, + ): notifications.INotificationRule { + return this.notifyOn(id, target, { + ...options, + events: [RepositoryNotificationEvents.PULL_REQUEST_CREATED], + }); + } + + public notifiyOnPullRequestMerged( + id: string, + target: notifications.INotificationRuleTarget, + options?: notifications.NotificationRuleOptions, + ): notifications.INotificationRule { + return this.notifyOn(id, target, { + ...options, + events: [RepositoryNotificationEvents.PULL_REQUEST_MERGED], + }); + } + + public notifyOnBranchOrTagCreated( + id: string, + target: notifications.INotificationRuleTarget, + options?: notifications.NotificationRuleOptions, + ): notifications.INotificationRule { + return this.notifyOn(id, target, { + ...options, + events: [RepositoryNotificationEvents.BRANCH_OR_TAG_CREATED], + }); + } + + public notifyOnBranchOrTagDeleted( + id: string, + target: notifications.INotificationRuleTarget, + options?: notifications.NotificationRuleOptions, + ): notifications.INotificationRule { + return this.notifyOn(id, target, { + ...options, + events: [RepositoryNotificationEvents.BRANCH_OR_TAG_DELETED], + }); + } + + public bindAsNotificationRuleSource(_scope: Construct): notifications.NotificationRuleSourceConfig { + return { + sourceArn: this.repositoryArn, + }; + } } export interface RepositoryProps { @@ -288,7 +473,7 @@ export interface RepositoryProps { } /** - * Provides a CodeCommit Repository + * Provides a CodeCommit Repository. */ export class Repository extends RepositoryBase { @@ -401,7 +586,7 @@ export class Repository extends RepositoryBase { */ export interface RepositoryTriggerOptions { /** - * A name for the trigger.Triggers on a repository must have unique names + * A name for the trigger.Triggers on a repository must have unique names. */ readonly name?: string; @@ -437,7 +622,7 @@ export enum RepositoryEventTrigger { } /** - * Returns the clone URL for a protocol + * Returns the clone URL for a protocol. */ function makeCloneUrl(stack: Stack, repositoryName: string, protocol: 'https' | 'ssh' | 'grc', region?: string) { switch (protocol) { @@ -448,3 +633,64 @@ function makeCloneUrl(stack: Stack, repositoryName: string, protocol: 'https' | return `codecommit::${region ?? stack.region}://${repositoryName}`; } } + +/** + * List of event types for AWS CodeCommit + * @see https://docs.aws.amazon.com/dtconsole/latest/userguide/concepts.html#events-ref-repositories + */ +export enum RepositoryNotificationEvents { + /** + * Trigger notication when comment made on commit. + */ + COMMIT_COMMENT = 'codecommit-repository-comments-on-commits', + + /** + * Trigger notification when comment made on pull request. + */ + PULL_REQUEST_COMMENT = 'codecommit-repository-comments-on-pull-requests', + + /** + * Trigger notification when approval status changed. + */ + APPROVAL_STATUS_CHANGED = 'codecommit-repository-approvals-status-changed', + + /** + * Trigger notifications when approval rule is overridden. + */ + APPROVAL_RULE_OVERRIDDEN = 'codecommit-repository-approvals-rule-override', + + /** + * Trigger notification when pull request created. + */ + PULL_REQUEST_CREATED = 'codecommit-repository-pull-request-created', + + /** + * Trigger notification when pull request source updated. + */ + PULL_REQUEST_SOURCE_UPDATED = 'codecommit-repository-pull-request-source-updated', + + /** + * Trigger notification when pull request status is changed. + */ + PULL_REQUEST_STATUS_CHANGED = 'codecommit-repository-pull-request-status-changed', + + /** + * Trigger notification when pull requset is merged. + */ + PULL_REQUEST_MERGED = 'codecommit-repository-pull-request-merged', + + /** + * Trigger notification when a branch or tag is created. + */ + BRANCH_OR_TAG_CREATED = 'codecommit-repository-branches-and-tags-created', + + /** + * Trigger notification when a branch or tag is deleted. + */ + BRANCH_OR_TAG_DELETED = 'codecommit-repository-branches-and-tags-deleted', + + /** + * Trigger notification when a branch or tag is updated. + */ + BRANCH_OR_TAG_UPDATED = 'codecommit-repository-branches-and-tags-updated', +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-codecommit/package.json b/packages/@aws-cdk/aws-codecommit/package.json index 6d622f85d076a..325c75e846904 100644 --- a/packages/@aws-cdk/aws-codecommit/package.json +++ b/packages/@aws-cdk/aws-codecommit/package.json @@ -88,6 +88,7 @@ "@aws-cdk/assert-internal": "0.0.0" }, "dependencies": { + "@aws-cdk/aws-codestarnotifications": "0.0.0", "@aws-cdk/aws-events": "0.0.0", "@aws-cdk/aws-iam": "0.0.0", "@aws-cdk/core": "0.0.0", @@ -95,6 +96,7 @@ }, "homepage": "https://github.com/aws/aws-cdk", "peerDependencies": { + "@aws-cdk/aws-codestarnotifications": "0.0.0", "@aws-cdk/aws-events": "0.0.0", "@aws-cdk/aws-iam": "0.0.0", "@aws-cdk/core": "0.0.0", diff --git a/packages/@aws-cdk/aws-codecommit/test/integ.repository-notification.expected.json b/packages/@aws-cdk/aws-codecommit/test/integ.repository-notification.expected.json new file mode 100644 index 0000000000000..c2f386ccebe0e --- /dev/null +++ b/packages/@aws-cdk/aws-codecommit/test/integ.repository-notification.expected.json @@ -0,0 +1,87 @@ +{ + "Resources": { + "MyCodecommitRepository26DB372B": { + "Type": "AWS::CodeCommit::Repository", + "Properties": { + "RepositoryName": "my-test-repository" + } + }, + "MyCodecommitRepositoryNotifyOnPullRequestCreated4CAB0621": { + "Type": "AWS::CodeStarNotifications::NotificationRule", + "Properties": { + "DetailType": "FULL", + "EventTypeIds": [ + "codecommit-repository-pull-request-created" + ], + "Name": "decommitMyCodecommitRepositoryNotifyOnPullRequestCreated65969BCB", + "Resource": { + "Fn::GetAtt": [ + "MyCodecommitRepository26DB372B", + "Arn" + ] + }, + "Targets": [ + { + "TargetAddress": { + "Ref": "MyTopic86869434" + }, + "TargetType": "SNS" + } + ] + } + }, + "MyCodecommitRepositoryNotifyOnPullRequestMerged80574FED": { + "Type": "AWS::CodeStarNotifications::NotificationRule", + "Properties": { + "DetailType": "FULL", + "EventTypeIds": [ + "codecommit-repository-pull-request-merged" + ], + "Name": "odecommitMyCodecommitRepositoryNotifyOnPullRequestMergedF426197C", + "Resource": { + "Fn::GetAtt": [ + "MyCodecommitRepository26DB372B", + "Arn" + ] + }, + "Targets": [ + { + "TargetAddress": { + "Ref": "MyTopic86869434" + }, + "TargetType": "SNS" + } + ] + } + }, + "MyTopic86869434": { + "Type": "AWS::SNS::Topic" + }, + "MyTopicPolicy12A5EC17": { + "Type": "AWS::SNS::TopicPolicy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "sns:Publish", + "Effect": "Allow", + "Principal": { + "Service": "codestar-notifications.amazonaws.com" + }, + "Resource": { + "Ref": "MyTopic86869434" + }, + "Sid": "0" + } + ], + "Version": "2012-10-17" + }, + "Topics": [ + { + "Ref": "MyTopic86869434" + } + ] + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-codecommit/test/integ.repository-notification.ts b/packages/@aws-cdk/aws-codecommit/test/integ.repository-notification.ts new file mode 100644 index 0000000000000..4f7a5926a47ac --- /dev/null +++ b/packages/@aws-cdk/aws-codecommit/test/integ.repository-notification.ts @@ -0,0 +1,19 @@ +#!/usr/bin/env node +import * as sns from '@aws-cdk/aws-sns'; +import * as cdk from '@aws-cdk/core'; +import * as codecommit from '../lib'; + +const app = new cdk.App(); + +const stack = new cdk.Stack(app, 'aws-cdk-codecommit'); + +const repository = new codecommit.Repository(stack, 'MyCodecommitRepository', { + repositoryName: 'my-test-repository', +}); + +const target = new sns.Topic(stack, 'MyTopic'); + +repository.notifyOnPullRequestCreated('NotifyOnPullRequestCreated', target); +repository.notifiyOnPullRequestMerged('NotifyOnPullRequestMerged', target); + +app.synth(); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-codecommit/test/test.notification-rule.ts b/packages/@aws-cdk/aws-codecommit/test/test.notification-rule.ts new file mode 100644 index 0000000000000..aa0e48a51a3ac --- /dev/null +++ b/packages/@aws-cdk/aws-codecommit/test/test.notification-rule.ts @@ -0,0 +1,66 @@ +import { expect, haveResource } from '@aws-cdk/assert-internal'; +import * as sns from '@aws-cdk/aws-sns'; +import * as cdk from '@aws-cdk/core'; +import { Test } from 'nodeunit'; +import * as codecommit from '../lib'; + +export = { + 'CodeCommit Repositories - can create notification rule'(test: Test) { + const stack = new cdk.Stack(); + const repository = new codecommit.Repository(stack, 'MyCodecommitRepository', { + repositoryName: 'my-test-repository', + }); + + const target = new sns.Topic(stack, 'MyTopic'); + + repository.notifyOnPullRequestCreated('NotifyOnPullRequestCreated', target); + + repository.notifiyOnPullRequestMerged('NotifyOnPullRequestMerged', target); + + expect(stack).to(haveResource('AWS::CodeStarNotifications::NotificationRule', { + Name: 'MyCodecommitRepositoryNotifyOnPullRequestCreatedBB14EA32', + DetailType: 'FULL', + EventTypeIds: [ + 'codecommit-repository-pull-request-created', + ], + Resource: { + 'Fn::GetAtt': [ + 'MyCodecommitRepository26DB372B', + 'Arn', + ], + }, + Targets: [ + { + TargetAddress: { + Ref: 'MyTopic86869434', + }, + TargetType: 'SNS', + }, + ], + })); + + expect(stack).to(haveResource('AWS::CodeStarNotifications::NotificationRule', { + Name: 'MyCodecommitRepositoryNotifyOnPullRequestMerged34A7EDF1', + DetailType: 'FULL', + EventTypeIds: [ + 'codecommit-repository-pull-request-merged', + ], + Resource: { + 'Fn::GetAtt': [ + 'MyCodecommitRepository26DB372B', + 'Arn', + ], + }, + Targets: [ + { + TargetAddress: { + Ref: 'MyTopic86869434', + }, + TargetType: 'SNS', + }, + ], + })); + + test.done(); + }, +}; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-codestarnotifications/lib/notification-rule.ts b/packages/@aws-cdk/aws-codestarnotifications/lib/notification-rule.ts index 0796557e38a77..f30cf11f58a99 100644 --- a/packages/@aws-cdk/aws-codestarnotifications/lib/notification-rule.ts +++ b/packages/@aws-cdk/aws-codestarnotifications/lib/notification-rule.ts @@ -62,7 +62,7 @@ export interface NotificationRuleProps extends NotificationRuleOptions { /** * The Amazon Resource Name (ARN) of the resource to associate with the notification rule. - * Currently, Supported sources include pipelines in AWS CodePipeline and build projects in AWS CodeBuild in this L2 constructor. + * Currently, Supported sources include pipelines in AWS CodePipeline, build projects in AWS CodeBuild, and repositories in AWS CodeCommit in this L2 constructor. * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-codestarnotifications-notificationrule.html#cfn-codestarnotifications-notificationrule-resource */ readonly source: INotificationRuleSource; diff --git a/packages/@aws-cdk/aws-codestarnotifications/test/helpers.ts b/packages/@aws-cdk/aws-codestarnotifications/test/helpers.ts index bf1d335c94d9f..3acf5e33ca334 100644 --- a/packages/@aws-cdk/aws-codestarnotifications/test/helpers.ts +++ b/packages/@aws-cdk/aws-codestarnotifications/test/helpers.ts @@ -22,6 +22,17 @@ export class FakeCodePipeline implements notifications.INotificationRuleSource { } } +export class FakeCodeCommit implements notifications.INotificationRuleSource { + readonly repositoryArn = 'arn:aws:codecommit::1234567890:MyCodecommitProject'; + readonly repositoryName = 'test-repository'; + + bindAsNotificationRuleSource(): notifications.NotificationRuleSourceConfig { + return { + sourceArn: this.repositoryArn, + }; + } +} + export class FakeSnsTopicTarget implements notifications.INotificationRuleTarget { readonly topicArn = 'arn:aws:sns::1234567890:MyTopic'; diff --git a/packages/@aws-cdk/aws-codestarnotifications/test/notification-rule.test.ts b/packages/@aws-cdk/aws-codestarnotifications/test/notification-rule.test.ts index a155b3583a524..332e808ca3176 100644 --- a/packages/@aws-cdk/aws-codestarnotifications/test/notification-rule.test.ts +++ b/packages/@aws-cdk/aws-codestarnotifications/test/notification-rule.test.ts @@ -4,6 +4,7 @@ import * as notifications from '../lib'; import { FakeCodeBuild, FakeCodePipeline, + FakeCodeCommit, FakeSlackTarget, FakeSnsTopicTarget, } from './helpers'; @@ -29,6 +30,26 @@ describe('NotificationRule', () => { }); }); + test('created new notification rule from repository source', () => { + const repository = new FakeCodeCommit(); + + new notifications.NotificationRule(stack, 'MyNotificationRule', { + source: repository, + events: [ + 'codecommit-repository-pull-request-created', + 'codecommit-repository-pull-request-merged', + ], + }); + + expect(stack).toHaveResourceLike('AWS::CodeStarNotifications::NotificationRule', { + Resource: repository.repositoryArn, + EventTypeIds: [ + 'codecommit-repository-pull-request-created', + 'codecommit-repository-pull-request-merged', + ], + }); + }); + test('created new notification rule with all parameters in constructor props', () => { const project = new FakeCodeBuild(); const slack = new FakeSlackTarget(); From 1d54a456cd5e2ff65251097f9a684e1ac200cc52 Mon Sep 17 00:00:00 2001 From: kaizen3031593 <36202692+kaizen3031593@users.noreply.github.com> Date: Wed, 25 Aug 2021 17:33:23 -0400 Subject: [PATCH 24/39] feat(rds): support 's3export' for Postgres database instances (#16124) As mentioned in #14546, Postgres database instances did not support `s3export` when most of the `s3 import/export` features were added in #10370. This PR fills in that gap now that `s3export` is [available](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/postgresql-s3-export.html) for Postgres. The supported versions are documented [here](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/postgresql-s3-export.html) and I manually verified via AWS CLI that Postgres 13+ supports `s3export` as well. Looking into the test suite, I think it makes more sense to modify the existing test than to create an entirely new one, similar to how other database instances test s3 import/export at the same time. But I can also move it to its own test if necessary. I also fixed a very minor doc typo in `cluster-instance.ts`. Closes #14546. ---- *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 | 7 ++-- .../@aws-cdk/aws-rds/lib/cluster-engine.ts | 2 +- .../@aws-cdk/aws-rds/lib/instance-engine.ts | 40 +++++++++++-------- .../aws-rds/test/instance-engine.test.ts | 6 +-- 4 files changed, 32 insertions(+), 23 deletions(-) diff --git a/packages/@aws-cdk/aws-rds/README.md b/packages/@aws-cdk/aws-rds/README.md index c604a2ad5bce8..054a660f3529e 100644 --- a/packages/@aws-cdk/aws-rds/README.md +++ b/packages/@aws-cdk/aws-rds/README.md @@ -359,11 +359,12 @@ You can read more about loading data to (or from) S3 here: * Aurora MySQL - [import](https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/AuroraMySQL.Integrating.LoadFromS3.html) and [export](https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/AuroraMySQL.Integrating.SaveIntoS3.html). -* Aurora PostgreSQL - [import](https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/AuroraPostgreSQL.Migrating.html) +* Aurora PostgreSQL - [import](https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/AuroraPostgreSQL.Migrating.html#USER_PostgreSQL.S3Import) and [export](https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/postgresql-s3-export.html). -* Microsoft SQL Server - [import & export](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/SQLServer.Procedural.Importing.html) +* Microsoft SQL Server - [import and export](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/SQLServer.Procedural.Importing.html) * PostgreSQL - [import](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/PostgreSQL.Procedural.Importing.html) -* Oracle - [import & export](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/oracle-s3-integration.html) + and [export](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/postgresql-s3-export.html) +* Oracle - [import and export](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/oracle-s3-integration.html) The following snippet sets up a database cluster with different S3 buckets where the data is imported and exported - diff --git a/packages/@aws-cdk/aws-rds/lib/cluster-engine.ts b/packages/@aws-cdk/aws-rds/lib/cluster-engine.ts index f92e738b2f90b..973f682e932c7 100644 --- a/packages/@aws-cdk/aws-rds/lib/cluster-engine.ts +++ b/packages/@aws-cdk/aws-rds/lib/cluster-engine.ts @@ -403,7 +403,7 @@ export interface AuroraPostgresEngineFeatures { readonly s3Import?: boolean; /** - * Whether this version of the Aurora Postgres cluster engine supports the S3 data import feature. + * Whether this version of the Aurora Postgres cluster engine supports the S3 data export feature. * * @default false */ diff --git a/packages/@aws-cdk/aws-rds/lib/instance-engine.ts b/packages/@aws-cdk/aws-rds/lib/instance-engine.ts index 7b1c8fad83eaf..eacb8097da55e 100644 --- a/packages/@aws-cdk/aws-rds/lib/instance-engine.ts +++ b/packages/@aws-cdk/aws-rds/lib/instance-engine.ts @@ -545,6 +545,13 @@ export interface PostgresEngineFeatures { * @default false */ readonly s3Import?: boolean; + + /** + * Whether this version of the Postgres engine supports the S3 data export feature. + * + * @default false + */ + readonly s3Export?: boolean; } /** @@ -779,13 +786,13 @@ export class PostgresEngineVersion { /** Version "10.13". */ public static readonly VER_10_13 = PostgresEngineVersion.of('10.13', '10', { s3Import: true }); /** Version "10.14". */ - public static readonly VER_10_14 = PostgresEngineVersion.of('10.14', '10', { s3Import: true }); + public static readonly VER_10_14 = PostgresEngineVersion.of('10.14', '10', { s3Import: true, s3Export: true }); /** Version "10.15". */ - public static readonly VER_10_15 = PostgresEngineVersion.of('10.15', '10', { s3Import: true }); + public static readonly VER_10_15 = PostgresEngineVersion.of('10.15', '10', { s3Import: true, s3Export: true }); /** Version "10.16". */ - public static readonly VER_10_16 = PostgresEngineVersion.of('10.16', '10', { s3Import: true }); + public static readonly VER_10_16 = PostgresEngineVersion.of('10.16', '10', { s3Import: true, s3Export: true }); /** Version "10.17". */ - public static readonly VER_10_17 = PostgresEngineVersion.of('10.17', '10', { s3Import: true }); + public static readonly VER_10_17 = PostgresEngineVersion.of('10.17', '10', { s3Import: true, s3Export: true }); /** Version "11" (only a major version, without a specific minor version). */ public static readonly VER_11 = PostgresEngineVersion.of('11', '11', { s3Import: true }); @@ -804,13 +811,13 @@ export class PostgresEngineVersion { /** Version "11.8". */ public static readonly VER_11_8 = PostgresEngineVersion.of('11.8', '11', { s3Import: true }); /** Version "11.9". */ - public static readonly VER_11_9 = PostgresEngineVersion.of('11.9', '11', { s3Import: true }); + public static readonly VER_11_9 = PostgresEngineVersion.of('11.9', '11', { s3Import: true, s3Export: true }); /** Version "11.10". */ - public static readonly VER_11_10 = PostgresEngineVersion.of('11.10', '11', { s3Import: true }); + public static readonly VER_11_10 = PostgresEngineVersion.of('11.10', '11', { s3Import: true, s3Export: true }); /** Version "11.11". */ - public static readonly VER_11_11 = PostgresEngineVersion.of('11.11', '11', { s3Import: true }); + public static readonly VER_11_11 = PostgresEngineVersion.of('11.11', '11', { s3Import: true, s3Export: true }); /** Version "11.12". */ - public static readonly VER_11_12 = PostgresEngineVersion.of('11.12', '11', { s3Import: true }); + public static readonly VER_11_12 = PostgresEngineVersion.of('11.12', '11', { s3Import: true, s3Export: true }); /** Version "12" (only a major version, without a specific minor version). */ public static readonly VER_12 = PostgresEngineVersion.of('12', '12', { s3Import: true }); @@ -819,22 +826,22 @@ export class PostgresEngineVersion { /** Version "12.3". */ public static readonly VER_12_3 = PostgresEngineVersion.of('12.3', '12', { s3Import: true }); /** Version "12.4". */ - public static readonly VER_12_4 = PostgresEngineVersion.of('12.4', '12', { s3Import: true }); + public static readonly VER_12_4 = PostgresEngineVersion.of('12.4', '12', { s3Import: true, s3Export: true }); /** Version "12.5". */ - public static readonly VER_12_5 = PostgresEngineVersion.of('12.5', '12', { s3Import: true }); + public static readonly VER_12_5 = PostgresEngineVersion.of('12.5', '12', { s3Import: true, s3Export: true }); /** Version "12.6". */ - public static readonly VER_12_6 = PostgresEngineVersion.of('12.6', '12', { s3Import: true }); + public static readonly VER_12_6 = PostgresEngineVersion.of('12.6', '12', { s3Import: true, s3Export: true }); /** Version "12.7". */ - public static readonly VER_12_7 = PostgresEngineVersion.of('12.7', '12', { s3Import: true }); + public static readonly VER_12_7 = PostgresEngineVersion.of('12.7', '12', { s3Import: true, s3Export: true }); /** Version "13" (only a major version, without a specific minor version). */ - public static readonly VER_13 = PostgresEngineVersion.of('13', '13', { s3Import: true }); + public static readonly VER_13 = PostgresEngineVersion.of('13', '13', { s3Import: true, s3Export: true }); /** Version "13.1". */ - public static readonly VER_13_1 = PostgresEngineVersion.of('13.1', '13', { s3Import: true }); + public static readonly VER_13_1 = PostgresEngineVersion.of('13.1', '13', { s3Import: true, s3Export: true }); /** Version "13.2". */ - public static readonly VER_13_2 = PostgresEngineVersion.of('13.2', '13', { s3Import: true }); + public static readonly VER_13_2 = PostgresEngineVersion.of('13.2', '13', { s3Import: true, s3Export: true }); /** Version "13.3". */ - public static readonly VER_13_3 = PostgresEngineVersion.of('13.3', '13', { s3Import: true }); + public static readonly VER_13_3 = PostgresEngineVersion.of('13.3', '13', { s3Import: true, s3Export: true }); /** * Create a new PostgresEngineVersion with an arbitrary version. @@ -864,6 +871,7 @@ export class PostgresEngineVersion { this.postgresMajorVersion = postgresMajorVersion; this._features = { s3Import: postgresFeatures?.s3Import ? 's3Import' : undefined, + s3Export: postgresFeatures?.s3Export ? 's3Export' : undefined, }; } } diff --git a/packages/@aws-cdk/aws-rds/test/instance-engine.test.ts b/packages/@aws-cdk/aws-rds/test/instance-engine.test.ts index 830ad0c97bd8e..e3b02c48770d4 100644 --- a/packages/@aws-cdk/aws-rds/test/instance-engine.test.ts +++ b/packages/@aws-cdk/aws-rds/test/instance-engine.test.ts @@ -274,12 +274,12 @@ nodeunitShim({ test.done(); }, - 'returns s3 import feature if the version supports it'(test: Test) { - const engineNewerVersion = rds.DatabaseInstanceEngine.postgres({ version: rds.PostgresEngineVersion.VER_12_3 }); + 'returns s3 import/export feature if the version supports it'(test: Test) { + const engineNewerVersion = rds.DatabaseInstanceEngine.postgres({ version: rds.PostgresEngineVersion.VER_13_3 }); const engineConfig = engineNewerVersion.bindToInstance(new cdk.Stack(), {}); test.equals(engineConfig.features?.s3Import, 's3Import'); - test.equals(engineConfig.features?.s3Export, undefined); + test.equals(engineConfig.features?.s3Export, 's3Export'); test.done(); }, From cd66fe4b101d7a377ceb733fe4d72d4cd6cec753 Mon Sep 17 00:00:00 2001 From: AWS CDK Team Date: Thu, 26 Aug 2021 09:31:38 +0000 Subject: [PATCH 25/39] chore(release): 1.120.0 --- CHANGELOG.md | 41 +++++++++++++++++++++++++++++++++++++++++ version.v1.json | 2 +- 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c207fc6012f7c..a17afe6a8cc59 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,47 @@ 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.120.0](https://github.com/aws/aws-cdk/compare/v1.119.0...v1.120.0) (2021-08-26) + + +### Features + +* **assertions:** queries and assertions against the Outputs and Mappings sections ([#15892](https://github.com/aws/aws-cdk/issues/15892)) ([90f95e1](https://github.com/aws/aws-cdk/commit/90f95e10f4dd9e4992731d6262dcfc767b65ab3f)) +* **aws-stepfunctions:** add support to heartbeat error inside catch block ([#16078](https://github.com/aws/aws-cdk/issues/16078)) ([2372b3c](https://github.com/aws/aws-cdk/commit/2372b3c360d13fb0224fc981a7bb1ae318581265)), closes [#16084](https://github.com/aws/aws-cdk/issues/16084) +* **cfnspec:** cloudformation spec v39.10.0 ([#16114](https://github.com/aws/aws-cdk/issues/16114)) ([7e0ad5d](https://github.com/aws/aws-cdk/commit/7e0ad5d17b30150922d0dfd81f42da11fadb8beb)) +* **cfnspec:** cloudformation spec v40.0.0 ([#16183](https://github.com/aws/aws-cdk/issues/16183)) ([b059124](https://github.com/aws/aws-cdk/commit/b059124b238e27751217cbdaaa01c38b00e80fc9)) +* **cloudwatch:** add support for cross-account alarms ([#16007](https://github.com/aws/aws-cdk/issues/16007)) ([e547ba0](https://github.com/aws/aws-cdk/commit/e547ba0d1491af0abe703132fa06fe786ffd7070)), closes [#15959](https://github.com/aws/aws-cdk/issues/15959) +* **codecommit:** make Repository a source for CodeStar Notifications ([#15739](https://github.com/aws/aws-cdk/issues/15739)) ([ae34d4a](https://github.com/aws/aws-cdk/commit/ae34d4a69a5073d8f0175b5282fa8bf92139fab5)) +* **cognito:** user pools - device tracking ([#16055](https://github.com/aws/aws-cdk/issues/16055)) ([64019bb](https://github.com/aws/aws-cdk/commit/64019bbf090e156261feb626a5a4bd7ff4f26545)), closes [#15013](https://github.com/aws/aws-cdk/issues/15013) +* **docdb:** cluster - deletion protection ([#15216](https://github.com/aws/aws-cdk/issues/15216)) ([0f7beb2](https://github.com/aws/aws-cdk/commit/0f7beb29be18d809052f4d46e415a0394c9299ab)) +* **ecs:** add support for Bottlerocket on ARM64 ([#15454](https://github.com/aws/aws-cdk/issues/15454)) ([cd280a8](https://github.com/aws/aws-cdk/commit/cd280a8f4f46eb50be3a25d80c00a807881832c4)), closes [#14466](https://github.com/aws/aws-cdk/issues/14466) +* **lambda:** nodejs14.x supports inline code ([#16131](https://github.com/aws/aws-cdk/issues/16131)) ([305f683](https://github.com/aws/aws-cdk/commit/305f683e86cca221705c0138572faa38043396eb)) +* **rds:** support 's3export' for Postgres database instances ([#16124](https://github.com/aws/aws-cdk/issues/16124)) ([1d54a45](https://github.com/aws/aws-cdk/commit/1d54a456cd5e2ff65251097f9a684e1ac200cc52)), closes [#14546](https://github.com/aws/aws-cdk/issues/14546) [#10370](https://github.com/aws/aws-cdk/issues/10370) [#14546](https://github.com/aws/aws-cdk/issues/14546) +* **s3-deployment:** exclude and include filters ([#16054](https://github.com/aws/aws-cdk/issues/16054)) ([d42e89e](https://github.com/aws/aws-cdk/commit/d42e89e01034dcba08c8f8ac0390a743143c4531)), closes [#14362](https://github.com/aws/aws-cdk/issues/14362) [#14362](https://github.com/aws/aws-cdk/issues/14362) + + +### Bug Fixes + +* **apigatewayv2:** http api - disallow empty string as domain name ([#16044](https://github.com/aws/aws-cdk/issues/16044)) ([9c39bcb](https://github.com/aws/aws-cdk/commit/9c39bcb970fc791e94d199b962cc006fca1a3320)) +* **appsync:** addSubscription only allows for field type ([#16097](https://github.com/aws/aws-cdk/issues/16097)) ([000d151](https://github.com/aws/aws-cdk/commit/000d151bec2215aa530819c3cf2c8c432352fec3)), closes [#10078](https://github.com/aws/aws-cdk/issues/10078) [#16071](https://github.com/aws/aws-cdk/issues/16071) +* **cfnspec:** changes to resource-level documentation not supported ([#16170](https://github.com/aws/aws-cdk/issues/16170)) ([82e4b4f](https://github.com/aws/aws-cdk/commit/82e4b4f07be202e2d6c6afa4f9ed0d9d6146f0a8)) +* **cli:** 'deploy' and 'diff' silently does nothing when given unknown stack name ([#16073](https://github.com/aws/aws-cdk/issues/16073)) ([f35b032](https://github.com/aws/aws-cdk/commit/f35b032cea4354992d3320e78c1ed0e2878a3fe7)), closes [#15866](https://github.com/aws/aws-cdk/issues/15866) +* **cli:** Python init template does not work in directory with '-' ([#15939](https://github.com/aws/aws-cdk/issues/15939)) ([3b2c790](https://github.com/aws/aws-cdk/commit/3b2c790c2b7d210868576540feab4e088376ab6c)), closes [#15938](https://github.com/aws/aws-cdk/issues/15938) +* **cli:** unknown command pytest in build container fails integration tests ([#16134](https://github.com/aws/aws-cdk/issues/16134)) ([0f7c0b4](https://github.com/aws/aws-cdk/commit/0f7c0b421327f1ffed28de79692191af187f23ca)), closes [#15939](https://github.com/aws/aws-cdk/issues/15939) +* **resourcegroups:** ResourceGroup not using TagType.STANDARD, causes deploy failure ([#16211](https://github.com/aws/aws-cdk/issues/16211)) ([cdee1af](https://github.com/aws/aws-cdk/commit/cdee1af03c34a1c08988e672bae6edc2538a8877)), closes [#12986](https://github.com/aws/aws-cdk/issues/12986) +* **s3:** bucket is not emptied before update when the name changes ([#16203](https://github.com/aws/aws-cdk/issues/16203)) ([b1d69d7](https://github.com/aws/aws-cdk/commit/b1d69d7b06cd2a2ae8f578e217bdf7fef50a0163)), closes [#14011](https://github.com/aws/aws-cdk/issues/14011) +* **ses:** drop spam rule appears in the incorrect order ([#16146](https://github.com/aws/aws-cdk/issues/16146)) ([677fedc](https://github.com/aws/aws-cdk/commit/677fedcc5351b8b5346970fac03e5e342f36265b)), closes [#16091](https://github.com/aws/aws-cdk/issues/16091) +* **sqs:** unable to import a FIFO queue when the queue ARN is a token ([#15976](https://github.com/aws/aws-cdk/issues/15976)) ([a1a65bc](https://github.com/aws/aws-cdk/commit/a1a65bc9a38b06ec51dff462e52b1beb8d421a56)), closes [#12466](https://github.com/aws/aws-cdk/issues/12466) +* **ssm:** StringParameter.fromStringParameterAttributes cannot accept version as a numeric Token ([#16048](https://github.com/aws/aws-cdk/issues/16048)) ([eb54cd4](https://github.com/aws/aws-cdk/commit/eb54cd416a48708898e30986058491e21125b2f7)), closes [#11913](https://github.com/aws/aws-cdk/issues/11913) +* (aws-ec2): fix vpc endpoint incorrect issue in China region ([#16139](https://github.com/aws/aws-cdk/issues/16139)) ([0d0db38](https://github.com/aws/aws-cdk/commit/0d0db38e3cdb557b4a641c5993068400847cc7df)), closes [#9864](https://github.com/aws/aws-cdk/issues/9864) +* KubectlHandler - insecure kubeconfig warning ([#16063](https://github.com/aws/aws-cdk/issues/16063)) ([82dd282](https://github.com/aws/aws-cdk/commit/82dd2822a86431d0aa0be896550d421810b80c67)), closes [#14560](https://github.com/aws/aws-cdk/issues/14560) + + +### Reverts + +* **cli:** 'deploy' and 'diff' silently does nothing when given unknown stack name ([#16125](https://github.com/aws/aws-cdk/issues/16125)) ([f2d77d3](https://github.com/aws/aws-cdk/commit/f2d77d336d535ef718813b4ed6b88b5d2af05cb9)), closes [aws/aws-cdk#16073](https://github.com/aws/aws-cdk/issues/16073) +* temporarily transfer [@skinny85](https://github.com/skinny85) module ownership ([#16206](https://github.com/aws/aws-cdk/issues/16206)) ([e678f10](https://github.com/aws/aws-cdk/commit/e678f104df4fb0377c6ad5c8abc4132433363871)) + ## [1.119.0](https://github.com/aws/aws-cdk/compare/v1.118.0...v1.119.0) (2021-08-17) diff --git a/version.v1.json b/version.v1.json index e02425c0f7b3d..7ff9a577d56ba 100644 --- a/version.v1.json +++ b/version.v1.json @@ -1,3 +1,3 @@ { - "version": "1.119.0" + "version": "1.120.0" } \ No newline at end of file From 7543f49407e0e3bbd75eb3146b9570a93fe17aa8 Mon Sep 17 00:00:00 2001 From: Elad Ben-Israel Date: Thu, 26 Aug 2021 13:29:54 +0300 Subject: [PATCH 26/39] Update CHANGELOG.md --- CHANGELOG.md | 5 ----- 1 file changed, 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a17afe6a8cc59..09152b13c5766 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,7 +26,6 @@ All notable changes to this project will be documented in this file. See [standa * **apigatewayv2:** http api - disallow empty string as domain name ([#16044](https://github.com/aws/aws-cdk/issues/16044)) ([9c39bcb](https://github.com/aws/aws-cdk/commit/9c39bcb970fc791e94d199b962cc006fca1a3320)) * **appsync:** addSubscription only allows for field type ([#16097](https://github.com/aws/aws-cdk/issues/16097)) ([000d151](https://github.com/aws/aws-cdk/commit/000d151bec2215aa530819c3cf2c8c432352fec3)), closes [#10078](https://github.com/aws/aws-cdk/issues/10078) [#16071](https://github.com/aws/aws-cdk/issues/16071) * **cfnspec:** changes to resource-level documentation not supported ([#16170](https://github.com/aws/aws-cdk/issues/16170)) ([82e4b4f](https://github.com/aws/aws-cdk/commit/82e4b4f07be202e2d6c6afa4f9ed0d9d6146f0a8)) -* **cli:** 'deploy' and 'diff' silently does nothing when given unknown stack name ([#16073](https://github.com/aws/aws-cdk/issues/16073)) ([f35b032](https://github.com/aws/aws-cdk/commit/f35b032cea4354992d3320e78c1ed0e2878a3fe7)), closes [#15866](https://github.com/aws/aws-cdk/issues/15866) * **cli:** Python init template does not work in directory with '-' ([#15939](https://github.com/aws/aws-cdk/issues/15939)) ([3b2c790](https://github.com/aws/aws-cdk/commit/3b2c790c2b7d210868576540feab4e088376ab6c)), closes [#15938](https://github.com/aws/aws-cdk/issues/15938) * **cli:** unknown command pytest in build container fails integration tests ([#16134](https://github.com/aws/aws-cdk/issues/16134)) ([0f7c0b4](https://github.com/aws/aws-cdk/commit/0f7c0b421327f1ffed28de79692191af187f23ca)), closes [#15939](https://github.com/aws/aws-cdk/issues/15939) * **resourcegroups:** ResourceGroup not using TagType.STANDARD, causes deploy failure ([#16211](https://github.com/aws/aws-cdk/issues/16211)) ([cdee1af](https://github.com/aws/aws-cdk/commit/cdee1af03c34a1c08988e672bae6edc2538a8877)), closes [#12986](https://github.com/aws/aws-cdk/issues/12986) @@ -38,10 +37,6 @@ All notable changes to this project will be documented in this file. See [standa * KubectlHandler - insecure kubeconfig warning ([#16063](https://github.com/aws/aws-cdk/issues/16063)) ([82dd282](https://github.com/aws/aws-cdk/commit/82dd2822a86431d0aa0be896550d421810b80c67)), closes [#14560](https://github.com/aws/aws-cdk/issues/14560) -### Reverts - -* **cli:** 'deploy' and 'diff' silently does nothing when given unknown stack name ([#16125](https://github.com/aws/aws-cdk/issues/16125)) ([f2d77d3](https://github.com/aws/aws-cdk/commit/f2d77d336d535ef718813b4ed6b88b5d2af05cb9)), closes [aws/aws-cdk#16073](https://github.com/aws/aws-cdk/issues/16073) -* temporarily transfer [@skinny85](https://github.com/skinny85) module ownership ([#16206](https://github.com/aws/aws-cdk/issues/16206)) ([e678f10](https://github.com/aws/aws-cdk/commit/e678f104df4fb0377c6ad5c8abc4132433363871)) ## [1.119.0](https://github.com/aws/aws-cdk/compare/v1.118.0...v1.119.0) (2021-08-17) From 0d96a6772da79fbafcb586b982e5bc5316d45589 Mon Sep 17 00:00:00 2001 From: Elad Ben-Israel Date: Thu, 26 Aug 2021 13:37:35 +0300 Subject: [PATCH 27/39] Apply suggestions from code review Co-authored-by: Niranjan Jayakar --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 09152b13c5766..5b5e333536dde 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ All notable changes to this project will be documented in this file. See [standa ### Features * **assertions:** queries and assertions against the Outputs and Mappings sections ([#15892](https://github.com/aws/aws-cdk/issues/15892)) ([90f95e1](https://github.com/aws/aws-cdk/commit/90f95e10f4dd9e4992731d6262dcfc767b65ab3f)) -* **aws-stepfunctions:** add support to heartbeat error inside catch block ([#16078](https://github.com/aws/aws-cdk/issues/16078)) ([2372b3c](https://github.com/aws/aws-cdk/commit/2372b3c360d13fb0224fc981a7bb1ae318581265)), closes [#16084](https://github.com/aws/aws-cdk/issues/16084) +* **stepfunctions:** add support to heartbeat error inside catch block ([#16078](https://github.com/aws/aws-cdk/issues/16078)) ([2372b3c](https://github.com/aws/aws-cdk/commit/2372b3c360d13fb0224fc981a7bb1ae318581265)), closes [#16084](https://github.com/aws/aws-cdk/issues/16084) * **cfnspec:** cloudformation spec v39.10.0 ([#16114](https://github.com/aws/aws-cdk/issues/16114)) ([7e0ad5d](https://github.com/aws/aws-cdk/commit/7e0ad5d17b30150922d0dfd81f42da11fadb8beb)) * **cfnspec:** cloudformation spec v40.0.0 ([#16183](https://github.com/aws/aws-cdk/issues/16183)) ([b059124](https://github.com/aws/aws-cdk/commit/b059124b238e27751217cbdaaa01c38b00e80fc9)) * **cloudwatch:** add support for cross-account alarms ([#16007](https://github.com/aws/aws-cdk/issues/16007)) ([e547ba0](https://github.com/aws/aws-cdk/commit/e547ba0d1491af0abe703132fa06fe786ffd7070)), closes [#15959](https://github.com/aws/aws-cdk/issues/15959) From 00b9c84da7351a2eeaff0a0f31728508597fe400 Mon Sep 17 00:00:00 2001 From: Elad Ben-Israel Date: Thu, 26 Aug 2021 13:40:47 +0300 Subject: [PATCH 28/39] Update CHANGELOG.md --- CHANGELOG.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b5e333536dde..dd16881355b70 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,6 @@ All notable changes to this project will be documented in this file. See [standa * **assertions:** queries and assertions against the Outputs and Mappings sections ([#15892](https://github.com/aws/aws-cdk/issues/15892)) ([90f95e1](https://github.com/aws/aws-cdk/commit/90f95e10f4dd9e4992731d6262dcfc767b65ab3f)) * **stepfunctions:** add support to heartbeat error inside catch block ([#16078](https://github.com/aws/aws-cdk/issues/16078)) ([2372b3c](https://github.com/aws/aws-cdk/commit/2372b3c360d13fb0224fc981a7bb1ae318581265)), closes [#16084](https://github.com/aws/aws-cdk/issues/16084) -* **cfnspec:** cloudformation spec v39.10.0 ([#16114](https://github.com/aws/aws-cdk/issues/16114)) ([7e0ad5d](https://github.com/aws/aws-cdk/commit/7e0ad5d17b30150922d0dfd81f42da11fadb8beb)) * **cfnspec:** cloudformation spec v40.0.0 ([#16183](https://github.com/aws/aws-cdk/issues/16183)) ([b059124](https://github.com/aws/aws-cdk/commit/b059124b238e27751217cbdaaa01c38b00e80fc9)) * **cloudwatch:** add support for cross-account alarms ([#16007](https://github.com/aws/aws-cdk/issues/16007)) ([e547ba0](https://github.com/aws/aws-cdk/commit/e547ba0d1491af0abe703132fa06fe786ffd7070)), closes [#15959](https://github.com/aws/aws-cdk/issues/15959) * **codecommit:** make Repository a source for CodeStar Notifications ([#15739](https://github.com/aws/aws-cdk/issues/15739)) ([ae34d4a](https://github.com/aws/aws-cdk/commit/ae34d4a69a5073d8f0175b5282fa8bf92139fab5)) @@ -33,8 +32,8 @@ All notable changes to this project will be documented in this file. See [standa * **ses:** drop spam rule appears in the incorrect order ([#16146](https://github.com/aws/aws-cdk/issues/16146)) ([677fedc](https://github.com/aws/aws-cdk/commit/677fedcc5351b8b5346970fac03e5e342f36265b)), closes [#16091](https://github.com/aws/aws-cdk/issues/16091) * **sqs:** unable to import a FIFO queue when the queue ARN is a token ([#15976](https://github.com/aws/aws-cdk/issues/15976)) ([a1a65bc](https://github.com/aws/aws-cdk/commit/a1a65bc9a38b06ec51dff462e52b1beb8d421a56)), closes [#12466](https://github.com/aws/aws-cdk/issues/12466) * **ssm:** StringParameter.fromStringParameterAttributes cannot accept version as a numeric Token ([#16048](https://github.com/aws/aws-cdk/issues/16048)) ([eb54cd4](https://github.com/aws/aws-cdk/commit/eb54cd416a48708898e30986058491e21125b2f7)), closes [#11913](https://github.com/aws/aws-cdk/issues/11913) -* (aws-ec2): fix vpc endpoint incorrect issue in China region ([#16139](https://github.com/aws/aws-cdk/issues/16139)) ([0d0db38](https://github.com/aws/aws-cdk/commit/0d0db38e3cdb557b4a641c5993068400847cc7df)), closes [#9864](https://github.com/aws/aws-cdk/issues/9864) -* KubectlHandler - insecure kubeconfig warning ([#16063](https://github.com/aws/aws-cdk/issues/16063)) ([82dd282](https://github.com/aws/aws-cdk/commit/82dd2822a86431d0aa0be896550d421810b80c67)), closes [#14560](https://github.com/aws/aws-cdk/issues/14560) +* **ec2:** fix vpc endpoint incorrect issue in China region ([#16139](https://github.com/aws/aws-cdk/issues/16139)) ([0d0db38](https://github.com/aws/aws-cdk/commit/0d0db38e3cdb557b4a641c5993068400847cc7df)), closes [#9864](https://github.com/aws/aws-cdk/issues/9864) +* **eks:** insecure kubeconfig warning ([#16063](https://github.com/aws/aws-cdk/issues/16063)) ([82dd282](https://github.com/aws/aws-cdk/commit/82dd2822a86431d0aa0be896550d421810b80c67)), closes [#14560](https://github.com/aws/aws-cdk/issues/14560) From 0889564a0c1b04d33909dd3fdb42147f23d67cbd Mon Sep 17 00:00:00 2001 From: Yihui Han <53243835+readybuilderone@users.noreply.github.com> Date: Fri, 27 Aug 2021 00:50:13 +0800 Subject: [PATCH 29/39] fix(apigatewayv2): api mapping key with two hyphens is disallowed (#16204) fix(apigatewayv2): fix api mapping key too strict issue. This PR fix the issue that Validation of apiMappingKey in ApiMappingProps is too strict. Closes: #15948 ---- # Considerations: The validation rule of the apiMappingKey was added in [PR #9141](https://github.com/aws/aws-cdk/pull/9141) to fix [issue #8983](https://github.com/aws/aws-cdk/issues/8983). However, as time changed, it looks like the validation rules have changed as well. But the validation rules are not clearly written out in AWS documentation, so I did [various validations](https://github.com/aws/aws-cdk/issues/15948#issuecomment-899520871) in the AWS console based on the unit test, and found the rules to be very complex. Thanks to @pahud and @nija-at 's suggestions, there is no need to stand in the way between CDK users and API Gateway supporting more possibilities in the future, I just removed the validation logic and the relative unit tests. *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../lib/common/api-mapping.ts | 5 - .../test/common/api-mapping.test.ts | 114 ------------------ 2 files changed, 119 deletions(-) diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/common/api-mapping.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/common/api-mapping.ts index f9ebfc89b52ca..a4031ddf783e2 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/common/api-mapping.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/common/api-mapping.ts @@ -103,11 +103,6 @@ export class ApiMapping extends Resource implements IApiMapping { } } - const paramRe = '^[a-zA-Z0-9]*[-_.+!,$]?[a-zA-Z0-9]*$'; - if (props.apiMappingKey && !new RegExp(paramRe).test(props.apiMappingKey)) { - throw new Error('An ApiMapping key may contain only letters, numbers and one of $-_.+!*\'(),'); - } - if (props.apiMappingKey === '') { throw new Error('empty string for api mapping key not allowed'); } diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/common/api-mapping.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/common/api-mapping.test.ts index 607afb5f8238f..ff53d3dad11fc 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/test/common/api-mapping.test.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/test/common/api-mapping.test.ts @@ -83,120 +83,6 @@ describe('ApiMapping', () => { }).toThrow(/empty string for api mapping key not allowed/); }); - test('apiMappingKey validation - single slash not allowed', () => { - - const stack = new Stack(); - const api = new HttpApi(stack, 'Api'); - - const dn = new DomainName(stack, 'DomainName', { - domainName, - certificate: Certificate.fromCertificateArn(stack, 'cert', certArn), - }); - - expect(() => { - new ApiMapping(stack, 'Mapping', { - api, - domainName: dn, - apiMappingKey: '/', - }); - }).toThrow(/An ApiMapping key may contain only letters, numbers and one of/); - }); - - test('apiMappingKey validation - prefix slash not allowd', () => { - - const stack = new Stack(); - const api = new HttpApi(stack, 'Api'); - - const dn = new DomainName(stack, 'DomainName', { - domainName, - certificate: Certificate.fromCertificateArn(stack, 'cert', certArn), - }); - - expect(() => { - new ApiMapping(stack, 'Mapping', { - api, - domainName: dn, - apiMappingKey: '/foo', - }); - }).toThrow(/An ApiMapping key may contain only letters, numbers and one of/); - }); - - test('apiMappingKey validation - slash in the middle not allowed', () => { - - const stack = new Stack(); - const api = new HttpApi(stack, 'Api'); - - const dn = new DomainName(stack, 'DomainName', { - domainName, - certificate: Certificate.fromCertificateArn(stack, 'cert', certArn), - }); - - expect(() => { - new ApiMapping(stack, 'Mapping', { - api, - domainName: dn, - apiMappingKey: 'foo/bar', - }); - }).toThrow(/An ApiMapping key may contain only letters, numbers and one of/); - }); - - test('apiMappingKey validation - trailing slash not allowed', () => { - - const stack = new Stack(); - const api = new HttpApi(stack, 'Api'); - - const dn = new DomainName(stack, 'DomainName', { - domainName, - certificate: Certificate.fromCertificateArn(stack, 'cert', certArn), - }); - - expect(() => { - new ApiMapping(stack, 'Mapping', { - api, - domainName: dn, - apiMappingKey: 'foo/', - }); - }).toThrow(/An ApiMapping key may contain only letters, numbers and one of/); - }); - - test('apiMappingKey validation - special character in the prefix not allowed', () => { - - const stack = new Stack(); - const api = new HttpApi(stack, 'Api'); - - const dn = new DomainName(stack, 'DomainName', { - domainName, - certificate: Certificate.fromCertificateArn(stack, 'cert', certArn), - }); - - expect(() => { - new ApiMapping(stack, 'Mapping', { - api, - domainName: dn, - apiMappingKey: '^foo', - }); - }).toThrow(/An ApiMapping key may contain only letters, numbers and one of/); - }); - - test('apiMappingKey validation - multiple special character not allowed', () => { - - const stack = new Stack(); - const api = new HttpApi(stack, 'Api'); - - const dn = new DomainName(stack, 'DomainName', { - domainName, - certificate: Certificate.fromCertificateArn(stack, 'cert', certArn), - }); - - expect(() => { - new ApiMapping(stack, 'Mapping', { - api, - domainName: dn, - apiMappingKey: 'foo.*$', - }); - }).toThrow(/An ApiMapping key may contain only letters, numbers and one of/); - }); - test('import mapping', () => { const stack = new Stack(); From 5cec2f8c4f2e08838e20b9757cda446f428b80c9 Mon Sep 17 00:00:00 2001 From: Wouter Klijn Date: Thu, 26 Aug 2021 19:30:15 +0200 Subject: [PATCH 30/39] chore(rds): add new versions of MariaDB, MySQL and SQL Server engines (#16115) Added every change as a separate commit, feel free to rebase as you see fit. ---- *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/lib/instance-engine.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-rds/lib/instance-engine.ts b/packages/@aws-cdk/aws-rds/lib/instance-engine.ts index eacb8097da55e..c070b0988e314 100644 --- a/packages/@aws-cdk/aws-rds/lib/instance-engine.ts +++ b/packages/@aws-cdk/aws-rds/lib/instance-engine.ts @@ -247,6 +247,8 @@ export class MariaDbEngineVersion { public static readonly VER_10_2_32 = MariaDbEngineVersion.of('10.2.32', '10.2'); /** Version "10.2.37". */ public static readonly VER_10_2_37 = MariaDbEngineVersion.of('10.2.37', '10.2'); + /** Version "10.2.39". */ + public static readonly VER_10_2_39 = MariaDbEngineVersion.of('10.2.39', '10.2'); /** Version "10.3" (only a major version, without a specific minor version). */ public static readonly VER_10_3 = MariaDbEngineVersion.of('10.3', '10.3'); @@ -462,6 +464,8 @@ export class MysqlEngineVersion { public static readonly VER_5_7_31 = MysqlEngineVersion.of('5.7.31', '5.7'); /** Version "5.7.33". */ public static readonly VER_5_7_33 = MysqlEngineVersion.of('5.7.33', '5.7'); + /** Version "5.7.34". */ + public static readonly VER_5_7_34 = MysqlEngineVersion.of('5.7.34', '5.7'); /** Version "8.0" (only a major version, without a specific minor version). */ public static readonly VER_8_0 = MysqlEngineVersion.of('8.0', '8.0'); @@ -1330,6 +1334,10 @@ export class SqlServerEngineVersion { public static readonly VER_13_00_5598_27_V1 = SqlServerEngineVersion.of('13.00.5598.27.v1', '13.00'); /** Version "13.00.5820.21.v1". */ public static readonly VER_13_00_5820_21_V1 = SqlServerEngineVersion.of('13.00.5820.21.v1', '13.00'); + /** Version "13.00.5850.14.v1". */ + public static readonly VER_13_00_5850_14_V1 = SqlServerEngineVersion.of('13.00.5850.14.v1', '13.00'); + /** Version "13.00.5882.1.v1". */ + public static readonly VER_13_00_5882_1_V1 = SqlServerEngineVersion.of('13.00.5882.1.v1', '13.00'); /** Version "14.00" (only a major version, without a specific minor version). */ public static readonly VER_14 = SqlServerEngineVersion.of('14.00', '14.00'); @@ -1358,8 +1366,13 @@ export class SqlServerEngineVersion { public static readonly VER_15 = SqlServerEngineVersion.of('15.00', '15.00'); /** Version "15.00.4043.16.v1". */ public static readonly VER_15_00_4043_16_V1 = SqlServerEngineVersion.of('15.00.4043.16.v1', '15.00'); - /** Version "15.00.4043.23.v1". */ + /** + * Version "15.00.4043.23.v1". + * @deprecated This version is erroneous. You might be looking for {@link SqlServerEngineVersion.VER_15_00_4073_23_V1}, instead. + */ public static readonly VER_15_00_4043_23_V1 = SqlServerEngineVersion.of('15.00.4043.23.v1', '15.00'); + /** Version "15.00.4073.23.v1". */ + public static readonly VER_15_00_4073_23_V1 = SqlServerEngineVersion.of('15.00.4073.23.v1', '15.00'); /** * Create a new SqlServerEngineVersion with an arbitrary version. From f40e8d6a502dd42e0a52d81f72abecaa2cdd920a Mon Sep 17 00:00:00 2001 From: Yuto Osawa Date: Fri, 27 Aug 2021 03:11:57 +0900 Subject: [PATCH 31/39] feat(ecs-patterns): add capacity provider strategies to queue processing service pattern (#15684) closes #14781 ---- *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-ecs-patterns/README.md | 51 +++++++++++++++++++ .../lib/base/queue-processing-service-base.ts | 10 +++- .../lib/ecs/queue-processing-ecs-service.ts | 1 + .../queue-processing-fargate-service.ts | 1 + .../@aws-cdk/aws-ecs-patterns/package.json | 2 + .../ec2/test.queue-processing-ecs-service.ts | 45 ++++++++++++++++ .../test.queue-processing-fargate-service.ts | 45 ++++++++++++++++ 7 files changed, 154 insertions(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-ecs-patterns/README.md b/packages/@aws-cdk/aws-ecs-patterns/README.md index 58dba9f6786cb..fbeb84acb49e6 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/README.md +++ b/packages/@aws-cdk/aws-ecs-patterns/README.md @@ -484,6 +484,57 @@ const queueProcessingFargateService = new QueueProcessingFargateService(stack, ' }); ``` +### Set capacityProviderStrategies for QueueProcessingFargateService + +```ts +const vpc = new ec2.Vpc(stack, 'Vpc', { maxAzs: 1 }); +const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); +cluster.enableFargateCapacityProviders(); + +const queueProcessingFargateService = new QueueProcessingFargateService(stack, 'Service', { + cluster, + memoryLimitMiB: 512, + image: ecs.ContainerImage.fromRegistry('test'), + capacityProviderStrategies: [ + { + capacityProvider: 'FARGATE_SPOT', + weight: 2, + }, + { + capacityProvider: 'FARGATE', + weight: 1, + }, + ], +}); +``` + +### Set capacityProviderStrategies for QueueProcessingEc2Service + +```ts +const vpc = new ec2.Vpc(stack, 'Vpc', { maxAzs: 1 }); +const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); +const autoScalingGroup = new autoscaling.AutoScalingGroup(stack, 'asg', { + vpc, + instanceType: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.MICRO), + machineImage: ecs.EcsOptimizedImage.amazonLinux2(), +}); +const capacityProvider = new ecs.AsgCapacityProvider(stack, 'provider', { + autoScalingGroup, +}); +cluster.addAsgCapacityProvider(capacityProvider); + +const queueProcessingFargateService = new QueueProcessingFargateService(stack, 'Service', { + cluster, + memoryLimitMiB: 512, + image: ecs.ContainerImage.fromRegistry('test'), + capacityProviderStrategies: [ + { + capacityProvider: capacityProvider.capacityProviderName, + }, + ], +}); +``` + ### Select specific vpc subnets for ApplicationLoadBalancedFargateService ```ts diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/base/queue-processing-service-base.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/base/queue-processing-service-base.ts index b2da8f97e6bc4..2c293dc3173de 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/base/queue-processing-service-base.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/base/queue-processing-service-base.ts @@ -1,7 +1,7 @@ import { ScalingInterval } from '@aws-cdk/aws-applicationautoscaling'; import { IVpc } from '@aws-cdk/aws-ec2'; import { - AwsLogDriver, BaseService, Cluster, ContainerImage, DeploymentController, DeploymentCircuitBreaker, + AwsLogDriver, BaseService, CapacityProviderStrategy, Cluster, ContainerImage, DeploymentController, DeploymentCircuitBreaker, ICluster, LogDriver, PropagatedTagSource, Secret, } from '@aws-cdk/aws-ecs'; import { IQueue, Queue } from '@aws-cdk/aws-sqs'; @@ -207,6 +207,14 @@ export interface QueueProcessingServiceBaseProps { * @default - disabled */ readonly circuitBreaker?: DeploymentCircuitBreaker; + + /** + * A list of Capacity Provider strategies used to place a service. + * + * @default - undefined + * + */ + readonly capacityProviderStrategies?: CapacityProviderStrategy[]; } /** diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/queue-processing-ecs-service.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/queue-processing-ecs-service.ts index d927284797b5b..0d9f612abfb76 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/queue-processing-ecs-service.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/queue-processing-ecs-service.ts @@ -123,6 +123,7 @@ export class QueueProcessingEc2Service extends QueueProcessingServiceBase { enableECSManagedTags: props.enableECSManagedTags, deploymentController: props.deploymentController, circuitBreaker: props.circuitBreaker, + capacityProviderStrategies: props.capacityProviderStrategies, }); this.configureAutoscalingForService(this.service); diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/queue-processing-fargate-service.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/queue-processing-fargate-service.ts index cb4b8d77a8188..e6f8b89c736b3 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/queue-processing-fargate-service.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/queue-processing-fargate-service.ts @@ -149,6 +149,7 @@ export class QueueProcessingFargateService extends QueueProcessingServiceBase { vpcSubnets: props.taskSubnets, assignPublicIp: props.assignPublicIp, circuitBreaker: props.circuitBreaker, + capacityProviderStrategies: props.capacityProviderStrategies, }); this.configureAutoscalingForService(this.service); diff --git a/packages/@aws-cdk/aws-ecs-patterns/package.json b/packages/@aws-cdk/aws-ecs-patterns/package.json index 2bed9fe6b9cc8..c9d3b46b8f8b2 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/package.json +++ b/packages/@aws-cdk/aws-ecs-patterns/package.json @@ -76,6 +76,7 @@ }, "dependencies": { "@aws-cdk/aws-applicationautoscaling": "0.0.0", + "@aws-cdk/aws-autoscaling": "0.0.0", "@aws-cdk/aws-certificatemanager": "0.0.0", "@aws-cdk/aws-ec2": "0.0.0", "@aws-cdk/aws-ecs": "0.0.0", @@ -94,6 +95,7 @@ "homepage": "https://github.com/aws/aws-cdk", "peerDependencies": { "@aws-cdk/aws-applicationautoscaling": "0.0.0", + "@aws-cdk/aws-autoscaling": "0.0.0", "@aws-cdk/aws-certificatemanager": "0.0.0", "@aws-cdk/aws-ec2": "0.0.0", "@aws-cdk/aws-ecs": "0.0.0", diff --git a/packages/@aws-cdk/aws-ecs-patterns/test/ec2/test.queue-processing-ecs-service.ts b/packages/@aws-cdk/aws-ecs-patterns/test/ec2/test.queue-processing-ecs-service.ts index 763cef9f33114..97bdb47c7b5b3 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/test/ec2/test.queue-processing-ecs-service.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/test/ec2/test.queue-processing-ecs-service.ts @@ -1,4 +1,5 @@ import { ABSENT, expect, haveResource, haveResourceLike } from '@aws-cdk/assert-internal'; +import * as autoscaling from '@aws-cdk/aws-autoscaling'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as ecs from '@aws-cdk/aws-ecs'; import * as sqs from '@aws-cdk/aws-sqs'; @@ -351,4 +352,48 @@ export = { test.done(); }, + + 'can set capacity provider strategies'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + const cluster = new ecs.Cluster(stack, 'Cluster', { vpc }); + const autoScalingGroup = new autoscaling.AutoScalingGroup(stack, 'asg', { + vpc, + instanceType: new ec2.InstanceType('bogus'), + machineImage: ecs.EcsOptimizedImage.amazonLinux2(), + }); + const capacityProvider = new ecs.AsgCapacityProvider(stack, 'provider', { + autoScalingGroup, + }); + cluster.addAsgCapacityProvider(capacityProvider); + + // WHEN + new ecsPatterns.QueueProcessingEc2Service(stack, 'Service', { + cluster, + image: ecs.ContainerImage.fromRegistry('test'), + memoryLimitMiB: 512, + capacityProviderStrategies: [ + { + capacityProvider: capacityProvider.capacityProviderName, + }, + ], + }); + + // THEN + expect(stack).to( + haveResource('AWS::ECS::Service', { + LaunchType: ABSENT, + CapacityProviderStrategy: [ + { + CapacityProvider: { + Ref: 'providerD3FF4D3A', + }, + }, + ], + }), + ); + + test.done(); + }, }; diff --git a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/test.queue-processing-fargate-service.ts b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/test.queue-processing-fargate-service.ts index 7a0f33e3a0a0c..6b5a05ccaaf7b 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/test.queue-processing-fargate-service.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/test.queue-processing-fargate-service.ts @@ -529,4 +529,49 @@ export = { test.done(); }, + + 'can set capacity provider strategies'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'MyVpc', {}); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { + vpc, + }); + cluster.enableFargateCapacityProviders(); + + // WHEN + new ecsPatterns.QueueProcessingFargateService(stack, 'Service', { + cluster, + image: ecs.ContainerImage.fromRegistry('test'), + capacityProviderStrategies: [ + { + capacityProvider: 'FARGATE_SPOT', + weight: 2, + }, + { + capacityProvider: 'FARGATE', + weight: 1, + }, + ], + }); + + // THEN + expect(stack).to( + haveResource('AWS::ECS::Service', { + LaunchType: ABSENT, + CapacityProviderStrategy: [ + { + CapacityProvider: 'FARGATE_SPOT', + Weight: 2, + }, + { + CapacityProvider: 'FARGATE', + Weight: 1, + }, + ], + }), + ); + + test.done(); + }, }; From 04d45474d80d3687a3fdf27f4d76dd1c8521eff0 Mon Sep 17 00:00:00 2001 From: benhawley7 <31001948+benhawley7@users.noreply.github.com> Date: Thu, 26 Aug 2021 19:53:35 +0100 Subject: [PATCH 32/39] fix(docs): unnecessary log group in Step Functions state machine x-ray example (#16159) Removes an unnecessary log group definition from the code example for enabling X-Ray in the State Machine. fixes https://github.com/aws/aws-cdk/issues/16158 ---- *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 | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/@aws-cdk/aws-stepfunctions/README.md b/packages/@aws-cdk/aws-stepfunctions/README.md index 4faf512ad0509..6171bac75805a 100644 --- a/packages/@aws-cdk/aws-stepfunctions/README.md +++ b/packages/@aws-cdk/aws-stepfunctions/README.md @@ -593,8 +593,6 @@ new sfn.StateMachine(stack, 'MyStateMachine', { Enable X-Ray tracing for StateMachine: ```ts -const logGroup = new logs.LogGroup(stack, 'MyLogGroup'); - new sfn.StateMachine(stack, 'MyStateMachine', { definition: sfn.Chain.start(new sfn.Pass(stack, 'Pass')), tracingEnabled: true From a42a1ea5a122f864936cdb0113b16fe92cc7205e Mon Sep 17 00:00:00 2001 From: Robert Djurasaj Date: Thu, 26 Aug 2021 14:12:50 -0600 Subject: [PATCH 33/39] feat(ec2): add m6i instances (#16081) https://aws.amazon.com/about-aws/whats-new/2021/08/amazon-ec2-m6i-instances/ ---- *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-ec2/lib/instance-types.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/@aws-cdk/aws-ec2/lib/instance-types.ts b/packages/@aws-cdk/aws-ec2/lib/instance-types.ts index a12dfb92061c6..013f7d3389f3c 100644 --- a/packages/@aws-cdk/aws-ec2/lib/instance-types.ts +++ b/packages/@aws-cdk/aws-ec2/lib/instance-types.ts @@ -442,6 +442,16 @@ export enum InstanceClass { */ M6G = 'm6g', + /** + * Standard instances based on Intel (Ice Lake), 6th generation. + */ + STANDARD6_INTEL = 'm6i', + + /** + * Standard instances based on Intel (Ice Lake), 6th generation. + */ + M6I = 'm6i', + /** * Standard instances, 6th generation with Graviton2 processors and local NVME drive */ From 2c3d21e2f1117a54510ba92748588ee95ab3631c Mon Sep 17 00:00:00 2001 From: ABevier Date: Thu, 26 Aug 2021 19:16:26 -0400 Subject: [PATCH 34/39] feat(ecs-patterns): Allow configuration of SSL policy for listeners created by ECS patterns (#15210) This pr adds the ability to specify the SSL policy of the listener created by an ECS L3 pattern. The Listener's SSL Policy is immutable once it is created so I felt like this is best way to add this feature. I plan to follow this PR up with another that implements #11841 and allows further configuration of the listener after it has been created. closes #8816 ---- *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-ecs-patterns/README.md | 25 +++++++++++++++++++ .../application-load-balanced-service-base.ts | 10 +++++++- ...ion-multiple-target-groups-service-base.ts | 24 +++++++++++++++--- .../aws-ecs-patterns/test/ec2/test.l3s-v2.ts | 12 ++++++++- .../aws-ecs-patterns/test/ec2/test.l3s.ts | 4 ++- 5 files changed, 68 insertions(+), 7 deletions(-) diff --git a/packages/@aws-cdk/aws-ecs-patterns/README.md b/packages/@aws-cdk/aws-ecs-patterns/README.md index fbeb84acb49e6..00212e704e294 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/README.md +++ b/packages/@aws-cdk/aws-ecs-patterns/README.md @@ -330,6 +330,31 @@ const scheduledFargateTask = new ScheduledFargateTask(stack, 'ScheduledFargateTa In addition to using the constructs, users can also add logic to customize these constructs: +### Configure HTTPS on an ApplicationLoadBalancedFargateService + +```ts +import { ApplicationLoadBalancedFargateService } from './application-load-balanced-fargate-service'; +import { HostedZone } from '@aws-cdk/aws-route53'; +import { Certificate } from '@aws-cdk/aws-certificatemanager'; +import { SslPolicy } from '@aws-cdk/aws-elasticloadbalancingv2'; + +const domainZone = HostedZone.fromLookup(this, 'Zone', { domainName: 'example.com' }); +const certificate = Certificate.fromCertificateArn(this, 'Cert', 'arn:aws:acm:us-east-1:123456:certificate/abcdefg'); + +const loadBalancedFargateService = new ApplicationLoadBalancedFargateService(stack, 'Service', { + vpc + cluster, + certificate, + sslPolicy: SslPolicy.RECOMMENDED, + domainName: 'api.example.com', + domainZone, + redirectHTTP: true, + taskImageOptions: { + image: ecs.ContainerImage.fromRegistry("amazon/amazon-ecs-sample"), + }, +}); +``` + ### Add Schedule-Based Auto-Scaling to an ApplicationLoadBalancedFargateService ```ts diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/base/application-load-balanced-service-base.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/base/application-load-balanced-service-base.ts index 3c9005af19daa..b4c41ed5aa990 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/base/application-load-balanced-service-base.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/base/application-load-balanced-service-base.ts @@ -6,7 +6,7 @@ import { } from '@aws-cdk/aws-ecs'; import { ApplicationListener, ApplicationLoadBalancer, ApplicationProtocol, ApplicationProtocolVersion, ApplicationTargetGroup, - IApplicationLoadBalancer, ListenerCertificate, ListenerAction, AddApplicationTargetsProps, + IApplicationLoadBalancer, ListenerCertificate, ListenerAction, AddApplicationTargetsProps, SslPolicy, } from '@aws-cdk/aws-elasticloadbalancingv2'; import { IRole } from '@aws-cdk/aws-iam'; import { ARecord, IHostedZone, RecordTarget, CnameRecord } from '@aws-cdk/aws-route53'; @@ -190,6 +190,13 @@ export interface ApplicationLoadBalancedServiceBaseProps { */ readonly listenerPort?: number; + /** + * The security policy that defines which ciphers and protocols are supported by the ALB Listener. + * + * @default - The recommended elastic load balancing security policy + */ + readonly sslPolicy?: SslPolicy; + /** * Specifies whether to propagate the tags from the task definition or the service to the tasks in the service. * Tags can only be propagated to the tasks within the service during service creation. @@ -441,6 +448,7 @@ export abstract class ApplicationLoadBalancedServiceBase extends CoreConstruct { protocol, port: props.listenerPort, open: props.openListener ?? true, + sslPolicy: props.sslPolicy, }); this.targetGroup = this.listener.addTargets('ECS', targetProps); diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/base/application-multiple-target-groups-service-base.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/base/application-multiple-target-groups-service-base.ts index 9d9c21c9e7b5e..e2cafab14e71a 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/base/application-multiple-target-groups-service-base.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/base/application-multiple-target-groups-service-base.ts @@ -4,7 +4,7 @@ import { AwsLogDriver, BaseService, CloudMapOptions, Cluster, ContainerDefinition, ContainerImage, ICluster, LogDriver, PropagatedTagSource, Protocol, Secret, } from '@aws-cdk/aws-ecs'; -import { ApplicationListener, ApplicationLoadBalancer, ApplicationProtocol, ApplicationTargetGroup } from '@aws-cdk/aws-elasticloadbalancingv2'; +import { ApplicationListener, ApplicationLoadBalancer, ApplicationProtocol, ApplicationTargetGroup, SslPolicy } from '@aws-cdk/aws-elasticloadbalancingv2'; import { IRole } from '@aws-cdk/aws-iam'; import { ARecord, IHostedZone, RecordTarget } from '@aws-cdk/aws-route53'; import { LoadBalancerTarget } from '@aws-cdk/aws-route53-targets'; @@ -332,6 +332,13 @@ export interface ApplicationListenerProps { * created for the load balancer's specified domain name. */ readonly certificate?: ICertificate; + + /** + * The security policy that defines which ciphers and protocols are supported by the ALB Listener. + * + * @default - The recommended elastic load balancing security policy + */ + readonly sslPolicy?: SslPolicy; } /** @@ -407,6 +414,7 @@ export abstract class ApplicationMultipleTargetGroupsServiceBase extends CoreCon listenerName: listenerProps.name, loadBalancer: lb, port: listenerProps.port, + sslPolicy: listenerProps.sslPolicy, }); this.listeners.push(listener); } @@ -503,7 +511,7 @@ export abstract class ApplicationMultipleTargetGroupsServiceBase extends CoreCon } private configListener(protocol: ApplicationProtocol, props: ListenerConfig): ApplicationListener { - const listener = this.createListener(props.listenerName, props.loadBalancer, protocol, props.port); + const listener = this.createListener(props, protocol); let certificate; if (protocol === ApplicationProtocol.HTTPS) { certificate = this.createListenerCertificate(props.listenerName, props.certificate, props.domainName, props.domainZone); @@ -567,11 +575,12 @@ export abstract class ApplicationMultipleTargetGroupsServiceBase extends CoreCon } } - private createListener(name: string, lb: ApplicationLoadBalancer, protocol?: ApplicationProtocol, port?: number): ApplicationListener { - return lb.addListener(name, { + private createListener({ loadBalancer, listenerName, port, sslPolicy }: ListenerConfig, protocol?: ApplicationProtocol): ApplicationListener { + return loadBalancer.addListener(listenerName, { protocol, open: true, port, + sslPolicy, }); } @@ -622,6 +631,13 @@ interface ListenerConfig { */ readonly certificate?: ICertificate; + /** + * SSL Policy for the listener + * + * @default null + */ + readonly sslPolicy?: SslPolicy; + /** * The domain name for the service, e.g. "api.example.com." * diff --git a/packages/@aws-cdk/aws-ecs-patterns/test/ec2/test.l3s-v2.ts b/packages/@aws-cdk/aws-ecs-patterns/test/ec2/test.l3s-v2.ts index 0946123907fe2..5c3c5889c62b6 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/test/ec2/test.l3s-v2.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/test/ec2/test.l3s-v2.ts @@ -2,7 +2,7 @@ import { expect, haveResource, haveResourceLike, SynthUtils } from '@aws-cdk/ass import { Certificate } from '@aws-cdk/aws-certificatemanager'; import { InstanceType, Vpc } from '@aws-cdk/aws-ec2'; import { AwsLogDriver, Cluster, ContainerImage, Ec2TaskDefinition, PropagatedTagSource, Protocol } from '@aws-cdk/aws-ecs'; -import { ApplicationProtocol } from '@aws-cdk/aws-elasticloadbalancingv2'; +import { ApplicationProtocol, SslPolicy } from '@aws-cdk/aws-elasticloadbalancingv2'; import { CompositePrincipal, Role, ServicePrincipal } from '@aws-cdk/aws-iam'; import { PublicHostedZone } from '@aws-cdk/aws-route53'; import { NamespaceType } from '@aws-cdk/aws-servicediscovery'; @@ -124,6 +124,7 @@ export = { name: 'listener', protocol: ApplicationProtocol.HTTPS, certificate: Certificate.fromCertificateArn(stack, 'Cert', 'helloworld'), + sslPolicy: SslPolicy.TLS12_EXT, }, ], }, @@ -240,6 +241,15 @@ export = { }, })); + expect(stack).to(haveResourceLike('AWS::ElasticLoadBalancingV2::Listener', { + Port: 443, + Protocol: 'HTTPS', + Certificates: [{ + CertificateArn: 'helloworld', + }], + SslPolicy: SslPolicy.TLS12_EXT, + })); + test.done(); }, diff --git a/packages/@aws-cdk/aws-ecs-patterns/test/ec2/test.l3s.ts b/packages/@aws-cdk/aws-ecs-patterns/test/ec2/test.l3s.ts index 5ef253d71bdb1..8e2f527fbe509 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/test/ec2/test.l3s.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/test/ec2/test.l3s.ts @@ -2,7 +2,7 @@ import { ABSENT, arrayWith, expect, haveResource, haveResourceLike, objectLike } import { Certificate } from '@aws-cdk/aws-certificatemanager'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as ecs from '@aws-cdk/aws-ecs'; -import { ApplicationLoadBalancer, ApplicationProtocol, ApplicationProtocolVersion, NetworkLoadBalancer } from '@aws-cdk/aws-elasticloadbalancingv2'; +import { ApplicationLoadBalancer, ApplicationProtocol, ApplicationProtocolVersion, NetworkLoadBalancer, SslPolicy } from '@aws-cdk/aws-elasticloadbalancingv2'; import { PublicHostedZone } from '@aws-cdk/aws-route53'; import * as cloudmap from '@aws-cdk/aws-servicediscovery'; import * as cdk from '@aws-cdk/core'; @@ -508,6 +508,7 @@ export = { domainName: 'api.example.com', domainZone: zone, certificate: Certificate.fromCertificateArn(stack, 'Cert', 'helloworld'), + sslPolicy: SslPolicy.TLS12_EXT, }); // THEN - stack contains a load balancer and a service @@ -519,6 +520,7 @@ export = { Certificates: [{ CertificateArn: 'helloworld', }], + SslPolicy: SslPolicy.TLS12_EXT, })); expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { From f42b233a76ae810634fa43a25604dbc65bdd63b9 Mon Sep 17 00:00:00 2001 From: Robert Djurasaj Date: Fri, 27 Aug 2021 04:18:44 -0600 Subject: [PATCH 35/39] feat(aws-cloudfront-origins): add custom headers to S3Origin (#16161) Closes #16160. @njlynch Can you please review this when time permits and see if it needs any further polishing. cc @nwitte-rocketloans ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../@aws-cdk/aws-cloudfront-origins/README.md | 18 +++++++ .../aws-cloudfront-origins/lib/s3-origin.ts | 9 +--- .../test/s3-origin.test.ts | 17 +++++-- .../@aws-cdk/aws-cloudfront/lib/origin.ts | 32 ++++++++++++ .../aws-cloudfront/test/origin.test.ts | 50 ++++++++++++++++++- 5 files changed, 114 insertions(+), 12 deletions(-) diff --git a/packages/@aws-cdk/aws-cloudfront-origins/README.md b/packages/@aws-cdk/aws-cloudfront-origins/README.md index 05d12bc1ae6f4..cb7af64ff8618 100644 --- a/packages/@aws-cdk/aws-cloudfront-origins/README.md +++ b/packages/@aws-cdk/aws-cloudfront-origins/README.md @@ -33,6 +33,24 @@ CloudFront's redirect and error handling will be used. In the latter case, the O underlying bucket. This can be used in conjunction with a bucket that is not public to require that your users access your content using CloudFront URLs and not S3 URLs directly. Alternatively, a custom origin access identity can be passed to the S3 origin in the properties. +### Adding Custom Headers + +You can configure CloudFront to add custom headers to the requests that it sends to your origin. These custom headers enable you to send and gather information from your origin that you don’t get with typical viewer requests. These headers can even be customized for each origin. CloudFront supports custom headers for both for custom and Amazon S3 origins. + +```ts +import * as cloudfront from '@aws-cdk/aws-cloudfront'; +import * as origins from '@aws-cdk/aws-cloudfront-origins'; + +const myBucket = new s3.Bucket(this, 'myBucket'); +new cloudfront.Distribution(this, 'myDist', { + defaultBehavior: { origin: new origins.S3Origin(myBucket, { + customHeaders: { + Foo: 'bar', + }, + })}, +}); +``` + ## ELBv2 Load Balancer An Elastic Load Balancing (ELB) v2 load balancer may be used as an origin. In order for a load balancer to serve as an origin, it must be publicly diff --git a/packages/@aws-cdk/aws-cloudfront-origins/lib/s3-origin.ts b/packages/@aws-cdk/aws-cloudfront-origins/lib/s3-origin.ts index a0ac4037d9741..4bdd39a2e5888 100644 --- a/packages/@aws-cdk/aws-cloudfront-origins/lib/s3-origin.ts +++ b/packages/@aws-cdk/aws-cloudfront-origins/lib/s3-origin.ts @@ -11,14 +11,7 @@ import { Construct } from '@aws-cdk/core'; /** * Properties to use to customize an S3 Origin. */ -export interface S3OriginProps { - /** - * An optional path that CloudFront appends to the origin domain name when CloudFront requests content from the origin. - * Must begin, but not end, with '/' (e.g., '/production/images'). - * - * @default '/' - */ - readonly originPath?: string; +export interface S3OriginProps extends cloudfront.OriginProps { /** * An optional Origin Access Identity of the origin identity cloudfront will use when calling your s3 bucket. * diff --git a/packages/@aws-cdk/aws-cloudfront-origins/test/s3-origin.test.ts b/packages/@aws-cdk/aws-cloudfront-origins/test/s3-origin.test.ts index c9e0ecb797464..e8e2c4b2c41b9 100644 --- a/packages/@aws-cdk/aws-cloudfront-origins/test/s3-origin.test.ts +++ b/packages/@aws-cdk/aws-cloudfront-origins/test/s3-origin.test.ts @@ -1,7 +1,7 @@ import '@aws-cdk/assert-internal/jest'; import * as cloudfront from '@aws-cdk/aws-cloudfront'; import * as s3 from '@aws-cdk/aws-s3'; -import { App, Stack } from '@aws-cdk/core'; +import { App, Duration, Stack } from '@aws-cdk/core'; import { S3Origin } from '../lib'; let app: App; @@ -34,16 +34,27 @@ describe('With bucket', () => { }); }); - test('can customize originPath property', () => { + test('can customize base origin properties', () => { const bucket = new s3.Bucket(stack, 'Bucket'); - const origin = new S3Origin(bucket, { originPath: '/assets' }); + const origin = new S3Origin(bucket, { + originPath: '/assets', + connectionTimeout: Duration.seconds(5), + connectionAttempts: 2, + customHeaders: { AUTH: 'NONE' }, + }); const originBindConfig = origin.bind(stack, { originId: 'StackOrigin029E19582' }); expect(stack.resolve(originBindConfig.originProperty)).toEqual({ id: 'StackOrigin029E19582', domainName: { 'Fn::GetAtt': ['Bucket83908E77', 'RegionalDomainName'] }, originPath: '/assets', + connectionTimeout: 5, + connectionAttempts: 2, + originCustomHeaders: [{ + headerName: 'AUTH', + headerValue: 'NONE', + }], s3OriginConfig: { originAccessIdentity: { 'Fn::Join': ['', diff --git a/packages/@aws-cdk/aws-cloudfront/lib/origin.ts b/packages/@aws-cdk/aws-cloudfront/lib/origin.ts index 9b841e413c405..0b7ab7796b97f 100644 --- a/packages/@aws-cdk/aws-cloudfront/lib/origin.ts +++ b/packages/@aws-cdk/aws-cloudfront/lib/origin.ts @@ -120,6 +120,7 @@ export abstract class OriginBase implements IOrigin { protected constructor(domainName: string, props: OriginProps = {}) { validateIntInRangeOrUndefined('connectionTimeout', 1, 10, props.connectionTimeout?.toSeconds()); validateIntInRangeOrUndefined('connectionAttempts', 1, 3, props.connectionAttempts, false); + validateCustomHeaders(props.customHeaders); this.domainName = domainName; this.originPath = this.validateOriginPath(props.originPath); @@ -205,3 +206,34 @@ function validateIntInRangeOrUndefined(name: string, min: number, max: number, v throw new Error(`${name}: Must be an int between ${min} and ${max}${seconds} (inclusive); received ${value}.`); } } + +/** + * Throws an error if custom header assignment is prohibited by CloudFront. + * @link: https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/add-origin-custom-headers.html#add-origin-custom-headers-denylist + */ +function validateCustomHeaders(customHeaders?: Record) { + if (!customHeaders || Object.entries(customHeaders).length === 0) { return; } + const customHeaderKeys = Object.keys(customHeaders); + const prohibitedHeaderKeys = [ + 'Cache-Control', 'Connection', 'Content-Length', 'Cookie', 'Host', 'If-Match', 'If-Modified-Since', 'If-None-Match', 'If-Range', 'If-Unmodified-Since', + 'Max-Forwards', 'Pragma', 'Proxy-Authorization', 'Proxy-Connection', 'Range', 'Request-Range', 'TE', 'Trailer', 'Transfer-Encoding', 'Upgrade', 'Via', + 'X-Real-Ip', + ]; + const prohibitedHeaderKeyPrefixes = [ + 'X-Amz-', 'X-Edge-', + ]; + + const prohibitedHeadersKeysMatches = customHeaderKeys.filter(customKey => { + return prohibitedHeaderKeys.map((prohibitedKey) => prohibitedKey.toLowerCase()).includes(customKey.toLowerCase()); + }); + const prohibitedHeaderPrefixMatches = customHeaderKeys.filter(customKey => { + return prohibitedHeaderKeyPrefixes.some(prohibitedKeyPrefix => customKey.toLowerCase().startsWith(prohibitedKeyPrefix.toLowerCase())); + }); + + if (prohibitedHeadersKeysMatches.length !== 0) { + throw new Error(`The following headers cannot be configured as custom origin headers: ${prohibitedHeadersKeysMatches.join(', ')}`); + } + if (prohibitedHeaderPrefixMatches.length !== 0) { + throw new Error(`The following headers cannot be used as prefixes for custom origin headers: ${prohibitedHeaderPrefixMatches.join(', ')}`); + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cloudfront/test/origin.test.ts b/packages/@aws-cdk/aws-cloudfront/test/origin.test.ts index b30362c7fe652..e6a59ff6179d6 100644 --- a/packages/@aws-cdk/aws-cloudfront/test/origin.test.ts +++ b/packages/@aws-cdk/aws-cloudfront/test/origin.test.ts @@ -57,4 +57,52 @@ test.each(['us-east-1', 'ap-southeast-2', 'eu-west-3', 'me-south-1']) enabled: true, originShieldRegion, }); -}); \ No newline at end of file +}); + +test('throw an error if Custom Headers keys are not permitted', () => { + // case sensitive + expect(() => { + new TestOrigin('example.com', { + customHeaders: { + Host: 'bad', + Cookie: 'bad', + Connection: 'bad', + TS: 'bad', + }, + }); + }).toThrow(/The following headers cannot be configured as custom origin headers: (.*?)/); + + // case insensitive + expect(() => { + new TestOrigin('example.com', { + customHeaders: { + hOst: 'bad', + cOOkIe: 'bad', + Connection: 'bad', + Ts: 'bad', + }, + }); + }).toThrow(/The following headers cannot be configured as custom origin headers: (.*?)/); +}); + +test('throw an error if Custom Headers are pre-fixed with non-permitted keys', () => { + // case sensitive + expect(() => { + new TestOrigin('example.com', { + customHeaders: { + 'X-Amz-dummy': 'bad', + 'X-Edge-dummy': 'bad', + }, + }); + }).toThrow(/The following headers cannot be used as prefixes for custom origin headers: (.*?)/); + + // case insensitive + expect(() => { + new TestOrigin('example.com', { + customHeaders: { + 'x-amZ-dummy': 'bad', + 'x-eDgE-dummy': 'bad', + }, + }); + }).toThrow(/The following headers cannot be used as prefixes for custom origin headers: (.*?)/); +}); From fe81be78322e3f1c23d2b02e59b56faa3b06e554 Mon Sep 17 00:00:00 2001 From: AWS CDK Automation <43080478+aws-cdk-automation@users.noreply.github.com> Date: Fri, 27 Aug 2021 14:00:20 +0300 Subject: [PATCH 36/39] feat(cfnspec): cloudformation spec v40.1.0 (#16254) Co-authored-by: AWS CDK Team Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> --- packages/@aws-cdk/cfnspec/CHANGELOG.md | 20 +++++++++++++++++++ packages/@aws-cdk/cfnspec/cfn.version | 2 +- ...0_CloudFormationResourceSpecification.json | 16 +++++++++++++-- 3 files changed, 35 insertions(+), 3 deletions(-) diff --git a/packages/@aws-cdk/cfnspec/CHANGELOG.md b/packages/@aws-cdk/cfnspec/CHANGELOG.md index 85274a4543fbf..5def28ee631e9 100644 --- a/packages/@aws-cdk/cfnspec/CHANGELOG.md +++ b/packages/@aws-cdk/cfnspec/CHANGELOG.md @@ -1,3 +1,23 @@ +# CloudFormation Resource Specification v40.1.0 + +## New Resource Types + + +## Attribute Changes + + +## Property Changes + +* AWS::CE::CostCategory SplitChargeRules (__added__) + +## Property Type Changes + +* AWS::EFS::FileSystem.LifecyclePolicy TransitionToPrimaryStorageClass (__added__) +* AWS::EFS::FileSystem.LifecyclePolicy TransitionToIA.Required (__changed__) + * Old: true + * New: false + + # CloudFormation Resource Specification v40.0.0 ## New Resource Types diff --git a/packages/@aws-cdk/cfnspec/cfn.version b/packages/@aws-cdk/cfnspec/cfn.version index e9340fd6f7115..312a84c9209ab 100644 --- a/packages/@aws-cdk/cfnspec/cfn.version +++ b/packages/@aws-cdk/cfnspec/cfn.version @@ -1 +1 @@ -40.0.0 +40.1.0 diff --git a/packages/@aws-cdk/cfnspec/spec-source/000_CloudFormationResourceSpecification.json b/packages/@aws-cdk/cfnspec/spec-source/000_CloudFormationResourceSpecification.json index 1bb4dde7c70f4..33d10e45ecb92 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/000_CloudFormationResourceSpecification.json +++ b/packages/@aws-cdk/cfnspec/spec-source/000_CloudFormationResourceSpecification.json @@ -23470,7 +23470,13 @@ "TransitionToIA": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-efs-filesystem-lifecyclepolicy.html#cfn-efs-filesystem-lifecyclepolicy-transitiontoia", "PrimitiveType": "String", - "Required": true, + "Required": false, + "UpdateType": "Mutable" + }, + "TransitionToPrimaryStorageClass": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-efs-filesystem-lifecyclepolicy.html#cfn-efs-filesystem-lifecyclepolicy-transitiontoprimarystorageclass", + "PrimitiveType": "String", + "Required": false, "UpdateType": "Mutable" } } @@ -61896,7 +61902,7 @@ } } }, - "ResourceSpecificationVersion": "40.0.0", + "ResourceSpecificationVersion": "40.1.0", "ResourceTypes": { "AWS::ACMPCA::Certificate": { "Attributes": { @@ -67257,6 +67263,12 @@ "PrimitiveType": "String", "Required": true, "UpdateType": "Mutable" + }, + "SplitChargeRules": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ce-costcategory.html#cfn-ce-costcategory-splitchargerules", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" } } }, From b838f95f0905316fe706779381c93bedaa9ad504 Mon Sep 17 00:00:00 2001 From: Niranjan Jayakar Date: Fri, 27 Aug 2021 14:48:32 +0100 Subject: [PATCH 37/39] feat(assertions): 'not' matcher (#16240) Supply a 'not' matcher that can be used to invert the matching pattern relates #15868 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/assertions/README.md | 29 +++++ packages/@aws-cdk/assertions/lib/match.ts | 39 +++++-- .../@aws-cdk/assertions/test/match.test.ts | 101 +++++++++++++++++- 3 files changed, 155 insertions(+), 14 deletions(-) diff --git a/packages/@aws-cdk/assertions/README.md b/packages/@aws-cdk/assertions/README.md index 67109b1156964..d651ab72c1bc3 100644 --- a/packages/@aws-cdk/assertions/README.md +++ b/packages/@aws-cdk/assertions/README.md @@ -238,6 +238,35 @@ target array. Out of order will be recorded as a match failure. Alternatively, the `Match.arrayEquals()` API can be used to assert that the target is exactly equal to the pattern array. +### Not Matcher + +The not matcher inverts the search pattern and matches all patterns in the path that does +not match the pattern specified. + +```ts +// Given a template - +// { +// "Resources": { +// "MyBar": { +// "Type": "Foo::Bar", +// "Properties": { +// "Fred": ["Flob", "Cat"] +// } +// } +// } +// } + +// The following will NOT throw an assertion error +assert.hasResourceProperties('Foo::Bar', { + Fred: Match.not(['Flob']), +}); + +// The following will throw an assertion error +assert.hasResourceProperties('Foo::Bar', Match.objectLike({ + Fred: Match.not(['Flob', 'Cat']); +}}); +``` + ## Strongly typed languages Some of the APIs documented above, such as `templateMatches()` and diff --git a/packages/@aws-cdk/assertions/lib/match.ts b/packages/@aws-cdk/assertions/lib/match.ts index 2a88bada59bac..802acdc603e70 100644 --- a/packages/@aws-cdk/assertions/lib/match.ts +++ b/packages/@aws-cdk/assertions/lib/match.ts @@ -55,6 +55,14 @@ export abstract class Match { public static objectEquals(pattern: {[key: string]: any}): Matcher { return new ObjectMatch('objectEquals', pattern, { partial: false }); } + + /** + * Matches any target which does NOT follow the specified pattern. + * @param pattern the pattern to NOT match + */ + public static not(pattern: any): Matcher { + return new NotMatch('not', pattern); + } } /** @@ -82,7 +90,6 @@ class LiteralMatch extends Matcher { super(); this.partialObjects = options.partialObjects ?? false; - this.name = 'exact'; if (Matcher.isMatcher(this.pattern)) { throw new Error('LiteralMatch cannot directly contain another matcher. ' + @@ -143,11 +150,6 @@ class ArrayMatch extends Matcher { super(); this.partial = options.subsequence ?? true; - if (this.partial) { - this.name = 'arrayWith'; - } else { - this.name = 'arrayEquals'; - } } public test(actual: any): MatchResult { @@ -211,11 +213,6 @@ class ObjectMatch extends Matcher { super(); this.partial = options.partial ?? true; - if (this.partial) { - this.name = 'objectLike'; - } else { - this.name = 'objectEquals'; - } } public test(actual: any): MatchResult { @@ -254,6 +251,26 @@ class ObjectMatch extends Matcher { } } +class NotMatch extends Matcher { + constructor( + public readonly name: string, + private readonly pattern: {[key: string]: any}) { + + super(); + } + + public test(actual: any): MatchResult { + const matcher = Matcher.isMatcher(this.pattern) ? this.pattern : new LiteralMatch(this.name, this.pattern); + + const innerResult = matcher.test(actual); + const result = new MatchResult(actual); + if (innerResult.failCount === 0) { + result.push(this, [], `Found unexpected match: ${JSON.stringify(actual, undefined, 2)}`); + } + return result; + } +} + function getType(obj: any): string { return Array.isArray(obj) ? 'array' : typeof obj; } \ No newline at end of file diff --git a/packages/@aws-cdk/assertions/test/match.test.ts b/packages/@aws-cdk/assertions/test/match.test.ts index 697c46e139c11..b46eb0d53d204 100644 --- a/packages/@aws-cdk/assertions/test/match.test.ts +++ b/packages/@aws-cdk/assertions/test/match.test.ts @@ -194,17 +194,112 @@ describe('Matchers', () => { expectFailure(matcher, { foo: 'bar', baz: 'qux' }, [/Unexpected key at \/baz/]); }); }); + + describe('not()', () => { + let matcher: Matcher; + + test('literal', () => { + matcher = Match.not('foo'); + expectPass(matcher, 'bar'); + expectPass(matcher, 3); + + expectFailure(matcher, 'foo', ['Found unexpected match: "foo"']); + }); + + test('object', () => { + matcher = Match.not({ foo: 'bar' }); + expectPass(matcher, 'bar'); + expectPass(matcher, 3); + expectPass(matcher, { foo: 'baz' }); + expectPass(matcher, { bar: 'foo' }); + + const msg = [ + 'Found unexpected match: {', + ' "foo": "bar"', + '}', + ].join('\n'); + expectFailure(matcher, { foo: 'bar' }, [msg]); + }); + + test('array', () => { + matcher = Match.not(['foo', 'bar']); + expectPass(matcher, 'foo'); + expectPass(matcher, []); + expectPass(matcher, ['bar']); + expectPass(matcher, ['foo', 3]); + + const msg = [ + 'Found unexpected match: [', + ' "foo",', + ' "bar"', + ']', + ].join('\n'); + expectFailure(matcher, ['foo', 'bar'], [msg]); + }); + + test('as a nested matcher', () => { + matcher = Match.exact({ + foo: { bar: Match.not([1, 2]) }, + }); + + expectPass(matcher, { + foo: { bar: [1] }, + }); + expectPass(matcher, { + foo: { bar: ['baz'] }, + }); + + const msg = [ + 'Found unexpected match: [', + ' 1,', + ' 2', + '] at /foo/bar', + ].join('\n'); + expectFailure(matcher, { + foo: { bar: [1, 2] }, + }, [msg]); + }); + + test('with nested matcher', () => { + matcher = Match.not({ + foo: { bar: Match.arrayWith([1]) }, + }); + + expectPass(matcher, { + foo: { bar: [2] }, + }); + expectPass(matcher, 'foo'); + + const msg = [ + 'Found unexpected match: {', + ' "foo": {', + ' "bar": [', + ' 1,', + ' 2', + ' ]', + ' }', + '}', + ].join('\n'); + expectFailure(matcher, { + foo: { bar: [1, 2] }, + }, [msg]); + }); + }); }); function expectPass(matcher: Matcher, target: any): void { expect(matcher.test(target).hasFailed()).toEqual(false); } -function expectFailure(matcher: Matcher, target: any, expected: (string | RegExp)[]): void { - const actual = matcher.test(target).toHumanStrings(); +function expectFailure(matcher: Matcher, target: any, expected: (string | RegExp)[] = []): void { + const result = matcher.test(target); + expect(result.failCount).toBeGreaterThan(0); + const actual = result.toHumanStrings(); + if (expected.length > 0) { + expect(actual.length).toEqual(expected.length); + } for (let i = 0; i < expected.length; i++) { const e = expected[i]; expect(actual[i]).toMatch(e); } - expect(expected.length).toEqual(actual.length); } \ No newline at end of file From de218ba3a294b5b98f93fc75a04ce42294e95008 Mon Sep 17 00:00:00 2001 From: kaizen3031593 <36202692+kaizen3031593@users.noreply.github.com> Date: Fri, 27 Aug 2021 11:40:18 -0400 Subject: [PATCH 38/39] feat(synthetics): add Python runtime and latest Nodejs runtime (#16069) This PR addresses the fact that the current synthetics module was built to support nodejs runtimes only by opening support for python runtimes. Closes #15138 and #16177. **Breaking Changes** - `Runtime('customRuntimeHere')` becomes `Runtime('customRuntime', 'runtimeFamily')` - `Code.fromAnything('path').bind(this, 'handler')` becomes `Code.fromAnything('path').bind(this, 'handler', 'runtimeFamily')` **Whats in this PR?** - Adds latest Nodejs runtime (`syn-nodejs-puppeteer-3.2`) and updates integ test to it. - Adds generic python script to the folder `test/canaries/python` in order to run unit & integration tests on it. - Adds new `RuntimeFamily` enum that is required by the `Runtime` object to differentiate between Python and Node. - Verifies the correct folder structure for Python runtimes (`python/.py`). - Updates readme. ---- *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-synthetics/README.md | 11 +- .../@aws-cdk/aws-synthetics/lib/canary.ts | 78 +------ packages/@aws-cdk/aws-synthetics/lib/code.ts | 26 ++- packages/@aws-cdk/aws-synthetics/lib/index.ts | 1 + .../@aws-cdk/aws-synthetics/lib/runtime.ts | 123 +++++++++++ .../test/canaries/python/canary.py | 61 ++++++ .../aws-synthetics/test/canary.test.ts | 21 +- .../@aws-cdk/aws-synthetics/test/code.test.ts | 59 ++++-- .../test/integ.canary.expected.json | 191 ++++++++++++++++-- .../aws-synthetics/test/integ.canary.ts | 15 +- 10 files changed, 471 insertions(+), 115 deletions(-) create mode 100644 packages/@aws-cdk/aws-synthetics/lib/runtime.ts create mode 100644 packages/@aws-cdk/aws-synthetics/test/canaries/python/canary.py diff --git a/packages/@aws-cdk/aws-synthetics/README.md b/packages/@aws-cdk/aws-synthetics/README.md index 6dcd911f7da5e..bd4fcaede272c 100644 --- a/packages/@aws-cdk/aws-synthetics/README.md +++ b/packages/@aws-cdk/aws-synthetics/README.md @@ -130,7 +130,7 @@ new synthetics.Canary(this, 'Bucket Canary', { }); ``` -> **Note:** For `code.fromAsset()` and `code.fromBucket()`, the canary resource requires the following folder structure: +> **Note:** Synthetics have a specified folder structure for canaries. For Node scripts supplied via `code.fromAsset()` or `code.fromBucket()`, the canary resource requires the following folder structure: > > ```plaintext > canary/ @@ -139,6 +139,15 @@ new synthetics.Canary(this, 'Bucket Canary', { > ├── .js > ``` > +> +> For Python scripts supplied via `code.fromAsset()` or `code.fromBucket()`, the canary resource requires the following folder structure: +> +> ```plaintext +> canary/ +> ├── python/ +> ├── .py +> ``` +> > See Synthetics [docs](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Canaries_WritingCanary.html). ### Alarms diff --git a/packages/@aws-cdk/aws-synthetics/lib/canary.ts b/packages/@aws-cdk/aws-synthetics/lib/canary.ts index e5061c4c999c2..c512c4d636a78 100644 --- a/packages/@aws-cdk/aws-synthetics/lib/canary.ts +++ b/packages/@aws-cdk/aws-synthetics/lib/canary.ts @@ -5,6 +5,7 @@ import * as s3 from '@aws-cdk/aws-s3'; import * as cdk from '@aws-cdk/core'; import { Construct } from 'constructs'; import { Code } from './code'; +import { Runtime } from './runtime'; import { Schedule } from './schedule'; import { CloudWatchSyntheticsMetrics } from './synthetics-canned-metrics.generated'; import { CfnCanary } from './synthetics.generated'; @@ -64,81 +65,6 @@ export interface CustomTestOptions { readonly handler: string, } -/** - * Runtime options for a canary - */ -export class Runtime { - /** - * `syn-1.0` includes the following: - * - * - Synthetics library 1.0 - * - Synthetics handler code 1.0 - * - Lambda runtime Node.js 10.x - * - Puppeteer-core version 1.14.0 - * - The Chromium version that matches Puppeteer-core 1.14.0 - * - * @see https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Library_nodejs_puppeteer.html#CloudWatch_Synthetics_runtimeversion-1.0 - */ - public static readonly SYNTHETICS_1_0 = new Runtime('syn-1.0'); - - /** - * `syn-nodejs-2.0` includes the following: - * - Lambda runtime Node.js 10.x - * - Puppeteer-core version 3.3.0 - * - Chromium version 83.0.4103.0 - * - * @see https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Library_nodejs_puppeteer.html#CloudWatch_Synthetics_runtimeversion-2.0 - */ - public static readonly SYNTHETICS_NODEJS_2_0 = new Runtime('syn-nodejs-2.0'); - - - /** - * `syn-nodejs-2.1` includes the following: - * - Lambda runtime Node.js 10.x - * - Puppeteer-core version 3.3.0 - * - Chromium version 83.0.4103.0 - * - * @see https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Library_nodejs_puppeteer.html#CloudWatch_Synthetics_runtimeversion-2.1 - */ - public static readonly SYNTHETICS_NODEJS_2_1 = new Runtime('syn-nodejs-2.1'); - - /** - * `syn-nodejs-2.2` includes the following: - * - Lambda runtime Node.js 10.x - * - Puppeteer-core version 3.3.0 - * - Chromium version 83.0.4103.0 - * - * @see https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Library_nodejs_puppeteer.html#CloudWatch_Synthetics_runtimeversion-2.2 - */ - public static readonly SYNTHETICS_NODEJS_2_2 = new Runtime('syn-nodejs-2.2'); - - /** - * `syn-nodejs-puppeteer-3.0` includes the following: - * - Lambda runtime Node.js 12.x - * - Puppeteer-core version 5.5.0 - * - Chromium version 88.0.4298.0 - * - * @see https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Library_nodejs_puppeteer.html#CloudWatch_Synthetics_runtimeversion-nodejs-puppeteer-3.0 - */ - public static readonly SYNTHETICS_NODEJS_PUPPETEER_3_0 = new Runtime('syn-nodejs-puppeteer-3.0'); - - /** - * `syn-nodejs-puppeteer-3.1` includes the following: - * - Lambda runtime Node.js 12.x - * - Puppeteer-core version 5.5.0 - * - Chromium version 88.0.4298.0 - * - * @see https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Library_nodejs_puppeteer.html#CloudWatch_Synthetics_runtimeversion-nodejs-puppeteer-3.1 - */ - public static readonly SYNTHETICS_NODEJS_PUPPETEER_3_1 = new Runtime('syn-nodejs-puppeteer-3.1'); - - /** - * @param name The name of the runtime version - */ - public constructor(public readonly name: string) { - } -} - /** * Options for specifying the s3 location that stores the data of each canary run. The artifacts bucket location **cannot** * be updated once the canary is created. @@ -398,7 +324,7 @@ export class Canary extends cdk.Resource { private createCode(props: CanaryProps): CfnCanary.CodeProperty { const codeConfig = { handler: props.test.handler, - ...props.test.code.bind(this, props.test.handler), + ...props.test.code.bind(this, props.test.handler, props.runtime.family), }; return { handler: codeConfig.handler, diff --git a/packages/@aws-cdk/aws-synthetics/lib/code.ts b/packages/@aws-cdk/aws-synthetics/lib/code.ts index dd75815098ec1..9eef28d4674c6 100644 --- a/packages/@aws-cdk/aws-synthetics/lib/code.ts +++ b/packages/@aws-cdk/aws-synthetics/lib/code.ts @@ -2,7 +2,8 @@ import * as fs from 'fs'; import * as path from 'path'; import * as s3 from '@aws-cdk/aws-s3'; import * as s3_assets from '@aws-cdk/aws-s3-assets'; -import { Construct } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { RuntimeFamily } from './runtime'; /** * The code the canary should execute @@ -56,7 +57,7 @@ export abstract class Code { * * @returns a bound `CodeConfig`. */ - public abstract bind(scope: Construct, handler: string): CodeConfig; + public abstract bind(scope: Construct, handler: string, family: RuntimeFamily): CodeConfig; } /** @@ -95,8 +96,8 @@ export class AssetCode extends Code { } } - public bind(scope: Construct, handler: string): CodeConfig { - this.validateCanaryAsset(handler); + public bind(scope: Construct, handler: string, family: RuntimeFamily): CodeConfig { + this.validateCanaryAsset(handler, family); // If the same AssetCode is used multiple times, retain only the first instantiation. if (!this.asset) { @@ -126,14 +127,19 @@ export class AssetCode extends Code { * * @param handler the canary handler */ - private validateCanaryAsset(handler: string) { + private validateCanaryAsset(handler: string, family: RuntimeFamily) { if (path.extname(this.assetPath) !== '.zip') { if (!fs.lstatSync(this.assetPath).isDirectory()) { throw new Error(`Asset must be a .zip file or a directory (${this.assetPath})`); } - const filename = `${handler.split('.')[0]}.js`; - if (!fs.existsSync(path.join(this.assetPath, 'nodejs', 'node_modules', filename))) { - throw new Error(`The canary resource requires that the handler is present at "nodejs/node_modules/${filename}" but not found at ${this.assetPath} (https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Canaries_WritingCanary.html#CloudWatch_Synthetics_Canaries_write_from_scratch)`); + const filename = handler.split('.')[0]; + const nodeFilename = `${filename}.js`; + const pythonFilename = `${filename}.py`; + if (family === RuntimeFamily.NODEJS && !fs.existsSync(path.join(this.assetPath, 'nodejs', 'node_modules', nodeFilename))) { + throw new Error(`The canary resource requires that the handler is present at "nodejs/node_modules/${nodeFilename}" but not found at ${this.assetPath} (https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Canaries_WritingCanary_Nodejs.html)`); + } + if (family === RuntimeFamily.PYTHON && !fs.existsSync(path.join(this.assetPath, 'python', pythonFilename))) { + throw new Error(`The canary resource requires that the handler is present at "python/${pythonFilename}" but not found at ${this.assetPath} (https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Canaries_WritingCanary_Python.html)`); } } } @@ -151,7 +157,7 @@ export class InlineCode extends Code { } } - public bind(_scope: Construct, handler: string): CodeConfig { + public bind(_scope: Construct, handler: string, _family: RuntimeFamily): CodeConfig { if (handler !== 'index.handler') { throw new Error(`The handler for inline code must be "index.handler" (got "${handler}")`); @@ -171,7 +177,7 @@ export class S3Code extends Code { super(); } - public bind(_scope: Construct, _handler: string): CodeConfig { + public bind(_scope: Construct, _handler: string, _family: RuntimeFamily): CodeConfig { return { s3Location: { bucketName: this.bucket.bucketName, diff --git a/packages/@aws-cdk/aws-synthetics/lib/index.ts b/packages/@aws-cdk/aws-synthetics/lib/index.ts index f769a0309352e..ee024834f2bf1 100644 --- a/packages/@aws-cdk/aws-synthetics/lib/index.ts +++ b/packages/@aws-cdk/aws-synthetics/lib/index.ts @@ -1,5 +1,6 @@ export * from './canary'; export * from './code'; +export * from './runtime'; export * from './schedule'; // AWS::Synthetics CloudFormation Resources: diff --git a/packages/@aws-cdk/aws-synthetics/lib/runtime.ts b/packages/@aws-cdk/aws-synthetics/lib/runtime.ts new file mode 100644 index 0000000000000..c710d68a34e35 --- /dev/null +++ b/packages/@aws-cdk/aws-synthetics/lib/runtime.ts @@ -0,0 +1,123 @@ +/** + * All known Lambda runtime families. + */ +export enum RuntimeFamily { + /** + * All Lambda runtimes that depend on Node.js. + */ + NODEJS, + + /** + * All lambda runtimes that depend on Python. + */ + PYTHON, + + /** + * Any future runtime family. + */ + OTHER, +} + +/** + * Runtime options for a canary + */ +export class Runtime { + /** + * **Deprecated by AWS Synthetics. You can't create canaries with deprecated runtimes.** + * + * `syn-1.0` includes the following: + * + * - Synthetics library 1.0 + * - Synthetics handler code 1.0 + * - Lambda runtime Node.js 10.x + * - Puppeteer-core version 1.14.0 + * - The Chromium version that matches Puppeteer-core 1.14.0 + * + * @see https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Library_nodejs_puppeteer.html#CloudWatch_Synthetics_runtimeversion-1.0 + */ + public static readonly SYNTHETICS_1_0 = new Runtime('syn-1.0', RuntimeFamily.NODEJS); + + /** + * **Deprecated by AWS Synthetics. You can't create canaries with deprecated runtimes.** + * + * `syn-nodejs-2.0` includes the following: + * - Lambda runtime Node.js 10.x + * - Puppeteer-core version 3.3.0 + * - Chromium version 83.0.4103.0 + * + * @see https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Library_nodejs_puppeteer.html#CloudWatch_Synthetics_runtimeversion-2.0 + */ + public static readonly SYNTHETICS_NODEJS_2_0 = new Runtime('syn-nodejs-2.0', RuntimeFamily.NODEJS); + + + /** + * **Deprecated by AWS Synthetics. You can't create canaries with deprecated runtimes.** + * + * `syn-nodejs-2.1` includes the following: + * - Lambda runtime Node.js 10.x + * - Puppeteer-core version 3.3.0 + * - Chromium version 83.0.4103.0 + * + * @see https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Library_nodejs_puppeteer.html#CloudWatch_Synthetics_runtimeversion-2.1 + */ + public static readonly SYNTHETICS_NODEJS_2_1 = new Runtime('syn-nodejs-2.1', RuntimeFamily.NODEJS); + + /** + * **Deprecated by AWS Synthetics. You can't create canaries with deprecated runtimes.** + * + * `syn-nodejs-2.2` includes the following: + * - Lambda runtime Node.js 10.x + * - Puppeteer-core version 3.3.0 + * - Chromium version 83.0.4103.0 + * + * @see https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Library_nodejs_puppeteer.html#CloudWatch_Synthetics_runtimeversion-2.2 + */ + public static readonly SYNTHETICS_NODEJS_2_2 = new Runtime('syn-nodejs-2.2', RuntimeFamily.NODEJS); + + /** + * `syn-nodejs-puppeteer-3.0` includes the following: + * - Lambda runtime Node.js 12.x + * - Puppeteer-core version 5.5.0 + * - Chromium version 88.0.4298.0 + * + * @see https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Library_nodejs_puppeteer.html#CloudWatch_Synthetics_runtimeversion-nodejs-puppeteer-3.0 + */ + public static readonly SYNTHETICS_NODEJS_PUPPETEER_3_0 = new Runtime('syn-nodejs-puppeteer-3.0', RuntimeFamily.NODEJS); + + /** + * `syn-nodejs-puppeteer-3.1` includes the following: + * - Lambda runtime Node.js 12.x + * - Puppeteer-core version 5.5.0 + * - Chromium version 88.0.4298.0 + * + * @see https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Library_nodejs_puppeteer.html#CloudWatch_Synthetics_runtimeversion-nodejs-puppeteer-3.1 + */ + public static readonly SYNTHETICS_NODEJS_PUPPETEER_3_1 = new Runtime('syn-nodejs-puppeteer-3.1', RuntimeFamily.NODEJS); + + /** + * `syn-nodejs-puppeteer-3.2` includes the following: + * - Lambda runtime Node.js 12.x + * - Puppeteer-core version 5.5.0 + * - Chromium version 88.0.4298.0 + * + * @see https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Library_nodejs_puppeteer.html#CloudWatch_Synthetics_runtimeversion-nodejs-puppeteer-3.2 + */ + public static readonly SYNTHETICS_NODEJS_PUPPETEER_3_2 = new Runtime('syn-nodejs-puppeteer-3.2', RuntimeFamily.NODEJS); + + /** + * `syn-python-selenium-1.0` includes the following: + * - Lambda runtime Python 3.8 + * - Selenium version 3.141.0 + * - Chromium version 83.0.4103.0 + * + * @see https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Library_python_selenium.html + */ + public static readonly SYNTHETICS_PYTHON_SELENIUM_1_0 = new Runtime('syn-python-selenium-1.0', RuntimeFamily.PYTHON); + + /** + * @param name The name of the runtime version + * @param family The Lambda runtime family + */ + public constructor(public readonly name: string, public readonly family: RuntimeFamily) { + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-synthetics/test/canaries/python/canary.py b/packages/@aws-cdk/aws-synthetics/test/canaries/python/canary.py new file mode 100644 index 0000000000000..2dbed4e312afe --- /dev/null +++ b/packages/@aws-cdk/aws-synthetics/test/canaries/python/canary.py @@ -0,0 +1,61 @@ +# This example comes from the AWS Synthetics service console "API canary" blueprint + +import json +import http.client +import urllib.parse +from aws_synthetics.selenium import synthetics_webdriver as syn_webdriver +from aws_synthetics.common import synthetics_logger as logger + + +def verify_request(method, url, post_data=None, headers={}): + parsed_url = urllib.parse.urlparse(url) + user_agent = str(syn_webdriver.get_canary_user_agent_string()) + if "User-Agent" in headers: + headers["User-Agent"] = " ".join([user_agent, headers["User-Agent"]]) + else: + headers["User-Agent"] = "{}".format(user_agent) + + logger.info("Making request with Method: '%s' URL: %s: Data: %s Headers: %s" % ( + method, url, json.dumps(post_data), json.dumps(headers))) + + if parsed_url.scheme == "https": + conn = http.client.HTTPSConnection(parsed_url.hostname, parsed_url.port) + else: + conn = http.client.HTTPConnection(parsed_url.hostname, parsed_url.port) + + conn.request(method, url, str(post_data), headers) + response = conn.getresponse() + logger.info("Status Code: %s " % response.status) + logger.info("Response Headers: %s" % json.dumps(response.headers.as_string())) + + if not response.status or response.status < 200 or response.status > 299: + try: + logger.error("Response: %s" % response.read().decode()) + finally: + if response.reason: + conn.close() + raise Exception("Failed: %s" % response.reason) + else: + conn.close() + raise Exception("Failed with status code: %s" % response.status) + + logger.info("Response: %s" % response.read().decode()) + logger.info("HTTP request successfully executed") + conn.close() + + +def main(): + + url = 'https://example.com/' + method = 'GET' + postData = "" + headers = {} + + verify_request(method, url, None, headers) + + logger.info("Canary successfully executed") + + +def handler(event, context): + logger.info("Selenium Python API canary") + main() \ No newline at end of file diff --git a/packages/@aws-cdk/aws-synthetics/test/canary.test.ts b/packages/@aws-cdk/aws-synthetics/test/canary.test.ts index 8a0baa8cb0c29..c4583ef5494cf 100644 --- a/packages/@aws-cdk/aws-synthetics/test/canary.test.ts +++ b/packages/@aws-cdk/aws-synthetics/test/canary.test.ts @@ -169,6 +169,25 @@ test('Runtime can be specified', () => { }); }); +test('Python runtime can be specified', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + new synthetics.Canary(stack, 'Canary', { + runtime: synthetics.Runtime.SYNTHETICS_PYTHON_SELENIUM_1_0, + test: synthetics.Test.custom({ + handler: 'index.handler', + code: synthetics.Code.fromInline('# Synthetics handler code'), + }), + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::Synthetics::Canary', { + RuntimeVersion: 'syn-python-selenium-1.0', + }); +}); + test('environment variables can be specified', () => { // GIVEN const stack = new Stack(); @@ -220,7 +239,7 @@ test('Runtime can be customized', () => { // WHEN new synthetics.Canary(stack, 'Canary', { - runtime: new synthetics.Runtime('fancy-future-runtime-1337.42'), + runtime: new synthetics.Runtime('fancy-future-runtime-1337.42', synthetics.RuntimeFamily.OTHER), test: synthetics.Test.custom({ handler: 'index.handler', code: synthetics.Code.fromInline('/* Synthetics handler code */'), diff --git a/packages/@aws-cdk/aws-synthetics/test/code.test.ts b/packages/@aws-cdk/aws-synthetics/test/code.test.ts index 95c7883b42dbf..84bb7ec61ded1 100644 --- a/packages/@aws-cdk/aws-synthetics/test/code.test.ts +++ b/packages/@aws-cdk/aws-synthetics/test/code.test.ts @@ -3,6 +3,7 @@ import { Template } from '@aws-cdk/assertions'; import * as s3 from '@aws-cdk/aws-s3'; import { App, Stack } from '@aws-cdk/core'; import * as synthetics from '../lib'; +import { RuntimeFamily } from '../lib'; describe(synthetics.Code.fromInline, () => { test('fromInline works', () => { @@ -16,7 +17,7 @@ describe(synthetics.Code.fromInline, () => { };`); // THEN - expect(inline.bind(stack, 'index.handler').inlineCode).toEqual(` + expect(inline.bind(stack, 'index.handler', RuntimeFamily.NODEJS).inlineCode).toEqual(` exports.handler = async () => { console.log(\'hello world\'); };`); @@ -32,13 +33,13 @@ describe(synthetics.Code.fromInline, () => { const stack = new Stack(new App(), 'canaries'); // THEN - expect(() => synthetics.Code.fromInline('code').bind(stack, 'canary.handler')) + expect(() => synthetics.Code.fromInline('code').bind(stack, 'canary.handler', RuntimeFamily.NODEJS)) .toThrowError('The handler for inline code must be "index.handler" (got "canary.handler")'); }); }); describe(synthetics.Code.fromAsset, () => { - test('fromAsset works', () => { + test('fromAsset works for node runtimes', () => { // GIVEN const stack = new Stack(new App(), 'canaries'); @@ -56,8 +57,32 @@ describe(synthetics.Code.fromAsset, () => { Template.fromStack(stack).hasResourceProperties('AWS::Synthetics::Canary', { Code: { Handler: 'canary.handler', - S3Bucket: stack.resolve(directoryAsset.bind(stack, 'canary.handler').s3Location?.bucketName), - S3Key: stack.resolve(directoryAsset.bind(stack, 'canary.handler').s3Location?.objectKey), + S3Bucket: stack.resolve(directoryAsset.bind(stack, 'canary.handler', synthetics.RuntimeFamily.NODEJS).s3Location?.bucketName), + S3Key: stack.resolve(directoryAsset.bind(stack, 'canary.handler', synthetics.RuntimeFamily.NODEJS).s3Location?.objectKey), + }, + }); + }); + + test('fromAsset works for python runtimes', () => { + // GIVEN + const stack = new Stack(new App(), 'canaries'); + + // WHEN + const directoryAsset = synthetics.Code.fromAsset(path.join(__dirname, 'canaries')); + new synthetics.Canary(stack, 'Canary', { + test: synthetics.Test.custom({ + handler: 'canary.handler', + code: directoryAsset, + }), + runtime: synthetics.Runtime.SYNTHETICS_PYTHON_SELENIUM_1_0, + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::Synthetics::Canary', { + Code: { + Handler: 'canary.handler', + S3Bucket: stack.resolve(directoryAsset.bind(stack, 'canary.handler', synthetics.RuntimeFamily.PYTHON).s3Location?.bucketName), + S3Key: stack.resolve(directoryAsset.bind(stack, 'canary.handler', synthetics.RuntimeFamily.PYTHON).s3Location?.objectKey), }, }); }); @@ -104,18 +129,28 @@ describe(synthetics.Code.fromAsset, () => { // THEN const assetPath = path.join(__dirname, 'canaries', 'nodejs', 'node_modules', 'canary.js'); - expect(() => synthetics.Code.fromAsset(assetPath).bind(stack, 'canary.handler')) + expect(() => synthetics.Code.fromAsset(assetPath).bind(stack, 'canary.handler', synthetics.RuntimeFamily.NODEJS)) .toThrowError(`Asset must be a .zip file or a directory (${assetPath})`); }); - test('fails if "nodejs/node_modules" folder structure not used', () => { + test('fails if node runtime and "nodejs/node_modules" folder structure not used', () => { // GIVEN const stack = new Stack(new App(), 'canaries'); // THEN const assetPath = path.join(__dirname, 'canaries', 'nodejs', 'node_modules'); - expect(() => synthetics.Code.fromAsset(assetPath).bind(stack, 'canary.handler')) - .toThrowError(`The canary resource requires that the handler is present at "nodejs/node_modules/canary.js" but not found at ${assetPath} (https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Canaries_WritingCanary.html#CloudWatch_Synthetics_Canaries_write_from_scratch)`); + expect(() => synthetics.Code.fromAsset(assetPath).bind(stack, 'canary.handler', synthetics.RuntimeFamily.NODEJS)) + .toThrowError(`The canary resource requires that the handler is present at "nodejs/node_modules/canary.js" but not found at ${assetPath} (https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Canaries_WritingCanary_Nodejs.html)`); + }); + + test('fails if python runtime and "python" folder structure not used', () => { + // GIVEN + const stack = new Stack(new App(), 'canaries'); + + // THEN + const assetPath = path.join(__dirname, 'canaries', 'python'); + expect(() => synthetics.Code.fromAsset(assetPath).bind(stack, 'canary.handler', synthetics.RuntimeFamily.PYTHON)) + .toThrowError(`The canary resource requires that the handler is present at "python/canary.py" but not found at ${assetPath} (https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Canaries_WritingCanary_Python.html)`); }); test('fails if handler is specified incorrectly', () => { @@ -124,8 +159,8 @@ describe(synthetics.Code.fromAsset, () => { // THEN const assetPath = path.join(__dirname, 'canaries', 'nodejs', 'node_modules'); - expect(() => synthetics.Code.fromAsset(assetPath).bind(stack, 'incorrect.handler')) - .toThrowError(`The canary resource requires that the handler is present at "nodejs/node_modules/incorrect.js" but not found at ${assetPath} (https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Canaries_WritingCanary.html#CloudWatch_Synthetics_Canaries_write_from_scratch)`); + expect(() => synthetics.Code.fromAsset(assetPath).bind(stack, 'incorrect.handler', synthetics.RuntimeFamily.NODEJS)) + .toThrowError(`The canary resource requires that the handler is present at "nodejs/node_modules/incorrect.js" but not found at ${assetPath} (https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Canaries_WritingCanary_Nodejs.html)`); }); }); @@ -138,7 +173,7 @@ describe(synthetics.Code.fromBucket, () => { // WHEN const code = synthetics.Code.fromBucket(bucket, 'code.js'); - const codeConfig = code.bind(stack, 'code.handler'); + const codeConfig = code.bind(stack, 'code.handler', RuntimeFamily.NODEJS); // THEN expect(codeConfig.s3Location?.bucketName).toEqual(bucket.bucketName); diff --git a/packages/@aws-cdk/aws-synthetics/test/integ.canary.expected.json b/packages/@aws-cdk/aws-synthetics/test/integ.canary.expected.json index 58412fee9bfbb..70d3908aa07f6 100644 --- a/packages/@aws-cdk/aws-synthetics/test/integ.canary.expected.json +++ b/packages/@aws-cdk/aws-synthetics/test/integ.canary.expected.json @@ -114,7 +114,7 @@ ] }, "Name": "canary-integ", - "RuntimeVersion": "syn-nodejs-2.0", + "RuntimeVersion": "syn-nodejs-puppeteer-3.2", "Schedule": { "DurationInSeconds": "0", "Expression": "rate(1 minute)" @@ -238,7 +238,7 @@ "Code": { "Handler": "canary.handler", "S3Bucket": { - "Ref": "AssetParameters5bf46c83158ab3b336aba1449c21b02cbac2ccea621f17d842593bb39e3e529bS3Bucket58589EB6" + "Ref": "AssetParameters9d00e437db1f5f8788ce938a3f00a9a1b946820e78c9b4c36207c8475db882bbS3Bucket59F507C2" }, "S3Key": { "Fn::Join": [ @@ -251,7 +251,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters5bf46c83158ab3b336aba1449c21b02cbac2ccea621f17d842593bb39e3e529bS3VersionKey8FF13E90" + "Ref": "AssetParameters9d00e437db1f5f8788ce938a3f00a9a1b946820e78c9b4c36207c8475db882bbS3VersionKeyEFB5FFF8" } ] } @@ -264,7 +264,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters5bf46c83158ab3b336aba1449c21b02cbac2ccea621f17d842593bb39e3e529bS3VersionKey8FF13E90" + "Ref": "AssetParameters9d00e437db1f5f8788ce938a3f00a9a1b946820e78c9b4c36207c8475db882bbS3VersionKeyEFB5FFF8" } ] } @@ -281,7 +281,7 @@ ] }, "Name": "assetcanary-one", - "RuntimeVersion": "syn-nodejs-2.0", + "RuntimeVersion": "syn-nodejs-puppeteer-3.2", "Schedule": { "DurationInSeconds": "0", "Expression": "rate(5 minutes)" @@ -448,7 +448,174 @@ ] }, "Name": "assetcanary-two", - "RuntimeVersion": "syn-nodejs-2.0", + "RuntimeVersion": "syn-nodejs-puppeteer-3.2", + "Schedule": { + "DurationInSeconds": "0", + "Expression": "rate(5 minutes)" + }, + "StartCanaryAfterCreation": true + } + }, + "MyPythonCanaryArtifactsBucket7AE88133": { + "Type": "AWS::S3::Bucket", + "Properties": { + "BucketEncryption": { + "ServerSideEncryptionConfiguration": [ + { + "ServerSideEncryptionByDefault": { + "SSEAlgorithm": "aws:kms" + } + } + ] + } + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "MyPythonCanaryServiceRole41A363E1": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": "s3:ListAllMyBuckets", + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": [ + "s3:PutObject", + "s3:GetBucketLocation" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "MyPythonCanaryArtifactsBucket7AE88133", + "Arn" + ] + }, + "/*" + ] + ] + } + }, + { + "Action": "cloudwatch:PutMetricData", + "Condition": { + "StringEquals": { + "cloudwatch:namespace": "CloudWatchSynthetics" + } + }, + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": [ + "logs:CreateLogStream", + "logs:CreateLogGroup", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:::*" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "canaryPolicy" + } + ] + } + }, + "MyPythonCanary9A3DE09E": { + "Type": "AWS::Synthetics::Canary", + "Properties": { + "ArtifactS3Location": { + "Fn::Join": [ + "", + [ + "s3://", + { + "Ref": "MyPythonCanaryArtifactsBucket7AE88133" + } + ] + ] + }, + "Code": { + "Handler": "canary.handler", + "S3Bucket": { + "Ref": "AssetParameters9d00e437db1f5f8788ce938a3f00a9a1b946820e78c9b4c36207c8475db882bbS3Bucket59F507C2" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters9d00e437db1f5f8788ce938a3f00a9a1b946820e78c9b4c36207c8475db882bbS3VersionKeyEFB5FFF8" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters9d00e437db1f5f8788ce938a3f00a9a1b946820e78c9b4c36207c8475db882bbS3VersionKeyEFB5FFF8" + } + ] + } + ] + } + ] + ] + } + }, + "ExecutionRoleArn": { + "Fn::GetAtt": [ + "MyPythonCanaryServiceRole41A363E1", + "Arn" + ] + }, + "Name": "py-canary-integ", + "RuntimeVersion": "syn-python-selenium-1.0", "Schedule": { "DurationInSeconds": "0", "Expression": "rate(5 minutes)" @@ -458,17 +625,17 @@ } }, "Parameters": { - "AssetParameters5bf46c83158ab3b336aba1449c21b02cbac2ccea621f17d842593bb39e3e529bS3Bucket58589EB6": { + "AssetParameters9d00e437db1f5f8788ce938a3f00a9a1b946820e78c9b4c36207c8475db882bbS3Bucket59F507C2": { "Type": "String", - "Description": "S3 bucket for asset \"5bf46c83158ab3b336aba1449c21b02cbac2ccea621f17d842593bb39e3e529b\"" + "Description": "S3 bucket for asset \"9d00e437db1f5f8788ce938a3f00a9a1b946820e78c9b4c36207c8475db882bb\"" }, - "AssetParameters5bf46c83158ab3b336aba1449c21b02cbac2ccea621f17d842593bb39e3e529bS3VersionKey8FF13E90": { + "AssetParameters9d00e437db1f5f8788ce938a3f00a9a1b946820e78c9b4c36207c8475db882bbS3VersionKeyEFB5FFF8": { "Type": "String", - "Description": "S3 key for asset version \"5bf46c83158ab3b336aba1449c21b02cbac2ccea621f17d842593bb39e3e529b\"" + "Description": "S3 key for asset version \"9d00e437db1f5f8788ce938a3f00a9a1b946820e78c9b4c36207c8475db882bb\"" }, - "AssetParameters5bf46c83158ab3b336aba1449c21b02cbac2ccea621f17d842593bb39e3e529bArtifactHash74DCED3D": { + "AssetParameters9d00e437db1f5f8788ce938a3f00a9a1b946820e78c9b4c36207c8475db882bbArtifactHash0E2FE9D4": { "Type": "String", - "Description": "Artifact hash for asset \"5bf46c83158ab3b336aba1449c21b02cbac2ccea621f17d842593bb39e3e529b\"" + "Description": "Artifact hash for asset \"9d00e437db1f5f8788ce938a3f00a9a1b946820e78c9b4c36207c8475db882bb\"" }, "AssetParametersb1b777dcb79a2fa2790059927207d10bf5f4747d6dd1516e2780726d9d6fa820S3Bucket705C3761": { "Type": "String", diff --git a/packages/@aws-cdk/aws-synthetics/test/integ.canary.ts b/packages/@aws-cdk/aws-synthetics/test/integ.canary.ts index cbb3a505889ad..54822badf1c99 100644 --- a/packages/@aws-cdk/aws-synthetics/test/integ.canary.ts +++ b/packages/@aws-cdk/aws-synthetics/test/integ.canary.ts @@ -29,7 +29,7 @@ new synthetics.Canary(stack, 'MyCanary', { }), schedule: synthetics.Schedule.rate(cdk.Duration.minutes(1)), artifactsBucketLocation: { bucket, prefix }, - runtime: synthetics.Runtime.SYNTHETICS_NODEJS_2_0, + runtime: synthetics.Runtime.SYNTHETICS_NODEJS_PUPPETEER_3_2, }); new synthetics.Canary(stack, 'MyCanaryOne', { @@ -38,7 +38,7 @@ new synthetics.Canary(stack, 'MyCanaryOne', { handler: 'canary.handler', code: synthetics.Code.fromAsset(path.join(__dirname, 'canaries')), }), - runtime: synthetics.Runtime.SYNTHETICS_NODEJS_2_0, + runtime: synthetics.Runtime.SYNTHETICS_NODEJS_PUPPETEER_3_2, }); new synthetics.Canary(stack, 'MyCanaryTwo', { @@ -47,7 +47,16 @@ new synthetics.Canary(stack, 'MyCanaryTwo', { handler: 'canary.handler', code: synthetics.Code.fromAsset(path.join(__dirname, 'canary.zip')), }), - runtime: synthetics.Runtime.SYNTHETICS_NODEJS_2_0, + runtime: synthetics.Runtime.SYNTHETICS_NODEJS_PUPPETEER_3_2, +}); + +new synthetics.Canary(stack, 'MyPythonCanary', { + canaryName: 'py-canary-integ', + test: synthetics.Test.custom({ + handler: 'canary.handler', + code: synthetics.Code.fromAsset(path.join(__dirname, 'canaries')), + }), + runtime: synthetics.Runtime.SYNTHETICS_PYTHON_SELENIUM_1_0, }); app.synth(); From a85ad392459c815d5c8e645dd3e8240d059024e6 Mon Sep 17 00:00:00 2001 From: Massimo Prencipe Date: Fri, 27 Aug 2021 19:29:36 +0300 Subject: [PATCH 39/39] fix(elasticloadbalancingv2): target group health check does not validate interval versus timeout (#16107) fix: Add validation to target group health check creation. Fixes issue #3703. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../lib/shared/base-target-group.ts | 5 +++ .../test/alb/listener.test.ts | 12 +++--- .../test/alb/target-group.test.ts | 41 +++++++++++++++++++ 3 files changed, 52 insertions(+), 6 deletions(-) diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-target-group.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-target-group.ts index 175f63ddc4d3d..96acd45b34a4c 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-target-group.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-target-group.ts @@ -297,6 +297,11 @@ export abstract class TargetGroupBase extends CoreConstruct implements ITargetGr * Set/replace the target group's health check */ public configureHealthCheck(healthCheck: HealthCheck) { + if (healthCheck.interval && healthCheck.timeout) { + if (healthCheck.interval.toMilliseconds() <= healthCheck.timeout.toMilliseconds()) { + throw new Error(`Healthcheck interval ${healthCheck.interval.toHumanString()} must be greater than the timeout ${healthCheck.timeout.toHumanString()}`); + } + } this.healthCheck = healthCheck; } diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/listener.test.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/listener.test.ts index 2d945aeb2621f..885c872482e9c 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/listener.test.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/listener.test.ts @@ -437,17 +437,17 @@ describe('tests', () => { }); group.configureHealthCheck({ unhealthyThresholdCount: 3, - timeout: cdk.Duration.hours(1), - interval: cdk.Duration.seconds(30), + timeout: cdk.Duration.seconds(30), + interval: cdk.Duration.seconds(60), path: '/test', }); // THEN expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { UnhealthyThresholdCount: 3, - HealthCheckIntervalSeconds: 30, + HealthCheckIntervalSeconds: 60, HealthCheckPath: '/test', - HealthCheckTimeoutSeconds: 3600, + HealthCheckTimeoutSeconds: 30, }); }); @@ -466,8 +466,8 @@ describe('tests', () => { group.configureHealthCheck({ unhealthyThresholdCount: 3, - timeout: cdk.Duration.hours(1), - interval: cdk.Duration.seconds(30), + timeout: cdk.Duration.seconds(30), + interval: cdk.Duration.seconds(60), path: '/test', protocol: elbv2.Protocol.TCP, }); diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/target-group.test.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/target-group.test.ts index 97c36c33ce237..f1a3db5eb9508 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/target-group.test.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/target-group.test.ts @@ -281,4 +281,45 @@ describe('tests', () => { }).toThrow(/Slow start duration value must be between 30 and 900 seconds./); }); }); + + test('Interval equal to timeout', () => { + // GIVEN + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'Stack'); + const vpc = new ec2.Vpc(stack, 'VPC', {}); + + // WHEN + const tg = new elbv2.ApplicationTargetGroup(stack, 'TargetGroup', { + vpc, + }); + + // THEN + expect(() => { + tg.configureHealthCheck({ + interval: cdk.Duration.seconds(60), + timeout: cdk.Duration.seconds(60), + }); + }).toThrow(/Healthcheck interval 1 minute must be greater than the timeout 1 minute/); + }); + + test('Interval smaller than timeout', () => { + // GIVEN + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'Stack'); + const vpc = new ec2.Vpc(stack, 'VPC', {}); + + // WHEN + const tg = new elbv2.ApplicationTargetGroup(stack, 'TargetGroup', { + vpc, + }); + + // THEN + expect(() => { + tg.configureHealthCheck({ + interval: cdk.Duration.seconds(60), + timeout: cdk.Duration.seconds(120), + }); + }).toThrow(/Healthcheck interval 1 minute must be greater than the timeout 2 minutes/); + }); + });