diff --git a/.github/workflows/auto-approve.yml b/.github/workflows/auto-approve.yml index 1f596f1a62de4..1717e8fc6111f 100644 --- a/.github/workflows/auto-approve.yml +++ b/.github/workflows/auto-approve.yml @@ -12,6 +12,6 @@ jobs: permissions: pull-requests: write steps: - - uses: hmarr/auto-approve-action@v3.2.0 + - uses: hmarr/auto-approve-action@v3.2.1 with: github-token: "${{ secrets.GITHUB_TOKEN }}" diff --git a/packages/@aws-cdk/aws-autoscaling/README.md b/packages/@aws-cdk/aws-autoscaling/README.md index a9805d2a2c253..7efe26c2593e4 100644 --- a/packages/@aws-cdk/aws-autoscaling/README.md +++ b/packages/@aws-cdk/aws-autoscaling/README.md @@ -25,7 +25,11 @@ declare const vpc: ec2.Vpc; new autoscaling.AutoScalingGroup(this, 'ASG', { vpc, instanceType: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.MICRO), - machineImage: new ec2.AmazonLinuxImage() // get the latest Amazon Linux image + + // The latest Amazon Linux image of a particular generation + machineImage: ec2.MachineImage.latestAmazonLinux({ + generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2, + }), }); ``` @@ -41,7 +45,9 @@ const mySecurityGroup = new ec2.SecurityGroup(this, 'SecurityGroup', { vpc }); new autoscaling.AutoScalingGroup(this, 'ASG', { vpc, instanceType: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.MICRO), - machineImage: new ec2.AmazonLinuxImage(), + machineImage: ec2.MachineImage.latestAmazonLinux({ + generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2, + }), securityGroup: mySecurityGroup, }); ``` @@ -538,6 +544,40 @@ new autoscaling.AutoScalingGroup(this, 'ASG', { }); ``` +## Connecting to your instances using SSM Session Manager + +SSM Session Manager makes it possible to connect to your instances from the +AWS Console, without preparing SSH keys. + +To do so, you need to: + +* Use an image with [SSM agent](https://docs.aws.amazon.com/systems-manager/latest/userguide/ssm-agent.html) installed + and configured. [Many images come with SSM Agent + preinstalled](https://docs.aws.amazon.com/systems-manager/latest/userguide/ami-preinstalled-agent.html), otherwise you + may need to manually put instructions to [install SSM + Agent](https://docs.aws.amazon.com/systems-manager/latest/userguide/sysman-manual-agent-install.html) into your + instance's UserData or use EC2 Init). +* Create the AutoScalingGroup with `ssmSessionPermissions: true`. + +If these conditions are met, you can connect to the instance from the EC2 Console. Example: + +```ts +declare const vpc: ec2.Vpc; + +new autoscaling.AutoScalingGroup(this, 'ASG', { + vpc, + instanceType: ec2.InstanceType.of(ec2.InstanceClass.T3, ec2.InstanceSize.MICRO), + + // Amazon Linux 2 comes with SSM Agent by default + machineImage: ec2.MachineImage.latestAmazonLinux({ + generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2, + }), + + // Turn on SSM + ssmSessionPermissions: true, +}); +``` + ## Configuring Instance Metadata Service (IMDS) ### Toggling IMDSv1 @@ -596,13 +636,13 @@ autoScalingGroup.addWarmPool({ ### Default Instance Warming -You can use the default instance warmup feature to improve the Amazon CloudWatch metrics used for dynamic scaling. -When default instance warmup is not enabled, each instance starts contributing usage data to the aggregated metrics -as soon as the instance reaches the InService state. However, if you enable default instance warmup, this lets +You can use the default instance warmup feature to improve the Amazon CloudWatch metrics used for dynamic scaling. +When default instance warmup is not enabled, each instance starts contributing usage data to the aggregated metrics +as soon as the instance reaches the InService state. However, if you enable default instance warmup, this lets your instances finish warming up before they contribute the usage data. -To optimize the performance of scaling policies that scale continuously, such as target tracking and step scaling -policies, we strongly recommend that you enable the default instance warmup, even if its value is set to 0 seconds. +To optimize the performance of scaling policies that scale continuously, such as target tracking and step scaling +policies, we strongly recommend that you enable the default instance warmup, even if its value is set to 0 seconds. To set up Default Instance Warming for an autoscaling group, simply pass it in as a prop diff --git a/packages/@aws-cdk/aws-autoscaling/lib/auto-scaling-group.ts b/packages/@aws-cdk/aws-autoscaling/lib/auto-scaling-group.ts index 9f229f5748f4e..36430ebda8cb9 100644 --- a/packages/@aws-cdk/aws-autoscaling/lib/auto-scaling-group.ts +++ b/packages/@aws-cdk/aws-autoscaling/lib/auto-scaling-group.ts @@ -365,6 +365,23 @@ export interface CommonAutoScalingGroupProps { * */ readonly capacityRebalance?: boolean; + + /** + * Add SSM session permissions to the instance role + * + * Setting this to `true` adds the necessary permissions to connect + * to the instance using SSM Session Manager. You can do this + * from the AWS Console. + * + * NOTE: Setting this flag to `true` may not be enough by itself. + * You must also use an AMI that comes with the SSM Agent, or install + * the SSM Agent yourself. See + * [Working with SSM Agent](https://docs.aws.amazon.com/systems-manager/latest/userguide/ssm-agent.html) + * in the SSM Developer Guide. + * + * @default false + */ + readonly ssmSessionPermissions?: boolean; } /** @@ -1278,6 +1295,10 @@ export class AutoScalingGroup extends AutoScalingGroupBase implements this.grantPrincipal = this._role; + if (props.ssmSessionPermissions) { + this.role.addManagedPolicy(iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonSSMManagedInstanceCore')); + } + const iamProfile = new iam.CfnInstanceProfile(this, 'InstanceProfile', { roles: [this.role.roleName], }); diff --git a/packages/@aws-cdk/aws-autoscaling/test/auto-scaling-group.test.ts b/packages/@aws-cdk/aws-autoscaling/test/auto-scaling-group.test.ts index 60e60e965b35d..0ea23a5bb5c29 100644 --- a/packages/@aws-cdk/aws-autoscaling/test/auto-scaling-group.test.ts +++ b/packages/@aws-cdk/aws-autoscaling/test/auto-scaling-group.test.ts @@ -2051,6 +2051,30 @@ test('add price-capacity-optimized', () => { }); }); +test('ssm permissions adds right managed policy', () => { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + new autoscaling.AutoScalingGroup(stack, 'mip-asg', { + vpc: mockVpc(stack), + machineImage: new AmazonLinuxImage(), + instanceType: InstanceType.of(ec2.InstanceClass.T3, ec2.InstanceSize.LARGE), + ssmSessionPermissions: true, + }); + + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { + ManagedPolicyArns: [ + { + 'Fn::Join': ['', [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':iam::aws:policy/AmazonSSMManagedInstanceCore', + ]], + }, + ], + }); +}); function mockSecurityGroup(stack: cdk.Stack) { return ec2.SecurityGroup.fromSecurityGroupId(stack, 'MySG', 'most-secure'); diff --git a/packages/@aws-cdk/aws-cloudwatch/README.md b/packages/@aws-cdk/aws-cloudwatch/README.md index 30b80f9646398..67828ee2b6864 100644 --- a/packages/@aws-cdk/aws-cloudwatch/README.md +++ b/packages/@aws-cdk/aws-cloudwatch/README.md @@ -697,3 +697,18 @@ new cloudwatch.Row(widgetA, widgetB); You can add a widget after object instantiation with the method `addWidget()`. + +### Interval duration for dashboard + +Interval duration for metrics in dashboard. You can specify `defaultInterval` with +the relative time(eg. 7 days) as `cdk.Duration.days(7)`. + +```ts +import * as cw from '@aws-cdk/aws-cloudwatch'; + +const dashboard = new cw.Dashboard(stack, 'Dash', { + defaultInterval: cdk.Duration.days(7), +}); +``` + +Here, the dashboard would show the metrics for the last 7 days. diff --git a/packages/@aws-cdk/aws-cloudwatch/lib/dashboard.ts b/packages/@aws-cdk/aws-cloudwatch/lib/dashboard.ts index f138d1f419ab6..37cd92fd9e29b 100644 --- a/packages/@aws-cdk/aws-cloudwatch/lib/dashboard.ts +++ b/packages/@aws-cdk/aws-cloudwatch/lib/dashboard.ts @@ -1,4 +1,4 @@ -import { Lazy, Resource, Stack, Token, Annotations } from '@aws-cdk/core'; +import { Lazy, Resource, Stack, Token, Annotations, Duration } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { CfnDashboard } from './cloudwatch.generated'; import { Column, Row } from './layout'; @@ -31,6 +31,14 @@ export interface DashboardProps { */ readonly dashboardName?: string; + /** + * Interval duration for metrics. + * You can specify defaultInterval with the relative time(eg. cdk.Duration.days(7)). + * + * @default When the dashboard loads, the defaultInterval time will be the default time range. + */ + readonly defaultInterval?: Duration + /** * The start of the time range to use for each widget on the dashboard. * You can specify start without specifying end to specify a relative time range that ends with the current time. @@ -107,6 +115,10 @@ export class Dashboard extends Resource { } } + if (props.start !== undefined && props.defaultInterval !== undefined) { + throw ('both properties defaultInterval and start cannot be set at once'); + } + const dashboard = new CfnDashboard(this, 'Resource', { dashboardName: this.physicalName, dashboardBody: Lazy.string({ @@ -114,8 +126,8 @@ export class Dashboard extends Resource { const column = new Column(...this.rows); column.position(0, 0); return Stack.of(this).toJsonString({ - start: props.start, - end: props.end, + start: props.defaultInterval !== undefined ? `-${props.defaultInterval?.toIsoString()}` : props.start, + end: props.defaultInterval !== undefined ? undefined : props.end, periodOverride: props.periodOverride, widgets: column.toJson(), }); diff --git a/packages/@aws-cdk/aws-cloudwatch/test/dashboard.test.ts b/packages/@aws-cdk/aws-cloudwatch/test/dashboard.test.ts index cf4c123e0171c..87641c935d448 100644 --- a/packages/@aws-cdk/aws-cloudwatch/test/dashboard.test.ts +++ b/packages/@aws-cdk/aws-cloudwatch/test/dashboard.test.ts @@ -1,5 +1,5 @@ import { Template, Annotations, Match } from '@aws-cdk/assertions'; -import { App, Stack } from '@aws-cdk/core'; +import { App, Duration, Stack } from '@aws-cdk/core'; import { Dashboard, GraphWidget, PeriodOverride, TextWidget, MathExpression, TextWidgetBackground } from '../lib'; describe('Dashboard', () => { @@ -131,6 +131,31 @@ describe('Dashboard', () => { }); + test('defaultInterval test', () => { + // GIVEN + const stack = new Stack(); + // WHEN + const dashboard = new Dashboard(stack, 'Dash', { + defaultInterval: Duration.days(7), + }); + dashboard.addWidgets( + new GraphWidget({ width: 1, height: 1 }), // GraphWidget has internal reference to current region + ); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::CloudWatch::Dashboard', { + DashboardBody: { + 'Fn::Join': ['', [ + '{"start":"-P7D",\ +"widgets":[{"type":"metric","width":1,"height":1,"x":0,"y":0,"properties":{"view":"timeSeries","region":"', + { Ref: 'AWS::Region' }, + '","yAxis":{}}}]}', + ]], + }, + }); + + }); + test('DashboardName is set when provided', () => { // GIVEN const app = new App(); diff --git a/packages/@aws-cdk/aws-cloudwatch/test/integ.dashboard.js.snapshot/DashboardIntegrationTestDefaultTestDeployAssert5BE38902.assets.json b/packages/@aws-cdk/aws-cloudwatch/test/integ.dashboard.js.snapshot/DashboardIntegrationTestDefaultTestDeployAssert5BE38902.assets.json index 54fff803ba3d9..cc6443bcd7bd8 100644 --- a/packages/@aws-cdk/aws-cloudwatch/test/integ.dashboard.js.snapshot/DashboardIntegrationTestDefaultTestDeployAssert5BE38902.assets.json +++ b/packages/@aws-cdk/aws-cloudwatch/test/integ.dashboard.js.snapshot/DashboardIntegrationTestDefaultTestDeployAssert5BE38902.assets.json @@ -1,5 +1,5 @@ { - "version": "30.0.0", + "version": "31.0.0", "files": { "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22": { "source": { diff --git a/packages/@aws-cdk/aws-cloudwatch/test/integ.dashboard.js.snapshot/DashboardIntegrationTestStack.assets.json b/packages/@aws-cdk/aws-cloudwatch/test/integ.dashboard.js.snapshot/DashboardIntegrationTestStack.assets.json index 595d92ea76c09..c1216e09c0031 100644 --- a/packages/@aws-cdk/aws-cloudwatch/test/integ.dashboard.js.snapshot/DashboardIntegrationTestStack.assets.json +++ b/packages/@aws-cdk/aws-cloudwatch/test/integ.dashboard.js.snapshot/DashboardIntegrationTestStack.assets.json @@ -1,7 +1,7 @@ { - "version": "30.0.0", + "version": "31.0.0", "files": { - "53eb5ec97b9df3953bc84bdc2aee87ace7b502c665b7e5b9f7b7d14dd46cea69": { + "1a70f8470c838c02020b9010528363b17eebd55d55c1a53fb3e0f6760a606c98": { "source": { "path": "DashboardIntegrationTestStack.template.json", "packaging": "file" @@ -9,7 +9,7 @@ "destinations": { "current_account-current_region": { "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", - "objectKey": "53eb5ec97b9df3953bc84bdc2aee87ace7b502c665b7e5b9f7b7d14dd46cea69.json", + "objectKey": "1a70f8470c838c02020b9010528363b17eebd55d55c1a53fb3e0f6760a606c98.json", "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" } } diff --git a/packages/@aws-cdk/aws-cloudwatch/test/integ.dashboard.js.snapshot/DashboardIntegrationTestStack.template.json b/packages/@aws-cdk/aws-cloudwatch/test/integ.dashboard.js.snapshot/DashboardIntegrationTestStack.template.json index cce2e902ed631..bae3b7498819d 100644 --- a/packages/@aws-cdk/aws-cloudwatch/test/integ.dashboard.js.snapshot/DashboardIntegrationTestStack.template.json +++ b/packages/@aws-cdk/aws-cloudwatch/test/integ.dashboard.js.snapshot/DashboardIntegrationTestStack.template.json @@ -3,7 +3,7 @@ "DashCCD7F836": { "Type": "AWS::CloudWatch::Dashboard", "Properties": { - "DashboardBody": "{\"widgets\":[{\"type\":\"text\",\"width\":6,\"height\":2,\"x\":0,\"y\":0,\"properties\":{\"markdown\":\"I don't have a background\",\"background\":\"transparent\"}}]}" + "DashboardBody": "{\"start\":\"-P7D\",\"widgets\":[{\"type\":\"text\",\"width\":6,\"height\":2,\"x\":0,\"y\":0,\"properties\":{\"markdown\":\"I don't have a background\",\"background\":\"transparent\"}}]}" } } }, diff --git a/packages/@aws-cdk/aws-cloudwatch/test/integ.dashboard.js.snapshot/cdk.out b/packages/@aws-cdk/aws-cloudwatch/test/integ.dashboard.js.snapshot/cdk.out index ae4b03c54e770..7925065efbcc4 100644 --- a/packages/@aws-cdk/aws-cloudwatch/test/integ.dashboard.js.snapshot/cdk.out +++ b/packages/@aws-cdk/aws-cloudwatch/test/integ.dashboard.js.snapshot/cdk.out @@ -1 +1 @@ -{"version":"30.0.0"} \ No newline at end of file +{"version":"31.0.0"} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cloudwatch/test/integ.dashboard.js.snapshot/integ.json b/packages/@aws-cdk/aws-cloudwatch/test/integ.dashboard.js.snapshot/integ.json index 97d4b4c695087..0f1d270fe004c 100644 --- a/packages/@aws-cdk/aws-cloudwatch/test/integ.dashboard.js.snapshot/integ.json +++ b/packages/@aws-cdk/aws-cloudwatch/test/integ.dashboard.js.snapshot/integ.json @@ -1,5 +1,5 @@ { - "version": "30.0.0", + "version": "31.0.0", "testCases": { "DashboardIntegrationTest/DefaultTest": { "stacks": [ diff --git a/packages/@aws-cdk/aws-cloudwatch/test/integ.dashboard.js.snapshot/manifest.json b/packages/@aws-cdk/aws-cloudwatch/test/integ.dashboard.js.snapshot/manifest.json index 7e16f2cbd9b40..e2232bc5e3d44 100644 --- a/packages/@aws-cdk/aws-cloudwatch/test/integ.dashboard.js.snapshot/manifest.json +++ b/packages/@aws-cdk/aws-cloudwatch/test/integ.dashboard.js.snapshot/manifest.json @@ -1,5 +1,5 @@ { - "version": "30.0.0", + "version": "31.0.0", "artifacts": { "DashboardIntegrationTestStack.assets": { "type": "cdk:asset-manifest", @@ -17,7 +17,7 @@ "validateOnSynth": false, "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", - "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/53eb5ec97b9df3953bc84bdc2aee87ace7b502c665b7e5b9f7b7d14dd46cea69.json", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/1a70f8470c838c02020b9010528363b17eebd55d55c1a53fb3e0f6760a606c98.json", "requiresBootstrapStackVersion": 6, "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", "additionalDependencies": [ diff --git a/packages/@aws-cdk/aws-cloudwatch/test/integ.dashboard.js.snapshot/tree.json b/packages/@aws-cdk/aws-cloudwatch/test/integ.dashboard.js.snapshot/tree.json index 20d55ea065cdf..208279e25ef53 100644 --- a/packages/@aws-cdk/aws-cloudwatch/test/integ.dashboard.js.snapshot/tree.json +++ b/packages/@aws-cdk/aws-cloudwatch/test/integ.dashboard.js.snapshot/tree.json @@ -18,7 +18,7 @@ "attributes": { "aws:cdk:cloudformation:type": "AWS::CloudWatch::Dashboard", "aws:cdk:cloudformation:props": { - "dashboardBody": "{\"widgets\":[{\"type\":\"text\",\"width\":6,\"height\":2,\"x\":0,\"y\":0,\"properties\":{\"markdown\":\"I don't have a background\",\"background\":\"transparent\"}}]}" + "dashboardBody": "{\"start\":\"-P7D\",\"widgets\":[{\"type\":\"text\",\"width\":6,\"height\":2,\"x\":0,\"y\":0,\"properties\":{\"markdown\":\"I don't have a background\",\"background\":\"transparent\"}}]}" } }, "constructInfo": { @@ -75,7 +75,7 @@ "path": "DashboardIntegrationTest/DefaultTest/Default", "constructInfo": { "fqn": "constructs.Construct", - "version": "10.1.252" + "version": "10.1.270" } }, "DeployAssert": { @@ -121,7 +121,7 @@ "path": "Tree", "constructInfo": { "fqn": "constructs.Construct", - "version": "10.1.252" + "version": "10.1.270" } } }, diff --git a/packages/@aws-cdk/aws-cloudwatch/test/integ.dashboard.ts b/packages/@aws-cdk/aws-cloudwatch/test/integ.dashboard.ts index 445d19698d809..b35722bb60975 100644 --- a/packages/@aws-cdk/aws-cloudwatch/test/integ.dashboard.ts +++ b/packages/@aws-cdk/aws-cloudwatch/test/integ.dashboard.ts @@ -7,7 +7,9 @@ const app = new cdk.App(); const stack = new cdk.Stack(app, 'DashboardIntegrationTestStack'); -const dashboard = new cloudwatch.Dashboard(stack, 'Dash'); +const dashboard = new cloudwatch.Dashboard(stack, 'Dash', { + defaultInterval: cdk.Duration.days(7), +}); dashboard.addWidgets(new cloudwatch.TextWidget({ markdown: 'I don\'t have a background', diff --git a/packages/@aws-cdk/aws-ec2/README.md b/packages/@aws-cdk/aws-ec2/README.md index 305d6bd7dbdaa..0640d3c63a2f4 100644 --- a/packages/@aws-cdk/aws-ec2/README.md +++ b/packages/@aws-cdk/aws-ec2/README.md @@ -788,7 +788,7 @@ AMIs control the OS that gets launched when you start your EC2 instance. The EC2 library contains constructs to select the AMI you want to use. Depending on the type of AMI, you select it a different way. Here are some -examples of things you might want to use: +examples of images you might want to use: [example of creating images](test/example.images.lit.ts) @@ -1039,27 +1039,27 @@ care of restarting your instance if it ever fails. declare const vpc: ec2.Vpc; declare const instanceType: ec2.InstanceType; -// AWS Linux +// Amazon Linux 1 new ec2.Instance(this, 'Instance1', { vpc, instanceType, - machineImage: new ec2.AmazonLinuxImage(), + machineImage: ec2.MachineImage.latestAmazonLinux(), }); -// AWS Linux 2 +// Amazon Linux 2 new ec2.Instance(this, 'Instance2', { vpc, instanceType, - machineImage: new ec2.AmazonLinuxImage({ + machineImage: ec2.MachineImage.latestAmazonLinux({ generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2, }), }); -// AWS Linux 2 with kernel 5.x +// Amazon Linux 2 with kernel 5.x new ec2.Instance(this, 'Instance3', { vpc, instanceType, - machineImage: new ec2.AmazonLinuxImage({ + machineImage: ec2.MachineImage.latestAmazonLinux({ generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2, kernel: ec2.AmazonLinuxKernel.KERNEL5_X, }), @@ -1069,7 +1069,7 @@ new ec2.Instance(this, 'Instance3', { new ec2.Instance(this, 'Instance4', { vpc, instanceType, - machineImage: new ec2.AmazonLinuxImage({ + machineImage: ec2.MachineImage.latestAmazonLinux({ generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2022, }), }); @@ -1078,7 +1078,7 @@ new ec2.Instance(this, 'Instance4', { new ec2.Instance(this, 'Instance5', { vpc, instanceType: ec2.InstanceType.of(ec2.InstanceClass.C7G, ec2.InstanceSize.LARGE), - machineImage: new ec2.AmazonLinuxImage({ + machineImage: ec2.MachineImage.latestAmazonLinux({ generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2, cpuType: ec2.AmazonLinuxCpuType.ARM_64, }), @@ -1151,6 +1151,48 @@ new ec2.Instance(this, 'Instance', { }); ``` +`InitCommand` can not be used to start long-running processes. At deploy time, +`cfn-init` will always wait for the process to exit before continuing, causing +the CloudFormation deployment to fail because the signal hasn't been received +within the expected timeout. + +Instead, you should install a service configuration file onto your machine `InitFile`, +and then use `InitService` to start it. + +If your Linux OS is using SystemD (like Amazon Linux 2 or higher), the CDK has +helpers to create a long-running service using CFN Init. You can create a +SystemD-compatible config file using `InitService.systemdConfigFile()`, and +start it immediately. The following examples shows how to start a trivial Python +3 web server: + +```ts +declare const vpc: ec2.Vpc; +declare const instanceType: ec2.InstanceType; + +new ec2.Instance(this, 'Instance', { + vpc, + instanceType, + machineImage: ec2.MachineImage.latestAmazonLinux({ + // Amazon Linux 2 uses SystemD + generation: ec2.AmazonLinuxGeneration: AMAZON_LINUX_2, + }), + + init: ec2.CloudFormationInit.fromElements([ + // Create a simple config file that runs a Python web server + ec2.InitService.systemdConfigFile('simpleserver', { + command: '/usr/bin/python3 -m http.server 8080', + cwd: '/var/www/html', + }), + // Start the server using SystemD + ec2.InitService.enable('simpleserver', { + serviceManager: ec2.ServiceManager.SYSTEMD, + }), + // Drop an example file to show the web server working + ec2.InitFile.fromString('/var/www/html/index.html', 'Hello! It\'s working!'), + ]), +}); +``` + You can have services restarted after the init process has made changes to the system. To do that, instantiate an `InitServiceRestartHandle` and pass it to the config elements that need to trigger the restart and the service itself. For example, the following @@ -1669,7 +1711,9 @@ The following demonstrates how to create a launch template with an Amazon Machin declare const vpc: ec2.Vpc; const template = new ec2.LaunchTemplate(this, 'LaunchTemplate', { - machineImage: ec2.MachineImage.latestAmazonLinux(), + machineImage: ec2.MachineImage.latestAmazonLinux({ + generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2, + }), securityGroup: new ec2.SecurityGroup(this, 'LaunchTemplateSG', { vpc: vpc, }), @@ -1699,7 +1743,42 @@ declare const instanceType: ec2.InstanceType; new ec2.Instance(this, 'Instance1', { vpc, instanceType, - machineImage: new ec2.AmazonLinuxImage(), + machineImage: ec2.MachineImage.latestAmazonLinux(), detailedMonitoring: true, }); ``` + +## Connecting to your instances using SSM Session Manager + +SSM Session Manager makes it possible to connect to your instances from the +AWS Console, without preparing SSH keys. + +To do so, you need to: + +* Use an image with [SSM agent](https://docs.aws.amazon.com/systems-manager/latest/userguide/ssm-agent.html) installed + and configured. [Many images come with SSM Agent + preinstalled](https://docs.aws.amazon.com/systems-manager/latest/userguide/ami-preinstalled-agent.html), otherwise you + may need to manually put instructions to [install SSM + Agent](https://docs.aws.amazon.com/systems-manager/latest/userguide/sysman-manual-agent-install.html) into your + instance's UserData or use EC2 Init). +* Create the instance with `ssmSessionPermissions: true`. + +If these conditions are met, you can connect to the instance from the EC2 Console. Example: + +```ts +declare const vpc: ec2.Vpc; +declare const instanceType: ec2.InstanceType; + +new ec2.Instance(this, 'Instance1', { + vpc, + instanceType, + + // Amazon Linux 2 comes with SSM Agent by default + machineImage: ec2.MachineImage.latestAmazonLinux({ + generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2, + }), + + // Turn on SSM + ssmSessionPermissions: true, +}); +``` diff --git a/packages/@aws-cdk/aws-ec2/lib/cfn-init-elements.ts b/packages/@aws-cdk/aws-ec2/lib/cfn-init-elements.ts index 646d6b2dcbfa4..fb347ac20f5df 100644 --- a/packages/@aws-cdk/aws-ec2/lib/cfn-init-elements.ts +++ b/packages/@aws-cdk/aws-ec2/lib/cfn-init-elements.ts @@ -781,6 +781,16 @@ export interface InitServiceOptions { * @default - No files trigger restart */ readonly serviceRestartHandle?: InitServiceRestartHandle; + + /** + * What service manager to use + * + * This needs to match the actual service manager on your Operating System. + * For example, Amazon Linux 1 uses SysVinit, but Amazon Linux 2 uses Systemd. + * + * @default ServiceManager.SYSVINIT for Linux images, ServiceManager.WINDOWS for Windows images + */ + readonly serviceManager?: ServiceManager; } /** @@ -806,6 +816,39 @@ export class InitService extends InitElement { return new InitService(serviceName, { enabled: false, ensureRunning: false }); } + /** + * Install a systemd-compatible config file for the given service + * + * This is a helper function to create a simple systemd configuration + * file that will allow running a service on the machine using `InitService.enable()`. + * + * Systemd allows many configuration options; this function does not pretend + * to expose all of them. If you need advanced configuration options, you + * can use `InitFile` to create exactly the configuration file you need + * at `/etc/systemd/system/${serviceName}.service`. + */ + public static systemdConfigFile(serviceName: string, options: SystemdConfigFileOptions): InitFile { + if (!options.command.startsWith('/')) { + throw new Error(`SystemD executables must use an absolute path, got '${options.command}'`); + } + + const lines = [ + '[Unit]', + ...(options.description ? [`Description=${options.description}`] : []), + ...(options.afterNetwork ?? true ? ['After=network.target'] : []), + '[Service]', + `ExecStart=${options.command}`, + ...(options.cwd ? [`WorkingDirectory=${options.cwd}`] : []), + ...(options.user ? [`User=${options.user}`] : []), + ...(options.group ? [`Group=${options.user}`] : []), + ...(options.keepRunning ?? true ? ['Restart=always'] : []), + '[Install]', + 'WantedBy=multi-user.target', + ]; + + return InitFile.fromString(`/etc/systemd/system/${serviceName}.service`, lines.join('\n')); + } + public readonly elementType = InitElementType.SERVICE.toString(); private constructor(private readonly serviceName: string, private readonly serviceOptions: InitServiceOptions) { @@ -814,11 +857,12 @@ export class InitService extends InitElement { /** @internal */ public _bind(options: InitBindOptions): InitElementConfig { - const serviceManager = options.platform === InitPlatform.LINUX ? 'sysvinit' : 'windows'; + const serviceManager = this.serviceOptions.serviceManager + ?? (options.platform === InitPlatform.LINUX ? ServiceManager.SYSVINIT : ServiceManager.WINDOWS); return { config: { - [serviceManager]: { + [serviceManagerToString(serviceManager)]: { [this.serviceName]: { enabled: this.serviceOptions.enabled, ensureRunning: this.serviceOptions.ensureRunning, @@ -970,3 +1014,93 @@ function standardS3Auth(role: iam.IRole, bucketName: string) { }, }; } + +/** + * The service manager that will be used by InitServices + * + * The value needs to match the service manager used by your operating + * system. + */ +export enum ServiceManager { + /** + * Use SysVinit + * + * This is the default for Linux systems. + */ + SYSVINIT, + + /** + * Use Windows + * + * This is the default for Windows systems. + */ + WINDOWS, + + /** + * Use systemd + */ + SYSTEMD, +} + +function serviceManagerToString(x: ServiceManager): string { + switch (x) { + case ServiceManager.SYSTEMD: return 'systemd'; + case ServiceManager.SYSVINIT: return 'sysvinit'; + case ServiceManager.WINDOWS: return 'windows'; + } +} + +/** + * Options for creating a SystemD configuration file + */ +export interface SystemdConfigFileOptions { + /** + * The command to run to start this service + */ + readonly command: string; + + /** + * The working directory for the command + * + * @default Root directory or home directory of specified user + */ + readonly cwd?: string; + + /** + * A description of this service + * + * @default - No description + */ + readonly description?: string; + + /** + * The user to execute the process under + * + * @default root + */ + readonly user?: string; + + /** + * The group to execute the process under + * + * @default root + */ + readonly group?: string; + + /** + * Keep the process running all the time + * + * Restarts the process when it exits for any reason other + * than the machine shutting down. + * + * @default true + */ + readonly keepRunning?: boolean; + + /** + * Start the service after the networking part of the OS comes up + * + * @default true + */ + readonly afterNetwork?: boolean; +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ec2/lib/instance.ts b/packages/@aws-cdk/aws-ec2/lib/instance.ts index 4997f7dcbda3e..b02e62ecafdaa 100644 --- a/packages/@aws-cdk/aws-ec2/lib/instance.ts +++ b/packages/@aws-cdk/aws-ec2/lib/instance.ts @@ -254,6 +254,23 @@ export interface InstanceProps { * @default - false */ readonly detailedMonitoring?: boolean; + + /** + * Add SSM session permissions to the instance role + * + * Setting this to `true` adds the necessary permissions to connect + * to the instance using SSM Session Manager. You can do this + * from the AWS Console. + * + * NOTE: Setting this flag to `true` may not be enough by itself. + * You must also use an AMI that comes with the SSM Agent, or install + * the SSM Agent yourself. See + * [Working with SSM Agent](https://docs.aws.amazon.com/systems-manager/latest/userguide/ssm-agent.html) + * in the SSM Developer Guide. + * + * @default false + */ + readonly ssmSessionPermissions?: boolean; } /** @@ -342,6 +359,10 @@ export class Instance extends Resource implements IInstance { }); this.grantPrincipal = this.role; + if (props.ssmSessionPermissions) { + this.role.addManagedPolicy(iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonSSMManagedInstanceCore')); + } + const iamProfile = new iam.CfnInstanceProfile(this, 'InstanceProfile', { roles: [this.role.roleName], }); diff --git a/packages/@aws-cdk/aws-ec2/lib/machine-image.ts b/packages/@aws-cdk/aws-ec2/lib/machine-image.ts index 4e1b87503004b..7d05353781b71 100644 --- a/packages/@aws-cdk/aws-ec2/lib/machine-image.ts +++ b/packages/@aws-cdk/aws-ec2/lib/machine-image.ts @@ -39,6 +39,20 @@ export abstract class MachineImage { * deployment. Be aware this will cause your instances to be replaced when a * new version of the image becomes available. Do not store stateful information * on the instance if you are using this image. + * + * N.B.: "latest" in the name of this function indicates that it always uses the most recent + * image of a particular generation of Amazon Linux, not that it uses the "latest generation". + * For backwards compatibility, this function uses Amazon Linux 1 if no generation + * is specified. + * + * Specify the desired generation using the `generation` property: + * + * ```ts + * ec2.MachineImage.latestAmazonLinux({ + * // Use Amazon Linux 2 + * generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2, + * }) + * ``` */ public static latestAmazonLinux(props?: AmazonLinuxImageProps): IMachineImage { return new AmazonLinuxImage(props); diff --git a/packages/@aws-cdk/aws-ec2/test/cfn-init-element.test.ts b/packages/@aws-cdk/aws-ec2/test/cfn-init-element.test.ts index 808783201ee3e..7b6c61e47c590 100644 --- a/packages/@aws-cdk/aws-ec2/test/cfn-init-element.test.ts +++ b/packages/@aws-cdk/aws-ec2/test/cfn-init-element.test.ts @@ -625,6 +625,51 @@ describe('InitService', () => { }); }); + test('can request systemd service', () => { + // WHEN + const service = ec2.InitService.enable('httpd', { + serviceManager: ec2.ServiceManager.SYSTEMD, + }); + + // THEN + const bindOptions = defaultOptions(InitPlatform.LINUX); + const rendered = service._bind(bindOptions).config; + + // THEN + expect(rendered.systemd).toEqual({ + httpd: { + enabled: true, + ensureRunning: true, + }, + }); + }); + + test('can create simple systemd config file', () => { + // WHEN + const file = ec2.InitService.systemdConfigFile('myserver', { + command: '/start/my/service', + cwd: '/my/dir', + user: 'ec2-user', + group: 'ec2-user', + description: 'my service', + }); + + // THEN + const bindOptions = defaultOptions(InitPlatform.LINUX); + const rendered = file._bind(bindOptions).config; + expect(rendered).toEqual({ + '/etc/systemd/system/myserver.service': expect.objectContaining({ + content: expect.any(String), + }), + }); + + const capture = rendered['/etc/systemd/system/myserver.service'].content; + expect(capture).toContain('ExecStart=/start/my/service'); + expect(capture).toContain('WorkingDirectory=/my/dir'); + expect(capture).toContain('User=ec2-user'); + expect(capture).toContain('Group=ec2-user'); + expect(capture).toContain('Description=my service'); + }); }); describe('InitSource', () => { diff --git a/packages/@aws-cdk/aws-ec2/test/instance.test.ts b/packages/@aws-cdk/aws-ec2/test/instance.test.ts index 9d91fc4fde8d2..ac64a7a6c78f5 100644 --- a/packages/@aws-cdk/aws-ec2/test/instance.test.ts +++ b/packages/@aws-cdk/aws-ec2/test/instance.test.ts @@ -602,3 +602,26 @@ test('cause replacement from s3 asset in userdata', () => { }), })); }); + +test('ssm permissions adds right managed policy', () => { + // WHEN + new Instance(stack, 'InstanceOne', { + vpc, + machineImage: new AmazonLinuxImage(), + instanceType: InstanceType.of(InstanceClass.T3, InstanceSize.LARGE), + ssmSessionPermissions: true, + }); + + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { + ManagedPolicyArns: [ + { + 'Fn::Join': ['', [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':iam::aws:policy/AmazonSSMManagedInstanceCore', + ]], + }, + ], + }); +}); + diff --git a/packages/@aws-cdk/aws-ecr/README.md b/packages/@aws-cdk/aws-ecr/README.md index 7a866e77f7a49..fdfbe2319f9d3 100644 --- a/packages/@aws-cdk/aws-ecr/README.md +++ b/packages/@aws-cdk/aws-ecr/README.md @@ -118,3 +118,21 @@ declare const repository: ecr.Repository; repository.addLifecycleRule({ tagPrefixList: ['prod'], maxImageCount: 9999 }); repository.addLifecycleRule({ maxImageAge: Duration.days(30) }); ``` + +### Repository deletion + +When a repository is removed from a stack (or the stack is deleted), the ECR +repository will be removed according to its removal policy (which by default will +simply orphan the repository and leave it in your AWS account). If the removal +policy is set to `RemovalPolicy.DESTROY`, the repository will be deleted as long +as it does not contain any images. + +To override this and force all images to get deleted during repository deletion, +enable the`autoDeleteImages` option. + +```ts +const repository = new Repository(this, 'MyTempRepo', { + removalPolicy: RemovalPolicy.DESTROY, + autoDeleteImages: true, +}); +``` diff --git a/packages/@aws-cdk/aws-ecr/lib/auto-delete-images-handler/index.ts b/packages/@aws-cdk/aws-ecr/lib/auto-delete-images-handler/index.ts new file mode 100644 index 0000000000000..fbc6ca60add7d --- /dev/null +++ b/packages/@aws-cdk/aws-ecr/lib/auto-delete-images-handler/index.ts @@ -0,0 +1,94 @@ +// eslint-disable-next-line import/no-extraneous-dependencies +import { ECR } from 'aws-sdk'; + +const AUTO_DELETE_IMAGES_TAG = 'aws-cdk:auto-delete-images'; + +const ecr = new ECR(); + +export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent) { + switch (event.RequestType) { + case 'Create': + break; + case 'Update': + return onUpdate(event); + case 'Delete': + return onDelete(event.ResourceProperties?.RepositoryName); + } +} + +async function onUpdate(event: AWSLambda.CloudFormationCustomResourceEvent) { + const updateEvent = event as AWSLambda.CloudFormationCustomResourceUpdateEvent; + const oldRepositoryName = updateEvent.OldResourceProperties?.RepositoryName; + const newRepositoryName = updateEvent.ResourceProperties?.RepositoryName; + const repositoryNameHasChanged = (newRepositoryName && oldRepositoryName) + && (newRepositoryName !== oldRepositoryName); + + /* If the name of the repository has changed, CloudFormation will try to delete the repository + and create a new one with the new name. So we have to delete the images in the + repository so that this operation does not fail. */ + if (repositoryNameHasChanged) { + return onDelete(oldRepositoryName); + } +} + +/** + * Recursively delete all images in the repository + * + * @param ECR.ListImagesRequest the repositoryName & nextToken if presented + */ +async function emptyRepository(params: ECR.ListImagesRequest) { + const listedImages = await ecr.listImages(params).promise(); + + const imageIds = listedImages?.imageIds ?? []; + const nextToken = listedImages.nextToken ?? null; + if (imageIds.length === 0) { + return; + } + + await ecr.batchDeleteImage({ + repositoryName: params.repositoryName, + imageIds, + }).promise(); + + if (nextToken) { + await emptyRepository({ + ...params, + nextToken, + }); + } +} + +async function onDelete(repositoryName: string) { + if (!repositoryName) { + throw new Error('No RepositoryName was provided.'); + } + + const response = await ecr.describeRepositories({ repositoryNames: [repositoryName] }).promise(); + const repository = response.repositories?.find(repo => repo.repositoryName === repositoryName); + + if (!await isRepositoryTaggedForDeletion(repository?.repositoryArn!)) { + process.stdout.write(`Repository does not have '${AUTO_DELETE_IMAGES_TAG}' tag, skipping cleaning.\n`); + return; + } + try { + await emptyRepository({ repositoryName }); + } catch (e) { + if (e.name !== 'RepositoryNotFoundException') { + throw e; + } + // Repository doesn't exist. Ignoring + } +} + +/** + * The repository will only be tagged for deletion if it's being deleted in the same + * deployment as this Custom Resource. + * + * If the Custom Resource is ever deleted before the repository, it must be because + * `autoDeleteImages` has been switched to false, in which case the tag would have + * been removed before we get to this Delete event. + */ +async function isRepositoryTaggedForDeletion(repositoryArn: string) { + const response = await ecr.listTagsForResource({ resourceArn: repositoryArn }).promise(); + return response.tags?.some(tag => tag.Key === AUTO_DELETE_IMAGES_TAG && tag.Value === 'true'); +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecr/lib/repository.ts b/packages/@aws-cdk/aws-ecr/lib/repository.ts index d32f3430e3c62..7992af2774b30 100644 --- a/packages/@aws-cdk/aws-ecr/lib/repository.ts +++ b/packages/@aws-cdk/aws-ecr/lib/repository.ts @@ -1,12 +1,29 @@ import { EOL } from 'os'; +import * as path from 'path'; import * as events from '@aws-cdk/aws-events'; import * as iam from '@aws-cdk/aws-iam'; import * as kms from '@aws-cdk/aws-kms'; -import { ArnFormat, IResource, Lazy, RemovalPolicy, Resource, Stack, Tags, Token, TokenComparison } from '@aws-cdk/core'; +import { + ArnFormat, + IResource, + Lazy, + RemovalPolicy, + Resource, + Stack, + Tags, + Token, + TokenComparison, + CustomResource, + CustomResourceProvider, + CustomResourceProviderRuntime, +} from '@aws-cdk/core'; import { IConstruct, Construct } from 'constructs'; import { CfnRepository } from './ecr.generated'; import { LifecycleRule, TagStatus } from './lifecycle'; +const AUTO_DELETE_IMAGES_RESOURCE_TYPE = 'Custom::ECRAutoDeleteImages'; +const AUTO_DELETE_IMAGES_TAG = 'aws-cdk:auto-delete-images'; + /** * Represents an ECR repository. */ @@ -479,6 +496,16 @@ export interface RepositoryProps { * @default TagMutability.MUTABLE */ readonly imageTagMutability?: TagMutability; + + /** + * Whether all images should be automatically deleted when the repository is + * removed from the stack or when the stack is deleted. + * + * Requires the `removalPolicy` to be set to `RemovalPolicy.DESTROY`. + * + * @default false + */ + readonly autoDeleteImages?: boolean; } export interface RepositoryAttributes { @@ -589,6 +616,7 @@ export class Repository extends RepositoryBase { private readonly lifecycleRules = new Array(); private readonly registryId?: string; private policyDocument?: iam.PolicyDocument; + private readonly _resource: CfnRepository; constructor(scope: Construct, id: string, props: RepositoryProps = {}) { super(scope, id, { @@ -606,6 +634,14 @@ export class Repository extends RepositoryBase { imageTagMutability: props.imageTagMutability || undefined, encryptionConfiguration: this.parseEncryption(props), }); + this._resource = resource; + + if (props.autoDeleteImages) { + if (props.removalPolicy !== RemovalPolicy.DESTROY) { + throw new Error('Cannot use \'autoDeleteImages\' property on a repository without setting removal policy to \'DESTROY\'.'); + } + this.enableAutoDeleteImages(); + } resource.applyRemovalPolicy(props.removalPolicy); @@ -741,6 +777,44 @@ export class Repository extends RepositoryBase { throw new Error(`Unexpected 'encryptionType': ${encryptionType}`); } + + private enableAutoDeleteImages() { + // Use a iam policy to allow the custom resource to list & delete + // images in the repository and the ability to get all repositories to find the arn needed on delete. + const provider = CustomResourceProvider.getOrCreateProvider(this, AUTO_DELETE_IMAGES_RESOURCE_TYPE, { + codeDirectory: path.join(__dirname, 'auto-delete-images-handler'), + runtime: CustomResourceProviderRuntime.NODEJS_14_X, + description: `Lambda function for auto-deleting images in ${this.repositoryName} repository.`, + policyStatements: [ + { + Effect: 'Allow', + Action: [ + 'ecr:BatchDeleteImage', + 'ecr:DescribeRepositories', + 'ecr:ListImages', + 'ecr:ListTagsForResource', + ], + Resource: [this._resource.attrArn], + }, + ], + }); + + const customResource = new CustomResource(this, 'AutoDeleteImagesCustomResource', { + resourceType: AUTO_DELETE_IMAGES_RESOURCE_TYPE, + serviceToken: provider.serviceToken, + properties: { + RepositoryName: Lazy.any({ produce: () => this.repositoryName }), + }, + }); + customResource.node.addDependency(this); + + // We also tag the repository to record the fact that we want it autodeleted. + // The custom resource will check this tag before actually doing the delete. + // Because tagging and untagging will ALWAYS happen before the CR is deleted, + // we can set `autoDeleteImages: false` without the removal of the CR emptying + // the repository as a side effect. + Tags.of(this._resource).add(AUTO_DELETE_IMAGES_TAG, 'true'); + } } function validateAnyRuleLast(rules: LifecycleRule[]) { diff --git a/packages/@aws-cdk/aws-ecr/test/auto-delete-images-handler.test.ts b/packages/@aws-cdk/aws-ecr/test/auto-delete-images-handler.test.ts new file mode 100644 index 0000000000000..9bbebdf32c498 --- /dev/null +++ b/packages/@aws-cdk/aws-ecr/test/auto-delete-images-handler.test.ts @@ -0,0 +1,397 @@ +const mockECRClient = { + listImages: jest.fn().mockReturnThis(), + batchDeleteImage: jest.fn().mockReturnThis(), + describeRepositories: jest.fn().mockReturnThis(), + listTagsForResource: jest.fn().mockReturnThis(), + promise: jest.fn(), +}; + +import { handler } from '../lib/auto-delete-images-handler'; + +jest.mock('aws-sdk', () => { + return { ECR: jest.fn(() => mockECRClient) }; +}); + +beforeEach(() => { + mockECRClient.listImages.mockReturnThis(); + mockECRClient.batchDeleteImage.mockReturnThis(); + mockECRClient.listTagsForResource.mockReturnThis(); + mockECRClient.describeRepositories.mockReturnThis(); + givenTaggedForDeletion(); +}); + +afterEach(() => { + jest.resetAllMocks(); +}); + +test('does nothing on create event', async () => { + // GIVEN + const event: Partial = { + RequestType: 'Create', + ResourceProperties: { + ServiceToken: 'Foo', + RepositoryName: 'MyRepo', + }, + }; + + // WHEN + await invokeHandler(event); + + // THEN + expect(mockECRClient.listImages).toHaveBeenCalledTimes(0); + expect(mockECRClient.batchDeleteImage).toHaveBeenCalledTimes(0); +}); + +test('does nothing on update event when everything remains the same', async () => { + // GIVEN + const event: Partial = { + RequestType: 'Update', + ResourceProperties: { + ServiceToken: 'Foo', + RepositoryName: 'MyRepo', + }, + OldResourceProperties: { + ServiceToken: 'Foo', + RepositoryName: 'MyRepo', + }, + }; + + // WHEN + await invokeHandler(event); + + // THEN + expect(mockECRClient.describeRepositories).toHaveBeenCalledTimes(0); + expect(mockECRClient.listImages).toHaveBeenCalledTimes(0); + expect(mockECRClient.batchDeleteImage).toHaveBeenCalledTimes(0); +}); + +test('does nothing on update event when the repository name remains the same but the service token changes', async () => { + // GIVEN + const event: Partial = { + RequestType: 'Update', + ResourceProperties: { + ServiceToken: 'Foo', + RepositoryName: 'MyRepo', + }, + OldResourceProperties: { + ServiceToken: 'Bar', + RespositoryName: 'MyRepo', + }, + }; + + // WHEN + await invokeHandler(event); + + // THEN + expect(mockECRClient.describeRepositories).toHaveBeenCalledTimes(0); + expect(mockECRClient.listImages).toHaveBeenCalledTimes(0); + expect(mockECRClient.batchDeleteImage).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', + RepositoryName: 'MyRepo', + }, + }; + + // WHEN + await invokeHandler(event); + + // THEN + expect(mockECRClient.listImages).toHaveBeenCalledTimes(0); + expect(mockECRClient.batchDeleteImage).toHaveBeenCalledTimes(0); + expect(mockECRClient.describeRepositories).toHaveBeenCalledTimes(0); +}); + +test('does nothing on update event when the old resource properties are absent', async () => { + // GIVEN + const event: Partial = { + RequestType: 'Update', + ResourceProperties: { + ServiceToken: 'Foo', + RepositoryName: 'MyRepo', + }, + }; + + // WHEN + await invokeHandler(event); + + // THEN + expect(mockECRClient.listImages).toHaveBeenCalledTimes(0); + expect(mockECRClient.batchDeleteImage).toHaveBeenCalledTimes(0); + expect(mockECRClient.describeRepositories).toHaveBeenCalledTimes(0); +}); + +test('deletes all objects when the name changes on update event', async () => { + // GIVEN + mockAwsPromise(mockECRClient.describeRepositories, { + repositories: [ + { repositoryArn: 'RepositoryArn', respositoryName: 'MyRepo' }, + ], + }); + + mockAwsPromise(mockECRClient.listImages, { + imageIds: [ + { imageDigest: 'ImageDigest1', imageTag: 'ImageTag1' }, + { imageDigest: 'ImageDigest2', imageTag: 'ImageTag2' }, + ], + }); + + const event: Partial = { + RequestType: 'Update', + OldResourceProperties: { + ServiceToken: 'Foo', + RepositoryName: 'MyRepo', + }, + ResourceProperties: { + ServiceToken: 'Foo', + RepositoryName: 'MyRepo-renamed', + }, + }; + + // WHEN + await invokeHandler(event); + + // THEN + expect(mockECRClient.listImages).toHaveBeenCalledTimes(1); + expect(mockECRClient.listImages).toHaveBeenCalledWith({ repositoryName: 'MyRepo' }); + expect(mockECRClient.batchDeleteImage).toHaveBeenCalledTimes(1); + expect(mockECRClient.batchDeleteImage).toHaveBeenCalledWith({ + repositoryName: 'MyRepo', + imageIds: [ + { imageDigest: 'ImageDigest1', imageTag: 'ImageTag1' }, + { imageDigest: 'ImageDigest2', imageTag: 'ImageTag2' }, + ], + }); + expect(mockECRClient.describeRepositories).toHaveBeenCalledTimes(1); +}); + +test('deletes no images on delete event when repository has no images', async () => { + // GIVEN + mockECRClient.promise.mockResolvedValue({ imageIds: [] }); // listedImages() call + + // WHEN + const event: Partial = { + RequestType: 'Delete', + ResourceProperties: { + ServiceToken: 'Foo', + RepositoryName: 'MyRepo', + }, + }; + await invokeHandler(event); + + // THEN + expect(mockECRClient.listImages).toHaveBeenCalledTimes(1); + expect(mockECRClient.listImages).toHaveBeenCalledWith({ repositoryName: 'MyRepo' }); + expect(mockECRClient.batchDeleteImage).toHaveBeenCalledTimes(0); + expect(mockECRClient.describeRepositories).toHaveBeenCalledTimes(1); +}); + +test('deletes all images on delete event', async () => { + mockECRClient.promise.mockResolvedValue({ // listedImages() call + imageIds: [ + { + imageTag: 'tag1', + imageDigest: 'sha256-1', + }, + { + imageTag: 'tag2', + imageDigest: 'sha256-2', + }, + ], + }); + + // WHEN + const event: Partial = { + RequestType: 'Delete', + ResourceProperties: { + ServiceToken: 'Foo', + RepositoryName: 'MyRepo', + }, + }; + await invokeHandler(event); + + // THEN + expect(mockECRClient.listImages).toHaveBeenCalledTimes(1); + expect(mockECRClient.listImages).toHaveBeenCalledWith({ repositoryName: 'MyRepo' }); + expect(mockECRClient.batchDeleteImage).toHaveBeenCalledTimes(1); + expect(mockECRClient.batchDeleteImage).toHaveBeenCalledWith({ + repositoryName: 'MyRepo', + imageIds: [ + { + imageTag: 'tag1', + imageDigest: 'sha256-1', + }, + { + imageTag: 'tag2', + imageDigest: 'sha256-2', + }, + ], + }); +}); + +test('does not empty repository if it is not tagged', async () => { + // GIVEN + givenNotTaggedForDeletion(); + mockECRClient.promise.mockResolvedValue({ // listedImages() call + imageIds: [ + { + imageTag: 'tag1', + imageDigest: 'sha256-1', + }, + { + imageTag: 'tag2', + imageDigest: 'sha256-2', + }, + ], + }); + + // WHEN + const event: Partial = { + RequestType: 'Delete', + ResourceProperties: { + ServiceToken: 'Foo', + RepositoryName: 'MyRepo', + }, + }; + await invokeHandler(event); + + // THEN + expect(mockECRClient.batchDeleteImage).not.toHaveBeenCalled(); +}); + +test('delete event where repo has many images does recurse appropriately', async () => { + // GIVEN + mockAwsPromise(mockECRClient.describeRepositories, { + repositories: [ + { repositoryArn: 'RepositoryArn', respositoryName: 'MyRepo' }, + ], + }); + + mockECRClient.promise // listedImages() call + .mockResolvedValueOnce({ + imageIds: [ + { + imageTag: 'tag1', + imageDigest: 'sha256-1', + }, + { + imageTag: 'tag2', + imageDigest: 'sha256-2', + }, + ], + nextToken: 'token1', + }) + .mockResolvedValueOnce(undefined) // batchDeleteImage() call + .mockResolvedValueOnce({ // listedImages() call + imageIds: [ + { + imageTag: 'tag3', + imageDigest: 'sha256-3', + }, + { + imageTag: 'tag4', + imageDigest: 'sha256-4', + }, + ], + }); + + // WHEN + const event: Partial = { + RequestType: 'Delete', + ResourceProperties: { + ServiceToken: 'Foo', + RepositoryName: 'MyRepo', + }, + }; + await invokeHandler(event); + + // THEN + expect(mockECRClient.describeRepositories).toHaveBeenCalledTimes(1); + expect(mockECRClient.listImages).toHaveBeenCalledTimes(2); + expect(mockECRClient.listImages).toHaveBeenCalledWith({ repositoryName: 'MyRepo' }); + expect(mockECRClient.batchDeleteImage).toHaveBeenCalledTimes(2); + expect(mockECRClient.batchDeleteImage).toHaveBeenNthCalledWith(1, { + repositoryName: 'MyRepo', + imageIds: [ + { + imageTag: 'tag1', + imageDigest: 'sha256-1', + }, + { + imageTag: 'tag2', + imageDigest: 'sha256-2', + }, + ], + }); + expect(mockECRClient.batchDeleteImage).toHaveBeenNthCalledWith(2, { + repositoryName: 'MyRepo', + imageIds: [ + { + imageTag: 'tag3', + imageDigest: 'sha256-3', + }, + { + imageTag: 'tag4', + imageDigest: 'sha256-4', + }, + ], + }); +}); + +test('does nothing when the repository does not exist', async () => { + // GIVEN + mockECRClient.promise.mockRejectedValue({ name: 'RepositoryNotFoundException' }); + + mockAwsPromise(mockECRClient.describeRepositories, { + repositories: [ + { repositoryArn: 'RepositoryArn', respositoryName: 'MyRepo' }, + ], + }); + + // WHEN + const event: Partial = { + RequestType: 'Delete', + ResourceProperties: { + ServiceToken: 'Foo', + RepositoryName: 'MyRepo', + }, + }; + await invokeHandler(event); + + expect(mockECRClient.batchDeleteImage).not.toHaveBeenCalled(); +}); + +// helper function to get around TypeScript expecting a complete event object, +// even though our tests only need some of the fields +async function invokeHandler(event: Partial) { + return handler(event as AWSLambda.CloudFormationCustomResourceEvent); +} + +function mockAwsPromise(fn: jest.Mock, value: A, when: 'once' | 'always' = 'always') { + (when === 'always' ? fn.mockReturnValue : fn.mockReturnValueOnce).call(fn, { + promise: () => value, + }); +} + +function givenTaggedForDeletion() { + mockAwsPromise(mockECRClient.listTagsForResource, { + tags: [ + { + Key: 'aws-cdk:auto-delete-images', + Value: 'true', + }, + ], + + }); +} + +function givenNotTaggedForDeletion() { + mockAwsPromise(mockECRClient.listTagsForResource, { + tags: [], + }); +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecr/test/integ.repository-auto-delete-images.js.snapshot/asset.6150227d515909a73f0bcde4b9e19b4b206cc65634027053380d700f6e53f08e/__entrypoint__.js b/packages/@aws-cdk/aws-ecr/test/integ.repository-auto-delete-images.js.snapshot/asset.6150227d515909a73f0bcde4b9e19b4b206cc65634027053380d700f6e53f08e/__entrypoint__.js new file mode 100644 index 0000000000000..c366685b1451b --- /dev/null +++ b/packages/@aws-cdk/aws-ecr/test/integ.repository-auto-delete-images.js.snapshot/asset.6150227d515909a73f0bcde4b9e19b4b206cc65634027053380d700f6e53f08e/__entrypoint__.js @@ -0,0 +1,147 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.withRetries = exports.handler = exports.external = void 0; +const https = require("https"); +const url = require("url"); +// for unit tests +exports.external = { + sendHttpRequest: defaultSendHttpRequest, + log: defaultLog, + includeStackTraces: true, + userHandlerIndex: './index', +}; +const CREATE_FAILED_PHYSICAL_ID_MARKER = 'AWSCDK::CustomResourceProviderFramework::CREATE_FAILED'; +const MISSING_PHYSICAL_ID_MARKER = 'AWSCDK::CustomResourceProviderFramework::MISSING_PHYSICAL_ID'; +async function handler(event, context) { + const sanitizedEvent = { ...event, ResponseURL: '...' }; + exports.external.log(JSON.stringify(sanitizedEvent, undefined, 2)); + // ignore DELETE event when the physical resource ID is the marker that + // indicates that this DELETE is a subsequent DELETE to a failed CREATE + // operation. + if (event.RequestType === 'Delete' && event.PhysicalResourceId === CREATE_FAILED_PHYSICAL_ID_MARKER) { + exports.external.log('ignoring DELETE event caused by a failed CREATE event'); + await submitResponse('SUCCESS', event); + return; + } + try { + // invoke the user handler. this is intentionally inside the try-catch to + // ensure that if there is an error it's reported as a failure to + // cloudformation (otherwise cfn waits). + // eslint-disable-next-line @typescript-eslint/no-require-imports + const userHandler = require(exports.external.userHandlerIndex).handler; + const result = await userHandler(sanitizedEvent, context); + // validate user response and create the combined event + const responseEvent = renderResponse(event, result); + // submit to cfn as success + await submitResponse('SUCCESS', responseEvent); + } + catch (e) { + const resp = { + ...event, + Reason: exports.external.includeStackTraces ? e.stack : e.message, + }; + if (!resp.PhysicalResourceId) { + // special case: if CREATE fails, which usually implies, we usually don't + // have a physical resource id. in this case, the subsequent DELETE + // operation does not have any meaning, and will likely fail as well. to + // address this, we use a marker so the provider framework can simply + // ignore the subsequent DELETE. + if (event.RequestType === 'Create') { + exports.external.log('CREATE failed, responding with a marker physical resource id so that the subsequent DELETE will be ignored'); + resp.PhysicalResourceId = CREATE_FAILED_PHYSICAL_ID_MARKER; + } + else { + // otherwise, if PhysicalResourceId is not specified, something is + // terribly wrong because all other events should have an ID. + exports.external.log(`ERROR: Malformed event. "PhysicalResourceId" is required: ${JSON.stringify(event)}`); + } + } + // this is an actual error, fail the activity altogether and exist. + await submitResponse('FAILED', resp); + } +} +exports.handler = handler; +function renderResponse(cfnRequest, handlerResponse = {}) { + // if physical ID is not returned, we have some defaults for you based + // on the request type. + const physicalResourceId = handlerResponse.PhysicalResourceId ?? cfnRequest.PhysicalResourceId ?? cfnRequest.RequestId; + // if we are in DELETE and physical ID was changed, it's an error. + if (cfnRequest.RequestType === 'Delete' && physicalResourceId !== cfnRequest.PhysicalResourceId) { + throw new Error(`DELETE: cannot change the physical resource ID from "${cfnRequest.PhysicalResourceId}" to "${handlerResponse.PhysicalResourceId}" during deletion`); + } + // merge request event and result event (result prevails). + return { + ...cfnRequest, + ...handlerResponse, + PhysicalResourceId: physicalResourceId, + }; +} +async function submitResponse(status, event) { + const json = { + Status: status, + Reason: event.Reason ?? status, + StackId: event.StackId, + RequestId: event.RequestId, + PhysicalResourceId: event.PhysicalResourceId || MISSING_PHYSICAL_ID_MARKER, + LogicalResourceId: event.LogicalResourceId, + NoEcho: event.NoEcho, + Data: event.Data, + }; + exports.external.log('submit response to cloudformation', json); + const responseBody = JSON.stringify(json); + const parsedUrl = url.parse(event.ResponseURL); + const req = { + hostname: parsedUrl.hostname, + path: parsedUrl.path, + method: 'PUT', + headers: { + 'content-type': '', + 'content-length': Buffer.byteLength(responseBody, 'utf8'), + }, + }; + const retryOptions = { + attempts: 5, + sleep: 1000, + }; + await withRetries(retryOptions, exports.external.sendHttpRequest)(req, responseBody); +} +async function defaultSendHttpRequest(options, responseBody) { + return new Promise((resolve, reject) => { + try { + const request = https.request(options, _ => resolve()); + request.on('error', reject); + request.write(responseBody); + request.end(); + } + catch (e) { + reject(e); + } + }); +} +function defaultLog(fmt, ...params) { + // eslint-disable-next-line no-console + console.log(fmt, ...params); +} +function withRetries(options, fn) { + return async (...xs) => { + let attempts = options.attempts; + let ms = options.sleep; + while (true) { + try { + return await fn(...xs); + } + catch (e) { + if (attempts-- <= 0) { + throw e; + } + await sleep(Math.floor(Math.random() * ms)); + ms *= 2; + } + } + }; +} +exports.withRetries = withRetries; +async function sleep(ms) { + return new Promise((ok) => setTimeout(ok, ms)); +} +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibm9kZWpzLWVudHJ5cG9pbnQuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJub2RlanMtZW50cnlwb2ludC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7QUFBQSwrQkFBK0I7QUFDL0IsMkJBQTJCO0FBRTNCLGlCQUFpQjtBQUNKLFFBQUEsUUFBUSxHQUFHO0lBQ3RCLGVBQWUsRUFBRSxzQkFBc0I7SUFDdkMsR0FBRyxFQUFFLFVBQVU7SUFDZixrQkFBa0IsRUFBRSxJQUFJO0lBQ3hCLGdCQUFnQixFQUFFLFNBQVM7Q0FDNUIsQ0FBQztBQUVGLE1BQU0sZ0NBQWdDLEdBQUcsd0RBQXdELENBQUM7QUFDbEcsTUFBTSwwQkFBMEIsR0FBRyw4REFBOEQsQ0FBQztBQVczRixLQUFLLFVBQVUsT0FBTyxDQUFDLEtBQWtELEVBQUUsT0FBMEI7SUFDMUcsTUFBTSxjQUFjLEdBQUcsRUFBRSxHQUFHLEtBQUssRUFBRSxXQUFXLEVBQUUsS0FBSyxFQUFFLENBQUM7SUFDeEQsZ0JBQVEsQ0FBQyxHQUFHLENBQUMsSUFBSSxDQUFDLFNBQVMsQ0FBQyxjQUFjLEVBQUUsU0FBUyxFQUFFLENBQUMsQ0FBQyxDQUFDLENBQUM7SUFFM0QsdUVBQXVFO0lBQ3ZFLHVFQUF1RTtJQUN2RSxhQUFhO0lBQ2IsSUFBSSxLQUFLLENBQUMsV0FBVyxLQUFLLFFBQVEsSUFBSSxLQUFLLENBQUMsa0JBQWtCLEtBQUssZ0NBQWdDLEVBQUU7UUFDbkcsZ0JBQVEsQ0FBQyxHQUFHLENBQUMsdURBQXVELENBQUMsQ0FBQztRQUN0RSxNQUFNLGNBQWMsQ0FBQyxTQUFTLEVBQUUsS0FBSyxDQUFDLENBQUM7UUFDdkMsT0FBTztLQUNSO0lBRUQsSUFBSTtRQUNGLHlFQUF5RTtRQUN6RSxpRUFBaUU7UUFDakUsd0NBQXdDO1FBQ3hDLGlFQUFpRTtRQUNqRSxNQUFNLFdBQVcsR0FBWSxPQUFPLENBQUMsZ0JBQVEsQ0FBQyxnQkFBZ0IsQ0FBQyxDQUFDLE9BQU8sQ0FBQztRQUN4RSxNQUFNLE1BQU0sR0FBRyxNQUFNLFdBQVcsQ0FBQyxjQUFjLEVBQUUsT0FBTyxDQUFDLENBQUM7UUFFMUQsdURBQXVEO1FBQ3ZELE1BQU0sYUFBYSxHQUFHLGNBQWMsQ0FBQyxLQUFLLEVBQUUsTUFBTSxDQUFDLENBQUM7UUFFcEQsMkJBQTJCO1FBQzNCLE1BQU0sY0FBYyxDQUFDLFNBQVMsRUFBRSxhQUFhLENBQUMsQ0FBQztLQUNoRDtJQUFDLE9BQU8sQ0FBQyxFQUFFO1FBQ1YsTUFBTSxJQUFJLEdBQWE7WUFDckIsR0FBRyxLQUFLO1lBQ1IsTUFBTSxFQUFFLGdCQUFRLENBQUMsa0JBQWtCLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxLQUFLLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxPQUFPO1NBQzFELENBQUM7UUFFRixJQUFJLENBQUMsSUFBSSxDQUFDLGtCQUFrQixFQUFFO1lBQzVCLHlFQUF5RTtZQUN6RSxtRUFBbUU7WUFDbkUsd0VBQXdFO1lBQ3hFLHFFQUFxRTtZQUNyRSxnQ0FBZ0M7WUFDaEMsSUFBSSxLQUFLLENBQUMsV0FBVyxLQUFLLFFBQVEsRUFBRTtnQkFDbEMsZ0JBQVEsQ0FBQyxHQUFHLENBQUMsNEdBQTRHLENBQUMsQ0FBQztnQkFDM0gsSUFBSSxDQUFDLGtCQUFrQixHQUFHLGdDQUFnQyxDQUFDO2FBQzVEO2lCQUFNO2dCQUNMLGtFQUFrRTtnQkFDbEUsNkRBQTZEO2dCQUM3RCxnQkFBUSxDQUFDLEdBQUcsQ0FBQyw2REFBNkQsSUFBSSxDQUFDLFNBQVMsQ0FBQyxLQUFLLENBQUMsRUFBRSxDQUFDLENBQUM7YUFDcEc7U0FDRjtRQUVELG1FQUFtRTtRQUNuRSxNQUFNLGNBQWMsQ0FBQyxRQUFRLEVBQUUsSUFBSSxDQUFDLENBQUM7S0FDdEM7QUFDSCxDQUFDO0FBbkRELDBCQW1EQztBQUVELFNBQVMsY0FBYyxDQUNyQixVQUF5RixFQUN6RixrQkFBMEMsRUFBRztJQUU3QyxzRUFBc0U7SUFDdEUsdUJBQXVCO0lBQ3ZCLE1BQU0sa0JBQWtCLEdBQUcsZUFBZSxDQUFDLGtCQUFrQixJQUFJLFVBQVUsQ0FBQyxrQkFBa0IsSUFBSSxVQUFVLENBQUMsU0FBUyxDQUFDO0lBRXZILGtFQUFrRTtJQUNsRSxJQUFJLFVBQVUsQ0FBQyxXQUFXLEtBQUssUUFBUSxJQUFJLGtCQUFrQixLQUFLLFVBQVUsQ0FBQyxrQkFBa0IsRUFBRTtRQUMvRixNQUFNLElBQUksS0FBSyxDQUFDLHdEQUF3RCxVQUFVLENBQUMsa0JBQWtCLFNBQVMsZUFBZSxDQUFDLGtCQUFrQixtQkFBbUIsQ0FBQyxDQUFDO0tBQ3RLO0lBRUQsMERBQTBEO0lBQzFELE9BQU87UUFDTCxHQUFHLFVBQVU7UUFDYixHQUFHLGVBQWU7UUFDbEIsa0JBQWtCLEVBQUUsa0JBQWtCO0tBQ3ZDLENBQUM7QUFDSixDQUFDO0FBRUQsS0FBSyxVQUFVLGNBQWMsQ0FBQyxNQUE0QixFQUFFLEtBQWU7SUFDekUsTUFBTSxJQUFJLEdBQW1EO1FBQzNELE1BQU0sRUFBRSxNQUFNO1FBQ2QsTUFBTSxFQUFFLEtBQUssQ0FBQyxNQUFNLElBQUksTUFBTTtRQUM5QixPQUFPLEVBQUUsS0FBSyxDQUFDLE9BQU87UUFDdEIsU0FBUyxFQUFFLEtBQUssQ0FBQyxTQUFTO1FBQzFCLGtCQUFrQixFQUFFLEtBQUssQ0FBQyxrQkFBa0IsSUFBSSwwQkFBMEI7UUFDMUUsaUJBQWlCLEVBQUUsS0FBSyxDQUFDLGlCQUFpQjtRQUMxQyxNQUFNLEVBQUUsS0FBSyxDQUFDLE1BQU07UUFDcEIsSUFBSSxFQUFFLEtBQUssQ0FBQyxJQUFJO0tBQ2pCLENBQUM7SUFFRixnQkFBUSxDQUFDLEdBQUcsQ0FBQyxtQ0FBbUMsRUFBRSxJQUFJLENBQUMsQ0FBQztJQUV4RCxNQUFNLFlBQVksR0FBRyxJQUFJLENBQUMsU0FBUyxDQUFDLElBQUksQ0FBQyxDQUFDO0lBQzFDLE1BQU0sU0FBUyxHQUFHLEdBQUcsQ0FBQyxLQUFLLENBQUMsS0FBSyxDQUFDLFdBQVcsQ0FBQyxDQUFDO0lBQy9DLE1BQU0sR0FBRyxHQUFHO1FBQ1YsUUFBUSxFQUFFLFNBQVMsQ0FBQyxRQUFRO1FBQzVCLElBQUksRUFBRSxTQUFTLENBQUMsSUFBSTtRQUNwQixNQUFNLEVBQUUsS0FBSztRQUNiLE9BQU8sRUFBRTtZQUNQLGNBQWMsRUFBRSxFQUFFO1lBQ2xCLGdCQUFnQixFQUFFLE1BQU0sQ0FBQyxVQUFVLENBQUMsWUFBWSxFQUFFLE1BQU0sQ0FBQztTQUMxRDtLQUNGLENBQUM7SUFFRixNQUFNLFlBQVksR0FBRztRQUNuQixRQUFRLEVBQUUsQ0FBQztRQUNYLEtBQUssRUFBRSxJQUFJO0tBQ1osQ0FBQztJQUNGLE1BQU0sV0FBVyxDQUFDLFlBQVksRUFBRSxnQkFBUSxDQUFDLGVBQWUsQ0FBQyxDQUFDLEdBQUcsRUFBRSxZQUFZLENBQUMsQ0FBQztBQUMvRSxDQUFDO0FBRUQsS0FBSyxVQUFVLHNCQUFzQixDQUFDLE9BQTZCLEVBQUUsWUFBb0I7SUFDdkYsT0FBTyxJQUFJLE9BQU8sQ0FBQyxDQUFDLE9BQU8sRUFBRSxNQUFNLEVBQUUsRUFBRTtRQUNyQyxJQUFJO1lBQ0YsTUFBTSxPQUFPLEdBQUcsS0FBSyxDQUFDLE9BQU8sQ0FBQyxPQUFPLEVBQUUsQ0FBQyxDQUFDLEVBQUUsQ0FBQyxPQUFPLEVBQUUsQ0FBQyxDQUFDO1lBQ3ZELE9BQU8sQ0FBQyxFQUFFLENBQUMsT0FBTyxFQUFFLE1BQU0sQ0FBQyxDQUFDO1lBQzVCLE9BQU8sQ0FBQyxLQUFLLENBQUMsWUFBWSxDQUFDLENBQUM7WUFDNUIsT0FBTyxDQUFDLEdBQUcsRUFBRSxDQUFDO1NBQ2Y7UUFBQyxPQUFPLENBQUMsRUFBRTtZQUNWLE1BQU0sQ0FBQyxDQUFDLENBQUMsQ0FBQztTQUNYO0lBQ0gsQ0FBQyxDQUFDLENBQUM7QUFDTCxDQUFDO0FBRUQsU0FBUyxVQUFVLENBQUMsR0FBVyxFQUFFLEdBQUcsTUFBYTtJQUMvQyxzQ0FBc0M7SUFDdEMsT0FBTyxDQUFDLEdBQUcsQ0FBQyxHQUFHLEVBQUUsR0FBRyxNQUFNLENBQUMsQ0FBQztBQUM5QixDQUFDO0FBU0QsU0FBZ0IsV0FBVyxDQUEwQixPQUFxQixFQUFFLEVBQTRCO0lBQ3RHLE9BQU8sS0FBSyxFQUFFLEdBQUcsRUFBSyxFQUFFLEVBQUU7UUFDeEIsSUFBSSxRQUFRLEdBQUcsT0FBTyxDQUFDLFFBQVEsQ0FBQztRQUNoQyxJQUFJLEVBQUUsR0FBRyxPQUFPLENBQUMsS0FBSyxDQUFDO1FBQ3ZCLE9BQU8sSUFBSSxFQUFFO1lBQ1gsSUFBSTtnQkFDRixPQUFPLE1BQU0sRUFBRSxDQUFDLEdBQUcsRUFBRSxDQUFDLENBQUM7YUFDeEI7WUFBQyxPQUFPLENBQUMsRUFBRTtnQkFDVixJQUFJLFFBQVEsRUFBRSxJQUFJLENBQUMsRUFBRTtvQkFDbkIsTUFBTSxDQUFDLENBQUM7aUJBQ1Q7Z0JBQ0QsTUFBTSxLQUFLLENBQUMsSUFBSSxDQUFDLEtBQUssQ0FBQyxJQUFJLENBQUMsTUFBTSxFQUFFLEdBQUcsRUFBRSxDQUFDLENBQUMsQ0FBQztnQkFDNUMsRUFBRSxJQUFJLENBQUMsQ0FBQzthQUNUO1NBQ0Y7SUFDSCxDQUFDLENBQUM7QUFDSixDQUFDO0FBaEJELGtDQWdCQztBQUVELEtBQUssVUFBVSxLQUFLLENBQUMsRUFBVTtJQUM3QixPQUFPLElBQUksT0FBTyxDQUFDLENBQUMsRUFBRSxFQUFFLEVBQUUsQ0FBQyxVQUFVLENBQUMsRUFBRSxFQUFFLEVBQUUsQ0FBQyxDQUFDLENBQUM7QUFDakQsQ0FBQyIsInNvdXJjZXNDb250ZW50IjpbImltcG9ydCAqIGFzIGh0dHBzIGZyb20gJ2h0dHBzJztcbmltcG9ydCAqIGFzIHVybCBmcm9tICd1cmwnO1xuXG4vLyBmb3IgdW5pdCB0ZXN0c1xuZXhwb3J0IGNvbnN0IGV4dGVybmFsID0ge1xuICBzZW5kSHR0cFJlcXVlc3Q6IGRlZmF1bHRTZW5kSHR0cFJlcXVlc3QsXG4gIGxvZzogZGVmYXVsdExvZyxcbiAgaW5jbHVkZVN0YWNrVHJhY2VzOiB0cnVlLFxuICB1c2VySGFuZGxlckluZGV4OiAnLi9pbmRleCcsXG59O1xuXG5jb25zdCBDUkVBVEVfRkFJTEVEX1BIWVNJQ0FMX0lEX01BUktFUiA9ICdBV1NDREs6OkN1c3RvbVJlc291cmNlUHJvdmlkZXJGcmFtZXdvcms6OkNSRUFURV9GQUlMRUQnO1xuY29uc3QgTUlTU0lOR19QSFlTSUNBTF9JRF9NQVJLRVIgPSAnQVdTQ0RLOjpDdXN0b21SZXNvdXJjZVByb3ZpZGVyRnJhbWV3b3JrOjpNSVNTSU5HX1BIWVNJQ0FMX0lEJztcblxuZXhwb3J0IHR5cGUgUmVzcG9uc2UgPSBBV1NMYW1iZGEuQ2xvdWRGb3JtYXRpb25DdXN0b21SZXNvdXJjZUV2ZW50ICYgSGFuZGxlclJlc3BvbnNlO1xuZXhwb3J0IHR5cGUgSGFuZGxlciA9IChldmVudDogQVdTTGFtYmRhLkNsb3VkRm9ybWF0aW9uQ3VzdG9tUmVzb3VyY2VFdmVudCwgY29udGV4dDogQVdTTGFtYmRhLkNvbnRleHQpID0+IFByb21pc2U8SGFuZGxlclJlc3BvbnNlIHwgdm9pZD47XG5leHBvcnQgdHlwZSBIYW5kbGVyUmVzcG9uc2UgPSB1bmRlZmluZWQgfCB7XG4gIERhdGE/OiBhbnk7XG4gIFBoeXNpY2FsUmVzb3VyY2VJZD86IHN0cmluZztcbiAgUmVhc29uPzogc3RyaW5nO1xuICBOb0VjaG8/OiBib29sZWFuO1xufTtcblxuZXhwb3J0IGFzeW5jIGZ1bmN0aW9uIGhhbmRsZXIoZXZlbnQ6IEFXU0xhbWJkYS5DbG91ZEZvcm1hdGlvbkN1c3RvbVJlc291cmNlRXZlbnQsIGNvbnRleHQ6IEFXU0xhbWJkYS5Db250ZXh0KSB7XG4gIGNvbnN0IHNhbml0aXplZEV2ZW50ID0geyAuLi5ldmVudCwgUmVzcG9uc2VVUkw6ICcuLi4nIH07XG4gIGV4dGVybmFsLmxvZyhKU09OLnN0cmluZ2lmeShzYW5pdGl6ZWRFdmVudCwgdW5kZWZpbmVkLCAyKSk7XG5cbiAgLy8gaWdub3JlIERFTEVURSBldmVudCB3aGVuIHRoZSBwaHlzaWNhbCByZXNvdXJjZSBJRCBpcyB0aGUgbWFya2VyIHRoYXRcbiAgLy8gaW5kaWNhdGVzIHRoYXQgdGhpcyBERUxFVEUgaXMgYSBzdWJzZXF1ZW50IERFTEVURSB0byBhIGZhaWxlZCBDUkVBVEVcbiAgLy8gb3BlcmF0aW9uLlxuICBpZiAoZXZlbnQuUmVxdWVzdFR5cGUgPT09ICdEZWxldGUnICYmIGV2ZW50LlBoeXNpY2FsUmVzb3VyY2VJZCA9PT0gQ1JFQVRFX0ZBSUxFRF9QSFlTSUNBTF9JRF9NQVJLRVIpIHtcbiAgICBleHRlcm5hbC5sb2coJ2lnbm9yaW5nIERFTEVURSBldmVudCBjYXVzZWQgYnkgYSBmYWlsZWQgQ1JFQVRFIGV2ZW50Jyk7XG4gICAgYXdhaXQgc3VibWl0UmVzcG9uc2UoJ1NVQ0NFU1MnLCBldmVudCk7XG4gICAgcmV0dXJuO1xuICB9XG5cbiAgdHJ5IHtcbiAgICAvLyBpbnZva2UgdGhlIHVzZXIgaGFuZGxlci4gdGhpcyBpcyBpbnRlbnRpb25hbGx5IGluc2lkZSB0aGUgdHJ5LWNhdGNoIHRvXG4gICAgLy8gZW5zdXJlIHRoYXQgaWYgdGhlcmUgaXMgYW4gZXJyb3IgaXQncyByZXBvcnRlZCBhcyBhIGZhaWx1cmUgdG9cbiAgICAvLyBjbG91ZGZvcm1hdGlvbiAob3RoZXJ3aXNlIGNmbiB3YWl0cykuXG4gICAgLy8gZXNsaW50LWRpc2FibGUtbmV4dC1saW5lIEB0eXBlc2NyaXB0LWVzbGludC9uby1yZXF1aXJlLWltcG9ydHNcbiAgICBjb25zdCB1c2VySGFuZGxlcjogSGFuZGxlciA9IHJlcXVpcmUoZXh0ZXJuYWwudXNlckhhbmRsZXJJbmRleCkuaGFuZGxlcjtcbiAgICBjb25zdCByZXN1bHQgPSBhd2FpdCB1c2VySGFuZGxlcihzYW5pdGl6ZWRFdmVudCwgY29udGV4dCk7XG5cbiAgICAvLyB2YWxpZGF0ZSB1c2VyIHJlc3BvbnNlIGFuZCBjcmVhdGUgdGhlIGNvbWJpbmVkIGV2ZW50XG4gICAgY29uc3QgcmVzcG9uc2VFdmVudCA9IHJlbmRlclJlc3BvbnNlKGV2ZW50LCByZXN1bHQpO1xuXG4gICAgLy8gc3VibWl0IHRvIGNmbiBhcyBzdWNjZXNzXG4gICAgYXdhaXQgc3VibWl0UmVzcG9uc2UoJ1NVQ0NFU1MnLCByZXNwb25zZUV2ZW50KTtcbiAgfSBjYXRjaCAoZSkge1xuICAgIGNvbnN0IHJlc3A6IFJlc3BvbnNlID0ge1xuICAgICAgLi4uZXZlbnQsXG4gICAgICBSZWFzb246IGV4dGVybmFsLmluY2x1ZGVTdGFja1RyYWNlcyA/IGUuc3RhY2sgOiBlLm1lc3NhZ2UsXG4gICAgfTtcblxuICAgIGlmICghcmVzcC5QaHlzaWNhbFJlc291cmNlSWQpIHtcbiAgICAgIC8vIHNwZWNpYWwgY2FzZTogaWYgQ1JFQVRFIGZhaWxzLCB3aGljaCB1c3VhbGx5IGltcGxpZXMsIHdlIHVzdWFsbHkgZG9uJ3RcbiAgICAgIC8vIGhhdmUgYSBwaHlzaWNhbCByZXNvdXJjZSBpZC4gaW4gdGhpcyBjYXNlLCB0aGUgc3Vic2VxdWVudCBERUxFVEVcbiAgICAgIC8vIG9wZXJhdGlvbiBkb2VzIG5vdCBoYXZlIGFueSBtZWFuaW5nLCBhbmQgd2lsbCBsaWtlbHkgZmFpbCBhcyB3ZWxsLiB0b1xuICAgICAgLy8gYWRkcmVzcyB0aGlzLCB3ZSB1c2UgYSBtYXJrZXIgc28gdGhlIHByb3ZpZGVyIGZyYW1ld29yayBjYW4gc2ltcGx5XG4gICAgICAvLyBpZ25vcmUgdGhlIHN1YnNlcXVlbnQgREVMRVRFLlxuICAgICAgaWYgKGV2ZW50LlJlcXVlc3RUeXBlID09PSAnQ3JlYXRlJykge1xuICAgICAgICBleHRlcm5hbC5sb2coJ0NSRUFURSBmYWlsZWQsIHJlc3BvbmRpbmcgd2l0aCBhIG1hcmtlciBwaHlzaWNhbCByZXNvdXJjZSBpZCBzbyB0aGF0IHRoZSBzdWJzZXF1ZW50IERFTEVURSB3aWxsIGJlIGlnbm9yZWQnKTtcbiAgICAgICAgcmVzcC5QaHlzaWNhbFJlc291cmNlSWQgPSBDUkVBVEVfRkFJTEVEX1BIWVNJQ0FMX0lEX01BUktFUjtcbiAgICAgIH0gZWxzZSB7XG4gICAgICAgIC8vIG90aGVyd2lzZSwgaWYgUGh5c2ljYWxSZXNvdXJjZUlkIGlzIG5vdCBzcGVjaWZpZWQsIHNvbWV0aGluZyBpc1xuICAgICAgICAvLyB0ZXJyaWJseSB3cm9uZyBiZWNhdXNlIGFsbCBvdGhlciBldmVudHMgc2hvdWxkIGhhdmUgYW4gSUQuXG4gICAgICAgIGV4dGVybmFsLmxvZyhgRVJST1I6IE1hbGZvcm1lZCBldmVudC4gXCJQaHlzaWNhbFJlc291cmNlSWRcIiBpcyByZXF1aXJlZDogJHtKU09OLnN0cmluZ2lmeShldmVudCl9YCk7XG4gICAgICB9XG4gICAgfVxuXG4gICAgLy8gdGhpcyBpcyBhbiBhY3R1YWwgZXJyb3IsIGZhaWwgdGhlIGFjdGl2aXR5IGFsdG9nZXRoZXIgYW5kIGV4aXN0LlxuICAgIGF3YWl0IHN1Ym1pdFJlc3BvbnNlKCdGQUlMRUQnLCByZXNwKTtcbiAgfVxufVxuXG5mdW5jdGlvbiByZW5kZXJSZXNwb25zZShcbiAgY2ZuUmVxdWVzdDogQVdTTGFtYmRhLkNsb3VkRm9ybWF0aW9uQ3VzdG9tUmVzb3VyY2VFdmVudCAmIHsgUGh5c2ljYWxSZXNvdXJjZUlkPzogc3RyaW5nIH0sXG4gIGhhbmRsZXJSZXNwb25zZTogdm9pZCB8IEhhbmRsZXJSZXNwb25zZSA9IHsgfSk6IFJlc3BvbnNlIHtcblxuICAvLyBpZiBwaHlzaWNhbCBJRCBpcyBub3QgcmV0dXJuZWQsIHdlIGhhdmUgc29tZSBkZWZhdWx0cyBmb3IgeW91IGJhc2VkXG4gIC8vIG9uIHRoZSByZXF1ZXN0IHR5cGUuXG4gIGNvbnN0IHBoeXNpY2FsUmVzb3VyY2VJZCA9IGhhbmRsZXJSZXNwb25zZS5QaHlzaWNhbFJlc291cmNlSWQgPz8gY2ZuUmVxdWVzdC5QaHlzaWNhbFJlc291cmNlSWQgPz8gY2ZuUmVxdWVzdC5SZXF1ZXN0SWQ7XG5cbiAgLy8gaWYgd2UgYXJlIGluIERFTEVURSBhbmQgcGh5c2ljYWwgSUQgd2FzIGNoYW5nZWQsIGl0J3MgYW4gZXJyb3IuXG4gIGlmIChjZm5SZXF1ZXN0LlJlcXVlc3RUeXBlID09PSAnRGVsZXRlJyAmJiBwaHlzaWNhbFJlc291cmNlSWQgIT09IGNmblJlcXVlc3QuUGh5c2ljYWxSZXNvdXJjZUlkKSB7XG4gICAgdGhyb3cgbmV3IEVycm9yKGBERUxFVEU6IGNhbm5vdCBjaGFuZ2UgdGhlIHBoeXNpY2FsIHJlc291cmNlIElEIGZyb20gXCIke2NmblJlcXVlc3QuUGh5c2ljYWxSZXNvdXJjZUlkfVwiIHRvIFwiJHtoYW5kbGVyUmVzcG9uc2UuUGh5c2ljYWxSZXNvdXJjZUlkfVwiIGR1cmluZyBkZWxldGlvbmApO1xuICB9XG5cbiAgLy8gbWVyZ2UgcmVxdWVzdCBldmVudCBhbmQgcmVzdWx0IGV2ZW50IChyZXN1bHQgcHJldmFpbHMpLlxuICByZXR1cm4ge1xuICAgIC4uLmNmblJlcXVlc3QsXG4gICAgLi4uaGFuZGxlclJlc3BvbnNlLFxuICAgIFBoeXNpY2FsUmVzb3VyY2VJZDogcGh5c2ljYWxSZXNvdXJjZUlkLFxuICB9O1xufVxuXG5hc3luYyBmdW5jdGlvbiBzdWJtaXRSZXNwb25zZShzdGF0dXM6ICdTVUNDRVNTJyB8ICdGQUlMRUQnLCBldmVudDogUmVzcG9uc2UpIHtcbiAgY29uc3QganNvbjogQVdTTGFtYmRhLkNsb3VkRm9ybWF0aW9uQ3VzdG9tUmVzb3VyY2VSZXNwb25zZSA9IHtcbiAgICBTdGF0dXM6IHN0YXR1cyxcbiAgICBSZWFzb246IGV2ZW50LlJlYXNvbiA/PyBzdGF0dXMsXG4gICAgU3RhY2tJZDogZXZlbnQuU3RhY2tJZCxcbiAgICBSZXF1ZXN0SWQ6IGV2ZW50LlJlcXVlc3RJZCxcbiAgICBQaHlzaWNhbFJlc291cmNlSWQ6IGV2ZW50LlBoeXNpY2FsUmVzb3VyY2VJZCB8fCBNSVNTSU5HX1BIWVNJQ0FMX0lEX01BUktFUixcbiAgICBMb2dpY2FsUmVzb3VyY2VJZDogZXZlbnQuTG9naWNhbFJlc291cmNlSWQsXG4gICAgTm9FY2hvOiBldmVudC5Ob0VjaG8sXG4gICAgRGF0YTogZXZlbnQuRGF0YSxcbiAgfTtcblxuICBleHRlcm5hbC5sb2coJ3N1Ym1pdCByZXNwb25zZSB0byBjbG91ZGZvcm1hdGlvbicsIGpzb24pO1xuXG4gIGNvbnN0IHJlc3BvbnNlQm9keSA9IEpTT04uc3RyaW5naWZ5KGpzb24pO1xuICBjb25zdCBwYXJzZWRVcmwgPSB1cmwucGFyc2UoZXZlbnQuUmVzcG9uc2VVUkwpO1xuICBjb25zdCByZXEgPSB7XG4gICAgaG9zdG5hbWU6IHBhcnNlZFVybC5ob3N0bmFtZSxcbiAgICBwYXRoOiBwYXJzZWRVcmwucGF0aCxcbiAgICBtZXRob2Q6ICdQVVQnLFxuICAgIGhlYWRlcnM6IHtcbiAgICAgICdjb250ZW50LXR5cGUnOiAnJyxcbiAgICAgICdjb250ZW50LWxlbmd0aCc6IEJ1ZmZlci5ieXRlTGVuZ3RoKHJlc3BvbnNlQm9keSwgJ3V0ZjgnKSxcbiAgICB9LFxuICB9O1xuXG4gIGNvbnN0IHJldHJ5T3B0aW9ucyA9IHtcbiAgICBhdHRlbXB0czogNSxcbiAgICBzbGVlcDogMTAwMCxcbiAgfTtcbiAgYXdhaXQgd2l0aFJldHJpZXMocmV0cnlPcHRpb25zLCBleHRlcm5hbC5zZW5kSHR0cFJlcXVlc3QpKHJlcSwgcmVzcG9uc2VCb2R5KTtcbn1cblxuYXN5bmMgZnVuY3Rpb24gZGVmYXVsdFNlbmRIdHRwUmVxdWVzdChvcHRpb25zOiBodHRwcy5SZXF1ZXN0T3B0aW9ucywgcmVzcG9uc2VCb2R5OiBzdHJpbmcpOiBQcm9taXNlPHZvaWQ+IHtcbiAgcmV0dXJuIG5ldyBQcm9taXNlKChyZXNvbHZlLCByZWplY3QpID0+IHtcbiAgICB0cnkge1xuICAgICAgY29uc3QgcmVxdWVzdCA9IGh0dHBzLnJlcXVlc3Qob3B0aW9ucywgXyA9PiByZXNvbHZlKCkpO1xuICAgICAgcmVxdWVzdC5vbignZXJyb3InLCByZWplY3QpO1xuICAgICAgcmVxdWVzdC53cml0ZShyZXNwb25zZUJvZHkpO1xuICAgICAgcmVxdWVzdC5lbmQoKTtcbiAgICB9IGNhdGNoIChlKSB7XG4gICAgICByZWplY3QoZSk7XG4gICAgfVxuICB9KTtcbn1cblxuZnVuY3Rpb24gZGVmYXVsdExvZyhmbXQ6IHN0cmluZywgLi4ucGFyYW1zOiBhbnlbXSkge1xuICAvLyBlc2xpbnQtZGlzYWJsZS1uZXh0LWxpbmUgbm8tY29uc29sZVxuICBjb25zb2xlLmxvZyhmbXQsIC4uLnBhcmFtcyk7XG59XG5cbmV4cG9ydCBpbnRlcmZhY2UgUmV0cnlPcHRpb25zIHtcbiAgLyoqIEhvdyBtYW55IHJldHJpZXMgKHdpbGwgYXQgbGVhc3QgdHJ5IG9uY2UpICovXG4gIHJlYWRvbmx5IGF0dGVtcHRzOiBudW1iZXI7XG4gIC8qKiBTbGVlcCBiYXNlLCBpbiBtcyAqL1xuICByZWFkb25seSBzbGVlcDogbnVtYmVyO1xufVxuXG5leHBvcnQgZnVuY3Rpb24gd2l0aFJldHJpZXM8QSBleHRlbmRzIEFycmF5PGFueT4sIEI+KG9wdGlvbnM6IFJldHJ5T3B0aW9ucywgZm46ICguLi54czogQSkgPT4gUHJvbWlzZTxCPik6ICguLi54czogQSkgPT4gUHJvbWlzZTxCPiB7XG4gIHJldHVybiBhc3luYyAoLi4ueHM6IEEpID0+IHtcbiAgICBsZXQgYXR0ZW1wdHMgPSBvcHRpb25zLmF0dGVtcHRzO1xuICAgIGxldCBtcyA9IG9wdGlvbnMuc2xlZXA7XG4gICAgd2hpbGUgKHRydWUpIHtcbiAgICAgIHRyeSB7XG4gICAgICAgIHJldHVybiBhd2FpdCBmbiguLi54cyk7XG4gICAgICB9IGNhdGNoIChlKSB7XG4gICAgICAgIGlmIChhdHRlbXB0cy0tIDw9IDApIHtcbiAgICAgICAgICB0aHJvdyBlO1xuICAgICAgICB9XG4gICAgICAgIGF3YWl0IHNsZWVwKE1hdGguZmxvb3IoTWF0aC5yYW5kb20oKSAqIG1zKSk7XG4gICAgICAgIG1zICo9IDI7XG4gICAgICB9XG4gICAgfVxuICB9O1xufVxuXG5hc3luYyBmdW5jdGlvbiBzbGVlcChtczogbnVtYmVyKTogUHJvbWlzZTx2b2lkPiB7XG4gIHJldHVybiBuZXcgUHJvbWlzZSgob2spID0+IHNldFRpbWVvdXQob2ssIG1zKSk7XG59Il19 \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecr/test/integ.repository-auto-delete-images.js.snapshot/asset.6150227d515909a73f0bcde4b9e19b4b206cc65634027053380d700f6e53f08e/index.js b/packages/@aws-cdk/aws-ecr/test/integ.repository-auto-delete-images.js.snapshot/asset.6150227d515909a73f0bcde4b9e19b4b206cc65634027053380d700f6e53f08e/index.js new file mode 100644 index 0000000000000..442bb4f1c65b8 --- /dev/null +++ b/packages/@aws-cdk/aws-ecr/test/integ.repository-auto-delete-images.js.snapshot/asset.6150227d515909a73f0bcde4b9e19b4b206cc65634027053380d700f6e53f08e/index.js @@ -0,0 +1,87 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.handler = void 0; +// eslint-disable-next-line import/no-extraneous-dependencies +const aws_sdk_1 = require("aws-sdk"); +const AUTO_DELETE_IMAGES_TAG = 'aws-cdk:auto-delete-images'; +const ecr = new aws_sdk_1.ECR(); +async function handler(event) { + switch (event.RequestType) { + case 'Create': + break; + case 'Update': + return onUpdate(event); + case 'Delete': + return onDelete(event.ResourceProperties?.RepositoryName); + } +} +exports.handler = handler; +async function onUpdate(event) { + const updateEvent = event; + const oldRepositoryName = updateEvent.OldResourceProperties?.RepositoryName; + const newRepositoryName = updateEvent.ResourceProperties?.RepositoryName; + const repositoryNameHasChanged = (newRepositoryName && oldRepositoryName) + && (newRepositoryName !== oldRepositoryName); + /* If the name of the repository has changed, CloudFormation will try to delete the repository + and create a new one with the new name. So we have to delete the images in the + repository so that this operation does not fail. */ + if (repositoryNameHasChanged) { + return onDelete(oldRepositoryName); + } +} +/** + * Recursively delete all images in the repository + * + * @param ECR.ListImagesRequest the repositoryName & nextToken if presented + */ +async function emptyRepository(params) { + const listedImages = await ecr.listImages(params).promise(); + const imageIds = listedImages?.imageIds ?? []; + const nextToken = listedImages.nextToken ?? null; + if (imageIds.length === 0) { + return; + } + await ecr.batchDeleteImage({ + repositoryName: params.repositoryName, + imageIds, + }).promise(); + if (nextToken) { + await emptyRepository({ + ...params, + nextToken, + }); + } +} +async function onDelete(repositoryName) { + if (!repositoryName) { + throw new Error('No RepositoryName was provided.'); + } + const response = await ecr.describeRepositories({ repositoryNames: [repositoryName] }).promise(); + const repository = response.repositories?.find(repo => repo.repositoryName === repositoryName); + if (!await isRepositoryTaggedForDeletion(repository?.repositoryArn)) { + process.stdout.write(`Repository does not have '${AUTO_DELETE_IMAGES_TAG}' tag, skipping cleaning.\n`); + return; + } + try { + await emptyRepository({ repositoryName }); + } + catch (e) { + if (e.name !== 'RepositoryNotFoundException') { + throw e; + } + // Repository doesn't exist. Ignoring + } +} +/** + * The repository will only be tagged for deletion if it's being deleted in the same + * deployment as this Custom Resource. + * + * If the Custom Resource is ever deleted before the repository, it must be because + * `autoDeleteImages` has been switched to false, in which case the tag would have + * been removed before we get to this Delete event. + */ +async function isRepositoryTaggedForDeletion(repositoryArn) { + const response = await ecr.listTagsForResource({ resourceArn: repositoryArn }).promise(); + return response.tags?.some(tag => tag.Key === AUTO_DELETE_IMAGES_TAG && tag.Value === 'true'); +} +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJpbmRleC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7QUFBQSw2REFBNkQ7QUFDN0QscUNBQThCO0FBRTlCLE1BQU0sc0JBQXNCLEdBQUcsNEJBQTRCLENBQUM7QUFFNUQsTUFBTSxHQUFHLEdBQUcsSUFBSSxhQUFHLEVBQUUsQ0FBQztBQUVmLEtBQUssVUFBVSxPQUFPLENBQUMsS0FBa0Q7SUFDOUUsUUFBUSxLQUFLLENBQUMsV0FBVyxFQUFFO1FBQ3pCLEtBQUssUUFBUTtZQUNYLE1BQU07UUFDUixLQUFLLFFBQVE7WUFDWCxPQUFPLFFBQVEsQ0FBQyxLQUFLLENBQUMsQ0FBQztRQUN6QixLQUFLLFFBQVE7WUFDWCxPQUFPLFFBQVEsQ0FBQyxLQUFLLENBQUMsa0JBQWtCLEVBQUUsY0FBYyxDQUFDLENBQUM7S0FDN0Q7QUFDSCxDQUFDO0FBVEQsMEJBU0M7QUFFRCxLQUFLLFVBQVUsUUFBUSxDQUFDLEtBQWtEO0lBQ3hFLE1BQU0sV0FBVyxHQUFHLEtBQTBELENBQUM7SUFDL0UsTUFBTSxpQkFBaUIsR0FBRyxXQUFXLENBQUMscUJBQXFCLEVBQUUsY0FBYyxDQUFDO0lBQzVFLE1BQU0saUJBQWlCLEdBQUcsV0FBVyxDQUFDLGtCQUFrQixFQUFFLGNBQWMsQ0FBQztJQUN6RSxNQUFNLHdCQUF3QixHQUFHLENBQUMsaUJBQWlCLElBQUksaUJBQWlCLENBQUM7V0FDcEUsQ0FBQyxpQkFBaUIsS0FBSyxpQkFBaUIsQ0FBQyxDQUFDO0lBRS9DOzswREFFc0Q7SUFDdEQsSUFBSSx3QkFBd0IsRUFBRTtRQUM1QixPQUFPLFFBQVEsQ0FBQyxpQkFBaUIsQ0FBQyxDQUFDO0tBQ3BDO0FBQ0gsQ0FBQztBQUVEOzs7O0dBSUc7QUFDSCxLQUFLLFVBQVUsZUFBZSxDQUFDLE1BQTZCO0lBQzFELE1BQU0sWUFBWSxHQUFHLE1BQU0sR0FBRyxDQUFDLFVBQVUsQ0FBQyxNQUFNLENBQUMsQ0FBQyxPQUFPLEVBQUUsQ0FBQztJQUU1RCxNQUFNLFFBQVEsR0FBRyxZQUFZLEVBQUUsUUFBUSxJQUFJLEVBQUUsQ0FBQztJQUM5QyxNQUFNLFNBQVMsR0FBRyxZQUFZLENBQUMsU0FBUyxJQUFJLElBQUksQ0FBQztJQUNqRCxJQUFJLFFBQVEsQ0FBQyxNQUFNLEtBQUssQ0FBQyxFQUFFO1FBQ3pCLE9BQU87S0FDUjtJQUVELE1BQU0sR0FBRyxDQUFDLGdCQUFnQixDQUFDO1FBQ3pCLGNBQWMsRUFBRSxNQUFNLENBQUMsY0FBYztRQUNyQyxRQUFRO0tBQ1QsQ0FBQyxDQUFDLE9BQU8sRUFBRSxDQUFDO0lBRWIsSUFBSSxTQUFTLEVBQUU7UUFDYixNQUFNLGVBQWUsQ0FBQztZQUNwQixHQUFHLE1BQU07WUFDVCxTQUFTO1NBQ1YsQ0FBQyxDQUFDO0tBQ0o7QUFDSCxDQUFDO0FBRUQsS0FBSyxVQUFVLFFBQVEsQ0FBQyxjQUFzQjtJQUM1QyxJQUFJLENBQUMsY0FBYyxFQUFFO1FBQ25CLE1BQU0sSUFBSSxLQUFLLENBQUMsaUNBQWlDLENBQUMsQ0FBQztLQUNwRDtJQUVELE1BQU0sUUFBUSxHQUFHLE1BQU0sR0FBRyxDQUFDLG9CQUFvQixDQUFDLEVBQUUsZUFBZSxFQUFFLENBQUMsY0FBYyxDQUFDLEVBQUUsQ0FBQyxDQUFDLE9BQU8sRUFBRSxDQUFDO0lBQ2pHLE1BQU0sVUFBVSxHQUFHLFFBQVEsQ0FBQyxZQUFZLEVBQUUsSUFBSSxDQUFDLElBQUksQ0FBQyxFQUFFLENBQUMsSUFBSSxDQUFDLGNBQWMsS0FBSyxjQUFjLENBQUMsQ0FBQztJQUUvRixJQUFJLENBQUMsTUFBTSw2QkFBNkIsQ0FBQyxVQUFVLEVBQUUsYUFBYyxDQUFDLEVBQUU7UUFDcEUsT0FBTyxDQUFDLE1BQU0sQ0FBQyxLQUFLLENBQUMsNkJBQTZCLHNCQUFzQiw2QkFBNkIsQ0FBQyxDQUFDO1FBQ3ZHLE9BQU87S0FDUjtJQUNELElBQUk7UUFDRixNQUFNLGVBQWUsQ0FBQyxFQUFFLGNBQWMsRUFBRSxDQUFDLENBQUM7S0FDM0M7SUFBQyxPQUFPLENBQUMsRUFBRTtRQUNWLElBQUksQ0FBQyxDQUFDLElBQUksS0FBSyw2QkFBNkIsRUFBRTtZQUM1QyxNQUFNLENBQUMsQ0FBQztTQUNUO1FBQ0QscUNBQXFDO0tBQ3RDO0FBQ0gsQ0FBQztBQUVEOzs7Ozs7O0dBT0c7QUFDSCxLQUFLLFVBQVUsNkJBQTZCLENBQUMsYUFBcUI7SUFDaEUsTUFBTSxRQUFRLEdBQUcsTUFBTSxHQUFHLENBQUMsbUJBQW1CLENBQUMsRUFBRSxXQUFXLEVBQUUsYUFBYSxFQUFFLENBQUMsQ0FBQyxPQUFPLEVBQUUsQ0FBQztJQUN6RixPQUFPLFFBQVEsQ0FBQyxJQUFJLEVBQUUsSUFBSSxDQUFDLEdBQUcsQ0FBQyxFQUFFLENBQUMsR0FBRyxDQUFDLEdBQUcsS0FBSyxzQkFBc0IsSUFBSSxHQUFHLENBQUMsS0FBSyxLQUFLLE1BQU0sQ0FBQyxDQUFDO0FBQ2hHLENBQUMiLCJzb3VyY2VzQ29udGVudCI6WyIvLyBlc2xpbnQtZGlzYWJsZS1uZXh0LWxpbmUgaW1wb3J0L25vLWV4dHJhbmVvdXMtZGVwZW5kZW5jaWVzXG5pbXBvcnQgeyBFQ1IgfSBmcm9tICdhd3Mtc2RrJztcblxuY29uc3QgQVVUT19ERUxFVEVfSU1BR0VTX1RBRyA9ICdhd3MtY2RrOmF1dG8tZGVsZXRlLWltYWdlcyc7XG5cbmNvbnN0IGVjciA9IG5ldyBFQ1IoKTtcblxuZXhwb3J0IGFzeW5jIGZ1bmN0aW9uIGhhbmRsZXIoZXZlbnQ6IEFXU0xhbWJkYS5DbG91ZEZvcm1hdGlvbkN1c3RvbVJlc291cmNlRXZlbnQpIHtcbiAgc3dpdGNoIChldmVudC5SZXF1ZXN0VHlwZSkge1xuICAgIGNhc2UgJ0NyZWF0ZSc6XG4gICAgICBicmVhaztcbiAgICBjYXNlICdVcGRhdGUnOlxuICAgICAgcmV0dXJuIG9uVXBkYXRlKGV2ZW50KTtcbiAgICBjYXNlICdEZWxldGUnOlxuICAgICAgcmV0dXJuIG9uRGVsZXRlKGV2ZW50LlJlc291cmNlUHJvcGVydGllcz8uUmVwb3NpdG9yeU5hbWUpO1xuICB9XG59XG5cbmFzeW5jIGZ1bmN0aW9uIG9uVXBkYXRlKGV2ZW50OiBBV1NMYW1iZGEuQ2xvdWRGb3JtYXRpb25DdXN0b21SZXNvdXJjZUV2ZW50KSB7XG4gIGNvbnN0IHVwZGF0ZUV2ZW50ID0gZXZlbnQgYXMgQVdTTGFtYmRhLkNsb3VkRm9ybWF0aW9uQ3VzdG9tUmVzb3VyY2VVcGRhdGVFdmVudDtcbiAgY29uc3Qgb2xkUmVwb3NpdG9yeU5hbWUgPSB1cGRhdGVFdmVudC5PbGRSZXNvdXJjZVByb3BlcnRpZXM/LlJlcG9zaXRvcnlOYW1lO1xuICBjb25zdCBuZXdSZXBvc2l0b3J5TmFtZSA9IHVwZGF0ZUV2ZW50LlJlc291cmNlUHJvcGVydGllcz8uUmVwb3NpdG9yeU5hbWU7XG4gIGNvbnN0IHJlcG9zaXRvcnlOYW1lSGFzQ2hhbmdlZCA9IChuZXdSZXBvc2l0b3J5TmFtZSAmJiBvbGRSZXBvc2l0b3J5TmFtZSlcbiAgICAmJiAobmV3UmVwb3NpdG9yeU5hbWUgIT09IG9sZFJlcG9zaXRvcnlOYW1lKTtcblxuICAvKiBJZiB0aGUgbmFtZSBvZiB0aGUgcmVwb3NpdG9yeSBoYXMgY2hhbmdlZCwgQ2xvdWRGb3JtYXRpb24gd2lsbCB0cnkgdG8gZGVsZXRlIHRoZSByZXBvc2l0b3J5XG4gICAgIGFuZCBjcmVhdGUgYSBuZXcgb25lIHdpdGggdGhlIG5ldyBuYW1lLiBTbyB3ZSBoYXZlIHRvIGRlbGV0ZSB0aGUgaW1hZ2VzIGluIHRoZVxuICAgICByZXBvc2l0b3J5IHNvIHRoYXQgdGhpcyBvcGVyYXRpb24gZG9lcyBub3QgZmFpbC4gKi9cbiAgaWYgKHJlcG9zaXRvcnlOYW1lSGFzQ2hhbmdlZCkge1xuICAgIHJldHVybiBvbkRlbGV0ZShvbGRSZXBvc2l0b3J5TmFtZSk7XG4gIH1cbn1cblxuLyoqXG4gKiBSZWN1cnNpdmVseSBkZWxldGUgYWxsIGltYWdlcyBpbiB0aGUgcmVwb3NpdG9yeVxuICpcbiAqIEBwYXJhbSBFQ1IuTGlzdEltYWdlc1JlcXVlc3QgdGhlIHJlcG9zaXRvcnlOYW1lICYgbmV4dFRva2VuIGlmIHByZXNlbnRlZFxuICovXG5hc3luYyBmdW5jdGlvbiBlbXB0eVJlcG9zaXRvcnkocGFyYW1zOiBFQ1IuTGlzdEltYWdlc1JlcXVlc3QpIHtcbiAgY29uc3QgbGlzdGVkSW1hZ2VzID0gYXdhaXQgZWNyLmxpc3RJbWFnZXMocGFyYW1zKS5wcm9taXNlKCk7XG5cbiAgY29uc3QgaW1hZ2VJZHMgPSBsaXN0ZWRJbWFnZXM/LmltYWdlSWRzID8/IFtdO1xuICBjb25zdCBuZXh0VG9rZW4gPSBsaXN0ZWRJbWFnZXMubmV4dFRva2VuID8/IG51bGw7XG4gIGlmIChpbWFnZUlkcy5sZW5ndGggPT09IDApIHtcbiAgICByZXR1cm47XG4gIH1cblxuICBhd2FpdCBlY3IuYmF0Y2hEZWxldGVJbWFnZSh7XG4gICAgcmVwb3NpdG9yeU5hbWU6IHBhcmFtcy5yZXBvc2l0b3J5TmFtZSxcbiAgICBpbWFnZUlkcyxcbiAgfSkucHJvbWlzZSgpO1xuXG4gIGlmIChuZXh0VG9rZW4pIHtcbiAgICBhd2FpdCBlbXB0eVJlcG9zaXRvcnkoe1xuICAgICAgLi4ucGFyYW1zLFxuICAgICAgbmV4dFRva2VuLFxuICAgIH0pO1xuICB9XG59XG5cbmFzeW5jIGZ1bmN0aW9uIG9uRGVsZXRlKHJlcG9zaXRvcnlOYW1lOiBzdHJpbmcpIHtcbiAgaWYgKCFyZXBvc2l0b3J5TmFtZSkge1xuICAgIHRocm93IG5ldyBFcnJvcignTm8gUmVwb3NpdG9yeU5hbWUgd2FzIHByb3ZpZGVkLicpO1xuICB9XG5cbiAgY29uc3QgcmVzcG9uc2UgPSBhd2FpdCBlY3IuZGVzY3JpYmVSZXBvc2l0b3JpZXMoeyByZXBvc2l0b3J5TmFtZXM6IFtyZXBvc2l0b3J5TmFtZV0gfSkucHJvbWlzZSgpO1xuICBjb25zdCByZXBvc2l0b3J5ID0gcmVzcG9uc2UucmVwb3NpdG9yaWVzPy5maW5kKHJlcG8gPT4gcmVwby5yZXBvc2l0b3J5TmFtZSA9PT0gcmVwb3NpdG9yeU5hbWUpO1xuXG4gIGlmICghYXdhaXQgaXNSZXBvc2l0b3J5VGFnZ2VkRm9yRGVsZXRpb24ocmVwb3NpdG9yeT8ucmVwb3NpdG9yeUFybiEpKSB7XG4gICAgcHJvY2Vzcy5zdGRvdXQud3JpdGUoYFJlcG9zaXRvcnkgZG9lcyBub3QgaGF2ZSAnJHtBVVRPX0RFTEVURV9JTUFHRVNfVEFHfScgdGFnLCBza2lwcGluZyBjbGVhbmluZy5cXG5gKTtcbiAgICByZXR1cm47XG4gIH1cbiAgdHJ5IHtcbiAgICBhd2FpdCBlbXB0eVJlcG9zaXRvcnkoeyByZXBvc2l0b3J5TmFtZSB9KTtcbiAgfSBjYXRjaCAoZSkge1xuICAgIGlmIChlLm5hbWUgIT09ICdSZXBvc2l0b3J5Tm90Rm91bmRFeGNlcHRpb24nKSB7XG4gICAgICB0aHJvdyBlO1xuICAgIH1cbiAgICAvLyBSZXBvc2l0b3J5IGRvZXNuJ3QgZXhpc3QuIElnbm9yaW5nXG4gIH1cbn1cblxuLyoqXG4gKiBUaGUgcmVwb3NpdG9yeSB3aWxsIG9ubHkgYmUgdGFnZ2VkIGZvciBkZWxldGlvbiBpZiBpdCdzIGJlaW5nIGRlbGV0ZWQgaW4gdGhlIHNhbWVcbiAqIGRlcGxveW1lbnQgYXMgdGhpcyBDdXN0b20gUmVzb3VyY2UuXG4gKlxuICogSWYgdGhlIEN1c3RvbSBSZXNvdXJjZSBpcyBldmVyIGRlbGV0ZWQgYmVmb3JlIHRoZSByZXBvc2l0b3J5LCBpdCBtdXN0IGJlIGJlY2F1c2VcbiAqIGBhdXRvRGVsZXRlSW1hZ2VzYCBoYXMgYmVlbiBzd2l0Y2hlZCB0byBmYWxzZSwgaW4gd2hpY2ggY2FzZSB0aGUgdGFnIHdvdWxkIGhhdmVcbiAqIGJlZW4gcmVtb3ZlZCBiZWZvcmUgd2UgZ2V0IHRvIHRoaXMgRGVsZXRlIGV2ZW50LlxuICovXG5hc3luYyBmdW5jdGlvbiBpc1JlcG9zaXRvcnlUYWdnZWRGb3JEZWxldGlvbihyZXBvc2l0b3J5QXJuOiBzdHJpbmcpIHtcbiAgY29uc3QgcmVzcG9uc2UgPSBhd2FpdCBlY3IubGlzdFRhZ3NGb3JSZXNvdXJjZSh7IHJlc291cmNlQXJuOiByZXBvc2l0b3J5QXJuIH0pLnByb21pc2UoKTtcbiAgcmV0dXJuIHJlc3BvbnNlLnRhZ3M/LnNvbWUodGFnID0+IHRhZy5LZXkgPT09IEFVVE9fREVMRVRFX0lNQUdFU19UQUcgJiYgdGFnLlZhbHVlID09PSAndHJ1ZScpO1xufSJdfQ== \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecr/test/integ.repository-auto-delete-images.js.snapshot/aws-ecr-integ-stack.assets.json b/packages/@aws-cdk/aws-ecr/test/integ.repository-auto-delete-images.js.snapshot/aws-ecr-integ-stack.assets.json new file mode 100644 index 0000000000000..309ab8a13dc43 --- /dev/null +++ b/packages/@aws-cdk/aws-ecr/test/integ.repository-auto-delete-images.js.snapshot/aws-ecr-integ-stack.assets.json @@ -0,0 +1,32 @@ +{ + "version": "31.0.0", + "files": { + "6150227d515909a73f0bcde4b9e19b4b206cc65634027053380d700f6e53f08e": { + "source": { + "path": "asset.6150227d515909a73f0bcde4b9e19b4b206cc65634027053380d700f6e53f08e", + "packaging": "zip" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "6150227d515909a73f0bcde4b9e19b4b206cc65634027053380d700f6e53f08e.zip", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + }, + "fae74e473c2d47b28ba8bb9bdcf00530bc1f6b9032ff87dc77d277c8b9c100a5": { + "source": { + "path": "aws-ecr-integ-stack.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "fae74e473c2d47b28ba8bb9bdcf00530bc1f6b9032ff87dc77d277c8b9c100a5.json", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + } + }, + "dockerImages": {} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecr/test/integ.repository-auto-delete-images.js.snapshot/aws-ecr-integ-stack.template.json b/packages/@aws-cdk/aws-ecr/test/integ.repository-auto-delete-images.js.snapshot/aws-ecr-integ-stack.template.json new file mode 100644 index 0000000000000..e755da927abf5 --- /dev/null +++ b/packages/@aws-cdk/aws-ecr/test/integ.repository-auto-delete-images.js.snapshot/aws-ecr-integ-stack.template.json @@ -0,0 +1,197 @@ +{ + "Resources": { + "Repo02AC86CF": { + "Type": "AWS::ECR::Repository", + "Properties": { + "RepositoryName": "delete-even-if-containing-images", + "Tags": [ + { + "Key": "aws-cdk:auto-delete-images", + "Value": "true" + } + ] + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "RepoAutoDeleteImagesCustomResource65201E29": { + "Type": "Custom::ECRAutoDeleteImages", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "CustomECRAutoDeleteImagesCustomResourceProviderHandler8D89C030", + "Arn" + ] + }, + "RepositoryName": { + "Ref": "Repo02AC86CF" + } + }, + "DependsOn": [ + "Repo02AC86CF" + ], + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "CustomECRAutoDeleteImagesCustomResourceProviderRole665F2773": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ] + }, + "ManagedPolicyArns": [ + { + "Fn::Sub": "arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + } + ], + "Policies": [ + { + "PolicyName": "Inline", + "PolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "ecr:BatchDeleteImage", + "ecr:DescribeRepositories", + "ecr:ListImages", + "ecr:ListTagsForResource" + ], + "Resource": [ + { + "Fn::GetAtt": [ + "Repo02AC86CF", + "Arn" + ] + } + ] + } + ] + } + } + ] + } + }, + "CustomECRAutoDeleteImagesCustomResourceProviderHandler8D89C030": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" + }, + "S3Key": "6150227d515909a73f0bcde4b9e19b4b206cc65634027053380d700f6e53f08e.zip" + }, + "Timeout": 900, + "MemorySize": 128, + "Handler": "__entrypoint__.handler", + "Role": { + "Fn::GetAtt": [ + "CustomECRAutoDeleteImagesCustomResourceProviderRole665F2773", + "Arn" + ] + }, + "Runtime": "nodejs14.x", + "Description": "Lambda function for auto-deleting images in undefined repository." + }, + "DependsOn": [ + "CustomECRAutoDeleteImagesCustomResourceProviderRole665F2773" + ] + } + }, + "Outputs": { + "RepositoryURI": { + "Value": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 4, + { + "Fn::Split": [ + ":", + { + "Fn::GetAtt": [ + "Repo02AC86CF", + "Arn" + ] + } + ] + } + ] + }, + ".dkr.ecr.", + { + "Fn::Select": [ + 3, + { + "Fn::Split": [ + ":", + { + "Fn::GetAtt": [ + "Repo02AC86CF", + "Arn" + ] + } + ] + } + ] + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Ref": "Repo02AC86CF" + } + ] + ] + } + } + }, + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecr/test/integ.repository-auto-delete-images.js.snapshot/cdk.out b/packages/@aws-cdk/aws-ecr/test/integ.repository-auto-delete-images.js.snapshot/cdk.out new file mode 100644 index 0000000000000..7925065efbcc4 --- /dev/null +++ b/packages/@aws-cdk/aws-ecr/test/integ.repository-auto-delete-images.js.snapshot/cdk.out @@ -0,0 +1 @@ +{"version":"31.0.0"} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecr/test/integ.repository-auto-delete-images.js.snapshot/cdkintegautodeleteimagesDefaultTestDeployAssert6B08011C.assets.json b/packages/@aws-cdk/aws-ecr/test/integ.repository-auto-delete-images.js.snapshot/cdkintegautodeleteimagesDefaultTestDeployAssert6B08011C.assets.json new file mode 100644 index 0000000000000..9ff70a8cba8bc --- /dev/null +++ b/packages/@aws-cdk/aws-ecr/test/integ.repository-auto-delete-images.js.snapshot/cdkintegautodeleteimagesDefaultTestDeployAssert6B08011C.assets.json @@ -0,0 +1,19 @@ +{ + "version": "31.0.0", + "files": { + "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22": { + "source": { + "path": "cdkintegautodeleteimagesDefaultTestDeployAssert6B08011C.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22.json", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + } + }, + "dockerImages": {} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecr/test/integ.repository-auto-delete-images.js.snapshot/cdkintegautodeleteimagesDefaultTestDeployAssert6B08011C.template.json b/packages/@aws-cdk/aws-ecr/test/integ.repository-auto-delete-images.js.snapshot/cdkintegautodeleteimagesDefaultTestDeployAssert6B08011C.template.json new file mode 100644 index 0000000000000..ad9d0fb73d1dd --- /dev/null +++ b/packages/@aws-cdk/aws-ecr/test/integ.repository-auto-delete-images.js.snapshot/cdkintegautodeleteimagesDefaultTestDeployAssert6B08011C.template.json @@ -0,0 +1,36 @@ +{ + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecr/test/integ.repository-auto-delete-images.js.snapshot/integ.json b/packages/@aws-cdk/aws-ecr/test/integ.repository-auto-delete-images.js.snapshot/integ.json new file mode 100644 index 0000000000000..3b51355c51c47 --- /dev/null +++ b/packages/@aws-cdk/aws-ecr/test/integ.repository-auto-delete-images.js.snapshot/integ.json @@ -0,0 +1,12 @@ +{ + "version": "31.0.0", + "testCases": { + "cdk-integ-auto-delete-images/DefaultTest": { + "stacks": [ + "aws-ecr-integ-stack" + ], + "assertionStack": "cdk-integ-auto-delete-images/DefaultTest/DeployAssert", + "assertionStackName": "cdkintegautodeleteimagesDefaultTestDeployAssert6B08011C" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecr/test/integ.repository-auto-delete-images.js.snapshot/manifest.json b/packages/@aws-cdk/aws-ecr/test/integ.repository-auto-delete-images.js.snapshot/manifest.json new file mode 100644 index 0000000000000..849b1be4fed32 --- /dev/null +++ b/packages/@aws-cdk/aws-ecr/test/integ.repository-auto-delete-images.js.snapshot/manifest.json @@ -0,0 +1,135 @@ +{ + "version": "31.0.0", + "artifacts": { + "aws-ecr-integ-stack.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "aws-ecr-integ-stack.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "aws-ecr-integ-stack": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "aws-ecr-integ-stack.template.json", + "validateOnSynth": false, + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", + "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/fae74e473c2d47b28ba8bb9bdcf00530bc1f6b9032ff87dc77d277c8b9c100a5.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "aws-ecr-integ-stack.assets" + ], + "lookupRole": { + "arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}", + "requiresBootstrapStackVersion": 8, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "dependencies": [ + "aws-ecr-integ-stack.assets" + ], + "metadata": { + "/aws-ecr-integ-stack/Repo/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "Repo02AC86CF" + } + ], + "/aws-ecr-integ-stack/Repo/AutoDeleteImagesCustomResource/Default": [ + { + "type": "aws:cdk:logicalId", + "data": "RepoAutoDeleteImagesCustomResource65201E29" + } + ], + "/aws-ecr-integ-stack/Custom::ECRAutoDeleteImagesCustomResourceProvider/Role": [ + { + "type": "aws:cdk:logicalId", + "data": "CustomECRAutoDeleteImagesCustomResourceProviderRole665F2773" + } + ], + "/aws-ecr-integ-stack/Custom::ECRAutoDeleteImagesCustomResourceProvider/Handler": [ + { + "type": "aws:cdk:logicalId", + "data": "CustomECRAutoDeleteImagesCustomResourceProviderHandler8D89C030" + } + ], + "/aws-ecr-integ-stack/RepositoryURI": [ + { + "type": "aws:cdk:logicalId", + "data": "RepositoryURI" + } + ], + "/aws-ecr-integ-stack/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/aws-ecr-integ-stack/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ] + }, + "displayName": "aws-ecr-integ-stack" + }, + "cdkintegautodeleteimagesDefaultTestDeployAssert6B08011C.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "cdkintegautodeleteimagesDefaultTestDeployAssert6B08011C.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "cdkintegautodeleteimagesDefaultTestDeployAssert6B08011C": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "cdkintegautodeleteimagesDefaultTestDeployAssert6B08011C.template.json", + "validateOnSynth": false, + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", + "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "cdkintegautodeleteimagesDefaultTestDeployAssert6B08011C.assets" + ], + "lookupRole": { + "arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}", + "requiresBootstrapStackVersion": 8, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "dependencies": [ + "cdkintegautodeleteimagesDefaultTestDeployAssert6B08011C.assets" + ], + "metadata": { + "/cdk-integ-auto-delete-images/DefaultTest/DeployAssert/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/cdk-integ-auto-delete-images/DefaultTest/DeployAssert/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ] + }, + "displayName": "cdk-integ-auto-delete-images/DefaultTest/DeployAssert" + }, + "Tree": { + "type": "cdk:tree", + "properties": { + "file": "tree.json" + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecr/test/integ.repository-auto-delete-images.js.snapshot/tree.json b/packages/@aws-cdk/aws-ecr/test/integ.repository-auto-delete-images.js.snapshot/tree.json new file mode 100644 index 0000000000000..3d686786ff658 --- /dev/null +++ b/packages/@aws-cdk/aws-ecr/test/integ.repository-auto-delete-images.js.snapshot/tree.json @@ -0,0 +1,191 @@ +{ + "version": "tree-0.1", + "tree": { + "id": "App", + "path": "", + "children": { + "aws-ecr-integ-stack": { + "id": "aws-ecr-integ-stack", + "path": "aws-ecr-integ-stack", + "children": { + "Repo": { + "id": "Repo", + "path": "aws-ecr-integ-stack/Repo", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-ecr-integ-stack/Repo/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::ECR::Repository", + "aws:cdk:cloudformation:props": { + "repositoryName": "delete-even-if-containing-images", + "tags": [ + { + "key": "aws-cdk:auto-delete-images", + "value": "true" + } + ] + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-ecr.CfnRepository", + "version": "0.0.0" + } + }, + "AutoDeleteImagesCustomResource": { + "id": "AutoDeleteImagesCustomResource", + "path": "aws-ecr-integ-stack/Repo/AutoDeleteImagesCustomResource", + "children": { + "Default": { + "id": "Default", + "path": "aws-ecr-integ-stack/Repo/AutoDeleteImagesCustomResource/Default", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnResource", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.CustomResource", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-ecr.Repository", + "version": "0.0.0" + } + }, + "Custom::ECRAutoDeleteImagesCustomResourceProvider": { + "id": "Custom::ECRAutoDeleteImagesCustomResourceProvider", + "path": "aws-ecr-integ-stack/Custom::ECRAutoDeleteImagesCustomResourceProvider", + "children": { + "Staging": { + "id": "Staging", + "path": "aws-ecr-integ-stack/Custom::ECRAutoDeleteImagesCustomResourceProvider/Staging", + "constructInfo": { + "fqn": "@aws-cdk/core.AssetStaging", + "version": "0.0.0" + } + }, + "Role": { + "id": "Role", + "path": "aws-ecr-integ-stack/Custom::ECRAutoDeleteImagesCustomResourceProvider/Role", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnResource", + "version": "0.0.0" + } + }, + "Handler": { + "id": "Handler", + "path": "aws-ecr-integ-stack/Custom::ECRAutoDeleteImagesCustomResourceProvider/Handler", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnResource", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.CustomResourceProvider", + "version": "0.0.0" + } + }, + "RepositoryURI": { + "id": "RepositoryURI", + "path": "aws-ecr-integ-stack/RepositoryURI", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnOutput", + "version": "0.0.0" + } + }, + "BootstrapVersion": { + "id": "BootstrapVersion", + "path": "aws-ecr-integ-stack/BootstrapVersion", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnParameter", + "version": "0.0.0" + } + }, + "CheckBootstrapVersion": { + "id": "CheckBootstrapVersion", + "path": "aws-ecr-integ-stack/CheckBootstrapVersion", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnRule", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.Stack", + "version": "0.0.0" + } + }, + "cdk-integ-auto-delete-images": { + "id": "cdk-integ-auto-delete-images", + "path": "cdk-integ-auto-delete-images", + "children": { + "DefaultTest": { + "id": "DefaultTest", + "path": "cdk-integ-auto-delete-images/DefaultTest", + "children": { + "Default": { + "id": "Default", + "path": "cdk-integ-auto-delete-images/DefaultTest/Default", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.1.270" + } + }, + "DeployAssert": { + "id": "DeployAssert", + "path": "cdk-integ-auto-delete-images/DefaultTest/DeployAssert", + "children": { + "BootstrapVersion": { + "id": "BootstrapVersion", + "path": "cdk-integ-auto-delete-images/DefaultTest/DeployAssert/BootstrapVersion", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnParameter", + "version": "0.0.0" + } + }, + "CheckBootstrapVersion": { + "id": "CheckBootstrapVersion", + "path": "cdk-integ-auto-delete-images/DefaultTest/DeployAssert/CheckBootstrapVersion", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnRule", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.Stack", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests.IntegTestCase", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests.IntegTest", + "version": "0.0.0" + } + }, + "Tree": { + "id": "Tree", + "path": "Tree", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.1.270" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.App", + "version": "0.0.0" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecr/test/integ.repository-auto-delete-images.ts b/packages/@aws-cdk/aws-ecr/test/integ.repository-auto-delete-images.ts new file mode 100644 index 0000000000000..1d57788631713 --- /dev/null +++ b/packages/@aws-cdk/aws-ecr/test/integ.repository-auto-delete-images.ts @@ -0,0 +1,20 @@ +import * as cdk from '@aws-cdk/core'; +import { IntegTest } from '@aws-cdk/integ-tests'; +import * as ecr from '../lib'; + +const app = new cdk.App(); +const stack = new cdk.Stack(app, 'aws-ecr-integ-stack'); + +const repo = new ecr.Repository(stack, 'Repo', { + repositoryName: 'delete-even-if-containing-images', + removalPolicy: cdk.RemovalPolicy.DESTROY, + autoDeleteImages: true, +}); + +new cdk.CfnOutput(stack, 'RepositoryURI', { + value: repo.repositoryUri, +}); + +new IntegTest(app, 'cdk-integ-auto-delete-images', { + testCases: [stack], +}); diff --git a/packages/@aws-cdk/aws-eks/lib/cluster-resource-handler/cluster.ts b/packages/@aws-cdk/aws-eks/lib/cluster-resource-handler/cluster.ts index 4b7fdda6d1dd1..e6939ba60d891 100644 --- a/packages/@aws-cdk/aws-eks/lib/cluster-resource-handler/cluster.ts +++ b/packages/@aws-cdk/aws-eks/lib/cluster-resource-handler/cluster.ts @@ -5,6 +5,8 @@ import { IsCompleteResponse, OnEventResponse } from '@aws-cdk/custom-resources/l // eslint-disable-next-line import/no-extraneous-dependencies import * as aws from 'aws-sdk'; import { EksClient, ResourceEvent, ResourceHandler } from './common'; +import { compareLoggingProps } from './compareLogging'; + const MAX_CLUSTER_NAME_LEN = 100; @@ -25,6 +27,9 @@ export class ClusterResourceHandler extends ResourceHandler { this.newProps = parseProps(this.event.ResourceProperties); this.oldProps = event.RequestType === 'Update' ? parseProps(event.OldResourceProperties) : {}; + // compare newProps and oldProps and update the newProps by appending disabled LogSetup if any + const compared: Partial = compareLoggingProps(this.oldProps, this.newProps); + this.newProps.logging = compared.logging; } // ------ diff --git a/packages/@aws-cdk/aws-eks/lib/cluster-resource-handler/compareLogging.ts b/packages/@aws-cdk/aws-eks/lib/cluster-resource-handler/compareLogging.ts new file mode 100644 index 0000000000000..0eb9a9b6d253f --- /dev/null +++ b/packages/@aws-cdk/aws-eks/lib/cluster-resource-handler/compareLogging.ts @@ -0,0 +1,45 @@ +/** + * This function compares the logging configuration from oldProps and newProps and returns + * the result that contains LogSetup with enabled:false if any. + * + * @param oldProps old properties + * @param newProps new properties + * @returns result with LogSet with enabled:false if any + */ + +export function compareLoggingProps(oldProps: Partial, + newProps: Partial): Partial { + const result: Partial = { logging: {} }; + let enabledTypes: AWS.EKS.LogType[] = []; + let disabledTypes: AWS.EKS.LogType[] = []; + + if (newProps.logging?.clusterLogging === undefined && oldProps.logging?.clusterLogging === undefined) { + return newProps; + } + // if newProps containes LogSetup + if (newProps.logging && newProps.logging.clusterLogging && newProps.logging.clusterLogging.length > 0) { + enabledTypes = newProps.logging.clusterLogging[0].types!; + // if oldProps contains LogSetup with enabled:true + if (oldProps.logging && oldProps.logging.clusterLogging && oldProps.logging.clusterLogging.length > 0) { + // LogType in oldProp but not in newProp should be considered disabled(enabled:false) + disabledTypes = oldProps.logging!.clusterLogging![0].types!.filter(t => !newProps.logging!.clusterLogging![0].types!.includes(t)); + } + } else { + // all enabled:true in oldProps will be enabled:false + disabledTypes = oldProps.logging!.clusterLogging![0].types!; + } + + if (enabledTypes.length > 0 || disabledTypes.length > 0) { + result.logging = { clusterLogging: [] }; + } + + // append the enabled:false LogSetup to the result + if (enabledTypes.length > 0) { + result.logging!.clusterLogging!.push({ types: enabledTypes, enabled: true }); + } + // append the enabled:false LogSetup to the result + if (disabledTypes.length > 0) { + result.logging!.clusterLogging!.push({ types: disabledTypes, enabled: false }); + } + return result; +} diff --git a/packages/@aws-cdk/aws-eks/test/compareLog.test.ts b/packages/@aws-cdk/aws-eks/test/compareLog.test.ts new file mode 100644 index 0000000000000..62c35cf078176 --- /dev/null +++ b/packages/@aws-cdk/aws-eks/test/compareLog.test.ts @@ -0,0 +1,89 @@ +import * as aws from 'aws-sdk'; +import * as eks from '../lib'; +import { compareLoggingProps } from '../lib/cluster-resource-handler/compareLogging'; + +describe('compareLoggingProps', () => { + + type Props = Partial; + const oldEnabledTypes: aws.EKS.LogTypes = [eks.ClusterLoggingTypes.API, eks.ClusterLoggingTypes.AUDIT]; + + test('when newProps.logging.clusterLogging is undefined, should disable all types with enabled:true in oldProps', () => { + const oldProps: Props = { + logging: { + clusterLogging: [{ types: oldEnabledTypes, enabled: true }], + }, + }; + + const newProps: Props = { + logging: {}, + }; + + const result = compareLoggingProps(oldProps, newProps); + + expect(result.logging?.clusterLogging).toEqual([{ types: oldEnabledTypes, enabled: false }]); + }); + + test('when newProps.logging is undefined, should disable all types with enabled:true in oldProps', () => { + const oldProps: Props = { + logging: { + clusterLogging: [{ types: oldEnabledTypes, enabled: true }], + }, + }; + + const newProps: Props = {}; + + const result = compareLoggingProps(oldProps, newProps); + + expect(result.logging?.clusterLogging).toEqual([{ types: oldEnabledTypes, enabled: false }]); + }); + + test('should disable types with enabled:true but not defined as enabled:true in newProps', () => { + const oldProps: Props = { + logging: { + clusterLogging: [{ types: oldEnabledTypes, enabled: true }], + }, + }; + + const newProps: Props = { + logging: { + clusterLogging: [{ types: [eks.ClusterLoggingTypes.AUDIT], enabled: true }], + }, + }; + + const result = compareLoggingProps(oldProps, newProps); + + expect(result.logging?.clusterLogging).toEqual([{ types: [eks.ClusterLoggingTypes.AUDIT], enabled: true }, + { types: [eks.ClusterLoggingTypes.API], enabled: false }]); + }); + + test('when oldProps.logging.clusterLogging is undefined and newProps.logging.clusterLogging is undefined, result should be newProps', () => { + const oldProps: Props = { + logging: {}, + }; + + const newProps: Props = { + logging: {}, + }; + + const result = compareLoggingProps(oldProps, newProps); + + expect(result).toEqual(newProps); + }); + + test('multiple enabled:true types in oldProps with clusterLogging undefined in newProps should all be disabled', () => { + const oldProps: Props = { + logging: { + clusterLogging: [{ types: oldEnabledTypes, enabled: true }], + }, + }; + + const newProps: Props = { + logging: {}, + }; + + const result = compareLoggingProps(oldProps, newProps); + + expect(result.logging?.clusterLogging).toEqual([{ types: oldEnabledTypes, enabled: false }]); + }); + +}); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-elasticloadbalancing/package.json b/packages/@aws-cdk/aws-elasticloadbalancing/package.json index f3b4e13aacb23..7651b907d9554 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancing/package.json +++ b/packages/@aws-cdk/aws-elasticloadbalancing/package.json @@ -84,6 +84,7 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/integ-runner": "0.0.0", "@aws-cdk/integ-tests": "0.0.0", + "@aws-cdk/aws-iam": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", "@types/jest": "^27.5.2" diff --git a/packages/@aws-cdk/aws-elasticloadbalancing/test/integ.instanceTarget.elb.js.snapshot/InstanceTargetTestDefaultTestDeployAssertAF607556.assets.json b/packages/@aws-cdk/aws-elasticloadbalancing/test/integ.instanceTarget.elb.js.snapshot/InstanceTargetTestDefaultTestDeployAssertAF607556.assets.json index 3493f6a48b43c..f9e2b4b4362b2 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancing/test/integ.instanceTarget.elb.js.snapshot/InstanceTargetTestDefaultTestDeployAssertAF607556.assets.json +++ b/packages/@aws-cdk/aws-elasticloadbalancing/test/integ.instanceTarget.elb.js.snapshot/InstanceTargetTestDefaultTestDeployAssertAF607556.assets.json @@ -1,5 +1,5 @@ { - "version": "29.0.0", + "version": "31.0.0", "files": { "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22": { "source": { diff --git a/packages/@aws-cdk/aws-elasticloadbalancing/test/integ.instanceTarget.elb.js.snapshot/aws-cdk-elb-instance-target-integ.assets.json b/packages/@aws-cdk/aws-elasticloadbalancing/test/integ.instanceTarget.elb.js.snapshot/aws-cdk-elb-instance-target-integ.assets.json index 1d178796401c7..d7bf74af2618a 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancing/test/integ.instanceTarget.elb.js.snapshot/aws-cdk-elb-instance-target-integ.assets.json +++ b/packages/@aws-cdk/aws-elasticloadbalancing/test/integ.instanceTarget.elb.js.snapshot/aws-cdk-elb-instance-target-integ.assets.json @@ -1,7 +1,7 @@ { - "version": "29.0.0", + "version": "31.0.0", "files": { - "11ca0111a871a53be970c5db0c5a24d4146213fd59f6d172b6fc1bc3de206cf9": { + "c8ab3e4e4503281b1f7df3028abab9a0ca3738640d31201b5118a18aaa225eab": { "source": { "path": "aws-cdk-elb-instance-target-integ.template.json", "packaging": "file" @@ -9,7 +9,7 @@ "destinations": { "current_account-current_region": { "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", - "objectKey": "11ca0111a871a53be970c5db0c5a24d4146213fd59f6d172b6fc1bc3de206cf9.json", + "objectKey": "c8ab3e4e4503281b1f7df3028abab9a0ca3738640d31201b5118a18aaa225eab.json", "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" } } diff --git a/packages/@aws-cdk/aws-elasticloadbalancing/test/integ.instanceTarget.elb.js.snapshot/aws-cdk-elb-instance-target-integ.template.json b/packages/@aws-cdk/aws-elasticloadbalancing/test/integ.instanceTarget.elb.js.snapshot/aws-cdk-elb-instance-target-integ.template.json index 07931ccc284e9..7823c4ecbcfb1 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancing/test/integ.instanceTarget.elb.js.snapshot/aws-cdk-elb-instance-target-integ.template.json +++ b/packages/@aws-cdk/aws-elasticloadbalancing/test/integ.instanceTarget.elb.js.snapshot/aws-cdk-elb-instance-target-integ.template.json @@ -272,6 +272,20 @@ ], "Version": "2012-10-17" }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/AmazonSSMManagedInstanceCore" + ] + ] + } + ], "Tags": [ { "Key": "Name", @@ -280,6 +294,32 @@ ] } }, + "targetInstanceInstanceRoleDefaultPolicy1E71262F": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "cloudformation:DescribeStackResource", + "cloudformation:SignalResource" + ], + "Effect": "Allow", + "Resource": { + "Ref": "AWS::StackId" + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "targetInstanceInstanceRoleDefaultPolicy1E71262F", + "Roles": [ + { + "Ref": "targetInstanceInstanceRole3F8EC526" + } + ] + } + }, "targetInstanceInstanceProfile0A012423": { "Type": "AWS::IAM::InstanceProfile", "Properties": { @@ -290,7 +330,7 @@ ] } }, - "targetInstance603C5817": { + "targetInstance603C5817b329f03eca862331": { "Type": "AWS::EC2::Instance", "Properties": { "AvailabilityZone": { @@ -307,7 +347,7 @@ "ImageId": { "Ref": "SsmParameterValueawsserviceamiamazonlinuxlatestamzn2amihvmx8664gp2C96584B6F00A464EAD1953AFF4B05118Parameter" }, - "InstanceType": "t2.micro", + "InstanceType": "t3.micro", "SecurityGroupIds": [ { "Fn::GetAtt": [ @@ -326,12 +366,77 @@ } ], "UserData": { - "Fn::Base64": "#!/bin/bash" + "Fn::Base64": { + "Fn::Join": [ + "", + [ + "#!/bin/bash\n# fingerprint: e15d4219bb1dd06a\n(\n set +e\n /opt/aws/bin/cfn-init -v --region ", + { + "Ref": "AWS::Region" + }, + " --stack ", + { + "Ref": "AWS::StackName" + }, + " --resource targetInstance603C5817b329f03eca862331 -c default\n /opt/aws/bin/cfn-signal -e $? --region ", + { + "Ref": "AWS::Region" + }, + " --stack ", + { + "Ref": "AWS::StackName" + }, + " --resource targetInstance603C5817b329f03eca862331\n cat /var/log/cfn-init.log >&2\n)" + ] + ] + } } }, "DependsOn": [ + "targetInstanceInstanceRoleDefaultPolicy1E71262F", "targetInstanceInstanceRole3F8EC526" - ] + ], + "CreationPolicy": { + "ResourceSignal": { + "Count": 1, + "Timeout": "PT30M" + } + }, + "Metadata": { + "AWS::CloudFormation::Init": { + "configSets": { + "default": [ + "config" + ] + }, + "config": { + "files": { + "/etc/systemd/system/pythonweb.service": { + "content": "[Unit]\nAfter=network.target\n[Service]\nExecStart=/usr/bin/python3 -m http.server 8080\nWorkingDirectory=/var/www/html\nRestart=always\n[Install]\nWantedBy=multi-user.target", + "encoding": "plain", + "mode": "000644", + "owner": "root", + "group": "root" + }, + "/var/www/html/index.html": { + "content": "Hello! You can see me!", + "encoding": "plain", + "mode": "000644", + "owner": "root", + "group": "root" + } + }, + "services": { + "systemd": { + "pythonweb": { + "enabled": true, + "ensureRunning": true + } + } + } + } + } + } }, "LBSecurityGroup8A41EA2B": { "Type": "AWS::EC2::SecurityGroup", @@ -386,10 +491,10 @@ "CrossZone": true, "Instances": [ { - "Ref": "targetInstance603C5817" + "Ref": "targetInstance603C5817b329f03eca862331" } ], - "Scheme": "internal", + "Scheme": "internet-facing", "SecurityGroups": [ { "Fn::GetAtt": [ @@ -400,10 +505,14 @@ ], "Subnets": [ { - "Ref": "VPCPrivateSubnet1Subnet8BCA10E0" + "Ref": "VPCPublicSubnet1SubnetB4246D30" } ] - } + }, + "DependsOn": [ + "VPCPublicSubnet1DefaultRoute91CEF279", + "VPCPublicSubnet1RouteTableAssociation0B0896DC" + ] } }, "Parameters": { diff --git a/packages/@aws-cdk/aws-elasticloadbalancing/test/integ.instanceTarget.elb.js.snapshot/cdk.out b/packages/@aws-cdk/aws-elasticloadbalancing/test/integ.instanceTarget.elb.js.snapshot/cdk.out index d8b441d447f8a..7925065efbcc4 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancing/test/integ.instanceTarget.elb.js.snapshot/cdk.out +++ b/packages/@aws-cdk/aws-elasticloadbalancing/test/integ.instanceTarget.elb.js.snapshot/cdk.out @@ -1 +1 @@ -{"version":"29.0.0"} \ No newline at end of file +{"version":"31.0.0"} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-elasticloadbalancing/test/integ.instanceTarget.elb.js.snapshot/integ.json b/packages/@aws-cdk/aws-elasticloadbalancing/test/integ.instanceTarget.elb.js.snapshot/integ.json index 23c31182b8fce..d8a20aea57f70 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancing/test/integ.instanceTarget.elb.js.snapshot/integ.json +++ b/packages/@aws-cdk/aws-elasticloadbalancing/test/integ.instanceTarget.elb.js.snapshot/integ.json @@ -1,5 +1,5 @@ { - "version": "29.0.0", + "version": "31.0.0", "testCases": { "InstanceTargetTest/DefaultTest": { "stacks": [ diff --git a/packages/@aws-cdk/aws-elasticloadbalancing/test/integ.instanceTarget.elb.js.snapshot/manifest.json b/packages/@aws-cdk/aws-elasticloadbalancing/test/integ.instanceTarget.elb.js.snapshot/manifest.json index f0e7e3434a175..8984d5a32c526 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancing/test/integ.instanceTarget.elb.js.snapshot/manifest.json +++ b/packages/@aws-cdk/aws-elasticloadbalancing/test/integ.instanceTarget.elb.js.snapshot/manifest.json @@ -1,5 +1,5 @@ { - "version": "29.0.0", + "version": "31.0.0", "artifacts": { "aws-cdk-elb-instance-target-integ.assets": { "type": "cdk:asset-manifest", @@ -17,7 +17,7 @@ "validateOnSynth": false, "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", - "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/11ca0111a871a53be970c5db0c5a24d4146213fd59f6d172b6fc1bc3de206cf9.json", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/c8ab3e4e4503281b1f7df3028abab9a0ca3738640d31201b5118a18aaa225eab.json", "requiresBootstrapStackVersion": 6, "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", "additionalDependencies": [ @@ -129,6 +129,12 @@ "data": "targetInstanceInstanceRole3F8EC526" } ], + "/aws-cdk-elb-instance-target-integ/targetInstance/InstanceRole/DefaultPolicy/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "targetInstanceInstanceRoleDefaultPolicy1E71262F" + } + ], "/aws-cdk-elb-instance-target-integ/targetInstance/InstanceProfile": [ { "type": "aws:cdk:logicalId", @@ -138,7 +144,7 @@ "/aws-cdk-elb-instance-target-integ/targetInstance/Resource": [ { "type": "aws:cdk:logicalId", - "data": "targetInstance603C5817" + "data": "targetInstance603C5817b329f03eca862331" } ], "/aws-cdk-elb-instance-target-integ/SsmParameterValue:--aws--service--ami-amazon-linux-latest--amzn2-ami-hvm-x86_64-gp2:C96584B6-F00A-464E-AD19-53AFF4B05118.Parameter": [ @@ -162,7 +168,10 @@ "/aws-cdk-elb-instance-target-integ/LB/Resource": [ { "type": "aws:cdk:logicalId", - "data": "LB8A12904C" + "data": "LB8A12904C", + "trace": [ + "!!DESTRUCTIVE_CHANGES: WILL_REPLACE" + ] } ], "/aws-cdk-elb-instance-target-integ/BootstrapVersion": [ @@ -177,19 +186,10 @@ "data": "CheckBootstrapVersion" } ], - "targetInstanceInstanceSecurityGroupfromawscdkelbinstancetargetintegLBSecurityGroup395870CC80E053AA6C": [ - { - "type": "aws:cdk:logicalId", - "data": "targetInstanceInstanceSecurityGroupfromawscdkelbinstancetargetintegLBSecurityGroup395870CC80E053AA6C", - "trace": [ - "!!DESTRUCTIVE_CHANGES: WILL_DESTROY" - ] - } - ], - "LBSecurityGrouptoawscdkelbinstancetargetintegtargetInstanceInstanceSecurityGroup4B82664E80A95A3BE8": [ + "targetInstance603C5817": [ { "type": "aws:cdk:logicalId", - "data": "LBSecurityGrouptoawscdkelbinstancetargetintegtargetInstanceInstanceSecurityGroup4B82664E80A95A3BE8", + "data": "targetInstance603C5817", "trace": [ "!!DESTRUCTIVE_CHANGES: WILL_DESTROY" ] diff --git a/packages/@aws-cdk/aws-elasticloadbalancing/test/integ.instanceTarget.elb.js.snapshot/tree.json b/packages/@aws-cdk/aws-elasticloadbalancing/test/integ.instanceTarget.elb.js.snapshot/tree.json index 6d4e4af19f1eb..ac9955bdfdf39 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancing/test/integ.instanceTarget.elb.js.snapshot/tree.json +++ b/packages/@aws-cdk/aws-elasticloadbalancing/test/integ.instanceTarget.elb.js.snapshot/tree.json @@ -31,7 +31,7 @@ } }, "constructInfo": { - "fqn": "@aws-cdk/aws-ec2.CfnVPC", + "fqn": "@aws-cdk/core.CfnResource", "version": "0.0.0" } }, @@ -75,7 +75,7 @@ } }, "constructInfo": { - "fqn": "@aws-cdk/aws-ec2.CfnSubnet", + "fqn": "@aws-cdk/core.CfnResource", "version": "0.0.0" } }, @@ -105,7 +105,7 @@ } }, "constructInfo": { - "fqn": "@aws-cdk/aws-ec2.CfnRouteTable", + "fqn": "@aws-cdk/core.CfnResource", "version": "0.0.0" } }, @@ -124,7 +124,7 @@ } }, "constructInfo": { - "fqn": "@aws-cdk/aws-ec2.CfnSubnetRouteTableAssociation", + "fqn": "@aws-cdk/core.CfnResource", "version": "0.0.0" } }, @@ -144,7 +144,7 @@ } }, "constructInfo": { - "fqn": "@aws-cdk/aws-ec2.CfnRoute", + "fqn": "@aws-cdk/core.CfnResource", "version": "0.0.0" } }, @@ -164,7 +164,7 @@ } }, "constructInfo": { - "fqn": "@aws-cdk/aws-ec2.CfnEIP", + "fqn": "@aws-cdk/core.CfnResource", "version": "0.0.0" } }, @@ -192,13 +192,13 @@ } }, "constructInfo": { - "fqn": "@aws-cdk/aws-ec2.CfnNatGateway", + "fqn": "@aws-cdk/core.CfnResource", "version": "0.0.0" } } }, "constructInfo": { - "fqn": "@aws-cdk/aws-ec2.PublicSubnet", + "fqn": "@aws-cdk/core.Resource", "version": "0.0.0" } }, @@ -242,7 +242,7 @@ } }, "constructInfo": { - "fqn": "@aws-cdk/aws-ec2.CfnSubnet", + "fqn": "@aws-cdk/core.CfnResource", "version": "0.0.0" } }, @@ -272,7 +272,7 @@ } }, "constructInfo": { - "fqn": "@aws-cdk/aws-ec2.CfnRouteTable", + "fqn": "@aws-cdk/core.CfnResource", "version": "0.0.0" } }, @@ -291,7 +291,7 @@ } }, "constructInfo": { - "fqn": "@aws-cdk/aws-ec2.CfnSubnetRouteTableAssociation", + "fqn": "@aws-cdk/core.CfnResource", "version": "0.0.0" } }, @@ -311,13 +311,13 @@ } }, "constructInfo": { - "fqn": "@aws-cdk/aws-ec2.CfnRoute", + "fqn": "@aws-cdk/core.CfnResource", "version": "0.0.0" } } }, "constructInfo": { - "fqn": "@aws-cdk/aws-ec2.PrivateSubnet", + "fqn": "@aws-cdk/core.Resource", "version": "0.0.0" } }, @@ -336,7 +336,7 @@ } }, "constructInfo": { - "fqn": "@aws-cdk/aws-ec2.CfnInternetGateway", + "fqn": "@aws-cdk/core.CfnResource", "version": "0.0.0" } }, @@ -355,13 +355,13 @@ } }, "constructInfo": { - "fqn": "@aws-cdk/aws-ec2.CfnVPCGatewayAttachment", + "fqn": "@aws-cdk/core.CfnResource", "version": "0.0.0" } } }, "constructInfo": { - "fqn": "@aws-cdk/aws-ec2.Vpc", + "fqn": "@aws-cdk/core.Resource", "version": "0.0.0" } }, @@ -399,7 +399,7 @@ } }, "constructInfo": { - "fqn": "@aws-cdk/aws-ec2.CfnSecurityGroup", + "fqn": "@aws-cdk/core.CfnResource", "version": "0.0.0" } }, @@ -428,13 +428,13 @@ } }, "constructInfo": { - "fqn": "@aws-cdk/aws-ec2.CfnSecurityGroupIngress", + "fqn": "@aws-cdk/core.CfnResource", "version": "0.0.0" } } }, "constructInfo": { - "fqn": "@aws-cdk/aws-ec2.SecurityGroup", + "fqn": "@aws-cdk/core.Resource", "version": "0.0.0" } }, @@ -468,6 +468,20 @@ ], "Version": "2012-10-17" }, + "managedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/AmazonSSMManagedInstanceCore" + ] + ] + } + ], "tags": [ { "key": "Name", @@ -480,6 +494,50 @@ "fqn": "@aws-cdk/aws-iam.CfnRole", "version": "0.0.0" } + }, + "DefaultPolicy": { + "id": "DefaultPolicy", + "path": "aws-cdk-elb-instance-target-integ/targetInstance/InstanceRole/DefaultPolicy", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-cdk-elb-instance-target-integ/targetInstance/InstanceRole/DefaultPolicy/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::IAM::Policy", + "aws:cdk:cloudformation:props": { + "policyDocument": { + "Statement": [ + { + "Action": [ + "cloudformation:DescribeStackResource", + "cloudformation:SignalResource" + ], + "Effect": "Allow", + "Resource": { + "Ref": "AWS::StackId" + } + } + ], + "Version": "2012-10-17" + }, + "policyName": "targetInstanceInstanceRoleDefaultPolicy1E71262F", + "roles": [ + { + "Ref": "targetInstanceInstanceRole3F8EC526" + } + ] + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-iam.CfnPolicy", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-iam.Policy", + "version": "0.0.0" + } } }, "constructInfo": { @@ -525,7 +583,7 @@ "imageId": { "Ref": "SsmParameterValueawsserviceamiamazonlinuxlatestamzn2amihvmx8664gp2C96584B6F00A464EAD1953AFF4B05118Parameter" }, - "instanceType": "t2.micro", + "instanceType": "t3.micro", "securityGroupIds": [ { "Fn::GetAtt": [ @@ -544,18 +602,41 @@ } ], "userData": { - "Fn::Base64": "#!/bin/bash" + "Fn::Base64": { + "Fn::Join": [ + "", + [ + "#!/bin/bash\n# fingerprint: e15d4219bb1dd06a\n(\n set +e\n /opt/aws/bin/cfn-init -v --region ", + { + "Ref": "AWS::Region" + }, + " --stack ", + { + "Ref": "AWS::StackName" + }, + " --resource targetInstance603C5817b329f03eca862331 -c default\n /opt/aws/bin/cfn-signal -e $? --region ", + { + "Ref": "AWS::Region" + }, + " --stack ", + { + "Ref": "AWS::StackName" + }, + " --resource targetInstance603C5817b329f03eca862331\n cat /var/log/cfn-init.log >&2\n)" + ] + ] + } } } }, "constructInfo": { - "fqn": "@aws-cdk/aws-ec2.CfnInstance", + "fqn": "@aws-cdk/core.CfnResource", "version": "0.0.0" } } }, "constructInfo": { - "fqn": "@aws-cdk/aws-ec2.Instance", + "fqn": "@aws-cdk/core.Resource", "version": "0.0.0" } }, @@ -605,7 +686,7 @@ } }, "constructInfo": { - "fqn": "@aws-cdk/aws-ec2.CfnSecurityGroup", + "fqn": "@aws-cdk/core.CfnResource", "version": "0.0.0" } }, @@ -634,13 +715,13 @@ } }, "constructInfo": { - "fqn": "@aws-cdk/aws-ec2.CfnSecurityGroupEgress", + "fqn": "@aws-cdk/core.CfnResource", "version": "0.0.0" } } }, "constructInfo": { - "fqn": "@aws-cdk/aws-ec2.SecurityGroup", + "fqn": "@aws-cdk/core.Resource", "version": "0.0.0" } }, @@ -661,10 +742,10 @@ "crossZone": true, "instances": [ { - "Ref": "targetInstance603C5817" + "Ref": "targetInstance603C5817b329f03eca862331" } ], - "scheme": "internal", + "scheme": "internet-facing", "securityGroups": [ { "Fn::GetAtt": [ @@ -675,19 +756,19 @@ ], "subnets": [ { - "Ref": "VPCPrivateSubnet1Subnet8BCA10E0" + "Ref": "VPCPublicSubnet1SubnetB4246D30" } ] } }, "constructInfo": { - "fqn": "@aws-cdk/aws-elasticloadbalancing.CfnLoadBalancer", + "fqn": "@aws-cdk/core.CfnResource", "version": "0.0.0" } } }, "constructInfo": { - "fqn": "@aws-cdk/aws-elasticloadbalancing.LoadBalancer", + "fqn": "@aws-cdk/core.Resource", "version": "0.0.0" } }, @@ -726,7 +807,7 @@ "path": "InstanceTargetTest/DefaultTest/Default", "constructInfo": { "fqn": "constructs.Construct", - "version": "10.1.237" + "version": "10.1.264" } }, "DeployAssert": { @@ -772,7 +853,7 @@ "path": "Tree", "constructInfo": { "fqn": "constructs.Construct", - "version": "10.1.237" + "version": "10.1.264" } } }, diff --git a/packages/@aws-cdk/aws-elasticloadbalancing/test/integ.instanceTarget.elb.ts b/packages/@aws-cdk/aws-elasticloadbalancing/test/integ.instanceTarget.elb.ts index 57ab3f1371ff0..a54154a229971 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancing/test/integ.instanceTarget.elb.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancing/test/integ.instanceTarget.elb.ts @@ -1,5 +1,7 @@ #!/usr/bin/env node import * as ec2 from '@aws-cdk/aws-ec2'; +// eslint-disable-next-line import/no-extraneous-dependencies +import * as iam from '@aws-cdk/aws-iam'; import * as cdk from '@aws-cdk/core'; import * as integ from '@aws-cdk/integ-tests'; import * as elb from '../lib'; @@ -14,16 +16,31 @@ const vpc = new ec2.Vpc(stack, 'VPC', { const instance = new ec2.Instance(stack, 'targetInstance', { vpc: vpc, instanceType: ec2.InstanceType.of( // t2.micro has free tier usage in aws - ec2.InstanceClass.T2, + ec2.InstanceClass.T3, ec2.InstanceSize.MICRO, ), machineImage: ec2.MachineImage.latestAmazonLinux({ generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2, }), + initOptions: { + timeout: cdk.Duration.minutes(30), + }, + init: ec2.CloudFormationInit.fromElements( + ec2.InitService.systemdConfigFile('pythonweb', { + command: '/usr/bin/python3 -m http.server 8080', + cwd: '/var/www/html', + }), + ec2.InitService.enable('pythonweb', { + serviceManager: ec2.ServiceManager.SYSTEMD, + }), + ec2.InitFile.fromString('/var/www/html/index.html', 'Hello! You can see me!'), + ), }); +instance.role.addManagedPolicy(iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonSSMManagedInstanceCore')); const elbalancer = new elb.LoadBalancer(stack, 'LB', { vpc, + internetFacing: true, }); elbalancer.addTarget(new elb.InstanceTarget(instance)); diff --git a/packages/@aws-cdk/aws-servicecatalogappregistry/README.md b/packages/@aws-cdk/aws-servicecatalogappregistry/README.md index 867803c159594..20e0f416d17cc 100644 --- a/packages/@aws-cdk/aws-servicecatalogappregistry/README.md +++ b/packages/@aws-cdk/aws-servicecatalogappregistry/README.md @@ -126,22 +126,6 @@ import * as cdk from "@aws-cdk/core"; const app = new App(); -class CustomAppRegistryAttributeGroup extends cdk.Stack { - public readonly attributeGroup: appreg.AttributeGroup - constructor(scope: Construct, id: string, props?: cdk.StackProps) { - super(scope, id, props); - const myAttributeGroup = new appreg.AttributeGroup(app, 'MyFirstAttributeGroup', { - attributeGroupName: 'MyAttributeGroupName', - description: 'Test attribute group', - attributes: {}, - }); - - this.attributeGroup = myAttributeGroup; - } -} - -const customAttributeGroup = new CustomAppRegistryAttributeGroup(app, 'AppRegistryAttributeGroup'); - const associatedApp = new appreg.ApplicationAssociator(app, 'AssociatedApplication', { applications: [appreg.TargetApplication.createApplicationStack({ applicationName: 'MyAssociatedApplication', @@ -154,7 +138,11 @@ const associatedApp = new appreg.ApplicationAssociator(app, 'AssociatedApplicati }); // Associate application to the attribute group. -customAttributeGroup.attributeGroup.associateWith(associatedApp.appRegistryApplication()); +associatedApp.appRegistryApplication.addAttributeGroup('MyAttributeGroup' , { + attributeGroupName: 'MyAttributeGroupName', + description: 'Test attribute group', + attributes: {}, +}); ``` diff --git a/packages/@aws-cdk/aws-servicecatalogappregistry/lib/application-associator.ts b/packages/@aws-cdk/aws-servicecatalogappregistry/lib/application-associator.ts index ecb17fc924f43..7c8ae60bc8e3e 100644 --- a/packages/@aws-cdk/aws-servicecatalogappregistry/lib/application-associator.ts +++ b/packages/@aws-cdk/aws-servicecatalogappregistry/lib/application-associator.ts @@ -77,7 +77,7 @@ export class ApplicationAssociator extends Construct { * Get the AppRegistry application. * */ - public appRegistryApplication(): IApplication { + public get appRegistryApplication(): IApplication { return this.application; } } diff --git a/packages/@aws-cdk/aws-servicecatalogappregistry/lib/application.ts b/packages/@aws-cdk/aws-servicecatalogappregistry/lib/application.ts index b630e1a883fb9..e5165676eb0da 100644 --- a/packages/@aws-cdk/aws-servicecatalogappregistry/lib/application.ts +++ b/packages/@aws-cdk/aws-servicecatalogappregistry/lib/application.ts @@ -3,7 +3,7 @@ import * as cdk from '@aws-cdk/core'; import { Names } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { StageStackAssociator } from './aspects/stack-associator'; -import { IAttributeGroup } from './attribute-group'; +import { AttributeGroup, IAttributeGroup } from './attribute-group'; import { getPrincipalsforSharing, hashValues, ShareOptions, SharePermission } from './common'; import { isAccountUnresolved } from './private/utils'; import { InputValidator } from './private/validation'; @@ -12,6 +12,29 @@ import { CfnApplication, CfnAttributeGroupAssociation, CfnResourceAssociation } const APPLICATION_READ_ONLY_RAM_PERMISSION_ARN = 'arn:aws:ram::aws:permission/AWSRAMPermissionServiceCatalogAppRegistryApplicationReadOnly'; const APPLICATION_ALLOW_ACCESS_RAM_PERMISSION_ARN = 'arn:aws:ram::aws:permission/AWSRAMPermissionServiceCatalogAppRegistryApplicationAllowAssociation'; +/** + * Properties for a Service Catalog AppRegistry Attribute Group + */ +export interface AttributeGroupAssociationProps { + /** + * Name for attribute group. + * + */ + readonly attributeGroupName: string; + + /** + * Description for attribute group. + * @default - No description provided + */ + readonly description?: string; + + /** + * A JSON of nested key-value pairs that represent the attributes in the group. + * Attributes maybe an empty JSON '{}', but must be explicitly stated. + */ + readonly attributes: { [key: string]: any }; +} + /** * A Service Catalog AppRegistry Application. */ @@ -41,6 +64,14 @@ export interface IApplication extends cdk.IResource { */ associateAttributeGroup(attributeGroup: IAttributeGroup): void; + /** + * Create an attribute group and associate this application with the created attribute group. + * + * @param id name of the AttributeGroup construct to be created. + * @param attributeGroupProps AppRegistry attribute group props + */ + addAttributeGroup(id: string, attributeGroupProps: AttributeGroupAssociationProps): IAttributeGroup; + /** * Associate this application with a CloudFormation stack. * @@ -114,6 +145,23 @@ abstract class ApplicationBase extends cdk.Resource implements IApplication { } } + /** + * Create an attribute group and associate this application with the created attribute group. + */ + public addAttributeGroup(id: string, props: AttributeGroupAssociationProps): IAttributeGroup { + const attributeGroup = new AttributeGroup(this, id, { + attributeGroupName: props.attributeGroupName, + attributes: props.attributes, + description: props.description, + }); + new CfnAttributeGroupAssociation(this, `AttributeGroupAssociation${this.generateUniqueHash(attributeGroup.node.addr)}`, { + application: this.applicationId, + attributeGroup: attributeGroup.attributeGroupId, + }); + this.associatedAttributeGroups.add(attributeGroup.node.addr); + return attributeGroup; + } + /** * Associate a stack with the application * If the resource is already associated, it will ignore duplicate request. diff --git a/packages/@aws-cdk/aws-servicecatalogappregistry/lib/aspects/stack-associator.ts b/packages/@aws-cdk/aws-servicecatalogappregistry/lib/aspects/stack-associator.ts index 25549c048937b..b6e81403d86ea 100644 --- a/packages/@aws-cdk/aws-servicecatalogappregistry/lib/aspects/stack-associator.ts +++ b/packages/@aws-cdk/aws-servicecatalogappregistry/lib/aspects/stack-associator.ts @@ -132,7 +132,7 @@ export class CheckedStageStackAssociator extends StackAssociatorBase { constructor(app: ApplicationAssociator, props?: StackAssociatorBaseProps) { super(props); - this.application = app.appRegistryApplication(); + this.application = app.appRegistryApplication; this.applicationAssociator = app; } } diff --git a/packages/@aws-cdk/aws-servicecatalogappregistry/lib/target-application.ts b/packages/@aws-cdk/aws-servicecatalogappregistry/lib/target-application.ts index a336f72a9df0e..e2e0bb124b57a 100644 --- a/packages/@aws-cdk/aws-servicecatalogappregistry/lib/target-application.ts +++ b/packages/@aws-cdk/aws-servicecatalogappregistry/lib/target-application.ts @@ -45,7 +45,7 @@ export interface CreateTargetApplicationOptions extends TargetApplicationCommonO /** * Whether create cloudFormation Output for application manager URL. * - * @default - Application containing stacks deployed via CDK. + * @default - true */ readonly emitApplicationManagerUrlAsOutput?: boolean; } diff --git a/packages/@aws-cdk/aws-servicecatalogappregistry/test/application-associator.test.ts b/packages/@aws-cdk/aws-servicecatalogappregistry/test/application-associator.test.ts index e6b43fce35dab..db2540a32b7ba 100644 --- a/packages/@aws-cdk/aws-servicecatalogappregistry/test/application-associator.test.ts +++ b/packages/@aws-cdk/aws-servicecatalogappregistry/test/application-associator.test.ts @@ -23,12 +23,12 @@ describe('Scope based Associations with Application within Same Account', () => }); const anotherStack = new AppRegistrySampleStack(app, 'SampleStack'); - Template.fromStack(appAssociator.appRegistryApplication().stack).resourceCountIs('AWS::ServiceCatalogAppRegistry::Application', 1); - Template.fromStack(appAssociator.appRegistryApplication().stack).hasResourceProperties('AWS::ServiceCatalogAppRegistry::Application', { + Template.fromStack(appAssociator.appRegistryApplication.stack).resourceCountIs('AWS::ServiceCatalogAppRegistry::Application', 1); + Template.fromStack(appAssociator.appRegistryApplication.stack).hasResourceProperties('AWS::ServiceCatalogAppRegistry::Application', { Name: 'MyAssociatedApplication', Tags: { managedBy: 'CDK_Application_Associator' }, }); - Template.fromStack(appAssociator.appRegistryApplication().stack).hasOutput('DefaultCdkApplicationApplicationManagerUrl27C138EF', {}); + Template.fromStack(appAssociator.appRegistryApplication.stack).hasOutput('DefaultCdkApplicationApplicationManagerUrl27C138EF', {}); Template.fromStack(anotherStack).resourceCountIs('AWS::ServiceCatalogAppRegistry::ResourceAssociation', 1); Template.fromStack(anotherStack).hasResourceProperties('AWS::ServiceCatalogAppRegistry::ResourceAssociation', { Application: 'MyAssociatedApplication', @@ -46,14 +46,14 @@ describe('Scope based Associations with Application within Same Account', () => }); const anotherStack = new AppRegistrySampleStack(app, 'SampleStack'); - Template.fromStack(appAssociator.appRegistryApplication().stack).resourceCountIs('AWS::ServiceCatalogAppRegistry::Application', 1); - Template.fromStack(appAssociator.appRegistryApplication().stack).hasResourceProperties('AWS::ServiceCatalogAppRegistry::Application', { + Template.fromStack(appAssociator.appRegistryApplication.stack).resourceCountIs('AWS::ServiceCatalogAppRegistry::Application', 1); + Template.fromStack(appAssociator.appRegistryApplication.stack).hasResourceProperties('AWS::ServiceCatalogAppRegistry::Application', { Name: 'MyAssociatedApplication', Tags: { managedBy: 'CDK_Application_Associator' }, }); expect( - Template.fromStack(appAssociator.appRegistryApplication().stack) + Template.fromStack(appAssociator.appRegistryApplication.stack) .findOutputs('*', {}), ).toEqual({}); Template.fromStack(anotherStack).resourceCountIs('AWS::ServiceCatalogAppRegistry::ResourceAssociation', 1); @@ -85,7 +85,7 @@ describe('Associate attribute group with Application', () => { })], }); - customAttributeGroup.attributeGroup.associateWith(appAssociator.appRegistryApplication()); + customAttributeGroup.attributeGroup.associateWith(appAssociator.appRegistryApplication); Template.fromStack(customAttributeGroup.attributeGroup.stack).resourceCountIs('AWS::ServiceCatalogAppRegistry::AttributeGroupAssociation', 1); Template.fromStack(customAttributeGroup.attributeGroup.stack).hasResourceProperties('AWS::ServiceCatalogAppRegistry::AttributeGroupAssociation', { Application: 'TestAssociatedApplication', @@ -137,7 +137,7 @@ describe('Scope based Associations with Application with Cross Region/Account', }); expect( - Template.fromStack(appAssociator.appRegistryApplication().stack).findOutputs('*', {}), + Template.fromStack(appAssociator.appRegistryApplication.stack).findOutputs('*', {}), ).toEqual({}); Template.fromStack(firstStack).resourceCountIs('AWS::ServiceCatalogAppRegistry::ResourceAssociation', 1); Template.fromStack(nestedStack).resourceCountIs('AWS::ServiceCatalogAppRegistry::ResourceAssociation', 1); @@ -268,7 +268,7 @@ describe('Scope based Associations with Application with Cross Region/Account', associateStage: true, }); app.synth(); - Template.fromStack(application.appRegistryApplication().stack).hasOutput('DefaultCdkApplicationApplicationManagerUrl27C138EF', {}); + Template.fromStack(application.appRegistryApplication.stack).hasOutput('DefaultCdkApplicationApplicationManagerUrl27C138EF', {}); Template.fromStack(pipelineStack).resourceCountIs('AWS::ServiceCatalogAppRegistry::ResourceAssociation', 1); }); }); diff --git a/packages/@aws-cdk/aws-servicecatalogappregistry/test/application.test.ts b/packages/@aws-cdk/aws-servicecatalogappregistry/test/application.test.ts index f78afa9d3ca3c..8bc60676081aa 100644 --- a/packages/@aws-cdk/aws-servicecatalogappregistry/test/application.test.ts +++ b/packages/@aws-cdk/aws-servicecatalogappregistry/test/application.test.ts @@ -160,6 +160,31 @@ describe('Application', () => { }); }), + test('associate new attribute group', () => { + application.addAttributeGroup('AttributeGroup', { + attributeGroupName: 'AttributeGroupName', + attributes: {}, + description: 'Description for Attribute Group', + }); + + Template.fromStack(stack).hasResourceProperties('AWS::ServiceCatalogAppRegistry::AttributeGroupAssociation', { + Application: { 'Fn::GetAtt': ['MyApplication5C63EC1D', 'Id'] }, + AttributeGroup: { 'Fn::GetAtt': ['MyApplicationAttributeGroup0BD166B6', 'Id'] }, + }); + + Template.fromStack(stack).templateMatches({ + Resources: { + MyApplicationAttributeGroup0BD166B6: { + Type: 'AWS::ServiceCatalogAppRegistry::AttributeGroup', + Properties: { + Name: 'AttributeGroupName', + Attributes: {}, + }, + }, + }, + }); + }), + test('duplicate attribute group association are idempotent', () => { const attributeGroup = new appreg.AttributeGroup(stack, 'AttributeGroup', { attributeGroupName: 'attributeGroupName', diff --git a/packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.application.js.snapshot/cdk.out b/packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.application.js.snapshot/cdk.out index b72fef144f05c..7925065efbcc4 100644 --- a/packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.application.js.snapshot/cdk.out +++ b/packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.application.js.snapshot/cdk.out @@ -1 +1 @@ -{"version":"30.1.0"} \ No newline at end of file +{"version":"31.0.0"} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.application.js.snapshot/integ-servicecatalogappregistry-application.assets.json b/packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.application.js.snapshot/integ-servicecatalogappregistry-application.assets.json index 9081ac09e1a15..fd7204bdc34c2 100644 --- a/packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.application.js.snapshot/integ-servicecatalogappregistry-application.assets.json +++ b/packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.application.js.snapshot/integ-servicecatalogappregistry-application.assets.json @@ -1,7 +1,7 @@ { - "version": "30.1.0", + "version": "31.0.0", "files": { - "2332c6df6777cc571585060fa4888d6d3b9ef548aa00dcbfc53fbdde386d7591": { + "5fbf2a286122f4bc412b1730f96351e289444b1122006f36e4ade8fae8442765": { "source": { "path": "integ-servicecatalogappregistry-application.template.json", "packaging": "file" @@ -9,7 +9,7 @@ "destinations": { "current_account-current_region": { "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", - "objectKey": "2332c6df6777cc571585060fa4888d6d3b9ef548aa00dcbfc53fbdde386d7591.json", + "objectKey": "5fbf2a286122f4bc412b1730f96351e289444b1122006f36e4ade8fae8442765.json", "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" } } diff --git a/packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.application.js.snapshot/integ-servicecatalogappregistry-application.template.json b/packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.application.js.snapshot/integ-servicecatalogappregistry-application.template.json index 9fcf50e708a56..db928079d96ac 100644 --- a/packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.application.js.snapshot/integ-servicecatalogappregistry-application.template.json +++ b/packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.application.js.snapshot/integ-servicecatalogappregistry-application.template.json @@ -39,10 +39,50 @@ } } }, - "TestApplicationRAMSharead8ba81b8cdd40199FD1": { + "TestApplicationmyAnotherAttributeGroup375F79DB": { + "Type": "AWS::ServiceCatalogAppRegistry::AttributeGroup", + "Properties": { + "Attributes": { + "stage": "alpha", + "teamMembers": [ + "markI", + "markII", + "markIII" + ], + "public": false, + "publishYear": 2021, + "plannedRoadMap": { + "alpha": "some time", + "beta": "another time", + "gamma": "penultimate time", + "release": "go time" + } + }, + "Name": "myAnotherAttributeGroup", + "Description": "my another attribute group description" + } + }, + "TestApplicationAttributeGroupAssociationb6f47e836a8c4FCAC29E": { + "Type": "AWS::ServiceCatalogAppRegistry::AttributeGroupAssociation", + "Properties": { + "Application": { + "Fn::GetAtt": [ + "TestApplication2FBC585F", + "Id" + ] + }, + "AttributeGroup": { + "Fn::GetAtt": [ + "TestApplicationmyAnotherAttributeGroup375F79DB", + "Id" + ] + } + } + }, + "TestApplicationRAMShare004736f08f8e57044D5D": { "Type": "AWS::RAM::ResourceShare", "Properties": { - "Name": "RAMSharead8ba81b8cdd", + "Name": "RAMShare004736f08f8e", "AllowExternalPrincipals": false, "PermissionArns": [ "arn:aws:ram::aws:permission/AWSRAMPermissionServiceCatalogAppRegistryApplicationReadOnly" diff --git a/packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.application.js.snapshot/integ.json b/packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.application.js.snapshot/integ.json index 519ddfd3c055c..dff088cc3537e 100644 --- a/packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.application.js.snapshot/integ.json +++ b/packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.application.js.snapshot/integ.json @@ -1,5 +1,5 @@ { - "version": "30.1.0", + "version": "31.0.0", "testCases": { "integ.application": { "stacks": [ diff --git a/packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.application.js.snapshot/manifest.json b/packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.application.js.snapshot/manifest.json index 689db41c3804f..65f104d2322af 100644 --- a/packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.application.js.snapshot/manifest.json +++ b/packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.application.js.snapshot/manifest.json @@ -1,5 +1,5 @@ { - "version": "30.1.0", + "version": "31.0.0", "artifacts": { "integ-servicecatalogappregistry-application.assets": { "type": "cdk:asset-manifest", @@ -17,7 +17,7 @@ "validateOnSynth": false, "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", - "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/2332c6df6777cc571585060fa4888d6d3b9ef548aa00dcbfc53fbdde386d7591.json", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/5fbf2a286122f4bc412b1730f96351e289444b1122006f36e4ade8fae8442765.json", "requiresBootstrapStackVersion": 6, "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", "additionalDependencies": [ @@ -51,10 +51,22 @@ "data": "TestApplicationAttributeGroupAssociation4ba7f5842818B8EE1C6F" } ], - "/integ-servicecatalogappregistry-application/TestApplication/RAMSharead8ba81b8cdd": [ + "/integ-servicecatalogappregistry-application/TestApplication/myAnotherAttributeGroup/Resource": [ { "type": "aws:cdk:logicalId", - "data": "TestApplicationRAMSharead8ba81b8cdd40199FD1" + "data": "TestApplicationmyAnotherAttributeGroup375F79DB" + } + ], + "/integ-servicecatalogappregistry-application/TestApplication/AttributeGroupAssociationb6f47e836a8c": [ + { + "type": "aws:cdk:logicalId", + "data": "TestApplicationAttributeGroupAssociationb6f47e836a8c4FCAC29E" + } + ], + "/integ-servicecatalogappregistry-application/TestApplication/RAMShare004736f08f8e": [ + { + "type": "aws:cdk:logicalId", + "data": "TestApplicationRAMShare004736f08f8e57044D5D" } ], "/integ-servicecatalogappregistry-application/TestAttributeGroup/Resource": [ @@ -80,6 +92,15 @@ "type": "aws:cdk:logicalId", "data": "CheckBootstrapVersion" } + ], + "TestApplicationRAMSharead8ba81b8cdd40199FD1": [ + { + "type": "aws:cdk:logicalId", + "data": "TestApplicationRAMSharead8ba81b8cdd40199FD1", + "trace": [ + "!!DESTRUCTIVE_CHANGES: WILL_DESTROY" + ] + } ] }, "displayName": "integ-servicecatalogappregistry-application" diff --git a/packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.application.js.snapshot/tree.json b/packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.application.js.snapshot/tree.json index ef111bc49a7b8..f55cdb42b5a66 100644 --- a/packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.application.js.snapshot/tree.json +++ b/packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.application.js.snapshot/tree.json @@ -75,13 +75,79 @@ "version": "0.0.0" } }, - "RAMSharead8ba81b8cdd": { - "id": "RAMSharead8ba81b8cdd", - "path": "integ-servicecatalogappregistry-application/TestApplication/RAMSharead8ba81b8cdd", + "myAnotherAttributeGroup": { + "id": "myAnotherAttributeGroup", + "path": "integ-servicecatalogappregistry-application/TestApplication/myAnotherAttributeGroup", + "children": { + "Resource": { + "id": "Resource", + "path": "integ-servicecatalogappregistry-application/TestApplication/myAnotherAttributeGroup/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::ServiceCatalogAppRegistry::AttributeGroup", + "aws:cdk:cloudformation:props": { + "attributes": { + "stage": "alpha", + "teamMembers": [ + "markI", + "markII", + "markIII" + ], + "public": false, + "publishYear": 2021, + "plannedRoadMap": { + "alpha": "some time", + "beta": "another time", + "gamma": "penultimate time", + "release": "go time" + } + }, + "name": "myAnotherAttributeGroup", + "description": "my another attribute group description" + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-servicecatalogappregistry.CfnAttributeGroup", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-servicecatalogappregistry.AttributeGroup", + "version": "0.0.0" + } + }, + "AttributeGroupAssociationb6f47e836a8c": { + "id": "AttributeGroupAssociationb6f47e836a8c", + "path": "integ-servicecatalogappregistry-application/TestApplication/AttributeGroupAssociationb6f47e836a8c", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::ServiceCatalogAppRegistry::AttributeGroupAssociation", + "aws:cdk:cloudformation:props": { + "application": { + "Fn::GetAtt": [ + "TestApplication2FBC585F", + "Id" + ] + }, + "attributeGroup": { + "Fn::GetAtt": [ + "TestApplicationmyAnotherAttributeGroup375F79DB", + "Id" + ] + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-servicecatalogappregistry.CfnAttributeGroupAssociation", + "version": "0.0.0" + } + }, + "RAMShare004736f08f8e": { + "id": "RAMShare004736f08f8e", + "path": "integ-servicecatalogappregistry-application/TestApplication/RAMShare004736f08f8e", "attributes": { "aws:cdk:cloudformation:type": "AWS::RAM::ResourceShare", "aws:cdk:cloudformation:props": { - "name": "RAMSharead8ba81b8cdd", + "name": "RAMShare004736f08f8e", "allowExternalPrincipals": false, "permissionArns": [ "arn:aws:ram::aws:permission/AWSRAMPermissionServiceCatalogAppRegistryApplicationReadOnly" @@ -241,7 +307,7 @@ "path": "Tree", "constructInfo": { "fqn": "constructs.Construct", - "version": "10.1.252" + "version": "10.1.270" } } }, diff --git a/packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.application.ts b/packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.application.ts index 9635a126e2b05..f44ba7f0a31d7 100644 --- a/packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.application.ts +++ b/packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.application.ts @@ -33,6 +33,26 @@ const attributeGroup = new appreg.AttributeGroup(stack, 'TestAttributeGroup', { application.associateStack(stack); application.associateAttributeGroup(attributeGroup); +application.addAttributeGroup('myAnotherAttributeGroup', { + attributeGroupName: 'myAnotherAttributeGroup', + attributes: { + stage: 'alpha', + teamMembers: [ + 'markI', + 'markII', + 'markIII', + ], + public: false, + publishYear: 2021, + plannedRoadMap: { + alpha: 'some time', + beta: 'another time', + gamma: 'penultimate time', + release: 'go time', + }, + }, + description: 'my another attribute group description', +}); const myRole = new iam.Role(stack, 'MyRole', { assumedBy: new iam.AccountPrincipal(stack.account), }); diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/states/state.ts b/packages/@aws-cdk/aws-stepfunctions/lib/states/state.ts index 66869e282b163..901743caec625 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/states/state.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/states/state.ts @@ -1,7 +1,7 @@ import { Token } from '@aws-cdk/core'; import { IConstruct, Construct, Node } from 'constructs'; import { Condition } from '../condition'; -import { FieldUtils, JsonPath } from '../fields'; +import { FieldUtils } from '../fields'; import { StateGraph } from '../state-graph'; import { CatchProps, Errors, IChainable, INextable, RetryProps } from '../types'; @@ -578,7 +578,6 @@ export function renderList(xs: T[], mapFn: (x: T) => any, sortFn?: (a: T, b: */ export function renderJsonPath(jsonPath?: string): undefined | null | string { if (jsonPath === undefined) { return undefined; } - if (jsonPath === JsonPath.DISCARD) { return null; } if (!Token.isUnresolved(jsonPath) && !jsonPath.startsWith('$')) { throw new Error(`Expected JSON path to start with '$', got: ${jsonPath}`); diff --git a/packages/@aws-cdk/aws-stepfunctions/test/state.test.ts b/packages/@aws-cdk/aws-stepfunctions/test/state.test.ts index 0b2157f7526ec..d878935dd5056 100644 --- a/packages/@aws-cdk/aws-stepfunctions/test/state.test.ts +++ b/packages/@aws-cdk/aws-stepfunctions/test/state.test.ts @@ -1,26 +1,33 @@ +import * as assert from '@aws-cdk/assertions'; import * as cdk from '@aws-cdk/core'; import { FakeTask } from './fake-task'; -import { renderGraph } from './private/render-util'; -import { JsonPath } from '../lib'; +import { JsonPath, StateMachine } from '../lib'; test('JsonPath.DISCARD can be used to discard a state\'s output', () => { - const stack = new cdk.Stack(); - + // GIVEN + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'TestStack'); const task = new FakeTask(stack, 'my-state', { inputPath: JsonPath.DISCARD, outputPath: JsonPath.DISCARD, resultPath: JsonPath.DISCARD, }); + new StateMachine(stack, 'state-machine', { + definition: task, + }); + + // WHEN + const definitionString = new assert.Capture(); + assert.Template.fromStack(stack).hasResourceProperties('AWS::StepFunctions::StateMachine', { + DefinitionString: definitionString, + }); + + // THEN + const definition = JSON.parse(definitionString.asString()); - expect(renderGraph(task)).toEqual({ - StartAt: 'my-state', + expect(definition).toMatchObject({ States: { 'my-state': { - End: true, - Type: 'Task', - Resource: expect.any(String), - Parameters: expect.any(Object), - // The important bits: InputPath: null, OutputPath: null, ResultPath: null, diff --git a/packages/@aws-cdk/cfnspec/build-tools/spec-diff.ts b/packages/@aws-cdk/cfnspec/build-tools/spec-diff.ts index b39f50aa17fdb..d575b1ed5939f 100644 --- a/packages/@aws-cdk/cfnspec/build-tools/spec-diff.ts +++ b/packages/@aws-cdk/cfnspec/build-tools/spec-diff.ts @@ -27,6 +27,8 @@ async function main() { oldSpec.ResourceTypes = {}; } + validatePropertyTypeNameConsistency(oldSpec, newSpec); + const out = jsonDiff(oldSpec, newSpec); // Here's the magic output format of this thing @@ -278,6 +280,56 @@ async function main() { } } +/** + * Safeguard check: make sure that all old property type names in the old spec exist in the new spec + * + * If not, it's probably because the service team renamed a type between spec + * version `v(N)` to `v(N+1)`.. In the CloudFormation spec itself, this is not a + * problem. However, CDK will have generated actual classes and interfaces with + * the type names at `v(N)`, which people will have written code against. If the + * classes and interfaces would have a new name at `v(N+1)`, all user code would + * break. + */ +function validatePropertyTypeNameConsistency(oldSpec: any, newSpec: any) { + const newPropsTypes = newSpec.PropertyTypes ?? {}; + const disappearedKeys = Object.keys(oldSpec.PropertyTypes ?? {}).filter(k => !(k in newPropsTypes)); + if (disappearedKeys.length === 0) { + return; + } + + const exampleJsonPatch = { + patch: { + description: 'Undoing upstream property type renames of because ', + operations: disappearedKeys.map((key) => ({ + op: 'move', + from: `/PropertyTypes/${key.split('.')[0]}.`, + path: `/PropertyTypes/${key}`, + })), + }, + }; + + process.stderr.write([ + '┌───────────────────────────────────────────────────────────────────────────────────────┐', + '│ ▐█', + '│ PROPERTY TYPES HAVE DISAPPEARED ▐█', + '│ ▐█', + '│ Some type names have disappeared from the old specification. ▐█', + '│ ▐█', + '│ This probably indicates that the service team renamed one of the types. We have ▐█', + '│ to keep the old type names though: renaming them would constitute a breaking change ▐█', + '│ to consumers of the L1 resources. ▐█', + '│ ▐█', + '└─▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▟█', + '', + 'See what the renames were, check out this PR locally and add a JSON patch file for these types:', + '', + '(Example)', + '', + JSON.stringify(exampleJsonPatch, undefined, 2), + ].join('\n')); + process.exitCode = 1; +} + main().catch(e => { process.stderr.write(e.stack); process.stderr.write('\n'); diff --git a/packages/@aws-cdk/cfnspec/build-tools/update.sh b/packages/@aws-cdk/cfnspec/build-tools/update.sh index 7b3c73eac2d3d..78fd3fdc57a92 100755 --- a/packages/@aws-cdk/cfnspec/build-tools/update.sh +++ b/packages/@aws-cdk/cfnspec/build-tools/update.sh @@ -10,6 +10,8 @@ scriptdir=$(cd $(dirname $0) && pwd) rm -f CHANGELOG.md.new + +# update-spec <SOURCE> <TARGETDIR> <IS_GZIPPED> <SHOULD_SPLIT> [<SVC> [...]] function update-spec() { local title=$1 local url=$2 diff --git a/packages/@aws-cdk/cfnspec/spec-source/cfn-docs/cfn-docs.json b/packages/@aws-cdk/cfnspec/spec-source/cfn-docs/cfn-docs.json index f5f9f8aa37cdc..67e28f2481630 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/cfn-docs/cfn-docs.json +++ b/packages/@aws-cdk/cfnspec/spec-source/cfn-docs/cfn-docs.json @@ -1932,7 +1932,7 @@ "properties": { "ApplicationId": "The application ID.", "Description": "A description of the configuration profile.", - "LocationUri": "A URI to locate the configuration. You can specify the following:\n\n- For the AWS AppConfig hosted configuration store and for feature flags, specify `hosted` .\n- For an AWS Systems Manager Parameter Store parameter, specify either the parameter name in the format `ssm-parameter://<parameter name>` or the ARN.\n- For an AWS Secrets Manager secret, specify the URI in the following format: `secrets-manager` ://<secret name>.\n- For an Amazon S3 object, specify the URI in the following format: `s3://<bucket>/<objectKey>` . Here is an example: `s3://my-bucket/my-app/us-east-1/my-config.json`\n- For an SSM document, specify either the document name in the format `ssm-document://<document name>` or the Amazon Resource Name (ARN).", + "LocationUri": "A URI to locate the configuration. You can specify the following:\n\n- For the AWS AppConfig hosted configuration store and for feature flags, specify `hosted` .\n- For an AWS Systems Manager Parameter Store parameter, specify either the parameter name in the format `ssm-parameter://<parameter name>` or the ARN.\n- For an AWS Secrets Manager secret, specify the URI in the following format: `secretsmanager` ://<secret name>.\n- For an Amazon S3 object, specify the URI in the following format: `s3://<bucket>/<objectKey>` . Here is an example: `s3://my-bucket/my-app/us-east-1/my-config.json`\n- For an SSM document, specify either the document name in the format `ssm-document://<document name>` or the Amazon Resource Name (ARN).", "Name": "A name for the configuration profile.", "RetrievalRoleArn": "The ARN of an IAM role with permission to access the configuration at the specified `LocationUri` .\n\n> A retrieval role ARN is not required for configurations stored in the AWS AppConfig hosted configuration store. It is required for all other sources that store your configuration.", "Tags": "Metadata to assign to the configuration profile. Tags help organize and categorize your AWS AppConfig resources. Each tag consists of a key and an optional value, both of which you define.", @@ -5221,7 +5221,7 @@ "description": "Contains customized metric specification information for a target tracking scaling policy for Application Auto Scaling.\n\nFor information about the available metrics for a service, see [AWS services that publish CloudWatch metrics](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/aws-services-cloudwatch-metrics.html) in the *Amazon CloudWatch User Guide* .\n\nTo create your customized metric specification:\n\n- Add values for each required parameter from CloudWatch. You can use an existing metric, or a new metric that you create. To use your own metric, you must first publish the metric to CloudWatch. For more information, see [Publish custom metrics](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/publishingMetrics.html) in the *Amazon CloudWatch User Guide* .\n- Choose a metric that changes proportionally with capacity. The value of the metric should increase or decrease in inverse proportion to the number of capacity units. That is, the value of the metric should decrease when capacity increases, and increase when capacity decreases.\n\nFor an example of how creating new metrics can be useful, see [Scaling based on Amazon SQS](https://docs.aws.amazon.com/autoscaling/ec2/userguide/as-using-sqs-queue.html) in the *Amazon EC2 Auto Scaling User Guide* . This topic mentions Auto Scaling groups, but the same scenario for Amazon SQS can apply to the target tracking scaling policies that you create for a Spot Fleet by using Application Auto Scaling.\n\nFor more information about the CloudWatch terminology below, see [Amazon CloudWatch concepts](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/cloudwatch_concepts.html) .\n\n`CustomizedMetricSpecification` is a property of the [AWS::ApplicationAutoScaling::ScalingPolicy TargetTrackingScalingPolicyConfiguration](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-applicationautoscaling-scalingpolicy-targettrackingscalingpolicyconfiguration.html) property type.", "properties": { "Dimensions": "The dimensions of the metric.\n\nConditional: If you published your metric with dimensions, you must specify the same dimensions in your scaling policy.", - "MetricName": "The name of the metric. To get the exact metric name, namespace, and dimensions, inspect the [Metric](https://docs.aws.amazon.com/AmazonCloudWatch/latest/APIReference/API_Metric.html) object that is returned by a call to [ListMetrics](https://docs.aws.amazon.com/AmazonCloudWatch/latest/APIReference/API_ListMetrics.html) .", + "MetricName": "The name of the metric. To get the exact metric name, namespace, and dimensions, inspect the [Metric](https://docs.aws.amazon.com/AmazonCloudWatch/latest/APIReference/API_Metric.html) object that's returned by a call to [ListMetrics](https://docs.aws.amazon.com/AmazonCloudWatch/latest/APIReference/API_ListMetrics.html) .", "Namespace": "The namespace of the metric.", "Statistic": "The statistic of the metric.", "Unit": "The unit of the metric. For a complete list of the units that CloudWatch supports, see the [MetricDatum](https://docs.aws.amazon.com/AmazonCloudWatch/latest/APIReference/API_MetricDatum.html) data type in the *Amazon CloudWatch API Reference* ." @@ -5487,6 +5487,20 @@ "WorkGroupConfiguration": "The configuration of the workgroup, which includes the location in Amazon S3 where query results are stored, the encryption option, if any, used for query results, whether Amazon CloudWatch Metrics are enabled for the workgroup, and the limit for the amount of bytes scanned (cutoff) per query, if it is specified. The `EnforceWorkGroupConfiguration` option determines whether workgroup settings override client-side query settings." } }, + "AWS::Athena::WorkGroup.AclConfiguration": { + "attributes": {}, + "description": "Indicates that an Amazon S3 canned ACL should be set to control ownership of stored query results. When Athena stores query results in Amazon S3, the canned ACL is set with the `x-amz-acl` request header. For more information about S3 Object Ownership, see [Object Ownership settings](https://docs.aws.amazon.com/AmazonS3/latest/userguide/about-object-ownership.html#object-ownership-overview) in the *Amazon S3 User Guide* .", + "properties": { + "S3AclOption": "The Amazon S3 canned ACL that Athena should specify when storing query results. Currently the only supported canned ACL is `BUCKET_OWNER_FULL_CONTROL` . If a query runs in a workgroup and the workgroup overrides client-side settings, then the Amazon S3 canned ACL specified in the workgroup's settings is used for all queries that run in the workgroup. For more information about Amazon S3 canned ACLs, see [Canned ACL](https://docs.aws.amazon.com/AmazonS3/latest/userguide/acl-overview.html#canned-acl) in the *Amazon S3 User Guide* ." + } + }, + "AWS::Athena::WorkGroup.CustomerContentEncryptionConfiguration": { + "attributes": {}, + "description": "Specifies the KMS key that is used to encrypt the user's data stores in Athena.", + "properties": { + "KmsKey": "The KMS key that is used to encrypt the user's data stores in Athena." + } + }, "AWS::Athena::WorkGroup.EncryptionConfiguration": { "attributes": {}, "description": "If query results are encrypted in Amazon S3, indicates the encryption option used (for example, `SSE_KMS` or `CSE_KMS` ) and key information.", @@ -5507,7 +5521,9 @@ "attributes": {}, "description": "The location in Amazon S3 where query and calculation results are stored and the encryption option, if any, used for query and calculation results. These are known as \"client-side settings\". If workgroup settings override client-side settings, then the query uses the workgroup settings.", "properties": { + "AclConfiguration": "Indicates that an Amazon S3 canned ACL should be set to control ownership of stored query results. Currently the only supported canned ACL is `BUCKET_OWNER_FULL_CONTROL` . This is a client-side setting. If workgroup settings override client-side settings, then the query uses the ACL configuration that is specified for the workgroup, and also uses the location for storing query results specified in the workgroup. For more information, see `WorkGroupConfiguration$EnforceWorkGroupConfiguration` and [Workgroup Settings Override Client-Side Settings](https://docs.aws.amazon.com/athena/latest/ug/workgroups-settings-override.html) .", "EncryptionConfiguration": "If query results are encrypted in Amazon S3, indicates the encryption option used (for example, `SSE_KMS` or `CSE_KMS` ) and key information. This is a client-side setting. If workgroup settings override client-side settings, then the query uses the encryption configuration that is specified for the workgroup, and also uses the location for storing query results specified in the workgroup. See `EnforceWorkGroupConfiguration` and [Workgroup Settings Override Client-Side Settings](https://docs.aws.amazon.com/athena/latest/ug/workgroups-settings-override.html) .", + "ExpectedBucketOwner": "The AWS account ID that you expect to be the owner of the Amazon S3 bucket specified by `ResultConfiguration$OutputLocation` . If set, Athena uses the value for `ExpectedBucketOwner` when it makes Amazon S3 calls to your specified output location. If the `ExpectedBucketOwner` AWS account ID does not match the actual owner of the Amazon S3 bucket, the call fails with a permissions error.\n\nThis is a client-side setting. If workgroup settings override client-side settings, then the query uses the `ExpectedBucketOwner` setting that is specified for the workgroup, and also uses the location for storing query results specified in the workgroup. See `WorkGroupConfiguration$EnforceWorkGroupConfiguration` and [Workgroup Settings Override Client-Side Settings](https://docs.aws.amazon.com/athena/latest/ug/workgroups-settings-override.html) .", "OutputLocation": "The location in Amazon S3 where your query results are stored, such as `s3://path/to/query/bucket/` . To run a query, you must specify the query results location using either a client-side setting for individual queries or a location specified by the workgroup. If workgroup settings override client-side settings, then the query uses the location specified for the workgroup. If no query location is set, Athena issues an error. For more information, see [Working with Query Results, Output Files, and Query History](https://docs.aws.amazon.com/athena/latest/ug/querying.html) and `EnforceWorkGroupConfiguration` ." } }, @@ -5515,9 +5531,12 @@ "attributes": {}, "description": "The configuration of the workgroup, which includes the location in Amazon S3 where query results are stored, the encryption option, if any, used for query results, whether Amazon CloudWatch Metrics are enabled for the workgroup, and the limit for the amount of bytes scanned (cutoff) per query, if it is specified. The `EnforceWorkGroupConfiguration` option determines whether workgroup settings override client-side query settings.", "properties": { + "AdditionalConfiguration": "Specifies a user defined JSON string that is passed to the notebook engine.", "BytesScannedCutoffPerQuery": "The upper limit (cutoff) for the amount of bytes a single query in a workgroup is allowed to scan. No default is defined.\n\n> This property currently supports integer types. Support for long values is planned.", + "CustomerContentEncryptionConfiguration": "Specifies the KMS key that is used to encrypt the user's data stores in Athena.", "EnforceWorkGroupConfiguration": "If set to \"true\", the settings for the workgroup override client-side settings. If set to \"false\", client-side settings are used. For more information, see [Workgroup Settings Override Client-Side Settings](https://docs.aws.amazon.com/athena/latest/ug/workgroups-settings-override.html) .", "EngineVersion": "The engine version that all queries running on the workgroup use. Queries on the `AmazonAthenaPreviewFunctionality` workgroup run on the preview engine regardless of this setting.", + "ExecutionRole": "Role used in a session for accessing the user's resources.", "PublishCloudWatchMetricsEnabled": "Indicates that the Amazon CloudWatch metrics are enabled for the workgroup.", "RequesterPaysEnabled": "If set to `true` , allows members assigned to a workgroup to reference Amazon S3 Requester Pays buckets in queries. If set to `false` , workgroup members cannot query data from Requester Pays buckets, and queries that retrieve data from Requester Pays buckets cause an error. The default is `false` . For more information about Requester Pays buckets, see [Requester Pays Buckets](https://docs.aws.amazon.com/AmazonS3/latest/dev/RequesterPaysBuckets.html) in the *Amazon Simple Storage Service Developer Guide* .", "ResultConfiguration": "Specifies the location in Amazon S3 where query results are stored and the encryption option, if any, used for query results. For more information, see [Working with Query Results, Output Files, and Query History](https://docs.aws.amazon.com/athena/latest/ug/querying.html) ." @@ -7377,6 +7396,24 @@ "ValidationDomain": "The domain name to which you want ACM to send validation emails. This domain name is the suffix of the email addresses that you want ACM to use. This must be the same as the `DomainName` value or a superdomain of the `DomainName` value. For example, if you request a certificate for `testing.example.com` , you can specify `example.com` as this value. In that case, ACM sends domain validation emails to the following five addresses:\n\n- admin@example.com\n- administrator@example.com\n- hostmaster@example.com\n- postmaster@example.com\n- webmaster@example.com" } }, + "AWS::Chatbot::MicrosoftTeamsChannelConfiguration": { + "attributes": { + "Arn": "", + "Ref": "When you pass the logical ID of this resource to the intrinsic Ref function, Ref returns the ARN of the configuration created." + }, + "description": "The `AWS::Chatbot::MicrosoftTeamsChannelConfiguration` resource configures a Microsoft Teams channel to allow users to use AWS Chatbot with AWS CloudFormation templates.\n\nThis resource requires some setup to be done in the AWS Chatbot console. To provide the required Microsoft Teams team and tenant IDs, you must perform the initial authorization flow with Microsoft Teams in the AWS Chatbot console, then copy and paste the IDs from the console. For more details, see steps 1-4 in [Setting Up AWS Chatbot with Microsoft Teams](https://docs.aws.amazon.com/chatbot/latest/adminguide/teams-setup.html#teams-client-setup) in the *AWS Chatbot Administrator Guide* .", + "properties": { + "ConfigurationName": "The name of the configuration.", + "GuardrailPolicies": "The list of IAM policy ARNs that are applied as channel guardrails. The AWS managed 'AdministratorAccess' policy is applied as a default if this is not set.", + "IamRoleArn": "The ARN of the IAM role that defines the permissions for AWS Chatbot .\n\nThis is a user-defined role that AWS Chatbot will assume. This is not the service-linked role. For more information, see [IAM Policies for AWS Chatbot](https://docs.aws.amazon.com/chatbot/latest/adminguide/chatbot-iam-policies.html) .", + "LoggingLevel": "Specifies the logging level for this configuration. This property affects the log entries pushed to Amazon CloudWatch Logs.\n\nLogging levels include `ERROR` , `INFO` , or `NONE` .", + "SnsTopicArns": "The ARNs of the SNS topics that deliver notifications to AWS Chatbot .", + "TeamId": "The ID of the Microsoft Team authorized with AWS Chatbot .\n\nTo get the team ID, you must perform the initial authorization flow with Microsoft Teams in the AWS Chatbot console. Then you can copy and paste the team ID from the console. For more details, see steps 1-4 in [Get started with Microsoft Teams](https://docs.aws.amazon.com/chatbot/latest/adminguide/teams-setup.html#teams-client-setup) in the *AWS Chatbot Administrator Guide* .", + "TeamsChannelId": "The ID of the Microsoft Teams channel.\n\nTo get the channel ID, open Microsoft Teams, right click on the channel name in the left pane, then choose Copy. An example of the channel ID syntax is: `19%3ab6ef35dc342d56ba5654e6fc6d25a071%40thread.tacv2` .", + "TeamsTenantId": "The ID of the Microsoft Teams tenant.\n\nTo get the tenant ID, you must perform the initial authorization flow with Microsoft Teams in the AWS Chatbot console. Then you can copy and paste the tenant ID from the console. For more details, see steps 1-4 in [Get started with Microsoft Teams](https://docs.aws.amazon.com/chatbot/latest/adminguide/teams-setup.html#teams-client-setup) in the *AWS Chatbot Administrator Guide* .", + "UserRoleRequired": "Enables use of a user role requirement in your chat configuration." + } + }, "AWS::Chatbot::SlackChannelConfiguration": { "attributes": { "Arn": "", @@ -8762,7 +8799,7 @@ "description": "A dimension is a name/value pair that is part of the identity of a metric. Because dimensions are part of the unique identifier for a metric, whenever you add a unique name/value pair to one of your metrics, you are creating a new variation of that metric. For example, many Amazon EC2 metrics publish `InstanceId` as a dimension name, and the actual instance ID as the value for that dimension.\n\nYou can assign up to 30 dimensions to a metric.", "properties": { "Name": "The name of the dimension.", - "Value": "The value of the dimension. Dimension values must contain only ASCII characters and must include at least one non-whitespace character." + "Value": "The value of the dimension. Dimension values must contain only ASCII characters and must include at least one non-whitespace character. ASCII control characters are not supported as part of dimension values." } }, "AWS::CloudWatch::AnomalyDetector.Metric": { @@ -10440,6 +10477,13 @@ "EntityTypes": "Up to 25 entity types that the model is trained to recognize." } }, + "AWS::Comprehend::Flywheel.EntityTypesListItem": { + "attributes": {}, + "description": "An entity type within a labeled training dataset that Amazon Comprehend uses to train a custom entity recognizer.", + "properties": { + "Type": "An entity type within a labeled training dataset that Amazon Comprehend uses to train a custom entity recognizer.\n\nEntity types must not contain the following invalid characters: \\n (line break), \\\\n (escaped line break, \\r (carriage return), \\\\r (escaped carriage return), \\t (tab), \\\\t (escaped tab), space, and , (comma)." + } + }, "AWS::Comprehend::Flywheel.TaskConfig": { "attributes": {}, "description": "Configuration about the custom classifier associated with the flywheel.", @@ -14610,14 +14654,14 @@ }, "AWS::EC2::IPAMResourceDiscoveryAssociation": { "attributes": { - "IpamArn": "", - "IpamRegion": "", + "IpamArn": "The IPAM ARN.", + "IpamRegion": "The IPAM home Region.", "IpamResourceDiscoveryAssociationArn": "The resource discovery association ARN.", "IpamResourceDiscoveryAssociationId": "The resource discovery association ID.", "IsDefault": "Defines if the resource discovery is the default. When you create an IPAM, a default resource discovery is created for your IPAM and it's associated with your IPAM.", "OwnerId": "The owner ID.", "Ref": "`Ref` returns the resource discovery ID. For example: `ipam-res-disco-111122223333` .", - "ResourceDiscoveryStatus": "", + "ResourceDiscoveryStatus": "The resource discovery status.\n\n- `active` - Connection or permissions required to read the results of the resource discovery are intact.\n- `not-found` - Connection or permissions required to read the results of the resource discovery are broken. This may happen if the owner of the resource discovery stopped sharing it or deleted the resource discovery. Verify the resource discovery still exists and the AWS RAM resource share is still intact.", "State": "The lifecycle state of the association when you associate or disassociate a resource discovery.\n\n- `associate-in-progress` - Resource discovery is being associated.\n- `associate-complete` - Resource discovery association is complete.\n- `associate-failed` - Resource discovery association has failed.\n- `disassociate-in-progress` - Resource discovery is being disassociated.\n- `disassociate-complete` - Resource discovery disassociation is complete.\n- `disassociate-failed` - Resource discovery disassociation has failed.\n- `isolate-in-progress` - AWS account that created the resource discovery association has been removed and the resource discovery associatation is being isolated.\n- `isolate-complete` - Resource discovery isolation is complete..\n- `restore-in-progress` - Resource discovery is being restored." }, "description": "An IPAM resource discovery association. An associated resource discovery is a resource discovery that has been associated with an IPAM. IPAM aggregates the resource CIDRs discovered by the associated resource discovery.", @@ -15491,9 +15535,9 @@ }, "AWS::EC2::NetworkInsightsAnalysis.AdditionalDetail": { "attributes": {}, - "description": "Describes an additional detail for a path analysis.", + "description": "Describes an additional detail for a path analysis. For more information, see [Reachability Analyzer additional detail codes](https://docs.aws.amazon.com/vpc/latest/reachability/additional-detail-codes.html) .", "properties": { - "AdditionalDetailType": "The information type.", + "AdditionalDetailType": "The additional detail code.", "Component": "The path component.", "LoadBalancers": "", "ServiceName": "" @@ -16675,7 +16719,6 @@ }, "AWS::EC2::VPCDHCPOptionsAssociation": { "attributes": { - "Id": "The ID of the DHCP options set.", "Ref": "`Ref` returns the ID of the DHCP options association." }, "description": "Associates a set of DHCP options with a VPC, or associates no DHCP options with the VPC.\n\nAfter you associate the options with the VPC, any existing instances and all new instances that you launch in that VPC use the options. You don't need to restart or relaunch the instances. They automatically pick up the changes within a few hours, depending on how frequently the instance renews its DHCP lease. You can explicitly renew the lease using the operating system on the instance.", @@ -16688,6 +16731,7 @@ "attributes": { "CreationTimestamp": "The date and time the VPC endpoint was created. For example: `Fri Sep 28 23:34:36 UTC 2018.`", "DnsEntries": "(Interface endpoints) The DNS entries for the endpoint. Each entry is a combination of the hosted zone ID and the DNS name. The entries are ordered as follows: regional public DNS, zonal public DNS, private DNS, and wildcard DNS. This order is not enforced for AWS Marketplace services.\n\nThe following is an example. In the first entry, the hosted zone ID is Z1HUB23UULQXV and the DNS name is vpce-01abc23456de78f9g-12abccd3.ec2.us-east-1.vpce.amazonaws.com.\n\n[\"Z1HUB23UULQXV:vpce-01abc23456de78f9g-12abccd3.ec2.us-east-1.vpce.amazonaws.com\", \"Z1HUB23UULQXV:vpce-01abc23456de78f9g-12abccd3-us-east-1a.ec2.us-east-1.vpce.amazonaws.com\", \"Z1C12344VYDITB0:ec2.us-east-1.amazonaws.com\"]\n\nIf you update the `PrivateDnsEnabled` or `SubnetIds` properties, the DNS entries in the list will change.", + "Id": "The ID of the VPC endpoint.", "NetworkInterfaceIds": "(Interface endpoints) The network interface IDs. If you update the `PrivateDnsEnabled` or `SubnetIds` properties, the items in this list might change.", "Ref": "`Ref` returns the ID of the VPC endpoint." }, @@ -16927,7 +16971,7 @@ }, "AWS::ECR::ReplicationConfiguration.RepositoryFilter": { "attributes": {}, - "description": "The filter settings used with image replication. Specifying a repository filter to a replication rule provides a method for controlling which repositories in a private registry are replicated. If no repository filter is specified, all images in the repository are replicated.", + "description": "The filter settings used with image replication. Specifying a repository filter to a replication rule provides a method for controlling which repositories in a private registry are replicated. If no filters are added, the contents of all repositories are replicated.", "properties": { "Filter": "The repository filter details. When the `PREFIX_MATCH` filter type is specified, this value is required and should be the repository name prefix to configure replication for.", "FilterType": "The repository filter type. The only supported value is `PREFIX_MATCH` , which is a repository name prefix specified with the `filter` parameter." @@ -35998,10 +36042,7 @@ "AWS::MediaPackage::PackagingConfiguration.EncryptionContractConfiguration": { "attributes": {}, "description": "Use `encryptionContractConfiguration` to configure one or more content encryption keys for your endpoints that use SPEKE Version 2.0. The encryption contract defines the content keys used to encrypt the audio and video tracks in your stream. To configure the encryption contract, specify which audio and video encryption presets to use. For more information about these presets, see [SPEKE Version 2.0 Presets](https://docs.aws.amazon.com/mediapackage/latest/ug/drm-content-speke-v2-presets.html) .\n\nNote the following considerations when using `encryptionContractConfiguration` :\n\n- You can use `encryptionContractConfiguration` for DASH endpoints that use SPEKE Version 2.0. SPEKE Version 2.0 relies on the CPIX Version 2.3 specification.\n- You cannot combine an `UNENCRYPTED` preset with `UNENCRYPTED` or `SHARED` presets across `presetSpeke20Audio` and `presetSpeke20Video` .\n- When you use a `SHARED` preset, you must use it for both `presetSpeke20Audio` and `presetSpeke20Video` .", - "properties": { - "PresetSpeke20Audio": "A collection of audio encryption presets.\n\nValue description:\n\n- `PRESET-AUDIO-1` - Use one content key to encrypt all of the audio tracks in your stream.\n- `PRESET-AUDIO-2` - Use one content key to encrypt all of the stereo audio tracks and one content key to encrypt all of the multichannel audio tracks.\n- `PRESET-AUDIO-3` - Use one content key to encrypt all of the stereo audio tracks, one content key to encrypt all of the multichannel audio tracks with 3 to 6 channels, and one content key to encrypt all of the multichannel audio tracks with more than 6 channels.\n- `SHARED` - Use the same content key for all of the audio and video tracks in your stream.\n- `UNENCRYPTED` - Don't encrypt any of the audio tracks in your stream.", - "PresetSpeke20Video": "A collection of video encryption presets.\n\nValue description:\n\n- `PRESET-VIDEO-1` - Use one content key to encrypt all of the video tracks in your stream.\n- `PRESET-VIDEO-2` - Use one content key to encrypt all of the SD video tracks and one content key for all HD and higher resolutions video tracks.\n- `PRESET-VIDEO-3` - Use one content key to encrypt all of the SD video tracks, one content key for HD video tracks and one content key for all UHD video tracks.\n- `PRESET-VIDEO-4` - Use one content key to encrypt all of the SD video tracks, one content key for HD video tracks, one content key for all UHD1 video tracks and one content key for all UHD2 video tracks.\n- `PRESET-VIDEO-5` - Use one content key to encrypt all of the SD video tracks, one content key for HD1 video tracks, one content key for HD2 video tracks, one content key for all UHD1 video tracks and one content key for all UHD2 video tracks.\n- `PRESET-VIDEO-6` - Use one content key to encrypt all of the SD video tracks, one content key for HD1 video tracks, one content key for HD2 video tracks and one content key for all UHD video tracks.\n- `PRESET-VIDEO-7` - Use one content key to encrypt all of the SD+HD1 video tracks, one content key for HD2 video tracks and one content key for all UHD video tracks.\n- `PRESET-VIDEO-8` - Use one content key to encrypt all of the SD+HD1 video tracks, one content key for HD2 video tracks, one content key for all UHD1 video tracks and one content key for all UHD2 video tracks.\n- `SHARED` - Use the same content key for all of the video and audio tracks in your stream.\n- `UNENCRYPTED` - Don't encrypt any of the video tracks in your stream." - } + "properties": {} }, "AWS::MediaPackage::PackagingConfiguration.HlsEncryption": { "attributes": {}, @@ -41562,6 +41603,7 @@ }, "AWS::RUM::AppMonitor": { "attributes": { + "Id": "The ID of the app monitor, such as `123456ab-1234-4ca9-9d2f-a1b2c3456789` .", "Ref": "`Ref` returns the name of the app monitor." }, "description": "Creates a CloudWatch RUM app monitor, which you can use to collect telemetry data from your application and send it to CloudWatch RUM. The data includes performance and reliability information such as page load time, client-side errors, and user behavior.\n\nAfter you create an app monitor, sign in to the CloudWatch RUM console to get the JavaScript code snippet to add to your web application. For more information, see [How do I find a code snippet that I've already generated?](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch-RUM-find-code-snippet.html)", @@ -43148,15 +43190,19 @@ "HostVPCId": "The ID of the VPC that you want to create the resolver endpoint in.", "IpAddressCount": "The number of IP addresses that the resolver endpoint can use for DNS queries.", "Name": "The name that you assigned to the resolver endpoint when you created the endpoint.", + "OutpostArn": "", + "PreferredInstanceType": "", "Ref": "`Ref` returns the `ResolverEndpoint` object.", "ResolverEndpointId": "The ID of the resolver endpoint.", - "ResolverEndpointType": "" + "ResolverEndpointType": "For the endpoint type you can choose either IPv4, IPv6. or dual-stack. A dual-stack endpoint means that it will resolve via both IPv4 and IPv6. If you choose either IPv4 or IPv6, this endpoint type is applied to all IP addresses." }, - "description": "Creates a Resolver endpoint. There are two types of Resolver endpoints, inbound and outbound:\n\n- An *inbound Resolver endpoint* forwards DNS queries to the DNS service for a VPC from your network.\n- An *outbound Resolver endpoint* forwards DNS queries from the DNS service for a VPC to your network.", + "description": "Creates a Resolver endpoint. There are two types of Resolver endpoints, inbound and outbound:\n\n- An *inbound Resolver endpoint* forwards DNS queries to the DNS service for a VPC from your network.\n- An *outbound Resolver endpoint* forwards DNS queries from the DNS service for a VPC to your network.\n\n> - You cannot update `ResolverEndpointType` and `IpAddresses` in the same request.\n> - When you update a dual-stack IP address, you must update both IP addresses. You can\u2019t update only an IPv4 or IPv6 and keep an existing IP address.", "properties": { "Direction": "Indicates whether the Resolver endpoint allows inbound or outbound DNS queries:\n\n- `INBOUND` : allows DNS queries to your VPC from your network\n- `OUTBOUND` : allows DNS queries from your VPC to your network", "IpAddresses": "The subnets and IP addresses in your VPC that DNS queries originate from (for outbound endpoints) or that you forward DNS queries to (for inbound endpoints). The subnet ID uniquely identifies a VPC.", "Name": "A friendly name that lets you easily find a configuration in the Resolver dashboard in the Route 53 console.", + "OutpostArn": "", + "PreferredInstanceType": "", "ResolverEndpointType": "The Resolver endpoint IP address type.", "SecurityGroupIds": "The ID of one or more security groups that control access to this VPC. The security group must include one or more inbound rules (for inbound endpoints) or outbound rules (for outbound endpoints). Inbound and outbound rules must allow TCP and UDP access. For inbound access, open port 53. For outbound access, open the port that you're using for DNS queries on your network.", "Tags": "Route 53 Resolver doesn't support updating tags through CloudFormation." @@ -43220,7 +43266,7 @@ "Name": "The name for the Resolver rule, which you specified when you created the Resolver rule.", "ResolverEndpointId": "The ID of the endpoint that the rule is associated with.", "RuleType": "When you want to forward DNS queries for specified domain name to resolvers on your network, specify `FORWARD` .\n\nWhen you have a forwarding rule to forward DNS queries for a domain to your network and you want Resolver to process queries for a subdomain of that domain, specify `SYSTEM` .\n\nFor example, to forward DNS queries for example.com to resolvers on your network, you create a rule and specify `FORWARD` for `RuleType` . To then have Resolver process queries for apex.example.com, you create a rule and specify `SYSTEM` for `RuleType` .\n\nCurrently, only Resolver can create rules that have a value of `RECURSIVE` for `RuleType` .", - "Tags": "Route 53 Resolver doesn't support updating tags through CloudFormation.", + "Tags": "Tags help organize and categorize your Resolver rules. Each tag consists of a key and an optional value, both of which you define.", "TargetIps": "An array that contains the IP addresses and ports that an outbound endpoint forwards DNS queries to. Typically, these are the IP addresses of DNS resolvers on your network." } }, @@ -43532,7 +43578,7 @@ }, "AWS::S3::Bucket.NotificationFilter": { "attributes": {}, - "description": "Specifies object key name filtering rules. For information about key name filtering, see [Configuring Event Notifications](https://docs.aws.amazon.com/AmazonS3/latest/dev/NotificationHowTo.html) in the *Amazon S3 User Guide* .", + "description": "Specifies object key name filtering rules. For information about key name filtering, see [Configuring event notifications using object key name filtering](https://docs.aws.amazon.com/AmazonS3/latest/userguide/notification-how-to-filtering.html) in the *Amazon S3 User Guide* .", "properties": { "S3Key": "A container for object key name prefix and suffix filtering rules." } @@ -44026,9 +44072,6 @@ }, "AWS::S3ObjectLambda::AccessPoint": { "attributes": { - "Alias": "", - "Alias.Status": "", - "Alias.Value": "", "Arn": "Specifies the ARN for the Object Lambda Access Point.", "CreationDate": "The date and time when the specified Object Lambda Access Point was created.", "PolicyStatus": "", @@ -44046,14 +44089,6 @@ "ObjectLambdaConfiguration": "A configuration used when creating an Object Lambda Access Point." } }, - "AWS::S3ObjectLambda::AccessPoint.Alias": { - "attributes": {}, - "description": "", - "properties": { - "Status": "", - "Value": "" - } - }, "AWS::S3ObjectLambda::AccessPoint.AwsLambda": { "attributes": {}, "description": "", @@ -46120,6 +46155,110 @@ "ImageName": "The name of the parent image.\n\n*Length Constraints* : Minimum length of 1. Maximum length of 63.\n\n*Pattern* : `^[a-zA-Z0-9]([-.]?[a-zA-Z0-9]){0,62}$`" } }, + "AWS::SageMaker::InferenceExperiment": { + "attributes": { + "Arn": "", + "CreationTime": "", + "EndpointMetadata": "", + "EndpointMetadata.EndpointConfigName": "", + "EndpointMetadata.EndpointName": "", + "EndpointMetadata.EndpointStatus": "", + "LastModifiedTime": "", + "Ref": "", + "Status": "" + }, + "description": "Creates an inference experiment using the configurations specified in the request.\n\nUse this API to setup and schedule an experiment to compare model variants on a Amazon SageMaker inference endpoint. For more information about inference experiments, see [Shadow tests](https://docs.aws.amazon.com/sagemaker/latest/dg/shadow-tests.html) .\n\nAmazon SageMaker begins your experiment at the scheduled time and routes traffic to your endpoint's model variants based on your specified configuration.\n\nWhile the experiment is in progress or after it has concluded, you can view metrics that compare your model variants. For more information, see [View, monitor, and edit shadow tests](https://docs.aws.amazon.com/sagemaker/latest/dg/shadow-tests-view-monitor-edit.html) .", + "properties": { + "DataStorageConfig": "", + "Description": "The description of the inference experiment.", + "DesiredState": "", + "EndpointName": "", + "KmsKey": "The AWS Key Management Service key that Amazon SageMaker uses to encrypt captured data at rest using Amazon S3 server-side encryption.", + "ModelVariants": "", + "Name": "The name of the inference experiment.", + "RoleArn": "The ARN of the IAM role that Amazon SageMaker can assume to access model artifacts and container images, and manage Amazon SageMaker Inference endpoints for model deployment.", + "Schedule": "The duration for which the inference experiment ran or will run.\n\nThe maximum duration that you can set for an inference experiment is 30 days.", + "ShadowModeConfig": "", + "StatusReason": "The error message for the inference experiment status result.", + "Tags": "", + "Type": "The type of the inference experiment." + } + }, + "AWS::SageMaker::InferenceExperiment.CaptureContentTypeHeader": { + "attributes": {}, + "description": "Configuration specifying how to treat different headers. If no headers are specified SageMaker will by default base64 encode when capturing the data.", + "properties": { + "CsvContentTypes": "The list of all content type headers that SageMaker will treat as CSV and capture accordingly.", + "JsonContentTypes": "The list of all content type headers that SageMaker will treat as JSON and capture accordingly." + } + }, + "AWS::SageMaker::InferenceExperiment.DataStorageConfig": { + "attributes": {}, + "description": "", + "properties": { + "ContentType": "", + "Destination": "", + "KmsKey": "" + } + }, + "AWS::SageMaker::InferenceExperiment.EndpointMetadata": { + "attributes": {}, + "description": "The metadata of the endpoint.", + "properties": { + "EndpointConfigName": "The name of the endpoint configuration.", + "EndpointName": "The name of the endpoint.", + "EndpointStatus": "The status of the endpoint. For possible values of the status of an endpoint, see `EndpointSummary$EndpointStatus` ." + } + }, + "AWS::SageMaker::InferenceExperiment.InferenceExperimentSchedule": { + "attributes": {}, + "description": "The start and end times of an inference experiment.\n\nThe maximum duration that you can set for an inference experiment is 30 days.", + "properties": { + "EndTime": "The timestamp at which the inference experiment ended or will end.", + "StartTime": "The timestamp at which the inference experiment started or will start." + } + }, + "AWS::SageMaker::InferenceExperiment.ModelInfrastructureConfig": { + "attributes": {}, + "description": "The configuration for the infrastructure that the model will be deployed to.", + "properties": { + "InfrastructureType": "The inference option to which to deploy your model. Possible values are the following:\n\n- `RealTime` : Deploy to real-time inference.", + "RealTimeInferenceConfig": "The infrastructure configuration for deploying the model to real-time inference." + } + }, + "AWS::SageMaker::InferenceExperiment.ModelVariantConfig": { + "attributes": {}, + "description": "Contains information about the deployment options of a model.", + "properties": { + "InfrastructureConfig": "The configuration for the infrastructure that the model will be deployed to.", + "ModelName": "The name of the Amazon SageMaker Model entity.", + "VariantName": "The name of the variant." + } + }, + "AWS::SageMaker::InferenceExperiment.RealTimeInferenceConfig": { + "attributes": {}, + "description": "The infrastructure configuration for deploying the model to a real-time inference endpoint.", + "properties": { + "InstanceCount": "The number of instances of the type specified by `InstanceType` .", + "InstanceType": "The instance type the model is deployed to." + } + }, + "AWS::SageMaker::InferenceExperiment.ShadowModeConfig": { + "attributes": {}, + "description": "The configuration of `ShadowMode` inference experiment type, which specifies a production variant to take all the inference requests, and a shadow variant to which Amazon SageMaker replicates a percentage of the inference requests. For the shadow variant it also specifies the percentage of requests that Amazon SageMaker replicates.", + "properties": { + "ShadowModelVariants": "List of shadow variant configurations.", + "SourceModelVariantName": "The name of the production variant, which takes all the inference requests." + } + }, + "AWS::SageMaker::InferenceExperiment.ShadowModelVariantConfig": { + "attributes": {}, + "description": "The name and sampling percentage of a shadow variant.", + "properties": { + "SamplingPercentage": "The percentage of inference requests that Amazon SageMaker replicates from the production variant to the shadow variant.", + "ShadowModelVariantName": "The name of the shadow variant." + } + }, "AWS::SageMaker::Model": { "attributes": { "ModelName": "The name of the model, such as `MyModel` .", @@ -47780,7 +47919,7 @@ "Arn": "The Amazon Resource Name (ARN) for the Amazon EventBridge Scheduler schedule.", "Ref": "When you pass the logical ID of this resource to the intrinsic `Ref` function, `Ref` returns the `Name` attribute of theschedule." }, - "description": "Creates the specified schedule.", + "description": "A *schedule* is the main resource you create, configure, and manage using Amazon EventBridge Scheduler.\n\nEvery schedule has a *schedule expression* that determines when, and with what frequency, the schedule runs. EventBridge Scheduler supports three types of schedules: rate, cron, and one-time schedules. For more information about different schedule types, see [Schedule types](https://docs.aws.amazon.com/scheduler/latest/UserGuide/schedule-types.html) in the *EventBridge Scheduler User Guide* .\n\nWhen you create a schedule, you configure a target for the schedule to invoke. A target is an API operation that EventBridge Scheduler calls on your behalf every time your schedule runs. EventBridge Scheduler supports two types of targets: *templated* targets invoke common API operations across a core groups of services, and customizeable *universal* targets that you can use to call more than 6,000 operations across over 270 services. For more information about configuring targets, see [Managing targets](https://docs.aws.amazon.com/scheduler/latest/UserGuide/managing-targets.html) in the *EventBridge Scheduler User Guide* .\n\nFor more information about managing schedules, changing the schedule state, setting up flexible time windows, and configuring a dead-letter queue for a schedule, see [Managing a schedule](https://docs.aws.amazon.com/scheduler/latest/UserGuide/managing-schedule.html) in the *EventBridge Scheduler User Guide* .", "properties": { "Description": "The description you specify for the schedule.", "EndDate": "The date, in UTC, before which the schedule can invoke its target. Depending on the schedule's recurrence expression, invocations might stop on, or before, the `EndDate` you specify.\nEventBridge Scheduler ignores `EndDate` for one-time schedules.", @@ -47791,7 +47930,7 @@ "ScheduleExpression": "The expression that defines when the schedule runs. The following formats are supported.\n\n- `at` expression - `at(yyyy-mm-ddThh:mm:ss)`\n- `rate` expression - `rate(unit value)`\n- `cron` expression - `cron(fields)`\n\nYou can use `at` expressions to create one-time schedules that invoke a target once, at the time and in the time zone, that you specify. You can use `rate` and `cron` expressions to create recurring schedules. Rate-based schedules are useful when you want to invoke a target at regular intervals, such as every 15 minutes or every five days. Cron-based schedules are useful when you want to invoke a target periodically at a specific time, such as at 8:00 am (UTC+0) every 1st day of the month.\n\nA `cron` expression consists of six fields separated by white spaces: `(minutes hours day_of_month month day_of_week year)` .\n\nA `rate` expression consists of a *value* as a positive integer, and a *unit* with the following options: `minute` | `minutes` | `hour` | `hours` | `day` | `days`\n\nFor more information and examples, see [Schedule types on EventBridge Scheduler](https://docs.aws.amazon.com/scheduler/latest/UserGuide/schedule-types.html) in the *EventBridge Scheduler User Guide* .", "ScheduleExpressionTimezone": "The timezone in which the scheduling expression is evaluated.", "StartDate": "The date, in UTC, after which the schedule can begin invoking its target. Depending on the schedule's recurrence expression, invocations might occur on, or after, the `StartDate` you specify.\nEventBridge Scheduler ignores `StartDate` for one-time schedules.", - "State": "Specifies whether the schedule is enabled or disabled.", + "State": "Specifies whether the schedule is enabled or disabled.\n\n*Allowed Values* : `ENABLED` | `DISABLED`", "Target": "The schedule's target details." } }, @@ -47852,8 +47991,8 @@ "attributes": {}, "description": "Allows you to configure a time window during which EventBridge Scheduler invokes the schedule.", "properties": { - "MaximumWindowInMinutes": "The maximum time window during which a schedule can be invoked.", - "Mode": "Determines whether the schedule is invoked within a flexible time window." + "MaximumWindowInMinutes": "The maximum time window during which a schedule can be invoked.\n\n*Minimum* : `1`\n\n*Maximum* : `1440`", + "Mode": "Determines whether the schedule is invoked within a flexible time window.\n\n*Allowed Values* : `OFF` | `FLEXIBLE`" } }, "AWS::Scheduler::Schedule.KinesisParameters": { @@ -47938,9 +48077,9 @@ "CreationDate": "The date and time at which the schedule group was created.", "LastModificationDate": "The time at which the schedule group was last modified.", "Ref": "When you pass the logical ID of this resource to the intrinsic `Ref` function, `Ref` returns the `Name` attribute of the schedule group.", - "State": "Specifies the state of the schedule group.\n\nValid values are `ACTIVE` and `DELETING` ." + "State": "Specifies the state of the schedule group.\n\n*Allowed Values* : `ACTIVE` | `DELETING`" }, - "description": "Creates the specified schedule group.", + "description": "A *schedule group* is an Amazon EventBridge Scheduler resource you use to organize your schedules.\n\nYour AWS account comes with a `default` scheduler group. You associate a new schedule with the `default` group or with schedule groups that you create and manage. You can create up to [500 schedule groups](https://docs.aws.amazon.com/scheduler/latest/UserGuide/scheduler-quotas.html) in your AWS account. With EventBridge Scheduler, you apply [tags](https://docs.aws.amazon.com/general/latest/gr/aws_tagging.html) to schedule groups, not to individual schedules to organize your resources.\n\nFor more information about managing schedule groups, see [Managing a schedule group](https://docs.aws.amazon.com/scheduler/latest/UserGuide/managing-schedule-group.html) in the *EventBridge Scheduler User Guide* .", "properties": { "Name": "The name of the schedule group.", "Tags": "An array of key-value pairs to apply to this resource.\n\nFor more information, see [Tag](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-resource-tags.html) ." @@ -49902,7 +50041,7 @@ "AvailableLabels": "The labels that one or more rules in this rule group add to matching web requests. These labels are defined in the `RuleLabels` for a `Rule` .", "Capacity": "The web ACL capacity units (WCUs) required for this rule group.\n\nWhen you create your own rule group, you define this, and you cannot change it after creation. When you add or modify the rules in a rule group, AWS WAF enforces this limit.\n\nAWS WAF uses WCUs to calculate and control the operating resources that are used to run your rules, rule groups, and web ACLs. AWS WAF calculates capacity differently for each rule type, to reflect the relative cost of each rule. Simple rules that cost little to run use fewer WCUs than more complex rules that use more processing power. Rule group capacity is fixed at creation, which helps users plan their web ACL WCU usage when they use a rule group. The WCU limit for web ACLs is 1,500.", "ConsumedLabels": "The labels that one or more rules in this rule group match against in label match statements. These labels are defined in a `LabelMatchStatement` specification, in the `Statement` definition of a rule.", - "CustomResponseBodies": "A map of custom response keys and content bodies. When you create a rule with a block action, you can send a custom response to the web request. You define these for the rule group, and then use them in the rules that you define in the rule group.\n\nFor information about customizing web requests and responses, see [Customizing web requests and responses in AWS WAF](https://docs.aws.amazon.com/waf/latest/developerguide/waf-custom-request-response.html) in the [AWS WAF Developer Guide](https://docs.aws.amazon.com/waf/latest/developerguide/waf-chapter.html) .\n\nFor information about the limits on count and size for custom request and response settings, see [AWS WAF quotas](https://docs.aws.amazon.com/waf/latest/developerguide/limits.html) in the [AWS WAF Developer Guide](https://docs.aws.amazon.com/waf/latest/developerguide/waf-chapter.html) .", + "CustomResponseBodies": "A map of custom response keys and content bodies. When you create a rule with a block action, you can send a custom response to the web request. You define these for the rule group, and then use them in the rules that you define in the rule group.\n\nFor information about customizing web requests and responses, see [Customizing web requests and responses in AWS WAF](https://docs.aws.amazon.com/waf/latest/developerguide/waf-custom-request-response.html) in the *AWS WAF Developer Guide* .\n\nFor information about the limits on count and size for custom request and response settings, see [AWS WAF quotas](https://docs.aws.amazon.com/waf/latest/developerguide/limits.html) in the *AWS WAF Developer Guide* .", "Description": "A description of the rule group that helps with identification.", "Name": "The name of the rule group. You cannot change the name of a rule group after you create it.", "Rules": "The rule statements used to identify the web requests that you want to allow, block, or count. Each rule includes one top-level statement that AWS WAF uses to identify matching web requests, and parameters that govern how AWS WAF handles them.", @@ -49915,7 +50054,7 @@ "attributes": {}, "description": "Specifies that AWS WAF should allow the request and optionally defines additional custom handling for the request.\n\nThis is used in the context of other settings, for example to specify values for `RuleAction` and web ACL `DefaultAction` .", "properties": { - "CustomRequestHandling": "Defines custom handling for the web request.\n\nFor information about customizing web requests and responses, see [Customizing web requests and responses in AWS WAF](https://docs.aws.amazon.com/waf/latest/developerguide/waf-custom-request-response.html) in the [AWS WAF Developer Guide](https://docs.aws.amazon.com/waf/latest/developerguide/waf-chapter.html) ." + "CustomRequestHandling": "Defines custom handling for the web request.\n\nFor information about customizing web requests and responses, see [Customizing web requests and responses in AWS WAF](https://docs.aws.amazon.com/waf/latest/developerguide/waf-custom-request-response.html) in the *AWS WAF Developer Guide* ." } }, "AWS::WAFv2::RuleGroup.AndStatement": { @@ -49929,7 +50068,7 @@ "attributes": {}, "description": "Specifies that AWS WAF should block the request and optionally defines additional custom handling for the response to the web request.\n\nThis is used in the context of other settings, for example to specify values for `RuleAction` and web ACL `DefaultAction` .", "properties": { - "CustomResponse": "Defines a custom response for the web request.\n\nFor information about customizing web requests and responses, see [Customizing web requests and responses in AWS WAF](https://docs.aws.amazon.com/waf/latest/developerguide/waf-custom-request-response.html) in the [AWS WAF Developer Guide](https://docs.aws.amazon.com/waf/latest/developerguide/waf-chapter.html) ." + "CustomResponse": "Defines a custom response for the web request.\n\nFor information about customizing web requests and responses, see [Customizing web requests and responses in AWS WAF](https://docs.aws.amazon.com/waf/latest/developerguide/waf-custom-request-response.html) in the *AWS WAF Developer Guide* ." } }, "AWS::WAFv2::RuleGroup.Body": { @@ -49954,7 +50093,7 @@ "attributes": {}, "description": "Specifies that AWS WAF should run a `CAPTCHA` check against the request:\n\n- If the request includes a valid, unexpired `CAPTCHA` token, AWS WAF applies any custom request handling and labels that you've configured and then allows the web request inspection to proceed to the next rule, similar to a `CountAction` .\n- If the request doesn't include a valid, unexpired token, AWS WAF discontinues the web ACL evaluation of the request and blocks it from going to its intended destination.\n\nAWS WAF generates a response that it sends back to the client, which includes the following:\n\n- The header `x-amzn-waf-action` with a value of `captcha` .\n- The HTTP status code `405 Method Not Allowed` .\n- If the request contains an `Accept` header with a value of `text/html` , the response includes a `CAPTCHA` JavaScript page interstitial.\n\nYou can configure the expiration time in the `CaptchaConfig` `ImmunityTimeProperty` setting at the rule and web ACL level. The rule setting overrides the web ACL setting.\n\nThis action option is available for rules. It isn't available for web ACL default actions.", "properties": { - "CustomRequestHandling": "Defines custom handling for the web request, used when the `CAPTCHA` inspection determines that the request's token is valid and unexpired.\n\nFor information about customizing web requests and responses, see [Customizing web requests and responses in AWS WAF](https://docs.aws.amazon.com/waf/latest/developerguide/waf-custom-request-response.html) in the [AWS WAF Developer Guide](https://docs.aws.amazon.com/waf/latest/developerguide/waf-chapter.html) ." + "CustomRequestHandling": "Defines custom handling for the web request, used when the `CAPTCHA` inspection determines that the request's token is valid and unexpired.\n\nFor information about customizing web requests and responses, see [Customizing web requests and responses in AWS WAF](https://docs.aws.amazon.com/waf/latest/developerguide/waf-custom-request-response.html) in the *AWS WAF Developer Guide* ." } }, "AWS::WAFv2::RuleGroup.CaptchaConfig": { @@ -49968,7 +50107,7 @@ "attributes": {}, "description": "Specifies that AWS WAF should run a `Challenge` check against the request to verify that the request is coming from a legitimate client session:\n\n- If the request includes a valid, unexpired challenge token, AWS WAF applies any custom request handling and labels that you've configured and then allows the web request inspection to proceed to the next rule, similar to a `CountAction` .\n- If the request doesn't include a valid, unexpired challenge token, AWS WAF discontinues the web ACL evaluation of the request and blocks it from going to its intended destination.\n\nAWS WAF then generates a challenge response that it sends back to the client, which includes the following:\n\n- The header `x-amzn-waf-action` with a value of `challenge` .\n- The HTTP status code `202 Request Accepted` .\n- If the request contains an `Accept` header with a value of `text/html` , the response includes a JavaScript page interstitial with a challenge script.\n\nChallenges run silent browser interrogations in the background, and don't generally affect the end user experience.\n\nA challenge enforces token acquisition using an interstitial JavaScript challenge that inspects the client session for legitimate behavior. The challenge blocks bots or at least increases the cost of operating sophisticated bots.\n\nAfter the client session successfully responds to the challenge, it receives a new token from AWS WAF , which the challenge script uses to resubmit the original request.\n\nYou can configure the expiration time in the `ChallengeConfig` `ImmunityTimeProperty` setting at the rule and web ACL level. The rule setting overrides the web ACL setting.\n\nThis action option is available for rules. It isn't available for web ACL default actions.", "properties": { - "CustomRequestHandling": "Defines custom handling for the web request, used when the challenge inspection determines that the request's token is valid and unexpired.\n\nFor information about customizing web requests and responses, see [Customizing web requests and responses in AWS WAF](https://docs.aws.amazon.com/waf/latest/developerguide/waf-custom-request-response.html) in the [AWS WAF Developer Guide](https://docs.aws.amazon.com/waf/latest/developerguide/waf-chapter.html) ." + "CustomRequestHandling": "Defines custom handling for the web request, used when the challenge inspection determines that the request's token is valid and unexpired.\n\nFor information about customizing web requests and responses, see [Customizing web requests and responses in AWS WAF](https://docs.aws.amazon.com/waf/latest/developerguide/waf-custom-request-response.html) in the *AWS WAF Developer Guide* ." } }, "AWS::WAFv2::RuleGroup.ChallengeConfig": { @@ -50000,7 +50139,7 @@ "attributes": {}, "description": "Specifies that AWS WAF should count the request. Optionally defines additional custom handling for the request.\n\nThis is used in the context of other settings, for example to specify values for `RuleAction` and web ACL `DefaultAction` .", "properties": { - "CustomRequestHandling": "Defines custom handling for the web request.\n\nFor information about customizing web requests and responses, see [Customizing web requests and responses in AWS WAF](https://docs.aws.amazon.com/waf/latest/developerguide/waf-custom-request-response.html) in the [AWS WAF Developer Guide](https://docs.aws.amazon.com/waf/latest/developerguide/waf-chapter.html) ." + "CustomRequestHandling": "Defines custom handling for the web request.\n\nFor information about customizing web requests and responses, see [Customizing web requests and responses in AWS WAF](https://docs.aws.amazon.com/waf/latest/developerguide/waf-custom-request-response.html) in the *AWS WAF Developer Guide* ." } }, "AWS::WAFv2::RuleGroup.CustomHTTPHeader": { @@ -50013,9 +50152,9 @@ }, "AWS::WAFv2::RuleGroup.CustomRequestHandling": { "attributes": {}, - "description": "Custom request handling behavior that inserts custom headers into a web request. You can add custom request handling for AWS WAF to use when the rule action doesn't block the request. For example, `CaptchaAction` for requests with valid t okens, and `AllowAction` .\n\nFor information about customizing web requests and responses, see [Customizing web requests and responses in AWS WAF](https://docs.aws.amazon.com/waf/latest/developerguide/waf-custom-request-response.html) in the [AWS WAF Developer Guide](https://docs.aws.amazon.com/waf/latest/developerguide/waf-chapter.html) .", + "description": "Custom request handling behavior that inserts custom headers into a web request. You can add custom request handling for AWS WAF to use when the rule action doesn't block the request. For example, `CaptchaAction` for requests with valid t okens, and `AllowAction` .\n\nFor information about customizing web requests and responses, see [Customizing web requests and responses in AWS WAF](https://docs.aws.amazon.com/waf/latest/developerguide/waf-custom-request-response.html) in the *AWS WAF Developer Guide* .", "properties": { - "InsertHeaders": "The HTTP headers to insert into the request. Duplicate header names are not allowed.\n\nFor information about the limits on count and size for custom request and response settings, see [AWS WAF quotas](https://docs.aws.amazon.com/waf/latest/developerguide/limits.html) in the [AWS WAF Developer Guide](https://docs.aws.amazon.com/waf/latest/developerguide/waf-chapter.html) ." + "InsertHeaders": "The HTTP headers to insert into the request. Duplicate header names are not allowed.\n\nFor information about the limits on count and size for custom request and response settings, see [AWS WAF quotas](https://docs.aws.amazon.com/waf/latest/developerguide/limits.html) in the *AWS WAF Developer Guide* ." } }, "AWS::WAFv2::RuleGroup.CustomResponse": { @@ -50023,15 +50162,15 @@ "description": "A custom response to send to the client. You can define a custom response for rule actions and default web ACL actions that are set to `Block` .\n\nFor information about customizing web requests and responses, see [Customizing web requests and responses in AWS WAF](https://docs.aws.amazon.com/waf/latest/developerguide/waf-custom-request-response.html) in the [AWS WAF Developer Guide](https://docs.aws.amazon.com/waf/latest/developerguide/waf-chapter.html) .", "properties": { "CustomResponseBodyKey": "References the response body that you want AWS WAF to return to the web request client. You can define a custom response for a rule action or a default web ACL action that is set to block. To do this, you first define the response body key and value in the `CustomResponseBodies` setting for the `WebACL` or `RuleGroup` where you want to use it. Then, in the rule action or web ACL default action `BlockAction` setting, you reference the response body using this key.", - "ResponseCode": "The HTTP status code to return to the client.\n\nFor a list of status codes that you can use in your custom responses, see [Supported status codes for custom response](https://docs.aws.amazon.com/waf/latest/developerguide/customizing-the-response-status-codes.html) in the [AWS WAF Developer Guide](https://docs.aws.amazon.com/waf/latest/developerguide/waf-chapter.html) .", - "ResponseHeaders": "The HTTP headers to use in the response. Duplicate header names are not allowed.\n\nFor information about the limits on count and size for custom request and response settings, see [AWS WAF quotas](https://docs.aws.amazon.com/waf/latest/developerguide/limits.html) in the [AWS WAF Developer Guide](https://docs.aws.amazon.com/waf/latest/developerguide/waf-chapter.html) ." + "ResponseCode": "The HTTP status code to return to the client.\n\nFor a list of status codes that you can use in your custom responses, see [Supported status codes for custom response](https://docs.aws.amazon.com/waf/latest/developerguide/customizing-the-response-status-codes.html) in the *AWS WAF Developer Guide* .", + "ResponseHeaders": "The HTTP headers to use in the response. Duplicate header names are not allowed.\n\nFor information about the limits on count and size for custom request and response settings, see [AWS WAF quotas](https://docs.aws.amazon.com/waf/latest/developerguide/limits.html) in the *AWS WAF Developer Guide* ." } }, "AWS::WAFv2::RuleGroup.CustomResponseBody": { "attributes": {}, "description": "The response body to use in a custom response to a web request. This is referenced by key from `CustomResponse` `CustomResponseBodyKey` .", "properties": { - "Content": "The payload of the custom response.\n\nYou can use JSON escape strings in JSON content. To do this, you must specify JSON content in the `ContentType` setting.\n\nFor information about the limits on count and size for custom request and response settings, see [AWS WAF quotas](https://docs.aws.amazon.com/waf/latest/developerguide/limits.html) in the [AWS WAF Developer Guide](https://docs.aws.amazon.com/waf/latest/developerguide/waf-chapter.html) .", + "Content": "The payload of the custom response.\n\nYou can use JSON escape strings in JSON content. To do this, you must specify JSON content in the `ContentType` setting.\n\nFor information about the limits on count and size for custom request and response settings, see [AWS WAF quotas](https://docs.aws.amazon.com/waf/latest/developerguide/limits.html) in the *AWS WAF Developer Guide* .", "ContentType": "The type of content in the payload that you are defining in the `Content` string." } }, @@ -50280,7 +50419,7 @@ "attributes": {}, "description": "Defines and enables Amazon CloudWatch metrics and web request sample collection.", "properties": { - "CloudWatchMetricsEnabled": "A boolean indicating whether the associated resource sends metrics to Amazon CloudWatch. For the list of available metrics, see [AWS WAF Metrics](https://docs.aws.amazon.com/waf/latest/developerguide/monitoring-cloudwatch.html#waf-metrics) .", + "CloudWatchMetricsEnabled": "A boolean indicating whether the associated resource sends metrics to Amazon CloudWatch. For the list of available metrics, see [AWS WAF Metrics](https://docs.aws.amazon.com/waf/latest/developerguide/monitoring-cloudwatch.html#waf-metrics) in the *AWS WAF Developer Guide* .", "MetricName": "A name of the Amazon CloudWatch metric dimension. The name can contain only the characters: A-Z, a-z, 0-9, - (hyphen), and _ (underscore). The name can be from one to 128 characters long. It can't contain whitespace or metric names that are reserved for AWS WAF , for example `All` and `Default_Action` .", "SampledRequestsEnabled": "A boolean indicating whether AWS WAF should store a sampling of the web requests that match the rules. You can view the sampled requests through the AWS WAF console." } @@ -50305,7 +50444,7 @@ "properties": { "CaptchaConfig": "Specifies how AWS WAF should handle `CAPTCHA` evaluations for rules that don't have their own `CaptchaConfig` settings. If you don't specify this, AWS WAF uses its default settings for `CaptchaConfig` .", "ChallengeConfig": "Specifies how AWS WAF should handle challenge evaluations for rules that don't have their own `ChallengeConfig` settings. If you don't specify this, AWS WAF uses its default settings for `ChallengeConfig` .", - "CustomResponseBodies": "A map of custom response keys and content bodies. When you create a rule with a block action, you can send a custom response to the web request. You define these for the web ACL, and then use them in the rules and default actions that you define in the web ACL.\n\nFor information about customizing web requests and responses, see [Customizing web requests and responses in AWS WAF](https://docs.aws.amazon.com/waf/latest/developerguide/waf-custom-request-response.html) in the [AWS WAF Developer Guide](https://docs.aws.amazon.com/waf/latest/developerguide/waf-chapter.html) .\n\nFor information about the limits on count and size for custom request and response settings, see [AWS WAF quotas](https://docs.aws.amazon.com/waf/latest/developerguide/limits.html) in the [AWS WAF Developer Guide](https://docs.aws.amazon.com/waf/latest/developerguide/waf-chapter.html) .", + "CustomResponseBodies": "A map of custom response keys and content bodies. When you create a rule with a block action, you can send a custom response to the web request. You define these for the web ACL, and then use them in the rules and default actions that you define in the web ACL.\n\nFor information about customizing web requests and responses, see [Customizing web requests and responses in AWS WAF](https://docs.aws.amazon.com/waf/latest/developerguide/waf-custom-request-response.html) in the *AWS WAF Developer Guide* .\n\nFor information about the limits on count and size for custom request and response settings, see [AWS WAF quotas](https://docs.aws.amazon.com/waf/latest/developerguide/limits.html) in the *AWS WAF Developer Guide* .", "DefaultAction": "The action to perform if none of the `Rules` contained in the `WebACL` match.", "Description": "A description of the web ACL that helps with identification.", "Name": "The name of the web ACL. You cannot change the name of a web ACL after you create it.", @@ -50336,7 +50475,7 @@ "attributes": {}, "description": "Specifies that AWS WAF should allow the request and optionally defines additional custom handling for the request.\n\nThis is used in the context of other settings, for example to specify values for a rule action or a web ACL default action.", "properties": { - "CustomRequestHandling": "Defines custom handling for the web request.\n\nFor information about customizing web requests and responses, see [Customizing web requests and responses in AWS WAF](https://docs.aws.amazon.com/waf/latest/developerguide/waf-custom-request-response.html) in the [AWS WAF Developer Guide](https://docs.aws.amazon.com/waf/latest/developerguide/waf-chapter.html) ." + "CustomRequestHandling": "Defines custom handling for the web request.\n\nFor information about customizing web requests and responses, see [Customizing web requests and responses in AWS WAF](https://docs.aws.amazon.com/waf/latest/developerguide/waf-custom-request-response.html) in the *AWS WAF Developer Guide* ." } }, "AWS::WAFv2::WebACL.AndStatement": { @@ -50350,7 +50489,7 @@ "attributes": {}, "description": "Specifies that AWS WAF should block the request and optionally defines additional custom handling for the response to the web request.\n\nThis is used in the context of other settings, for example to specify values for a rule action or a web ACL default action.", "properties": { - "CustomResponse": "Defines a custom response for the web request.\n\nFor information about customizing web requests and responses, see [Customizing web requests and responses in AWS WAF](https://docs.aws.amazon.com/waf/latest/developerguide/waf-custom-request-response.html) in the [AWS WAF Developer Guide](https://docs.aws.amazon.com/waf/latest/developerguide/waf-chapter.html) ." + "CustomResponse": "Defines a custom response for the web request.\n\nFor information about customizing web requests and responses, see [Customizing web requests and responses in AWS WAF](https://docs.aws.amazon.com/waf/latest/developerguide/waf-custom-request-response.html) in the *AWS WAF Developer Guide* ." } }, "AWS::WAFv2::WebACL.Body": { @@ -50375,7 +50514,7 @@ "attributes": {}, "description": "Specifies that AWS WAF should run a `CAPTCHA` check against the request:\n\n- If the request includes a valid, unexpired `CAPTCHA` token, AWS WAF allows the web request inspection to proceed to the next rule, similar to a `CountAction` .\n- If the request doesn't include a valid, unexpired `CAPTCHA` token, AWS WAF discontinues the web ACL evaluation of the request and blocks it from going to its intended destination.\n\nAWS WAF generates a response that it sends back to the client, which includes the following:\n\n- The header `x-amzn-waf-action` with a value of `captcha` .\n- The HTTP status code `405 Method Not Allowed` .\n- If the request contains an `Accept` header with a value of `text/html` , the response includes a `CAPTCHA` challenge.\n\nYou can configure the expiration time in the `CaptchaConfig` `ImmunityTimeProperty` setting at the rule and web ACL level. The rule setting overrides the web ACL setting.\n\nThis action option is available for rules. It isn't available for web ACL default actions.", "properties": { - "CustomRequestHandling": "Defines custom handling for the web request, used when the `CAPTCHA` inspection determines that the request's token is valid and unexpired.\n\nFor information about customizing web requests and responses, see [Customizing web requests and responses in AWS WAF](https://docs.aws.amazon.com/waf/latest/developerguide/waf-custom-request-response.html) in the [AWS WAF Developer Guide](https://docs.aws.amazon.com/waf/latest/developerguide/waf-chapter.html) ." + "CustomRequestHandling": "Defines custom handling for the web request, used when the `CAPTCHA` inspection determines that the request's token is valid and unexpired.\n\nFor information about customizing web requests and responses, see [Customizing web requests and responses in AWS WAF](https://docs.aws.amazon.com/waf/latest/developerguide/waf-custom-request-response.html) in the *AWS WAF Developer Guide* ." } }, "AWS::WAFv2::WebACL.CaptchaConfig": { @@ -50421,7 +50560,7 @@ "attributes": {}, "description": "Specifies that AWS WAF should count the request. Optionally defines additional custom handling for the request.\n\nThis is used in the context of other settings, for example to specify values for a rule action or a web ACL default action.", "properties": { - "CustomRequestHandling": "Defines custom handling for the web request.\n\nFor information about customizing web requests and responses, see [Customizing web requests and responses in AWS WAF](https://docs.aws.amazon.com/waf/latest/developerguide/waf-custom-request-response.html) in the [AWS WAF Developer Guide](https://docs.aws.amazon.com/waf/latest/developerguide/waf-chapter.html) ." + "CustomRequestHandling": "Defines custom handling for the web request.\n\nFor information about customizing web requests and responses, see [Customizing web requests and responses in AWS WAF](https://docs.aws.amazon.com/waf/latest/developerguide/waf-custom-request-response.html) in the *AWS WAF Developer Guide* ." } }, "AWS::WAFv2::WebACL.CustomHTTPHeader": { @@ -50434,9 +50573,9 @@ }, "AWS::WAFv2::WebACL.CustomRequestHandling": { "attributes": {}, - "description": "Custom request handling behavior that inserts custom headers into a web request. You can add custom request handling for AWS WAF to use when the rule action doesn't block the request. For example, `CaptchaAction` for requests with valid t okens, and `AllowAction` .\n\nFor information about customizing web requests and responses, see [Customizing web requests and responses in AWS WAF](https://docs.aws.amazon.com/waf/latest/developerguide/waf-custom-request-response.html) in the [AWS WAF Developer Guide](https://docs.aws.amazon.com/waf/latest/developerguide/waf-chapter.html) .", + "description": "Custom request handling behavior that inserts custom headers into a web request. You can add custom request handling for AWS WAF to use when the rule action doesn't block the request. For example, `CaptchaAction` for requests with valid t okens, and `AllowAction` .\n\nFor information about customizing web requests and responses, see [Customizing web requests and responses in AWS WAF](https://docs.aws.amazon.com/waf/latest/developerguide/waf-custom-request-response.html) in the *AWS WAF Developer Guide* .", "properties": { - "InsertHeaders": "The HTTP headers to insert into the request. Duplicate header names are not allowed.\n\nFor information about the limits on count and size for custom request and response settings, see [AWS WAF quotas](https://docs.aws.amazon.com/waf/latest/developerguide/limits.html) in the [AWS WAF Developer Guide](https://docs.aws.amazon.com/waf/latest/developerguide/waf-chapter.html) ." + "InsertHeaders": "The HTTP headers to insert into the request. Duplicate header names are not allowed.\n\nFor information about the limits on count and size for custom request and response settings, see [AWS WAF quotas](https://docs.aws.amazon.com/waf/latest/developerguide/limits.html) in the *AWS WAF Developer Guide* ." } }, "AWS::WAFv2::WebACL.CustomResponse": { @@ -50444,15 +50583,15 @@ "description": "A custom response to send to the client. You can define a custom response for rule actions and default web ACL actions that are set to the block action.\n\nFor information about customizing web requests and responses, see [Customizing web requests and responses in AWS WAF](https://docs.aws.amazon.com/waf/latest/developerguide/waf-custom-request-response.html) in the [AWS WAF Developer Guide](https://docs.aws.amazon.com/waf/latest/developerguide/waf-chapter.html) .", "properties": { "CustomResponseBodyKey": "References the response body that you want AWS WAF to return to the web request client. You can define a custom response for a rule action or a default web ACL action that is set to block. To do this, you first define the response body key and value in the `CustomResponseBodies` setting for the `WebACL` or `RuleGroup` where you want to use it. Then, in the rule action or web ACL default action `BlockAction` setting, you reference the response body using this key.", - "ResponseCode": "The HTTP status code to return to the client.\n\nFor a list of status codes that you can use in your custom responses, see [Supported status codes for custom response](https://docs.aws.amazon.com/waf/latest/developerguide/customizing-the-response-status-codes.html) in the [AWS WAF Developer Guide](https://docs.aws.amazon.com/waf/latest/developerguide/waf-chapter.html) .", - "ResponseHeaders": "The HTTP headers to use in the response. Duplicate header names are not allowed.\n\nFor information about the limits on count and size for custom request and response settings, see [AWS WAF quotas](https://docs.aws.amazon.com/waf/latest/developerguide/limits.html) in the [AWS WAF Developer Guide](https://docs.aws.amazon.com/waf/latest/developerguide/waf-chapter.html) ." + "ResponseCode": "The HTTP status code to return to the client.\n\nFor a list of status codes that you can use in your custom responses, see [Supported status codes for custom response](https://docs.aws.amazon.com/waf/latest/developerguide/customizing-the-response-status-codes.html) in the *AWS WAF Developer Guide* .", + "ResponseHeaders": "The HTTP headers to use in the response. Duplicate header names are not allowed.\n\nFor information about the limits on count and size for custom request and response settings, see [AWS WAF quotas](https://docs.aws.amazon.com/waf/latest/developerguide/limits.html) in the *AWS WAF Developer Guide* ." } }, "AWS::WAFv2::WebACL.CustomResponseBody": { "attributes": {}, "description": "The response body to use in a custom response to a web request. This is referenced by key from `CustomResponse` `CustomResponseBodyKey` .", "properties": { - "Content": "The payload of the custom response.\n\nYou can use JSON escape strings in JSON content. To do this, you must specify JSON content in the `ContentType` setting.\n\nFor information about the limits on count and size for custom request and response settings, see [AWS WAF quotas](https://docs.aws.amazon.com/waf/latest/developerguide/limits.html) in the [AWS WAF Developer Guide](https://docs.aws.amazon.com/waf/latest/developerguide/waf-chapter.html) .", + "Content": "The payload of the custom response.\n\nYou can use JSON escape strings in JSON content. To do this, you must specify JSON content in the `ContentType` setting.\n\nFor information about the limits on count and size for custom request and response settings, see [AWS WAF quotas](https://docs.aws.amazon.com/waf/latest/developerguide/limits.html) in the *AWS WAF Developer Guide* .", "ContentType": "The type of content in the payload that you are defining in the `Content` string." } }, @@ -50822,7 +50961,7 @@ "attributes": {}, "description": "Defines and enables Amazon CloudWatch metrics and web request sample collection.", "properties": { - "CloudWatchMetricsEnabled": "A boolean indicating whether the associated resource sends metrics to Amazon CloudWatch. For the list of available metrics, see [AWS WAF Metrics](https://docs.aws.amazon.com/waf/latest/developerguide/monitoring-cloudwatch.html#waf-metrics) .", + "CloudWatchMetricsEnabled": "A boolean indicating whether the associated resource sends metrics to Amazon CloudWatch. For the list of available metrics, see [AWS WAF Metrics](https://docs.aws.amazon.com/waf/latest/developerguide/monitoring-cloudwatch.html#waf-metrics) in the *AWS WAF Developer Guide* .", "MetricName": "A name of the Amazon CloudWatch metric dimension. The name can contain only the characters: A-Z, a-z, 0-9, - (hyphen), and _ (underscore). The name can be from one to 128 characters long. It can't contain whitespace or metric names that are reserved for AWS WAF , for example `All` and `Default_Action` .", "SampledRequestsEnabled": "A boolean indicating whether AWS WAF should store a sampling of the web requests that match the rules. You can view the sampled requests through the AWS WAF console." } diff --git a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/500_WAFv2_RuleGroup_Rename_Properties_patch.json b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/500_WAFv2_RuleGroup_Rename_Properties_patch.json index c040a1da5b032..7ed7c8d51b4d4 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/500_WAFv2_RuleGroup_Rename_Properties_patch.json +++ b/packages/@aws-cdk/cfnspec/spec-source/specification/000_cfn/500_WAFv2_RuleGroup_Rename_Properties_patch.json @@ -3,99 +3,29 @@ "description": "Reverting property type names from FooAction to Foo, which were introduced as part of this PR: https://github.com/aws/aws-cdk/pull/23984", "operations": [ { - "op": "remove", - "path": "/PropertyTypes/AWS::WAFv2::RuleGroup.AllowAction" + "op": "move", + "from": "/PropertyTypes/AWS::WAFv2::RuleGroup.AllowAction", + "path": "/PropertyTypes/AWS::WAFv2::RuleGroup.Allow" }, { - "op": "add", - "path": "/PropertyTypes/AWS::WAFv2::RuleGroup.Allow", - "value": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-rulegroup-allowaction.html", - "Properties": { - "CustomRequestHandling": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-rulegroup-allowaction.html#cfn-wafv2-rulegroup-allowaction-customrequesthandling", - "Required": false, - "Type": "CustomRequestHandling", - "UpdateType": "Mutable" - } - } - } + "op": "move", + "from": "/PropertyTypes/AWS::WAFv2::RuleGroup.BlockAction", + "path": "/PropertyTypes/AWS::WAFv2::RuleGroup.Block" }, { - "op": "remove", - "path": "/PropertyTypes/AWS::WAFv2::RuleGroup.BlockAction" + "op": "move", + "from": "/PropertyTypes/AWS::WAFv2::RuleGroup.CaptchaAction", + "path": "/PropertyTypes/AWS::WAFv2::RuleGroup.Captcha" }, { - "op": "add", - "path": "/PropertyTypes/AWS::WAFv2::RuleGroup.Block", - "value": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-rulegroup-blockaction.html", - "Properties": { - "CustomResponse": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-rulegroup-blockaction.html#cfn-wafv2-rulegroup-blockaction-customresponse", - "Required": false, - "Type": "CustomResponse", - "UpdateType": "Mutable" - } - } - } + "op": "move", + "from": "/PropertyTypes/AWS::WAFv2::RuleGroup.ChallengeAction", + "path": "/PropertyTypes/AWS::WAFv2::RuleGroup.Challenge" }, { - "op": "remove", - "path": "/PropertyTypes/AWS::WAFv2::RuleGroup.CaptchaAction" - }, - { - "op": "add", - "path": "/PropertyTypes/AWS::WAFv2::RuleGroup.Captcha", - "value": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-rulegroup-captchaaction.html", - "Properties": { - "CustomRequestHandling": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-rulegroup-captchaaction.html#cfn-wafv2-rulegroup-captchaaction-customrequesthandling", - "Required": false, - "Type": "CustomRequestHandling", - "UpdateType": "Mutable" - } - } - } - }, - { - "op": "remove", - "path": "/PropertyTypes/AWS::WAFv2::RuleGroup.ChallengeAction" - }, - { - "op": "add", - "path": "/PropertyTypes/AWS::WAFv2::RuleGroup.Challenge", - "value": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-rulegroup-challengeaction.html", - "Properties": { - "CustomRequestHandling": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-rulegroup-challengeaction.html#cfn-wafv2-rulegroup-challengeaction-customrequesthandling", - "Required": false, - "Type": "CustomRequestHandling", - "UpdateType": "Mutable" - } - } - } - }, - { - "op": "remove", - "path": "/PropertyTypes/AWS::WAFv2::RuleGroup.CountAction" - }, - { - "op": "add", - "path": "/PropertyTypes/AWS::WAFv2::RuleGroup.Count", - "value": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-rulegroup-countaction.html", - "Properties": { - "CustomRequestHandling": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-rulegroup-countaction.html#cfn-wafv2-rulegroup-countaction-customrequesthandling", - "Required": false, - "Type": "CustomRequestHandling", - "UpdateType": "Mutable" - } - } - } + "op": "move", + "from": "/PropertyTypes/AWS::WAFv2::RuleGroup.CountAction", + "path": "/PropertyTypes/AWS::WAFv2::RuleGroup.Count" } ] } diff --git a/packages/@aws-cdk/core/lib/private/cloudformation-lang.ts b/packages/@aws-cdk/core/lib/private/cloudformation-lang.ts index c31d097c4f9f9..9e278eebf8a00 100644 --- a/packages/@aws-cdk/core/lib/private/cloudformation-lang.ts +++ b/packages/@aws-cdk/core/lib/private/cloudformation-lang.ts @@ -452,4 +452,4 @@ class ScopedCache<O extends object, K, V> { } } -const stringifyCache = new ScopedCache<Stack, string, string>(); \ No newline at end of file +const stringifyCache = new ScopedCache<Stack, string, string>(); diff --git a/packages/@aws-cdk/core/lib/private/resolve.ts b/packages/@aws-cdk/core/lib/private/resolve.ts index 9041ecfc4f15d..b16ab314182f0 100644 --- a/packages/@aws-cdk/core/lib/private/resolve.ts +++ b/packages/@aws-cdk/core/lib/private/resolve.ts @@ -192,6 +192,14 @@ export function resolve(obj: any, options: IResolveOptions): any { return arr; } + // + // literal null -- from JsonNull resolution, preserved as-is (semantically meaningful) + // + + if (obj === null) { + return obj; + } + // // tokens - invoke 'resolve' and continue to resolve recursively // diff --git a/packages/@aws-cdk/core/lib/token.ts b/packages/@aws-cdk/core/lib/token.ts index 0ff003b2c28c5..c6af813dd6da2 100644 --- a/packages/@aws-cdk/core/lib/token.ts +++ b/packages/@aws-cdk/core/lib/token.ts @@ -4,7 +4,7 @@ import { unresolved } from './private/encoding'; import { Intrinsic } from './private/intrinsic'; import { resolve } from './private/resolve'; import { TokenMap } from './private/token-map'; -import { IResolvable, ITokenResolver } from './resolvable'; +import { IResolvable, ITokenResolver, IResolveContext } from './resolvable'; import { TokenizedStringFragments } from './string-fragments'; /** @@ -236,12 +236,18 @@ export class Tokenization { * An object which serializes to the JSON `null` literal, and which can safely * be passed across languages where `undefined` and `null` are not different. */ -export class JsonNull { +export class JsonNull implements IResolvable { /** The canonical instance of `JsonNull`. */ public static readonly INSTANCE = new JsonNull(); + public readonly creationStack: string[] = []; + private constructor() { } + public resolve(_ctx: IResolveContext): any { + return null; + } + /** Obtains the JSON representation of this object (`null`) */ public toJSON(): any { return null; diff --git a/packages/aws-cdk/lib/api/util/rwlock.ts b/packages/aws-cdk/lib/api/util/rwlock.ts index 6667bd17afe06..6fb0e4c945dfd 100644 --- a/packages/aws-cdk/lib/api/util/rwlock.ts +++ b/packages/aws-cdk/lib/api/util/rwlock.ts @@ -14,13 +14,12 @@ import * as path from 'path'; export class RWLock { private readonly pidString: string; private readonly writerFile: string; - private readonly readerFile: string; + private readCounter = 0; constructor(public readonly directory: string) { this.pidString = `${process.pid}`; this.writerFile = path.join(this.directory, 'synth.lock'); - this.readerFile = path.join(this.directory, `read.${this.pidString}.lock`); } /** @@ -62,14 +61,26 @@ export class RWLock { return this.doAcquireRead(); } + /** + * Obtains the name fo a (new) `readerFile` to use. This includes a counter so + * that if multiple threads of the same PID attempt to concurrently acquire + * the same lock, they're guaranteed to use a different reader file name (only + * one thread will ever execute JS code at once, guaranteeing the readCounter + * is incremented "atomically" from the point of view of this PID.). + */ + private readerFile(): string { + return path.join(this.directory, `read.${this.pidString}.${++this.readCounter}.lock`); + } + /** * Do the actual acquiring of a read lock. */ private async doAcquireRead(): Promise<ILock> { - await writeFileAtomic(this.readerFile, this.pidString); + const readerFile = this.readerFile(); + await writeFileAtomic(readerFile, this.pidString); return { release: async () => { - await deleteFile(this.readerFile); + await deleteFile(readerFile); }, }; } @@ -102,7 +113,7 @@ export class RWLock { * Check the current readers (if any) */ private async currentReaders(): Promise<number[]> { - const re = /^read\.([^.]+)\.lock$/; + const re = /^read\.([^.]+)\.[^.]+\.lock$/; const ret = new Array<number>(); let children; @@ -156,9 +167,10 @@ async function readFileIfExists(filename: string): Promise<string | undefined> { } } +let tmpCounter = 0; async function writeFileAtomic(filename: string, contents: string): Promise<void> { await fs.mkdir(path.dirname(filename), { recursive: true }); - const tmpFile = `${filename}.${process.pid}`; + const tmpFile = `${filename}.${process.pid}_${++tmpCounter}`; await fs.writeFile(tmpFile, contents, { encoding: 'utf-8' }); await fs.rename(tmpFile, filename); } @@ -181,4 +193,4 @@ function processExists(pid: number) { } catch (e) { return false; } -} \ No newline at end of file +} diff --git a/packages/aws-cdk/lib/cdk-toolkit.ts b/packages/aws-cdk/lib/cdk-toolkit.ts index 012da02865bfb..e2b11284c54c8 100644 --- a/packages/aws-cdk/lib/cdk-toolkit.ts +++ b/packages/aws-cdk/lib/cdk-toolkit.ts @@ -86,7 +86,7 @@ export enum AssetBuildTime { * Build assets just-in-time, before publishing */ JUST_IN_TIME, -}; +} /** * Toolkit logic @@ -489,11 +489,7 @@ export class CdkToolkit { roleArn: options.roleArn, toolkitStackName: options.toolkitStackName, tags, - deploymentMethod: { - method: 'change-set', - changeSetName: options.changeSetName, - execute: options.execute, - }, + deploymentMethod: options.deploymentMethod, usePreviousParameters: true, progress: options.progress, rollback: options.rollback, @@ -612,16 +608,16 @@ export class CdkToolkit { /** * Bootstrap the CDK Toolkit stack in the accounts used by the specified stack(s). * - * @param environmentSpecs environment names that need to have toolkit support - * provisioned, as a glob filter. If none is provided, - * all stacks are implicitly selected. - * @param toolkitStackName the name to be used for the CDK Toolkit stack. + * @param userEnvironmentSpecs environment names that need to have toolkit support + * provisioned, as a glob filter. If none is provided, all stacks are implicitly selected. + * @param bootstrapper Legacy or modern. + * @param options The name, role ARN, bootstrapping parameters, etc. to be used for the CDK Toolkit stack. */ public async bootstrap(userEnvironmentSpecs: string[], bootstrapper: Bootstrapper, options: BootstrapEnvironmentOptions): Promise<void> { // If there is an '--app' argument and an environment looks like a glob, we - // select the environments from the app. Otherwise use what the user said. + // select the environments from the app. Otherwise, use what the user said. - // By default glob for everything + // By default, glob for everything const environmentSpecs = userEnvironmentSpecs.length > 0 ? [...userEnvironmentSpecs] : ['**']; // Partition into globs and non-globs (this will mutate environmentSpecs). @@ -1085,7 +1081,7 @@ export interface ImportOptions extends CfnDeployOptions { readonly recordResourceMapping?: string; /** - * Path to a file with with the physical resource mapping to CDK constructs in JSON format + * Path to a file with the physical resource mapping to CDK constructs in JSON format * * @default - No mapping file */ diff --git a/packages/aws-cdk/lib/cli.ts b/packages/aws-cdk/lib/cli.ts index 0715386fdf3b7..3b8761490aabd 100644 --- a/packages/aws-cdk/lib/cli.ts +++ b/packages/aws-cdk/lib/cli.ts @@ -470,7 +470,7 @@ export async function exec(args: string[], synthesizer?: Synthesizer): Promise<n return cli.list(args.STACKS, { long: args.long, json: argv.json }); case 'diff': - const enableDiffNoFail = isFeatureEnabled(configuration, cxapi.ENABLE_DIFF_NO_FAIL); + const enableDiffNoFail = isFeatureEnabled(configuration, cxapi.ENABLE_DIFF_NO_FAIL_CONTEXT); return cli.diff({ stackNames: args.STACKS, exclusively: args.exclusively, @@ -575,8 +575,11 @@ export async function exec(args: string[], synthesizer?: Synthesizer): Promise<n selector, toolkitStackName, roleArn: args.roleArn, - execute: args.execute, - changeSetName: args.changeSetName, + deploymentMethod: { + method: 'change-set', + execute: args.execute, + changeSetName: args.changeSetName, + }, progress: configuration.settings.get(['progress']), rollback: configuration.settings.get(['rollback']), recordResourceMapping: args['record-resource-mapping'], @@ -596,7 +599,10 @@ export async function exec(args: string[], synthesizer?: Synthesizer): Promise<n toolkitStackName, roleArn: args.roleArn, reuseAssets: args['build-exclude'], - changeSetName: args.changeSetName, + deploymentMethod: { + method: 'change-set', + changeSetName: args.changeSetName, + }, force: args.force, progress: configuration.settings.get(['progress']), rollback: configuration.settings.get(['rollback']), diff --git a/packages/aws-cdk/lib/import.ts b/packages/aws-cdk/lib/import.ts index f3caf736373be..0c8861f5c246e 100644 --- a/packages/aws-cdk/lib/import.ts +++ b/packages/aws-cdk/lib/import.ts @@ -119,7 +119,7 @@ export class ResourceImporter { * Based on the provided resource mapping, prepare CFN structures for import (template, * ResourcesToImport structure) and perform the import operation (CloudFormation deployment) * - * @param resourceMap Mapping from CDK construct tree path to physical resource import identifiers + * @param importMap Mapping from CDK construct tree path to physical resource import identifiers * @param options Options to pass to CloudFormation deploy operation */ public async importResources(importMap: ImportMap, options: DeployStackOptions) {