diff --git a/.github/workflows/issue-label-assign.yml b/.github/workflows/issue-label-assign.yml index 17b33b8455c95..d5f6c6461ff7c 100644 --- a/.github/workflows/issue-label-assign.yml +++ b/.github/workflows/issue-label-assign.yml @@ -228,5 +228,8 @@ jobs: {"area":"@aws-cdk/region-info","keywords":["region-info","fact"],"labels":["@aws-cdk/region-info"],"assignees":["skinny85"]}, {"area":"aws-cdk-lib","keywords":["aws-cdk-lib","cdk-v2","v2","ubergen"],"labels":["aws-cdk-lib"],"assignees":["nija-at"]}, {"area":"monocdk","keywords":["monocdk","monocdk-experiment"],"labels":["monocdk"],"assignees":["nija-at"]}, - {"area":"@aws-cdk/yaml-cfn","keywords":["(aws-yaml-cfn)","(yaml-cfn)"],"labels":["@aws-cdk/aws-yaml-cfn"],"assignees":["skinny85"]} + {"area":"@aws-cdk/yaml-cfn","keywords":["(aws-yaml-cfn)","(yaml-cfn)"],"labels":["@aws-cdk/aws-yaml-cfn"],"assignees":["skinny85"]}, + {"area":"@aws-cdk/aws-apprunner","keywords":["apprunner","aws-apprunner"],"labels":["@aws-cdk/aws-apprunner"],"assignees":["corymhall"]}, + {"area":"@aws-cdk/aws-lightsail","keywords":["lightsail","aws-lightsail"],"labels":["@aws-cdk/aws-lightsail"],"assignees":["corymhall"]}, + {"area":"@aws-cdk/aws-aps","keywords":["aps","aws-aps","prometheus"],"labels":["@aws-cdk/aws-aps"],"assignees":["corymhall"]} ] diff --git a/.github/workflows/pr-linter.yml b/.github/workflows/pr-linter.yml index d88d64d89537d..8231b94fa2319 100644 --- a/.github/workflows/pr-linter.yml +++ b/.github/workflows/pr-linter.yml @@ -24,7 +24,7 @@ jobs: uses: actions/checkout@v2 - name: Install & Build prlint - run: cd tools/@aws-cdk/prlint && yarn install --frozen-lockfile && yarn build+test + run: yarn install --frozen-lockfile && cd tools/@aws-cdk/prlint && yarn build+test - name: Validate uses: ./tools/@aws-cdk/prlint diff --git a/.mergify.yml b/.mergify.yml index c30db93044503..49320bf2385ee 100644 --- a/.mergify.yml +++ b/.mergify.yml @@ -37,10 +37,9 @@ pull_request_rules: actions: comment: message: Thank you for contributing! Your pull request will be automatically updated and merged (do not update manually, and be sure to [allow changes to be pushed to your fork](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/allowing-changes-to-a-pull-request-branch-created-from-a-fork)). - merge: - strict: smart + queue: + name: default method: squash - strict_method: merge commit_message: title+body conditions: - base!=release @@ -60,11 +59,9 @@ pull_request_rules: actions: comment: message: Thank you for contributing! Your pull request will be automatically updated and merged without squashing (do not update manually, and be sure to [allow changes to be pushed to your fork](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/allowing-changes-to-a-pull-request-branch-created-from-a-fork)). - merge: - strict: smart - # Merge instead of squash + queue: + name: default method: merge - strict_method: merge commit_message: title+body conditions: - -title~=(WIP|wip) @@ -106,12 +103,10 @@ pull_request_rules: actions: comment: message: Thanks Dependabot! - merge: - # 'strict: false' disables Mergify keeping the branch up-to-date from master. - # It's not necessary: Dependabot will do that itself. - # It's not dangerous: GitHub branch protection settings prevent merging stale branches. - strict: false + queue: + name: default method: squash + commit_message: title+body conditions: - -title~=(WIP|wip) - -label~=(blocked|do-not-merge) diff --git a/allowed-breaking-changes.txt b/allowed-breaking-changes.txt index 6def9776b6dc9..6794bfab3f542 100644 --- a/allowed-breaking-changes.txt +++ b/allowed-breaking-changes.txt @@ -77,4 +77,8 @@ strengthened:@aws-cdk/aws-lambda-event-sources.ManagedKafkaEventSourceProps # Remove IO2 from autoscaling EbsDeviceVolumeType. This value is not supported # at the moment and was not supported in the past. -removed:@aws-cdk/aws-autoscaling.EbsDeviceVolumeType.IO2 \ No newline at end of file +removed:@aws-cdk/aws-autoscaling.EbsDeviceVolumeType.IO2 + +# Remove autoTerminationPolicy from stepfunctions-tasks EmrCreateClusterProps. This value is not supported by stepfunctions at the moment and was not supported in the past. +removed:@aws-cdk/aws-stepfunctions-tasks.EmrCreateCluster.AutoTerminationPolicyProperty +removed:@aws-cdk/aws-stepfunctions-tasks.EmrCreateClusterProps.autoTerminationPolicy diff --git a/pack.sh b/pack.sh index 7bf4984e33d4d..ca49f5667f8f7 100755 --- a/pack.sh +++ b/pack.sh @@ -36,21 +36,14 @@ function lerna_scopes() { done } -# Compile examples with respect to "decdk" directory, as all packages will -# be symlinked there so they can all be included. -echo "Extracting code samples" >&2 -scripts/run-rosetta.sh $TMPDIR/jsii.txt - -echo "Infusing examples back into assemblies" >&2 -$ROSETTA infuse \ - samples.tabl.json \ - $(cat $TMPDIR/jsii.txt) +scripts/run-rosetta.sh --infuse --pkgs-from $TMPDIR/jsii.txt # Jsii packaging (all at once using jsii-pacmak) echo "Packaging jsii modules" >&2 $PACMAK \ --verbose \ --rosetta-tablet samples.tabl.json \ + --rosetta-unknown-snippets=fail \ $(cat $TMPDIR/jsii.txt) # Non-jsii packaging, which means running 'package' in every individual diff --git a/package.json b/package.json index aea497b0788f1..2dcf60dfa02c0 100644 --- a/package.json +++ b/package.json @@ -77,6 +77,8 @@ "@aws-cdk/assertions/fs-extra/**", "@aws-cdk/aws-amplify-alpha/yaml", "@aws-cdk/aws-amplify-alpha/yaml/**", + "@aws-cdk/aws-iot-actions-alpha/case", + "@aws-cdk/aws-iot-actions-alpha/case/**", "@aws-cdk/aws-amplify/yaml", "@aws-cdk/aws-amplify/yaml/**", "@aws-cdk/aws-codebuild/yaml", @@ -91,6 +93,8 @@ "@aws-cdk/aws-eks/yaml/**", "@aws-cdk/aws-events-targets/aws-sdk", "@aws-cdk/aws-events-targets/aws-sdk/**", + "@aws-cdk/aws-iot-actions/case", + "@aws-cdk/aws-iot-actions/case/**", "@aws-cdk/aws-s3-deployment/case", "@aws-cdk/aws-s3-deployment/case/**", "@aws-cdk/cloud-assembly-schema/jsonschema", diff --git a/packages/@aws-cdk-containers/ecs-service-extensions/lib/extensions/extension-interfaces.ts b/packages/@aws-cdk-containers/ecs-service-extensions/lib/extensions/extension-interfaces.ts index 8e70a53a59d8e..634dfe2624350 100644 --- a/packages/@aws-cdk-containers/ecs-service-extensions/lib/extensions/extension-interfaces.ts +++ b/packages/@aws-cdk-containers/ecs-service-extensions/lib/extensions/extension-interfaces.ts @@ -1,6 +1,6 @@ import * as ecs from '@aws-cdk/aws-ecs'; import * as cdk from '@aws-cdk/core'; -import { Service } from '../service'; +import { Service, connectToProps } from '../service'; // keep this import separate from other imports to reduce chance for merge conflicts with v2-main // eslint-disable-next-line no-duplicate-imports, import/order @@ -225,8 +225,9 @@ export abstract class ServiceExtension { * * @param service - The other service to connect to. */ - public connectToService(service: Service) { + public connectToService(service: Service, connectToProp: connectToProps) { service = service; + connectToProp = connectToProp; } } diff --git a/packages/@aws-cdk-containers/ecs-service-extensions/lib/service.ts b/packages/@aws-cdk-containers/ecs-service-extensions/lib/service.ts index 957dcef280cd7..c1c61843e905d 100644 --- a/packages/@aws-cdk-containers/ecs-service-extensions/lib/service.ts +++ b/packages/@aws-cdk-containers/ecs-service-extensions/lib/service.ts @@ -10,6 +10,19 @@ import { ServiceDescription } from './service-description'; // eslint-disable-next-line no-duplicate-imports, import/order import { Construct } from '@aws-cdk/core'; +/** + * connectToProps will have all the extra parameters which are required for connecting services. + */ +export interface connectToProps { + /** + * local_bind_port is the local port that this application should + * use when calling the upstream service in ECS Consul Mesh Extension + * Currently, this parameter will only be used in the ECSConsulMeshExtension + * https://github.com/aws-ia/ecs-consul-mesh-extension + */ + readonly local_bind_port?: number; +} + /** * The settings for an ECS Service. */ @@ -313,10 +326,10 @@ export class Service extends Construct { * * @param service */ - public connectTo(service: Service) { + public connectTo(service: Service, connectToProps: connectToProps = {}) { for (const extensions in this.serviceDescription.extensions) { if (this.serviceDescription.extensions[extensions]) { - this.serviceDescription.extensions[extensions].connectToService(service); + this.serviceDescription.extensions[extensions].connectToService(service, connectToProps); } } } diff --git a/packages/@aws-cdk/aws-apigateway/lib/api-definition.ts b/packages/@aws-cdk/aws-apigateway/lib/api-definition.ts index 850087920f152..96b9a5aced9e1 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/api-definition.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/api-definition.ts @@ -3,6 +3,10 @@ import * as s3_assets from '@aws-cdk/aws-s3-assets'; // keep this import separate from other imports to reduce chance for merge conflicts with v2-main // eslint-disable-next-line no-duplicate-imports, import/order +import * as cxapi from '@aws-cdk/cx-api'; +import { Node } from 'constructs'; +import { CfnRestApi } from './apigateway.generated'; +import { IRestApi } from './restapi'; import { Construct } from '@aws-cdk/core'; /** @@ -82,6 +86,15 @@ export abstract class ApiDefinition { * assume it's initialized. You may just use it as a construct scope. */ public abstract bind(scope: Construct): ApiDefinitionConfig; + + /** + * Called after the CFN RestApi resource has been created to allow the Api + * Definition to bind to it. Specifically it's required to allow assets to add + * metadata for tooling like SAM CLI to be able to find their origins. + */ + public bindAfterCreate(_scope: Construct, _restApi: IRestApi) { + return; + } } /** @@ -198,4 +211,18 @@ export class AssetApiDefinition extends ApiDefinition { }, }; } + + public bindAfterCreate(scope: Construct, restApi: IRestApi) { + if (!scope.node.tryGetContext(cxapi.ASSET_RESOURCE_METADATA_ENABLED_CONTEXT)) { + return; // not enabled + } + + if (!this.asset) { + throw new Error('bindToResource() must be called after bind()'); + } + + const child = Node.of(restApi).defaultChild as CfnRestApi; + child.addMetadata(cxapi.ASSET_RESOURCE_METADATA_PATH_KEY, this.asset.assetPath); + child.addMetadata(cxapi.ASSET_RESOURCE_METADATA_PROPERTY_KEY, 'BodyS3Location'); + } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigateway/lib/restapi.ts b/packages/@aws-cdk/aws-apigateway/lib/restapi.ts index e213ddad7f22f..7cfebff08c9e0 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/restapi.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/restapi.ts @@ -626,6 +626,9 @@ export class SpecRestApi extends RestApiBase { endpointConfiguration: this._configureEndpoints(props), parameters: props.parameters, }); + + props.apiDefinition.bindAfterCreate(this, this); + this.node.defaultChild = resource; this.restApiId = resource.ref; this.restApiRootResourceId = resource.attrRootResourceId; diff --git a/packages/@aws-cdk/aws-apigateway/test/api-definition.test.ts b/packages/@aws-cdk/aws-apigateway/test/api-definition.test.ts index 709900b36c71d..c4af056038451 100644 --- a/packages/@aws-cdk/aws-apigateway/test/api-definition.test.ts +++ b/packages/@aws-cdk/aws-apigateway/test/api-definition.test.ts @@ -1,7 +1,9 @@ import '@aws-cdk/assert-internal/jest'; import * as path from 'path'; +import { ResourcePart } from '@aws-cdk/assert-internal'; import * as s3 from '@aws-cdk/aws-s3'; import * as cdk from '@aws-cdk/core'; +import * as cxapi from '@aws-cdk/cx-api'; import * as apigw from '../lib'; describe('api definition', () => { @@ -73,6 +75,23 @@ describe('api definition', () => { expect(synthesized.assets.length).toEqual(1); }); + + test('asset metadata added to RestApi resource that contains Asset Api Definition', () => { + const stack = new cdk.Stack(); + stack.node.setContext(cxapi.ASSET_RESOURCE_METADATA_ENABLED_CONTEXT, true); + const assetApiDefinition = apigw.ApiDefinition.fromAsset(path.join(__dirname, 'sample-definition.yaml')); + new apigw.SpecRestApi(stack, 'API', { + apiDefinition: assetApiDefinition, + }); + + expect(stack).toHaveResource('AWS::ApiGateway::RestApi', { + Metadata: { + 'aws:asset:path': 'asset.68497ac876de4e963fc8f7b5f1b28844c18ecc95e3f7c6e9e0bf250e03c037fb.yaml', + 'aws:asset:property': 'BodyS3Location', + }, + }, ResourcePart.CompleteDefinition); + + }); }); describe('apigateway.ApiDefinition.fromBucket', () => { diff --git a/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.request-authorizer.lit.expected.json b/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.request-authorizer.lit.expected.json index e976e6426e0a5..e59d8e02743fc 100644 --- a/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.request-authorizer.lit.expected.json +++ b/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.request-authorizer.lit.expected.json @@ -72,14 +72,14 @@ ] } }, - "Handler": "index.handler", "Role": { "Fn::GetAtt": [ "MyAuthorizerFunctionServiceRole8A34C19E", "Arn" ] }, - "Runtime": "nodejs10.x" + "Handler": "index.handler", + "Runtime": "nodejs14.x" }, "DependsOn": [ "MyAuthorizerFunctionServiceRole8A34C19E" @@ -313,4 +313,4 @@ } } } -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.request-authorizer.lit.ts b/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.request-authorizer.lit.ts index d8160e3be6f49..066badbe4e0bb 100644 --- a/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.request-authorizer.lit.ts +++ b/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.request-authorizer.lit.ts @@ -15,7 +15,7 @@ const app = new App(); const stack = new Stack(app, 'RequestAuthorizerInteg'); const authorizerFn = new lambda.Function(stack, 'MyAuthorizerFunction', { - runtime: lambda.Runtime.NODEJS_10_X, + runtime: lambda.Runtime.NODEJS_14_X, handler: 'index.handler', code: lambda.AssetCode.fromAsset(path.join(__dirname, 'integ.request-authorizer.handler')), }); diff --git a/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.token-authorizer-iam-role.expected.json b/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.token-authorizer-iam-role.expected.json index 1f3a157fc36b9..27e2f89e8f631 100644 --- a/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.token-authorizer-iam-role.expected.json +++ b/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.token-authorizer-iam-role.expected.json @@ -72,14 +72,14 @@ ] } }, - "Handler": "index.handler", "Role": { "Fn::GetAtt": [ "MyAuthorizerFunctionServiceRole8A34C19E", "Arn" ] }, - "Runtime": "nodejs10.x" + "Handler": "index.handler", + "Runtime": "nodejs14.x" }, "DependsOn": [ "MyAuthorizerFunctionServiceRole8A34C19E" diff --git a/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.token-authorizer-iam-role.ts b/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.token-authorizer-iam-role.ts index 47d05cf481009..5890d03f9bc3a 100644 --- a/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.token-authorizer-iam-role.ts +++ b/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.token-authorizer-iam-role.ts @@ -16,7 +16,7 @@ const app = new App(); const stack = new Stack(app, 'TokenAuthorizerIAMRoleInteg'); const authorizerFn = new lambda.Function(stack, 'MyAuthorizerFunction', { - runtime: lambda.Runtime.NODEJS_10_X, + runtime: lambda.Runtime.NODEJS_14_X, handler: 'index.handler', code: lambda.AssetCode.fromAsset(path.join(__dirname, 'integ.token-authorizer.handler')), }); diff --git a/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.token-authorizer.lit.expected.json b/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.token-authorizer.lit.expected.json index bb4c493fac04b..8a56511175436 100644 --- a/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.token-authorizer.lit.expected.json +++ b/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.token-authorizer.lit.expected.json @@ -72,14 +72,14 @@ ] } }, - "Handler": "index.handler", "Role": { "Fn::GetAtt": [ "MyAuthorizerFunctionServiceRole8A34C19E", "Arn" ] }, - "Runtime": "nodejs10.x" + "Handler": "index.handler", + "Runtime": "nodejs14.x" }, "DependsOn": [ "MyAuthorizerFunctionServiceRole8A34C19E" @@ -313,4 +313,4 @@ } } } -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.token-authorizer.lit.ts b/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.token-authorizer.lit.ts index e62e476e8cd4a..655fa91962b1a 100644 --- a/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.token-authorizer.lit.ts +++ b/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.token-authorizer.lit.ts @@ -15,7 +15,7 @@ const app = new App(); const stack = new Stack(app, 'TokenAuthorizerInteg'); const authorizerFn = new lambda.Function(stack, 'MyAuthorizerFunction', { - runtime: lambda.Runtime.NODEJS_10_X, + runtime: lambda.Runtime.NODEJS_14_X, handler: 'index.handler', code: lambda.AssetCode.fromAsset(path.join(__dirname, 'integ.token-authorizer.handler')), }); diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.cors.expected.json b/packages/@aws-cdk/aws-apigateway/test/integ.cors.expected.json index 6d17b2e53232e..146d5220f7540 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integ.cors.expected.json +++ b/packages/@aws-cdk/aws-apigateway/test/integ.cors.expected.json @@ -564,14 +564,14 @@ ] } }, - "Handler": "index.handler", "Role": { "Fn::GetAtt": [ "handlerServiceRole187D5A5A", "Arn" ] }, - "Runtime": "nodejs10.x" + "Handler": "index.handler", + "Runtime": "nodejs14.x" }, "DependsOn": [ "handlerServiceRole187D5A5A" diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.cors.ts b/packages/@aws-cdk/aws-apigateway/test/integ.cors.ts index 5b3fc8bdc36c2..e5617f4c9b202 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integ.cors.ts +++ b/packages/@aws-cdk/aws-apigateway/test/integ.cors.ts @@ -12,7 +12,7 @@ class TestStack extends Stack { const api = new apigw.RestApi(this, 'cors-api-test'); const handler = new lambda.Function(this, 'handler', { - runtime: lambda.Runtime.NODEJS_10_X, + runtime: lambda.Runtime.NODEJS_14_X, handler: 'index.handler', code: lambda.Code.fromAsset(path.join(__dirname, 'integ.cors.handler')), }); diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.lambda-api.latebound-deploymentstage.expected.json b/packages/@aws-cdk/aws-apigateway/test/integ.lambda-api.latebound-deploymentstage.expected.json index 17dd7ccf222e8..8cf5ef7c0e552 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integ.lambda-api.latebound-deploymentstage.expected.json +++ b/packages/@aws-cdk/aws-apigateway/test/integ.lambda-api.latebound-deploymentstage.expected.json @@ -37,14 +37,14 @@ "Code": { "ZipFile": "foo" }, - "Handler": "index.handler", "Role": { "Fn::GetAtt": [ "myfnServiceRole7822DC24", "Arn" ] }, - "Runtime": "nodejs10.x" + "Handler": "index.handler", + "Runtime": "nodejs14.x" }, "DependsOn": [ "myfnServiceRole7822DC24" diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.lambda-api.latebound-deploymentstage.ts b/packages/@aws-cdk/aws-apigateway/test/integ.lambda-api.latebound-deploymentstage.ts index d6176fdb78301..615c934f918fe 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integ.lambda-api.latebound-deploymentstage.ts +++ b/packages/@aws-cdk/aws-apigateway/test/integ.lambda-api.latebound-deploymentstage.ts @@ -9,7 +9,7 @@ class LateBoundDeploymentStageStack extends Stack { const fn = new Function(this, 'myfn', { code: Code.fromInline('foo'), - runtime: Runtime.NODEJS_10_X, + runtime: Runtime.NODEJS_14_X, handler: 'index.handler', }); diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.books.expected.json b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.books.expected.json index 91af30b6ef8d4..a77027807057d 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.books.expected.json +++ b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.books.expected.json @@ -37,14 +37,14 @@ "Code": { "ZipFile": "exports.handler = function echoHandlerCode(event, _, callback) {\n return callback(undefined, {\n isBase64Encoded: false,\n statusCode: 200,\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(event),\n });\n}" }, - "Handler": "index.handler", "Role": { "Fn::GetAtt": [ "BooksHandlerServiceRole5B6A8847", "Arn" ] }, - "Runtime": "nodejs10.x" + "Handler": "index.handler", + "Runtime": "nodejs14.x" }, "DependsOn": [ "BooksHandlerServiceRole5B6A8847" @@ -87,14 +87,14 @@ "Code": { "ZipFile": "exports.handler = function echoHandlerCode(event, _, callback) {\n return callback(undefined, {\n isBase64Encoded: false,\n statusCode: 200,\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(event),\n });\n}" }, - "Handler": "index.handler", "Role": { "Fn::GetAtt": [ "BookHandlerServiceRole894768AD", "Arn" ] }, - "Runtime": "nodejs10.x" + "Handler": "index.handler", + "Runtime": "nodejs14.x" }, "DependsOn": [ "BookHandlerServiceRole894768AD" @@ -137,14 +137,14 @@ "Code": { "ZipFile": "exports.handler = function helloCode(_event, _context, callback) {\n return callback(undefined, {\n statusCode: 200,\n body: 'hello, world!',\n });\n}" }, - "Handler": "index.handler", "Role": { "Fn::GetAtt": [ "HelloServiceRole1E55EA16", "Arn" ] }, - "Runtime": "nodejs10.x" + "Handler": "index.handler", + "Runtime": "nodejs14.x" }, "DependsOn": [ "HelloServiceRole1E55EA16" diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.books.ts b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.books.ts index 9c647c83dcebd..0ec001e3d1a3a 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.books.ts +++ b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.books.ts @@ -7,19 +7,19 @@ class BookStack extends cdk.Stack { super(scope, id); const booksHandler = new apigw.LambdaIntegration(new lambda.Function(this, 'BooksHandler', { - runtime: lambda.Runtime.NODEJS_10_X, + runtime: lambda.Runtime.NODEJS_14_X, handler: 'index.handler', code: lambda.Code.fromInline(`exports.handler = ${echoHandlerCode}`), })); const bookHandler = new apigw.LambdaIntegration(new lambda.Function(this, 'BookHandler', { - runtime: lambda.Runtime.NODEJS_10_X, + runtime: lambda.Runtime.NODEJS_14_X, handler: 'index.handler', code: lambda.Code.fromInline(`exports.handler = ${echoHandlerCode}`), })); const hello = new apigw.LambdaIntegration(new lambda.Function(this, 'Hello', { - runtime: lambda.Runtime.NODEJS_10_X, + runtime: lambda.Runtime.NODEJS_14_X, handler: 'index.handler', code: lambda.Code.fromInline(`exports.handler = ${helloCode}`), })); diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.expected.json b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.expected.json index a0fb6357db3c7..606c5f2098a0d 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.expected.json +++ b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.expected.json @@ -651,14 +651,14 @@ "Code": { "ZipFile": "exports.handler = function handlerCode(event, _, callback) {\n return callback(undefined, {\n isBase64Encoded: false,\n statusCode: 200,\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(event),\n });\n }" }, - "Handler": "index.handler", "Role": { "Fn::GetAtt": [ "MyHandlerServiceRoleFFA06653", "Arn" ] }, - "Runtime": "nodejs10.x" + "Handler": "index.handler", + "Runtime": "nodejs14.x" }, "DependsOn": [ "MyHandlerServiceRoleFFA06653" diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.multistack.expected.json b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.multistack.expected.json index 5aa57732955bc..198d0e80231ae 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.multistack.expected.json +++ b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.multistack.expected.json @@ -38,15 +38,15 @@ "Code": { "ZipFile": "exports.handler = async function(event) {\n return {\n 'headers': { 'Content-Type': 'text/plain' },\n 'statusCode': 200\n }\n }" }, - "Handler": "index.handler", "Role": { "Fn::GetAtt": [ "firstLambdaServiceRoleB6408C31", "Arn" ] }, - "Runtime": "nodejs10.x", - "FunctionName": "FirstLambda" + "FunctionName": "FirstLambda", + "Handler": "index.handler", + "Runtime": "nodejs14.x" }, "DependsOn": [ "firstLambdaServiceRoleB6408C31" diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.multistack.ts b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.multistack.ts index 0ac4e01241eba..75b6e219613de 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.multistack.ts +++ b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.multistack.ts @@ -20,7 +20,7 @@ class FirstStack extends cdk.Stack { } }`), handler: 'index.handler', - runtime: lambda.Runtime.NODEJS_10_X, + runtime: lambda.Runtime.NODEJS_14_X, }); } } diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.multiuse.expected.json b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.multiuse.expected.json index 6a7cea680ef60..0ed3cb19c6aad 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.multiuse.expected.json +++ b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.multiuse.expected.json @@ -37,14 +37,14 @@ "Code": { "ZipFile": "exports.handler = function helloCode(_event, _context, callback) {\n return callback(undefined, {\n statusCode: 200,\n body: 'hello, world!',\n });\n}" }, - "Handler": "index.handler", "Role": { "Fn::GetAtt": [ "HelloServiceRole1E55EA16", "Arn" ] }, - "Runtime": "nodejs10.x" + "Handler": "index.handler", + "Runtime": "nodejs14.x" }, "DependsOn": [ "HelloServiceRole1E55EA16" diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.multiuse.ts b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.multiuse.ts index a248bf9041158..612bd0e83b963 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.multiuse.ts +++ b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.multiuse.ts @@ -7,7 +7,7 @@ class MultiStack extends cdk.Stack { super(scope, id); const hello = new apigw.LambdaIntegration(new lambda.Function(this, 'Hello', { - runtime: lambda.Runtime.NODEJS_10_X, + runtime: lambda.Runtime.NODEJS_14_X, handler: 'index.handler', code: lambda.Code.fromInline(`exports.handler = ${helloCode}`), })); diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.ts b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.ts index 10e3d357108d7..537ec3031d58a 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.ts +++ b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.ts @@ -23,7 +23,7 @@ class Test extends cdk.Stack { }); const handler = new lambda.Function(this, 'MyHandler', { - runtime: lambda.Runtime.NODEJS_10_X, + runtime: lambda.Runtime.NODEJS_14_X, code: lambda.Code.fromInline(`exports.handler = ${handlerCode}`), handler: 'index.handler', }); diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.vpc-endpoint.expected.json b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.vpc-endpoint.expected.json index 9051ff580c010..4279ff70893aa 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.vpc-endpoint.expected.json +++ b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.vpc-endpoint.expected.json @@ -95,15 +95,15 @@ "MyVpcPublicSubnet1NATGatewayAD3400C1": { "Type": "AWS::EC2::NatGateway", "Properties": { + "SubnetId": { + "Ref": "MyVpcPublicSubnet1SubnetF6608456" + }, "AllocationId": { "Fn::GetAtt": [ "MyVpcPublicSubnet1EIP096967CB", "AllocationId" ] }, - "SubnetId": { - "Ref": "MyVpcPublicSubnet1SubnetF6608456" - }, "Tags": [ { "Key": "Name", @@ -192,15 +192,15 @@ "MyVpcPublicSubnet2NATGateway91BFBEC9": { "Type": "AWS::EC2::NatGateway", "Properties": { + "SubnetId": { + "Ref": "MyVpcPublicSubnet2Subnet492B6BFB" + }, "AllocationId": { "Fn::GetAtt": [ "MyVpcPublicSubnet2EIP8CCBA239", "AllocationId" ] }, - "SubnetId": { - "Ref": "MyVpcPublicSubnet2Subnet492B6BFB" - }, "Tags": [ { "Key": "Name", @@ -289,15 +289,15 @@ "MyVpcPublicSubnet3NATGatewayD4B50EBE": { "Type": "AWS::EC2::NatGateway", "Properties": { + "SubnetId": { + "Ref": "MyVpcPublicSubnet3Subnet57EEE236" + }, "AllocationId": { "Fn::GetAtt": [ "MyVpcPublicSubnet3EIPC5ACADAB", "AllocationId" ] }, - "SubnetId": { - "Ref": "MyVpcPublicSubnet3Subnet57EEE236" - }, "Tags": [ { "Key": "Name", diff --git a/packages/@aws-cdk/aws-batch/README.md b/packages/@aws-cdk/aws-batch/README.md index f2900da8cda0f..67d31ea468b41 100644 --- a/packages/@aws-cdk/aws-batch/README.md +++ b/packages/@aws-cdk/aws-batch/README.md @@ -44,17 +44,17 @@ In **MANAGED** mode, AWS will handle the provisioning of compute resources to ac Below is an example of each available type of compute environment: ```ts -const defaultVpc = new ec2.Vpc(this, 'VPC'); +declare const vpc: ec2.Vpc; // default is managed -const awsManagedEnvironment = new batch.ComputeEnvironment(stack, 'AWS-Managed-Compute-Env', { +const awsManagedEnvironment = new batch.ComputeEnvironment(this, 'AWS-Managed-Compute-Env', { computeResources: { - vpc + vpc, } }); -const customerManagedEnvironment = new batch.ComputeEnvironment(stack, 'Customer-Managed-Compute-Env', { - managed: false // unmanaged environment +const customerManagedEnvironment = new batch.ComputeEnvironment(this, 'Customer-Managed-Compute-Env', { + managed: false, // unmanaged environment }); ``` @@ -65,7 +65,7 @@ It is possible to have AWS Batch submit spotfleet requests for obtaining compute ```ts const vpc = new ec2.Vpc(this, 'VPC'); -const spotEnvironment = new batch.ComputeEnvironment(stack, 'MySpotEnvironment', { +const spotEnvironment = new batch.ComputeEnvironment(this, 'MySpotEnvironment', { computeResources: { type: batch.ComputeResourceType.SPOT, bidPercentage: 75, // Bids for resources at 75% of the on-demand price @@ -81,7 +81,7 @@ It is possible to have AWS Batch submit jobs to be run on Fargate compute resour ```ts const vpc = new ec2.Vpc(this, 'VPC'); -const fargateSpotEnvironment = new batch.ComputeEnvironment(stack, 'MyFargateEnvironment', { +const fargateSpotEnvironment = new batch.ComputeEnvironment(this, 'MyFargateEnvironment', { computeResources: { type: batch.ComputeResourceType.FARGATE_SPOT, vpc, @@ -119,7 +119,8 @@ The alternative would be to use the `BEST_FIT_PROGRESSIVE` strategy in order for Simply define your Launch Template: -```ts +```text +// This example is only available in TypeScript const myLaunchTemplate = new ec2.CfnLaunchTemplate(this, 'LaunchTemplate', { launchTemplateName: 'extra-storage-template', launchTemplateData: { @@ -129,17 +130,20 @@ const myLaunchTemplate = new ec2.CfnLaunchTemplate(this, 'LaunchTemplate', { ebs: { encrypted: true, volumeSize: 100, - volumeType: 'gp2' - } - } - ] - } + volumeType: 'gp2', + }, + }, + ], + }, }); ``` and use it: ```ts +declare const vpc: ec2.Vpc; +declare const myLaunchTemplate: ec2.CfnLaunchTemplate; + const myComputeEnv = new batch.ComputeEnvironment(this, 'ComputeEnv', { computeResources: { launchTemplate: { @@ -168,6 +172,7 @@ Occasionally, you will need to deviate from the default processing AMI. ECS Optimized Amazon Linux 2 example: ```ts +declare const vpc: ec2.Vpc; const myComputeEnv = new batch.ComputeEnvironment(this, 'ComputeEnv', { computeResources: { image: new ecs.EcsOptimizedAmi({ @@ -181,11 +186,12 @@ const myComputeEnv = new batch.ComputeEnvironment(this, 'ComputeEnv', { Custom based AMI example: ```ts +declare const vpc: ec2.Vpc; const myComputeEnv = new batch.ComputeEnvironment(this, 'ComputeEnv', { computeResources: { image: ec2.MachineImage.genericLinux({ "[aws-region]": "[ami-ID]", - }) + }), vpc, } }); @@ -196,7 +202,8 @@ const myComputeEnv = new batch.ComputeEnvironment(this, 'ComputeEnv', { Jobs are always submitted to a specific queue. This means that you have to create a queue before you can start submitting jobs. Each queue is mapped to at least one (and no more than three) compute environment. When the job is scheduled for execution, AWS Batch will select the compute environment based on ordinal priority and available capacity in each environment. ```ts -const jobQueue = new batch.JobQueue(stack, 'JobQueue', { +declare const computeEnvironment: batch.ComputeEnvironment; +const jobQueue = new batch.JobQueue(this, 'JobQueue', { computeEnvironments: [ { // Defines a collection of compute resources to handle assigned batch jobs @@ -213,13 +220,20 @@ const jobQueue = new batch.JobQueue(stack, 'JobQueue', { Sometimes you might have jobs that are more important than others, and when submitted, should take precedence over the existing jobs. To achieve this, you can create a priority based execution strategy, by assigning each queue its own priority: ```ts -const highPrioQueue = new batch.JobQueue(stack, 'JobQueue', { - computeEnvironments: sharedComputeEnvs, +declare const sharedComputeEnvs: batch.ComputeEnvironment; +const highPrioQueue = new batch.JobQueue(this, 'JobQueue', { + computeEnvironments: [{ + computeEnvironment: sharedComputeEnvs, + order: 1, + }], priority: 2, }); -const lowPrioQueue = new batch.JobQueue(stack, 'JobQueue', { - computeEnvironments: sharedComputeEnvs, +const lowPrioQueue = new batch.JobQueue(this, 'JobQueue', { + computeEnvironments: [{ + computeEnvironment: sharedComputeEnvs, + order: 1, + }], priority: 1, }); ``` @@ -241,9 +255,11 @@ const jobQueue = batch.JobQueue.fromJobQueueArn(this, 'imported-job-queue', 'arn A Batch Job definition helps AWS Batch understand important details about how to run your application in the scope of a Batch Job. This involves key information like resource requirements, what containers to run, how the compute environment should be prepared, and more. Below is a simple example of how to create a job definition: ```ts -const repo = ecr.Repository.fromRepositoryName(stack, 'batch-job-repo', 'todo-list'); +import * as ecr from '@aws-cdk/aws-ecr'; -new batch.JobDefinition(stack, 'batch-job-def-from-ecr', { +const repo = ecr.Repository.fromRepositoryName(this, 'batch-job-repo', 'todo-list'); + +new batch.JobDefinition(this, 'batch-job-def-from-ecr', { container: { image: new ecs.EcrImage(repo, 'latest'), }, @@ -255,7 +271,7 @@ new batch.JobDefinition(stack, 'batch-job-def-from-ecr', { Below is an example of how you can create a Batch Job Definition from a local Docker application. ```ts -new batch.JobDefinition(stack, 'batch-job-def-from-local', { +new batch.JobDefinition(this, 'batch-job-def-from-local', { container: { // todo-list is a directory containing a Dockerfile to build the application image: ecs.ContainerImage.fromAsset('../todo-list'), @@ -268,14 +284,16 @@ new batch.JobDefinition(stack, 'batch-job-def-from-local', { You can provide custom log driver and its configuration for the container. ```ts -new batch.JobDefinition(stack, 'job-def', { +import * as ssm from '@aws-cdk/aws-ssm'; + +new batch.JobDefinition(this, 'job-def', { container: { image: ecs.EcrImage.fromRegistry('docker/whalesay'), logConfiguration: { logDriver: batch.LogDriver.AWSLOGS, options: { 'awslogs-region': 'us-east-1' }, secretOptions: [ - batch.ExposedSecret.fromParametersStore('xyz', ssm.StringParameter.fromStringParameterName(stack, 'parameter', 'xyz')), + batch.ExposedSecret.fromParametersStore('xyz', ssm.StringParameter.fromStringParameterName(this, 'parameter', 'xyz')), ], }, }, @@ -303,8 +321,8 @@ Below is an example: ```ts // Without revision -const job = batch.JobDefinition.fromJobDefinitionName(this, 'imported-job-definition', 'my-job-definition'); +const job1 = batch.JobDefinition.fromJobDefinitionName(this, 'imported-job-definition', 'my-job-definition'); // With revision -const job = batch.JobDefinition.fromJobDefinitionName(this, 'imported-job-definition', 'my-job-definition:3'); +const job2 = batch.JobDefinition.fromJobDefinitionName(this, 'imported-job-definition', 'my-job-definition:3'); ``` diff --git a/packages/@aws-cdk/aws-batch/package.json b/packages/@aws-cdk/aws-batch/package.json index bcbbc905cebb0..68faf56d0e656 100644 --- a/packages/@aws-cdk/aws-batch/package.json +++ b/packages/@aws-cdk/aws-batch/package.json @@ -28,7 +28,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", diff --git a/packages/@aws-cdk/aws-batch/rosetta/default.ts-fixture b/packages/@aws-cdk/aws-batch/rosetta/default.ts-fixture new file mode 100644 index 0000000000000..6fcfe8682a3ff --- /dev/null +++ b/packages/@aws-cdk/aws-batch/rosetta/default.ts-fixture @@ -0,0 +1,14 @@ +// Fixture with packages imported, but nothing else +import { Construct } from 'constructs'; +import { Stack } from '@aws-cdk/core'; +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as batch from '@aws-cdk/aws-batch'; +import * as ecs from '@aws-cdk/aws-ecs'; + +class Fixture extends Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + + /// here + } +} diff --git a/packages/@aws-cdk/aws-cloudfront/README.md b/packages/@aws-cdk/aws-cloudfront/README.md index 10c42d68efcfd..e10f5c39d596b 100644 --- a/packages/@aws-cdk/aws-cloudfront/README.md +++ b/packages/@aws-cdk/aws-cloudfront/README.md @@ -39,10 +39,7 @@ An S3 bucket can be added as an origin. If the bucket is configured as a website documents. ```ts -import * as cloudfront from '@aws-cdk/aws-cloudfront'; -import * as origins from '@aws-cdk/aws-cloudfront-origins'; - -// Creates a distribution for a S3 bucket. +// Creates a distribution from an S3 bucket. const myBucket = new s3.Bucket(this, 'myBucket'); new cloudfront.Distribution(this, 'myDist', { defaultBehavior: { origin: new origins.S3Origin(myBucket) }, @@ -61,15 +58,13 @@ An Elastic Load Balancing (ELB) v2 load balancer may be used as an origin. In or accessible (`internetFacing` is true). Both Application and Network load balancers are supported. ```ts -import * as ec2 from '@aws-cdk/aws-ec2'; -import * as elbv2 from '@aws-cdk/aws-elasticloadbalancingv2'; - -const vpc = new ec2.Vpc(...); +// Creates a distribution from an ELBv2 load balancer +declare const vpc: ec2.Vpc; // Create an application load balancer in a VPC. 'internetFacing' must be 'true' // for CloudFront to access the load balancer and use it as an origin. const lb = new elbv2.ApplicationLoadBalancer(this, 'LB', { vpc, - internetFacing: true + internetFacing: true, }); new cloudfront.Distribution(this, 'myDist', { defaultBehavior: { origin: new origins.LoadBalancerV2Origin(lb) }, @@ -81,6 +76,7 @@ new cloudfront.Distribution(this, 'myDist', { Origins can also be created from any other HTTP endpoint, given the domain name, and optionally, other origin properties. ```ts +// Creates a distribution from an HTTP endpoint new cloudfront.Distribution(this, 'myDist', { defaultBehavior: { origin: new origins.HttpOrigin('www.example.com') }, }); @@ -98,10 +94,17 @@ may either be created by ACM, or created elsewhere and imported into ACM. When a from SNI only and a minimum protocol version of TLSv1.2_2021 if the '@aws-cdk/aws-cloudfront:defaultSecurityPolicyTLSv1.2_2021' feature flag is set, and TLSv1.2_2019 otherwise. ```ts +// To use your own domain name in a Distribution, you must associate a certificate +import * as acm from '@aws-cdk/aws-certificatemanager'; +import * as route53 from '@aws-cdk/aws-route53'; + +declare const hostedZone: route53.HostedZone; const myCertificate = new acm.DnsValidatedCertificate(this, 'mySiteCert', { domainName: 'www.example.com', hostedZone, }); + +declare const myBucket: s3.Bucket; new cloudfront.Distribution(this, 'myDist', { defaultBehavior: { origin: new origins.S3Origin(myBucket) }, domainNames: ['www.example.com'], @@ -112,10 +115,12 @@ new cloudfront.Distribution(this, 'myDist', { However, you can customize the minimum protocol version for the certificate while creating the distribution using `minimumProtocolVersion` property. ```ts +// Create a Distribution with a custom domain name and a minimum protocol version. +declare const myBucket: s3.Bucket; new cloudfront.Distribution(this, 'myDist', { defaultBehavior: { origin: new origins.S3Origin(myBucket) }, domainNames: ['www.example.com'], - minimumProtocolVersion: SecurityPolicyProtocol.TLS_V1_2016 + minimumProtocolVersion: cloudfront.SecurityPolicyProtocol.TLS_V1_2016, }); ``` @@ -129,12 +134,14 @@ The properties of the default behavior can be adjusted as part of the distributi methods and viewer protocol policy of the cache. ```ts +// Create a Distribution with configured HTTP methods and viewer protocol policy of the cache. +declare const myBucket: s3.Bucket; const myWebDistribution = new cloudfront.Distribution(this, 'myDist', { defaultBehavior: { origin: new origins.S3Origin(myBucket), - allowedMethods: AllowedMethods.ALLOW_ALL, - viewerProtocolPolicy: ViewerProtocolPolicy.REDIRECT_TO_HTTPS, - } + allowedMethods: cloudfront.AllowedMethods.ALLOW_ALL, + viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS, + }, }); ``` @@ -143,25 +150,30 @@ and enable customization for a specific set of resources based on a URL path pat override the default viewer protocol policy for all of the images. ```ts +// Add a behavior to a Distribution after initial creation. +declare const myBucket: s3.Bucket; +declare const myWebDistribution: cloudfront.Distribution; myWebDistribution.addBehavior('/images/*.jpg', new origins.S3Origin(myBucket), { - viewerProtocolPolicy: ViewerProtocolPolicy.REDIRECT_TO_HTTPS, + viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS, }); ``` These behaviors can also be specified at distribution creation time. ```ts +// Create a Distribution with additional behaviors at creation time. +declare const myBucket: s3.Bucket; const bucketOrigin = new origins.S3Origin(myBucket); new cloudfront.Distribution(this, 'myDist', { defaultBehavior: { origin: bucketOrigin, - allowedMethods: AllowedMethods.ALLOW_ALL, - viewerProtocolPolicy: ViewerProtocolPolicy.REDIRECT_TO_HTTPS, + allowedMethods: cloudfront.AllowedMethods.ALLOW_ALL, + viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS, }, additionalBehaviors: { '/images/*.jpg': { origin: bucketOrigin, - viewerProtocolPolicy: ViewerProtocolPolicy.REDIRECT_TO_HTTPS, + viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS, }, }, }); @@ -176,15 +188,19 @@ or you can create your own cache policy that’s specific to your needs. See https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/controlling-the-cache-key.html for more details. ```ts -// Using an existing cache policy +// Using an existing cache policy for a Distribution +declare const bucketOrigin: origins.S3Origin; new cloudfront.Distribution(this, 'myDistManagedPolicy', { defaultBehavior: { origin: bucketOrigin, cachePolicy: cloudfront.CachePolicy.CACHING_OPTIMIZED, }, }); +``` -// Creating a custom cache policy -- all parameters optional +```ts +// Creating a custom cache policy for a Distribution -- all parameters optional +declare const bucketOrigin: origins.S3Origin; const myCachePolicy = new cloudfront.CachePolicy(this, 'myCachePolicy', { cachePolicyName: 'MyPolicy', comment: 'A default policy', @@ -215,25 +231,30 @@ or you can create your own origin request policy that’s specific to your needs See https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/controlling-origin-requests.html for more details. ```ts -// Using an existing origin request policy +// Using an existing origin request policy for a Distribution +declare const bucketOrigin: origins.S3Origin; new cloudfront.Distribution(this, 'myDistManagedPolicy', { defaultBehavior: { origin: bucketOrigin, originRequestPolicy: cloudfront.OriginRequestPolicy.CORS_S3_ORIGIN, }, }); -// Creating a custom origin request policy -- all parameters optional -const myOriginRequestPolicy = new cloudfront.OriginRequestPolicy(stack, 'OriginRequestPolicy', { +``` + +```ts +// Creating a custom origin request policy for a Distribution -- all parameters optional +declare const bucketOrigin: origins.S3Origin; +const myOriginRequestPolicy = new cloudfront.OriginRequestPolicy(this, 'OriginRequestPolicy', { originRequestPolicyName: 'MyPolicy', comment: 'A default policy', cookieBehavior: cloudfront.OriginRequestCookieBehavior.none(), headerBehavior: cloudfront.OriginRequestHeaderBehavior.all('CloudFront-Is-Android-Viewer'), queryStringBehavior: cloudfront.OriginRequestQueryStringBehavior.allowList('username'), }); + new cloudfront.Distribution(this, 'myDistCustomPolicy', { defaultBehavior: { origin: bucketOrigin, - cachePolicy: myCachePolicy, originRequestPolicy: myOriginRequestPolicy, }, }); @@ -241,23 +262,26 @@ new cloudfront.Distribution(this, 'myDistCustomPolicy', { ### Validating signed URLs or signed cookies with Trusted Key Groups -CloudFront Distribution now supports validating signed URLs or signed cookies using key groups. When a cache behavior contains trusted key groups, CloudFront requires signed URLs or signed cookies for all requests that match the cache behavior. - -Example: +CloudFront Distribution supports validating signed URLs or signed cookies using key groups. +When a cache behavior contains trusted key groups, CloudFront requires signed URLs or signed +cookies for all requests that match the cache behavior. ```ts +// Validating signed URLs or signed cookies with Trusted Key Groups + // public key in PEM format -const pubKey = new PublicKey(stack, 'MyPubKey', { +declare const publicKey: string; +const pubKey = new cloudfront.PublicKey(this, 'MyPubKey', { encodedKey: publicKey, }); -const keyGroup = new KeyGroup(stack, 'MyKeyGroup', { +const keyGroup = new cloudfront.KeyGroup(this, 'MyKeyGroup', { items: [ pubKey, ], }); -new cloudfront.Distribution(stack, 'Dist', { +new cloudfront.Distribution(this, 'Dist', { defaultBehavior: { origin: new origins.HttpOrigin('www.example.com'), trustedKeyGroups: [ @@ -269,23 +293,27 @@ new cloudfront.Distribution(stack, 'Dist', { ### Lambda@Edge -Lambda@Edge is an extension of AWS Lambda, a compute service that lets you execute functions that customize the content that CloudFront delivers. -You can author Node.js or Python functions in the US East (N. Virginia) region, -and then execute them in AWS locations globally that are closer to the viewer, -without provisioning or managing servers. -Lambda@Edge functions are associated with a specific behavior and event type. -Lambda@Edge can be used to rewrite URLs, -alter responses based on headers or cookies, -or authorize requests based on headers or authorization tokens. +Lambda@Edge is an extension of AWS Lambda, a compute service that lets you execute +functions that customize the content that CloudFront delivers. You can author Node.js +or Python functions in the US East (N. Virginia) region, and then execute them in AWS +locations globally that are closer to the viewer, without provisioning or managing servers. +Lambda@Edge functions are associated with a specific behavior and event type. Lambda@Edge +can be used to rewrite URLs, alter responses based on headers or cookies, or authorize +requests based on headers or authorization tokens. -The following shows a Lambda@Edge function added to the default behavior and triggered on every request: +The following shows a Lambda@Edge function added to the default behavior and triggered +on every request: ```ts +// A Lambda@Edge function added to default behavior of a Distribution +// and triggered on every request const myFunc = new cloudfront.experimental.EdgeFunction(this, 'MyFunction', { runtime: lambda.Runtime.NODEJS_12_X, handler: 'index.handler', code: lambda.Code.fromAsset(path.join(__dirname, 'lambda-handler')), }); + +declare const myBucket: s3.Bucket; new cloudfront.Distribution(this, 'myDist', { defaultBehavior: { origin: new origins.S3Origin(myBucket), @@ -309,6 +337,7 @@ new cloudfront.Distribution(this, 'myDist', { If the stack is in `us-east-1`, a "normal" `lambda.Function` can be used instead of an `EdgeFunction`. ```ts +// Using a lambda Function instead of an EdgeFunction for stacks in `us-east-`. const myFunc = new lambda.Function(this, 'MyFunction', { runtime: lambda.Runtime.NODEJS_12_X, handler: 'index.handler', @@ -320,18 +349,20 @@ If the stack is not in `us-east-1`, and you need references from different appli you can also set a specific stack ID for each Lambda@Edge. ```ts +// Setting stackIds for EdgeFunctions that can be referenced from different applications +// on the same account. const myFunc1 = new cloudfront.experimental.EdgeFunction(this, 'MyFunction1', { runtime: lambda.Runtime.NODEJS_12_X, handler: 'index.handler', code: lambda.Code.fromAsset(path.join(__dirname, 'lambda-handler1')), - stackId: 'edge-lambda-stack-id-1' + stackId: 'edge-lambda-stack-id-1', }); const myFunc2 = new cloudfront.experimental.EdgeFunction(this, 'MyFunction2', { runtime: lambda.Runtime.NODEJS_12_X, handler: 'index.handler', code: lambda.Code.fromAsset(path.join(__dirname, 'lambda-handler2')), - stackId: 'edge-lambda-stack-id-2' + stackId: 'edge-lambda-stack-id-2', }); ``` @@ -339,9 +370,13 @@ Lambda@Edge functions can also be associated with additional behaviors, either at or after Distribution creation time. ```ts +// Associating a Lambda@Edge function with additional behaviors. + +declare const myFunc: cloudfront.experimental.EdgeFunction; // assigning at Distribution creation +declare const myBucket: s3.Bucket; const myOrigin = new origins.S3Origin(myBucket); -new cloudfront.Distribution(this, 'myDist', { +const myDistribution = new cloudfront.Distribution(this, 'myDist', { defaultBehavior: { origin: myOrigin }, additionalBehaviors: { 'images/*': { @@ -371,16 +406,19 @@ myDistribution.addBehavior('images/*', myOrigin, { Adding an existing Lambda@Edge function created in a different stack to a CloudFront distribution. ```ts +// Adding an existing Lambda@Edge function created in a different stack +// to a CloudFront distribution. +declare const s3Bucket: s3.Bucket; const functionVersion = lambda.Version.fromVersionArn(this, 'Version', 'arn:aws:lambda:us-east-1:123456789012:function:functionName:1'); new cloudfront.Distribution(this, 'distro', { defaultBehavior: { origin: new origins.S3Origin(s3Bucket), edgeLambdas: [ - { - functionVersion, - eventType: cloudfront.LambdaEdgeEventType.VIEWER_REQUEST - }, + { + functionVersion, + eventType: cloudfront.LambdaEdgeEventType.VIEWER_REQUEST, + }, ], }, }); @@ -391,11 +429,13 @@ new cloudfront.Distribution(this, 'distro', { You can also deploy CloudFront functions and add them to a CloudFront distribution. ```ts -const cfFunction = new cloudfront.Function(stack, 'Function', { +// Add a cloudfront Function to a Distribution +const cfFunction = new cloudfront.Function(this, 'Function', { code: cloudfront.FunctionCode.fromInline('function handler(event) { return event.request }'), }); -new cloudfront.Distribution(stack, 'distro', { +declare const s3Bucket: s3.Bucket; +new cloudfront.Distribution(this, 'distro', { defaultBehavior: { origin: new origins.S3Origin(s3Bucket), functionAssociations: [{ @@ -416,6 +456,8 @@ You can configure CloudFront to create log files that contain detailed informati The logs can go to either an existing bucket, or a bucket will be created for you. ```ts +// Configure logging for Distributions + // Simplest form - creates a new bucket and logs to it. new cloudfront.Distribution(this, 'myDist', { defaultBehavior: { origin: new origins.HttpOrigin('www.example.com') }, @@ -438,7 +480,8 @@ Existing distributions can be imported as well; note that like most imported con However, it can be used as a reference for other higher-level constructs. ```ts -const distribution = cloudfront.Distribution.fromDistributionAttributes(scope, 'ImportedDist', { +// Using a reference to an imported Distribution +const distribution = cloudfront.Distribution.fromDistributionAttributes(this, 'ImportedDist', { domainName: 'd111111abcdef8.cloudfront.net', distributionId: '012345ABCDEF', }); @@ -452,23 +495,25 @@ const distribution = cloudfront.Distribution.fromDistributionAttributes(scope, ' Example usage: ```ts -const sourceBucket = new Bucket(this, 'Bucket'); +// Using a CloudFrontWebDistribution construct. -const distribution = new CloudFrontWebDistribution(this, 'MyDistribution', { - originConfigs: [ - { - s3OriginSource: { - s3BucketSource: sourceBucket - }, - behaviors : [ {isDefaultBehavior: true}] - } - ] - }); +declare const sourceBucket: s3.Bucket; +const distribution = new cloudfront.CloudFrontWebDistribution(this, 'MyDistribution', { + originConfigs: [ + { + s3OriginSource: { + s3BucketSource: sourceBucket, + }, + behaviors : [ {isDefaultBehavior: true}], + }, + ], +}); ``` ### Viewer certificate -By default, CloudFront Web Distributions will answer HTTPS requests with CloudFront's default certificate, only containing the distribution `domainName` (e.g. d111111abcdef8.cloudfront.net). +By default, CloudFront Web Distributions will answer HTTPS requests with CloudFront's default certificate, +only containing the distribution `domainName` (e.g. d111111abcdef8.cloudfront.net). You can customize the viewer certificate property to provide a custom certificate and/or list of domain name aliases to fit your needs. See [Using Alternate Domain Names and HTTPS](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/using-https-alternate-domain-names.html) in the CloudFront User Guide. @@ -486,7 +531,10 @@ Example: You can change the default certificate by one stored AWS Certificate Manager, or ACM. Those certificate can either be generated by AWS, or purchased by another CA imported into ACM. -For more information, see [the aws-certificatemanager module documentation](https://docs.aws.amazon.com/cdk/api/latest/docs/aws-certificatemanager-readme.html) or [Importing Certificates into AWS Certificate Manager](https://docs.aws.amazon.com/acm/latest/userguide/import-certificate.html) in the AWS Certificate Manager User Guide. +For more information, see +[the aws-certificatemanager module documentation](https://docs.aws.amazon.com/cdk/api/latest/docs/aws-certificatemanager-readme.html) +or [Importing Certificates into AWS Certificate Manager](https://docs.aws.amazon.com/acm/latest/userguide/import-certificate.html) +in the AWS Certificate Manager User Guide. Example: @@ -504,22 +552,26 @@ Example: ### Trusted Key Groups -CloudFront Web Distributions supports validating signed URLs or signed cookies using key groups. When a cache behavior contains trusted key groups, CloudFront requires signed URLs or signed cookies for all requests that match the cache behavior. +CloudFront Web Distributions supports validating signed URLs or signed cookies using key groups. +When a cache behavior contains trusted key groups, CloudFront requires signed URLs or signed cookies for all requests that match the cache behavior. Example: ```ts -const pubKey = new PublicKey(stack, 'MyPubKey', { +// Using trusted key groups for Cloudfront Web Distributions. +declare const sourceBucket: s3.Bucket; +declare const publicKey: string; +const pubKey = new cloudfront.PublicKey(this, 'MyPubKey', { encodedKey: publicKey, }); -const keyGroup = new KeyGroup(stack, 'MyKeyGroup', { +const keyGroup = new cloudfront.KeyGroup(this, 'MyKeyGroup', { items: [ pubKey, ], }); -new CloudFrontWebDistribution(stack, 'AnAmazingWebsiteProbably', { +new cloudfront.CloudFrontWebDistribution(this, 'AnAmazingWebsiteProbably', { originConfigs: [ { s3OriginSource: { @@ -547,15 +599,25 @@ See [Restricting the Geographic Distribution of Your Content](https://docs.aws.a Example: ```ts -new cloudfront.CloudFrontWebDistribution(stack, 'MyDistribution', { - //... - geoRestriction: GeoRestriction.whitelist('US', 'UK') +// Adding restrictions to a Cloudfront Web Distribution. +declare const sourceBucket: s3.Bucket; +new cloudfront.CloudFrontWebDistribution(this, 'MyDistribution', { + originConfigs: [ + { + s3OriginSource: { + s3BucketSource: sourceBucket, + }, + behaviors : [ {isDefaultBehavior: true}], + }, + ], + geoRestriction: cloudfront.GeoRestriction.whitelist('US', 'UK'), }); ``` ### Connection behaviors between CloudFront and your origin -CloudFront provides you even more control over the connection behaviors between CloudFront and your origin. You can now configure the number of connection attempts CloudFront will make to your origin and the origin connection timeout for each attempt. +CloudFront provides you even more control over the connection behaviors between CloudFront and your origin. +You can now configure the number of connection attempts CloudFront will make to your origin and the origin connection timeout for each attempt. See [Origin Connection Attempts](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/distribution-web-values-specify.html#origin-connection-attempts) @@ -564,43 +626,49 @@ See [Origin Connection Timeout](https://docs.aws.amazon.com/AmazonCloudFront/lat Example usage: ```ts -const distribution = new CloudFrontWebDistribution(this, 'MyDistribution', { - originConfigs: [ +// Configuring connection behaviors between Cloudfront and your origin +const distribution = new cloudfront.CloudFrontWebDistribution(this, 'MyDistribution', { + originConfigs: [ + { + connectionAttempts: 3, + connectionTimeout: Duration.seconds(10), + behaviors: [ { - ..., - connectionAttempts: 3, - connectionTimeout: cdk.Duration.seconds(10), - } - ] + isDefaultBehavior: true, + }, + ], + }, + ], }); ``` #### Origin Fallback In case the origin source is not available and answers with one of the -specified status code the failover origin source will be used. +specified status codes the failover origin source will be used. ```ts -new CloudFrontWebDistribution(stack, 'ADistribution', { +// Configuring origin fallback options for the CloudFrontWebDistribution +new cloudfront.CloudFrontWebDistribution(this, 'ADistribution', { originConfigs: [ { s3OriginSource: { - s3BucketSource: s3.Bucket.fromBucketName(stack, 'aBucket', 'myoriginbucket'), + s3BucketSource: s3.Bucket.fromBucketName(this, 'aBucket', 'myoriginbucket'), originPath: '/', originHeaders: { 'myHeader': '42', }, - originShieldRegion: 'us-west-2' + originShieldRegion: 'us-west-2', }, failoverS3OriginSource: { - s3BucketSource: s3.Bucket.fromBucketName(stack, 'aBucketFallback', 'myoriginbucketfallback'), + s3BucketSource: s3.Bucket.fromBucketName(this, 'aBucketFallback', 'myoriginbucketfallback'), originPath: '/somewhere', originHeaders: { 'myHeader2': '21', }, - originShieldRegion: 'us-east-1' + originShieldRegion: 'us-east-1', }, - failoverCriteriaStatusCodes: [FailoverStatusCode.INTERNAL_SERVER_ERROR], + failoverCriteriaStatusCodes: [cloudfront.FailoverStatusCode.INTERNAL_SERVER_ERROR], behaviors: [ { isDefaultBehavior: true, @@ -613,7 +681,8 @@ new CloudFrontWebDistribution(stack, 'ADistribution', { ## KeyGroup & PublicKey API -Now you can create a key group to use with CloudFront signed URLs and signed cookies. You can add public keys to use with CloudFront features such as signed URLs, signed cookies, and field-level encryption. +You can create a key group to use with CloudFront signed URLs and signed cookies +You can add public keys to use with CloudFront features such as signed URLs, signed cookies, and field-level encryption. The following example command uses OpenSSL to generate an RSA key pair with a length of 2048 bits and save to the file named `private_key.pem`. @@ -632,15 +701,16 @@ Note: Don't forget to copy/paste the contents of `public_key.pem` file including Example: ```ts - new cloudfront.KeyGroup(stack, 'MyKeyGroup', { - items: [ - new cloudfront.PublicKey(stack, 'MyPublicKey', { - encodedKey: '...', // contents of public_key.pem file - // comment: 'Key is expiring on ...', - }), - ], - // comment: 'Key group containing public keys ...', - }); +// Create a key group to use with CloudFront signed URLs and signed cookies. +new cloudfront.KeyGroup(this, 'MyKeyGroup', { + items: [ + new cloudfront.PublicKey(this, 'MyPublicKey', { + encodedKey: '...', // contents of public_key.pem file + // comment: 'Key is expiring on ...', + }), + ], + // comment: 'Key group containing public keys ...', +}); ``` See: diff --git a/packages/@aws-cdk/aws-cloudfront/lib/web-distribution.ts b/packages/@aws-cdk/aws-cloudfront/lib/web-distribution.ts index ab2fbbd44b03c..c0a332a2e1b89 100644 --- a/packages/@aws-cdk/aws-cloudfront/lib/web-distribution.ts +++ b/packages/@aws-cdk/aws-cloudfront/lib/web-distribution.ts @@ -721,19 +721,17 @@ export interface CloudFrontWebDistributionAttributes { * Here's how you can use this construct: * * ```ts - * import { CloudFrontWebDistribution } from '@aws-cdk/aws-cloudfront' + * const sourceBucket = new s3.Bucket(this, 'Bucket'); * - * const sourceBucket = new Bucket(this, 'Bucket'); - * - * const distribution = new CloudFrontWebDistribution(this, 'MyDistribution', { - * originConfigs: [ - * { - * s3OriginSource: { - * s3BucketSource: sourceBucket - * }, - * behaviors : [ {isDefaultBehavior: true}] - * } - * ] + * const distribution = new cloudfront.CloudFrontWebDistribution(this, 'MyDistribution', { + * originConfigs: [ + * { + * s3OriginSource: { + * s3BucketSource: sourceBucket, + * }, + * behaviors : [ {isDefaultBehavior: true}], + * }, + * ], * }); * ``` * diff --git a/packages/@aws-cdk/aws-cloudfront/package.json b/packages/@aws-cdk/aws-cloudfront/package.json index 8a43d54731a76..b0fdf92f48da2 100644 --- a/packages/@aws-cdk/aws-cloudfront/package.json +++ b/packages/@aws-cdk/aws-cloudfront/package.json @@ -28,7 +28,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", diff --git a/packages/@aws-cdk/aws-cloudfront/rosetta/default.ts-fixture b/packages/@aws-cdk/aws-cloudfront/rosetta/default.ts-fixture new file mode 100644 index 0000000000000..1ac8c7fec5da3 --- /dev/null +++ b/packages/@aws-cdk/aws-cloudfront/rosetta/default.ts-fixture @@ -0,0 +1,17 @@ +import { Construct } from 'constructs'; +import { Duration, Stack } from '@aws-cdk/core'; +import * as cloudfront from '@aws-cdk/aws-cloudfront'; +import * as origins from '@aws-cdk/aws-cloudfront-origins'; +import * as s3 from '@aws-cdk/aws-s3'; +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as elbv2 from '@aws-cdk/aws-elasticloadbalancingv2'; +import * as lambda from '@aws-cdk/aws-lambda'; +import * as path from 'path'; + +class Context extends Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + + /// here + } +} diff --git a/packages/@aws-cdk/aws-codedeploy/README.md b/packages/@aws-cdk/aws-codedeploy/README.md index e8f878973ee6a..9b6aa5cda5d8d 100644 --- a/packages/@aws-cdk/aws-codedeploy/README.md +++ b/packages/@aws-cdk/aws-codedeploy/README.md @@ -22,10 +22,8 @@ The CDK currently supports Amazon EC2, on-premise and AWS Lambda applications. To create a new CodeDeploy Application that deploys to EC2/on-premise instances: ```ts -import * as codedeploy from '@aws-cdk/aws-codedeploy'; - const application = new codedeploy.ServerApplication(this, 'CodeDeployApplication', { - applicationName: 'MyApplication', // optional property + applicationName: 'MyApplication', // optional property }); ``` @@ -33,7 +31,9 @@ To import an already existing Application: ```ts const application = codedeploy.ServerApplication.fromServerApplicationName( - this, 'ExistingCodeDeployApplication', 'MyExistingApplication' + this, + 'ExistingCodeDeployApplication', + 'MyExistingApplication', ); ``` @@ -42,47 +42,51 @@ const application = codedeploy.ServerApplication.fromServerApplicationName( To create a new CodeDeploy Deployment Group that deploys to EC2/on-premise instances: ```ts +import * as autoscaling from '@aws-cdk/aws-autoscaling'; +import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; + +declare const application: codedeploy.ServerApplication; +declare const asg: autoscaling.AutoScalingGroup; +declare const alarm: cloudwatch.Alarm; const deploymentGroup = new codedeploy.ServerDeploymentGroup(this, 'CodeDeployDeploymentGroup', { - application, - deploymentGroupName: 'MyDeploymentGroup', - autoScalingGroups: [asg1, asg2], - // adds User Data that installs the CodeDeploy agent on your auto-scaling groups hosts - // default: true - installAgent: true, - // adds EC2 instances matching tags - ec2InstanceTags: new codedeploy.InstanceTagSet( - { - // any instance with tags satisfying - // key1=v1 or key1=v2 or key2 (any value) or value v3 (any key) - // will match this group - 'key1': ['v1', 'v2'], - 'key2': [], - '': ['v3'], - }, - ), - // adds on-premise instances matching tags - onPremiseInstanceTags: new codedeploy.InstanceTagSet( - // only instances with tags (key1=v1 or key1=v2) AND key2=v3 will match this set - { - 'key1': ['v1', 'v2'], - }, - { - 'key2': ['v3'], - }, - ), - // CloudWatch alarms - alarms: [ - new cloudwatch.Alarm(/* ... */), - ], - // whether to ignore failure to fetch the status of alarms from CloudWatch - // default: false - ignorePollAlarmsFailure: false, - // auto-rollback configuration - autoRollback: { - failedDeployment: true, // default: true - stoppedDeployment: true, // default: false - deploymentInAlarm: true, // default: true if you provided any alarms, false otherwise + application, + deploymentGroupName: 'MyDeploymentGroup', + autoScalingGroups: [asg], + // adds User Data that installs the CodeDeploy agent on your auto-scaling groups hosts + // default: true + installAgent: true, + // adds EC2 instances matching tags + ec2InstanceTags: new codedeploy.InstanceTagSet( + { + // any instance with tags satisfying + // key1=v1 or key1=v2 or key2 (any value) or value v3 (any key) + // will match this group + 'key1': ['v1', 'v2'], + 'key2': [], + '': ['v3'], + }, + ), + // adds on-premise instances matching tags + onPremiseInstanceTags: new codedeploy.InstanceTagSet( + // only instances with tags (key1=v1 or key1=v2) AND key2=v3 will match this set + { + 'key1': ['v1', 'v2'], + }, + { + 'key2': ['v3'], }, + ), + // CloudWatch alarms + alarms: [alarm], + // whether to ignore failure to fetch the status of alarms from CloudWatch + // default: false + ignorePollAlarmsFailure: false, + // auto-rollback configuration + autoRollback: { + failedDeployment: true, // default: true + stoppedDeployment: true, // default: false + deploymentInAlarm: true, // default: true if you provided any alarms, false otherwise + }, }); ``` @@ -92,10 +96,14 @@ one will be automatically created. To import an already existing Deployment Group: ```ts -const deploymentGroup = codedeploy.ServerDeploymentGroup.fromLambdaDeploymentGroupAttributes(this, 'ExistingCodeDeployDeploymentGroup', { +declare const application: codedeploy.ServerApplication; +const deploymentGroup = codedeploy.ServerDeploymentGroup.fromServerDeploymentGroupAttributes( + this, + 'ExistingCodeDeployDeploymentGroup', { application, deploymentGroupName: 'MyExistingDeploymentGroup', -}); + }, +); ``` ### Load balancers @@ -108,18 +116,15 @@ with the `loadBalancer` property when creating a Deployment Group. With Classic Elastic Load Balancer, you provide it directly: ```ts -import * as lb from '@aws-cdk/aws-elasticloadbalancing'; +import * as elb from '@aws-cdk/aws-elasticloadbalancing'; -const elb = new lb.LoadBalancer(this, 'ELB', { - // ... -}); -elb.addTarget(/* ... */); -elb.addListener({ - // ... +declare const lb: elb.LoadBalancer; +lb.addListener({ + externalPort: 80, }); const deploymentGroup = new codedeploy.ServerDeploymentGroup(this, 'DeploymentGroup', { - loadBalancer: codedeploy.LoadBalancer.classic(elb), + loadBalancer: codedeploy.LoadBalancer.classic(lb), }); ``` @@ -127,17 +132,11 @@ With Application Load Balancer or Network Load Balancer, you provide a Target Group as the load balancer: ```ts -import * as lbv2 from '@aws-cdk/aws-elasticloadbalancingv2'; +import * as elbv2 from '@aws-cdk/aws-elasticloadbalancingv2'; -const alb = new lbv2.ApplicationLoadBalancer(this, 'ALB', { - // ... -}); -const listener = alb.addListener('Listener', { - // ... -}); -const targetGroup = listener.addTargets('Fleet', { - // ... -}); +declare const alb: elbv2.ApplicationLoadBalancer; +const listener = alb.addListener('Listener', { port: 80 }); +const targetGroup = listener.addTargets('Fleet', { port: 80 }); const deploymentGroup = new codedeploy.ServerDeploymentGroup(this, 'DeploymentGroup', { loadBalancer: codedeploy.LoadBalancer.application(targetGroup), @@ -150,7 +149,7 @@ You can also pass a Deployment Configuration when creating the Deployment Group: ```ts const deploymentGroup = new codedeploy.ServerDeploymentGroup(this, 'CodeDeployDeploymentGroup', { - deploymentConfig: codedeploy.ServerDeploymentConfig.ALL_AT_ONCE, + deploymentConfig: codedeploy.ServerDeploymentConfig.ALL_AT_ONCE, }); ``` @@ -160,10 +159,10 @@ You can also create a custom Deployment Configuration: ```ts const deploymentConfig = new codedeploy.ServerDeploymentConfig(this, 'DeploymentConfiguration', { - deploymentConfigName: 'MyDeploymentConfiguration', // optional property - // one of these is required, but both cannot be specified at the same time - minHealthyHostCount: 2, - minHealthyHostPercentage: 75, + deploymentConfigName: 'MyDeploymentConfiguration', // optional property + // one of these is required, but both cannot be specified at the same time + minimumHealthyHosts: codedeploy.MinimumHealthyHosts.count(2), + // minimumHealthyHosts: codedeploy.MinimumHealthyHosts.percentage(75), }); ``` @@ -171,7 +170,9 @@ Or import an existing one: ```ts const deploymentConfig = codedeploy.ServerDeploymentConfig.fromServerDeploymentConfigName( - this, 'ExistingDeploymentConfiguration', 'MyExistingDeploymentConfiguration' + this, + 'ExistingDeploymentConfiguration', + 'MyExistingDeploymentConfiguration', ); ``` @@ -180,10 +181,8 @@ const deploymentConfig = codedeploy.ServerDeploymentConfig.fromServerDeploymentC To create a new CodeDeploy Application that deploys to a Lambda function: ```ts -import * as codedeploy from '@aws-cdk/aws-codedeploy'; - const application = new codedeploy.LambdaApplication(this, 'CodeDeployApplication', { - applicationName: 'MyApplication', // optional property + applicationName: 'MyApplication', // optional property }); ``` @@ -191,7 +190,9 @@ To import an already existing Application: ```ts const application = codedeploy.LambdaApplication.fromLambdaApplicationName( - this, 'ExistingCodeDeployApplication', 'MyExistingApplication' + this, + 'ExistingCodeDeployApplication', + 'MyExistingApplication', ); ``` @@ -204,18 +205,15 @@ When you publish a new version of the function to your stack, CodeDeploy will se To create a new CodeDeploy Deployment Group that deploys to a Lambda function: ```ts -import * as codedeploy from '@aws-cdk/aws-codedeploy'; -import * as lambda from '@aws-cdk/aws-lambda'; - -const myApplication = new codedeploy.LambdaApplication(..); -const func = new lambda.Function(..); +declare const myApplication: codedeploy.LambdaApplication; +declare const func: lambda.Function; const version = func.addVersion('1'); const version1Alias = new lambda.Alias(this, 'alias', { aliasName: 'prod', - version + version, }); -const deploymentGroup = new codedeploy.LambdaDeploymentGroup(stack, 'BlueGreenDeployment', { +const deploymentGroup = new codedeploy.LambdaDeploymentGroup(this, 'BlueGreenDeployment', { application: myApplication, // optional property: one will be created for you if not provided alias: version1Alias, deploymentConfig: codedeploy.LambdaDeploymentConfig.LINEAR_10PERCENT_EVERY_1MINUTE, @@ -237,12 +235,15 @@ you can do so with the CustomLambdaDeploymentConfig construct, letting you specify precisely how fast a new function version is deployed. ```ts -const config = new codedeploy.CustomLambdaDeploymentConfig(stack, 'CustomConfig', { +const config = new codedeploy.CustomLambdaDeploymentConfig(this, 'CustomConfig', { type: codedeploy.CustomLambdaDeploymentConfigType.CANARY, interval: Duration.minutes(1), percentage: 5, }); -const deploymentGroup = new codedeploy.LambdaDeploymentGroup(stack, 'BlueGreenDeployment', { + +declare const application: codedeploy.LambdaApplication; +declare const alias: lambda.Alias; +const deploymentGroup = new codedeploy.LambdaDeploymentGroup(this, 'BlueGreenDeployment', { application, alias, deploymentConfig: config, @@ -252,7 +253,7 @@ const deploymentGroup = new codedeploy.LambdaDeploymentGroup(stack, 'BlueGreenDe You can specify a custom name for your deployment config, but if you do you will not be able to update the interval/percentage through CDK. ```ts -const config = new codedeploy.CustomLambdaDeploymentConfig(stack, 'CustomConfig', { +const config = new codedeploy.CustomLambdaDeploymentConfig(this, 'CustomConfig', { type: codedeploy.CustomLambdaDeploymentConfigType.CANARY, interval: Duration.minutes(1), percentage: 5, @@ -265,26 +266,31 @@ const config = new codedeploy.CustomLambdaDeploymentConfig(stack, 'CustomConfig' CodeDeploy will roll back if the deployment fails. You can optionally trigger a rollback when one or more alarms are in a failed state: ```ts -const deploymentGroup = new codedeploy.LambdaDeploymentGroup(stack, 'BlueGreenDeployment', { +import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; + +declare const alias: lambda.Alias; +const alarm = new cloudwatch.Alarm(this, 'Errors', { + comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD, + threshold: 1, + evaluationPeriods: 1, + metric: alias.metricErrors(), +}); +const deploymentGroup = new codedeploy.LambdaDeploymentGroup(this, 'BlueGreenDeployment', { alias, deploymentConfig: codedeploy.LambdaDeploymentConfig.LINEAR_10PERCENT_EVERY_1MINUTE, alarms: [ // pass some alarms when constructing the deployment group - new cloudwatch.Alarm(stack, 'Errors', { - comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD, - threshold: 1, - evaluationPeriods: 1, - metric: alias.metricErrors() - }) - ] + alarm, + ], }); // or add alarms to an existing group -deploymentGroup.addAlarm(new cloudwatch.Alarm(stack, 'BlueGreenErrors', { +declare const blueGreenAlias: lambda.Alias; +deploymentGroup.addAlarm(new cloudwatch.Alarm(this, 'BlueGreenErrors', { comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD, threshold: 1, evaluationPeriods: 1, - metric: blueGreenAlias.metricErrors() + metric: blueGreenAlias.metricErrors(), })); ``` @@ -295,18 +301,19 @@ With either hook, you have the opportunity to run logic that determines whether For example, with PreTraffic hook you could run integration tests against the newly created Lambda version (but not serving traffic). With PostTraffic hook, you could run end-to-end validation checks. ```ts -const warmUpUserCache = new lambda.Function(..); -const endToEndValidation = new lambda.Function(..); +declare const warmUpUserCache: lambda.Function; +declare const endToEndValidation: lambda.Function; +declare const alias: lambda.Alias; // pass a hook whe creating the deployment group -const deploymentGroup = new codedeploy.LambdaDeploymentGroup(stack, 'BlueGreenDeployment', { +const deploymentGroup = new codedeploy.LambdaDeploymentGroup(this, 'BlueGreenDeployment', { alias: alias, deploymentConfig: codedeploy.LambdaDeploymentConfig.LINEAR_10PERCENT_EVERY_1MINUTE, preHook: warmUpUserCache, }); // or configure one on an existing deployment group -deploymentGroup.onPostHook(endToEndValidation); +deploymentGroup.addPostHook(endToEndValidation); ``` ### Import an existing Deployment Group @@ -314,8 +321,9 @@ deploymentGroup.onPostHook(endToEndValidation); To import an already existing Deployment Group: ```ts -const deploymentGroup = codedeploy.LambdaDeploymentGroup.import(this, 'ExistingCodeDeployDeploymentGroup', { - application, - deploymentGroupName: 'MyExistingDeploymentGroup', +declare const application: codedeploy.LambdaApplication; +const deploymentGroup = codedeploy.LambdaDeploymentGroup.fromLambdaDeploymentGroupAttributes(this, 'ExistingCodeDeployDeploymentGroup', { + application, + deploymentGroupName: 'MyExistingDeploymentGroup', }); ``` diff --git a/packages/@aws-cdk/aws-codedeploy/package.json b/packages/@aws-cdk/aws-codedeploy/package.json index f79f5dc10a70d..52636610a2453 100644 --- a/packages/@aws-cdk/aws-codedeploy/package.json +++ b/packages/@aws-cdk/aws-codedeploy/package.json @@ -28,7 +28,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", diff --git a/packages/@aws-cdk/aws-codedeploy/rosetta/default.ts-fixture b/packages/@aws-cdk/aws-codedeploy/rosetta/default.ts-fixture new file mode 100644 index 0000000000000..4baee63065a45 --- /dev/null +++ b/packages/@aws-cdk/aws-codedeploy/rosetta/default.ts-fixture @@ -0,0 +1,13 @@ +// Fixture with packages imported, but nothing else +import { Construct } from 'constructs'; +import { Duration, Stack } from '@aws-cdk/core'; +import * as codedeploy from '@aws-cdk/aws-codedeploy'; +import * as lambda from '@aws-cdk/aws-lambda'; + +class Fixture extends Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + + /// here + } +} diff --git a/packages/@aws-cdk/aws-codepipeline/README.md b/packages/@aws-cdk/aws-codepipeline/README.md index 976c9932ca210..b9e5e3954980e 100644 --- a/packages/@aws-cdk/aws-codepipeline/README.md +++ b/packages/@aws-cdk/aws-codepipeline/README.md @@ -16,14 +16,14 @@ To construct an empty Pipeline: ```ts -import * as codepipeline from '@aws-cdk/aws-codepipeline'; - +// Construct an empty Pipeline const pipeline = new codepipeline.Pipeline(this, 'MyFirstPipeline'); ``` To give the Pipeline a nice, human-readable name: ```ts +// Give the Pipeline a nice, human-readable name const pipeline = new codepipeline.Pipeline(this, 'MyFirstPipeline', { pipelineName: 'MyPipeline', }); @@ -40,6 +40,7 @@ the creation of the Customer Master Keys by passing `crossAccountKeys: false` when defining the Pipeline: ```ts +// Don't create Customer Master Keys const pipeline = new codepipeline.Pipeline(this, 'MyFirstPipeline', { crossAccountKeys: false, }); @@ -50,6 +51,7 @@ you can configure it by passing `enableKeyRotation: true` when creating the pipe Note that key rotation will incur an additional cost of **$1/month**. ```ts +// Enable key rotation for the generated KMS key const pipeline = new codepipeline.Pipeline(this, 'MyFirstPipeline', { // ... enableKeyRotation: true, @@ -61,6 +63,7 @@ const pipeline = new codepipeline.Pipeline(this, 'MyFirstPipeline', { You can provide Stages when creating the Pipeline: ```ts +// Provide a Stage when creating a pipeline const pipeline = new codepipeline.Pipeline(this, 'MyFirstPipeline', { stages: [ { @@ -76,6 +79,8 @@ const pipeline = new codepipeline.Pipeline(this, 'MyFirstPipeline', { Or append a Stage to an existing Pipeline: ```ts +// Append a Stage to an existing Pipeline +declare const pipeline: codepipeline.Pipeline; const sourceStage = pipeline.addStage({ stageName: 'Source', actions: [ // optional property @@ -87,12 +92,17 @@ const sourceStage = pipeline.addStage({ You can insert the new Stage at an arbitrary point in the Pipeline: ```ts +// Insert a new Stage at an arbitrary point +declare const pipeline: codepipeline.Pipeline; +declare const anotherStage: codepipeline.IStage; +declare const yetAnotherStage: codepipeline.IStage; + const someStage = pipeline.addStage({ stageName: 'SomeStage', placement: { // note: you can only specify one of the below properties rightBefore: anotherStage, - justAfter: anotherStage + justAfter: yetAnotherStage, } }); ``` @@ -106,6 +116,9 @@ in the `actions` property, or you can use the `IStage.addAction()` method to mutate an existing Stage: ```ts +// Use the `IStage.addAction()` method to mutate an existing Stage. +declare const sourceStage: codepipeline.IStage; +declare const someAction: codepipeline.Action; sourceStage.addAction(someAction); ``` @@ -114,6 +127,7 @@ sourceStage.addAction(someAction); To make your own custom CodePipeline Action requires registering the action provider. Look to the `JenkinsProvider` in `@aws-cdk/aws-codepipeline-actions` for an implementation example. ```ts +// Make a custom CodePipeline Action new codepipeline.CustomActionRegistration(this, 'GenericGitSourceProviderResource', { category: codepipeline.ActionCategory.SOURCE, artifactBounds: { minInputs: 0, maxInputs: 0, minOutputs: 1, maxOutputs: 1 }, @@ -140,7 +154,8 @@ new codepipeline.CustomActionRegistration(this, 'GenericGitSourceProviderResourc description: 'SSH git clone URL', type: 'String', }, - ] + ], +}); ``` ## Cross-account CodePipelines @@ -163,11 +178,16 @@ example, the following action deploys to an imported S3 bucket from a different account: ```ts +// Deploy an imported S3 bucket from a different account +declare const stage: codepipeline.IStage; +declare const input: codepipeline.Artifact; stage.addAction(new codepipeline_actions.S3DeployAction({ bucket: s3.Bucket.fromBucketAttributes(this, 'Bucket', { account: '123456789012', // ... }), + input: input, + actionName: 's3-deploy-action', // ... })); ``` @@ -175,8 +195,15 @@ stage.addAction(new codepipeline_actions.S3DeployAction({ Actions that don't accept a resource object accept an explicit `account` parameter: ```ts +// Actions that don't accept a resource objet accept an explicit `account` parameter +declare const stage: codepipeline.IStage; +declare const templatePath: codepipeline.ArtifactPath; stage.addAction(new codepipeline_actions.CloudFormationCreateUpdateStackAction({ account: '123456789012', + templatePath, + adminPermissions: false, + stackName: Stack.of(this).stackName, + actionName: 'cloudformation-create-update', // ... })); ``` @@ -192,7 +219,14 @@ If you do not want to use the generated role, you can also explicitly pass a account the role belongs to: ```ts +// Explicitly pass in a `role` when creating an action. +declare const stage: codepipeline.IStage; +declare const templatePath: codepipeline.ArtifactPath; stage.addAction(new codepipeline_actions.CloudFormationCreateUpdateStackAction({ + templatePath, + adminPermissions: false, + stackName: Stack.of(this).stackName, + actionName: 'cloudformation-create-update', // ... role: iam.Role.fromRoleArn(this, 'ActionRole', '...'), })); @@ -205,11 +239,16 @@ pass to actions can also be in different *Regions*. For example, the following Action deploys to an imported S3 bucket from a different Region: ```ts +// Deploy to an imported S3 bucket from a different Region. +declare const stage: codepipeline.IStage; +declare const input: codepipeline.Artifact; stage.addAction(new codepipeline_actions.S3DeployAction({ bucket: s3.Bucket.fromBucketAttributes(this, 'Bucket', { region: 'us-west-1', // ... }), + input: input, + actionName: 's3-deploy-action', // ... })); ``` @@ -218,7 +257,14 @@ Actions that don't take an AWS resource will accept an explicit `region` parameter: ```ts +// Actions that don't take an AWS resource will accept an explicit `region` parameter. +declare const stage: codepipeline.IStage; +declare const templatePath: codepipeline.ArtifactPath; stage.addAction(new codepipeline_actions.CloudFormationCreateUpdateStackAction({ + templatePath, + adminPermissions: false, + stackName: Stack.of(this).stackName, + actionName: 'cloudformation-create-update', // ... region: 'us-west-1', })); @@ -235,6 +281,7 @@ place to serve as replication buckets, you can supply these at Pipeline definiti time using the `crossRegionReplicationBuckets` parameter. Example: ```ts +// Supply replication buckets for the Pipeline instead of using the generated support stack const pipeline = new codepipeline.Pipeline(this, 'MyFirstPipeline', { // ... @@ -260,6 +307,8 @@ If you're passing a replication bucket created in a different stack, like this: ```ts +// Passing a replication bucket created in a different stack. +const app = new App(); const replicationStack = new Stack(app, 'ReplicationStack', { env: { region: 'us-west-1', @@ -273,7 +322,7 @@ const replicationBucket = new s3.Bucket(replicationStack, 'ReplicationBucket', { }); // later... -new codepipeline.Pipeline(pipelineStack, 'Pipeline', { +new codepipeline.Pipeline(replicationStack, 'Pipeline', { crossRegionReplicationBuckets: { 'us-west-1': replicationBucket, }, @@ -289,6 +338,13 @@ and so you can't reference them across environments. In this case, you need to use an alias in place of the key when creating the bucket: ```ts +// Passing an encrypted replication bucket created in a different stack. +const app = new App(); +const replicationStack = new Stack(app, 'ReplicationStack', { + env: { + region: 'us-west-1', + }, +}); const key = new kms.Key(replicationStack, 'ReplicationKey'); const alias = new kms.Alias(replicationStack, 'ReplicationAlias', { // aliasName is required @@ -312,14 +368,16 @@ you access the appropriate property of the interface returned from `variables`, which represents a single variable. Example: -```ts -// MyAction is some action type that produces variables +```ts fixture=action +// MyAction is some action type that produces variables, like EcrSourceAction const myAction = new MyAction({ // ... + actionName: 'myAction', }); new OtherAction({ // ... config: myAction.variables.myVariable, + actionName: 'otherAction', }); ``` @@ -327,10 +385,12 @@ The namespace name that will be used will be automatically generated by the pipe based on the stage and action name; you can pass a custom name when creating the action instance: -```ts +```ts fixture=action +// MyAction is some action type that produces variables, like EcrSourceAction const myAction = new MyAction({ // ... variablesNamespace: 'MyNamespace', + actionName: 'myAction', }); ``` @@ -338,10 +398,12 @@ There are also global variables available, not tied to any action; these are accessed through static properties of the `GlobalVariables` class: -```ts +```ts fixture=action +// OtherAction is some action type that produces variables, like EcrSourceAction new OtherAction({ // ... config: codepipeline.GlobalVariables.executionId, + actionName: 'otherAction', }); ``` @@ -358,6 +420,7 @@ for more details on how to use the variables feature. A pipeline can be used as a target for a CloudWatch event rule: ```ts +// A pipeline being used as a target for a CloudWatch event rule. import * as targets from '@aws-cdk/aws-events-targets'; import * as events from '@aws-cdk/aws-events'; @@ -366,6 +429,7 @@ const rule = new events.Rule(this, 'Daily', { schedule: events.Schedule.rate(Duration.days(1)), }); +declare const pipeline: codepipeline.Pipeline; rule.addTarget(new targets.CodePipeline(pipeline)); ``` @@ -380,7 +444,14 @@ the pipeline, stages or action, use the `onXxx` methods on the respective construct: ```ts -myPipeline.onStateChange('MyPipelineStateChange', target); +// Define event rules for events emitted by the pipeline +import * as events from '@aws-cdk/aws-events'; + +declare const myPipeline: codepipeline.Pipeline; +declare const myStage: codepipeline.IStage; +declare const myAction: codepipeline.Action; +declare const target: events.IRuleTarget; +myPipeline.onStateChange('MyPipelineStateChange', { target: target } ); myStage.onStateChange('MyStageStateChange', target); myAction.onStateChange('MyActionStateChange', target); ``` @@ -391,11 +462,14 @@ To define CodeStar Notification rules for Pipelines, use one of the `notifyOnXxx They are very similar to `onXxx()` methods for CloudWatch events: ```ts -const target = new chatbot.SlackChannelConfiguration(stack, 'MySlackChannel', { +// Define CodeStar Notification rules for Pipelines +import * as chatbot from '@aws-cdk/aws-chatbot'; +const target = new chatbot.SlackChannelConfiguration(this, 'MySlackChannel', { slackChannelConfigurationName: 'YOUR_CHANNEL_NAME', slackWorkspaceId: 'YOUR_SLACK_WORKSPACE_ID', slackChannelId: 'YOUR_SLACK_CHANNEL_ID', }); +declare const pipeline: codepipeline.Pipeline; const rule = pipeline.notifyOnExecutionStateChange('NotifyOnExecutionStateChange', target); ``` diff --git a/packages/@aws-cdk/aws-codepipeline/lib/custom-action-registration.ts b/packages/@aws-cdk/aws-codepipeline/lib/custom-action-registration.ts index f32a952a24d83..207d69d02e268 100644 --- a/packages/@aws-cdk/aws-codepipeline/lib/custom-action-registration.ts +++ b/packages/@aws-cdk/aws-codepipeline/lib/custom-action-registration.ts @@ -79,7 +79,7 @@ export interface CustomActionRegistrationProps { /** * The provider of the Action. - * @example 'MyCustomActionProvider' + * For example, `'MyCustomActionProvider'` */ readonly provider: string; diff --git a/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts b/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts index f453b0c5748ae..22c1a47856ada 100644 --- a/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts +++ b/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts @@ -263,15 +263,19 @@ abstract class PipelineBase extends Resource implements IPipeline { * * @example * // create a pipeline - * const pipeline = new Pipeline(this, 'Pipeline'); + * import * as codecommit from '@aws-cdk/aws-codecommit'; + * + * const pipeline = new codepipeline.Pipeline(this, 'Pipeline'); * * // add a stage * const sourceStage = pipeline.addStage({ stageName: 'Source' }); * * // add a source action to the stage + * declare const repo: codecommit.Repository; + * declare const sourceArtifact: codepipeline.Artifact; * sourceStage.addAction(new codepipeline_actions.CodeCommitSourceAction({ * actionName: 'Source', - * outputArtifactName: 'SourceArtifact', + * output: sourceArtifact, * repository: repo, * })); * diff --git a/packages/@aws-cdk/aws-codepipeline/package.json b/packages/@aws-cdk/aws-codepipeline/package.json index 24dec6803d121..84f28a8804685 100644 --- a/packages/@aws-cdk/aws-codepipeline/package.json +++ b/packages/@aws-cdk/aws-codepipeline/package.json @@ -28,7 +28,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", diff --git a/packages/@aws-cdk/aws-codepipeline/rosetta/action.ts-fixture b/packages/@aws-cdk/aws-codepipeline/rosetta/action.ts-fixture new file mode 100644 index 0000000000000..43b0b75b3788e --- /dev/null +++ b/packages/@aws-cdk/aws-codepipeline/rosetta/action.ts-fixture @@ -0,0 +1,61 @@ +import { Construct } from 'constructs'; +import { Duration, Stack } from '@aws-cdk/core'; +import * as codepipeline from '@aws-cdk/aws-codepipeline'; + +interface MyActionProps { + variablesNamespace?: string; + actionName: string; +} + +class MyAction extends codepipeline.Action { + public variables: { [key: string]: string }; + protected readonly providedActionProperties: codepipeline.ActionProperties; + + constructor(props: MyActionProps) { + super(); + this.providedActionProperties = { + ...props, + category: codepipeline.ActionCategory.SOURCE, + provider: 'Fake', + artifactBounds: { minInputs: 0, maxInputs: 0, minOutputs: 1, maxOutputs: 4 }, + }; + this.variables = { 'myVariable': 'var' }; + } + + public bound(_scope: Construct, _stage: codepipeline.IStage, _options: codepipeline.ActionBindOptions): + codepipeline.ActionConfig { + return {}; + } +} + +interface OtherActionProps { + config: string; + actionName: string; +} + +class OtherAction extends codepipeline.Action { + protected readonly providedActionProperties: codepipeline.ActionProperties; + + constructor(props: OtherActionProps) { + super(); + this.providedActionProperties = { + ...props, + category: codepipeline.ActionCategory.SOURCE, + provider: 'Fake', + artifactBounds: { minInputs: 0, maxInputs: 0, minOutputs: 1, maxOutputs: 4 }, + }; + } + + public bound(_scope: Construct, _stage: codepipeline.IStage, _options: codepipeline.ActionBindOptions): + codepipeline.ActionConfig { + return {}; + } +} + +class Context extends Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + + /// here + } +} diff --git a/packages/@aws-cdk/aws-codepipeline/rosetta/default.ts-fixture b/packages/@aws-cdk/aws-codepipeline/rosetta/default.ts-fixture new file mode 100644 index 0000000000000..b46720fa572c4 --- /dev/null +++ b/packages/@aws-cdk/aws-codepipeline/rosetta/default.ts-fixture @@ -0,0 +1,15 @@ +import { Construct } from 'constructs'; +import { App, Duration, PhysicalName, Stack } from '@aws-cdk/core'; +import * as codepipeline from '@aws-cdk/aws-codepipeline'; +import * as codepipeline_actions from '@aws-cdk/aws-codepipeline-actions'; +import * as s3 from '@aws-cdk/aws-s3'; +import * as iam from '@aws-cdk/aws-iam'; +import * as kms from '@aws-cdk/aws-kms'; + +class Context extends Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + + /// here + } +} diff --git a/packages/@aws-cdk/aws-cognito/README.md b/packages/@aws-cdk/aws-cognito/README.md index 2315662f49d10..906480586b769 100644 --- a/packages/@aws-cdk/aws-cognito/README.md +++ b/packages/@aws-cdk/aws-cognito/README.md @@ -314,29 +314,55 @@ new cognito.UserPool(this, 'UserPool', { The default for account recovery is by phone if available and by email otherwise. A user will not be allowed to reset their password via phone if they are also using it for MFA. + ### Emails Cognito sends emails to users in the user pool, when particular actions take place, such as welcome emails, invitation emails, password resets, etc. The address from which these emails are sent can be configured on the user pool. -Read more about [email settings here](https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-email.html). +Read more at [Email settings for User Pools](https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-email.html). + +By default, user pools are configured to use Cognito's built in email capability, which will send emails +from `no-reply@verificationemail.com`. If you want to use a custom email address you can configure +Cognito to send emails through Amazon SES, which is detailed below. ```ts new cognito.UserPool(this, 'myuserpool', { - // ... - emailSettings: { - from: 'noreply@myawesomeapp.com', + email: UserPoolEmail.withCognito('support@myawesomeapp.com'), +}); +``` + +For typical production environments, the default email limit is below the required delivery volume. +To enable a higher delivery volume, you can configure the UserPool to send emails through Amazon SES. To do +so, follow the steps in the [Cognito Developer Guide](https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-email.html#user-pool-email-developer) +to verify an email address, move the account out of the SES sandbox, and grant Cognito email permissions via an +authorization policy. + +Once the SES setup is complete, the UserPool can be configured to use the SES email. + +```ts +new cognito.UserPool(this, 'myuserpool', { + email: UserPoolEmail.withSES({ + fromEmail: 'noreply@myawesomeapp.com', + fromName: 'Awesome App', replyTo: 'support@myawesomeapp.com', - }, + }), }); ``` -By default, user pools are configured to use Cognito's built-in email capability, but it can also be configured to use -Amazon SES, however, support for Amazon SES is not available in the CDK yet. If you would like this to be implemented, -give [this issue](https://github.com/aws/aws-cdk/issues/6768) a +1. Until then, you can use the [cfn -layer](https://docs.aws.amazon.com/cdk/latest/guide/cfn_layer.html) to configure this. +Sending emails through SES requires that SES be configured (as described above) in one of the regions - `us-east-1`, `us-west-1`, or `eu-west-1`. +If the UserPool is being created in a different region, `sesRegion` must be used to specify the correct SES region. + +```ts +new cognito.UserPool(this, 'myuserpool', { + email: UserPoolEmail.withSES({ + sesRegion: 'us-east-1', + fromEmail: 'noreply@myawesomeapp.com', + fromName: 'Awesome App', + replyTo: 'support@myawesomeapp.com', + }), +}); -If an email address contains non-ASCII characters, it will be encoded using the [punycode -encoding](https://en.wikipedia.org/wiki/Punycode) when generating the template for Cloudformation. +``` ### Device Tracking diff --git a/packages/@aws-cdk/aws-cognito/lib/index.ts b/packages/@aws-cdk/aws-cognito/lib/index.ts index cab56671c2b9e..7d5ce97fc2c76 100644 --- a/packages/@aws-cdk/aws-cognito/lib/index.ts +++ b/packages/@aws-cdk/aws-cognito/lib/index.ts @@ -4,6 +4,7 @@ export * from './user-pool'; export * from './user-pool-attr'; export * from './user-pool-client'; export * from './user-pool-domain'; +export * from './user-pool-email'; export * from './user-pool-idp'; export * from './user-pool-idps'; export * from './user-pool-resource-server'; diff --git a/packages/@aws-cdk/aws-cognito/lib/user-pool-email.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool-email.ts new file mode 100644 index 0000000000000..2d5b8af06447f --- /dev/null +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool-email.ts @@ -0,0 +1,203 @@ +import { Stack, Token } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { toASCII as punycodeEncode } from 'punycode/'; + +/** + * The valid Amazon SES configuration regions + */ +const REGIONS = ['us-east-1', 'us-west-2', 'eu-west-1']; + +/** + * Configuration for Cognito sending emails via Amazon SES + */ +export interface UserPoolSESOptions { + /** + * The verified Amazon SES email address that Cognito should + * use to send emails. + * + * The email address used must be a verified email address + * in Amazon SES and must be configured to allow Cognito to + * send emails. + * + * @see https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-email.html + */ + readonly fromEmail: string; + + /** + * An optional name that should be used as the sender's name + * along with the email. + * + * @default - no name + */ + readonly fromName?: string; + + /** + * The destination to which the receiver of the email should reploy to. + * + * @default - same as the fromEmail + */ + readonly replyTo?: string; + + /** + * The name of a configuration set in Amazon SES that should + * be applied to emails sent via Cognito. + * + * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cognito-userpool-emailconfiguration.html#cfn-cognito-userpool-emailconfiguration-configurationset + * + * @default - no configuration set + */ + readonly configurationSetName?: string; + + /** + * Required if the UserPool region is different than the SES region. + * + * If sending emails with a Amazon SES verified email address, + * and the region that SES is configured is different than the + * region in which the UserPool is deployed, you must specify that + * region here. + * + * Must be 'us-east-1', 'us-west-2', or 'eu-west-1' + * + * @default - The same region as the Cognito UserPool + */ + readonly sesRegion?: string; +} + +/** + * Result of binding email settings with a user pool + */ +interface UserPoolEmailConfig { + /** + * The name of the configuration set in SES. + * + * @default - none + */ + readonly configurationSet?: string; + + /** + * Specifies whether to use Cognito's built in email functionality + * or SES. + * + * @default - Cognito built in email functionality + */ + readonly emailSendingAccount?: string; + + /** + * Identifies either the sender's email address or the sender's + * name with their email address. + * + * If emailSendingAccount is DEVELOPER then this cannot be specified. + * + * @default 'no-reply@verificationemail.com' + */ + readonly from?: string; + + /** + * The destination to which the receiver of the email should reply to. + * + * @default - same as `from` + */ + readonly replyToEmailAddress?: string; + + /** + * The ARN of a verified email address in Amazon SES. + * + * required if emailSendingAccount is DEVELOPER or if + * 'from' is provided. + * + * @default - none + */ + readonly sourceArn?: string; +} + +/** + * Configure how Cognito sends emails + */ +export abstract class UserPoolEmail { + /** + * Send email using Cognito + */ + public static withCognito(replyTo?: string): UserPoolEmail { + return new CognitoEmail(replyTo); + } + + /** + * Send email using SES + */ + public static withSES(options: UserPoolSESOptions): UserPoolEmail { + return new SESEmail(options); + } + + + /** + * Returns the email configuration for a Cognito UserPool + * that controls how Cognito will send emails + * @internal + */ + public abstract _bind(scope: Construct): UserPoolEmailConfig; + +} + +class CognitoEmail extends UserPoolEmail { + constructor(private readonly replyTo?: string) { + super(); + } + + public _bind(_scope: Construct): UserPoolEmailConfig { + return { + replyToEmailAddress: encodeAndTest(this.replyTo), + emailSendingAccount: 'COGNITO_DEFAULT', + }; + + } +} + +class SESEmail extends UserPoolEmail { + constructor(private readonly options: UserPoolSESOptions) { + super(); + } + + public _bind(scope: Construct): UserPoolEmailConfig { + const region = Stack.of(scope).region; + + if (Token.isUnresolved(region) && !this.options.sesRegion) { + throw new Error('Your stack region cannot be determined so "sesRegion" is required in SESOptions'); + } + + if (this.options.sesRegion && !REGIONS.includes(this.options.sesRegion)) { + throw new Error(`sesRegion must be one of 'us-east-1', 'us-west-2', 'eu-west-1'. received ${this.options.sesRegion}`); + } else if (!this.options.sesRegion && !REGIONS.includes(region)) { + throw new Error(`Your stack is in ${region}, which is not a SES Region. Please provide a valid value for 'sesRegion'`); + } + + let from = this.options.fromEmail; + if (this.options.fromName) { + from = `${this.options.fromName} <${this.options.fromEmail}>`; + } + + return { + from: encodeAndTest(from), + replyToEmailAddress: encodeAndTest(this.options.replyTo), + configurationSet: this.options.configurationSetName, + emailSendingAccount: 'DEVELOPER', + sourceArn: Stack.of(scope).formatArn({ + service: 'ses', + resource: 'identity', + resourceName: encodeAndTest(this.options.fromEmail), + region: this.options.sesRegion ?? region, + }), + }; + } +} + +function encodeAndTest(input: string | undefined): string | undefined { + if (input) { + const local = input.split('@')[0]; + if (!/[\p{ASCII}]+/u.test(local)) { + throw new Error('the local part of the email address must use ASCII characters only'); + } + return punycodeEncode(input); + } else { + return undefined; + } +} diff --git a/packages/@aws-cdk/aws-cognito/lib/user-pool.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool.ts index adc3f51bf37a2..836537ef6c465 100644 --- a/packages/@aws-cdk/aws-cognito/lib/user-pool.ts +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool.ts @@ -8,6 +8,7 @@ import { StandardAttributeNames } from './private/attr-names'; import { ICustomAttribute, StandardAttribute, StandardAttributes } from './user-pool-attr'; import { UserPoolClient, UserPoolClientOptions } from './user-pool-client'; import { UserPoolDomain, UserPoolDomainOptions } from './user-pool-domain'; +import { UserPoolEmail } from './user-pool-email'; import { IUserPoolIdentityProvider } from './user-pool-idp'; import { UserPoolResourceServer, UserPoolResourceServerOptions } from './user-pool-resource-server'; @@ -570,10 +571,18 @@ export interface UserPoolProps { /** * Email settings for a user pool. + * * @default - see defaults on each property of EmailSettings. + * @deprecated Use 'email' instead. */ readonly emailSettings?: EmailSettings; + /** + * Email settings for a user pool. + * @default - cognito will use the default email configuration + */ + readonly email?: UserPoolEmail; + /** * Lambda functions to use for supported Cognito triggers. * @see https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-identity-pools-working-with-aws-lambda-triggers.html @@ -788,6 +797,14 @@ export class UserPool extends UserPoolBase { const passwordPolicy = this.configurePasswordPolicy(props); + if (props.email && props.emailSettings) { + throw new Error('you must either provide "email" or "emailSettings", but not both'); + } + const emailConfiguration = props.email ? props.email._bind(this) : undefinedIfNoKeys({ + from: encodePuny(props.emailSettings?.from), + replyToEmailAddress: encodePuny(props.emailSettings?.replyTo), + }); + const userPool = new CfnUserPool(this, 'Resource', { userPoolName: props.userPoolName, usernameAttributes: signIn.usernameAttrs, @@ -805,10 +822,7 @@ export class UserPool extends UserPoolBase { mfaConfiguration: props.mfa, enabledMfas: this.mfaConfiguration(props), policies: passwordPolicy !== undefined ? { passwordPolicy } : undefined, - emailConfiguration: undefinedIfNoKeys({ - from: encodePuny(props.emailSettings?.from), - replyToEmailAddress: encodePuny(props.emailSettings?.replyTo), - }), + emailConfiguration, usernameConfiguration: undefinedIfNoKeys({ caseSensitive: props.signInCaseSensitive, }), diff --git a/packages/@aws-cdk/aws-cognito/test/user-pool.test.ts b/packages/@aws-cdk/aws-cognito/test/user-pool.test.ts index 5aa6c48ee99fc..c499335c7ba9c 100644 --- a/packages/@aws-cdk/aws-cognito/test/user-pool.test.ts +++ b/packages/@aws-cdk/aws-cognito/test/user-pool.test.ts @@ -3,7 +3,7 @@ import { Role, ServicePrincipal } from '@aws-cdk/aws-iam'; import * as lambda from '@aws-cdk/aws-lambda'; import { CfnParameter, Duration, Stack, Tags } from '@aws-cdk/core'; import { Construct } from 'constructs'; -import { AccountRecovery, Mfa, NumberAttribute, StringAttribute, UserPool, UserPoolIdentityProvider, UserPoolOperation, VerificationEmailStyle } from '../lib'; +import { AccountRecovery, Mfa, NumberAttribute, StringAttribute, UserPool, UserPoolIdentityProvider, UserPoolOperation, VerificationEmailStyle, UserPoolEmail } from '../lib'; describe('User Pool', () => { test('default setup', () => { @@ -1388,6 +1388,285 @@ describe('User Pool', () => { }, }); }); + + test('email transmission with cyrillic characters in the domain are encoded', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + new UserPool(stack, 'Pool', { + email: UserPoolEmail.withSES({ + sesRegion: 'us-east-1', + fromEmail: 'user@домен.рф', + replyTo: 'user@домен.рф', + }), + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::Cognito::UserPool', { + EmailConfiguration: { + From: 'user@xn--d1acufc.xn--p1ai', + ReplyToEmailAddress: 'user@xn--d1acufc.xn--p1ai', + }, + }); + }); + + test('email transmission with cyrillic characters in the local part throw error', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + expect(() => new UserPool(stack, 'Pool', { + email: UserPoolEmail.withSES({ + sesRegion: 'us-east-1', + fromEmail: 'от@домен.рф', + replyTo: 'user@домен.рф', + }), + })).toThrow(/the local part of the email address must use ASCII characters only/); + }); + + test('email transmission with cyrillic characters in the local part of replyTo throw error', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + expect(() => new UserPool(stack, 'Pool', { + email: UserPoolEmail.withSES({ + sesRegion: 'us-east-1', + fromEmail: 'user@домен.рф', + replyTo: 'от@домен.рф', + }), + })).toThrow(/the local part of the email address must use ASCII characters only/); + }); + + test('email withCognito transmission with cyrillic characters in the local part of replyTo throw error', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + expect(() => new UserPool(stack, 'Pool', { + email: UserPoolEmail.withCognito('от@домен.рф'), + })).toThrow(/the local part of the email address must use ASCII characters only/); + }); + + test('email withCognito', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + new UserPool(stack, 'Pool', { + email: UserPoolEmail.withCognito(), + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::Cognito::UserPool', { + EmailConfiguration: { + EmailSendingAccount: 'COGNITO_DEFAULT', + }, + }); + }); + + test('email withCognito and replyTo', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + new UserPool(stack, 'Pool', { + email: UserPoolEmail.withCognito('reply@example.com'), + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::Cognito::UserPool', { + EmailConfiguration: { + EmailSendingAccount: 'COGNITO_DEFAULT', + ReplyToEmailAddress: 'reply@example.com', + }, + }); + }); + + test('email withSES with custom email and no region', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + expect(() => new UserPool(stack, 'Pool', { + email: UserPoolEmail.withSES({ + fromEmail: 'mycustomemail@example.com', + replyTo: 'reply@example.com', + }), + })).toThrow(/Your stack region cannot be determined/); + + }); + + test('email withSES with no name', () => { + // GIVEN + const stack = new Stack(undefined, undefined, { + env: { + region: 'us-east-1', + account: '11111111111', + }, + }); + + // WHEN + new UserPool(stack, 'Pool', { + email: UserPoolEmail.withSES({ + fromEmail: 'mycustomemail@example.com', + replyTo: 'reply@example.com', + configurationSetName: 'default', + }), + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::Cognito::UserPool', { + EmailConfiguration: { + EmailSendingAccount: 'DEVELOPER', + From: 'mycustomemail@example.com', + ReplyToEmailAddress: 'reply@example.com', + ConfigurationSet: 'default', + SourceArn: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':ses:us-east-1:11111111111:identity/mycustomemail@example.com', + ], + ], + }, + }, + }); + + }); + + test('email withSES', () => { + // GIVEN + const stack = new Stack(undefined, undefined, { + env: { + region: 'us-east-1', + account: '11111111111', + }, + }); + + // WHEN + new UserPool(stack, 'Pool', { + email: UserPoolEmail.withSES({ + fromEmail: 'mycustomemail@example.com', + fromName: 'My Custom Email', + replyTo: 'reply@example.com', + configurationSetName: 'default', + }), + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::Cognito::UserPool', { + EmailConfiguration: { + EmailSendingAccount: 'DEVELOPER', + From: 'My Custom Email ', + ReplyToEmailAddress: 'reply@example.com', + ConfigurationSet: 'default', + SourceArn: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':ses:us-east-1:11111111111:identity/mycustomemail@example.com', + ], + ], + }, + }, + }); + + }); + + test('email withSES with valid region', () => { + // GIVEN + const stack = new Stack(undefined, undefined, { + env: { + region: 'us-east-2', + account: '11111111111', + }, + }); + + // WHEN + new UserPool(stack, 'Pool', { + email: UserPoolEmail.withSES({ + fromEmail: 'mycustomemail@example.com', + fromName: 'My Custom Email', + sesRegion: 'us-east-1', + replyTo: 'reply@example.com', + configurationSetName: 'default', + }), + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::Cognito::UserPool', { + EmailConfiguration: { + EmailSendingAccount: 'DEVELOPER', + From: 'My Custom Email ', + ReplyToEmailAddress: 'reply@example.com', + ConfigurationSet: 'default', + SourceArn: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':ses:us-east-1:11111111111:identity/mycustomemail@example.com', + ], + ], + }, + }, + }); + + }); + test('email withSES invalid region throws error', () => { + // GIVEN + const stack = new Stack(undefined, undefined, { + env: { + region: 'us-east-2', + account: '11111111111', + }, + }); + + // WHEN + expect(() => new UserPool(stack, 'Pool', { + email: UserPoolEmail.withSES({ + fromEmail: 'mycustomemail@example.com', + fromName: 'My Custom Email', + replyTo: 'reply@example.com', + configurationSetName: 'default', + }), + })).toThrow(/Please provide a valid value/); + + }); + + test('email withSES invalid sesRegion throws error', () => { + // GIVEN + const stack = new Stack(undefined, undefined, { + env: { + account: '11111111111', + }, + }); + + // WHEN + expect(() => new UserPool(stack, 'Pool', { + email: UserPoolEmail.withSES({ + sesRegion: 'us-east-2', + fromEmail: 'mycustomemail@example.com', + fromName: 'My Custom Email', + replyTo: 'reply@example.com', + configurationSetName: 'default', + }), + })).toThrow(/sesRegion must be one of/); + + }); }); test('device tracking is configured correctly', () => { diff --git a/packages/@aws-cdk/aws-ec2/lib/instance-types.ts b/packages/@aws-cdk/aws-ec2/lib/instance-types.ts index 1fc4e02f25daa..ec5f2b91e17e7 100644 --- a/packages/@aws-cdk/aws-ec2/lib/instance-types.ts +++ b/packages/@aws-cdk/aws-ec2/lib/instance-types.ts @@ -68,6 +68,26 @@ export enum InstanceClass { */ M5AD = 'm5ad', + /** + * Standard instances for high performance computing, 5th generation + */ + STANDARD5_HIGH_PERFORMANCE = 'm5n', + + /** + * Standard instances for high performance computing, 5th generation + */ + M5N = 'm5n', + + /** + * Standard instances with local NVME drive for high performance computing, 5th generation + */ + STANDARD5_NVME_DRIVE_HIGH_PERFORMANCE = 'm5dn', + + /** + * Standard instances with local NVME drive for high performance computing, 5th generation + */ + M5DN = 'm5dn', + /** * Memory optimized instances, 3rd generation */ @@ -446,6 +466,16 @@ export enum InstanceClass { */ G4DN = 'g4dn', + /** + * Graphics-optimized instances, 5th generation + */ + GRAPHICS5 = 'g5', + + /** + * Graphics-optimized instances, 5th generation + */ + G5 = 'g5', + /** * Parallel-processing optimized instances, 2nd generation */ @@ -646,6 +676,11 @@ export enum InstanceSize { */ XLARGE32 = '32xlarge', + /** + * Instance size XLARGE48 (48xlarge) + */ + XLARGE48 = '48xlarge', + /** * Instance size METAL (metal) */ diff --git a/packages/@aws-cdk/aws-ec2/lib/nat.ts b/packages/@aws-cdk/aws-ec2/lib/nat.ts index 4743799762b51..f4ce8aa242053 100644 --- a/packages/@aws-cdk/aws-ec2/lib/nat.ts +++ b/packages/@aws-cdk/aws-ec2/lib/nat.ts @@ -233,10 +233,8 @@ class NatGatewayProvider extends NatProvider { // Create the NAT gateways let i = 0; for (const sub of options.natSubnets) { - const gateway = sub.addNatGateway(); - if (this.props.eipAllocationIds) { - gateway.allocationId = pickN(i, this.props.eipAllocationIds); - } + const eipAllocationId = this.props.eipAllocationIds ? pickN(i, this.props.eipAllocationIds) : undefined; + const gateway = sub.addNatGateway(eipAllocationId); this.gateways.add(sub.availabilityZone, gateway.ref); i++; } diff --git a/packages/@aws-cdk/aws-ec2/lib/vpc.ts b/packages/@aws-cdk/aws-ec2/lib/vpc.ts index cd70f71582718..6d1722b3135d8 100644 --- a/packages/@aws-cdk/aws-ec2/lib/vpc.ts +++ b/packages/@aws-cdk/aws-ec2/lib/vpc.ts @@ -1840,11 +1840,11 @@ export class PublicSubnet extends Subnet implements IPublicSubnet { * Also adds the EIP for the managed NAT. * @returns A ref to the the NAT Gateway ID */ - public addNatGateway() { + public addNatGateway(eipAllocationId?: string) { // Create a NAT Gateway in this public subnet const ngw = new CfnNatGateway(this, 'NATGateway', { subnetId: this.subnetId, - allocationId: new CfnEIP(this, 'EIP', { + allocationId: eipAllocationId ?? new CfnEIP(this, 'EIP', { domain: 'vpc', }).attrAllocationId, }); diff --git a/packages/@aws-cdk/aws-ec2/test/vpc.test.ts b/packages/@aws-cdk/aws-ec2/test/vpc.test.ts index eedc950ab584b..433f1355bd72b 100644 --- a/packages/@aws-cdk/aws-ec2/test/vpc.test.ts +++ b/packages/@aws-cdk/aws-ec2/test/vpc.test.ts @@ -585,6 +585,31 @@ describe('vpc', () => { }); + test('EIP passed with NAT gateway does not create duplicate EIP', () => { + const stack = getTestStack(); + new Vpc(stack, 'VPC', { + cidr: '10.0.0.0/16', + subnetConfiguration: [ + { + cidrMask: 24, + name: 'ingress', + subnetType: SubnetType.PUBLIC, + }, + { + cidrMask: 24, + name: 'application', + subnetType: SubnetType.PRIVATE_WITH_NAT, + }, + ], + natGatewayProvider: NatProvider.gateway({ eipAllocationIds: ['b'] }), + natGateways: 1, + }); + expect(stack).toCountResources('AWS::EC2::EIP', 0); + expect(stack).toHaveResource('AWS::EC2::NatGateway', { + AllocationId: 'b', + }); + }); + test('with mis-matched nat and subnet configs it throws', () => { const stack = getTestStack(); expect(() => new Vpc(stack, 'VPC', { diff --git a/packages/@aws-cdk/aws-ecr-assets/lib/image-asset.ts b/packages/@aws-cdk/aws-ecr-assets/lib/image-asset.ts index e579baf55ac41..78e706587dd16 100644 --- a/packages/@aws-cdk/aws-ecr-assets/lib/image-asset.ts +++ b/packages/@aws-cdk/aws-ecr-assets/lib/image-asset.ts @@ -1,7 +1,7 @@ import * as fs from 'fs'; import * as path from 'path'; import * as ecr from '@aws-cdk/aws-ecr'; -import { Annotations, AssetStaging, FeatureFlags, FileFingerprintOptions, IgnoreMode, Stack, SymlinkFollowMode, Token, Stage } from '@aws-cdk/core'; +import { Annotations, AssetStaging, FeatureFlags, FileFingerprintOptions, IgnoreMode, Stack, SymlinkFollowMode, Token, Stage, CfnResource } from '@aws-cdk/core'; import * as cxapi from '@aws-cdk/cx-api'; import { Construct } from 'constructs'; @@ -147,6 +147,30 @@ export class DockerImageAsset extends CoreConstruct implements IAsset { */ public readonly assetHash: string; + /** + * The path to the asset, relative to the current Cloud Assembly + * + * If asset staging is disabled, this will just be the original path. + * + * If asset staging is enabled it will be the staged path. + */ + private readonly assetPath: string; + + /** + * The path to the Dockerfile, relative to the assetPath + */ + private readonly dockerfilePath?: string; + + /** + * Build args to pass to the `docker build` command. + */ + private readonly dockerBuildArgs?: { [key: string]: string }; + + /** + * Docker target to build to + */ + private readonly dockerBuildTarget?: string; + constructor(scope: Construct, id: string, props: DockerImageAssetProps) { super(scope, id); @@ -160,7 +184,8 @@ export class DockerImageAsset extends CoreConstruct implements IAsset { } // validate the docker file exists - const file = path.join(dir, props.file || 'Dockerfile'); + this.dockerfilePath = props.file || 'Dockerfile'; + const file = path.join(dir, this.dockerfilePath); if (!fs.existsSync(file)) { throw new Error(`Cannot find file at ${file}`); } @@ -223,10 +248,14 @@ export class DockerImageAsset extends CoreConstruct implements IAsset { this.assetHash = staging.assetHash; const stack = Stack.of(this); + this.assetPath = staging.relativeStagedPath(stack); + this.dockerBuildArgs = props.buildArgs; + this.dockerBuildTarget = props.target; + const location = stack.synthesizer.addDockerImageAsset({ - directoryName: staging.relativeStagedPath(stack), - dockerBuildArgs: props.buildArgs, - dockerBuildTarget: props.target, + directoryName: this.assetPath, + dockerBuildArgs: this.dockerBuildArgs, + dockerBuildTarget: this.dockerBuildTarget, dockerFile: props.file, sourceHash: staging.assetHash, }); @@ -234,6 +263,38 @@ export class DockerImageAsset extends CoreConstruct implements IAsset { this.repository = ecr.Repository.fromRepositoryName(this, 'Repository', location.repositoryName); this.imageUri = location.imageUri; } + + /** + * Adds CloudFormation template metadata to the specified resource with + * information that indicates which resource property is mapped to this local + * asset. This can be used by tools such as SAM CLI to provide local + * experience such as local invocation and debugging of Lambda functions. + * + * Asset metadata will only be included if the stack is synthesized with the + * "aws:cdk:enable-asset-metadata" context key defined, which is the default + * behavior when synthesizing via the CDK Toolkit. + * + * @see https://github.com/aws/aws-cdk/issues/1432 + * + * @param resource The CloudFormation resource which is using this asset [disable-awslint:ref-via-interface] + * @param resourceProperty The property name where this asset is referenced + */ + public addResourceMetadata(resource: CfnResource, resourceProperty: string) { + if (!this.node.tryGetContext(cxapi.ASSET_RESOURCE_METADATA_ENABLED_CONTEXT)) { + return; // not enabled + } + + // tell tools such as SAM CLI that the resourceProperty of this resource + // points to a local path and include the path to de dockerfile, docker build args, and target, + // in order to enable local invocation of this function. + resource.cfnOptions.metadata = resource.cfnOptions.metadata || { }; + resource.cfnOptions.metadata[cxapi.ASSET_RESOURCE_METADATA_PATH_KEY] = this.assetPath; + resource.cfnOptions.metadata[cxapi.ASSET_RESOURCE_METADATA_DOCKERFILE_PATH_KEY] = this.dockerfilePath; + resource.cfnOptions.metadata[cxapi.ASSET_RESOURCE_METADATA_DOCKER_BUILD_ARGS_KEY] = this.dockerBuildArgs; + resource.cfnOptions.metadata[cxapi.ASSET_RESOURCE_METADATA_DOCKER_BUILD_TARGET_KEY] = this.dockerBuildTarget; + resource.cfnOptions.metadata[cxapi.ASSET_RESOURCE_METADATA_PROPERTY_KEY] = resourceProperty; + } + } function validateProps(props: DockerImageAssetProps) { diff --git a/packages/@aws-cdk/aws-ecs/README.md b/packages/@aws-cdk/aws-ecs/README.md index 6a27855e031f5..2c4f04e64b257 100644 --- a/packages/@aws-cdk/aws-ecs/README.md +++ b/packages/@aws-cdk/aws-ecs/README.md @@ -427,6 +427,25 @@ The task execution role is automatically granted read permissions on the secrets files is restricted to the EC2 launch type for files hosted on S3. Further details provided in the AWS documentation about [specifying environment variables](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/taskdef-envfiles.html). +### System controls + +To set system controls (kernel parameters) on the container, use the `systemControls` prop: + +```ts +declare const taskDefinition: ecs.TaskDefinition; + +taskDefinition.addContainer('container', { + image: ecs.ContainerImage.fromRegistry("amazon/amazon-ecs-sample"), + memoryLimitMiB: 1024, + systemControls: [ + { + namespace: 'net', + value: 'ipv4.tcp_tw_recycle', + }, + ], +}); +``` + ## Service A `Service` instantiates a `TaskDefinition` on a `Cluster` a given number of diff --git a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts index f817a5280315e..625041468a0a4 100644 --- a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts @@ -307,6 +307,15 @@ export interface ContainerDefinitionOptions { * @default - No inference accelerators assigned. */ readonly inferenceAcceleratorResources?: string[]; + + /** + * A list of namespaced kernel parameters to set in the container. + * + * @default - No system controls are set. + * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ecs-taskdefinition-systemcontrol.html + * @see https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task_definition_parameters.html#container_definition_systemcontrols + */ + readonly systemControls?: SystemControl[]; } /** @@ -669,6 +678,7 @@ export class ContainerDefinition extends CoreConstruct { linuxParameters: this.linuxParameters && this.linuxParameters.renderLinuxParameters(), resourceRequirements: (!this.props.gpuCount && this.inferenceAcceleratorResources.length == 0 ) ? undefined : renderResourceRequirements(this.props.gpuCount, this.inferenceAcceleratorResources), + systemControls: this.props.systemControls && renderSystemControls(this.props.systemControls), }; } } @@ -1040,3 +1050,25 @@ function renderVolumeFrom(vf: VolumeFrom): CfnTaskDefinition.VolumeFromProperty readOnly: vf.readOnly, }; } + +/** + * Kernel parameters to set in the container + */ +export interface SystemControl { + /** + * The namespaced kernel parameter for which to set a value. + */ + readonly namespace: string; + + /** + * The value for the namespaced kernel parameter specified in namespace. + */ + readonly value: string; +} + +function renderSystemControls(systemControls: SystemControl[]): CfnTaskDefinition.SystemControlProperty[] { + return systemControls.map(sc => ({ + namespace: sc.namespace, + value: sc.value, + })); +} diff --git a/packages/@aws-cdk/aws-ecs/test/container-definition.test.ts b/packages/@aws-cdk/aws-ecs/test/container-definition.test.ts index a5153b82d331a..a384294900342 100644 --- a/packages/@aws-cdk/aws-ecs/test/container-definition.test.ts +++ b/packages/@aws-cdk/aws-ecs/test/container-definition.test.ts @@ -85,6 +85,9 @@ describe('container definition', () => { secrets: { SECRET: ecs.Secret.fromSecretsManager(secret), }, + systemControls: [ + { namespace: 'SomeNamespace', value: 'SomeValue' }, + ], }); // THEN @@ -218,6 +221,12 @@ describe('container definition', () => { ], StartTimeout: 2, StopTimeout: 5, + SystemControls: [ + { + Namespace: 'SomeNamespace', + Value: 'SomeValue', + }, + ], User: 'rootUser', WorkingDirectory: 'a/b/c', }, @@ -753,6 +762,40 @@ describe('container definition', () => { }); }); + test('can specify system controls', () => { + // GIVEN + const stack = new cdk.Stack(); + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'TaskDef'); + + // WHEN + taskDefinition.addContainer('cont', { + image: ecs.ContainerImage.fromRegistry('test'), + memoryLimitMiB: 1024, + systemControls: [ + { namespace: 'SomeNamespace1', value: 'SomeValue1' }, + { namespace: 'SomeNamespace2', value: 'SomeValue2' }, + ], + }); + + // THEN + expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { + ContainerDefinitions: [ + { + SystemControls: [ + { + Namespace: 'SomeNamespace1', + Value: 'SomeValue1', + }, + { + Namespace: 'SomeNamespace2', + Value: 'SomeValue2', + }, + ], + }, + ], + }); + }); + describe('Environment Files', () => { describe('with EC2 task definitions', () => { test('can add asset environment file to the container definition', () => { diff --git a/packages/@aws-cdk/aws-eks/README.md b/packages/@aws-cdk/aws-eks/README.md index e1d78b774450d..ee082134b51f0 100644 --- a/packages/@aws-cdk/aws-eks/README.md +++ b/packages/@aws-cdk/aws-eks/README.md @@ -62,10 +62,10 @@ cluster.addManifest('mypod', { { name: 'hello', image: 'paulbouwer/hello-kubernetes:1.5', - ports: [ { containerPort: 8080 } ] - } - ] - } + ports: [ { containerPort: 8080 } ], + }, + ], + }, }); ``` @@ -102,8 +102,8 @@ The following is a qualitative diagram of the various possible components involv ```text +-----------------------------------------------+ +-----------------+ - | EKS Cluster | kubectl | | - | ----------- |<-------------+| Kubectl Handler | + | EKS Cluster | kubectl | | + |-----------------------------------------------|<-------------+| Kubectl Handler | | | | | | | +-----------------+ | +--------------------+ +-----------------+ | @@ -195,23 +195,22 @@ cluster.addNodegroupCapacity('custom-node-group', { minSize: 4, diskSize: 100, amiType: eks.NodegroupAmiType.AL2_X86_64_GPU, - ... }); ``` To set node taints, you can set `taints` option. ```ts +declare const cluster: eks.Cluster; cluster.addNodegroupCapacity('custom-node-group', { instanceTypes: [new ec2.InstanceType('m5.large')], taints: [ { - effect: TaintEffect.NO_SCHEDULE, + effect: eks.TaintEffect.NO_SCHEDULE, key: 'foo', value: 'bar', - } - ] - ... + }, + ], }); ``` @@ -224,6 +223,7 @@ Spot Instances, we recommend that you configure a Spot managed node group to use ```ts +declare const cluster: eks.Cluster; cluster.addNodegroupCapacity('extra-ng-spot', { instanceTypes: [ new ec2.InstanceType('c5.large'), @@ -245,6 +245,8 @@ When supplying a custom user data script, it must be encoded in the MIME multi-p for mode details. ```ts +declare const cluster: eks.Cluster; + const userData = `MIME-Version: 1.0 Content-Type: multipart/mixed; boundary="==MYBOUNDARY==" @@ -262,6 +264,7 @@ const lt = new ec2.CfnLaunchTemplate(this, 'LaunchTemplate', { userData: Fn.base64(userData), }, }); + cluster.addNodegroupCapacity('extra-ng', { launchTemplateSpec: { id: lt.ref, @@ -275,6 +278,7 @@ Note that when using a custom AMI, Amazon EKS doesn't merge any user data. Which In the following example, `/ect/eks/bootstrap.sh` from the AMI will be used to bootstrap the node. ```ts +declare const cluster: eks.Cluster; const userData = ec2.UserData.forLinux(); userData.addCommands( 'set -o xtrace', @@ -319,17 +323,19 @@ through the `addFargateProfile()` method. The following example adds a profile that will match all pods from the "default" namespace: ```ts +declare const cluster: eks.Cluster; cluster.addFargateProfile('MyProfile', { - selectors: [ { namespace: 'default' } ] + selectors: [ { namespace: 'default' } ], }); ``` You can also directly use the `FargateProfile` construct to create profiles under different scopes: ```ts -new eks.FargateProfile(scope, 'MyProfile', { +declare const cluster: eks.Cluster; +new eks.FargateProfile(this, 'MyProfile', { cluster, - ... + selectors: [ { namespace: 'default' } ], }); ``` @@ -360,30 +366,33 @@ For a detailed overview please visit [Self Managed Nodes](https://docs.aws.amazo Creating an auto-scaling group and connecting it to the cluster is done using the `cluster.addAutoScalingGroupCapacity` method: ```ts +declare const cluster: eks.Cluster; cluster.addAutoScalingGroupCapacity('frontend-nodes', { instanceType: new ec2.InstanceType('t2.medium'), minCapacity: 3, - vpcSubnets: { subnetType: ec2.SubnetType.PUBLIC } + vpcSubnets: { subnetType: ec2.SubnetType.PUBLIC }, }); ``` To connect an already initialized auto-scaling group, use the `cluster.connectAutoScalingGroupCapacity()` method: ```ts -const asg = new ec2.AutoScalingGroup(...); -cluster.connectAutoScalingGroupCapacity(asg); +declare const cluster: eks.Cluster; +declare const asg: autoscaling.AutoScalingGroup; +cluster.connectAutoScalingGroupCapacity(asg, {}); ``` To connect a self-managed node group to an imported cluster, use the `cluster.connectAutoScalingGroupCapacity()` method: ```ts -const importedCluster = eks.Cluster.fromClusterAttributes(stack, 'ImportedCluster', { +declare const cluster: eks.Cluster; +declare const asg: autoscaling.AutoScalingGroup; +const importedCluster = eks.Cluster.fromClusterAttributes(this, 'ImportedCluster', { clusterName: cluster.clusterName, clusterSecurityGroupId: cluster.clusterSecurityGroupId, }); -const asg = new ec2.AutoScalingGroup(...); -importedCluster.connectAutoScalingGroupCapacity(asg); +importedCluster.connectAutoScalingGroupCapacity(asg, {}); ``` In both cases, the [cluster security group](https://docs.aws.amazon.com/eks/latest/userguide/sec-group-reqs.html#cluster-sg) will be automatically attached to @@ -396,13 +405,14 @@ You can customize the [/etc/eks/boostrap.sh](https://github.com/awslabs/amazon-e for bootstrapping the node to the EKS cluster. For example, you can use `kubeletExtraArgs` to add custom node labels or taints. ```ts +declare const cluster: eks.Cluster; cluster.addAutoScalingGroupCapacity('spot', { instanceType: new ec2.InstanceType('t3.large'), minCapacity: 2, bootstrapOptions: { kubeletExtraArgs: '--node-labels foo=bar,goo=far', - awsApiRetryAttempts: 5 - } + awsApiRetryAttempts: 5, + }, }); ``` @@ -410,7 +420,7 @@ To disable bootstrapping altogether (i.e. to fully customize user-data), set `bo You can also configure the cluster to use an auto-scaling group as the default capacity: ```ts -cluster = new eks.Cluster(this, 'HelloEKS', { +const cluster = new eks.Cluster(this, 'HelloEKS', { version: eks.KubernetesVersion.V1_21, defaultCapacityType: eks.DefaultCapacityType.EC2, }); @@ -421,8 +431,9 @@ To access the `AutoScalingGroup` that was created on your behalf, you can use `c You can also independently create an `AutoScalingGroup` and connect it to the cluster using the `cluster.connectAutoScalingGroupCapacity` method: ```ts -const asg = new ec2.AutoScalingGroup(...) -cluster.connectAutoScalingGroupCapacity(asg); +declare const cluster: eks.Cluster; +declare const asg: autoscaling.AutoScalingGroup; +cluster.connectAutoScalingGroupCapacity(asg, {}); ``` This will add the necessary user-data to access the apiserver and configure all connections, roles, and tags needed for the instances in the auto-scaling group to properly join the cluster. @@ -433,10 +444,11 @@ When using self-managed nodes, you can configure the capacity to use spot instan To enable spot capacity, use the `spotPrice` property: ```ts +declare const cluster: eks.Cluster; cluster.addAutoScalingGroupCapacity('spot', { spotPrice: '0.1094', instanceType: new ec2.InstanceType('t3.large'), - maxCapacity: 10 + maxCapacity: 10, }); ``` @@ -458,17 +470,26 @@ To disable the installation of the termination handler, set the `spotInterruptHa #### Bottlerocket [Bottlerocket](https://aws.amazon.com/bottlerocket/) is a Linux-based open-source operating system that is purpose-built by Amazon Web Services for running containers on virtual machines or bare metal hosts. -At this moment, `Bottlerocket` is only supported when using self-managed auto-scaling groups. -> **NOTICE**: Bottlerocket is only available in [some supported AWS regions](https://github.com/bottlerocket-os/bottlerocket/blob/develop/QUICKSTART-EKS.md#finding-an-ami). +`Bottlerocket` is supported when using managed nodegroups or self-managed auto-scaling groups. + +To create a Bottlerocket managed nodegroup: + +```ts +declare const cluster: eks.Cluster; +cluster.addNodegroupCapacity('BottlerocketNG', { + amiType: eks.NodegroupAmiType.BOTTLEROCKET_X86_64, +}); +``` The following example will create an auto-scaling group of 2 `t3.small` Linux instances running with the `Bottlerocket` AMI. ```ts +declare const cluster: eks.Cluster; cluster.addAutoScalingGroupCapacity('BottlerocketNodes', { instanceType: new ec2.InstanceType('t3.small'), minCapacity: 2, - machineImageType: eks.MachineImageType.BOTTLEROCKET + machineImageType: eks.MachineImageType.BOTTLEROCKET, }); ``` @@ -480,6 +501,8 @@ For example, if the Amazon EKS cluster version is `1.17`, the Bottlerocket AMI v Please note Bottlerocket does not allow to customize bootstrap options and `bootstrapOptions` properties is not supported when you create the `Bottlerocket` capacity. +For more details about Bottlerocket, see [Bottlerocket FAQs](https://aws.amazon.com/bottlerocket/faqs/) and [Bottlerocket Open Source Blog](https://aws.amazon.com/blogs/opensource/announcing-the-general-availability-of-bottlerocket-an-open-source-linux-distribution-purpose-built-to-run-containers/). + ### Endpoint Access When you create a new cluster, Amazon EKS creates an endpoint for the managed Kubernetes API server that you use to communicate with your cluster (using Kubernetes management tools such as `kubectl`) @@ -492,7 +515,7 @@ You can configure the [cluster endpoint access](https://docs.aws.amazon.com/eks/ ```ts const cluster = new eks.Cluster(this, 'hello-eks', { version: eks.KubernetesVersion.V1_21, - endpointAccess: eks.EndpointAccess.PRIVATE // No access outside of your VPC. + endpointAccess: eks.EndpointAccess.PRIVATE, // No access outside of your VPC. }); ``` @@ -503,12 +526,12 @@ The default value is `eks.EndpointAccess.PUBLIC_AND_PRIVATE`. Which means the cl You can specify the VPC of the cluster using the `vpc` and `vpcSubnets` properties: ```ts -const vpc = new ec2.Vpc(this, 'Vpc'); +declare const vpc: ec2.Vpc; new eks.Cluster(this, 'HelloEKS', { version: eks.KubernetesVersion.V1_21, vpc, - vpcSubnets: [{ subnetType: ec2.SubnetType.PRIVATE }] + vpcSubnets: [{ subnetType: ec2.SubnetType.PRIVATE }], }); ``` @@ -516,12 +539,17 @@ new eks.Cluster(this, 'HelloEKS', { If you do not specify a VPC, one will be created on your behalf, which you can then access via `cluster.vpc`. The cluster VPC will be associated to any EKS managed capacity (i.e Managed Node Groups and Fargate Profiles). +Please note that the `vpcSubnets` property defines the subnets where EKS will place the _control plane_ ENIs. To choose +the subnets where EKS will place the worker nodes, please refer to the **Provisioning clusters** section above. + If you allocate self managed capacity, you can specify which subnets should the auto-scaling group use: ```ts -const vpc = new ec2.Vpc(this, 'Vpc'); +declare const vpc: ec2.Vpc; +declare const cluster: eks.Cluster; cluster.addAutoScalingGroupCapacity('nodes', { - vpcSubnets: { subnets: vpc.privateSubnets } + vpcSubnets: { subnets: vpc.privateSubnets }, + instanceType: new ec2.InstanceType('t2.medium'), }); ``` @@ -537,6 +565,8 @@ Breaking this down, it means that if the endpoint exposes private access (via `E If the endpoint does not expose private access (via `EndpointAccess.PUBLIC`) **or** the VPC does not contain private subnets, the function will not be provisioned within the VPC. +If your use-case requires control over the IAM role that the KubeCtl Handler assumes, a custom role can be passed through the ClusterProps (as `kubectlLambdaRole`) of the EKS Cluster construct. + #### Cluster Handler The `ClusterHandler` is a set of Lambda functions (`onEventHandler`, `isCompleteHandler`) responsible for interacting with the EKS API in order to control the cluster lifecycle. To provision these functions inside the VPC, set the `placeClusterHandlerInVpc` property to `true`. This will place the functions inside the private subnets of the VPC based on the selection strategy specified in the [`vpcSubnets`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-eks.Cluster.html#vpcsubnetsspan-classapi-icon-api-icon-experimental-titlethis-api-element-is-experimental-it-may-change-without-noticespan) property. @@ -544,16 +574,17 @@ The `ClusterHandler` is a set of Lambda functions (`onEventHandler`, `isComplete You can configure the environment of the Cluster Handler functions by specifying it at cluster instantiation. For example, this can be useful in order to configure an http proxy: ```ts +declare const proxyInstanceSecurityGroup: ec2.SecurityGroup; const cluster = new eks.Cluster(this, 'hello-eks', { version: eks.KubernetesVersion.V1_21, clusterHandlerEnvironment: { - https_proxy: 'http://proxy.myproxy.com' + https_proxy: 'http://proxy.myproxy.com', }, /** * If the proxy is not open publicly, you can pass a security group to the * Cluster Handler Lambdas so that it can reach the proxy. */ - clusterHandlerSecurityGroup: proxyInstanceSecurityGroup + clusterHandlerSecurityGroup: proxyInstanceSecurityGroup, }); ``` @@ -569,8 +600,8 @@ You can configure the environment of this function by specifying it at cluster i const cluster = new eks.Cluster(this, 'hello-eks', { version: eks.KubernetesVersion.V1_21, kubectlEnvironment: { - 'http_proxy': 'http://proxy.myproxy.com' - } + 'http_proxy': 'http://proxy.myproxy.com', + }, }); ``` @@ -604,13 +635,21 @@ const layer = new lambda.LayerVersion(this, 'KubectlLayer', { Now specify when the cluster is defined: ```ts -const cluster = new eks.Cluster(this, 'MyCluster', { +declare const layer: lambda.LayerVersion; +declare const vpc: ec2.Vpc; + +const cluster1 = new eks.Cluster(this, 'MyCluster', { kubectlLayer: layer, + vpc, + clusterName: 'cluster-name', + version: eks.KubernetesVersion.V1_21, }); // or -const cluster = eks.Cluster.fromClusterAttributes(this, 'MyCluster', { +const cluster2 = eks.Cluster.fromClusterAttributes(this, 'MyCluster', { kubectlLayer: layer, + vpc, + clusterName: 'cluster-name', }); ``` @@ -619,15 +658,17 @@ const cluster = eks.Cluster.fromClusterAttributes(this, 'MyCluster', { By default, the kubectl provider is configured with 1024MiB of memory. You can use the `kubectlMemory` option to specify the memory size for the AWS Lambda function: ```ts -import { Size } from '@aws-cdk/core'; - new eks.Cluster(this, 'MyCluster', { - kubectlMemory: Size.gibibytes(4) + kubectlMemory: Size.gibibytes(4), + version: eks.KubernetesVersion.V1_21, }); // or +declare const vpc: ec2.Vpc; eks.Cluster.fromClusterAttributes(this, 'MyCluster', { - kubectlMemory: Size.gibibytes(4) + kubectlMemory: Size.gibibytes(4), + vpc, + clusterName: 'cluster-name', }); ``` @@ -637,6 +678,7 @@ Instance types with `ARM64` architecture are supported in both managed nodegroup Amazon Linux 2 AMI for ARM64 will be automatically selected. ```ts +declare const cluster: eks.Cluster; // add a managed ARM64 nodegroup cluster.addNodegroupCapacity('extra-ng-arm', { instanceTypes: [new ec2.InstanceType('m6g.medium')], @@ -655,7 +697,7 @@ cluster.addAutoScalingGroupCapacity('self-ng-arm', { When you create a cluster, you can specify a `mastersRole`. The `Cluster` construct will associate this role with the `system:masters` [RBAC](https://kubernetes.io/docs/reference/access-authn-authz/rbac/) group, giving it super-user access to the cluster. ```ts -const role = new iam.Role(...); +declare const role: iam.Role; new eks.Cluster(this, 'HelloEKS', { version: eks.KubernetesVersion.V1_21, mastersRole: role, @@ -685,7 +727,7 @@ You can use the `secretsEncryptionKey` to configure which key the cluster will u const secretsKey = new kms.Key(this, 'SecretsKey'); const cluster = new eks.Cluster(this, 'MyCluster', { secretsEncryptionKey: secretsKey, - // ... + version: eks.KubernetesVersion.V1_21, }); ``` @@ -694,13 +736,15 @@ You can also use a similar configuration for running a cluster built using the F ```ts const secretsKey = new kms.Key(this, 'SecretsKey'); const cluster = new eks.FargateCluster(this, 'MyFargateCluster', { - secretsEncryptionKey: secretsKey + secretsEncryptionKey: secretsKey, + version: eks.KubernetesVersion.V1_21, }); ``` The Amazon Resource Name (ARN) for that CMK can be retrieved. ```ts +declare const cluster: eks.Cluster; const clusterEncryptionConfigKeyArn = cluster.clusterEncryptionConfigKeyArn; ``` @@ -720,6 +764,7 @@ Furthermore, when auto-scaling group capacity is added to the cluster, the IAM i For example, let's say you want to grant an IAM user administrative privileges on your cluster: ```ts +declare const cluster: eks.Cluster; const adminUser = new iam.User(this, 'Admin'); cluster.awsAuth.addUserMapping(adminUser, { groups: [ 'system:masters' ]}); ``` @@ -727,7 +772,9 @@ cluster.awsAuth.addUserMapping(adminUser, { groups: [ 'system:masters' ]}); A convenience method for mapping a role to the `system:masters` group is also available: ```ts -cluster.awsAuth.addMastersRole(role) +declare const cluster: eks.Cluster; +declare const role: iam.Role; +cluster.awsAuth.addMastersRole(role); ``` ### Cluster Security Group @@ -739,6 +786,7 @@ between each other. The ID for that security group can be retrieved after creating the cluster. ```ts +declare const cluster: eks.Cluster; const clusterSecurityGroupId = cluster.clusterSecurityGroupId; ``` @@ -758,10 +806,11 @@ unfortunately beyond the scope of this documentation. With services account you can provide Kubernetes Pods access to AWS resources. ```ts +declare const cluster: eks.Cluster; // add service account const serviceAccount = cluster.addServiceAccount('MyServiceAccount'); -const bucket = new Bucket(this, 'Bucket'); +const bucket = new s3.Bucket(this, 'Bucket'); bucket.grantReadWrite(serviceAccount); const mypod = cluster.addManifest('mypod', { @@ -769,23 +818,22 @@ const mypod = cluster.addManifest('mypod', { kind: 'Pod', metadata: { name: 'mypod' }, spec: { - serviceAccountName: serviceAccount.serviceAccountName + serviceAccountName: serviceAccount.serviceAccountName, containers: [ { name: 'hello', image: 'paulbouwer/hello-kubernetes:1.5', ports: [ { containerPort: 8080 } ], - - } - ] - } + }, + ], + }, }); // create the resource after the service account. mypod.node.addDependency(serviceAccount); // print the IAM role arn for this service account -new cdk.CfnOutput(this, 'ServiceAccountIamRole', { value: serviceAccount.role.roleArn }) +new CfnOutput(this, 'ServiceAccountIamRole', { value: serviceAccount.role.roleArn }); ``` Note that using `serviceAccount.serviceAccountName` above **does not** translate into a resource dependency. @@ -799,9 +847,12 @@ To do so, pass the `openIdConnectProvider` property when you import the cluster const provider = eks.OpenIdConnectProvider.fromOpenIdConnectProviderArn(this, 'Provider', 'arn:aws:iam::123456:oidc-provider/oidc.eks.eu-west-1.amazonaws.com/id/AB123456ABC'); // or create a new one using an existing issuer url -const provider = new eks.OpenIdConnectProvider(this, 'Provider', issuerUrl); +declare const issuerUrl: string; +const provider2 = new eks.OpenIdConnectProvider(this, 'Provider', { + url: issuerUrl, +}); -const cluster = eks.Cluster.fromClusterAttributes({ +const cluster = eks.Cluster.fromClusterAttributes(this, 'MyCluster', { clusterName: 'Cluster', openIdConnectProvider: provider, kubectlRoleArn: 'arn:aws:iam::123456:role/service-role/k8sservicerole', @@ -809,10 +860,8 @@ const cluster = eks.Cluster.fromClusterAttributes({ const serviceAccount = cluster.addServiceAccount('MyServiceAccount'); -const bucket = new Bucket(this, 'Bucket'); +const bucket = new s3.Bucket(this, 'Bucket'); bucket.grantReadWrite(serviceAccount); - -// ... ``` Note that adding service accounts requires running `kubectl` commands against the cluster. @@ -836,6 +885,7 @@ The following examples will deploy the [paulbouwer/hello-kubernetes](https://git service on the cluster: ```ts +declare const cluster: eks.Cluster; const appLabel = { app: "hello-kubernetes" }; const deployment = { @@ -852,12 +902,12 @@ const deployment = { { name: "hello-kubernetes", image: "paulbouwer/hello-kubernetes:1.5", - ports: [ { containerPort: 8080 } ] - } - ] - } - } - } + ports: [ { containerPort: 8080 } ], + }, + ], + }, + }, + }, }; const service = { @@ -867,14 +917,14 @@ const service = { spec: { type: "LoadBalancer", ports: [ { port: 80, targetPort: 8080 } ], - selector: appLabel + selector: appLabel, } }; // option 1: use a construct -new KubernetesManifest(this, 'hello-kub', { +new eks.KubernetesManifest(this, 'hello-kub', { cluster, - manifest: [ deployment, service ] + manifest: [ deployment, service ], }); // or, option2: use `addManifest` @@ -885,13 +935,16 @@ cluster.addManifest('hello-kub', service, deployment); The following example will deploy the resource manifest hosting on remote server: -```ts +```text +// This example is only available in TypeScript + import * as yaml from 'js-yaml'; import * as request from 'sync-request'; +declare const cluster: eks.Cluster; const manifestUrl = 'https://url/of/manifest.yaml'; const manifest = yaml.safeLoadAll(request('GET', manifestUrl).getBody()); -cluster.addManifest('my-resource', ...manifest); +cluster.addManifest('my-resource', manifest); ``` #### Dependencies @@ -904,18 +957,19 @@ You can represent dependencies between `KubernetesManifest`s using `resource.node.addDependency()`: ```ts +declare const cluster: eks.Cluster; const namespace = cluster.addManifest('my-namespace', { apiVersion: 'v1', kind: 'Namespace', - metadata: { name: 'my-app' } + metadata: { name: 'my-app' }, }); const service = cluster.addManifest('my-service', { metadata: { name: 'myservice', - namespace: 'my-app' + namespace: 'my-app', }, - spec: // ... + spec: { }, // ... }); service.node.addDependency(namespace); // will apply `my-namespace` before `my-service`. @@ -945,8 +999,9 @@ Pruning is enabled by default but can be disabled through the `prune` option when a cluster is defined: ```ts -new Cluster(this, 'MyCluster', { - prune: false +new eks.Cluster(this, 'MyCluster', { + version: eks.KubernetesVersion.V1_21, + prune: false, }); ``` @@ -956,9 +1011,10 @@ The `kubectl` CLI supports applying a manifest by skipping the validation. This can be accomplished by setting the `skipValidation` flag to `true` in the `KubernetesManifest` props. ```ts +declare const cluster: eks.Cluster; new eks.KubernetesManifest(this, 'HelloAppWithoutValidation', { - cluster: this.cluster, - manifest: [ deployment, service ], + cluster, + manifest: [{ foo: 'bar' }], skipValidation: true, }); ``` @@ -975,19 +1031,20 @@ to add Kubernetes resources to this cluster using Helm. The following example will install the [NGINX Ingress Controller](https://kubernetes.github.io/ingress-nginx/) to your cluster using Helm. ```ts +declare const cluster: eks.Cluster; // option 1: use a construct -new HelmChart(this, 'NginxIngress', { +new eks.HelmChart(this, 'NginxIngress', { cluster, chart: 'nginx-ingress', repository: 'https://helm.nginx.com/stable', - namespace: 'kube-system' + namespace: 'kube-system', }); // or, option2: use `addHelmChart` cluster.addHelmChart('NginxIngress', { chart: 'nginx-ingress', repository: 'https://helm.nginx.com/stable', - namespace: 'kube-system' + namespace: 'kube-system', }); ``` @@ -1011,8 +1068,13 @@ resource or if Helm charts depend on each other. You can use charts: ```ts -const chart1 = cluster.addHelmChart(...); -const chart2 = cluster.addHelmChart(...); +declare const cluster: eks.Cluster; +const chart1 = cluster.addHelmChart('MyChart', { + chart: 'foo', +}); +const chart2 = cluster.addHelmChart('MyChart', { + chart: 'bar', +}); chart2.node.addDependency(chart1); ``` @@ -1050,7 +1112,7 @@ For this reason, to avoid possible confusion, we will create the chart in a sepa `+ my-chart.ts` -```ts +```ts nofixture import * as s3 from '@aws-cdk/aws-s3'; import * as constructs from 'constructs'; import * as cdk8s from 'cdk8s'; @@ -1065,16 +1127,14 @@ export class MyChart extends cdk8s.Chart { super(scope, id); new kplus.Pod(this, 'Pod', { - spec: { - containers: [ - new kplus.Container({ - image: 'my-image', - env: { - BUCKET_NAME: kplus.EnvValue.fromValue(props.bucket.bucketName), - }, - }), - ], - }, + containers: [ + new kplus.Container({ + image: 'my-image', + env: { + BUCKET_NAME: kplus.EnvValue.fromValue(props.bucket.bucketName), + }, + }), + ], }); } } @@ -1082,10 +1142,8 @@ export class MyChart extends cdk8s.Chart { Then, in your AWS CDK app: -```ts -import * as s3 from '@aws-cdk/aws-s3'; -import * as cdk8s from 'cdk8s'; -import { MyChart } from './my-chart'; +```ts fixture=cdk8schart +declare const cluster: eks.Cluster; // some bucket.. const bucket = new s3.Bucket(this, 'Bucket'); @@ -1103,7 +1161,7 @@ You can also compose a few stock `cdk8s+` constructs into your own custom constr you'll need to use is the one from the [`constructs`](https://github.com/aws/constructs) module, and not from `@aws-cdk/core` like you normally would. This is why we used `new cdk8s.App()` as the scope of the chart above. -```ts +```ts nofixture import * as constructs from 'constructs'; import * as cdk8s from 'cdk8s'; import * as kplus from 'cdk8s-plus'; @@ -1114,21 +1172,21 @@ export interface LoadBalancedWebService { readonly replicas: number; } +const app = new cdk8s.App(); +const chart = new cdk8s.Chart(app, 'my-chart'); + export class LoadBalancedWebService extends constructs.Construct { constructor(scope: constructs.Construct, id: string, props: LoadBalancedWebService) { super(scope, id); const deployment = new kplus.Deployment(chart, 'Deployment', { - spec: { - replicas: props.replicas, - podSpecTemplate: { - containers: [ new kplus.Container({ image: props.image }) ] - } - }, + replicas: props.replicas, + containers: [ new kplus.Container({ image: props.image }) ], }); - deployment.expose({port: props.port, serviceType: kplus.ServiceType.LOAD_BALANCER}) - + deployment.expose(props.port, { + serviceType: kplus.ServiceType.LOAD_BALANCER, + }); } } ``` @@ -1146,11 +1204,12 @@ resources. The following example can be used to patch the `hello-kubernetes` deployment from the example above with 5 replicas. ```ts -new KubernetesPatch(this, 'hello-kub-deployment-label', { +declare const cluster: eks.Cluster; +new eks.KubernetesPatch(this, 'hello-kub-deployment-label', { cluster, resourceName: "deployment/hello-kubernetes", applyPatch: { spec: { replicas: 5 } }, - restorePatch: { spec: { replicas: 3 } } + restorePatch: { spec: { replicas: 3 } }, }) ``` @@ -1162,8 +1221,9 @@ and use that as part of your CDK application. For example, you can fetch the address of a [`LoadBalancer`](https://kubernetes.io/docs/concepts/services-networking/service/#loadbalancer) type service: ```ts +declare const cluster: eks.Cluster; // query the load balancer address -const myServiceAddress = new KubernetesObjectValue(this, 'LoadBalancerAttribute', { +const myServiceAddress = new eks.KubernetesObjectValue(this, 'LoadBalancerAttribute', { cluster: cluster, objectType: 'service', objectName: 'my-service', @@ -1172,9 +1232,11 @@ const myServiceAddress = new KubernetesObjectValue(this, 'LoadBalancerAttribute' // pass the address to a lambda function const proxyFunction = new lambda.Function(this, 'ProxyFunction', { - ... + handler: 'index.handler', + code: lambda.Code.fromInline('my-code'), + runtime: lambda.Runtime.NODEJS_14_X, environment: { - myServiceAddress: myServiceAddress.value + myServiceAddress: myServiceAddress.value, }, }) ``` @@ -1182,6 +1244,7 @@ const proxyFunction = new lambda.Function(this, 'ProxyFunction', { Specifically, since the above use-case is quite common, there is an easier way to access that information: ```ts +declare const cluster: eks.Cluster; const loadBalancerAddress = cluster.getServiceLoadBalancerAddress('my-service'); ``` @@ -1205,6 +1268,7 @@ Then, you can use `addManifest` or `addHelmChart` to define resources inside your Kubernetes cluster. For example: ```ts +declare const cluster: eks.Cluster; cluster.addManifest('Test', { apiVersion: 'v1', kind: 'ConfigMap', diff --git a/packages/@aws-cdk/aws-eks/lib/cluster.ts b/packages/@aws-cdk/aws-eks/lib/cluster.ts index 2c762ab666327..78f4c20e8b29d 100644 --- a/packages/@aws-cdk/aws-eks/lib/cluster.ts +++ b/packages/@aws-cdk/aws-eks/lib/cluster.ts @@ -117,6 +117,15 @@ export interface ICluster extends IResource, ec2.IConnectable { */ readonly kubectlPrivateSubnets?: ec2.ISubnet[]; + /** + * An IAM role that can perform kubectl operations against this cluster. + * + * The role should be mapped to the `system:masters` Kubernetes RBAC role. + * + * This role is directly passed to the lambda handler that sends Kube Ctl commands to the cluster. + */ + readonly kubectlLambdaRole?: iam.IRole; + /** * An AWS Lambda layer that includes `kubectl`, `helm` and the `aws` CLI. * @@ -271,6 +280,18 @@ export interface ClusterAttributes { */ readonly kubectlRoleArn?: string; + /** + * An IAM role that can perform kubectl operations against this cluster. + * + * The role should be mapped to the `system:masters` Kubernetes RBAC role. + * + * This role is directly passed to the lambda handler that sends Kube Ctl commands + * to the cluster. + * @default - if not specified, the default role created by a lambda function will + * be used. + */ + readonly kubectlLambdaRole?: iam.IRole; + /** * Environment variables to use when running `kubectl` against this cluster. * @default - no additional variables @@ -369,11 +390,7 @@ export interface CommonClusterOptions { * * For example, to only select private subnets, supply the following: * - * ```ts - * vpcSubnets: [ - * { subnetType: ec2.SubnetType.Private } - * ] - * ``` + * `vpcSubnets: [{ subnetType: ec2.SubnetType.PRIVATE }]` * * @default - All public and private subnets */ @@ -485,9 +502,9 @@ export interface ClusterOptions extends CommonClusterOptions { * * ```ts * const layer = new lambda.LayerVersion(this, 'kubectl-layer', { - * code: lambda.Code.fromAsset(`${__dirname}/layer.zip`)), - * compatibleRuntimes: [lambda.Runtime.PROVIDED] - * }) + * code: lambda.Code.fromAsset(`${__dirname}/layer.zip`), + * compatibleRuntimes: [lambda.Runtime.PROVIDED], + * }); * ``` * * @default - the layer provided by the `aws-lambda-layer-kubectl` SAR app. @@ -531,9 +548,9 @@ export interface ClusterOptions extends CommonClusterOptions { * * ```ts * const layer = new lambda.LayerVersion(this, 'proxy-agent-layer', { - * code: lambda.Code.fromAsset(`${__dirname}/layer.zip`)), - * compatibleRuntimes: [lambda.Runtime.NODEJS_12_X] - * }) + * code: lambda.Code.fromAsset(`${__dirname}/layer.zip`), + * compatibleRuntimes: [lambda.Runtime.NODEJS_12_X], + * }); * ``` * * @default - a layer bundled with this module. @@ -702,6 +719,14 @@ export interface ClusterProps extends ClusterOptions { * @default NODEGROUP */ readonly defaultCapacityType?: DefaultCapacityType; + + + /** + * The IAM role to pass to the Kubectl Lambda Handler. + * + * @default - Default Lambda IAM Execution Role + */ + readonly kubectlLambdaRole?: iam.IRole; } /** @@ -771,6 +796,7 @@ abstract class ClusterBase extends Resource implements ICluster { public abstract readonly clusterSecurityGroup: ec2.ISecurityGroup; public abstract readonly clusterEncryptionConfigKeyArn: string; public abstract readonly kubectlRole?: iam.IRole; + public abstract readonly kubectlLambdaRole?: iam.IRole; public abstract readonly kubectlEnvironment?: { [key: string]: string }; public abstract readonly kubectlSecurityGroup?: ec2.ISecurityGroup; public abstract readonly kubectlPrivateSubnets?: ec2.ISubnet[]; @@ -1010,7 +1036,7 @@ export class Cluster extends ClusterBase { /** * The AWS generated ARN for the Cluster resource * - * @example arn:aws:eks:us-west-2:666666666666:cluster/prod + * For example, `arn:aws:eks:us-west-2:666666666666:cluster/prod` */ public readonly clusterArn: string; @@ -1019,7 +1045,7 @@ export class Cluster extends ClusterBase { * * This is the URL inside the kubeconfig file to use with kubectl * - * @example https://5E1D0CEXAMPLEA591B746AFC5AB30262.yl4.us-west-2.eks.amazonaws.com + * For example, `https://5E1D0CEXAMPLEA591B746AFC5AB30262.yl4.us-west-2.eks.amazonaws.com` */ public readonly clusterEndpoint: string; @@ -1077,6 +1103,18 @@ export class Cluster extends ClusterBase { */ public readonly kubectlRole?: iam.IRole; + /** + * An IAM role that can perform kubectl operations against this cluster. + * + * The role should be mapped to the `system:masters` Kubernetes RBAC role. + * + * This role is directly passed to the lambda handler that sends Kube Ctl commands to the cluster. + * @default - if not specified, the default role created by a lambda function will + * be used. + */ + + public readonly kubectlLambdaRole?: iam.IRole; + /** * Custom environment variables when running `kubectl` against this cluster. */ @@ -1195,6 +1233,7 @@ export class Cluster extends ClusterBase { this.prune = props.prune ?? true; this.vpc = props.vpc || new ec2.Vpc(this, 'DefaultVpc'); this.version = props.version; + this.kubectlLambdaRole = props.kubectlLambdaRole ? props.kubectlLambdaRole : undefined; this.tagSubnets(); @@ -1796,7 +1835,8 @@ export interface BootstrapOptions { /** * Extra arguments to add to the kubelet. Useful for adding labels or taints. * - * @example --node-labels foo=bar,goo=far + * For example, `--node-labels foo=bar,goo=far`. + * * @default - none */ readonly kubeletExtraArgs?: string; @@ -1867,6 +1907,7 @@ class ImportedCluster extends ClusterBase { public readonly clusterArn: string; public readonly connections = new ec2.Connections(); public readonly kubectlRole?: iam.IRole; + public readonly kubectlLambdaRole?: iam.IRole; public readonly kubectlEnvironment?: { [key: string]: string; } | undefined; public readonly kubectlSecurityGroup?: ec2.ISecurityGroup | undefined; public readonly kubectlPrivateSubnets?: ec2.ISubnet[] | undefined; diff --git a/packages/@aws-cdk/aws-eks/lib/kubectl-provider.ts b/packages/@aws-cdk/aws-eks/lib/kubectl-provider.ts index b5bd8ed51b876..0e5db3c6a51e3 100644 --- a/packages/@aws-cdk/aws-eks/lib/kubectl-provider.ts +++ b/packages/@aws-cdk/aws-eks/lib/kubectl-provider.ts @@ -77,6 +77,7 @@ export class KubectlProvider extends NestedStack { description: 'onEvent handler for EKS kubectl resource provider', memorySize, environment: cluster.kubectlEnvironment, + role: cluster.kubectlLambdaRole ? cluster.kubectlLambdaRole : undefined, // defined only when using private access vpc: cluster.kubectlPrivateSubnets ? cluster.vpc : undefined, diff --git a/packages/@aws-cdk/aws-eks/lib/managed-nodegroup.ts b/packages/@aws-cdk/aws-eks/lib/managed-nodegroup.ts index 69dd8223edc09..ec91d54abb610 100644 --- a/packages/@aws-cdk/aws-eks/lib/managed-nodegroup.ts +++ b/packages/@aws-cdk/aws-eks/lib/managed-nodegroup.ts @@ -34,7 +34,15 @@ export enum NodegroupAmiType { /** * Amazon Linux 2 (ARM-64) */ - AL2_ARM_64 = 'AL2_ARM_64' + AL2_ARM_64 = 'AL2_ARM_64', + /** + * Bottlerocket Linux(ARM-64) + */ + BOTTLEROCKET_ARM_64 = 'BOTTLEROCKET_ARM_64', + /** + * Bottlerocket(x86-64) + */ + BOTTLEROCKET_X86_64 = 'BOTTLEROCKET_x86_64', } /** diff --git a/packages/@aws-cdk/aws-eks/package.json b/packages/@aws-cdk/aws-eks/package.json index ac9ed3f9f1fe2..48d5dad161a32 100644 --- a/packages/@aws-cdk/aws-eks/package.json +++ b/packages/@aws-cdk/aws-eks/package.json @@ -28,7 +28,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", diff --git a/packages/@aws-cdk/aws-eks/rosetta/cdk8schart.ts-fixture b/packages/@aws-cdk/aws-eks/rosetta/cdk8schart.ts-fixture new file mode 100644 index 0000000000000..d0e854aa4b57d --- /dev/null +++ b/packages/@aws-cdk/aws-eks/rosetta/cdk8schart.ts-fixture @@ -0,0 +1,35 @@ +import { Construct } from 'constructs'; +import { CfnOutput, Fn, Size, Stack } from '@aws-cdk/core'; +import * as eks from '@aws-cdk/aws-eks'; +import * as s3 from '@aws-cdk/aws-s3'; +import * as cdk8s from 'cdk8s'; +import * as kplus from 'cdk8s-plus'; + +interface MyChartProps { + readonly bucket: s3.Bucket; +} + +class MyChart extends cdk8s.Chart { + constructor(scope: Construct, id: string, props: MyChartProps) { + super(scope, id); + + new kplus.Pod(this, 'Pod', { + containers: [ + new kplus.Container({ + image: 'my-image', + env: { + BUCKET_NAME: kplus.EnvValue.fromValue(props.bucket.bucketName), + }, + }), + ], + }); + } +} + +class Context extends Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + + /// here + } +} diff --git a/packages/@aws-cdk/aws-eks/rosetta/default.ts-fixture b/packages/@aws-cdk/aws-eks/rosetta/default.ts-fixture index 65a6aabb9e5b4..a18b3f87d0f3d 100644 --- a/packages/@aws-cdk/aws-eks/rosetta/default.ts-fixture +++ b/packages/@aws-cdk/aws-eks/rosetta/default.ts-fixture @@ -1,8 +1,15 @@ -import * as cdk from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { CfnOutput, Fn, Size, Stack } from '@aws-cdk/core'; import * as eks from '@aws-cdk/aws-eks'; +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as lambda from '@aws-cdk/aws-lambda'; +import * as iam from '@aws-cdk/aws-iam'; +import * as kms from '@aws-cdk/aws-kms'; +import * as s3 from '@aws-cdk/aws-s3'; +import * as autoscaling from '@aws-cdk/aws-autoscaling'; -class Context extends cdk.Construct { - constructor(scope: cdk.Construct, id: string) { +class Context extends Stack { + constructor(scope: Construct, id: string) { super(scope, id); /// here diff --git a/packages/@aws-cdk/aws-eks/test/cluster.test.ts b/packages/@aws-cdk/aws-eks/test/cluster.test.ts index 63a17d9c5d64d..b8b9c91042f32 100644 --- a/packages/@aws-cdk/aws-eks/test/cluster.test.ts +++ b/packages/@aws-cdk/aws-eks/test/cluster.test.ts @@ -2202,6 +2202,42 @@ describe('cluster', () => { }, }); + }); + + test('kubectl provider passes iam role environment to kube ctl lambda', () => { + + const { stack } = testFixture(); + + const kubectlRole = new iam.Role(stack, 'KubectlIamRole', { + assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'), + }); + + // using _ syntax to silence warning about _cluster not being used, when it is + const cluster = new eks.Cluster(stack, 'Cluster1', { + version: CLUSTER_VERSION, + prune: false, + endpointAccess: eks.EndpointAccess.PRIVATE, + kubectlLambdaRole: kubectlRole, + }); + + cluster.addManifest('resource', { + kind: 'ConfigMap', + apiVersion: 'v1', + data: { + hello: 'world', + }, + metadata: { + name: 'config-map', + }, + }); + + // the kubectl provider is inside a nested stack. + const nested = stack.node.tryFindChild('@aws-cdk/aws-eks.KubectlProvider') as cdk.NestedStack; + expect(nested).toHaveResourceLike('AWS::Lambda::Function', { + Role: { + Ref: 'referencetoStackKubectlIamRole02F8947EArn', + }, + }); }); diff --git a/packages/@aws-cdk/aws-eks/test/integ.eks-bottlerocket-ng.expected.json b/packages/@aws-cdk/aws-eks/test/integ.eks-bottlerocket-ng.expected.json new file mode 100644 index 0000000000000..7755a615e42e5 --- /dev/null +++ b/packages/@aws-cdk/aws-eks/test/integ.eks-bottlerocket-ng.expected.json @@ -0,0 +1,1432 @@ +{ + "Resources": { + "AdminRole38563C57": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::12345678:root" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "Vpc8378EB38": { + "Type": "AWS::EC2::VPC", + "Properties": { + "CidrBlock": "10.0.0.0/16", + "EnableDnsHostnames": true, + "EnableDnsSupport": true, + "InstanceTenancy": "default", + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-eks-cluster-test/Vpc" + } + ] + } + }, + "VpcPublicSubnet1Subnet5C2D37C4": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.0.0/19", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1a", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + }, + { + "Key": "kubernetes.io/role/elb", + "Value": "1" + }, + { + "Key": "Name", + "Value": "aws-cdk-eks-cluster-test/Vpc/PublicSubnet1" + } + ] + } + }, + "VpcPublicSubnet1RouteTable6C95E38E": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "kubernetes.io/role/elb", + "Value": "1" + }, + { + "Key": "Name", + "Value": "aws-cdk-eks-cluster-test/Vpc/PublicSubnet1" + } + ] + } + }, + "VpcPublicSubnet1RouteTableAssociation97140677": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet1RouteTable6C95E38E" + }, + "SubnetId": { + "Ref": "VpcPublicSubnet1Subnet5C2D37C4" + } + } + }, + "VpcPublicSubnet1DefaultRoute3DA9E72A": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet1RouteTable6C95E38E" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VpcIGWD7BA715C" + } + }, + "DependsOn": [ + "VpcVPCGWBF912B6E" + ] + }, + "VpcPublicSubnet1EIPD7E02669": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc", + "Tags": [ + { + "Key": "kubernetes.io/role/elb", + "Value": "1" + }, + { + "Key": "Name", + "Value": "aws-cdk-eks-cluster-test/Vpc/PublicSubnet1" + } + ] + } + }, + "VpcPublicSubnet1NATGateway4D7517AA": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "SubnetId": { + "Ref": "VpcPublicSubnet1Subnet5C2D37C4" + }, + "AllocationId": { + "Fn::GetAtt": [ + "VpcPublicSubnet1EIPD7E02669", + "AllocationId" + ] + }, + "Tags": [ + { + "Key": "kubernetes.io/role/elb", + "Value": "1" + }, + { + "Key": "Name", + "Value": "aws-cdk-eks-cluster-test/Vpc/PublicSubnet1" + } + ] + } + }, + "VpcPublicSubnet2Subnet691E08A3": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.32.0/19", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1b", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + }, + { + "Key": "kubernetes.io/role/elb", + "Value": "1" + }, + { + "Key": "Name", + "Value": "aws-cdk-eks-cluster-test/Vpc/PublicSubnet2" + } + ] + } + }, + "VpcPublicSubnet2RouteTable94F7E489": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "kubernetes.io/role/elb", + "Value": "1" + }, + { + "Key": "Name", + "Value": "aws-cdk-eks-cluster-test/Vpc/PublicSubnet2" + } + ] + } + }, + "VpcPublicSubnet2RouteTableAssociationDD5762D8": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet2RouteTable94F7E489" + }, + "SubnetId": { + "Ref": "VpcPublicSubnet2Subnet691E08A3" + } + } + }, + "VpcPublicSubnet2DefaultRoute97F91067": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet2RouteTable94F7E489" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VpcIGWD7BA715C" + } + }, + "DependsOn": [ + "VpcVPCGWBF912B6E" + ] + }, + "VpcPublicSubnet3SubnetBE12F0B6": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.64.0/19", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1c", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + }, + { + "Key": "kubernetes.io/role/elb", + "Value": "1" + }, + { + "Key": "Name", + "Value": "aws-cdk-eks-cluster-test/Vpc/PublicSubnet3" + } + ] + } + }, + "VpcPublicSubnet3RouteTable93458DBB": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "kubernetes.io/role/elb", + "Value": "1" + }, + { + "Key": "Name", + "Value": "aws-cdk-eks-cluster-test/Vpc/PublicSubnet3" + } + ] + } + }, + "VpcPublicSubnet3RouteTableAssociation1F1EDF02": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet3RouteTable93458DBB" + }, + "SubnetId": { + "Ref": "VpcPublicSubnet3SubnetBE12F0B6" + } + } + }, + "VpcPublicSubnet3DefaultRoute4697774F": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet3RouteTable93458DBB" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VpcIGWD7BA715C" + } + }, + "DependsOn": [ + "VpcVPCGWBF912B6E" + ] + }, + "VpcPrivateSubnet1Subnet536B997A": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.96.0/19", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1a", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + }, + { + "Key": "kubernetes.io/role/internal-elb", + "Value": "1" + }, + { + "Key": "Name", + "Value": "aws-cdk-eks-cluster-test/Vpc/PrivateSubnet1" + } + ] + } + }, + "VpcPrivateSubnet1RouteTableB2C5B500": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "kubernetes.io/role/internal-elb", + "Value": "1" + }, + { + "Key": "Name", + "Value": "aws-cdk-eks-cluster-test/Vpc/PrivateSubnet1" + } + ] + } + }, + "VpcPrivateSubnet1RouteTableAssociation70C59FA6": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet1RouteTableB2C5B500" + }, + "SubnetId": { + "Ref": "VpcPrivateSubnet1Subnet536B997A" + } + } + }, + "VpcPrivateSubnet1DefaultRouteBE02A9ED": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet1RouteTableB2C5B500" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VpcPublicSubnet1NATGateway4D7517AA" + } + } + }, + "VpcPrivateSubnet2Subnet3788AAA1": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.128.0/19", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1b", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + }, + { + "Key": "kubernetes.io/role/internal-elb", + "Value": "1" + }, + { + "Key": "Name", + "Value": "aws-cdk-eks-cluster-test/Vpc/PrivateSubnet2" + } + ] + } + }, + "VpcPrivateSubnet2RouteTableA678073B": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "kubernetes.io/role/internal-elb", + "Value": "1" + }, + { + "Key": "Name", + "Value": "aws-cdk-eks-cluster-test/Vpc/PrivateSubnet2" + } + ] + } + }, + "VpcPrivateSubnet2RouteTableAssociationA89CAD56": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet2RouteTableA678073B" + }, + "SubnetId": { + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" + } + } + }, + "VpcPrivateSubnet2DefaultRoute060D2087": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet2RouteTableA678073B" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VpcPublicSubnet1NATGateway4D7517AA" + } + } + }, + "VpcPrivateSubnet3SubnetF258B56E": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.160.0/19", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1c", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + }, + { + "Key": "kubernetes.io/role/internal-elb", + "Value": "1" + }, + { + "Key": "Name", + "Value": "aws-cdk-eks-cluster-test/Vpc/PrivateSubnet3" + } + ] + } + }, + "VpcPrivateSubnet3RouteTableD98824C7": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "kubernetes.io/role/internal-elb", + "Value": "1" + }, + { + "Key": "Name", + "Value": "aws-cdk-eks-cluster-test/Vpc/PrivateSubnet3" + } + ] + } + }, + "VpcPrivateSubnet3RouteTableAssociation16BDDC43": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet3RouteTableD98824C7" + }, + "SubnetId": { + "Ref": "VpcPrivateSubnet3SubnetF258B56E" + } + } + }, + "VpcPrivateSubnet3DefaultRoute94B74F0D": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet3RouteTableD98824C7" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VpcPublicSubnet1NATGateway4D7517AA" + } + } + }, + "VpcIGWD7BA715C": { + "Type": "AWS::EC2::InternetGateway", + "Properties": { + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-eks-cluster-test/Vpc" + } + ] + } + }, + "VpcVPCGWBF912B6E": { + "Type": "AWS::EC2::VPCGatewayAttachment", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "InternetGatewayId": { + "Ref": "VpcIGWD7BA715C" + } + } + }, + "ClusterRoleFA261979": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "eks.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/AmazonEKSClusterPolicy" + ] + ] + } + ] + } + }, + "ClusterControlPlaneSecurityGroupD274242C": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "EKS Control Plane Security Group", + "SecurityGroupEgress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow all outbound traffic by default", + "IpProtocol": "-1" + } + ], + "VpcId": { + "Ref": "Vpc8378EB38" + } + } + }, + "ClusterCreationRole360249B6": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::12345678:root" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + }, + "DependsOn": [ + "VpcIGWD7BA715C", + "VpcPrivateSubnet1DefaultRouteBE02A9ED", + "VpcPrivateSubnet1RouteTableB2C5B500", + "VpcPrivateSubnet1RouteTableAssociation70C59FA6", + "VpcPrivateSubnet1Subnet536B997A", + "VpcPrivateSubnet2DefaultRoute060D2087", + "VpcPrivateSubnet2RouteTableA678073B", + "VpcPrivateSubnet2RouteTableAssociationA89CAD56", + "VpcPrivateSubnet2Subnet3788AAA1", + "VpcPrivateSubnet3DefaultRoute94B74F0D", + "VpcPrivateSubnet3RouteTableD98824C7", + "VpcPrivateSubnet3RouteTableAssociation16BDDC43", + "VpcPrivateSubnet3SubnetF258B56E", + "VpcPublicSubnet1DefaultRoute3DA9E72A", + "VpcPublicSubnet1EIPD7E02669", + "VpcPublicSubnet1NATGateway4D7517AA", + "VpcPublicSubnet1RouteTable6C95E38E", + "VpcPublicSubnet1RouteTableAssociation97140677", + "VpcPublicSubnet1Subnet5C2D37C4", + "VpcPublicSubnet2DefaultRoute97F91067", + "VpcPublicSubnet2RouteTable94F7E489", + "VpcPublicSubnet2RouteTableAssociationDD5762D8", + "VpcPublicSubnet2Subnet691E08A3", + "VpcPublicSubnet3DefaultRoute4697774F", + "VpcPublicSubnet3RouteTable93458DBB", + "VpcPublicSubnet3RouteTableAssociation1F1EDF02", + "VpcPublicSubnet3SubnetBE12F0B6", + "Vpc8378EB38", + "VpcVPCGWBF912B6E" + ] + }, + "ClusterCreationRoleDefaultPolicyE8BDFC7B": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "iam:PassRole", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "ClusterRoleFA261979", + "Arn" + ] + } + }, + { + "Action": [ + "eks:CreateCluster", + "eks:DescribeCluster", + "eks:DescribeUpdate", + "eks:DeleteCluster", + "eks:UpdateClusterVersion", + "eks:UpdateClusterConfig", + "eks:CreateFargateProfile", + "eks:TagResource", + "eks:UntagResource" + ], + "Effect": "Allow", + "Resource": [ + "*" + ] + }, + { + "Action": [ + "eks:DescribeFargateProfile", + "eks:DeleteFargateProfile" + ], + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": [ + "iam:GetRole", + "iam:listAttachedRolePolicies" + ], + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": "iam:CreateServiceLinkedRole", + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": [ + "ec2:DescribeInstances", + "ec2:DescribeNetworkInterfaces", + "ec2:DescribeSecurityGroups", + "ec2:DescribeSubnets", + "ec2:DescribeRouteTables", + "ec2:DescribeDhcpOptions", + "ec2:DescribeVpcs" + ], + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "ClusterCreationRoleDefaultPolicyE8BDFC7B", + "Roles": [ + { + "Ref": "ClusterCreationRole360249B6" + } + ] + }, + "DependsOn": [ + "VpcIGWD7BA715C", + "VpcPrivateSubnet1DefaultRouteBE02A9ED", + "VpcPrivateSubnet1RouteTableB2C5B500", + "VpcPrivateSubnet1RouteTableAssociation70C59FA6", + "VpcPrivateSubnet1Subnet536B997A", + "VpcPrivateSubnet2DefaultRoute060D2087", + "VpcPrivateSubnet2RouteTableA678073B", + "VpcPrivateSubnet2RouteTableAssociationA89CAD56", + "VpcPrivateSubnet2Subnet3788AAA1", + "VpcPrivateSubnet3DefaultRoute94B74F0D", + "VpcPrivateSubnet3RouteTableD98824C7", + "VpcPrivateSubnet3RouteTableAssociation16BDDC43", + "VpcPrivateSubnet3SubnetF258B56E", + "VpcPublicSubnet1DefaultRoute3DA9E72A", + "VpcPublicSubnet1EIPD7E02669", + "VpcPublicSubnet1NATGateway4D7517AA", + "VpcPublicSubnet1RouteTable6C95E38E", + "VpcPublicSubnet1RouteTableAssociation97140677", + "VpcPublicSubnet1Subnet5C2D37C4", + "VpcPublicSubnet2DefaultRoute97F91067", + "VpcPublicSubnet2RouteTable94F7E489", + "VpcPublicSubnet2RouteTableAssociationDD5762D8", + "VpcPublicSubnet2Subnet691E08A3", + "VpcPublicSubnet3DefaultRoute4697774F", + "VpcPublicSubnet3RouteTable93458DBB", + "VpcPublicSubnet3RouteTableAssociation1F1EDF02", + "VpcPublicSubnet3SubnetBE12F0B6", + "Vpc8378EB38", + "VpcVPCGWBF912B6E" + ] + }, + "Cluster9EE0221C": { + "Type": "Custom::AWSCDK-EKS-Cluster", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "awscdkawseksClusterResourceProviderNestedStackawscdkawseksClusterResourceProviderNestedStackResource9827C454", + "Outputs.awscdkeksclustertestawscdkawseksClusterResourceProviderframeworkonEvent503C1667Arn" + ] + }, + "Config": { + "version": "1.21", + "roleArn": { + "Fn::GetAtt": [ + "ClusterRoleFA261979", + "Arn" + ] + }, + "resourcesVpcConfig": { + "subnetIds": [ + { + "Ref": "VpcPublicSubnet1Subnet5C2D37C4" + }, + { + "Ref": "VpcPublicSubnet2Subnet691E08A3" + }, + { + "Ref": "VpcPublicSubnet3SubnetBE12F0B6" + }, + { + "Ref": "VpcPrivateSubnet1Subnet536B997A" + }, + { + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" + }, + { + "Ref": "VpcPrivateSubnet3SubnetF258B56E" + } + ], + "securityGroupIds": [ + { + "Fn::GetAtt": [ + "ClusterControlPlaneSecurityGroupD274242C", + "GroupId" + ] + } + ], + "endpointPublicAccess": true, + "endpointPrivateAccess": true + } + }, + "AssumeRoleArn": { + "Fn::GetAtt": [ + "ClusterCreationRole360249B6", + "Arn" + ] + }, + "AttributesRevision": 2 + }, + "DependsOn": [ + "ClusterCreationRoleDefaultPolicyE8BDFC7B", + "ClusterCreationRole360249B6", + "VpcIGWD7BA715C", + "VpcPrivateSubnet1DefaultRouteBE02A9ED", + "VpcPrivateSubnet1RouteTableB2C5B500", + "VpcPrivateSubnet1RouteTableAssociation70C59FA6", + "VpcPrivateSubnet1Subnet536B997A", + "VpcPrivateSubnet2DefaultRoute060D2087", + "VpcPrivateSubnet2RouteTableA678073B", + "VpcPrivateSubnet2RouteTableAssociationA89CAD56", + "VpcPrivateSubnet2Subnet3788AAA1", + "VpcPrivateSubnet3DefaultRoute94B74F0D", + "VpcPrivateSubnet3RouteTableD98824C7", + "VpcPrivateSubnet3RouteTableAssociation16BDDC43", + "VpcPrivateSubnet3SubnetF258B56E", + "VpcPublicSubnet1DefaultRoute3DA9E72A", + "VpcPublicSubnet1EIPD7E02669", + "VpcPublicSubnet1NATGateway4D7517AA", + "VpcPublicSubnet1RouteTable6C95E38E", + "VpcPublicSubnet1RouteTableAssociation97140677", + "VpcPublicSubnet1Subnet5C2D37C4", + "VpcPublicSubnet2DefaultRoute97F91067", + "VpcPublicSubnet2RouteTable94F7E489", + "VpcPublicSubnet2RouteTableAssociationDD5762D8", + "VpcPublicSubnet2Subnet691E08A3", + "VpcPublicSubnet3DefaultRoute4697774F", + "VpcPublicSubnet3RouteTable93458DBB", + "VpcPublicSubnet3RouteTableAssociation1F1EDF02", + "VpcPublicSubnet3SubnetBE12F0B6", + "Vpc8378EB38", + "VpcVPCGWBF912B6E" + ], + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "ClusterKubectlReadyBarrier200052AF": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Type": "String", + "Value": "aws:cdk:eks:kubectl-ready" + }, + "DependsOn": [ + "ClusterCreationRoleDefaultPolicyE8BDFC7B", + "ClusterCreationRole360249B6", + "Cluster9EE0221C" + ] + }, + "ClusterAwsAuthmanifestFE51F8AE": { + "Type": "Custom::AWSCDK-EKS-KubernetesResource", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "awscdkawseksKubectlProviderNestedStackawscdkawseksKubectlProviderNestedStackResourceA7AEBA6B", + "Outputs.awscdkeksclustertestawscdkawseksKubectlProviderframeworkonEventC681B49AArn" + ] + }, + "Manifest": { + "Fn::Join": [ + "", + [ + "[{\"apiVersion\":\"v1\",\"kind\":\"ConfigMap\",\"metadata\":{\"name\":\"aws-auth\",\"namespace\":\"kube-system\",\"labels\":{\"aws.cdk.eks/prune-c842be348c45337cd97b8759de76d5a68b4910d487\":\"\"}},\"data\":{\"mapRoles\":\"[{\\\"rolearn\\\":\\\"", + { + "Fn::GetAtt": [ + "AdminRole38563C57", + "Arn" + ] + }, + "\\\",\\\"username\\\":\\\"", + { + "Fn::GetAtt": [ + "AdminRole38563C57", + "Arn" + ] + }, + "\\\",\\\"groups\\\":[\\\"system:masters\\\"]},{\\\"rolearn\\\":\\\"", + { + "Fn::GetAtt": [ + "ClusterNodegroupBottlerocketNG1NodeGroupRoleF0E6A2C6", + "Arn" + ] + }, + "\\\",\\\"username\\\":\\\"system:node:{{EC2PrivateDNSName}}\\\",\\\"groups\\\":[\\\"system:bootstrappers\\\",\\\"system:nodes\\\"]},{\\\"rolearn\\\":\\\"", + { + "Fn::GetAtt": [ + "ClusterNodegroupBottlerocketNG2NodeGroupRole8BD62EDB", + "Arn" + ] + }, + "\\\",\\\"username\\\":\\\"system:node:{{EC2PrivateDNSName}}\\\",\\\"groups\\\":[\\\"system:bootstrappers\\\",\\\"system:nodes\\\"]}]\",\"mapUsers\":\"[]\",\"mapAccounts\":\"[]\"}}]" + ] + ] + }, + "ClusterName": { + "Ref": "Cluster9EE0221C" + }, + "RoleArn": { + "Fn::GetAtt": [ + "ClusterCreationRole360249B6", + "Arn" + ] + }, + "PruneLabel": "aws.cdk.eks/prune-c842be348c45337cd97b8759de76d5a68b4910d487", + "Overwrite": true + }, + "DependsOn": [ + "ClusterKubectlReadyBarrier200052AF" + ], + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "ClusterNodegroupBottlerocketNG1NodeGroupRoleF0E6A2C6": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": { + "Fn::Join": [ + "", + [ + "ec2.", + { + "Ref": "AWS::URLSuffix" + } + ] + ] + } + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/AmazonEKSWorkerNodePolicy" + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/AmazonEKS_CNI_Policy" + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/AmazonEC2ContainerRegistryReadOnly" + ] + ] + } + ] + } + }, + "ClusterNodegroupBottlerocketNG1B78D1784": { + "Type": "AWS::EKS::Nodegroup", + "Properties": { + "ClusterName": { + "Ref": "Cluster9EE0221C" + }, + "NodeRole": { + "Fn::GetAtt": [ + "ClusterNodegroupBottlerocketNG1NodeGroupRoleF0E6A2C6", + "Arn" + ] + }, + "Subnets": [ + { + "Ref": "VpcPrivateSubnet1Subnet536B997A" + }, + { + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" + }, + { + "Ref": "VpcPrivateSubnet3SubnetF258B56E" + } + ], + "AmiType": "BOTTLEROCKET_x86_64", + "ForceUpdateEnabled": true, + "ScalingConfig": { + "DesiredSize": 2, + "MaxSize": 2, + "MinSize": 1 + } + } + }, + "ClusterNodegroupBottlerocketNG2NodeGroupRole8BD62EDB": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": { + "Fn::Join": [ + "", + [ + "ec2.", + { + "Ref": "AWS::URLSuffix" + } + ] + ] + } + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/AmazonEKSWorkerNodePolicy" + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/AmazonEKS_CNI_Policy" + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/AmazonEC2ContainerRegistryReadOnly" + ] + ] + } + ] + } + }, + "ClusterNodegroupBottlerocketNG299226DAB": { + "Type": "AWS::EKS::Nodegroup", + "Properties": { + "ClusterName": { + "Ref": "Cluster9EE0221C" + }, + "NodeRole": { + "Fn::GetAtt": [ + "ClusterNodegroupBottlerocketNG2NodeGroupRole8BD62EDB", + "Arn" + ] + }, + "Subnets": [ + { + "Ref": "VpcPrivateSubnet1Subnet536B997A" + }, + { + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" + }, + { + "Ref": "VpcPrivateSubnet3SubnetF258B56E" + } + ], + "AmiType": "BOTTLEROCKET_ARM_64", + "ForceUpdateEnabled": true, + "ScalingConfig": { + "DesiredSize": 2, + "MaxSize": 2, + "MinSize": 1 + } + } + }, + "awscdkawseksClusterResourceProviderNestedStackawscdkawseksClusterResourceProviderNestedStackResource9827C454": { + "Type": "AWS::CloudFormation::Stack", + "Properties": { + "TemplateURL": { + "Fn::Join": [ + "", + [ + "https://s3.test-region.", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Ref": "AssetParametersdcdc759e2644fb3c4847d9a160ce99f0f40f137c825ae9cc094323ed4839bab9S3BucketA775E312" + }, + "/", + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParametersdcdc759e2644fb3c4847d9a160ce99f0f40f137c825ae9cc094323ed4839bab9S3VersionKeyFDABEE9B" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParametersdcdc759e2644fb3c4847d9a160ce99f0f40f137c825ae9cc094323ed4839bab9S3VersionKeyFDABEE9B" + } + ] + } + ] + } + ] + ] + }, + "Parameters": { + "referencetoawscdkeksclustertestAssetParameters26ac61b4195cccf80ff73f332788ad7ffaab36d81ce570340a583a8364901665S3Bucket1771F046Ref": { + "Ref": "AssetParameters26ac61b4195cccf80ff73f332788ad7ffaab36d81ce570340a583a8364901665S3Bucket1B280681" + }, + "referencetoawscdkeksclustertestAssetParameters26ac61b4195cccf80ff73f332788ad7ffaab36d81ce570340a583a8364901665S3VersionKeyDA854AFERef": { + "Ref": "AssetParameters26ac61b4195cccf80ff73f332788ad7ffaab36d81ce570340a583a8364901665S3VersionKeyB1E02791" + }, + "referencetoawscdkeksclustertestClusterCreationRole95F44854Arn": { + "Fn::GetAtt": [ + "ClusterCreationRole360249B6", + "Arn" + ] + }, + "referencetoawscdkeksclustertestAssetParameters5afea6e8e6c743a8d1766f21465e28d471e56bcb95c5970054b0514bc62a3720S3BucketDA4E9DCDRef": { + "Ref": "AssetParameters5afea6e8e6c743a8d1766f21465e28d471e56bcb95c5970054b0514bc62a3720S3Bucket3B443230" + }, + "referencetoawscdkeksclustertestAssetParameters5afea6e8e6c743a8d1766f21465e28d471e56bcb95c5970054b0514bc62a3720S3VersionKey6F8004B6Ref": { + "Ref": "AssetParameters5afea6e8e6c743a8d1766f21465e28d471e56bcb95c5970054b0514bc62a3720S3VersionKeyAA4674FB" + }, + "referencetoawscdkeksclustertestAssetParametersdaeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1S3Bucket0815E7B5Ref": { + "Ref": "AssetParametersdaeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1S3BucketDC4B98B1" + }, + "referencetoawscdkeksclustertestAssetParametersdaeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1S3VersionKey657736ADRef": { + "Ref": "AssetParametersdaeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1S3VersionKeyA495226F" + } + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "awscdkawseksKubectlProviderNestedStackawscdkawseksKubectlProviderNestedStackResourceA7AEBA6B": { + "Type": "AWS::CloudFormation::Stack", + "Properties": { + "TemplateURL": { + "Fn::Join": [ + "", + [ + "https://s3.test-region.", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Ref": "AssetParameters8a135d8a645edaff330758972da87b3dddc295ce07475e8d9ea8fad8c35dcb22S3Bucket0782C98E" + }, + "/", + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters8a135d8a645edaff330758972da87b3dddc295ce07475e8d9ea8fad8c35dcb22S3VersionKey5E9D14CC" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters8a135d8a645edaff330758972da87b3dddc295ce07475e8d9ea8fad8c35dcb22S3VersionKey5E9D14CC" + } + ] + } + ] + } + ] + ] + }, + "Parameters": { + "referencetoawscdkeksclustertestClusterD76DFF87Arn": { + "Fn::GetAtt": [ + "Cluster9EE0221C", + "Arn" + ] + }, + "referencetoawscdkeksclustertestClusterCreationRole95F44854Arn": { + "Fn::GetAtt": [ + "ClusterCreationRole360249B6", + "Arn" + ] + }, + "referencetoawscdkeksclustertestAssetParameters4129bbca38164ecb28fee8e5b674f0d05e5957b4b8ed97d9c950527b5cc4ce10S3Bucket3929FA93Ref": { + "Ref": "AssetParameters4129bbca38164ecb28fee8e5b674f0d05e5957b4b8ed97d9c950527b5cc4ce10S3BucketC6FAEEC9" + }, + "referencetoawscdkeksclustertestAssetParameters4129bbca38164ecb28fee8e5b674f0d05e5957b4b8ed97d9c950527b5cc4ce10S3VersionKey14530D6BRef": { + "Ref": "AssetParameters4129bbca38164ecb28fee8e5b674f0d05e5957b4b8ed97d9c950527b5cc4ce10S3VersionKeyA7EE7421" + }, + "referencetoawscdkeksclustertestVpcPrivateSubnet1Subnet32A4EC2ARef": { + "Ref": "VpcPrivateSubnet1Subnet536B997A" + }, + "referencetoawscdkeksclustertestVpcPrivateSubnet2Subnet5CC53627Ref": { + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" + }, + "referencetoawscdkeksclustertestVpcPrivateSubnet3Subnet7F5D6918Ref": { + "Ref": "VpcPrivateSubnet3SubnetF258B56E" + }, + "referencetoawscdkeksclustertestClusterD76DFF87ClusterSecurityGroupId": { + "Fn::GetAtt": [ + "Cluster9EE0221C", + "ClusterSecurityGroupId" + ] + }, + "referencetoawscdkeksclustertestAssetParameterse9882ab123687399f934da0d45effe675ecc8ce13b40cb946f3e1d6141fe8d68S3BucketB4E9C142Ref": { + "Ref": "AssetParameterse9882ab123687399f934da0d45effe675ecc8ce13b40cb946f3e1d6141fe8d68S3BucketAEADE8C7" + }, + "referencetoawscdkeksclustertestAssetParameterse9882ab123687399f934da0d45effe675ecc8ce13b40cb946f3e1d6141fe8d68S3VersionKey1C7C1F5FRef": { + "Ref": "AssetParameterse9882ab123687399f934da0d45effe675ecc8ce13b40cb946f3e1d6141fe8d68S3VersionKeyE415415F" + }, + "referencetoawscdkeksclustertestAssetParametersea17febe6d04c66048f3e8e060c71685c0cb53122abceff44842d27bc0d4a03eS3Bucket6ADB5CE5Ref": { + "Ref": "AssetParametersea17febe6d04c66048f3e8e060c71685c0cb53122abceff44842d27bc0d4a03eS3BucketD3288998" + }, + "referencetoawscdkeksclustertestAssetParametersea17febe6d04c66048f3e8e060c71685c0cb53122abceff44842d27bc0d4a03eS3VersionKey314C5B11Ref": { + "Ref": "AssetParametersea17febe6d04c66048f3e8e060c71685c0cb53122abceff44842d27bc0d4a03eS3VersionKeyB00C0565" + }, + "referencetoawscdkeksclustertestAssetParametersdaeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1S3Bucket0815E7B5Ref": { + "Ref": "AssetParametersdaeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1S3BucketDC4B98B1" + }, + "referencetoawscdkeksclustertestAssetParametersdaeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1S3VersionKey657736ADRef": { + "Ref": "AssetParametersdaeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1S3VersionKeyA495226F" + } + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + } + }, + "Outputs": { + "ClusterConfigCommand43AAE40F": { + "Value": { + "Fn::Join": [ + "", + [ + "aws eks update-kubeconfig --name ", + { + "Ref": "Cluster9EE0221C" + }, + " --region test-region --role-arn ", + { + "Fn::GetAtt": [ + "AdminRole38563C57", + "Arn" + ] + } + ] + ] + } + }, + "ClusterGetTokenCommand06AE992E": { + "Value": { + "Fn::Join": [ + "", + [ + "aws eks get-token --cluster-name ", + { + "Ref": "Cluster9EE0221C" + }, + " --region test-region --role-arn ", + { + "Fn::GetAtt": [ + "AdminRole38563C57", + "Arn" + ] + } + ] + ] + } + } + }, + "Parameters": { + "AssetParameters26ac61b4195cccf80ff73f332788ad7ffaab36d81ce570340a583a8364901665S3Bucket1B280681": { + "Type": "String", + "Description": "S3 bucket for asset \"26ac61b4195cccf80ff73f332788ad7ffaab36d81ce570340a583a8364901665\"" + }, + "AssetParameters26ac61b4195cccf80ff73f332788ad7ffaab36d81ce570340a583a8364901665S3VersionKeyB1E02791": { + "Type": "String", + "Description": "S3 key for asset version \"26ac61b4195cccf80ff73f332788ad7ffaab36d81ce570340a583a8364901665\"" + }, + "AssetParameters26ac61b4195cccf80ff73f332788ad7ffaab36d81ce570340a583a8364901665ArtifactHash9EA5AC29": { + "Type": "String", + "Description": "Artifact hash for asset \"26ac61b4195cccf80ff73f332788ad7ffaab36d81ce570340a583a8364901665\"" + }, + "AssetParameters5afea6e8e6c743a8d1766f21465e28d471e56bcb95c5970054b0514bc62a3720S3Bucket3B443230": { + "Type": "String", + "Description": "S3 bucket for asset \"5afea6e8e6c743a8d1766f21465e28d471e56bcb95c5970054b0514bc62a3720\"" + }, + "AssetParameters5afea6e8e6c743a8d1766f21465e28d471e56bcb95c5970054b0514bc62a3720S3VersionKeyAA4674FB": { + "Type": "String", + "Description": "S3 key for asset version \"5afea6e8e6c743a8d1766f21465e28d471e56bcb95c5970054b0514bc62a3720\"" + }, + "AssetParameters5afea6e8e6c743a8d1766f21465e28d471e56bcb95c5970054b0514bc62a3720ArtifactHash3D7A279D": { + "Type": "String", + "Description": "Artifact hash for asset \"5afea6e8e6c743a8d1766f21465e28d471e56bcb95c5970054b0514bc62a3720\"" + }, + "AssetParametersdaeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1S3BucketDC4B98B1": { + "Type": "String", + "Description": "S3 bucket for asset \"daeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1\"" + }, + "AssetParametersdaeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1S3VersionKeyA495226F": { + "Type": "String", + "Description": "S3 key for asset version \"daeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1\"" + }, + "AssetParametersdaeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1ArtifactHashA521A16F": { + "Type": "String", + "Description": "Artifact hash for asset \"daeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1\"" + }, + "AssetParameters4129bbca38164ecb28fee8e5b674f0d05e5957b4b8ed97d9c950527b5cc4ce10S3BucketC6FAEEC9": { + "Type": "String", + "Description": "S3 bucket for asset \"4129bbca38164ecb28fee8e5b674f0d05e5957b4b8ed97d9c950527b5cc4ce10\"" + }, + "AssetParameters4129bbca38164ecb28fee8e5b674f0d05e5957b4b8ed97d9c950527b5cc4ce10S3VersionKeyA7EE7421": { + "Type": "String", + "Description": "S3 key for asset version \"4129bbca38164ecb28fee8e5b674f0d05e5957b4b8ed97d9c950527b5cc4ce10\"" + }, + "AssetParameters4129bbca38164ecb28fee8e5b674f0d05e5957b4b8ed97d9c950527b5cc4ce10ArtifactHash528547CD": { + "Type": "String", + "Description": "Artifact hash for asset \"4129bbca38164ecb28fee8e5b674f0d05e5957b4b8ed97d9c950527b5cc4ce10\"" + }, + "AssetParameterse9882ab123687399f934da0d45effe675ecc8ce13b40cb946f3e1d6141fe8d68S3BucketAEADE8C7": { + "Type": "String", + "Description": "S3 bucket for asset \"e9882ab123687399f934da0d45effe675ecc8ce13b40cb946f3e1d6141fe8d68\"" + }, + "AssetParameterse9882ab123687399f934da0d45effe675ecc8ce13b40cb946f3e1d6141fe8d68S3VersionKeyE415415F": { + "Type": "String", + "Description": "S3 key for asset version \"e9882ab123687399f934da0d45effe675ecc8ce13b40cb946f3e1d6141fe8d68\"" + }, + "AssetParameterse9882ab123687399f934da0d45effe675ecc8ce13b40cb946f3e1d6141fe8d68ArtifactHashD9A515C3": { + "Type": "String", + "Description": "Artifact hash for asset \"e9882ab123687399f934da0d45effe675ecc8ce13b40cb946f3e1d6141fe8d68\"" + }, + "AssetParametersea17febe6d04c66048f3e8e060c71685c0cb53122abceff44842d27bc0d4a03eS3BucketD3288998": { + "Type": "String", + "Description": "S3 bucket for asset \"ea17febe6d04c66048f3e8e060c71685c0cb53122abceff44842d27bc0d4a03e\"" + }, + "AssetParametersea17febe6d04c66048f3e8e060c71685c0cb53122abceff44842d27bc0d4a03eS3VersionKeyB00C0565": { + "Type": "String", + "Description": "S3 key for asset version \"ea17febe6d04c66048f3e8e060c71685c0cb53122abceff44842d27bc0d4a03e\"" + }, + "AssetParametersea17febe6d04c66048f3e8e060c71685c0cb53122abceff44842d27bc0d4a03eArtifactHash4654D012": { + "Type": "String", + "Description": "Artifact hash for asset \"ea17febe6d04c66048f3e8e060c71685c0cb53122abceff44842d27bc0d4a03e\"" + }, + "AssetParametersdcdc759e2644fb3c4847d9a160ce99f0f40f137c825ae9cc094323ed4839bab9S3BucketA775E312": { + "Type": "String", + "Description": "S3 bucket for asset \"dcdc759e2644fb3c4847d9a160ce99f0f40f137c825ae9cc094323ed4839bab9\"" + }, + "AssetParametersdcdc759e2644fb3c4847d9a160ce99f0f40f137c825ae9cc094323ed4839bab9S3VersionKeyFDABEE9B": { + "Type": "String", + "Description": "S3 key for asset version \"dcdc759e2644fb3c4847d9a160ce99f0f40f137c825ae9cc094323ed4839bab9\"" + }, + "AssetParametersdcdc759e2644fb3c4847d9a160ce99f0f40f137c825ae9cc094323ed4839bab9ArtifactHashBC5BD0D7": { + "Type": "String", + "Description": "Artifact hash for asset \"dcdc759e2644fb3c4847d9a160ce99f0f40f137c825ae9cc094323ed4839bab9\"" + }, + "AssetParameters8a135d8a645edaff330758972da87b3dddc295ce07475e8d9ea8fad8c35dcb22S3Bucket0782C98E": { + "Type": "String", + "Description": "S3 bucket for asset \"8a135d8a645edaff330758972da87b3dddc295ce07475e8d9ea8fad8c35dcb22\"" + }, + "AssetParameters8a135d8a645edaff330758972da87b3dddc295ce07475e8d9ea8fad8c35dcb22S3VersionKey5E9D14CC": { + "Type": "String", + "Description": "S3 key for asset version \"8a135d8a645edaff330758972da87b3dddc295ce07475e8d9ea8fad8c35dcb22\"" + }, + "AssetParameters8a135d8a645edaff330758972da87b3dddc295ce07475e8d9ea8fad8c35dcb22ArtifactHash75F0D468": { + "Type": "String", + "Description": "Artifact hash for asset \"8a135d8a645edaff330758972da87b3dddc295ce07475e8d9ea8fad8c35dcb22\"" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-eks/test/integ.eks-bottlerocket-ng.ts b/packages/@aws-cdk/aws-eks/test/integ.eks-bottlerocket-ng.ts new file mode 100644 index 0000000000000..d27d92d984d4c --- /dev/null +++ b/packages/@aws-cdk/aws-eks/test/integ.eks-bottlerocket-ng.ts @@ -0,0 +1,47 @@ +/// !cdk-integ pragma:ignore-assets +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as iam from '@aws-cdk/aws-iam'; +import { App } from '@aws-cdk/core'; +import * as eks from '../lib'; +import { NodegroupAmiType } from '../lib'; +import { TestStack } from './util'; + + +class EksClusterStack extends TestStack { + + private cluster: eks.Cluster; + private vpc: ec2.IVpc; + + constructor(scope: App, id: string) { + super(scope, id); + + // allow all account users to assume this role in order to admin the cluster + const mastersRole = new iam.Role(this, 'AdminRole', { + assumedBy: new iam.AccountRootPrincipal(), + }); + + // just need one nat gateway to simplify the test + this.vpc = new ec2.Vpc(this, 'Vpc', { maxAzs: 3, natGateways: 1 }); + + // create the cluster with a default nodegroup capacity + this.cluster = new eks.Cluster(this, 'Cluster', { + vpc: this.vpc, + mastersRole, + defaultCapacity: 0, + version: eks.KubernetesVersion.V1_21, + }); + + this.cluster.addNodegroupCapacity('BottlerocketNG1', { + amiType: NodegroupAmiType.BOTTLEROCKET_X86_64, + }); + this.cluster.addNodegroupCapacity('BottlerocketNG2', { + amiType: NodegroupAmiType.BOTTLEROCKET_ARM_64, + }); + } +} + +const app = new App(); + +new EksClusterStack(app, 'aws-cdk-eks-cluster-test'); + +app.synth(); diff --git a/packages/@aws-cdk/aws-eks/test/nodegroup.test.ts b/packages/@aws-cdk/aws-eks/test/nodegroup.test.ts index 47ef518f68032..2f188ad393d34 100644 --- a/packages/@aws-cdk/aws-eks/test/nodegroup.test.ts +++ b/packages/@aws-cdk/aws-eks/test/nodegroup.test.ts @@ -2,6 +2,7 @@ import '@aws-cdk/assert-internal/jest'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as cdk from '@aws-cdk/core'; import * as eks from '../lib'; +import { NodegroupAmiType } from '../lib'; import { testFixture } from './util'; /* eslint-disable max-len */ @@ -99,7 +100,7 @@ describe('node group', () => { }); - test('create nodegroup correctly', () => { + test('create a default nodegroup correctly', () => { // GIVEN const { stack, vpc } = testFixture(); @@ -139,6 +140,97 @@ describe('node group', () => { }); + }); + + test('create a x86_64 bottlerocket nodegroup correctly', () => { + // GIVEN + const { stack, vpc } = testFixture(); + + // WHEN + const cluster = new eks.Cluster(stack, 'Cluster', { + vpc, + defaultCapacity: 0, + version: CLUSTER_VERSION, + }); + new eks.Nodegroup(stack, 'Nodegroup', { + cluster, + amiType: NodegroupAmiType.BOTTLEROCKET_X86_64, + }); + + // THEN + expect(stack).toHaveResourceLike('AWS::EKS::Nodegroup', { + ClusterName: { + Ref: 'Cluster9EE0221C', + }, + NodeRole: { + 'Fn::GetAtt': [ + 'NodegroupNodeGroupRole038A128B', + 'Arn', + ], + }, + Subnets: [ + { + Ref: 'VPCPrivateSubnet1Subnet8BCA10E0', + }, + { + Ref: 'VPCPrivateSubnet2SubnetCFCDAA7A', + }, + ], + AmiType: 'BOTTLEROCKET_x86_64', + ForceUpdateEnabled: true, + ScalingConfig: { + DesiredSize: 2, + MaxSize: 2, + MinSize: 1, + }, + }); + + + }); + test('create a ARM_64 bottlerocket nodegroup correctly', () => { + // GIVEN + const { stack, vpc } = testFixture(); + + // WHEN + const cluster = new eks.Cluster(stack, 'Cluster', { + vpc, + defaultCapacity: 0, + version: CLUSTER_VERSION, + }); + new eks.Nodegroup(stack, 'Nodegroup', { + cluster, + amiType: NodegroupAmiType.BOTTLEROCKET_ARM_64, + }); + + // THEN + expect(stack).toHaveResourceLike('AWS::EKS::Nodegroup', { + ClusterName: { + Ref: 'Cluster9EE0221C', + }, + NodeRole: { + 'Fn::GetAtt': [ + 'NodegroupNodeGroupRole038A128B', + 'Arn', + ], + }, + Subnets: [ + { + Ref: 'VPCPrivateSubnet1Subnet8BCA10E0', + }, + { + Ref: 'VPCPrivateSubnet2SubnetCFCDAA7A', + }, + ], + AmiType: 'BOTTLEROCKET_ARM_64', + ForceUpdateEnabled: true, + ScalingConfig: { + DesiredSize: 2, + MaxSize: 2, + MinSize: 1, + }, + }); + + }); test('aws-auth will be updated', () => { // GIVEN diff --git a/packages/@aws-cdk/aws-eks/test/pinger/pinger.ts b/packages/@aws-cdk/aws-eks/test/pinger/pinger.ts index 1165da1ca90df..3702484a68339 100644 --- a/packages/@aws-cdk/aws-eks/test/pinger/pinger.ts +++ b/packages/@aws-cdk/aws-eks/test/pinger/pinger.ts @@ -12,6 +12,7 @@ export interface PingerProps { readonly url: string; readonly securityGroup?: ec2.SecurityGroup; readonly vpc?: ec2.IVpc; + readonly subnets?: ec2.ISubnet[]; } export class Pinger extends CoreConstruct { @@ -25,6 +26,7 @@ export class Pinger extends CoreConstruct { handler: 'index.handler', runtime: lambda.Runtime.PYTHON_3_6, vpc: props.vpc, + vpcSubnets: props.subnets ? { subnets: props.subnets } : undefined, securityGroups: props.securityGroup ? [props.securityGroup] : undefined, timeout: Duration.minutes(10), }); diff --git a/packages/@aws-cdk/aws-iot-actions/NOTICE b/packages/@aws-cdk/aws-iot-actions/NOTICE index 5fc3826926b5b..39cd25bf899ae 100644 --- a/packages/@aws-cdk/aws-iot-actions/NOTICE +++ b/packages/@aws-cdk/aws-iot-actions/NOTICE @@ -1,2 +1,32 @@ AWS Cloud Development Kit (AWS CDK) Copyright 2018-2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +------------------------------------------------------------------------------- + +The AWS CDK includes the following third-party software/licensing: + +** case - https://www.npmjs.com/package/case +Copyright (c) 2013 Nathan Bubna + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +---------------- diff --git a/packages/@aws-cdk/aws-iot-actions/README.md b/packages/@aws-cdk/aws-iot-actions/README.md index b18182a80a9ad..e02f67cee0d45 100644 --- a/packages/@aws-cdk/aws-iot-actions/README.md +++ b/packages/@aws-cdk/aws-iot-actions/README.md @@ -22,6 +22,9 @@ supported AWS Services. Instances of these classes should be passed to Currently supported are: - Invoke a Lambda function +- Put objects to a S3 bucket +- Put logs to CloudWatch Logs +- Put records to Kinesis Data Firehose stream ## Invoke a Lambda function @@ -49,6 +52,59 @@ new iot.TopicRule(this, 'TopicRule', { }); ``` +## Put objects to a S3 bucket + +The code snippet below creates an AWS IoT Rule that put objects to a S3 bucket +when it is triggered. + +```ts +import * as iot from '@aws-cdk/aws-iot'; +import * as actions from '@aws-cdk/aws-iot-actions'; +import * as s3 from '@aws-cdk/aws-s3'; + +const bucket = new s3.Bucket(this, 'MyBucket'); + +new iot.TopicRule(this, 'TopicRule', { + sql: iot.IotSql.fromStringAsVer20160323("SELECT topic(2) as device_id FROM 'device/+/data'"), + actions: [new actions.S3PutObjectAction(bucket)], +}); +``` + +The property `key` of `S3PutObjectAction` is given the value `${topic()}/${timestamp()}` by default. This `${topic()}` +and `${timestamp()}` is called Substitution templates. For more information see +[this documentation](https://docs.aws.amazon.com/iot/latest/developerguide/iot-substitution-templates.html). +In above sample, `${topic()}` is replaced by a given MQTT topic as `device/001/data`. And `${timestamp()}` is replaced +by the number of the current timestamp in milliseconds as `1636289461203`. So if the MQTT broker receives an MQTT topic +`device/001/data` on `2021-11-07T00:00:00.000Z`, the S3 bucket object will be put to `device/001/data/1636243200000`. + +You can also set specific `key` as following: + +```ts +new iot.TopicRule(this, 'TopicRule', { + sql: iot.IotSql.fromStringAsVer20160323( + "SELECT topic(2) as device_id, year, month, day FROM 'device/+/data'", + ), + actions: [ + new actions.S3PutObjectAction(bucket, { + key: '${year}/${month}/${day}/${topic(2)}', + }), + ], +}); +``` + +If you wanna set access control to the S3 bucket object, you can specify `accessControl` as following: + +```ts +new iot.TopicRule(this, 'TopicRule', { + sql: iot.IotSql.fromStringAsVer20160323("SELECT * FROM 'device/+/data'"), + actions: [ + new actions.S3PutObjectAction(bucket, { + accessControl: s3.BucketAccessControl.PUBLIC_READ, + }), + ], +}); +``` + ## Put logs to CloudWatch Logs The code snippet below creates an AWS IoT Rule that put logs to CloudWatch Logs @@ -66,3 +122,32 @@ new iot.TopicRule(this, 'TopicRule', { actions: [new actions.CloudWatchLogsAction(logGroup)], }); ``` + + +## Put records to Kinesis Data Firehose stream + +The code snippet below creates an AWS IoT Rule that put records to Put records +to Kinesis Data Firehose stream when it is triggered. + +```ts +import * as iot from '@aws-cdk/aws-iot'; +import * as actions from '@aws-cdk/aws-iot-actions'; +import * as s3 from '@aws-cdk/aws-s3'; +import * as firehose from '@aws-cdk/aws-kinesisfirehose'; +import * as destinations from '@aws-cdk/aws-kinesisfirehose-destinations'; + +const bucket = new s3.Bucket(this, 'MyBucket'); +const stream = new firehose.DeliveryStream(this, 'MyStream', { + destinations: [new destinations.S3Bucket(bucket)], +}); + +const topicRule = new iot.TopicRule(this, 'TopicRule', { + sql: iot.IotSql.fromStringAsVer20160323("SELECT * FROM 'device/+/data'"), + actions: [ + new actions.FirehoseStreamAction(stream, { + batchMode: true, + recordSeparator: actions.FirehoseStreamRecordSeparator.NEWLINE, + }), + ], +}); +``` diff --git a/packages/@aws-cdk/aws-iot-actions/lib/cloudwatch-logs-action.ts b/packages/@aws-cdk/aws-iot-actions/lib/cloudwatch-logs-action.ts index dda14de887774..fb8f2779f32e7 100644 --- a/packages/@aws-cdk/aws-iot-actions/lib/cloudwatch-logs-action.ts +++ b/packages/@aws-cdk/aws-iot-actions/lib/cloudwatch-logs-action.ts @@ -1,18 +1,13 @@ import * as iam from '@aws-cdk/aws-iam'; import * as iot from '@aws-cdk/aws-iot'; import * as logs from '@aws-cdk/aws-logs'; +import { CommonActionProps } from './common-action-props'; import { singletonActionRole } from './private/role'; /** * Configuration properties of an action for CloudWatch Logs. */ -export interface CloudWatchLogsActionProps { - /** - * The IAM role that allows access to the CloudWatch log group. - * - * @default a new role will be created - */ - readonly role?: iam.IRole; +export interface CloudWatchLogsActionProps extends CommonActionProps { } /** diff --git a/packages/@aws-cdk/aws-iot-actions/lib/common-action-props.ts b/packages/@aws-cdk/aws-iot-actions/lib/common-action-props.ts new file mode 100644 index 0000000000000..5a9b52d8b5f27 --- /dev/null +++ b/packages/@aws-cdk/aws-iot-actions/lib/common-action-props.ts @@ -0,0 +1,13 @@ +import * as iam from '@aws-cdk/aws-iam'; + +/** + * Common properties shared by Actions it access to AWS service. + */ +export interface CommonActionProps { + /** + * The IAM role that allows access to AWS service. + * + * @default a new role will be created + */ + readonly role?: iam.IRole; +} diff --git a/packages/@aws-cdk/aws-iot-actions/lib/firehose-stream-action.ts b/packages/@aws-cdk/aws-iot-actions/lib/firehose-stream-action.ts new file mode 100644 index 0000000000000..c694bef7cad38 --- /dev/null +++ b/packages/@aws-cdk/aws-iot-actions/lib/firehose-stream-action.ts @@ -0,0 +1,88 @@ +import * as iam from '@aws-cdk/aws-iam'; +import * as iot from '@aws-cdk/aws-iot'; +import * as firehose from '@aws-cdk/aws-kinesisfirehose'; +import { CommonActionProps } from './common-action-props'; +import { singletonActionRole } from './private/role'; + +/** + * Record Separator to be used to separate records. + */ +export enum FirehoseStreamRecordSeparator { + /** + * Separate by a new line + */ + NEWLINE = '\n', + + /** + * Separate by a tab + */ + TAB = '\t', + + /** + * Separate by a windows new line + */ + WINDOWS_NEWLINE = '\r\n', + + /** + * Separate by a commma + */ + COMMA = ',', +} + +/** + * Configuration properties of an action for the Kinesis Data Firehose stream. + */ +export interface FirehoseStreamActionProps extends CommonActionProps { + /** + * Whether to deliver the Kinesis Data Firehose stream as a batch by using `PutRecordBatch`. + * When batchMode is true and the rule's SQL statement evaluates to an Array, each Array + * element forms one record in the PutRecordBatch request. The resulting array can't have + * more than 500 records. + * + * @default false + */ + readonly batchMode?: boolean; + + /** + * A character separator that will be used to separate records written to the Kinesis Data Firehose stream. + * + * @default - none -- the stream does not use a separator + */ + readonly recordSeparator?: FirehoseStreamRecordSeparator; +} + + +/** + * The action to put the record from an MQTT message to the Kinesis Data Firehose stream. + */ +export class FirehoseStreamAction implements iot.IAction { + private readonly batchMode?: boolean; + private readonly recordSeparator?: string; + private readonly role?: iam.IRole; + + /** + * @param stream The Kinesis Data Firehose stream to which to put records. + * @param props Optional properties to not use default + */ + constructor(private readonly stream: firehose.IDeliveryStream, props: FirehoseStreamActionProps = {}) { + this.batchMode = props.batchMode; + this.recordSeparator = props.recordSeparator; + this.role = props.role; + } + + bind(rule: iot.ITopicRule): iot.ActionConfig { + const role = this.role ?? singletonActionRole(rule); + this.stream.grantPutRecords(role); + + return { + configuration: { + firehose: { + batchMode: this.batchMode, + deliveryStreamName: this.stream.deliveryStreamName, + roleArn: role.roleArn, + separator: this.recordSeparator, + }, + }, + }; + } +} diff --git a/packages/@aws-cdk/aws-iot-actions/lib/index.ts b/packages/@aws-cdk/aws-iot-actions/lib/index.ts index ef917fd0e2181..ce74a2ff2b685 100644 --- a/packages/@aws-cdk/aws-iot-actions/lib/index.ts +++ b/packages/@aws-cdk/aws-iot-actions/lib/index.ts @@ -1,2 +1,5 @@ export * from './cloudwatch-logs-action'; +export * from './common-action-props'; +export * from './firehose-stream-action'; export * from './lambda-function-action'; +export * from './s3-put-object-action'; diff --git a/packages/@aws-cdk/aws-iot-actions/lib/s3-put-object-action.ts b/packages/@aws-cdk/aws-iot-actions/lib/s3-put-object-action.ts new file mode 100644 index 0000000000000..f690bf813a922 --- /dev/null +++ b/packages/@aws-cdk/aws-iot-actions/lib/s3-put-object-action.ts @@ -0,0 +1,67 @@ +import * as iam from '@aws-cdk/aws-iam'; +import * as iot from '@aws-cdk/aws-iot'; +import * as s3 from '@aws-cdk/aws-s3'; +import { kebab as toKebabCase } from 'case'; +import { CommonActionProps } from './common-action-props'; +import { singletonActionRole } from './private/role'; + +/** + * Configuration properties of an action for s3. + */ +export interface S3PutObjectActionProps extends CommonActionProps { + /** + * The Amazon S3 canned ACL that controls access to the object identified by the object key. + * @see https://docs.aws.amazon.com/AmazonS3/latest/userguide/acl-overview.html#canned-acl + * + * @default None + */ + readonly accessControl?: s3.BucketAccessControl; + + /** + * The path to the file where the data is written. + * + * Supports substitution templates. + * @see https://docs.aws.amazon.com/iot/latest/developerguide/iot-substitution-templates.html + * + * @default '${topic()}/${timestamp()}' + */ + readonly key?: string; +} + +/** + * The action to write the data from an MQTT message to an Amazon S3 bucket. + */ +export class S3PutObjectAction implements iot.IAction { + private readonly accessControl?: string; + private readonly key?: string; + private readonly role?: iam.IRole; + + /** + * @param bucket The Amazon S3 bucket to which to write data. + * @param props Optional properties to not use default + */ + constructor(private readonly bucket: s3.IBucket, props: S3PutObjectActionProps = {}) { + this.accessControl = props.accessControl; + this.key = props.key; + this.role = props.role; + } + + bind(rule: iot.ITopicRule): iot.ActionConfig { + const role = this.role ?? singletonActionRole(rule); + role.addToPrincipalPolicy(new iam.PolicyStatement({ + actions: ['s3:PutObject'], + resources: [this.bucket.arnForObjects('*')], + })); + + return { + configuration: { + s3: { + bucketName: this.bucket.bucketName, + cannedAcl: this.accessControl && toKebabCase(this.accessControl.toString()), + key: this.key ?? '${topic()}/${timestamp()}', + roleArn: role.roleArn, + }, + }, + }; + } +} diff --git a/packages/@aws-cdk/aws-iot-actions/package.json b/packages/@aws-cdk/aws-iot-actions/package.json index fb40db84577c3..b996897b7719d 100644 --- a/packages/@aws-cdk/aws-iot-actions/package.json +++ b/packages/@aws-cdk/aws-iot-actions/package.json @@ -71,6 +71,7 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assertions": "0.0.0", + "@aws-cdk/aws-kinesisfirehose-destinations": "0.0.0", "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cdk-integ-tools": "0.0.0", "@aws-cdk/pkglint": "0.0.0", @@ -81,20 +82,28 @@ "dependencies": { "@aws-cdk/aws-iam": "0.0.0", "@aws-cdk/aws-iot": "0.0.0", + "@aws-cdk/aws-kinesisfirehose": "0.0.0", "@aws-cdk/aws-lambda": "0.0.0", "@aws-cdk/aws-logs": "0.0.0", + "@aws-cdk/aws-s3": "0.0.0", "@aws-cdk/core": "0.0.0", + "case": "1.6.3", "constructs": "^3.3.69" }, "homepage": "https://github.com/aws/aws-cdk", "peerDependencies": { "@aws-cdk/aws-iam": "0.0.0", "@aws-cdk/aws-iot": "0.0.0", + "@aws-cdk/aws-kinesisfirehose": "0.0.0", "@aws-cdk/aws-lambda": "0.0.0", "@aws-cdk/aws-logs": "0.0.0", + "@aws-cdk/aws-s3": "0.0.0", "@aws-cdk/core": "0.0.0", "constructs": "^3.3.69" }, + "bundledDependencies": [ + "case" + ], "engines": { "node": ">= 10.13.0 <13 || >=13.7.0" }, diff --git a/packages/@aws-cdk/aws-iot-actions/test/cloudwatch-logs/cloudwatch-logs-action.test.ts b/packages/@aws-cdk/aws-iot-actions/test/cloudwatch/cloudwatch-logs-action.test.ts similarity index 86% rename from packages/@aws-cdk/aws-iot-actions/test/cloudwatch-logs/cloudwatch-logs-action.test.ts rename to packages/@aws-cdk/aws-iot-actions/test/cloudwatch/cloudwatch-logs-action.test.ts index 3b6ecd2d57fbc..4499cdd35d6f1 100644 --- a/packages/@aws-cdk/aws-iot-actions/test/cloudwatch-logs/cloudwatch-logs-action.test.ts +++ b/packages/@aws-cdk/aws-iot-actions/test/cloudwatch/cloudwatch-logs-action.test.ts @@ -1,4 +1,4 @@ -import { Template } from '@aws-cdk/assertions'; +import { Template, Match } from '@aws-cdk/assertions'; import * as iam from '@aws-cdk/aws-iam'; import * as iot from '@aws-cdk/aws-iot'; import * as logs from '@aws-cdk/aws-logs'; @@ -95,32 +95,12 @@ test('can set role', () => { Template.fromStack(stack).hasResourceProperties('AWS::IoT::TopicRule', { TopicRulePayload: { Actions: [ - { - CloudwatchLogs: { - LogGroupName: 'my-log-group', - RoleArn: 'arn:aws:iam::123456789012:role/ForTest', - }, - }, + Match.objectLike({ CloudwatchLogs: { RoleArn: 'arn:aws:iam::123456789012:role/ForTest' } }), ], }, }); Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { - PolicyDocument: { - Statement: [ - { - Action: ['logs:CreateLogStream', 'logs:PutLogEvents'], - Effect: 'Allow', - Resource: 'arn:aws:logs:us-east-1:123456789012:log-group:my-log-group:*', - }, - { - Action: 'logs:DescribeLogStreams', - Effect: 'Allow', - Resource: 'arn:aws:logs:us-east-1:123456789012:log-group:my-log-group:*', - }, - ], - Version: '2012-10-17', - }, PolicyName: 'MyRolePolicy64AB00A5', Roles: ['ForTest'], }); diff --git a/packages/@aws-cdk/aws-iot-actions/test/cloudwatch-logs/integ.cloudwatch-logs-action.expected.json b/packages/@aws-cdk/aws-iot-actions/test/cloudwatch/integ.cloudwatch-logs-action.expected.json similarity index 100% rename from packages/@aws-cdk/aws-iot-actions/test/cloudwatch-logs/integ.cloudwatch-logs-action.expected.json rename to packages/@aws-cdk/aws-iot-actions/test/cloudwatch/integ.cloudwatch-logs-action.expected.json diff --git a/packages/@aws-cdk/aws-iot-actions/test/cloudwatch-logs/integ.cloudwatch-logs-action.ts b/packages/@aws-cdk/aws-iot-actions/test/cloudwatch/integ.cloudwatch-logs-action.ts similarity index 100% rename from packages/@aws-cdk/aws-iot-actions/test/cloudwatch-logs/integ.cloudwatch-logs-action.ts rename to packages/@aws-cdk/aws-iot-actions/test/cloudwatch/integ.cloudwatch-logs-action.ts diff --git a/packages/@aws-cdk/aws-iot-actions/test/kinesis-firehose/firehose-stream-action.test.ts b/packages/@aws-cdk/aws-iot-actions/test/kinesis-firehose/firehose-stream-action.test.ts new file mode 100644 index 0000000000000..2941cc1db270c --- /dev/null +++ b/packages/@aws-cdk/aws-iot-actions/test/kinesis-firehose/firehose-stream-action.test.ts @@ -0,0 +1,143 @@ +import { Template, Match } from '@aws-cdk/assertions'; +import * as iam from '@aws-cdk/aws-iam'; +import * as iot from '@aws-cdk/aws-iot'; +import * as firehose from '@aws-cdk/aws-kinesisfirehose'; +import * as cdk from '@aws-cdk/core'; +import * as actions from '../../lib'; + +test('Default firehose stream action', () => { + // GIVEN + const stack = new cdk.Stack(); + const topicRule = new iot.TopicRule(stack, 'MyTopicRule', { + sql: iot.IotSql.fromStringAsVer20160323("SELECT topic(2) as device_id FROM 'device/+/data'"), + }); + const stream = firehose.DeliveryStream.fromDeliveryStreamArn(stack, 'MyStream', 'arn:aws:firehose:xx-west-1:111122223333:deliverystream/my-stream'); + + // WHEN + topicRule.addAction( + new actions.FirehoseStreamAction(stream), + ); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::IoT::TopicRule', { + TopicRulePayload: { + Actions: [ + { + Firehose: { + DeliveryStreamName: 'my-stream', + RoleArn: { + 'Fn::GetAtt': ['MyTopicRuleTopicRuleActionRoleCE2D05DA', 'Arn'], + }, + }, + }, + ], + }, + }); + + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: 'iot.amazonaws.com', + }, + }, + ], + Version: '2012-10-17', + }, + }); + + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: ['firehose:PutRecord', 'firehose:PutRecordBatch'], + Effect: 'Allow', + Resource: 'arn:aws:firehose:xx-west-1:111122223333:deliverystream/my-stream', + }, + ], + Version: '2012-10-17', + }, + PolicyName: 'MyTopicRuleTopicRuleActionRoleDefaultPolicy54A701F7', + Roles: [ + { Ref: 'MyTopicRuleTopicRuleActionRoleCE2D05DA' }, + ], + }); +}); + +test('can set batchMode', () => { + // GIVEN + const stack = new cdk.Stack(); + const topicRule = new iot.TopicRule(stack, 'MyTopicRule', { + sql: iot.IotSql.fromStringAsVer20160323("SELECT topic(2) as device_id FROM 'device/+/data'"), + }); + const stream = firehose.DeliveryStream.fromDeliveryStreamArn(stack, 'MyStream', 'arn:aws:firehose:xx-west-1:111122223333:deliverystream/my-stream'); + + // WHEN + topicRule.addAction( + new actions.FirehoseStreamAction(stream, { batchMode: true }), + ); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::IoT::TopicRule', { + TopicRulePayload: { + Actions: [ + Match.objectLike({ Firehose: { BatchMode: true } }), + ], + }, + }); +}); + +test('can set separotor', () => { + // GIVEN + const stack = new cdk.Stack(); + const topicRule = new iot.TopicRule(stack, 'MyTopicRule', { + sql: iot.IotSql.fromStringAsVer20160323("SELECT topic(2) as device_id FROM 'device/+/data'"), + }); + const stream = firehose.DeliveryStream.fromDeliveryStreamArn(stack, 'MyStream', 'arn:aws:firehose:xx-west-1:111122223333:deliverystream/my-stream'); + + // WHEN + topicRule.addAction( + new actions.FirehoseStreamAction(stream, { recordSeparator: actions.FirehoseStreamRecordSeparator.NEWLINE }), + ); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::IoT::TopicRule', { + TopicRulePayload: { + Actions: [ + Match.objectLike({ Firehose: { Separator: '\n' } }), + ], + }, + }); +}); + +test('can set role', () => { + // GIVEN + const stack = new cdk.Stack(); + const topicRule = new iot.TopicRule(stack, 'MyTopicRule', { + sql: iot.IotSql.fromStringAsVer20160323("SELECT topic(2) as device_id FROM 'device/+/data'"), + }); + const stream = firehose.DeliveryStream.fromDeliveryStreamArn(stack, 'MyStream', 'arn:aws:firehose:xx-west-1:111122223333:deliverystream/my-stream'); + const role = iam.Role.fromRoleArn(stack, 'MyRole', 'arn:aws:iam::123456789012:role/ForTest'); + + // WHEN + topicRule.addAction( + new actions.FirehoseStreamAction(stream, { role }), + ); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::IoT::TopicRule', { + TopicRulePayload: { + Actions: [ + Match.objectLike({ Firehose: { RoleArn: 'arn:aws:iam::123456789012:role/ForTest' } }), + ], + }, + }); + + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { + PolicyName: 'MyRolePolicy64AB00A5', + Roles: ['ForTest'], + }); +}); diff --git a/packages/@aws-cdk/aws-iot-actions/test/kinesis-firehose/integ.firehose-stream-action.expected.json b/packages/@aws-cdk/aws-iot-actions/test/kinesis-firehose/integ.firehose-stream-action.expected.json new file mode 100644 index 0000000000000..d1565669b5c04 --- /dev/null +++ b/packages/@aws-cdk/aws-iot-actions/test/kinesis-firehose/integ.firehose-stream-action.expected.json @@ -0,0 +1,306 @@ +{ + "Resources": { + "TopicRule40A4EA44": { + "Type": "AWS::IoT::TopicRule", + "Properties": { + "TopicRulePayload": { + "Actions": [ + { + "Firehose": { + "BatchMode": true, + "DeliveryStreamName": { + "Ref": "MyStream5C050E93" + }, + "RoleArn": { + "Fn::GetAtt": [ + "TopicRuleTopicRuleActionRole246C4F77", + "Arn" + ] + }, + "Separator": "\n" + } + } + ], + "AwsIotSqlVersion": "2016-03-23", + "Sql": "SELECT * FROM 'device/+/data'" + } + } + }, + "TopicRuleTopicRuleActionRole246C4F77": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "iot.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "TopicRuleTopicRuleActionRoleDefaultPolicy99ADD687": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "firehose:PutRecord", + "firehose:PutRecordBatch" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "MyStream5C050E93", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "TopicRuleTopicRuleActionRoleDefaultPolicy99ADD687", + "Roles": [ + { + "Ref": "TopicRuleTopicRuleActionRole246C4F77" + } + ] + } + }, + "MyBucketF68F3FF0": { + "Type": "AWS::S3::Bucket", + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "MyStreamServiceRole8C50608A": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "firehose.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "MyStreamS3DestinationRole5E0BA960": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "firehose.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "MyStreamS3DestinationRoleDefaultPolicy401EF6F2": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*", + "s3:DeleteObject*", + "s3:PutObject", + "s3:Abort*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "MyBucketF68F3FF0", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "MyBucketF68F3FF0", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + }, + { + "Action": [ + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "MyStreamLogGroupAB67AB09", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "MyStreamS3DestinationRoleDefaultPolicy401EF6F2", + "Roles": [ + { + "Ref": "MyStreamS3DestinationRole5E0BA960" + } + ] + } + }, + "MyStreamLogGroupAB67AB09": { + "Type": "AWS::Logs::LogGroup", + "Properties": { + "RetentionInDays": 731 + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "MyStreamLogGroupS3Destination423E82A8": { + "Type": "AWS::Logs::LogStream", + "Properties": { + "LogGroupName": { + "Ref": "MyStreamLogGroupAB67AB09" + } + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "MyStream5C050E93": { + "Type": "AWS::KinesisFirehose::DeliveryStream", + "Properties": { + "DeliveryStreamType": "DirectPut", + "ExtendedS3DestinationConfiguration": { + "BucketARN": { + "Fn::GetAtt": [ + "MyBucketF68F3FF0", + "Arn" + ] + }, + "CloudWatchLoggingOptions": { + "Enabled": true, + "LogGroupName": { + "Ref": "MyStreamLogGroupAB67AB09" + }, + "LogStreamName": { + "Ref": "MyStreamLogGroupS3Destination423E82A8" + } + }, + "RoleARN": { + "Fn::GetAtt": [ + "MyStreamS3DestinationRole5E0BA960", + "Arn" + ] + } + } + }, + "DependsOn": [ + "MyStreamS3DestinationRoleDefaultPolicy401EF6F2" + ] + } + }, + "Mappings": { + "awscdkawskinesisfirehoseCidrBlocks": { + "af-south-1": { + "FirehoseCidrBlock": "13.244.121.224/27" + }, + "ap-east-1": { + "FirehoseCidrBlock": "18.162.221.32/27" + }, + "ap-northeast-1": { + "FirehoseCidrBlock": "13.113.196.224/27" + }, + "ap-northeast-2": { + "FirehoseCidrBlock": "13.209.1.64/27" + }, + "ap-northeast-3": { + "FirehoseCidrBlock": "13.208.177.192/27" + }, + "ap-south-1": { + "FirehoseCidrBlock": "13.232.67.32/27" + }, + "ap-southeast-1": { + "FirehoseCidrBlock": "13.228.64.192/27" + }, + "ap-southeast-2": { + "FirehoseCidrBlock": "13.210.67.224/27" + }, + "ca-central-1": { + "FirehoseCidrBlock": "35.183.92.128/27" + }, + "cn-north-1": { + "FirehoseCidrBlock": "52.81.151.32/27" + }, + "cn-northwest-1": { + "FirehoseCidrBlock": "161.189.23.64/27" + }, + "eu-central-1": { + "FirehoseCidrBlock": "35.158.127.160/27" + }, + "eu-north-1": { + "FirehoseCidrBlock": "13.53.63.224/27" + }, + "eu-south-1": { + "FirehoseCidrBlock": "15.161.135.128/27" + }, + "eu-west-1": { + "FirehoseCidrBlock": "52.19.239.192/27" + }, + "eu-west-2": { + "FirehoseCidrBlock": "18.130.1.96/27" + }, + "eu-west-3": { + "FirehoseCidrBlock": "35.180.1.96/27" + }, + "me-south-1": { + "FirehoseCidrBlock": "15.185.91.0/27" + }, + "sa-east-1": { + "FirehoseCidrBlock": "18.228.1.128/27" + }, + "us-east-1": { + "FirehoseCidrBlock": "52.70.63.192/27" + }, + "us-east-2": { + "FirehoseCidrBlock": "13.58.135.96/27" + }, + "us-gov-east-1": { + "FirehoseCidrBlock": "18.253.138.96/27" + }, + "us-gov-west-1": { + "FirehoseCidrBlock": "52.61.204.160/27" + }, + "us-west-1": { + "FirehoseCidrBlock": "13.57.135.192/27" + }, + "us-west-2": { + "FirehoseCidrBlock": "52.89.255.224/27" + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-iot-actions/test/kinesis-firehose/integ.firehose-stream-action.ts b/packages/@aws-cdk/aws-iot-actions/test/kinesis-firehose/integ.firehose-stream-action.ts new file mode 100644 index 0000000000000..9287f1294b4dd --- /dev/null +++ b/packages/@aws-cdk/aws-iot-actions/test/kinesis-firehose/integ.firehose-stream-action.ts @@ -0,0 +1,38 @@ +/// !cdk-integ pragma:ignore-assets +import * as iot from '@aws-cdk/aws-iot'; +import * as firehose from '@aws-cdk/aws-kinesisfirehose'; +import * as destinations from '@aws-cdk/aws-kinesisfirehose-destinations'; +import * as s3 from '@aws-cdk/aws-s3'; +import * as cdk from '@aws-cdk/core'; +import * as actions from '../../lib'; + + +const app = new cdk.App(); + +class TestStack extends cdk.Stack { + constructor(scope: cdk.App, id: string, props?: cdk.StackProps) { + super(scope, id, props); + + const topicRule = new iot.TopicRule(this, 'TopicRule', { + sql: iot.IotSql.fromStringAsVer20160323( + "SELECT * FROM 'device/+/data'", + ), + }); + + const bucket = new s3.Bucket(this, 'MyBucket', { + removalPolicy: cdk.RemovalPolicy.DESTROY, + }); + const stream = new firehose.DeliveryStream(this, 'MyStream', { + destinations: [new destinations.S3Bucket(bucket)], + }); + topicRule.addAction( + new actions.FirehoseStreamAction(stream, { + batchMode: true, + recordSeparator: actions.FirehoseStreamRecordSeparator.NEWLINE, + }), + ); + } +} + +new TestStack(app, 'test-stack'); +app.synth(); diff --git a/packages/@aws-cdk/aws-iot-actions/test/s3/integ.s3-put-object-action.expected.json b/packages/@aws-cdk/aws-iot-actions/test/s3/integ.s3-put-object-action.expected.json new file mode 100644 index 0000000000000..4e530f04da2c1 --- /dev/null +++ b/packages/@aws-cdk/aws-iot-actions/test/s3/integ.s3-put-object-action.expected.json @@ -0,0 +1,86 @@ +{ + "Resources": { + "TopicRule40A4EA44": { + "Type": "AWS::IoT::TopicRule", + "Properties": { + "TopicRulePayload": { + "Actions": [ + { + "S3": { + "BucketName": { + "Ref": "MyBucketF68F3FF0" + }, + "CannedAcl": "bucket-owner-full-control", + "Key": "${year}/${month}/${day}/${topic(2)}", + "RoleArn": { + "Fn::GetAtt": [ + "TopicRuleTopicRuleActionRole246C4F77", + "Arn" + ] + } + } + } + ], + "AwsIotSqlVersion": "2016-03-23", + "Sql": "SELECT topic(2) as device_id, year, month, day FROM 'device/+/data'" + } + } + }, + "TopicRuleTopicRuleActionRole246C4F77": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "iot.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "TopicRuleTopicRuleActionRoleDefaultPolicy99ADD687": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "s3:PutObject", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "MyBucketF68F3FF0", + "Arn" + ] + }, + "/*" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "TopicRuleTopicRuleActionRoleDefaultPolicy99ADD687", + "Roles": [ + { + "Ref": "TopicRuleTopicRuleActionRole246C4F77" + } + ] + } + }, + "MyBucketF68F3FF0": { + "Type": "AWS::S3::Bucket", + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-iot-actions/test/s3/integ.s3-put-object-action.ts b/packages/@aws-cdk/aws-iot-actions/test/s3/integ.s3-put-object-action.ts new file mode 100644 index 0000000000000..9e100e0254eaf --- /dev/null +++ b/packages/@aws-cdk/aws-iot-actions/test/s3/integ.s3-put-object-action.ts @@ -0,0 +1,32 @@ +/// !cdk-integ pragma:ignore-assets +import * as iot from '@aws-cdk/aws-iot'; +import * as s3 from '@aws-cdk/aws-s3'; +import * as cdk from '@aws-cdk/core'; +import * as actions from '../../lib'; + +const app = new cdk.App(); + +class TestStack extends cdk.Stack { + constructor(scope: cdk.App, id: string, props?: cdk.StackProps) { + super(scope, id, props); + + const topicRule = new iot.TopicRule(this, 'TopicRule', { + sql: iot.IotSql.fromStringAsVer20160323( + "SELECT topic(2) as device_id, year, month, day FROM 'device/+/data'", + ), + }); + + const bucket = new s3.Bucket(this, 'MyBucket', { + removalPolicy: cdk.RemovalPolicy.DESTROY, + }); + topicRule.addAction( + new actions.S3PutObjectAction(bucket, { + key: '${year}/${month}/${day}/${topic(2)}', + accessControl: s3.BucketAccessControl.BUCKET_OWNER_FULL_CONTROL, + }), + ); + } +} + +new TestStack(app, 'test-stack'); +app.synth(); diff --git a/packages/@aws-cdk/aws-iot-actions/test/s3/s3-put-object-action.test.ts b/packages/@aws-cdk/aws-iot-actions/test/s3/s3-put-object-action.test.ts new file mode 100644 index 0000000000000..567bd59d05083 --- /dev/null +++ b/packages/@aws-cdk/aws-iot-actions/test/s3/s3-put-object-action.test.ts @@ -0,0 +1,148 @@ +import { Template, Match } from '@aws-cdk/assertions'; +import * as iam from '@aws-cdk/aws-iam'; +import * as iot from '@aws-cdk/aws-iot'; +import * as s3 from '@aws-cdk/aws-s3'; +import * as cdk from '@aws-cdk/core'; +import * as actions from '../../lib'; + +test('Default s3 action', () => { + // GIVEN + const stack = new cdk.Stack(); + const topicRule = new iot.TopicRule(stack, 'MyTopicRule', { + sql: iot.IotSql.fromStringAsVer20160323("SELECT topic(2) as device_id FROM 'device/+/data'"), + }); + const bucket = s3.Bucket.fromBucketArn(stack, 'MyBucket', 'arn:aws:s3::123456789012:test-bucket'); + + // WHEN + topicRule.addAction( + new actions.S3PutObjectAction(bucket), + ); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::IoT::TopicRule', { + TopicRulePayload: { + Actions: [ + { + S3: { + BucketName: 'test-bucket', + Key: '${topic()}/${timestamp()}', + RoleArn: { + 'Fn::GetAtt': ['MyTopicRuleTopicRuleActionRoleCE2D05DA', 'Arn'], + }, + }, + }, + ], + }, + }); + + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: 'iot.amazonaws.com', + }, + }, + ], + Version: '2012-10-17', + }, + }); + + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: 's3:PutObject', + Effect: 'Allow', + Resource: 'arn:aws:s3::123456789012:test-bucket/*', + }, + ], + Version: '2012-10-17', + }, + PolicyName: 'MyTopicRuleTopicRuleActionRoleDefaultPolicy54A701F7', + Roles: [ + { Ref: 'MyTopicRuleTopicRuleActionRoleCE2D05DA' }, + ], + }); +}); + +test('can set key of bucket', () => { + // GIVEN + const stack = new cdk.Stack(); + const topicRule = new iot.TopicRule(stack, 'MyTopicRule', { + sql: iot.IotSql.fromStringAsVer20160323("SELECT topic(2) as device_id FROM 'device/+/data'"), + }); + const bucket = s3.Bucket.fromBucketArn(stack, 'MyBucket', 'arn:aws:s3::123456789012:test-bucket'); + + // WHEN + topicRule.addAction( + new actions.S3PutObjectAction(bucket, { + key: 'test-key', + }), + ); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::IoT::TopicRule', { + TopicRulePayload: { + Actions: [ + Match.objectLike({ S3: { Key: 'test-key' } }), + ], + }, + }); +}); + +test('can set canned ACL and it convert to kebab case', () => { + // GIVEN + const stack = new cdk.Stack(); + const topicRule = new iot.TopicRule(stack, 'MyTopicRule', { + sql: iot.IotSql.fromStringAsVer20160323("SELECT topic(2) as device_id FROM 'device/+/data'"), + }); + const bucket = s3.Bucket.fromBucketArn(stack, 'MyBucket', 'arn:aws:s3::123456789012:test-bucket'); + + // WHEN + topicRule.addAction( + new actions.S3PutObjectAction(bucket, { + accessControl: s3.BucketAccessControl.BUCKET_OWNER_FULL_CONTROL, + }), + ); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::IoT::TopicRule', { + TopicRulePayload: { + Actions: [ + Match.objectLike({ S3: { CannedAcl: 'bucket-owner-full-control' } }), + ], + }, + }); +}); + +test('can set role', () => { + // GIVEN + const stack = new cdk.Stack(); + const topicRule = new iot.TopicRule(stack, 'MyTopicRule', { + sql: iot.IotSql.fromStringAsVer20160323("SELECT topic(2) as device_id FROM 'device/+/data'"), + }); + const bucket = s3.Bucket.fromBucketArn(stack, 'MyBucket', 'arn:aws:s3::123456789012:test-bucket'); + const role = iam.Role.fromRoleArn(stack, 'MyRole', 'arn:aws:iam::123456789012:role/ForTest'); + + // WHEN + topicRule.addAction( + new actions.S3PutObjectAction(bucket, { role }), + ); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::IoT::TopicRule', { + TopicRulePayload: { + Actions: [ + Match.objectLike({ S3: { RoleArn: 'arn:aws:iam::123456789012:role/ForTest' } }), + ], + }, + }); + + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { + PolicyName: 'MyRolePolicy64AB00A5', + Roles: ['ForTest'], + }); +}); diff --git a/packages/@aws-cdk/aws-kinesis/lib/stream.ts b/packages/@aws-cdk/aws-kinesis/lib/stream.ts index b2fed1eb10329..8cc08fa70bfdc 100644 --- a/packages/@aws-cdk/aws-kinesis/lib/stream.ts +++ b/packages/@aws-cdk/aws-kinesis/lib/stream.ts @@ -12,6 +12,8 @@ const READ_OPERATIONS = [ 'kinesis:GetShardIterator', 'kinesis:ListShards', 'kinesis:SubscribeToShard', + 'kinesis:DescribeStream', + 'kinesis:ListStreams', ]; const WRITE_OPERATIONS = [ diff --git a/packages/@aws-cdk/aws-kinesis/test/integ.stream.expected.json b/packages/@aws-cdk/aws-kinesis/test/integ.stream.expected.json index 15055271413a2..41230acc599a2 100644 --- a/packages/@aws-cdk/aws-kinesis/test/integ.stream.expected.json +++ b/packages/@aws-cdk/aws-kinesis/test/integ.stream.expected.json @@ -44,6 +44,8 @@ "kinesis:GetShardIterator", "kinesis:ListShards", "kinesis:SubscribeToShard", + "kinesis:DescribeStream", + "kinesis:ListStreams", "kinesis:PutRecord", "kinesis:PutRecords" ], diff --git a/packages/@aws-cdk/aws-kinesis/test/stream.test.ts b/packages/@aws-cdk/aws-kinesis/test/stream.test.ts index 089261c6ebdae..dee29db89d384 100644 --- a/packages/@aws-cdk/aws-kinesis/test/stream.test.ts +++ b/packages/@aws-cdk/aws-kinesis/test/stream.test.ts @@ -503,6 +503,8 @@ describe('Kinesis data streams', () => { 'kinesis:GetShardIterator', 'kinesis:ListShards', 'kinesis:SubscribeToShard', + 'kinesis:DescribeStream', + 'kinesis:ListStreams', ], Effect: 'Allow', Resource: { @@ -811,6 +813,8 @@ describe('Kinesis data streams', () => { 'kinesis:GetShardIterator', 'kinesis:ListShards', 'kinesis:SubscribeToShard', + 'kinesis:DescribeStream', + 'kinesis:ListStreams', 'kinesis:PutRecord', 'kinesis:PutRecords', ], @@ -884,6 +888,8 @@ describe('Kinesis data streams', () => { 'kinesis:GetShardIterator', 'kinesis:ListShards', 'kinesis:SubscribeToShard', + 'kinesis:DescribeStream', + 'kinesis:ListStreams', ], Effect: 'Allow', Resource: { @@ -1050,6 +1056,8 @@ describe('Kinesis data streams', () => { 'kinesis:GetShardIterator', 'kinesis:ListShards', 'kinesis:SubscribeToShard', + 'kinesis:DescribeStream', + 'kinesis:ListStreams', 'kinesis:PutRecord', 'kinesis:PutRecords', ], diff --git a/packages/@aws-cdk/aws-kinesisfirehose/lib/delivery-stream.ts b/packages/@aws-cdk/aws-kinesisfirehose/lib/delivery-stream.ts index 7dfaed8eb384b..35230fc284bd9 100644 --- a/packages/@aws-cdk/aws-kinesisfirehose/lib/delivery-stream.ts +++ b/packages/@aws-cdk/aws-kinesisfirehose/lib/delivery-stream.ts @@ -358,13 +358,6 @@ export class DeliveryStream extends DeliveryStreamBase { roleArn: role.roleArn, } : undefined; const readStreamGrant = props.sourceStream?.grantRead(role); - /* - * Firehose still uses the deprecated DescribeStream API instead of the modern DescribeStreamSummary API. - * kinesis.IStream.grantRead does not provide DescribeStream permissions so we add it manually here. - */ - if (readStreamGrant && readStreamGrant.principalStatement) { - readStreamGrant.principalStatement.addActions('kinesis:DescribeStream'); - } const destinationConfig = props.destinations[0].bind(this, {}); diff --git a/packages/@aws-cdk/aws-kinesisfirehose/test/integ.delivery-stream.source-stream.expected.json b/packages/@aws-cdk/aws-kinesisfirehose/test/integ.delivery-stream.source-stream.expected.json index eb46541a1cdf2..896d0487a091c 100644 --- a/packages/@aws-cdk/aws-kinesisfirehose/test/integ.delivery-stream.source-stream.expected.json +++ b/packages/@aws-cdk/aws-kinesisfirehose/test/integ.delivery-stream.source-stream.expected.json @@ -119,7 +119,8 @@ "kinesis:GetShardIterator", "kinesis:ListShards", "kinesis:SubscribeToShard", - "kinesis:DescribeStream" + "kinesis:DescribeStream", + "kinesis:ListStreams" ], "Effect": "Allow", "Resource": { diff --git a/packages/@aws-cdk/aws-lambda-destinations/README.md b/packages/@aws-cdk/aws-lambda-destinations/README.md index 404b0b3157adb..675ca0b175757 100644 --- a/packages/@aws-cdk/aws-lambda-destinations/README.md +++ b/packages/@aws-cdk/aws-lambda-destinations/README.md @@ -24,15 +24,17 @@ The following destinations are supported Example with a SNS topic for successful invocations: ```ts -import * as lambda from '@aws-cdk/aws-lambda'; -import * as destinations from '@aws-cdk/aws-lambda-destinations'; +// An sns topic for successful invocations of a lambda function import * as sns from '@aws-cdk/aws-sns'; const myTopic = new sns.Topic(this, 'Topic'); const myFn = new lambda.Function(this, 'Fn', { - // other props - onSuccess: new destinations.SnsDestination(myTopic) + runtime: lambda.Runtime.NODEJS_12_X, + handler: 'index.handler', + code: lambda.Code.fromAsset(path.join(__dirname, 'lambda-handler')), + // sns topic for successful invocations + onSuccess: new destinations.SnsDestination(myTopic), }) ``` @@ -71,27 +73,27 @@ In case of failure, the record contains the reason and error object: ```json { - "version": "1.0", - "timestamp": "2019-11-24T21:52:47.333Z", - "requestContext": { - "requestId": "8ea123e4-1db7-4aca-ad10-d9ca1234c1fd", - "functionArn": "arn:aws:lambda:sa-east-1:123456678912:function:event-destinations:$LATEST", - "condition": "RetriesExhausted", - "approximateInvokeCount": 3 - }, - "requestPayload": { - "Success": false - }, - "responseContext": { - "statusCode": 200, - "executedVersion": "$LATEST", - "functionError": "Handled" - }, - "responsePayload": { - "errorMessage": "Failure from event, Success = false, I am failing!", - "errorType": "Error", - "stackTrace": [ "exports.handler (/var/task/index.js:18:18)" ] - } + "version": "1.0", + "timestamp": "2019-11-24T21:52:47.333Z", + "requestContext": { + "requestId": "8ea123e4-1db7-4aca-ad10-d9ca1234c1fd", + "functionArn": "arn:aws:lambda:sa-east-1:123456678912:function:event-destinations:$LATEST", + "condition": "RetriesExhausted", + "approximateInvokeCount": 3 + }, + "requestPayload": { + "Success": false + }, + "responseContext": { + "statusCode": 200, + "executedVersion": "$LATEST", + "functionError": "Handled" + }, + "responsePayload": { + "errorMessage": "Failure from event, Success = false, I am failing!", + "errorType": "Error", + "stackTrace": [ "exports.handler (/var/task/index.js:18:18)" ] + } } ``` @@ -112,18 +114,17 @@ The `responseOnly` option of `LambdaDestination` allows to auto-extract the resp invocation record: ```ts -import * as lambda from '@aws-cdk/aws-lambda'; -import * as destinations from '@aws-cdk/aws-lambda-destinations'; - -const destinationFn = new lambda.Function(this, 'Destination', { - // props -}); +// Auto-extract response payload with a lambda destination +declare const destinationFn: lambda.Function; const sourceFn = new lambda.Function(this, 'Source', { - // other props + runtime: lambda.Runtime.NODEJS_12_X, + handler: 'index.handler', + code: lambda.Code.fromAsset(path.join(__dirname, 'lambda-handler')), + // auto-extract on success onSuccess: new destinations.LambdaDestination(destinationFn, { - responseOnly: true // auto-extract - }); + responseOnly: true, + }), }) ``` diff --git a/packages/@aws-cdk/aws-lambda-destinations/package.json b/packages/@aws-cdk/aws-lambda-destinations/package.json index 17ce33782126c..12108477a91de 100644 --- a/packages/@aws-cdk/aws-lambda-destinations/package.json +++ b/packages/@aws-cdk/aws-lambda-destinations/package.json @@ -28,7 +28,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", diff --git a/packages/@aws-cdk/aws-lambda-destinations/rosetta/default.ts-fixture b/packages/@aws-cdk/aws-lambda-destinations/rosetta/default.ts-fixture new file mode 100644 index 0000000000000..fb7d525a027aa --- /dev/null +++ b/packages/@aws-cdk/aws-lambda-destinations/rosetta/default.ts-fixture @@ -0,0 +1,14 @@ +// Fixture with packages imported, but nothing else +import { Construct } from 'constructs'; +import { Stack } from '@aws-cdk/core'; +import * as lambda from '@aws-cdk/aws-lambda'; +import * as destinations from '@aws-cdk/aws-lambda-destinations'; +import * as path from 'path'; + +class Fixture extends Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + + /// here + } +} diff --git a/packages/@aws-cdk/aws-lambda-event-sources/README.md b/packages/@aws-cdk/aws-lambda-event-sources/README.md index e1ba637699efd..9abd6e59d9669 100644 --- a/packages/@aws-cdk/aws-lambda-event-sources/README.md +++ b/packages/@aws-cdk/aws-lambda-event-sources/README.md @@ -24,12 +24,14 @@ sources regardless of the underlying mechanism they use. The following code sets up a lambda function with an SQS queue event source - ```ts -const fn = new lambda.Function(this, 'MyFunction', { /* ... */ }); +import { SqsEventSource } from '@aws-cdk/aws-lambda-event-sources'; +declare const fn: lambda.Function; const queue = new sqs.Queue(this, 'MyQueue'); -const eventSource = fn.addEventSource(new SqsEventSource(queue)); +const eventSource = new SqsEventSource(queue); +fn.addEventSource(eventSource); -const eventSourceId = eventSource.eventSourceId; +const eventSourceId = eventSource.eventSourceMappingId; ``` The `eventSourceId` property contains the event source id. This will be a @@ -58,16 +60,15 @@ behavior: * __enabled__: If the SQS event source mapping should be enabled. The default is true. ```ts -import * as sqs from '@aws-cdk/aws-sqs'; import { SqsEventSource } from '@aws-cdk/aws-lambda-event-sources'; -import { Duration } from '@aws-cdk/core'; const queue = new sqs.Queue(this, 'MyQueue', { - visibilityTimeout: Duration.seconds(30) // default, - receiveMessageWaitTime: Duration.seconds(20) // default + visibilityTimeout: Duration.seconds(30), // default, + receiveMessageWaitTime: Duration.seconds(20), // default }); +declare const fn: lambda.Function; -lambda.addEventSource(new SqsEventSource(queue, { +fn.addEventSource(new SqsEventSource(queue, { batchSize: 10, // default maxBatchingWindow: Duration.minutes(5), })); @@ -88,11 +89,12 @@ Amazon S3 to publish and which Lambda function to invoke. import * as s3 from '@aws-cdk/aws-s3'; import { S3EventSource } from '@aws-cdk/aws-lambda-event-sources'; -const bucket = new s3.Bucket(...); +const bucket = new s3.Bucket(this, 'mybucket'); +declare const fn: lambda.Function; -lambda.addEventSource(new S3EventSource(bucket, { +fn.addEventSource(new S3EventSource(bucket, { events: [ s3.EventType.OBJECT_CREATED, s3.EventType.OBJECT_REMOVED ], - filters: [ { prefix: 'subdir/' } ] // optional + filters: [ { prefix: 'subdir/' } ], // optional })); ``` @@ -118,12 +120,13 @@ Accounts](https://docs.aws.amazon.com/lambda/latest/dg/with-sns.html). import * as sns from '@aws-cdk/aws-sns'; import { SnsEventSource } from '@aws-cdk/aws-lambda-event-sources'; -const topic = new sns.Topic(...); +declare const topic: sns.Topic; const deadLetterQueue = new sqs.Queue(this, 'deadLetterQueue'); -lambda.addEventSource(new SnsEventSource(topic, { - filterPolicy: { ... }, - deadLetterQueue: deadLetterQueue +declare const fn: lambda.Function; +fn.addEventSource(new SnsEventSource(topic, { + filterPolicy: { }, + deadLetterQueue: deadLetterQueue, })); ``` @@ -157,24 +160,19 @@ and add it to your Lambda function. The following parameters will impact Amazon ```ts import * as dynamodb from '@aws-cdk/aws-dynamodb'; -import * as lambda from '@aws-cdk/aws-lambda'; -import * as sqs from '@aws-cdk/aws-sqs'; import { DynamoEventSource, SqsDlq } from '@aws-cdk/aws-lambda-event-sources'; -const table = new dynamodb.Table(..., { - partitionKey: ..., - stream: dynamodb.StreamViewType.NEW_IMAGE // make sure stream is configured -}); +declare const table: dynamodb.Table; const deadLetterQueue = new sqs.Queue(this, 'deadLetterQueue'); -const function = new lambda.Function(...); -function.addEventSource(new DynamoEventSource(table, { +declare const fn: lambda.Function; +fn.addEventSource(new DynamoEventSource(table, { startingPosition: lambda.StartingPosition.TRIM_HORIZON, batchSize: 5, bisectBatchOnError: true, onFailure: new SqsDlq(deadLetterQueue), - retryAttempts: 10 + retryAttempts: 10, })); ``` @@ -202,15 +200,15 @@ behavior: * __enabled__: If the DynamoDB Streams event source mapping should be enabled. The default is true. ```ts -import * as lambda from '@aws-cdk/aws-lambda'; import * as kinesis from '@aws-cdk/aws-kinesis'; import { KinesisEventSource } from '@aws-cdk/aws-lambda-event-sources'; const stream = new kinesis.Stream(this, 'MyStream'); +declare const myFunction: lambda.Function; myFunction.addEventSource(new KinesisEventSource(stream, { batchSize: 100, // default - startingPosition: lambda.StartingPosition.TRIM_HORIZON + startingPosition: lambda.StartingPosition.TRIM_HORIZON, })); ``` @@ -222,27 +220,26 @@ The following code sets up Amazon MSK as an event source for a lambda function. MSK cluster, as described in [Username/Password authentication](https://docs.aws.amazon.com/msk/latest/developerguide/msk-password.html). ```ts -import * as lambda from '@aws-cdk/aws-lambda'; -import * as msk from '@aws-cdk/aws-lambda'; -import { Secret } from '@aws-cdk/aws-secretmanager'; +import { Secret } from '@aws-cdk/aws-secretsmanager'; import { ManagedKafkaEventSource } from '@aws-cdk/aws-lambda-event-sources'; // Your MSK cluster arn -const cluster = 'arn:aws:kafka:us-east-1:0123456789019:cluster/SalesCluster/abcd1234-abcd-cafe-abab-9876543210ab-4'; +const clusterArn = 'arn:aws:kafka:us-east-1:0123456789019:cluster/SalesCluster/abcd1234-abcd-cafe-abab-9876543210ab-4'; // The Kafka topic you want to subscribe to -const topic = 'some-cool-topic' +const topic = 'some-cool-topic'; // The secret that allows access to your MSK cluster // You still have to make sure that it is associated with your cluster as described in the documentation const secret = new Secret(this, 'Secret', { secretName: 'AmazonMSK_KafkaSecret' }); +declare const myFunction: lambda.Function; myFunction.addEventSource(new ManagedKafkaEventSource({ clusterArn, topic: topic, secret: secret, batchSize: 100, // default - startingPosition: lambda.StartingPosition.TRIM_HORIZON + startingPosition: lambda.StartingPosition.TRIM_HORIZON, })); ``` @@ -250,25 +247,25 @@ The following code sets up a self managed Kafka cluster as an event source. User will need to be set up as described in [Managing access and permissions](https://docs.aws.amazon.com/lambda/latest/dg/smaa-permissions.html#smaa-permissions-add-secret). ```ts -import * as lambda from '@aws-cdk/aws-lambda'; -import { Secret } from '@aws-cdk/aws-secretmanager'; +import { Secret } from '@aws-cdk/aws-secretsmanager'; import { SelfManagedKafkaEventSource } from '@aws-cdk/aws-lambda-event-sources'; // The list of Kafka brokers -const bootstrapServers = ['kafka-broker:9092'] +const bootstrapServers = ['kafka-broker:9092']; // The Kafka topic you want to subscribe to -const topic = 'some-cool-topic' +const topic = 'some-cool-topic'; // The secret that allows access to your self hosted Kafka cluster -const secret = new Secret(this, 'Secret', { ... }); +declare const secret: Secret; +declare const myFunction: lambda.Function; myFunction.addEventSource(new SelfManagedKafkaEventSource({ bootstrapServers: bootstrapServers, topic: topic, secret: secret, batchSize: 100, // default - startingPosition: lambda.StartingPosition.TRIM_HORIZON + startingPosition: lambda.StartingPosition.TRIM_HORIZON, })); ``` diff --git a/packages/@aws-cdk/aws-lambda-event-sources/lib/kafka.ts b/packages/@aws-cdk/aws-lambda-event-sources/lib/kafka.ts index 54e98a47bb55d..e31fac89100cb 100644 --- a/packages/@aws-cdk/aws-lambda-event-sources/lib/kafka.ts +++ b/packages/@aws-cdk/aws-lambda-event-sources/lib/kafka.ts @@ -3,7 +3,7 @@ import { ISecurityGroup, IVpc, SubnetSelection } from '@aws-cdk/aws-ec2'; import * as iam from '@aws-cdk/aws-iam'; import * as lambda from '@aws-cdk/aws-lambda'; import * as secretsmanager from '@aws-cdk/aws-secretsmanager'; -import { Stack } from '@aws-cdk/core'; +import { Stack, Names } from '@aws-cdk/core'; import { StreamEventSource, StreamEventSourceProps } from './stream'; // keep this import separate from other imports to reduce chance for merge conflicts with v2-main @@ -101,6 +101,7 @@ export interface SelfManagedKafkaEventSourceProps extends KafkaEventSourceProps export class ManagedKafkaEventSource extends StreamEventSource { // This is to work around JSII inheritance problems private innerProps: ManagedKafkaEventSourceProps; + private _eventSourceMappingId?: string = undefined; constructor(props: ManagedKafkaEventSourceProps) { super(props); @@ -108,8 +109,8 @@ export class ManagedKafkaEventSource extends StreamEventSource { } public bind(target: lambda.IFunction) { - target.addEventSourceMapping( - `KafkaEventSource:${this.innerProps.clusterArn}${this.innerProps.topic}`, + const eventSourceMapping = target.addEventSourceMapping( + `KafkaEventSource:${Names.nodeUniqueId(target.node)}${this.innerProps.topic}`, this.enrichMappingOptions({ eventSourceArn: this.innerProps.clusterArn, startingPosition: this.innerProps.startingPosition, @@ -118,6 +119,8 @@ export class ManagedKafkaEventSource extends StreamEventSource { }), ); + this._eventSourceMappingId = eventSourceMapping.eventSourceMappingId; + if (this.innerProps.secret !== undefined) { this.innerProps.secret.grantRead(target); } @@ -146,6 +149,16 @@ export class ManagedKafkaEventSource extends StreamEventSource { ? undefined : sourceAccessConfigurations; } + + /** + * The identifier for this EventSourceMapping + */ + public get eventSourceMappingId(): string { + if (!this._eventSourceMappingId) { + throw new Error('KafkaEventSource is not yet bound to an event source mapping'); + } + return this._eventSourceMappingId; + } } /** diff --git a/packages/@aws-cdk/aws-lambda-event-sources/package.json b/packages/@aws-cdk/aws-lambda-event-sources/package.json index 6f0295a38c625..3bd4375cc9d24 100644 --- a/packages/@aws-cdk/aws-lambda-event-sources/package.json +++ b/packages/@aws-cdk/aws-lambda-event-sources/package.json @@ -28,7 +28,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", diff --git a/packages/@aws-cdk/aws-lambda-event-sources/rosetta/default.ts-fixture b/packages/@aws-cdk/aws-lambda-event-sources/rosetta/default.ts-fixture new file mode 100644 index 0000000000000..d2cdb69c90519 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda-event-sources/rosetta/default.ts-fixture @@ -0,0 +1,13 @@ +// Fixture with packages imported, but nothing else +import { Construct } from 'constructs'; +import { Duration, Stack } from '@aws-cdk/core'; +import * as lambda from '@aws-cdk/aws-lambda'; +import * as sqs from '@aws-cdk/aws-sqs'; + +class Fixture extends Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + + /// here + } +} diff --git a/packages/@aws-cdk/aws-lambda-event-sources/test/integ.kinesis.expected.json b/packages/@aws-cdk/aws-lambda-event-sources/test/integ.kinesis.expected.json index aafb84ca19c72..c1690f2f03aac 100644 --- a/packages/@aws-cdk/aws-lambda-event-sources/test/integ.kinesis.expected.json +++ b/packages/@aws-cdk/aws-lambda-event-sources/test/integ.kinesis.expected.json @@ -42,7 +42,9 @@ "kinesis:GetRecords", "kinesis:GetShardIterator", "kinesis:ListShards", - "kinesis:SubscribeToShard" + "kinesis:SubscribeToShard", + "kinesis:DescribeStream", + "kinesis:ListStreams" ], "Effect": "Allow", "Resource": { diff --git a/packages/@aws-cdk/aws-lambda-event-sources/test/integ.kinesiswithdlq.expected.json b/packages/@aws-cdk/aws-lambda-event-sources/test/integ.kinesiswithdlq.expected.json index 4d0a6c1a54707..616adaef6a86a 100644 --- a/packages/@aws-cdk/aws-lambda-event-sources/test/integ.kinesiswithdlq.expected.json +++ b/packages/@aws-cdk/aws-lambda-event-sources/test/integ.kinesiswithdlq.expected.json @@ -56,7 +56,9 @@ "kinesis:GetRecords", "kinesis:GetShardIterator", "kinesis:ListShards", - "kinesis:SubscribeToShard" + "kinesis:SubscribeToShard", + "kinesis:DescribeStream", + "kinesis:ListStreams" ], "Effect": "Allow", "Resource": { diff --git a/packages/@aws-cdk/aws-lambda-event-sources/test/kafka.test.ts b/packages/@aws-cdk/aws-lambda-event-sources/test/kafka.test.ts index 804069373c114..1455931278129 100644 --- a/packages/@aws-cdk/aws-lambda-event-sources/test/kafka.test.ts +++ b/packages/@aws-cdk/aws-lambda-event-sources/test/kafka.test.ts @@ -488,5 +488,24 @@ describe('KafkaEventSource', () => { ]), }); }); + + test('ManagedKafkaEventSource name conforms to construct id rules', () => { + // GIVEN + const stack = new cdk.Stack(); + const fn = new TestFunction(stack, 'Fn'); + const clusterArn = 'some-arn'; + const kafkaTopic = 'some-topic'; + + const mskEventMapping = new sources.ManagedKafkaEventSource( + { + clusterArn, + topic: kafkaTopic, + startingPosition: lambda.StartingPosition.TRIM_HORIZON, + }); + + // WHEN + fn.addEventSource(mskEventMapping); + expect(mskEventMapping.eventSourceMappingId).toBeDefined(); + }); }); }); diff --git a/packages/@aws-cdk/aws-lambda-event-sources/test/kinesis.test.ts b/packages/@aws-cdk/aws-lambda-event-sources/test/kinesis.test.ts index 96701d6c83f7a..e77fec71e5079 100644 --- a/packages/@aws-cdk/aws-lambda-event-sources/test/kinesis.test.ts +++ b/packages/@aws-cdk/aws-lambda-event-sources/test/kinesis.test.ts @@ -30,6 +30,8 @@ describe('KinesisEventSource', () => { 'kinesis:GetShardIterator', 'kinesis:ListShards', 'kinesis:SubscribeToShard', + 'kinesis:DescribeStream', + 'kinesis:ListStreams', ], 'Effect': 'Allow', 'Resource': { diff --git a/packages/@aws-cdk/aws-lambda-go/README.md b/packages/@aws-cdk/aws-lambda-go/README.md index 16fcffee919ea..e4de2d1641113 100644 --- a/packages/@aws-cdk/aws-lambda-go/README.md +++ b/packages/@aws-cdk/aws-lambda-go/README.md @@ -29,7 +29,7 @@ Define a `GoFunction`: ```ts new lambda.GoFunction(this, 'handler', { - entry: 'app/cmd/api' + entry: 'app/cmd/api', }); ``` @@ -154,7 +154,7 @@ Use the `bundling.dockerImage` prop to use a custom bundling image: new lambda.GoFunction(this, 'handler', { entry: 'app/cmd/api', bundling: { - dockerImage: cdk.DockerImage.fromBuild('/path/to/Dockerfile'), + dockerImage: DockerImage.fromBuild('/path/to/Dockerfile'), }, }); ``` @@ -174,7 +174,9 @@ new lambda.GoFunction(this, 'handler', { It is possible to run additional commands by specifying the `commandHooks` prop: -```ts +```text +// This example only available in TypeScript +// Run additional commands on a GoFunction via `commandHooks` property new lambda.GoFunction(this, 'handler', { bundling: { commandHooks: { diff --git a/packages/@aws-cdk/aws-lambda-go/lib/types.ts b/packages/@aws-cdk/aws-lambda-go/lib/types.ts index bcd334809be33..28e058e1a685e 100644 --- a/packages/@aws-cdk/aws-lambda-go/lib/types.ts +++ b/packages/@aws-cdk/aws-lambda-go/lib/types.ts @@ -106,7 +106,7 @@ export interface BundlingOptions { * * Commands are chained with `&&`. * - * @example + * ```text * { * // Run tests prior to bundling * beforeBundling(inputDir: string, outputDir: string): string[] { @@ -114,6 +114,7 @@ export interface BundlingOptions { * } * // ... * } + * ``` */ export interface ICommandHooks { /** diff --git a/packages/@aws-cdk/aws-lambda-go/package.json b/packages/@aws-cdk/aws-lambda-go/package.json index 939988ea2410f..5df9aae607538 100644 --- a/packages/@aws-cdk/aws-lambda-go/package.json +++ b/packages/@aws-cdk/aws-lambda-go/package.json @@ -30,7 +30,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", diff --git a/packages/@aws-cdk/aws-lambda-go/rosetta/default.ts-fixture b/packages/@aws-cdk/aws-lambda-go/rosetta/default.ts-fixture new file mode 100644 index 0000000000000..96f3f6933780c --- /dev/null +++ b/packages/@aws-cdk/aws-lambda-go/rosetta/default.ts-fixture @@ -0,0 +1,12 @@ +// Fixture with packages imported, but nothing else +import { Construct } from 'constructs'; +import { DockerImage, Stack } from '@aws-cdk/core'; +import * as lambda from '@aws-cdk/aws-lambda-go'; + +class Fixture extends Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + + /// here + } +} diff --git a/packages/@aws-cdk/aws-lambda-go/test/integ.function.expected.json b/packages/@aws-cdk/aws-lambda-go/test/integ.function.expected.json index 9ad2aa2afce94..fe40549eba6d3 100644 --- a/packages/@aws-cdk/aws-lambda-go/test/integ.function.expected.json +++ b/packages/@aws-cdk/aws-lambda-go/test/integ.function.expected.json @@ -36,7 +36,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameterse04b349f3d0535498861679603e52edaa01d01ee442f4f665c16e22e5bc81820S3Bucket854EE9A9" + "Ref": "AssetParametersafe3256c3d565f40df78c0343322cb3d8b20d5dbc5e0ff560dd9ed4677e0adb1S3Bucket6ED74DF1" }, "S3Key": { "Fn::Join": [ @@ -49,7 +49,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameterse04b349f3d0535498861679603e52edaa01d01ee442f4f665c16e22e5bc81820S3VersionKey2AD7C6E5" + "Ref": "AssetParametersafe3256c3d565f40df78c0343322cb3d8b20d5dbc5e0ff560dd9ed4677e0adb1S3VersionKeyB0821FCE" } ] } @@ -62,7 +62,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameterse04b349f3d0535498861679603e52edaa01d01ee442f4f665c16e22e5bc81820S3VersionKey2AD7C6E5" + "Ref": "AssetParametersafe3256c3d565f40df78c0343322cb3d8b20d5dbc5e0ff560dd9ed4677e0adb1S3VersionKeyB0821FCE" } ] } @@ -87,17 +87,17 @@ } }, "Parameters": { - "AssetParameterse04b349f3d0535498861679603e52edaa01d01ee442f4f665c16e22e5bc81820S3Bucket854EE9A9": { + "AssetParametersafe3256c3d565f40df78c0343322cb3d8b20d5dbc5e0ff560dd9ed4677e0adb1S3Bucket6ED74DF1": { "Type": "String", - "Description": "S3 bucket for asset \"e04b349f3d0535498861679603e52edaa01d01ee442f4f665c16e22e5bc81820\"" + "Description": "S3 bucket for asset \"afe3256c3d565f40df78c0343322cb3d8b20d5dbc5e0ff560dd9ed4677e0adb1\"" }, - "AssetParameterse04b349f3d0535498861679603e52edaa01d01ee442f4f665c16e22e5bc81820S3VersionKey2AD7C6E5": { + "AssetParametersafe3256c3d565f40df78c0343322cb3d8b20d5dbc5e0ff560dd9ed4677e0adb1S3VersionKeyB0821FCE": { "Type": "String", - "Description": "S3 key for asset version \"e04b349f3d0535498861679603e52edaa01d01ee442f4f665c16e22e5bc81820\"" + "Description": "S3 key for asset version \"afe3256c3d565f40df78c0343322cb3d8b20d5dbc5e0ff560dd9ed4677e0adb1\"" }, - "AssetParameterse04b349f3d0535498861679603e52edaa01d01ee442f4f665c16e22e5bc81820ArtifactHash245320AE": { + "AssetParametersafe3256c3d565f40df78c0343322cb3d8b20d5dbc5e0ff560dd9ed4677e0adb1ArtifactHash2DFD542A": { "Type": "String", - "Description": "Artifact hash for asset \"e04b349f3d0535498861679603e52edaa01d01ee442f4f665c16e22e5bc81820\"" + "Description": "Artifact hash for asset \"afe3256c3d565f40df78c0343322cb3d8b20d5dbc5e0ff560dd9ed4677e0adb1\"" } } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-lambda-go/test/lambda-handler-vendor/cmd/api/main.go b/packages/@aws-cdk/aws-lambda-go/test/lambda-handler-vendor/cmd/api/main.go index 3dc0f71439536..a704fea2def58 100644 --- a/packages/@aws-cdk/aws-lambda-go/test/lambda-handler-vendor/cmd/api/main.go +++ b/packages/@aws-cdk/aws-lambda-go/test/lambda-handler-vendor/cmd/api/main.go @@ -1,20 +1,12 @@ package main -import ( - "context" - "fmt" - - "github.com/aws/aws-lambda-go/lambda" -) - -type MyEvent struct { - Name string `json:"name"` -} - -func HandleRequest(ctx context.Context, name MyEvent) (string, error) { - return fmt.Sprintf("Hello %s!", name.Name), nil -} +// Intentionally empty. There were issues with 'gopkg.in', so: +// - we cannot depend on 'github.com/aws/aws-lambda-go' +// - since: it has a dependency on 'gopkg.in/yaml.v3' +// - therefore: we cannot type the handler properly here +// +// It doesn't matter that this isn't an actual Lambda handler, we +// just need the test build to succeed. func main() { - lambda.Start(HandleRequest) } diff --git a/packages/@aws-cdk/aws-lambda-go/test/lambda-handler-vendor/go.mod b/packages/@aws-cdk/aws-lambda-go/test/lambda-handler-vendor/go.mod index 24ac2f8c41afa..9f22f42e6b18c 100644 --- a/packages/@aws-cdk/aws-lambda-go/test/lambda-handler-vendor/go.mod +++ b/packages/@aws-cdk/aws-lambda-go/test/lambda-handler-vendor/go.mod @@ -2,9 +2,3 @@ module aws-lambda-golang go 1.16 -require ( - github.com/aws/aws-lambda-go v1.19.1 - github.com/kr/text v0.2.0 // indirect - github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect - gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b // indirect -) diff --git a/packages/@aws-cdk/aws-lambda-go/test/lambda-handler-vendor/go.sum b/packages/@aws-cdk/aws-lambda-go/test/lambda-handler-vendor/go.sum deleted file mode 100644 index 94f7ebf0f685e..0000000000000 --- a/packages/@aws-cdk/aws-lambda-go/test/lambda-handler-vendor/go.sum +++ /dev/null @@ -1,32 +0,0 @@ -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/aws/aws-lambda-go v1.19.1 h1:5iUHbIZ2sG6Yq/J1IN3sWm3+vAB1CWwhI21NffLNuNI= -github.com/aws/aws-lambda-go v1.19.1/go.mod h1:jJmlefzPfGnckuHdXX7/80O3BvUUi12XOkbv4w9SGLU= -github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/urfave/cli/v2 v2.2.0/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b h1:QRR6H1YWRnHb4Y/HeNFCTJLFVxaq6wH4YuVdsUOr75U= -gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclpTYkz2zFM+lzLJFO4gQ= -gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/packages/@aws-cdk/aws-lambda-nodejs/README.md b/packages/@aws-cdk/aws-lambda-nodejs/README.md index ae6f6014fa9a1..81fb45b3b1f4a 100644 --- a/packages/@aws-cdk/aws-lambda-nodejs/README.md +++ b/packages/@aws-cdk/aws-lambda-nodejs/README.md @@ -173,7 +173,7 @@ new lambda.NodejsFunction(this, 'my-handler', { bundling: { minify: true, // minify code, defaults to false sourceMap: true, // include source map, defaults to false - sourceMapMode: SourceMapMode.INLINE, // defaults to SourceMapMode.DEFAULT + sourceMapMode: lambda.SourceMapMode.INLINE, // defaults to SourceMapMode.DEFAULT sourcesContent: false, // do not include original source into source map, defaults to true target: 'es2020', // target environment for the generated JavaScript code loader: { // Use the 'dataurl' loader for '.png' files @@ -184,13 +184,13 @@ new lambda.NodejsFunction(this, 'my-handler', { 'process.env.PRODUCTION': JSON.stringify(true), 'process.env.NUMBER': JSON.stringify(123), }, - logLevel: LogLevel.SILENT, // defaults to LogLevel.WARNING + logLevel: lambda.LogLevel.SILENT, // defaults to LogLevel.WARNING keepNames: true, // defaults to false tsconfig: 'custom-tsconfig.json', // use custom-tsconfig.json instead of default, metafile: true, // include meta file, defaults to false banner: '/* comments */', // requires esbuild >= 0.9.0, defaults to none footer: '/* comments */', // requires esbuild >= 0.9.0, defaults to none - charset: Charset.UTF8, // do not escape non-ASCII characters, defaults to Charset.ASCII + charset: lambda.Charset.UTF8, // do not escape non-ASCII characters, defaults to Charset.ASCII }, }); ``` @@ -199,18 +199,28 @@ new lambda.NodejsFunction(this, 'my-handler', { It is possible to run additional commands by specifying the `commandHooks` prop: -```ts +```text +// This example only available in TypeScript +// Run additional props via `commandHooks` new lambda.NodejsFunction(this, 'my-handler-with-commands', { bundling: { commandHooks: { - // Copy a file so that it will be included in the bundled asset + beforeBundling(inputDir: string, outputDir: string): string[] { + return [ + `echo hello > ${inputDir}/a.txt`, + `cp ${inputDir}/a.txt ${outputDir}`, + ]; + }, afterBundling(inputDir: string, outputDir: string): string[] { - return [`cp ${inputDir}/my-binary.node ${outputDir}`]; - } + return [`cp ${inputDir}/b.txt ${outputDir}/txt`]; + }, + beforeInstall() { + return []; + }, // ... - } + }, // ... - } + }, }); ``` @@ -262,9 +272,9 @@ Use `bundling.buildArgs` to pass build arguments when building the Docker bundli ```ts new lambda.NodejsFunction(this, 'my-handler', { bundling: { - buildArgs: { - HTTPS_PROXY: 'https://127.0.0.1:3001', - }, + buildArgs: { + HTTPS_PROXY: 'https://127.0.0.1:3001', + }, } }); ``` @@ -274,7 +284,7 @@ Use `bundling.dockerImage` to use a custom Docker bundling image: ```ts new lambda.NodejsFunction(this, 'my-handler', { bundling: { - dockerImage: cdk.DockerImage.fromBuild('/path/to/Dockerfile'), + dockerImage: DockerImage.fromBuild('/path/to/Dockerfile'), }, }); ``` diff --git a/packages/@aws-cdk/aws-lambda-nodejs/lib/types.ts b/packages/@aws-cdk/aws-lambda-nodejs/lib/types.ts index b21d7a5fee5c2..e16e9db8120b6 100644 --- a/packages/@aws-cdk/aws-lambda-nodejs/lib/types.ts +++ b/packages/@aws-cdk/aws-lambda-nodejs/lib/types.ts @@ -52,7 +52,7 @@ export interface BundlingOptions { * * @see https://esbuild.github.io/api/#loader * - * @example { '.png': 'dataurl' } + * For example, `{ '.png': 'dataurl' }`. * * @default - use esbuild default loaders */ @@ -92,7 +92,7 @@ export interface BundlingOptions { * * This can be useful if you need to do multiple builds of the same code with different settings. * - * @example { 'tsconfig': 'path/custom.tsconfig.json' } + * For example, `{ 'tsconfig': 'path/custom.tsconfig.json' }`. * * @default - automatically discovered by `esbuild` */ @@ -103,19 +103,18 @@ export interface BundlingOptions { * * The metadata in this JSON file follows this schema (specified using TypeScript syntax): * - * ```typescript - * { - * outputs: { - * [path: string]: { - * bytes: number - * inputs: { - * [path: string]: { bytesInOutput: number } - * } - * imports: { path: string }[] - * exports: string[] - * } - * } + * ```text + * { + * outputs: { + * [path: string]: { + * bytes: number + * inputs: { + * [path: string]: { bytesInOutput: number } + * } + * imports: { path: string }[] + * exports: string[] * } + * } * } * ``` * This data can then be analyzed by other tools. For example, @@ -171,8 +170,9 @@ export interface BundlingOptions { /** * Replace global identifiers with constant expressions. * - * @example { 'process.env.DEBUG': 'true' } - * @example { 'process.env.API_KEY': JSON.stringify('xxx-xxxx-xxx') } + * For example, `{ 'process.env.DEBUG': 'true' }`. + * + * Another example, `{ 'process.env.API_KEY': JSON.stringify('xxx-xxxx-xxx') }`. * * @default - no replacements are made */ @@ -273,15 +273,14 @@ export interface BundlingOptions { * * Commands are chained with `&&`. * - * @example - * { - * // Copy a file from the input directory to the output directory - * // to include it in the bundled asset - * afterBundling(inputDir: string, outputDir: string): string[] { - * return [`cp ${inputDir}/my-binary.node ${outputDir}`]; - * } - * // ... + * The following example (specified in TypeScript) copies a file from the input + * directory to the output directory to include it in the bundled asset: + * + * ```text + * afterBundling(inputDir: string, outputDir: string): string[]{ + * return [`cp ${inputDir}/my-binary.node ${outputDir}`]; * } + * ``` */ export interface ICommandHooks { /** diff --git a/packages/@aws-cdk/aws-lambda-nodejs/package.json b/packages/@aws-cdk/aws-lambda-nodejs/package.json index 42256c76533ca..b27b6346b7a0c 100644 --- a/packages/@aws-cdk/aws-lambda-nodejs/package.json +++ b/packages/@aws-cdk/aws-lambda-nodejs/package.json @@ -28,7 +28,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", diff --git a/packages/@aws-cdk/aws-lambda-nodejs/rosetta/default.ts-fixture b/packages/@aws-cdk/aws-lambda-nodejs/rosetta/default.ts-fixture new file mode 100644 index 0000000000000..d1ec43165548e --- /dev/null +++ b/packages/@aws-cdk/aws-lambda-nodejs/rosetta/default.ts-fixture @@ -0,0 +1,12 @@ +// Fixture with packages imported, but nothing else +import { Construct } from 'constructs'; +import { DockerImage, Stack } from '@aws-cdk/core'; +import * as lambda from '@aws-cdk/aws-lambda-nodejs'; + +class Fixture extends Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + + /// here + } +} diff --git a/packages/@aws-cdk/aws-lambda-python/README.md b/packages/@aws-cdk/aws-lambda-python/README.md index 4106b6210b871..f238a3f309410 100644 --- a/packages/@aws-cdk/aws-lambda-python/README.md +++ b/packages/@aws-cdk/aws-lambda-python/README.md @@ -25,14 +25,11 @@ To use this module, you will need to have Docker installed. Define a `PythonFunction`: ```ts -import * as lambda from "@aws-cdk/aws-lambda"; -import { PythonFunction } from "@aws-cdk/aws-lambda-python"; - -new PythonFunction(this, 'MyFunction', { +new lambda.PythonFunction(this, 'MyFunction', { entry: '/path/to/my/function', // required index: 'my_index.py', // optional, defaults to 'index.py' handler: 'my_exported_func', // optional, defaults to 'handler' - runtime: lambda.Runtime.PYTHON_3_6, // optional, defaults to lambda.Runtime.PYTHON_3_7 + runtime: Runtime.PYTHON_3_6, // optional, defaults to lambda.Runtime.PYTHON_3_7 }); ``` diff --git a/packages/@aws-cdk/aws-lambda-python/package.json b/packages/@aws-cdk/aws-lambda-python/package.json index ea33de04517ff..6cfec00d17893 100644 --- a/packages/@aws-cdk/aws-lambda-python/package.json +++ b/packages/@aws-cdk/aws-lambda-python/package.json @@ -28,7 +28,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", diff --git a/packages/@aws-cdk/aws-lambda-python/rosetta/default.ts-fixture b/packages/@aws-cdk/aws-lambda-python/rosetta/default.ts-fixture new file mode 100644 index 0000000000000..516396a167e8e --- /dev/null +++ b/packages/@aws-cdk/aws-lambda-python/rosetta/default.ts-fixture @@ -0,0 +1,13 @@ +// Fixture with packages imported, but nothing else +import { Construct } from 'constructs'; +import { Stack } from '@aws-cdk/core'; +import { Runtime } from '@aws-cdk/aws-lambda'; +import * as lambda from '@aws-cdk/aws-lambda-python'; + +class Fixture extends Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + + /// here + } +} diff --git a/packages/@aws-cdk/aws-lambda/lib/code.ts b/packages/@aws-cdk/aws-lambda/lib/code.ts index 293c91f1485d9..f51e91de9bfb7 100644 --- a/packages/@aws-cdk/aws-lambda/lib/code.ts +++ b/packages/@aws-cdk/aws-lambda/lib/code.ts @@ -517,28 +517,45 @@ export interface AssetImageCodeProps extends ecr_assets.DockerImageAssetOptions */ export class AssetImageCode extends Code { public readonly isInline: boolean = false; + private asset?: ecr_assets.DockerImageAsset; constructor(private readonly directory: string, private readonly props: AssetImageCodeProps) { super(); } public bind(scope: Construct): CodeConfig { - const asset = new ecr_assets.DockerImageAsset(scope, 'AssetImage', { - directory: this.directory, - ...this.props, - }); - - asset.repository.grantPull(new iam.ServicePrincipal('lambda.amazonaws.com')); + // If the same AssetImageCode is used multiple times, retain only the first instantiation. + if (!this.asset) { + this.asset = new ecr_assets.DockerImageAsset(scope, 'AssetImage', { + directory: this.directory, + ...this.props, + }); + this.asset.repository.grantPull(new iam.ServicePrincipal('lambda.amazonaws.com')); + } else if (cdk.Stack.of(this.asset) !== cdk.Stack.of(scope)) { + throw new Error(`Asset is already associated with another stack '${cdk.Stack.of(this.asset).stackName}'. ` + + 'Create a new Code instance for every stack.'); + } return { image: { - imageUri: asset.imageUri, + imageUri: this.asset.imageUri, entrypoint: this.props.entrypoint, cmd: this.props.cmd, workingDirectory: this.props.workingDirectory, }, }; } + + public bindToResource(resource: cdk.CfnResource, options: ResourceBindOptions = { }) { + if (!this.asset) { + throw new Error('bindToResource() must be called after bind()'); + } + + const resourceProperty = options.resourceProperty || 'Code.ImageUri'; + + // https://github.com/aws/aws-cdk/issues/14593 + this.asset.addResourceMetadata(resource, resourceProperty); + } } /** diff --git a/packages/@aws-cdk/aws-lambda/test/code.test.ts b/packages/@aws-cdk/aws-lambda/test/code.test.ts index 194ebda9aff37..9b67102d8e8c8 100644 --- a/packages/@aws-cdk/aws-lambda/test/code.test.ts +++ b/packages/@aws-cdk/aws-lambda/test/code.test.ts @@ -332,6 +332,110 @@ describe('code', () => { }, }); }); + + test('only one Asset object gets created even if multiple functions use the same AssetImageCode', () => { + // given + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'MyStack'); + const directoryAsset = lambda.Code.fromAssetImage(path.join(__dirname, 'docker-lambda-handler')); + + // when + new lambda.Function(stack, 'Fn1', { + code: directoryAsset, + handler: lambda.Handler.FROM_IMAGE, + runtime: lambda.Runtime.FROM_IMAGE, + }); + + new lambda.Function(stack, 'Fn2', { + code: directoryAsset, + handler: lambda.Handler.FROM_IMAGE, + runtime: lambda.Runtime.FROM_IMAGE, + }); + + // then + const assembly = app.synth(); + const synthesized = assembly.stacks[0]; + + // Func1 has an asset, Func2 does not + expect(synthesized.assets.length).toEqual(1); + }); + + test('adds code asset metadata', () => { + // given + const stack = new cdk.Stack(); + stack.node.setContext(cxapi.ASSET_RESOURCE_METADATA_ENABLED_CONTEXT, true); + + const dockerfilePath = 'Dockerfile'; + const dockerBuildTarget = 'stage'; + const dockerBuildArgs = { arg1: 'val1', arg2: 'val2' }; + + // when + new lambda.Function(stack, 'Fn', { + code: lambda.Code.fromAssetImage(path.join(__dirname, 'docker-lambda-handler'), { + file: dockerfilePath, + target: dockerBuildTarget, + buildArgs: dockerBuildArgs, + }), + handler: lambda.Handler.FROM_IMAGE, + runtime: lambda.Runtime.FROM_IMAGE, + }); + + // then + expect(stack).toHaveResource('AWS::Lambda::Function', { + Metadata: { + [cxapi.ASSET_RESOURCE_METADATA_PATH_KEY]: 'asset.650a009a909c30e767a843a84ff7812616447251d245e0ab65d9bfb37f413e32', + [cxapi.ASSET_RESOURCE_METADATA_DOCKERFILE_PATH_KEY]: dockerfilePath, + [cxapi.ASSET_RESOURCE_METADATA_DOCKER_BUILD_ARGS_KEY]: dockerBuildArgs, + [cxapi.ASSET_RESOURCE_METADATA_DOCKER_BUILD_TARGET_KEY]: dockerBuildTarget, + [cxapi.ASSET_RESOURCE_METADATA_PROPERTY_KEY]: 'Code.ImageUri', + }, + }, ResourcePart.CompleteDefinition); + }); + + test('adds code asset metadata with default dockerfile path', () => { + // given + const stack = new cdk.Stack(); + stack.node.setContext(cxapi.ASSET_RESOURCE_METADATA_ENABLED_CONTEXT, true); + + // when + new lambda.Function(stack, 'Fn', { + code: lambda.Code.fromAssetImage(path.join(__dirname, 'docker-lambda-handler')), + handler: lambda.Handler.FROM_IMAGE, + runtime: lambda.Runtime.FROM_IMAGE, + }); + + // then + expect(stack).toHaveResource('AWS::Lambda::Function', { + Metadata: { + [cxapi.ASSET_RESOURCE_METADATA_PATH_KEY]: 'asset.a3cc4528c34874616814d9b3436ff0e5d01514c1d563ed8899657ca00982f308', + [cxapi.ASSET_RESOURCE_METADATA_DOCKERFILE_PATH_KEY]: 'Dockerfile', + [cxapi.ASSET_RESOURCE_METADATA_PROPERTY_KEY]: 'Code.ImageUri', + }, + }, ResourcePart.CompleteDefinition); + }); + + test('fails if asset is bound with a second stack', () => { + // given + const app = new cdk.App(); + const asset = lambda.Code.fromAssetImage(path.join(__dirname, 'docker-lambda-handler')); + + // when + const stack1 = new cdk.Stack(app, 'Stack1'); + new lambda.Function(stack1, 'Fn', { + code: asset, + handler: lambda.Handler.FROM_IMAGE, + runtime: lambda.Runtime.FROM_IMAGE, + }); + + const stack2 = new cdk.Stack(app, 'Stack2'); + + // then + expect(() => new lambda.Function(stack2, 'Fn', { + code: asset, + handler: lambda.Handler.FROM_IMAGE, + runtime: lambda.Runtime.FROM_IMAGE, + })).toThrow(/already associated/); + }); }); describe('lambda.Code.fromDockerBuild', () => { diff --git a/packages/@aws-cdk/aws-rds/lib/cluster-engine.ts b/packages/@aws-cdk/aws-rds/lib/cluster-engine.ts index cde235257ff8e..26a4743b7e598 100644 --- a/packages/@aws-cdk/aws-rds/lib/cluster-engine.ts +++ b/packages/@aws-cdk/aws-rds/lib/cluster-engine.ts @@ -336,6 +336,8 @@ export class AuroraMysqlEngineVersion { public static readonly VER_2_09_2 = AuroraMysqlEngineVersion.builtIn_5_7('2.09.2'); /** Version "5.7.mysql_aurora.2.10.0". */ public static readonly VER_2_10_0 = AuroraMysqlEngineVersion.builtIn_5_7('2.10.0'); + /** Version "5.7.mysql_aurora.2.10.1". */ + public static readonly VER_2_10_1 = AuroraMysqlEngineVersion.builtIn_5_7('2.10.1'); /** * Create a new AuroraMysqlEngineVersion with an arbitrary version. diff --git a/packages/@aws-cdk/aws-redshift/README.md b/packages/@aws-cdk/aws-redshift/README.md index 8ff734a6be255..7fd769670d808 100644 --- a/packages/@aws-cdk/aws-redshift/README.md +++ b/packages/@aws-cdk/aws-redshift/README.md @@ -167,6 +167,34 @@ new Table(this, 'Table', { }); ``` +The table can be configured to have distStyle attribute and a distKey column: + +```ts fixture=cluster +new Table(this, 'Table', { + tableColumns: [ + { name: 'col1', dataType: 'varchar(4)', distKey: true }, + { name: 'col2', dataType: 'float' }, + ], + cluster: cluster, + databaseName: 'databaseName', + distStyle: TableDistStyle.KEY, +}); +``` + +The table can also be configured to have sortStyle attribute and sortKey columns: + +```ts fixture=cluster +new Table(this, 'Table', { + tableColumns: [ + { name: 'col1', dataType: 'varchar(4)', sortKey: true }, + { name: 'col2', dataType: 'float', sortKey: true }, + ], + cluster: cluster, + databaseName: 'databaseName', + sortStyle: TableSortStyle.COMPOUND, +}); +``` + ### Granting Privileges You can give a user privileges to perform certain actions on a table by using the diff --git a/packages/@aws-cdk/aws-redshift/lib/private/database-query-provider/privileges.ts b/packages/@aws-cdk/aws-redshift/lib/private/database-query-provider/privileges.ts index 9f2064d0e5e5a..d95a9f9d97395 100644 --- a/packages/@aws-cdk/aws-redshift/lib/private/database-query-provider/privileges.ts +++ b/packages/@aws-cdk/aws-redshift/lib/private/database-query-provider/privileges.ts @@ -1,7 +1,9 @@ /* eslint-disable-next-line import/no-unresolved */ import * as AWSLambda from 'aws-lambda'; import { TablePrivilege, UserTablePrivilegesHandlerProps } from '../handler-props'; -import { ClusterProps, executeStatement, makePhysicalId } from './util'; +import { executeStatement } from './redshift-data'; +import { ClusterProps } from './types'; +import { makePhysicalId } from './util'; export async function handler(props: UserTablePrivilegesHandlerProps & ClusterProps, event: AWSLambda.CloudFormationCustomResourceEvent) { const username = props.username; diff --git a/packages/@aws-cdk/aws-redshift/lib/private/database-query-provider/redshift-data.ts b/packages/@aws-cdk/aws-redshift/lib/private/database-query-provider/redshift-data.ts new file mode 100644 index 0000000000000..45bf6d9810b98 --- /dev/null +++ b/packages/@aws-cdk/aws-redshift/lib/private/database-query-provider/redshift-data.ts @@ -0,0 +1,34 @@ +/* eslint-disable-next-line import/no-extraneous-dependencies */ +import * as RedshiftData from 'aws-sdk/clients/redshiftdata'; +import { ClusterProps } from './types'; + +const redshiftData = new RedshiftData(); + +export async function executeStatement(statement: string, clusterProps: ClusterProps): Promise { + const executeStatementProps = { + ClusterIdentifier: clusterProps.clusterName, + Database: clusterProps.databaseName, + SecretArn: clusterProps.adminUserArn, + Sql: statement, + }; + const executedStatement = await redshiftData.executeStatement(executeStatementProps).promise(); + if (!executedStatement.Id) { + throw new Error('Service error: Statement execution did not return a statement ID'); + } + await waitForStatementComplete(executedStatement.Id); +} + +const waitTimeout = 100; +async function waitForStatementComplete(statementId: string): Promise { + await new Promise((resolve: (value: void) => void) => { + setTimeout(() => resolve(), waitTimeout); + }); + const statement = await redshiftData.describeStatement({ Id: statementId }).promise(); + if (statement.Status !== 'FINISHED' && statement.Status !== 'FAILED' && statement.Status !== 'ABORTED') { + return waitForStatementComplete(statementId); + } else if (statement.Status === 'FINISHED') { + return; + } else { + throw new Error(`Statement status was ${statement.Status}: ${statement.Error}`); + } +} diff --git a/packages/@aws-cdk/aws-redshift/lib/private/database-query-provider/table.ts b/packages/@aws-cdk/aws-redshift/lib/private/database-query-provider/table.ts index a2e2a4dc4bee9..0716477eb54fe 100644 --- a/packages/@aws-cdk/aws-redshift/lib/private/database-query-provider/table.ts +++ b/packages/@aws-cdk/aws-redshift/lib/private/database-query-provider/table.ts @@ -1,20 +1,21 @@ /* eslint-disable-next-line import/no-unresolved */ import * as AWSLambda from 'aws-lambda'; import { Column } from '../../table'; -import { TableHandlerProps } from '../handler-props'; -import { ClusterProps, executeStatement } from './util'; +import { executeStatement } from './redshift-data'; +import { ClusterProps, TableAndClusterProps, TableSortStyle } from './types'; +import { areColumnsEqual, getDistKeyColumn, getSortKeyColumns } from './util'; -export async function handler(props: TableHandlerProps & ClusterProps, event: AWSLambda.CloudFormationCustomResourceEvent) { +export async function handler(props: TableAndClusterProps, event: AWSLambda.CloudFormationCustomResourceEvent) { const tableNamePrefix = props.tableName.prefix; - const tableNameSuffix = props.tableName.generateSuffix ? `${event.RequestId.substring(0, 8)}` : ''; + const tableNameSuffix = props.tableName.generateSuffix === 'true' ? `${event.RequestId.substring(0, 8)}` : ''; const tableColumns = props.tableColumns; - const clusterProps = props; + const tableAndClusterProps = props; if (event.RequestType === 'Create') { - const tableName = await createTable(tableNamePrefix, tableNameSuffix, tableColumns, clusterProps); + const tableName = await createTable(tableNamePrefix, tableNameSuffix, tableColumns, tableAndClusterProps); return { PhysicalResourceId: tableName }; } else if (event.RequestType === 'Delete') { - await dropTable(event.PhysicalResourceId, clusterProps); + await dropTable(event.PhysicalResourceId, tableAndClusterProps); return; } else if (event.RequestType === 'Update') { const tableName = await updateTable( @@ -22,8 +23,8 @@ export async function handler(props: TableHandlerProps & ClusterProps, event: AW tableNamePrefix, tableNameSuffix, tableColumns, - clusterProps, - event.OldResourceProperties as TableHandlerProps & ClusterProps, + tableAndClusterProps, + event.OldResourceProperties as TableAndClusterProps, ); return { PhysicalResourceId: tableName }; } else { @@ -32,10 +33,33 @@ export async function handler(props: TableHandlerProps & ClusterProps, event: AW } } -async function createTable(tableNamePrefix: string, tableNameSuffix: string, tableColumns: Column[], clusterProps: ClusterProps): Promise { +async function createTable( + tableNamePrefix: string, + tableNameSuffix: string, + tableColumns: Column[], + tableAndClusterProps: TableAndClusterProps, +): Promise { const tableName = tableNamePrefix + tableNameSuffix; const tableColumnsString = tableColumns.map(column => `${column.name} ${column.dataType}`).join(); - await executeStatement(`CREATE TABLE ${tableName} (${tableColumnsString})`, clusterProps); + + let statement = `CREATE TABLE ${tableName} (${tableColumnsString})`; + + if (tableAndClusterProps.distStyle) { + statement += ` DISTSTYLE ${tableAndClusterProps.distStyle}`; + } + + const distKeyColumn = getDistKeyColumn(tableColumns); + if (distKeyColumn) { + statement += ` DISTKEY(${distKeyColumn.name})`; + } + + const sortKeyColumns = getSortKeyColumns(tableColumns); + if (sortKeyColumns.length > 0) { + const sortKeyColumnsString = getSortKeyColumnsString(sortKeyColumns); + statement += ` ${tableAndClusterProps.sortStyle} SORTKEY(${sortKeyColumnsString})`; + } + + await executeStatement(statement, tableAndClusterProps); return tableName; } @@ -48,28 +72,79 @@ async function updateTable( tableNamePrefix: string, tableNameSuffix: string, tableColumns: Column[], - clusterProps: ClusterProps, - oldResourceProperties: TableHandlerProps & ClusterProps, + tableAndClusterProps: TableAndClusterProps, + oldResourceProperties: TableAndClusterProps, ): Promise { + const alterationStatements: string[] = []; + const oldClusterProps = oldResourceProperties; - if (clusterProps.clusterName !== oldClusterProps.clusterName || clusterProps.databaseName !== oldClusterProps.databaseName) { - return createTable(tableNamePrefix, tableNameSuffix, tableColumns, clusterProps); + if (tableAndClusterProps.clusterName !== oldClusterProps.clusterName || tableAndClusterProps.databaseName !== oldClusterProps.databaseName) { + return createTable(tableNamePrefix, tableNameSuffix, tableColumns, tableAndClusterProps); } const oldTableNamePrefix = oldResourceProperties.tableName.prefix; if (tableNamePrefix !== oldTableNamePrefix) { - return createTable(tableNamePrefix, tableNameSuffix, tableColumns, clusterProps); + return createTable(tableNamePrefix, tableNameSuffix, tableColumns, tableAndClusterProps); } const oldTableColumns = oldResourceProperties.tableColumns; if (!oldTableColumns.every(oldColumn => tableColumns.some(column => column.name === oldColumn.name && column.dataType === oldColumn.dataType))) { - return createTable(tableNamePrefix, tableNameSuffix, tableColumns, clusterProps); + return createTable(tableNamePrefix, tableNameSuffix, tableColumns, tableAndClusterProps); } - const additions = tableColumns.filter(column => { + const columnAdditions = tableColumns.filter(column => { return !oldTableColumns.some(oldColumn => column.name === oldColumn.name && column.dataType === oldColumn.dataType); }).map(column => `ADD ${column.name} ${column.dataType}`); - await Promise.all(additions.map(addition => executeStatement(`ALTER TABLE ${tableName} ${addition}`, clusterProps))); + if (columnAdditions.length > 0) { + alterationStatements.push(...columnAdditions.map(addition => `ALTER TABLE ${tableName} ${addition}`)); + } + + const oldDistStyle = oldResourceProperties.distStyle; + if ((!oldDistStyle && tableAndClusterProps.distStyle) || + (oldDistStyle && !tableAndClusterProps.distStyle)) { + return createTable(tableNamePrefix, tableNameSuffix, tableColumns, tableAndClusterProps); + } else if (oldDistStyle !== tableAndClusterProps.distStyle) { + alterationStatements.push(`ALTER TABLE ${tableName} ALTER DISTSTYLE ${tableAndClusterProps.distStyle}`); + } + + const oldDistKey = getDistKeyColumn(oldTableColumns)?.name; + const newDistKey = getDistKeyColumn(tableColumns)?.name; + if ((!oldDistKey && newDistKey ) || (oldDistKey && !newDistKey)) { + return createTable(tableNamePrefix, tableNameSuffix, tableColumns, tableAndClusterProps); + } else if (oldDistKey !== newDistKey) { + alterationStatements.push(`ALTER TABLE ${tableName} ALTER DISTKEY ${newDistKey}`); + } + + const oldSortKeyColumns = getSortKeyColumns(oldTableColumns); + const newSortKeyColumns = getSortKeyColumns(tableColumns); + const oldSortStyle = oldResourceProperties.sortStyle; + const newSortStyle = tableAndClusterProps.sortStyle; + if ((oldSortStyle === newSortStyle && !areColumnsEqual(oldSortKeyColumns, newSortKeyColumns)) + || (oldSortStyle !== newSortStyle)) { + switch (newSortStyle) { + case TableSortStyle.INTERLEAVED: + // INTERLEAVED sort key addition requires replacement. + // https://docs.aws.amazon.com/redshift/latest/dg/r_ALTER_TABLE.html + return createTable(tableNamePrefix, tableNameSuffix, tableColumns, tableAndClusterProps); + + case TableSortStyle.COMPOUND: { + const sortKeyColumnsString = getSortKeyColumnsString(newSortKeyColumns); + alterationStatements.push(`ALTER TABLE ${tableName} ALTER ${newSortStyle} SORTKEY(${sortKeyColumnsString})`); + break; + } + + case TableSortStyle.AUTO: { + alterationStatements.push(`ALTER TABLE ${tableName} ALTER SORTKEY ${newSortStyle}`); + break; + } + } + } + + await Promise.all(alterationStatements.map(statement => executeStatement(statement, tableAndClusterProps))); return tableName; } + +function getSortKeyColumnsString(sortKeyColumns: Column[]) { + return sortKeyColumns.map(column => column.name).join(); +} diff --git a/packages/@aws-cdk/aws-redshift/lib/private/database-query-provider/types.ts b/packages/@aws-cdk/aws-redshift/lib/private/database-query-provider/types.ts new file mode 100644 index 0000000000000..6d80398b7f41b --- /dev/null +++ b/packages/@aws-cdk/aws-redshift/lib/private/database-query-provider/types.ts @@ -0,0 +1,26 @@ +import { DatabaseQueryHandlerProps, TableHandlerProps } from '../handler-props'; + +export type ClusterProps = Omit; +export type TableAndClusterProps = TableHandlerProps & ClusterProps; + +/** + * The sort style of a table. + * This has been duplicated here to exporting private types. + */ +export enum TableSortStyle { + /** + * Amazon Redshift assigns an optimal sort key based on the table data. + */ + AUTO = 'AUTO', + + /** + * Specifies that the data is sorted using a compound key made up of all of the listed columns, + * in the order they are listed. + */ + COMPOUND = 'COMPOUND', + + /** + * Specifies that the data is sorted using an interleaved sort key. + */ + INTERLEAVED = 'INTERLEAVED', +} diff --git a/packages/@aws-cdk/aws-redshift/lib/private/database-query-provider/user.ts b/packages/@aws-cdk/aws-redshift/lib/private/database-query-provider/user.ts index 707af78714e43..ae19440230ae9 100644 --- a/packages/@aws-cdk/aws-redshift/lib/private/database-query-provider/user.ts +++ b/packages/@aws-cdk/aws-redshift/lib/private/database-query-provider/user.ts @@ -3,7 +3,9 @@ import * as AWSLambda from 'aws-lambda'; /* eslint-disable-next-line import/no-extraneous-dependencies */ import * as SecretsManager from 'aws-sdk/clients/secretsmanager'; import { UserHandlerProps } from '../handler-props'; -import { ClusterProps, executeStatement, makePhysicalId } from './util'; +import { executeStatement } from './redshift-data'; +import { ClusterProps } from './types'; +import { makePhysicalId } from './util'; const secretsManager = new SecretsManager(); diff --git a/packages/@aws-cdk/aws-redshift/lib/private/database-query-provider/util.ts b/packages/@aws-cdk/aws-redshift/lib/private/database-query-provider/util.ts index d834cd474f986..c6fe3709bd136 100644 --- a/packages/@aws-cdk/aws-redshift/lib/private/database-query-provider/util.ts +++ b/packages/@aws-cdk/aws-redshift/lib/private/database-query-provider/util.ts @@ -1,40 +1,33 @@ -/* eslint-disable-next-line import/no-extraneous-dependencies */ -import * as RedshiftData from 'aws-sdk/clients/redshiftdata'; -import { DatabaseQueryHandlerProps } from '../handler-props'; +import { Column } from '../../table'; +import { ClusterProps } from './types'; -const redshiftData = new RedshiftData(); +export function makePhysicalId(resourceName: string, clusterProps: ClusterProps, requestId: string): string { + return `${clusterProps.clusterName}:${clusterProps.databaseName}:${resourceName}:${requestId}`; +} -export type ClusterProps = Omit; +export function getDistKeyColumn(columns: Column[]): Column | undefined { + // string comparison is required for custom resource since everything is passed as string + const distKeyColumns = columns.filter(column => column.distKey === true || (column.distKey as unknown as string) === 'true'); -export async function executeStatement(statement: string, clusterProps: ClusterProps): Promise { - const executeStatementProps = { - ClusterIdentifier: clusterProps.clusterName, - Database: clusterProps.databaseName, - SecretArn: clusterProps.adminUserArn, - Sql: statement, - }; - const executedStatement = await redshiftData.executeStatement(executeStatementProps).promise(); - if (!executedStatement.Id) { - throw new Error('Service error: Statement execution did not return a statement ID'); + if (distKeyColumns.length === 0) { + return undefined; + } else if (distKeyColumns.length > 1) { + throw new Error('Multiple dist key columns found'); } - await waitForStatementComplete(executedStatement.Id); + + return distKeyColumns[0]; } -const waitTimeout = 100; -async function waitForStatementComplete(statementId: string): Promise { - await new Promise((resolve: (value: void) => void) => { - setTimeout(() => resolve(), waitTimeout); - }); - const statement = await redshiftData.describeStatement({ Id: statementId }).promise(); - if (statement.Status !== 'FINISHED' && statement.Status !== 'FAILED' && statement.Status !== 'ABORTED') { - return waitForStatementComplete(statementId); - } else if (statement.Status === 'FINISHED') { - return; - } else { - throw new Error(`Statement status was ${statement.Status}: ${statement.Error}`); - } +export function getSortKeyColumns(columns: Column[]): Column[] { + // string comparison is required for custom resource since everything is passed as string + return columns.filter(column => column.sortKey === true || (column.sortKey as unknown as string) === 'true'); } -export function makePhysicalId(resourceName: string, clusterProps: ClusterProps, requestId: string): string { - return `${clusterProps.clusterName}:${clusterProps.databaseName}:${resourceName}:${requestId}`; +export function areColumnsEqual(columnsA: Column[], columnsB: Column[]): boolean { + if (columnsA.length !== columnsB.length) { + return false; + } + return columnsA.every(columnA => { + return columnsB.find(column => column.name === columnA.name && column.dataType === columnA.dataType); + }); } diff --git a/packages/@aws-cdk/aws-redshift/lib/private/handler-props.ts b/packages/@aws-cdk/aws-redshift/lib/private/handler-props.ts index b00cc667a2ced..97089078f00a2 100644 --- a/packages/@aws-cdk/aws-redshift/lib/private/handler-props.ts +++ b/packages/@aws-cdk/aws-redshift/lib/private/handler-props.ts @@ -1,4 +1,4 @@ -import { Column } from '../table'; +import { Column, TableDistStyle, TableSortStyle } from '../table'; export interface DatabaseQueryHandlerProps { readonly handler: string; @@ -15,9 +15,11 @@ export interface UserHandlerProps { export interface TableHandlerProps { readonly tableName: { readonly prefix: string; - readonly generateSuffix: boolean; + readonly generateSuffix: string; }; readonly tableColumns: Column[]; + readonly distStyle?: TableDistStyle; + readonly sortStyle: TableSortStyle; } export interface TablePrivilege { diff --git a/packages/@aws-cdk/aws-redshift/lib/table.ts b/packages/@aws-cdk/aws-redshift/lib/table.ts index 337abdedd00a1..c9bf7c4c46ac2 100644 --- a/packages/@aws-cdk/aws-redshift/lib/table.ts +++ b/packages/@aws-cdk/aws-redshift/lib/table.ts @@ -4,6 +4,7 @@ import { ICluster } from './cluster'; import { DatabaseOptions } from './database-options'; import { DatabaseQuery } from './private/database-query'; import { HandlerName } from './private/database-query-provider/handler-name'; +import { getDistKeyColumn, getSortKeyColumns } from './private/database-query-provider/util'; import { TableHandlerProps } from './private/handler-props'; import { IUser } from './user'; @@ -66,6 +67,20 @@ export interface Column { * The data type of the column. */ readonly dataType: string; + + /** + * Boolean value that indicates whether the column is to be configured as DISTKEY. + * + * @default - column is not DISTKEY + */ + readonly distKey?: boolean; + + /** + * Boolean value that indicates whether the column is to be configured as SORTKEY. + * + * @default - column is not a SORTKEY + */ + readonly sortKey?: boolean; } /** @@ -84,6 +99,20 @@ export interface TableProps extends DatabaseOptions { */ readonly tableColumns: Column[]; + /** + * The distribution style of the table. + * + * @default TableDistStyle.AUTO + */ + readonly distStyle?: TableDistStyle; + + /** + * The sort style of the table. + * + * @default TableSortStyle.AUTO if no sort key is specified, TableSortStyle.COMPOUND if a sort key is specified + */ + readonly sortStyle?: TableSortStyle; + /** * The policy to apply when this resource is removed from the application. * @@ -183,6 +212,14 @@ export class Table extends TableBase { constructor(scope: Construct, id: string, props: TableProps) { super(scope, id); + this.validateDistKeyColumns(props.tableColumns); + if (props.distStyle) { + this.validateDistStyle(props.distStyle, props.tableColumns); + } + if (props.sortStyle) { + this.validateSortStyle(props.sortStyle, props.tableColumns); + } + this.tableColumns = props.tableColumns; this.cluster = props.cluster; this.databaseName = props.databaseName; @@ -194,9 +231,11 @@ export class Table extends TableBase { properties: { tableName: { prefix: props.tableName ?? cdk.Names.uniqueId(this), - generateSuffix: !props.tableName, + generateSuffix: !props.tableName ? 'true' : 'false', }, tableColumns: this.tableColumns, + distStyle: props.distStyle, + sortStyle: props.sortStyle ?? this.getDefaultSortStyle(props.tableColumns), }, }); @@ -219,4 +258,83 @@ export class Table extends TableBase { public applyRemovalPolicy(policy: cdk.RemovalPolicy): void { this.resource.applyRemovalPolicy(policy); } + + private validateDistKeyColumns(columns: Column[]): void { + try { + getDistKeyColumn(columns); + } catch (err) { + throw new Error('Only one column can be configured as distKey.'); + } + } + + private validateDistStyle(distStyle: TableDistStyle, columns: Column[]): void { + const distKeyColumn = getDistKeyColumn(columns); + if (distKeyColumn && distStyle !== TableDistStyle.KEY) { + throw new Error(`Only 'TableDistStyle.KEY' can be configured when distKey is also configured. Found ${distStyle}`); + } + if (!distKeyColumn && distStyle === TableDistStyle.KEY) { + throw new Error('distStyle of "TableDistStyle.KEY" can only be configured when distKey is also configured.'); + } + } + + private validateSortStyle(sortStyle: TableSortStyle, columns: Column[]): void { + const sortKeyColumns = getSortKeyColumns(columns); + if (sortKeyColumns.length === 0 && sortStyle !== TableSortStyle.AUTO) { + throw new Error(`sortStyle of '${sortStyle}' can only be configured when sortKey is also configured.`); + } + if (sortKeyColumns.length > 0 && sortStyle === TableSortStyle.AUTO) { + throw new Error(`sortStyle of '${TableSortStyle.AUTO}' cannot be configured when sortKey is also configured.`); + } + } + + private getDefaultSortStyle(columns: Column[]): TableSortStyle { + const sortKeyColumns = getSortKeyColumns(columns); + return (sortKeyColumns.length === 0) ? TableSortStyle.AUTO : TableSortStyle.COMPOUND; + } +} + +/** + * The data distribution style of a table. + */ +export enum TableDistStyle { + /** + * Amazon Redshift assigns an optimal distribution style based on the table data + */ + AUTO = 'AUTO', + + /** + * The data in the table is spread evenly across the nodes in a cluster in a round-robin distribution. + */ + EVEN = 'EVEN', + + /** + * The data is distributed by the values in the DISTKEY column. + */ + KEY = 'KEY', + + /** + * A copy of the entire table is distributed to every node. + */ + ALL = 'ALL', +} + +/** + * The sort style of a table. + */ +export enum TableSortStyle { + /** + * Amazon Redshift assigns an optimal sort key based on the table data. + */ + AUTO = 'AUTO', + + /** + * Specifies that the data is sorted using a compound key made up of all of the listed columns, + * in the order they are listed. + */ + COMPOUND = 'COMPOUND', + + /** + * Specifies that the data is sorted using an interleaved sort key. + */ + INTERLEAVED = 'INTERLEAVED', } diff --git a/packages/@aws-cdk/aws-redshift/rosetta/cluster.ts-fixture b/packages/@aws-cdk/aws-redshift/rosetta/cluster.ts-fixture index 82d98ca3e381e..4c7ab6ccdb771 100644 --- a/packages/@aws-cdk/aws-redshift/rosetta/cluster.ts-fixture +++ b/packages/@aws-cdk/aws-redshift/rosetta/cluster.ts-fixture @@ -1,7 +1,7 @@ // Fixture with cluster already created import { Construct, SecretValue, Stack } from '@aws-cdk/core'; import { Vpc } from '@aws-cdk/aws-ec2'; -import { Cluster, Table, TableAction, User } from '@aws-cdk/aws-redshift'; +import { Cluster, Table, TableAction, TableDistStyle, TableSortStyle, User } from '@aws-cdk/aws-redshift'; class Fixture extends Stack { constructor(scope: Construct, id: string) { diff --git a/packages/@aws-cdk/aws-redshift/test/database-query-provider/table.test.ts b/packages/@aws-cdk/aws-redshift/test/database-query-provider/table.test.ts index 956efca1ab81f..7c5534d59a785 100644 --- a/packages/@aws-cdk/aws-redshift/test/database-query-provider/table.test.ts +++ b/packages/@aws-cdk/aws-redshift/test/database-query-provider/table.test.ts @@ -1,18 +1,30 @@ /* eslint-disable-next-line import/no-unresolved */ import type * as AWSLambda from 'aws-lambda'; +const mockExecuteStatement = jest.fn(() => ({ promise: jest.fn(() => ({ Id: 'statementId' })) })); +jest.mock('aws-sdk/clients/redshiftdata', () => class { + executeStatement = mockExecuteStatement; + describeStatement = () => ({ promise: jest.fn(() => ({ Status: 'FINISHED' })) }); +}); +import { Column, TableDistStyle, TableSortStyle } from '../../lib'; +import { handler as manageTable } from '../../lib/private/database-query-provider/table'; +import { TableAndClusterProps } from '../../lib/private/database-query-provider/types'; + +type ResourcePropertiesType = TableAndClusterProps & { ServiceToken: string }; + const tableNamePrefix = 'tableNamePrefix'; const tableColumns = [{ name: 'col1', dataType: 'varchar(1)' }]; const clusterName = 'clusterName'; const adminUserArn = 'adminUserArn'; const databaseName = 'databaseName'; const physicalResourceId = 'PhysicalResourceId'; -const resourceProperties = { +const resourceProperties: ResourcePropertiesType = { tableName: { prefix: tableNamePrefix, - generateSuffix: true, + generateSuffix: 'true', }, tableColumns, + sortStyle: TableSortStyle.AUTO, clusterName, adminUserArn, databaseName, @@ -30,13 +42,6 @@ const genericEvent: AWSLambda.CloudFormationCustomResourceEventCommon = { ResourceType: '', }; -const mockExecuteStatement = jest.fn(() => ({ promise: jest.fn(() => ({ Id: 'statementId' })) })); -jest.mock('aws-sdk/clients/redshiftdata', () => class { - executeStatement = mockExecuteStatement; - describeStatement = () => ({ promise: jest.fn(() => ({ Status: 'FINISHED' })) }); -}); -import { handler as manageTable } from '../../lib/private/database-query-provider/table'; - beforeEach(() => { jest.clearAllMocks(); }); @@ -64,7 +69,7 @@ describe('create', () => { ...resourceProperties, tableName: { ...resourceProperties.tableName, - generateSuffix: false, + generateSuffix: 'false', }, }; @@ -75,6 +80,60 @@ describe('create', () => { Sql: `CREATE TABLE ${tableNamePrefix} (col1 varchar(1))`, })); }); + + test('serializes distKey and distStyle in statement', async () => { + const event = baseEvent; + const newResourceProperties: ResourcePropertiesType = { + ...resourceProperties, + tableColumns: [{ name: 'col1', dataType: 'varchar(1)', distKey: true }], + distStyle: TableDistStyle.KEY, + }; + + await manageTable(newResourceProperties, event); + + expect(mockExecuteStatement).toHaveBeenCalledWith(expect.objectContaining({ + Sql: `CREATE TABLE ${tableNamePrefix}${requestIdTruncated} (col1 varchar(1)) DISTSTYLE KEY DISTKEY(col1)`, + })); + }); + + test('serializes sortKeys and sortStyle in statement', async () => { + const event = baseEvent; + const newResourceProperties: ResourcePropertiesType = { + ...resourceProperties, + tableColumns: [ + { name: 'col1', dataType: 'varchar(1)', sortKey: true }, + { name: 'col2', dataType: 'varchar(1)' }, + { name: 'col3', dataType: 'varchar(1)', sortKey: true }, + ], + sortStyle: TableSortStyle.COMPOUND, + }; + + await manageTable(newResourceProperties, event); + + expect(mockExecuteStatement).toHaveBeenCalledWith(expect.objectContaining({ + Sql: `CREATE TABLE ${tableNamePrefix}${requestIdTruncated} (col1 varchar(1),col2 varchar(1),col3 varchar(1)) COMPOUND SORTKEY(col1,col3)`, + })); + }); + + test('serializes distKey and sortKeys as string booleans', async () => { + const event = baseEvent; + const newResourceProperties: ResourcePropertiesType = { + ...resourceProperties, + tableColumns: [ + { name: 'col1', dataType: 'varchar(4)', distKey: 'true' as unknown as boolean }, + { name: 'col2', dataType: 'float', sortKey: 'true' as unknown as boolean }, + { name: 'col3', dataType: 'float', sortKey: 'true' as unknown as boolean }, + ], + distStyle: TableDistStyle.KEY, + sortStyle: TableSortStyle.COMPOUND, + }; + + await manageTable(newResourceProperties, event); + + expect(mockExecuteStatement).toHaveBeenCalledWith(expect.objectContaining({ + Sql: `CREATE TABLE ${tableNamePrefix}${requestIdTruncated} (col1 varchar(4),col2 float,col3 float) DISTSTYLE KEY DISTKEY(col1) COMPOUND SORTKEY(col2,col3)`, + })); + }); }); describe('delete', () => { @@ -199,4 +258,251 @@ describe('update', () => { Sql: `ALTER TABLE ${physicalResourceId} ADD ${newTableColumnName} ${newTableColumnDataType}`, })); }); + + describe('distStyle and distKey', () => { + test('replaces if distStyle is added', async () => { + const newResourceProperties: ResourcePropertiesType = { + ...resourceProperties, + distStyle: TableDistStyle.EVEN, + }; + + await expect(manageTable(newResourceProperties, event)).resolves.not.toMatchObject({ + PhysicalResourceId: physicalResourceId, + }); + expect(mockExecuteStatement).toHaveBeenCalledWith(expect.objectContaining({ + Sql: `CREATE TABLE ${tableNamePrefix}${requestIdTruncated} (col1 varchar(1)) DISTSTYLE EVEN`, + })); + }); + + test('replaces if distStyle is removed', async () => { + const newEvent = { + ...event, + OldResourceProperties: { + ...event.OldResourceProperties, + distStyle: TableDistStyle.EVEN, + }, + }; + const newResourceProperties = { + ...resourceProperties, + }; + + await expect(manageTable(newResourceProperties, newEvent)).resolves.not.toMatchObject({ + PhysicalResourceId: physicalResourceId, + }); + expect(mockExecuteStatement).toHaveBeenCalledWith(expect.objectContaining({ + Sql: `CREATE TABLE ${tableNamePrefix}${requestIdTruncated} (col1 varchar(1))`, + })); + }); + + test('does not replace if distStyle is changed', async () => { + const newEvent: AWSLambda.CloudFormationCustomResourceEvent = { + ...event, + OldResourceProperties: { + ...event.OldResourceProperties, + distStyle: TableDistStyle.EVEN, + }, + }; + const newDistStyle = TableDistStyle.ALL; + const newResourceProperties: ResourcePropertiesType = { + ...resourceProperties, + distStyle: newDistStyle, + }; + + await expect(manageTable(newResourceProperties, newEvent)).resolves.toMatchObject({ + PhysicalResourceId: physicalResourceId, + }); + expect(mockExecuteStatement).toHaveBeenCalledWith(expect.objectContaining({ + Sql: `ALTER TABLE ${physicalResourceId} ALTER DISTSTYLE ${newDistStyle}`, + })); + }); + + test('replaces if distKey is added', async () => { + const newResourceProperties: ResourcePropertiesType = { + ...resourceProperties, + tableColumns: [{ name: 'col1', dataType: 'varchar(1)', distKey: true }], + }; + + await expect(manageTable(newResourceProperties, event)).resolves.not.toMatchObject({ + PhysicalResourceId: physicalResourceId, + }); + expect(mockExecuteStatement).toHaveBeenCalledWith(expect.objectContaining({ + Sql: `CREATE TABLE ${tableNamePrefix}${requestIdTruncated} (col1 varchar(1)) DISTKEY(col1)`, + })); + }); + + test('replaces if distKey is removed', async () => { + const newEvent: AWSLambda.CloudFormationCustomResourceEvent = { + ...event, + OldResourceProperties: { + ...event.OldResourceProperties, + tableColumns: [{ name: 'col1', dataType: 'varchar(1)', distKey: true }], + }, + }; + const newResourceProperties: ResourcePropertiesType = { + ...resourceProperties, + }; + + await expect(manageTable(newResourceProperties, newEvent)).resolves.not.toMatchObject({ + PhysicalResourceId: physicalResourceId, + }); + expect(mockExecuteStatement).toHaveBeenCalledWith(expect.objectContaining({ + Sql: `CREATE TABLE ${tableNamePrefix}${requestIdTruncated} (col1 varchar(1))`, + })); + }); + + test('does not replace if distKey is changed', async () => { + const newEvent: AWSLambda.CloudFormationCustomResourceEvent = { + ...event, + OldResourceProperties: { + ...event.OldResourceProperties, + tableColumns: [ + { name: 'col1', dataType: 'varchar(1)', distKey: true }, + { name: 'col2', dataType: 'varchar(1)' }, + ], + }, + }; + const newDistKey = 'col2'; + const newResourceProperties: ResourcePropertiesType = { + ...resourceProperties, + tableColumns: [ + { name: 'col1', dataType: 'varchar(1)' }, + { name: 'col2', dataType: 'varchar(1)', distKey: true }, + ], + }; + + await expect(manageTable(newResourceProperties, newEvent)).resolves.toMatchObject({ + PhysicalResourceId: physicalResourceId, + }); + expect(mockExecuteStatement).toHaveBeenCalledWith(expect.objectContaining({ + Sql: `ALTER TABLE ${physicalResourceId} ALTER DISTKEY ${newDistKey}`, + })); + }); + }); + + describe('sortStyle and sortKeys', () => { + const oldTableColumnsWithSortKeys: Column[] = [ + { name: 'col1', dataType: 'varchar(1)', sortKey: true }, + { name: 'col2', dataType: 'varchar(1)' }, + ]; + const newTableColumnsWithSortKeys: Column[] = [ + { name: 'col1', dataType: 'varchar(1)' }, + { name: 'col2', dataType: 'varchar(1)', sortKey: true }, + ]; + + test('replaces when same sortStyle, different sortKey columns: INTERLEAVED', async () => { + const newEvent: AWSLambda.CloudFormationCustomResourceEvent = { + ...event, + OldResourceProperties: { + ...event.OldResourceProperties, + tableColumns: oldTableColumnsWithSortKeys, + sortStyle: TableSortStyle.INTERLEAVED, + }, + }; + const newResourceProperties: ResourcePropertiesType = { + ...resourceProperties, + tableColumns: newTableColumnsWithSortKeys, + sortStyle: TableSortStyle.INTERLEAVED, + }; + + await expect(manageTable(newResourceProperties, newEvent)).resolves.not.toMatchObject({ + PhysicalResourceId: physicalResourceId, + }); + expect(mockExecuteStatement).toHaveBeenCalledWith(expect.objectContaining({ + Sql: `CREATE TABLE ${tableNamePrefix}${requestIdTruncated} (col1 varchar(1),col2 varchar(1)) INTERLEAVED SORTKEY(col2)`, + })); + }); + + test('replaces when differnt sortStyle: INTERLEAVED', async () => { + const newEvent: AWSLambda.CloudFormationCustomResourceEvent = { + ...event, + OldResourceProperties: { + ...event.OldResourceProperties, + tableColumns: oldTableColumnsWithSortKeys, + sortStyle: TableSortStyle.AUTO, + }, + }; + const newResourceProperties: ResourcePropertiesType = { + ...resourceProperties, + tableColumns: oldTableColumnsWithSortKeys, + sortStyle: TableSortStyle.INTERLEAVED, + }; + + await expect(manageTable(newResourceProperties, newEvent)).resolves.not.toMatchObject({ + PhysicalResourceId: physicalResourceId, + }); + expect(mockExecuteStatement).toHaveBeenCalledWith(expect.objectContaining({ + Sql: `CREATE TABLE ${tableNamePrefix}${requestIdTruncated} (col1 varchar(1),col2 varchar(1)) INTERLEAVED SORTKEY(col1)`, + })); + }); + + test('does not replace when same sortStyle, different sortKey columns: COMPOUND', async () => { + const newEvent: AWSLambda.CloudFormationCustomResourceEvent = { + ...event, + OldResourceProperties: { + ...event.OldResourceProperties, + tableColumns: oldTableColumnsWithSortKeys, + sortStyle: TableSortStyle.COMPOUND, + }, + }; + const newResourceProperties: ResourcePropertiesType = { + ...resourceProperties, + tableColumns: newTableColumnsWithSortKeys, + sortStyle: TableSortStyle.COMPOUND, + }; + + await expect(manageTable(newResourceProperties, newEvent)).resolves.toMatchObject({ + PhysicalResourceId: physicalResourceId, + }); + expect(mockExecuteStatement).toHaveBeenCalledWith(expect.objectContaining({ + Sql: `ALTER TABLE ${physicalResourceId} ALTER COMPOUND SORTKEY(col2)`, + })); + }); + + test('does not replace when differnt sortStyle: COMPOUND', async () => { + const newEvent: AWSLambda.CloudFormationCustomResourceEvent = { + ...event, + OldResourceProperties: { + ...event.OldResourceProperties, + tableColumns: oldTableColumnsWithSortKeys, + sortStyle: TableSortStyle.AUTO, + }, + }; + const newResourceProperties: ResourcePropertiesType = { + ...resourceProperties, + tableColumns: oldTableColumnsWithSortKeys, + sortStyle: TableSortStyle.COMPOUND, + }; + + await expect(manageTable(newResourceProperties, newEvent)).resolves.toMatchObject({ + PhysicalResourceId: physicalResourceId, + }); + expect(mockExecuteStatement).toHaveBeenCalledWith(expect.objectContaining({ + Sql: `ALTER TABLE ${physicalResourceId} ALTER COMPOUND SORTKEY(col1)`, + })); + }); + + test('does not replace when differnt sortStyle: AUTO', async () => { + const newEvent: AWSLambda.CloudFormationCustomResourceEvent = { + ...event, + OldResourceProperties: { + ...event.OldResourceProperties, + tableColumns: oldTableColumnsWithSortKeys, + sortStyle: TableSortStyle.COMPOUND, + }, + }; + const newResourceProperties: ResourcePropertiesType = { + ...resourceProperties, + tableColumns: oldTableColumnsWithSortKeys, + sortStyle: TableSortStyle.AUTO, + }; + + await expect(manageTable(newResourceProperties, newEvent)).resolves.toMatchObject({ + PhysicalResourceId: physicalResourceId, + }); + expect(mockExecuteStatement).toHaveBeenCalledWith(expect.objectContaining({ + Sql: `ALTER TABLE ${physicalResourceId} ALTER SORTKEY AUTO`, + })); + }); + }); + }); diff --git a/packages/@aws-cdk/aws-redshift/test/integ.database.expected.json b/packages/@aws-cdk/aws-redshift/test/integ.database.expected.json index 4cfb1faea5118..6e909192a7f3d 100644 --- a/packages/@aws-cdk/aws-redshift/test/integ.database.expected.json +++ b/packages/@aws-cdk/aws-redshift/test/integ.database.expected.json @@ -1167,7 +1167,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameters7eb6a250bd5ce32c07f08f536377d71e59ad43e16e25b9aa6e50f6fc20fdfc4fS3Bucket3B967306" + "Ref": "AssetParameters85597bcd6a07abd4673fe02c7e92e21df5859eee0d831e9db67f4d2e74d4d066S3Bucket0B347C2E" }, "S3Key": { "Fn::Join": [ @@ -1180,7 +1180,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters7eb6a250bd5ce32c07f08f536377d71e59ad43e16e25b9aa6e50f6fc20fdfc4fS3VersionKeyC171429B" + "Ref": "AssetParameters85597bcd6a07abd4673fe02c7e92e21df5859eee0d831e9db67f4d2e74d4d066S3VersionKey932D0479" } ] } @@ -1193,7 +1193,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters7eb6a250bd5ce32c07f08f536377d71e59ad43e16e25b9aa6e50f6fc20fdfc4fS3VersionKeyC171429B" + "Ref": "AssetParameters85597bcd6a07abd4673fe02c7e92e21df5859eee0d831e9db67f4d2e74d4d066S3VersionKey932D0479" } ] } @@ -1369,35 +1369,44 @@ "databaseName": "my_db", "tableName": { "prefix": "awscdkredshiftclusterdatabaseTable24923533", - "generateSuffix": true + "generateSuffix": "true" }, "tableColumns": [ { "name": "col1", - "dataType": "varchar(4)" + "dataType": "varchar(4)", + "distKey": true }, { "name": "col2", - "dataType": "float" + "dataType": "float", + "sortKey": true + }, + { + "name": "col3", + "dataType": "float", + "sortKey": true } - ] + ], + "distStyle": "KEY", + "sortStyle": "INTERLEAVED" }, "UpdateReplacePolicy": "Delete", "DeletionPolicy": "Delete" } }, "Parameters": { - "AssetParameters7eb6a250bd5ce32c07f08f536377d71e59ad43e16e25b9aa6e50f6fc20fdfc4fS3Bucket3B967306": { + "AssetParameters85597bcd6a07abd4673fe02c7e92e21df5859eee0d831e9db67f4d2e74d4d066S3Bucket0B347C2E": { "Type": "String", - "Description": "S3 bucket for asset \"7eb6a250bd5ce32c07f08f536377d71e59ad43e16e25b9aa6e50f6fc20fdfc4f\"" + "Description": "S3 bucket for asset \"85597bcd6a07abd4673fe02c7e92e21df5859eee0d831e9db67f4d2e74d4d066\"" }, - "AssetParameters7eb6a250bd5ce32c07f08f536377d71e59ad43e16e25b9aa6e50f6fc20fdfc4fS3VersionKeyC171429B": { + "AssetParameters85597bcd6a07abd4673fe02c7e92e21df5859eee0d831e9db67f4d2e74d4d066S3VersionKey932D0479": { "Type": "String", - "Description": "S3 key for asset version \"7eb6a250bd5ce32c07f08f536377d71e59ad43e16e25b9aa6e50f6fc20fdfc4f\"" + "Description": "S3 key for asset version \"85597bcd6a07abd4673fe02c7e92e21df5859eee0d831e9db67f4d2e74d4d066\"" }, - "AssetParameters7eb6a250bd5ce32c07f08f536377d71e59ad43e16e25b9aa6e50f6fc20fdfc4fArtifactHash0EE8CD3D": { + "AssetParameters85597bcd6a07abd4673fe02c7e92e21df5859eee0d831e9db67f4d2e74d4d066ArtifactHash78689978": { "Type": "String", - "Description": "Artifact hash for asset \"7eb6a250bd5ce32c07f08f536377d71e59ad43e16e25b9aa6e50f6fc20fdfc4f\"" + "Description": "Artifact hash for asset \"85597bcd6a07abd4673fe02c7e92e21df5859eee0d831e9db67f4d2e74d4d066\"" }, "AssetParametersdaeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1S3BucketDC4B98B1": { "Type": "String", @@ -1412,4 +1421,4 @@ "Description": "Artifact hash for asset \"daeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1\"" } } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-redshift/test/integ.database.ts b/packages/@aws-cdk/aws-redshift/test/integ.database.ts index 6e4893c0c0089..d5079b83f0c1b 100644 --- a/packages/@aws-cdk/aws-redshift/test/integ.database.ts +++ b/packages/@aws-cdk/aws-redshift/test/integ.database.ts @@ -39,7 +39,13 @@ const databaseOptions = { const user = new redshift.User(stack, 'User', databaseOptions); const table = new redshift.Table(stack, 'Table', { ...databaseOptions, - tableColumns: [{ name: 'col1', dataType: 'varchar(4)' }, { name: 'col2', dataType: 'float' }], + tableColumns: [ + { name: 'col1', dataType: 'varchar(4)', distKey: true }, + { name: 'col2', dataType: 'float', sortKey: true }, + { name: 'col3', dataType: 'float', sortKey: true }, + ], + distStyle: redshift.TableDistStyle.KEY, + sortStyle: redshift.TableSortStyle.INTERLEAVED, }); table.grant(user, redshift.TableAction.INSERT, redshift.TableAction.DELETE); diff --git a/packages/@aws-cdk/aws-redshift/test/table.test.ts b/packages/@aws-cdk/aws-redshift/test/table.test.ts index 97f66b57042f5..571a87fff5227 100644 --- a/packages/@aws-cdk/aws-redshift/test/table.test.ts +++ b/packages/@aws-cdk/aws-redshift/test/table.test.ts @@ -5,7 +5,10 @@ import * as redshift from '../lib'; describe('cluster table', () => { const tableName = 'tableName'; - const tableColumns = [{ name: 'col1', dataType: 'varchar(4)' }, { name: 'col2', dataType: 'float' }]; + const tableColumns: redshift.Column[] = [ + { name: 'col1', dataType: 'varchar(4)' }, + { name: 'col2', dataType: 'float' }, + ]; let stack: cdk.Stack; let vpc: ec2.Vpc; @@ -40,7 +43,7 @@ describe('cluster table', () => { Template.fromStack(stack).hasResourceProperties('Custom::RedshiftDatabaseQuery', { tableName: { prefix: 'Table', - generateSuffix: true, + generateSuffix: 'true', }, tableColumns, }); @@ -67,7 +70,7 @@ describe('cluster table', () => { Template.fromStack(stack).hasResourceProperties('Custom::RedshiftDatabaseQuery', { tableName: { prefix: tableName, - generateSuffix: false, + generateSuffix: 'false', }, }); }); @@ -135,4 +138,110 @@ describe('cluster table', () => { DeletionPolicy: 'Delete', }); }); + + describe('distKey and distStyle', () => { + it('throws if more than one distKeys are configured', () => { + const updatedTableColumns: redshift.Column[] = [ + ...tableColumns, + { name: 'col3', dataType: 'varchar(4)', distKey: true }, + { name: 'col4', dataType: 'float', distKey: true }, + ]; + + expect( + () => new redshift.Table(stack, 'Table', { + ...databaseOptions, + tableColumns: updatedTableColumns, + }), + ).toThrow(/Only one column can be configured as distKey./); + }); + + it('throws if distStyle other than KEY is configured with configured distKey column', () => { + const updatedTableColumns: redshift.Column[] = [ + ...tableColumns, + { name: 'col3', dataType: 'varchar(4)', distKey: true }, + ]; + + expect( + () => new redshift.Table(stack, 'Table', { + ...databaseOptions, + tableColumns: updatedTableColumns, + distStyle: redshift.TableDistStyle.EVEN, + }), + ).toThrow(`Only 'TableDistStyle.KEY' can be configured when distKey is also configured. Found ${redshift.TableDistStyle.EVEN}`); + }); + + it('throws if KEY distStyle is configired with no distKey column', () => { + expect( + () => new redshift.Table(stack, 'Table', { + ...databaseOptions, + tableColumns, + distStyle: redshift.TableDistStyle.KEY, + }), + ).toThrow('distStyle of "TableDistStyle.KEY" can only be configured when distKey is also configured.'); + }); + }); + + describe('sortKeys and sortStyle', () => { + it('configures default sortStyle based on sortKeys if no sortStyle is passed: AUTO', () => { + // GIVEN + const tableColumnsWithoutSortKey = tableColumns; + + // WHEN + new redshift.Table(stack, 'Table', { + ...databaseOptions, + tableColumns: tableColumnsWithoutSortKey, + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('Custom::RedshiftDatabaseQuery', { + sortStyle: redshift.TableSortStyle.AUTO, + }); + }); + + it('configures default sortStyle based on sortKeys if no sortStyle is passed: COMPOUND', () => { + // GIVEN + const tableColumnsWithSortKey: redshift.Column[] = [ + ...tableColumns, + { name: 'col3', dataType: 'varchar(4)', sortKey: true }, + ]; + + // WHEN + new redshift.Table(stack, 'Table', { + ...databaseOptions, + tableColumns: tableColumnsWithSortKey, + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('Custom::RedshiftDatabaseQuery', { + sortStyle: redshift.TableSortStyle.COMPOUND, + }); + }); + + it('throws if sortStlye other than AUTO is passed with no configured sortKeys', () => { + expect( + () => new redshift.Table(stack, 'Table', { + ...databaseOptions, + tableColumns, + sortStyle: redshift.TableSortStyle.COMPOUND, + }), + ).toThrow(`sortStyle of '${redshift.TableSortStyle.COMPOUND}' can only be configured when sortKey is also configured.`); + }); + + it('throws if sortStlye of AUTO is passed with some configured sortKeys', () => { + // GIVEN + const tableColumnsWithSortKey: redshift.Column[] = [ + ...tableColumns, + { name: 'col3', dataType: 'varchar(4)', sortKey: true }, + ]; + + // THEN + expect( + () => new redshift.Table(stack, 'Table', { + ...databaseOptions, + tableColumns: tableColumnsWithSortKey, + sortStyle: redshift.TableSortStyle.AUTO, + }), + ).toThrow(`sortStyle of '${redshift.TableSortStyle.AUTO}' cannot be configured when sortKey is also configured.`); + }); + }); }); diff --git a/packages/@aws-cdk/aws-securityhub/README.md b/packages/@aws-cdk/aws-securityhub/README.md index 831f2af57d18b..c4b1bebcf6e6c 100644 --- a/packages/@aws-cdk/aws-securityhub/README.md +++ b/packages/@aws-cdk/aws-securityhub/README.md @@ -15,6 +15,6 @@ This module is part of the [AWS Cloud Development Kit](https://github.com/aws/aws-cdk) project. -```ts +```ts nofixture import * as securityhub from '@aws-cdk/aws-securityhub'; ``` diff --git a/packages/@aws-cdk/aws-securityhub/package.json b/packages/@aws-cdk/aws-securityhub/package.json index b1fef1fe83a49..bb36e474a4b74 100644 --- a/packages/@aws-cdk/aws-securityhub/package.json +++ b/packages/@aws-cdk/aws-securityhub/package.json @@ -28,7 +28,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", diff --git a/packages/@aws-cdk/aws-sns-subscriptions/lib/lambda.ts b/packages/@aws-cdk/aws-sns-subscriptions/lib/lambda.ts index aa7581653d5ba..58c7a2aceb16b 100644 --- a/packages/@aws-cdk/aws-sns-subscriptions/lib/lambda.ts +++ b/packages/@aws-cdk/aws-sns-subscriptions/lib/lambda.ts @@ -1,7 +1,7 @@ import * as iam from '@aws-cdk/aws-iam'; import * as lambda from '@aws-cdk/aws-lambda'; import * as sns from '@aws-cdk/aws-sns'; -import { Names, Stack } from '@aws-cdk/core'; +import { Names, Stack, Token } from '@aws-cdk/core'; import { SubscriptionProps } from './subscription'; // keep this import separate from other imports to reduce chance for merge conflicts with v2-main @@ -36,6 +36,12 @@ export class LambdaSubscription implements sns.ITopicSubscription { principal: new iam.ServicePrincipal('sns.amazonaws.com'), }); + // if the topic and function are created in different stacks + // then we need to make sure the topic is created first + if (topic instanceof sns.Topic && topic.stack !== this.fn.stack) { + this.fn.stack.addDependency(topic.stack); + } + return { subscriberScope: this.fn, subscriberId: topic.node.id, @@ -50,6 +56,14 @@ export class LambdaSubscription implements sns.ITopicSubscription { private regionFromArn(topic: sns.ITopic): string | undefined { // no need to specify `region` for topics defined within the same stack. if (topic instanceof sns.Topic) { + if (topic.stack !== this.fn.stack) { + // only if we know the region, will not work for + // env agnostic stacks + if (!Token.isUnresolved(topic.stack.region) && + (topic.stack.region !== this.fn.stack.region)) { + return topic.stack.region; + } + } return undefined; } return Stack.of(topic).parseArn(topic.topicArn).region; diff --git a/packages/@aws-cdk/aws-sns-subscriptions/lib/sqs.ts b/packages/@aws-cdk/aws-sns-subscriptions/lib/sqs.ts index 6cf89ebc53c60..8bbb77927381f 100644 --- a/packages/@aws-cdk/aws-sns-subscriptions/lib/sqs.ts +++ b/packages/@aws-cdk/aws-sns-subscriptions/lib/sqs.ts @@ -1,7 +1,7 @@ import * as iam from '@aws-cdk/aws-iam'; import * as sns from '@aws-cdk/aws-sns'; import * as sqs from '@aws-cdk/aws-sqs'; -import { Names, Stack } from '@aws-cdk/core'; +import { Names, Stack, Token } from '@aws-cdk/core'; import { SubscriptionProps } from './subscription'; // keep this import separate from other imports to reduce chance for merge conflicts with v2-main @@ -61,6 +61,12 @@ export class SqsSubscription implements sns.ITopicSubscription { })); } + // if the topic and queue are created in different stacks + // then we need to make sure the topic is created first + if (topic instanceof sns.Topic && topic.stack !== this.queue.stack) { + this.queue.stack.addDependency(topic.stack); + } + return { subscriberScope: this.queue, subscriberId: Names.nodeUniqueId(topic.node), @@ -76,6 +82,14 @@ export class SqsSubscription implements sns.ITopicSubscription { private regionFromArn(topic: sns.ITopic): string | undefined { // no need to specify `region` for topics defined within the same stack if (topic instanceof sns.Topic) { + if (topic.stack !== this.queue.stack) { + // only if we know the region, will not work for + // env agnostic stacks + if (!Token.isUnresolved(topic.stack.region) && + (topic.stack.region !== this.queue.stack.region)) { + return topic.stack.region; + } + } return undefined; } return Stack.of(topic).parseArn(topic.topicArn).region; diff --git a/packages/@aws-cdk/aws-sns-subscriptions/test/integ.sns-lambda-cross-region.expected.json b/packages/@aws-cdk/aws-sns-subscriptions/test/integ.sns-lambda-cross-region.expected.json new file mode 100644 index 0000000000000..de5216b565954 --- /dev/null +++ b/packages/@aws-cdk/aws-sns-subscriptions/test/integ.sns-lambda-cross-region.expected.json @@ -0,0 +1,116 @@ +[ + { + "Resources": { + "MyTopic86869434": { + "Type": "AWS::SNS::Topic", + "Properties": { + "TopicName": "topicstackopicstackmytopicc43e67afb24f28bb94f9" + } + } + } + }, + { + "Resources": { + "EchoServiceRoleBE28060B": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + } + }, + "Echo11F3FB29": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "ZipFile": "exports.handler = function handler(event, _context, callback) {\n /* eslint-disable no-console */\n console.log('====================================================');\n console.log(JSON.stringify(event, undefined, 2));\n console.log('====================================================');\n return callback(undefined, event);\n}" + }, + "Role": { + "Fn::GetAtt": [ + "EchoServiceRoleBE28060B", + "Arn" + ] + }, + "Handler": "index.handler", + "Runtime": "nodejs10.x" + }, + "DependsOn": [ + "EchoServiceRoleBE28060B" + ] + }, + "EchoAllowInvokeTopicStackMyTopicC43E67AF32CF6EFA": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "Echo11F3FB29", + "Arn" + ] + }, + "Principal": "sns.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":sns:us-east-1:12345678:topicstackopicstackmytopicc43e67afb24f28bb94f9" + ] + ] + } + } + }, + "EchoMyTopic4CB8819E": { + "Type": "AWS::SNS::Subscription", + "Properties": { + "Protocol": "lambda", + "TopicArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":sns:us-east-1:12345678:topicstackopicstackmytopicc43e67afb24f28bb94f9" + ] + ] + }, + "Endpoint": { + "Fn::GetAtt": [ + "Echo11F3FB29", + "Arn" + ] + }, + "Region": "us-east-1" + } + } + } + } +] \ No newline at end of file diff --git a/packages/@aws-cdk/aws-sns-subscriptions/test/integ.sns-lambda-cross-region.ts b/packages/@aws-cdk/aws-sns-subscriptions/test/integ.sns-lambda-cross-region.ts new file mode 100644 index 0000000000000..cfec9592e3dba --- /dev/null +++ b/packages/@aws-cdk/aws-sns-subscriptions/test/integ.sns-lambda-cross-region.ts @@ -0,0 +1,35 @@ +import * as lambda from '@aws-cdk/aws-lambda'; +import * as sns from '@aws-cdk/aws-sns'; +import * as cdk from '@aws-cdk/core'; +import * as subs from '../lib'; + +/// !cdk-integ * +const app = new cdk.App(); + +const topicStack = new cdk.Stack(app, 'TopicStack', { + env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: 'us-east-1' }, +}); +const topic = new sns.Topic(topicStack, 'MyTopic', { + topicName: cdk.PhysicalName.GENERATE_IF_NEEDED, +}); + +const functionStack = new cdk.Stack(app, 'FunctionStack', { + env: { region: 'us-east-2' }, +}); +const fction = new lambda.Function(functionStack, 'Echo', { + handler: 'index.handler', + runtime: lambda.Runtime.NODEJS_10_X, + code: lambda.Code.fromInline(`exports.handler = ${handler.toString()}`), +}); + +topic.addSubscription(new subs.LambdaSubscription(fction)); + +app.synth(); + +function handler(event: any, _context: any, callback: any) { + /* eslint-disable no-console */ + console.log('===================================================='); + console.log(JSON.stringify(event, undefined, 2)); + console.log('===================================================='); + return callback(undefined, event); +} diff --git a/packages/@aws-cdk/aws-sns-subscriptions/test/integ.sns-sqs-cross-region.lit.expected.json b/packages/@aws-cdk/aws-sns-subscriptions/test/integ.sns-sqs-cross-region.lit.expected.json new file mode 100644 index 0000000000000..5bbffb5e31628 --- /dev/null +++ b/packages/@aws-cdk/aws-sns-subscriptions/test/integ.sns-sqs-cross-region.lit.expected.json @@ -0,0 +1,90 @@ +[ + { + "Resources": { + "MyTopic86869434": { + "Type": "AWS::SNS::Topic", + "Properties": { + "TopicName": "topicstackopicstackmytopicc43e67afb24f28bb94f9" + } + } + } + }, + { + "Resources": { + "MyQueueE6CA6235": { + "Type": "AWS::SQS::Queue", + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "MyQueuePolicy6BBEDDAC": { + "Type": "AWS::SQS::QueuePolicy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "sqs:SendMessage", + "Condition": { + "ArnEquals": { + "aws:SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":sns:us-east-1:12345678:topicstackopicstackmytopicc43e67afb24f28bb94f9" + ] + ] + } + } + }, + "Effect": "Allow", + "Principal": { + "Service": "sns.amazonaws.com" + }, + "Resource": { + "Fn::GetAtt": [ + "MyQueueE6CA6235", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "Queues": [ + { + "Ref": "MyQueueE6CA6235" + } + ] + } + }, + "MyQueueTopicStackMyTopicC43E67AFC8DC8B4A": { + "Type": "AWS::SNS::Subscription", + "Properties": { + "Protocol": "sqs", + "TopicArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":sns:us-east-1:12345678:topicstackopicstackmytopicc43e67afb24f28bb94f9" + ] + ] + }, + "Endpoint": { + "Fn::GetAtt": [ + "MyQueueE6CA6235", + "Arn" + ] + }, + "Region": "us-east-1" + } + } + } + } +] \ No newline at end of file diff --git a/packages/@aws-cdk/aws-sns-subscriptions/test/integ.sns-sqs-cross-region.lit.ts b/packages/@aws-cdk/aws-sns-subscriptions/test/integ.sns-sqs-cross-region.lit.ts new file mode 100644 index 0000000000000..ca53a70194e03 --- /dev/null +++ b/packages/@aws-cdk/aws-sns-subscriptions/test/integ.sns-sqs-cross-region.lit.ts @@ -0,0 +1,25 @@ +import * as sns from '@aws-cdk/aws-sns'; +import * as sqs from '@aws-cdk/aws-sqs'; +import * as cdk from '@aws-cdk/core'; +import * as subs from '../lib'; + +/// !cdk-integ * +const app = new cdk.App(); + +/// !show +const topicStack = new cdk.Stack(app, 'TopicStack', { + env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: 'us-east-1' }, +}); +const topic = new sns.Topic(topicStack, 'MyTopic', { + topicName: cdk.PhysicalName.GENERATE_IF_NEEDED, +}); + +const queueStack = new cdk.Stack(app, 'QueueStack', { + env: { region: 'us-east-2' }, +}); +const queue = new sqs.Queue(queueStack, 'MyQueue'); + +topic.addSubscription(new subs.SqsSubscription(queue)); +/// !hide + +app.synth(); diff --git a/packages/@aws-cdk/aws-sns-subscriptions/test/subs.test.ts b/packages/@aws-cdk/aws-sns-subscriptions/test/subs.test.ts index 8be564b5a9188..671937a3ed01e 100644 --- a/packages/@aws-cdk/aws-sns-subscriptions/test/subs.test.ts +++ b/packages/@aws-cdk/aws-sns-subscriptions/test/subs.test.ts @@ -3,7 +3,7 @@ import * as kms from '@aws-cdk/aws-kms'; import * as lambda from '@aws-cdk/aws-lambda'; import * as sns from '@aws-cdk/aws-sns'; import * as sqs from '@aws-cdk/aws-sqs'; -import { CfnParameter, Duration, RemovalPolicy, Stack, Token } from '@aws-cdk/core'; +import { App, CfnParameter, Duration, RemovalPolicy, Stack, Token } from '@aws-cdk/core'; import * as subs from '../lib'; /* eslint-disable quote-props */ @@ -308,6 +308,455 @@ test('queue subscription', () => { }); }); +test('queue subscription cross region', () => { + const app = new App(); + const topicStack = new Stack(app, 'TopicStack', { + env: { + account: '11111111111', + region: 'us-east-1', + }, + }); + const queueStack = new Stack(app, 'QueueStack', { + env: { + account: '11111111111', + region: 'us-east-2', + }, + }); + + const topic1 = new sns.Topic(topicStack, 'Topic', { + topicName: 'topicName', + displayName: 'displayName', + }); + + const queue = new sqs.Queue(queueStack, 'MyQueue'); + + topic1.addSubscription(new subs.SqsSubscription(queue)); + + expect(topicStack).toMatchTemplate({ + 'Resources': { + 'TopicBFC7AF6E': { + 'Type': 'AWS::SNS::Topic', + 'Properties': { + 'DisplayName': 'displayName', + 'TopicName': 'topicName', + }, + }, + }, + }); + + expect(queueStack).toMatchTemplate({ + 'Resources': { + 'MyQueueE6CA6235': { + 'Type': 'AWS::SQS::Queue', + 'UpdateReplacePolicy': 'Delete', + 'DeletionPolicy': 'Delete', + }, + 'MyQueuePolicy6BBEDDAC': { + 'Type': 'AWS::SQS::QueuePolicy', + 'Properties': { + 'PolicyDocument': { + 'Statement': [ + { + 'Action': 'sqs:SendMessage', + 'Condition': { + 'ArnEquals': { + 'aws:SourceArn': { + 'Fn::Join': [ + '', + [ + 'arn:', + { + 'Ref': 'AWS::Partition', + }, + ':sns:us-east-1:11111111111:topicName', + ], + ], + }, + }, + }, + 'Effect': 'Allow', + 'Principal': { + 'Service': 'sns.amazonaws.com', + }, + 'Resource': { + 'Fn::GetAtt': [ + 'MyQueueE6CA6235', + 'Arn', + ], + }, + }, + ], + 'Version': '2012-10-17', + }, + 'Queues': [ + { + 'Ref': 'MyQueueE6CA6235', + }, + ], + }, + }, + 'MyQueueTopicStackTopicFBF76EB349BDFA94': { + 'Type': 'AWS::SNS::Subscription', + 'Properties': { + 'Protocol': 'sqs', + 'TopicArn': { + 'Fn::Join': [ + '', + [ + 'arn:', + { + 'Ref': 'AWS::Partition', + }, + ':sns:us-east-1:11111111111:topicName', + ], + ], + }, + 'Endpoint': { + 'Fn::GetAtt': [ + 'MyQueueE6CA6235', + 'Arn', + ], + }, + 'Region': 'us-east-1', + }, + }, + }, + }); +}); + +test('queue subscription cross region, env agnostic', () => { + const app = new App(); + const topicStack = new Stack(app, 'TopicStack', {}); + const queueStack = new Stack(app, 'QueueStack', {}); + + const topic1 = new sns.Topic(topicStack, 'Topic', { + topicName: 'topicName', + displayName: 'displayName', + }); + + const queue = new sqs.Queue(queueStack, 'MyQueue'); + + topic1.addSubscription(new subs.SqsSubscription(queue)); + + expect(topicStack).toMatchTemplate({ + 'Resources': { + 'TopicBFC7AF6E': { + 'Type': 'AWS::SNS::Topic', + 'Properties': { + 'DisplayName': 'displayName', + 'TopicName': 'topicName', + }, + }, + }, + 'Outputs': { + 'ExportsOutputRefTopicBFC7AF6ECB4A357A': { + 'Value': { + 'Ref': 'TopicBFC7AF6E', + }, + 'Export': { + 'Name': 'TopicStack:ExportsOutputRefTopicBFC7AF6ECB4A357A', + }, + }, + }, + }); + + expect(queueStack).toMatchTemplate({ + 'Resources': { + 'MyQueueE6CA6235': { + 'Type': 'AWS::SQS::Queue', + 'UpdateReplacePolicy': 'Delete', + 'DeletionPolicy': 'Delete', + }, + 'MyQueuePolicy6BBEDDAC': { + 'Type': 'AWS::SQS::QueuePolicy', + 'Properties': { + 'PolicyDocument': { + 'Statement': [ + { + 'Action': 'sqs:SendMessage', + 'Condition': { + 'ArnEquals': { + 'aws:SourceArn': { + 'Fn::ImportValue': 'TopicStack:ExportsOutputRefTopicBFC7AF6ECB4A357A', + }, + }, + }, + 'Effect': 'Allow', + 'Principal': { + 'Service': 'sns.amazonaws.com', + }, + 'Resource': { + 'Fn::GetAtt': [ + 'MyQueueE6CA6235', + 'Arn', + ], + }, + }, + ], + 'Version': '2012-10-17', + }, + 'Queues': [ + { + 'Ref': 'MyQueueE6CA6235', + }, + ], + }, + }, + 'MyQueueTopicStackTopicFBF76EB349BDFA94': { + 'Type': 'AWS::SNS::Subscription', + 'Properties': { + 'Protocol': 'sqs', + 'TopicArn': { + 'Fn::ImportValue': 'TopicStack:ExportsOutputRefTopicBFC7AF6ECB4A357A', + }, + 'Endpoint': { + 'Fn::GetAtt': [ + 'MyQueueE6CA6235', + 'Arn', + ], + }, + }, + }, + }, + }); +}); + +test('queue subscription cross region, topic env agnostic', () => { + const app = new App(); + const topicStack = new Stack(app, 'TopicStack', {}); + const queueStack = new Stack(app, 'QueueStack', { + env: { + account: '11111111111', + region: 'us-east-1', + }, + }); + + const topic1 = new sns.Topic(topicStack, 'Topic', { + topicName: 'topicName', + displayName: 'displayName', + }); + + const queue = new sqs.Queue(queueStack, 'MyQueue'); + + topic1.addSubscription(new subs.SqsSubscription(queue)); + + expect(topicStack).toMatchTemplate({ + 'Resources': { + 'TopicBFC7AF6E': { + 'Type': 'AWS::SNS::Topic', + 'Properties': { + 'DisplayName': 'displayName', + 'TopicName': 'topicName', + }, + }, + }, + }); + + expect(queueStack).toMatchTemplate({ + 'Resources': { + 'MyQueueE6CA6235': { + 'Type': 'AWS::SQS::Queue', + 'UpdateReplacePolicy': 'Delete', + 'DeletionPolicy': 'Delete', + }, + 'MyQueuePolicy6BBEDDAC': { + 'Type': 'AWS::SQS::QueuePolicy', + 'Properties': { + 'PolicyDocument': { + 'Statement': [ + { + 'Action': 'sqs:SendMessage', + 'Condition': { + 'ArnEquals': { + 'aws:SourceArn': { + 'Fn::Join': [ + '', + [ + 'arn:', + { + 'Ref': 'AWS::Partition', + }, + ':sns:', + { + 'Ref': 'AWS::Region', + }, + ':', + { + 'Ref': 'AWS::AccountId', + }, + ':topicName', + ], + ], + }, + }, + }, + 'Effect': 'Allow', + 'Principal': { + 'Service': 'sns.amazonaws.com', + }, + 'Resource': { + 'Fn::GetAtt': [ + 'MyQueueE6CA6235', + 'Arn', + ], + }, + }, + ], + 'Version': '2012-10-17', + }, + 'Queues': [ + { + 'Ref': 'MyQueueE6CA6235', + }, + ], + }, + }, + 'MyQueueTopicStackTopicFBF76EB349BDFA94': { + 'Type': 'AWS::SNS::Subscription', + 'Properties': { + 'Protocol': 'sqs', + 'TopicArn': { + 'Fn::Join': [ + '', + [ + 'arn:', + { + 'Ref': 'AWS::Partition', + }, + ':sns:', + { + 'Ref': 'AWS::Region', + }, + ':', + { + 'Ref': 'AWS::AccountId', + }, + ':topicName', + ], + ], + }, + 'Endpoint': { + 'Fn::GetAtt': [ + 'MyQueueE6CA6235', + 'Arn', + ], + }, + }, + }, + }, + }); +}); + +test('queue subscription cross region, queue env agnostic', () => { + const app = new App(); + const topicStack = new Stack(app, 'TopicStack', { + env: { + account: '11111111111', + region: 'us-east-1', + }, + }); + const queueStack = new Stack(app, 'QueueStack', {}); + + const topic1 = new sns.Topic(topicStack, 'Topic', { + topicName: 'topicName', + displayName: 'displayName', + }); + + const queue = new sqs.Queue(queueStack, 'MyQueue'); + + topic1.addSubscription(new subs.SqsSubscription(queue)); + + expect(topicStack).toMatchTemplate({ + 'Resources': { + 'TopicBFC7AF6E': { + 'Type': 'AWS::SNS::Topic', + 'Properties': { + 'DisplayName': 'displayName', + 'TopicName': 'topicName', + }, + }, + }, + }); + + expect(queueStack).toMatchTemplate({ + 'Resources': { + 'MyQueueE6CA6235': { + 'Type': 'AWS::SQS::Queue', + 'UpdateReplacePolicy': 'Delete', + 'DeletionPolicy': 'Delete', + }, + 'MyQueuePolicy6BBEDDAC': { + 'Type': 'AWS::SQS::QueuePolicy', + 'Properties': { + 'PolicyDocument': { + 'Statement': [ + { + 'Action': 'sqs:SendMessage', + 'Condition': { + 'ArnEquals': { + 'aws:SourceArn': { + 'Fn::Join': [ + '', + [ + 'arn:', + { + 'Ref': 'AWS::Partition', + }, + ':sns:us-east-1:11111111111:topicName', + ], + ], + }, + }, + }, + 'Effect': 'Allow', + 'Principal': { + 'Service': 'sns.amazonaws.com', + }, + 'Resource': { + 'Fn::GetAtt': [ + 'MyQueueE6CA6235', + 'Arn', + ], + }, + }, + ], + 'Version': '2012-10-17', + }, + 'Queues': [ + { + 'Ref': 'MyQueueE6CA6235', + }, + ], + }, + }, + 'MyQueueTopicStackTopicFBF76EB349BDFA94': { + 'Type': 'AWS::SNS::Subscription', + 'Properties': { + 'Protocol': 'sqs', + 'TopicArn': { + 'Fn::Join': [ + '', + [ + 'arn:', + { + 'Ref': 'AWS::Partition', + }, + ':sns:us-east-1:11111111111:topicName', + ], + ], + }, + 'Endpoint': { + 'Fn::GetAtt': [ + 'MyQueueE6CA6235', + 'Arn', + ], + }, + 'Region': 'us-east-1', + }, + }, + }, + }); +}); test('queue subscription with user provided dlq', () => { const queue = new sqs.Queue(stack, 'MyQueue'); const dlQueue = new sqs.Queue(stack, 'DeadLetterQueue', { @@ -712,6 +1161,243 @@ test('lambda subscription', () => { }); }); +test('lambda subscription, cross region env agnostic', () => { + const app = new App(); + const topicStack = new Stack(app, 'TopicStack', {}); + const lambdaStack = new Stack(app, 'LambdaStack', {}); + + const topic1 = new sns.Topic(topicStack, 'Topic', { + topicName: 'topicName', + displayName: 'displayName', + }); + const fction = new lambda.Function(lambdaStack, 'MyFunc', { + runtime: lambda.Runtime.NODEJS_10_X, + handler: 'index.handler', + code: lambda.Code.fromInline('exports.handler = function(e, c, cb) { return cb() }'), + }); + + topic1.addSubscription(new subs.LambdaSubscription(fction)); + + expect(lambdaStack).toMatchTemplate({ + 'Resources': { + 'MyFuncServiceRole54065130': { + 'Type': 'AWS::IAM::Role', + 'Properties': { + 'AssumeRolePolicyDocument': { + 'Statement': [ + { + 'Action': 'sts:AssumeRole', + 'Effect': 'Allow', + 'Principal': { + 'Service': 'lambda.amazonaws.com', + }, + }, + ], + 'Version': '2012-10-17', + }, + 'ManagedPolicyArns': [ + { + 'Fn::Join': [ + '', + [ + 'arn:', + { + 'Ref': 'AWS::Partition', + }, + ':iam::aws:policy/service-role/AWSLambdaBasicExecutionRole', + ], + ], + }, + ], + }, + }, + 'MyFunc8A243A2C': { + 'Type': 'AWS::Lambda::Function', + 'Properties': { + 'Code': { + 'ZipFile': 'exports.handler = function(e, c, cb) { return cb() }', + }, + 'Role': { + 'Fn::GetAtt': [ + 'MyFuncServiceRole54065130', + 'Arn', + ], + }, + 'Handler': 'index.handler', + 'Runtime': 'nodejs10.x', + }, + 'DependsOn': [ + 'MyFuncServiceRole54065130', + ], + }, + 'MyFuncAllowInvokeTopicStackTopicFBF76EB3D4A699EF': { + 'Type': 'AWS::Lambda::Permission', + 'Properties': { + 'Action': 'lambda:InvokeFunction', + 'FunctionName': { + 'Fn::GetAtt': [ + 'MyFunc8A243A2C', + 'Arn', + ], + }, + 'Principal': 'sns.amazonaws.com', + 'SourceArn': { + 'Fn::ImportValue': 'TopicStack:ExportsOutputRefTopicBFC7AF6ECB4A357A', + }, + }, + }, + 'MyFuncTopic3B7C24C5': { + 'Type': 'AWS::SNS::Subscription', + 'Properties': { + 'Protocol': 'lambda', + 'TopicArn': { + 'Fn::ImportValue': 'TopicStack:ExportsOutputRefTopicBFC7AF6ECB4A357A', + }, + 'Endpoint': { + 'Fn::GetAtt': [ + 'MyFunc8A243A2C', + 'Arn', + ], + }, + }, + }, + }, + }); +}); + +test('lambda subscription, cross region', () => { + const app = new App(); + const topicStack = new Stack(app, 'TopicStack', { + env: { + account: '11111111111', + region: 'us-east-1', + }, + }); + const lambdaStack = new Stack(app, 'LambdaStack', { + env: { + account: '11111111111', + region: 'us-east-2', + }, + }); + + const topic1 = new sns.Topic(topicStack, 'Topic', { + topicName: 'topicName', + displayName: 'displayName', + }); + const fction = new lambda.Function(lambdaStack, 'MyFunc', { + runtime: lambda.Runtime.NODEJS_10_X, + handler: 'index.handler', + code: lambda.Code.fromInline('exports.handler = function(e, c, cb) { return cb() }'), + }); + + topic1.addSubscription(new subs.LambdaSubscription(fction)); + + expect(lambdaStack).toMatchTemplate({ + 'Resources': { + 'MyFuncServiceRole54065130': { + 'Type': 'AWS::IAM::Role', + 'Properties': { + 'AssumeRolePolicyDocument': { + 'Statement': [ + { + 'Action': 'sts:AssumeRole', + 'Effect': 'Allow', + 'Principal': { + 'Service': 'lambda.amazonaws.com', + }, + }, + ], + 'Version': '2012-10-17', + }, + 'ManagedPolicyArns': [ + { + 'Fn::Join': [ + '', + [ + 'arn:', + { + 'Ref': 'AWS::Partition', + }, + ':iam::aws:policy/service-role/AWSLambdaBasicExecutionRole', + ], + ], + }, + ], + }, + }, + 'MyFunc8A243A2C': { + 'Type': 'AWS::Lambda::Function', + 'Properties': { + 'Code': { + 'ZipFile': 'exports.handler = function(e, c, cb) { return cb() }', + }, + 'Role': { + 'Fn::GetAtt': [ + 'MyFuncServiceRole54065130', + 'Arn', + ], + }, + 'Handler': 'index.handler', + 'Runtime': 'nodejs10.x', + }, + 'DependsOn': [ + 'MyFuncServiceRole54065130', + ], + }, + 'MyFuncAllowInvokeTopicStackTopicFBF76EB3D4A699EF': { + 'Type': 'AWS::Lambda::Permission', + 'Properties': { + 'Action': 'lambda:InvokeFunction', + 'FunctionName': { + 'Fn::GetAtt': [ + 'MyFunc8A243A2C', + 'Arn', + ], + }, + 'Principal': 'sns.amazonaws.com', + 'SourceArn': { + 'Fn::Join': [ + '', + [ + 'arn:', + { + 'Ref': 'AWS::Partition', + }, + ':sns:us-east-1:11111111111:topicName', + ], + ], + }, + }, + }, + 'MyFuncTopic3B7C24C5': { + 'Type': 'AWS::SNS::Subscription', + 'Properties': { + 'Protocol': 'lambda', + 'TopicArn': { + 'Fn::Join': [ + '', + [ + 'arn:', + { + 'Ref': 'AWS::Partition', + }, + ':sns:us-east-1:11111111111:topicName', + ], + ], + }, + 'Endpoint': { + 'Fn::GetAtt': [ + 'MyFunc8A243A2C', + 'Arn', + ], + }, + 'Region': 'us-east-1', + }, + }, + }, + }); +}); + test('email subscription', () => { topic.addSubscription(new subs.EmailSubscription('foo@bar.com')); diff --git a/packages/@aws-cdk/aws-ssmcontacts/README.md b/packages/@aws-cdk/aws-ssmcontacts/README.md index 6418d35f765af..cab7c329a4bab 100644 --- a/packages/@aws-cdk/aws-ssmcontacts/README.md +++ b/packages/@aws-cdk/aws-ssmcontacts/README.md @@ -16,5 +16,5 @@ This module is part of the [AWS Cloud Development Kit](https://github.com/aws/aws-cdk) project. ```ts -import ssmcontacts = require('@aws-cdk/aws-ssmcontacts'); +import * as ssmcontacts from '@aws-cdk/aws-ssmcontacts'; ``` diff --git a/packages/@aws-cdk/aws-ssmcontacts/package.json b/packages/@aws-cdk/aws-ssmcontacts/package.json index 7a5181ba58adb..603c082e46845 100644 --- a/packages/@aws-cdk/aws-ssmcontacts/package.json +++ b/packages/@aws-cdk/aws-ssmcontacts/package.json @@ -7,6 +7,13 @@ "jsii": { "outdir": "dist", "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + }, "targets": { "dotnet": { "namespace": "Amazon.CDK.AWS.SSMContacts", diff --git a/packages/@aws-cdk/aws-ssmcontacts/rosetta/default.ts-fixture b/packages/@aws-cdk/aws-ssmcontacts/rosetta/default.ts-fixture new file mode 100644 index 0000000000000..e8deb6060d76d --- /dev/null +++ b/packages/@aws-cdk/aws-ssmcontacts/rosetta/default.ts-fixture @@ -0,0 +1,11 @@ +// Fixture with packages imported, but nothing else +import { Construct } from 'constructs'; +import { Stack } from '@aws-cdk/core'; + +class Fixture extends Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + + /// here + } +} diff --git a/packages/@aws-cdk/aws-ssmincidents/README.md b/packages/@aws-cdk/aws-ssmincidents/README.md index 169151903df0d..411c652fc7d8c 100644 --- a/packages/@aws-cdk/aws-ssmincidents/README.md +++ b/packages/@aws-cdk/aws-ssmincidents/README.md @@ -16,5 +16,5 @@ This module is part of the [AWS Cloud Development Kit](https://github.com/aws/aws-cdk) project. ```ts -import ssmincidents = require('@aws-cdk/aws-ssmincidents'); +import * as ssmincidents from '@aws-cdk/aws-ssmincidents'; ``` diff --git a/packages/@aws-cdk/aws-ssmincidents/package.json b/packages/@aws-cdk/aws-ssmincidents/package.json index 5cc2d1d7061c4..dfbe2be486b87 100644 --- a/packages/@aws-cdk/aws-ssmincidents/package.json +++ b/packages/@aws-cdk/aws-ssmincidents/package.json @@ -7,6 +7,13 @@ "jsii": { "outdir": "dist", "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + }, "targets": { "dotnet": { "namespace": "Amazon.CDK.AWS.SSMIncidents", diff --git a/packages/@aws-cdk/aws-ssmincidents/rosetta/default.ts-fixture b/packages/@aws-cdk/aws-ssmincidents/rosetta/default.ts-fixture new file mode 100644 index 0000000000000..e8deb6060d76d --- /dev/null +++ b/packages/@aws-cdk/aws-ssmincidents/rosetta/default.ts-fixture @@ -0,0 +1,11 @@ +// Fixture with packages imported, but nothing else +import { Construct } from 'constructs'; +import { Stack } from '@aws-cdk/core'; + +class Fixture extends Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + + /// here + } +} diff --git a/packages/@aws-cdk/aws-sso/README.md b/packages/@aws-cdk/aws-sso/README.md index 8da757ee4994c..d1f2b8988da89 100644 --- a/packages/@aws-cdk/aws-sso/README.md +++ b/packages/@aws-cdk/aws-sso/README.md @@ -16,5 +16,5 @@ This module is part of the [AWS Cloud Development Kit](https://github.com/aws/aws-cdk) project. ```ts -import sso = require('@aws-cdk/aws-sso'); +import * as sso from '@aws-cdk/aws-sso'; ``` diff --git a/packages/@aws-cdk/aws-sso/lib/rosetta/default.ts-fixture b/packages/@aws-cdk/aws-sso/lib/rosetta/default.ts-fixture new file mode 100644 index 0000000000000..e8deb6060d76d --- /dev/null +++ b/packages/@aws-cdk/aws-sso/lib/rosetta/default.ts-fixture @@ -0,0 +1,11 @@ +// Fixture with packages imported, but nothing else +import { Construct } from 'constructs'; +import { Stack } from '@aws-cdk/core'; + +class Fixture extends Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + + /// here + } +} diff --git a/packages/@aws-cdk/aws-sso/package.json b/packages/@aws-cdk/aws-sso/package.json index 3146e96ec3d6d..38e6621ed0fa0 100644 --- a/packages/@aws-cdk/aws-sso/package.json +++ b/packages/@aws-cdk/aws-sso/package.json @@ -7,6 +7,13 @@ "jsii": { "outdir": "dist", "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + }, "targets": { "dotnet": { "namespace": "Amazon.CDK.AWS.SSO", diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/README.md b/packages/@aws-cdk/aws-stepfunctions-tasks/README.md index f4721f21fda00..2f5d463067a35 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/README.md +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/README.md @@ -700,20 +700,6 @@ new tasks.EmrCreateCluster(this, 'Create Cluster', { }); ``` -If you want to use an [auto-termination policy](https://docs.aws.amazon.com/emr/latest/ManagementGuide/emr-auto-termination-policy.html), -you can specify the `autoTerminationPolicy` property. Set the `idleTimeout` as a `Duration` between 60 seconds and 7 days. -`autoTerminationPolicy` requires the EMR release label to be 5.30.0 or above. - -```ts -new tasks.EmrCreateCluster(this, 'Create Cluster', { - instances: {}, - name: 'ClusterName', - autoTerminationPolicy: { - idleTimeout: Duration.seconds(120), - }, -}); -``` - ### Termination Protection Locks a cluster (job flow) so the EC2 instances in the cluster cannot be diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/emr/emr-create-cluster.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/emr/emr-create-cluster.ts index eed78656efec0..4cac7c7180bde 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/emr/emr-create-cluster.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/emr/emr-create-cluster.ts @@ -5,7 +5,6 @@ import { Construct } from 'constructs'; import { integrationResourceArn, validatePatternSupported } from '../private/task-utils'; import { ApplicationConfigPropertyToJson, - AutoTerminationPolicyPropertyToJson, BootstrapActionConfigToJson, ConfigurationPropertyToJson, InstancesConfigPropertyToJson, @@ -68,15 +67,6 @@ export interface EmrCreateClusterProps extends sfn.TaskStateBaseProps { */ readonly autoScalingRole?: iam.IRole; - /** - * An auto-termination policy for an Amazon EMR cluster. An auto-termination policy defines the amount of - * idle time in seconds after which a cluster automatically terminates. The value must be between - * 60 seconds and 7 days. - * - * @default - None - */ - readonly autoTerminationPolicy?: EmrCreateCluster.AutoTerminationPolicyProperty; - /** * A list of bootstrap actions to run before Hadoop starts on the cluster nodes. * @@ -279,7 +269,6 @@ export class EmrCreateCluster extends sfn.TaskStateBase { AdditionalInfo: cdk.stringToCloudFormation(this.props.additionalInfo), Applications: cdk.listMapper(ApplicationConfigPropertyToJson)(this.props.applications), AutoScalingRole: cdk.stringToCloudFormation(this._autoScalingRole?.roleName), - AutoTerminationPolicy: this.props.autoTerminationPolicy ? AutoTerminationPolicyPropertyToJson(this.props.autoTerminationPolicy) : undefined, BootstrapActions: cdk.listMapper(BootstrapActionConfigToJson)(this.props.bootstrapActions), Configurations: cdk.listMapper(ConfigurationPropertyToJson)(this.props.configurations), CustomAmiId: cdk.stringToCloudFormation(this.props.customAmiId), @@ -1400,20 +1389,6 @@ export namespace EmrCreateCluster { readonly version?: string; } - /** - * Auto-termination policy for the EMR cluster. - * - * @see https://docs.aws.amazon.com/emr/latest/APIReference/API_AutoTerminationPolicy.html - * - */ - export interface AutoTerminationPolicyProperty { - - /** - * Specifies the amount of idle time after which the cluster automatically terminates. - */ - readonly idleTimeout: cdk.Duration; - } - /** * Configuration of the script to run during a bootstrap action. * diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/emr/private/cluster-utils.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/emr/private/cluster-utils.ts index fd05d71370584..c8ae8a50a360c 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/emr/private/cluster-utils.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/emr/private/cluster-utils.ts @@ -4,6 +4,8 @@ import { EmrModifyInstanceGroupByName } from '../emr-modify-instance-group-by-na /** * Render the KerberosAttributesProperty as JSON + * + * @param property */ export function KerberosAttributesPropertyToJson(property: EmrCreateCluster.KerberosAttributesProperty) { return { @@ -17,6 +19,8 @@ export function KerberosAttributesPropertyToJson(property: EmrCreateCluster.Kerb /** * Render the InstancesConfigProperty to JSON + * + * @param property */ export function InstancesConfigPropertyToJson(property: EmrCreateCluster.InstancesConfigProperty) { return { @@ -42,6 +46,8 @@ export function InstancesConfigPropertyToJson(property: EmrCreateCluster.Instanc /** * Render the ApplicationConfigProperty as JSON + * + * @param property */ export function ApplicationConfigPropertyToJson(property: EmrCreateCluster.ApplicationConfigProperty) { return { @@ -52,17 +58,10 @@ export function ApplicationConfigPropertyToJson(property: EmrCreateCluster.Appli }; } -/** - * Render the AutoTerminationPolicyProperty as JSON - */ -export function AutoTerminationPolicyPropertyToJson(property: EmrCreateCluster.AutoTerminationPolicyProperty) { - return { - IdleTimeout: cdk.numberToCloudFormation(property.idleTimeout.toSeconds()), - }; -} - /** * Render the ConfigurationProperty as JSON + * + * @param property */ export function ConfigurationPropertyToJson(property: EmrCreateCluster.ConfigurationProperty) { return { @@ -74,6 +73,8 @@ export function ConfigurationPropertyToJson(property: EmrCreateCluster.Configura /** * Render the EbsBlockDeviceConfigProperty as JSON + * + * @param property */ export function EbsBlockDeviceConfigPropertyToJson(property: EmrCreateCluster.EbsBlockDeviceConfigProperty) { return { @@ -88,6 +89,8 @@ export function EbsBlockDeviceConfigPropertyToJson(property: EmrCreateCluster.Eb /** * Render the EbsConfigurationProperty to JSON + * + * @param property */ export function EbsConfigurationPropertyToJson(property: EmrCreateCluster.EbsConfigurationProperty) { return { @@ -114,6 +117,8 @@ export function InstanceTypeConfigPropertyToJson(property: EmrCreateCluster.Inst /** * Render the InstanceFleetProvisioningSpecificationsProperty to JSON + * + * @param property */ export function InstanceFleetProvisioningSpecificationsPropertyToJson(property: EmrCreateCluster.InstanceFleetProvisioningSpecificationsProperty) { return { @@ -128,6 +133,8 @@ export function InstanceFleetProvisioningSpecificationsPropertyToJson(property: /** * Render the InstanceFleetConfigProperty as JSON + * + * @param property */ export function InstanceFleetConfigPropertyToJson(property: EmrCreateCluster.InstanceFleetConfigProperty) { return { @@ -145,6 +152,8 @@ export function InstanceFleetConfigPropertyToJson(property: EmrCreateCluster.Ins /** * Render the MetricDimensionProperty as JSON + * + * @param property */ export function MetricDimensionPropertyToJson(property: EmrCreateCluster.MetricDimensionProperty) { return { @@ -155,6 +164,8 @@ export function MetricDimensionPropertyToJson(property: EmrCreateCluster.MetricD /** * Render the ScalingTriggerProperty to JSON + * + * @param property */ export function ScalingTriggerPropertyToJson(property: EmrCreateCluster.ScalingTriggerProperty) { return { @@ -174,6 +185,8 @@ export function ScalingTriggerPropertyToJson(property: EmrCreateCluster.ScalingT /** * Render the ScalingActionProperty to JSON + * + * @param property */ export function ScalingActionPropertyToJson(property: EmrCreateCluster.ScalingActionProperty) { return { @@ -188,6 +201,8 @@ export function ScalingActionPropertyToJson(property: EmrCreateCluster.ScalingAc /** * Render the ScalingRuleProperty to JSON + * + * @param property */ export function ScalingRulePropertyToJson(property: EmrCreateCluster.ScalingRuleProperty) { return { @@ -200,6 +215,8 @@ export function ScalingRulePropertyToJson(property: EmrCreateCluster.ScalingRule /** * Render the AutoScalingPolicyProperty to JSON + * + * @param property */ export function AutoScalingPolicyPropertyToJson(property: EmrCreateCluster.AutoScalingPolicyProperty) { return { @@ -213,6 +230,8 @@ export function AutoScalingPolicyPropertyToJson(property: EmrCreateCluster.AutoS /** * Render the InstanceGroupConfigProperty to JSON + * + * @param property */ export function InstanceGroupConfigPropertyToJson(property: EmrCreateCluster.InstanceGroupConfigProperty) { return { @@ -231,6 +250,8 @@ export function InstanceGroupConfigPropertyToJson(property: EmrCreateCluster.Ins /** * Render the PlacementTypeProperty to JSON + * + * @param property */ export function PlacementTypePropertyToJson(property: EmrCreateCluster.PlacementTypeProperty) { return { @@ -241,6 +262,8 @@ export function PlacementTypePropertyToJson(property: EmrCreateCluster.Placement /** * Render the BootstrapActionProperty as JSON + * + * @param property */ export function BootstrapActionConfigToJson(property: EmrCreateCluster.BootstrapActionConfigProperty) { return { @@ -254,6 +277,8 @@ export function BootstrapActionConfigToJson(property: EmrCreateCluster.Bootstrap /** * Render the InstanceGroupModifyConfigProperty to JSON + * + * @param property */ export function InstanceGroupModifyConfigPropertyToJson(property: EmrModifyInstanceGroupByName.InstanceGroupModifyConfigProperty) { return { @@ -266,6 +291,8 @@ export function InstanceGroupModifyConfigPropertyToJson(property: EmrModifyInsta /** * Render the ShrinkPolicyProperty to JSON + * + * @param property */ function ShrinkPolicyPropertyToJson(property: EmrModifyInstanceGroupByName.ShrinkPolicyProperty) { return { @@ -276,6 +303,8 @@ function ShrinkPolicyPropertyToJson(property: EmrModifyInstanceGroupByName.Shrin /** * Render the InstanceResizePolicyProperty to JSON + * + * @param property */ function InstanceResizePolicyPropertyToJson(property: EmrModifyInstanceGroupByName.InstanceResizePolicyProperty) { return { diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/emr/emr-create-cluster.test.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/emr/emr-create-cluster.test.ts index 22e13475ceda2..3d846ec1d06d2 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/emr/emr-create-cluster.test.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/emr/emr-create-cluster.test.ts @@ -864,58 +864,6 @@ test('Create Cluster with InstanceFleet with allocation strategy=capacity-optimi }); }); -test('Create Cluster with AutoTerminationPolicy', () => { - // WHEN - const task = new EmrCreateCluster(stack, 'Task', { - instances: {}, - clusterRole, - name: 'Cluster', - serviceRole, - autoScalingRole, - autoTerminationPolicy: { - idleTimeout: cdk.Duration.seconds(120), - }, - integrationPattern: sfn.IntegrationPattern.REQUEST_RESPONSE, - }); - - // THEN - expect(stack.resolve(task.toStateJson())).toEqual({ - Type: 'Task', - Resource: { - 'Fn::Join': [ - '', - [ - 'arn:', - { - Ref: 'AWS::Partition', - }, - ':states:::elasticmapreduce:createCluster', - ], - ], - }, - End: true, - Parameters: { - Name: 'Cluster', - Instances: { - KeepJobFlowAliveWhenNoSteps: true, - }, - VisibleToAllUsers: true, - JobFlowRole: { - Ref: 'ClusterRoleD9CA7471', - }, - ServiceRole: { - Ref: 'ServiceRole4288B192', - }, - AutoScalingRole: { - Ref: 'AutoScalingRole015ADA0A', - }, - AutoTerminationPolicy: { - IdleTimeout: 120, - }, - }, - }); -}); - test('Create Cluster with InstanceFleet', () => { // WHEN const task = new EmrCreateCluster(stack, 'Task', { diff --git a/packages/@aws-cdk/aws-timestream/README.md b/packages/@aws-cdk/aws-timestream/README.md index 80120fe64f2ca..c5b4cb3de1e85 100644 --- a/packages/@aws-cdk/aws-timestream/README.md +++ b/packages/@aws-cdk/aws-timestream/README.md @@ -16,5 +16,5 @@ This module is part of the [AWS Cloud Development Kit](https://github.com/aws/aws-cdk) project. ```ts -import timestream = require('@aws-cdk/aws-timestream'); +import * as timestream from '@aws-cdk/aws-timestream'; ``` diff --git a/packages/@aws-cdk/aws-timestream/package.json b/packages/@aws-cdk/aws-timestream/package.json index 96208e6d94b0b..9aa39709df395 100644 --- a/packages/@aws-cdk/aws-timestream/package.json +++ b/packages/@aws-cdk/aws-timestream/package.json @@ -7,6 +7,13 @@ "jsii": { "outdir": "dist", "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + }, "targets": { "dotnet": { "namespace": "Amazon.CDK.AWS.Timestream", diff --git a/packages/@aws-cdk/aws-timestream/rosetta/default.ts-fixture b/packages/@aws-cdk/aws-timestream/rosetta/default.ts-fixture new file mode 100644 index 0000000000000..e8deb6060d76d --- /dev/null +++ b/packages/@aws-cdk/aws-timestream/rosetta/default.ts-fixture @@ -0,0 +1,11 @@ +// Fixture with packages imported, but nothing else +import { Construct } from 'constructs'; +import { Stack } from '@aws-cdk/core'; + +class Fixture extends Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + + /// here + } +} diff --git a/packages/@aws-cdk/aws-transfer/package.json b/packages/@aws-cdk/aws-transfer/package.json index db27c864db2b6..cc1d96d9fb22e 100644 --- a/packages/@aws-cdk/aws-transfer/package.json +++ b/packages/@aws-cdk/aws-transfer/package.json @@ -28,7 +28,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", diff --git a/packages/@aws-cdk/aws-transfer/rosetta/default.ts-fixture b/packages/@aws-cdk/aws-transfer/rosetta/default.ts-fixture new file mode 100644 index 0000000000000..e8deb6060d76d --- /dev/null +++ b/packages/@aws-cdk/aws-transfer/rosetta/default.ts-fixture @@ -0,0 +1,11 @@ +// Fixture with packages imported, but nothing else +import { Construct } from 'constructs'; +import { Stack } from '@aws-cdk/core'; + +class Fixture extends Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + + /// here + } +} diff --git a/packages/@aws-cdk/aws-wafv2/package.json b/packages/@aws-cdk/aws-wafv2/package.json index 6fe2bfe3c21f0..4c689f62396f0 100644 --- a/packages/@aws-cdk/aws-wafv2/package.json +++ b/packages/@aws-cdk/aws-wafv2/package.json @@ -28,7 +28,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", diff --git a/packages/@aws-cdk/aws-wafv2/rosetta/default.ts-fixture b/packages/@aws-cdk/aws-wafv2/rosetta/default.ts-fixture new file mode 100644 index 0000000000000..e8deb6060d76d --- /dev/null +++ b/packages/@aws-cdk/aws-wafv2/rosetta/default.ts-fixture @@ -0,0 +1,11 @@ +// Fixture with packages imported, but nothing else +import { Construct } from 'constructs'; +import { Stack } from '@aws-cdk/core'; + +class Fixture extends Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + + /// here + } +} diff --git a/packages/@aws-cdk/aws-wisdom/README.md b/packages/@aws-cdk/aws-wisdom/README.md index d942b3358d363..218dae4f51735 100644 --- a/packages/@aws-cdk/aws-wisdom/README.md +++ b/packages/@aws-cdk/aws-wisdom/README.md @@ -16,5 +16,5 @@ This module is part of the [AWS Cloud Development Kit](https://github.com/aws/aws-cdk) project. ```ts -import aws-wisdom = require('@aws-cdk/aws-wisdom'); +import * as wisdom from '@aws-cdk/aws-wisdom'; ``` diff --git a/packages/@aws-cdk/aws-wisdom/package.json b/packages/@aws-cdk/aws-wisdom/package.json index a0bf376211e4c..aa12d6e4aa992 100644 --- a/packages/@aws-cdk/aws-wisdom/package.json +++ b/packages/@aws-cdk/aws-wisdom/package.json @@ -7,6 +7,13 @@ "jsii": { "outdir": "dist", "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + }, "targets": { "dotnet": { "namespace": "Amazon.CDK.AWS.Wisdom", diff --git a/packages/@aws-cdk/aws-wisdom/rosetta/default.ts-fixture b/packages/@aws-cdk/aws-wisdom/rosetta/default.ts-fixture new file mode 100644 index 0000000000000..e8deb6060d76d --- /dev/null +++ b/packages/@aws-cdk/aws-wisdom/rosetta/default.ts-fixture @@ -0,0 +1,11 @@ +// Fixture with packages imported, but nothing else +import { Construct } from 'constructs'; +import { Stack } from '@aws-cdk/core'; + +class Fixture extends Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + + /// here + } +} diff --git a/packages/@aws-cdk/aws-xray/README.md b/packages/@aws-cdk/aws-xray/README.md index 77a0090e7f659..c543893c88228 100644 --- a/packages/@aws-cdk/aws-xray/README.md +++ b/packages/@aws-cdk/aws-xray/README.md @@ -16,5 +16,5 @@ This module is part of the [AWS Cloud Development Kit](https://github.com/aws/aws-cdk) project. ```ts -import xray = require('@aws-cdk/aws-xray'); +import * as xray from '@aws-cdk/aws-xray'; ``` diff --git a/packages/@aws-cdk/aws-xray/package.json b/packages/@aws-cdk/aws-xray/package.json index cdb613288b36b..7a5d9bfb5c5cd 100644 --- a/packages/@aws-cdk/aws-xray/package.json +++ b/packages/@aws-cdk/aws-xray/package.json @@ -7,6 +7,13 @@ "jsii": { "outdir": "dist", "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + }, "targets": { "dotnet": { "namespace": "Amazon.CDK.AWS.XRay", diff --git a/packages/@aws-cdk/aws-xray/rosetta/default.ts-fixture b/packages/@aws-cdk/aws-xray/rosetta/default.ts-fixture new file mode 100644 index 0000000000000..e8deb6060d76d --- /dev/null +++ b/packages/@aws-cdk/aws-xray/rosetta/default.ts-fixture @@ -0,0 +1,11 @@ +// Fixture with packages imported, but nothing else +import { Construct } from 'constructs'; +import { Stack } from '@aws-cdk/core'; + +class Fixture extends Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + + /// here + } +} diff --git a/packages/@aws-cdk/cfnspec/CHANGELOG.md b/packages/@aws-cdk/cfnspec/CHANGELOG.md index 913546215a38d..b42cbbb0134a3 100644 --- a/packages/@aws-cdk/cfnspec/CHANGELOG.md +++ b/packages/@aws-cdk/cfnspec/CHANGELOG.md @@ -1,3 +1,275 @@ +# CloudFormation Resource Specification v48.0.0 + +## New Resource Types + +* AWS::Batch::SchedulingPolicy +* AWS::IoTWireless::FuotaTask +* AWS::IoTWireless::MulticastGroup + +## Attribute Changes + +* AWS::EC2::NetworkInterface Id (__deleted__) +* AWS::EC2::NetworkInterface SecondaryPrivateIpAddresses.DuplicatesAllowed (__deleted__) +* AWS::EC2::NetworkInterface Documentation (__changed__) + * Old: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-networkinterface.html + * New: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-network-interface.html +* AWS::IoTAnalytics::Channel Id (__deleted__) +* AWS::IoTAnalytics::Dataset Id (__deleted__) +* AWS::IoTAnalytics::Datastore Id (__deleted__) +* AWS::IoTAnalytics::Pipeline Id (__deleted__) + +## Property Changes + +* AWS::ApiGateway::Stage Variables.DuplicatesAllowed (__deleted__) +* AWS::AppConfig::ConfigurationProfile Type (__added__) +* AWS::CloudFront::Function FunctionMetadata (__deleted__) +* AWS::CloudWatch::AnomalyDetector MetricName.Required (__changed__) + * Old: true + * New: false +* AWS::CloudWatch::AnomalyDetector Namespace.Required (__changed__) + * Old: true + * New: false +* AWS::CloudWatch::AnomalyDetector Stat.Required (__changed__) + * Old: true + * New: false +* AWS::EC2::CapacityReservation OutPostArn (__added__) +* AWS::EC2::CapacityReservation PlacementGroupArn (__added__) +* AWS::EC2::NetworkInterface Description.Documentation (__changed__) + * Old: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-networkinterface.html#cfn-ec2-networkinterface-description + * New: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-network-interface.html#cfn-awsec2networkinterface-description +* AWS::EC2::NetworkInterface GroupSet.Documentation (__changed__) + * Old: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-networkinterface.html#cfn-ec2-networkinterface-groupset + * New: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-network-interface.html#cfn-awsec2networkinterface-groupset +* AWS::EC2::NetworkInterface GroupSet.DuplicatesAllowed (__changed__) + * Old: true + * New: false +* AWS::EC2::NetworkInterface InterfaceType.Documentation (__changed__) + * Old: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-networkinterface.html#cfn-ec2-networkinterface-interfacetype + * New: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-network-interface.html#cfn-ec2-networkinterface-interfacetype +* AWS::EC2::NetworkInterface Ipv6AddressCount.Documentation (__changed__) + * Old: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-networkinterface.html#cfn-ec2-networkinterface-ipv6addresscount + * New: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-network-interface.html#cfn-ec2-networkinterface-ipv6addresscount +* AWS::EC2::NetworkInterface Ipv6Addresses.Documentation (__changed__) + * Old: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-networkinterface.html#cfn-ec2-networkinterface-ipv6addresses + * New: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-network-interface.html#cfn-ec2-networkinterface-ipv6addresses +* AWS::EC2::NetworkInterface PrivateIpAddress.Documentation (__changed__) + * Old: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-networkinterface.html#cfn-ec2-networkinterface-privateipaddress + * New: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-network-interface.html#cfn-awsec2networkinterface-privateipaddress +* AWS::EC2::NetworkInterface PrivateIpAddresses.Documentation (__changed__) + * Old: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-networkinterface.html#cfn-ec2-networkinterface-privateipaddresses + * New: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-network-interface.html#cfn-awsec2networkinterface-privateipaddresses +* AWS::EC2::NetworkInterface PrivateIpAddresses.DuplicatesAllowed (__changed__) + * Old: true + * New: false +* AWS::EC2::NetworkInterface PrivateIpAddresses.UpdateType (__changed__) + * Old: Mutable + * New: Conditional +* AWS::EC2::NetworkInterface SecondaryPrivateIpAddressCount.Documentation (__changed__) + * Old: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-networkinterface.html#cfn-ec2-networkinterface-secondaryprivateipaddresscount + * New: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-network-interface.html#cfn-awsec2networkinterface-secondaryprivateipcount +* AWS::EC2::NetworkInterface SourceDestCheck.Documentation (__changed__) + * Old: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-networkinterface.html#cfn-ec2-networkinterface-sourcedestcheck + * New: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-network-interface.html#cfn-awsec2networkinterface-sourcedestcheck +* AWS::EC2::NetworkInterface SubnetId.Documentation (__changed__) + * Old: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-networkinterface.html#cfn-ec2-networkinterface-subnetid + * New: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-network-interface.html#cfn-awsec2networkinterface-subnetid +* AWS::EC2::NetworkInterface Tags.Documentation (__changed__) + * Old: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-networkinterface.html#cfn-ec2-networkinterface-tags + * New: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-network-interface.html#cfn-awsec2networkinterface-tags +* AWS::FSx::FileSystem FileSystemTypeVersion (__added__) +* AWS::FSx::FileSystem OntapConfiguration (__added__) +* AWS::FinSpace::Environment DataBundles (__added__) +* AWS::FinSpace::Environment SuperuserParameters (__added__) +* AWS::IoTAnalytics::Channel ChannelName.Required (__changed__) + * Old: true + * New: false +* AWS::IoTAnalytics::Channel Tags.DuplicatesAllowed (__deleted__) +* AWS::IoTAnalytics::Dataset Actions.DuplicatesAllowed (__deleted__) +* AWS::IoTAnalytics::Dataset ContentDeliveryRules.DuplicatesAllowed (__deleted__) +* AWS::IoTAnalytics::Dataset DatasetName.Required (__changed__) + * Old: true + * New: false +* AWS::IoTAnalytics::Dataset LateDataRules.DuplicatesAllowed (__deleted__) +* AWS::IoTAnalytics::Dataset Tags.DuplicatesAllowed (__deleted__) +* AWS::IoTAnalytics::Dataset Triggers.DuplicatesAllowed (__deleted__) +* AWS::IoTAnalytics::Datastore Tags.DuplicatesAllowed (__deleted__) +* AWS::IoTAnalytics::Pipeline PipelineActivities.DuplicatesAllowed (__deleted__) +* AWS::IoTAnalytics::Pipeline PipelineName.Required (__changed__) + * Old: true + * New: false +* AWS::IoTAnalytics::Pipeline Tags.DuplicatesAllowed (__deleted__) +* AWS::Lightsail::Instance Location (__deleted__) +* AWS::Lightsail::Instance State (__deleted__) +* AWS::MemoryDB::Cluster ClusterEndpoint (__deleted__) +* AWS::Redshift::Cluster Endpoint (__deleted__) +* AWS::S3ObjectLambda::AccessPoint ObjectLambdaConfiguration.Required (__changed__) + * Old: false + * New: true + +## Property Type Changes + +* AWS::CloudWatch::AnomalyDetector.Metric (__added__) +* AWS::CloudWatch::AnomalyDetector.MetricDataQueries (__added__) +* AWS::CloudWatch::AnomalyDetector.MetricDataQuery (__added__) +* AWS::CloudWatch::AnomalyDetector.MetricStat (__added__) +* AWS::FSx::FileSystem.DiskIopsConfiguration (__added__) +* AWS::FSx::FileSystem.OntapConfiguration (__added__) +* AWS::FinSpace::Environment.SuperuserParameters (__added__) +* AWS::ApiGateway::Stage.CanarySetting StageVariableOverrides.DuplicatesAllowed (__deleted__) +* AWS::ApiGateway::Stage.MethodSetting CacheDataEncrypted.Documentation (__changed__) + * Old: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-apitgateway-stage-methodsetting.html#cfn-apigateway-stage-methodsetting-cachedataencrypted + * New: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-apigateway-stage-methodsetting.html#cfn-apigateway-stage-methodsetting-cachedataencrypted +* AWS::ApiGateway::Stage.MethodSetting CacheTtlInSeconds.Documentation (__changed__) + * Old: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-apitgateway-stage-methodsetting.html#cfn-apigateway-stage-methodsetting-cachettlinseconds + * New: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-apigateway-stage-methodsetting.html#cfn-apigateway-stage-methodsetting-cachettlinseconds +* AWS::ApiGateway::Stage.MethodSetting CachingEnabled.Documentation (__changed__) + * Old: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-apitgateway-stage-methodsetting.html#cfn-apigateway-stage-methodsetting-cachingenabled + * New: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-apigateway-stage-methodsetting.html#cfn-apigateway-stage-methodsetting-cachingenabled +* AWS::ApiGateway::Stage.MethodSetting DataTraceEnabled.Documentation (__changed__) + * Old: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-apitgateway-stage-methodsetting.html#cfn-apigateway-stage-methodsetting-datatraceenabled + * New: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-apigateway-stage-methodsetting.html#cfn-apigateway-stage-methodsetting-datatraceenabled +* AWS::ApiGateway::Stage.MethodSetting HttpMethod.Documentation (__changed__) + * Old: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-apitgateway-stage-methodsetting.html#cfn-apigateway-stage-methodsetting-httpmethod + * New: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-apigateway-stage-methodsetting.html#cfn-apigateway-stage-methodsetting-httpmethod +* AWS::ApiGateway::Stage.MethodSetting LoggingLevel.Documentation (__changed__) + * Old: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-apitgateway-stage-methodsetting.html#cfn-apigateway-stage-methodsetting-logginglevel + * New: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-apigateway-stage-methodsetting.html#cfn-apigateway-stage-methodsetting-logginglevel +* AWS::ApiGateway::Stage.MethodSetting MetricsEnabled.Documentation (__changed__) + * Old: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-apitgateway-stage-methodsetting.html#cfn-apigateway-stage-methodsetting-metricsenabled + * New: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-apigateway-stage-methodsetting.html#cfn-apigateway-stage-methodsetting-metricsenabled +* AWS::ApiGateway::Stage.MethodSetting ResourcePath.Documentation (__changed__) + * Old: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-apitgateway-stage-methodsetting.html#cfn-apigateway-stage-methodsetting-resourcepath + * New: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-apigateway-stage-methodsetting.html#cfn-apigateway-stage-methodsetting-resourcepath +* AWS::ApiGateway::Stage.MethodSetting ThrottlingBurstLimit.Documentation (__changed__) + * Old: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-apitgateway-stage-methodsetting.html#cfn-apigateway-stage-methodsetting-throttlingburstlimit + * New: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-apigateway-stage-methodsetting.html#cfn-apigateway-stage-methodsetting-throttlingburstlimit +* AWS::ApiGateway::Stage.MethodSetting ThrottlingRateLimit.Documentation (__changed__) + * Old: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-apitgateway-stage-methodsetting.html#cfn-apigateway-stage-methodsetting-throttlingratelimit + * New: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-apigateway-stage-methodsetting.html#cfn-apigateway-stage-methodsetting-throttlingratelimit +* AWS::AppMesh::GatewayRoute.GatewayRouteSpec Priority (__added__) +* AWS::CloudFront::Distribution.CacheBehavior ResponseHeadersPolicyId (__added__) +* AWS::CloudFront::Distribution.DefaultCacheBehavior ResponseHeadersPolicyId (__added__) +* AWS::EC2::NetworkInterface.PrivateIpAddressSpecification Primary.Documentation (__changed__) + * Old: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-networkinterface-privateipaddressspecification.html#cfn-ec2-networkinterface-privateipaddressspecification-primary + * New: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-network-interface-privateipspec.html#cfn-ec2-networkinterface-privateipspecification-primary +* AWS::EC2::NetworkInterface.PrivateIpAddressSpecification PrivateIpAddress.Documentation (__changed__) + * Old: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-networkinterface-privateipaddressspecification.html#cfn-ec2-networkinterface-privateipaddressspecification-privateipaddress + * New: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-network-interface-privateipspec.html#cfn-ec2-networkinterface-privateipspecification-privateipaddress +* AWS::IoTAnalytics::Dataset.ContainerAction Variables.DuplicatesAllowed (__deleted__) +* AWS::IoTAnalytics::Dataset.DatasetContentVersionValue DatasetName.Documentation (__changed__) + * Old: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-dataset-datasetcontentversionvalue.html#cfn-iotanalytics-dataset-datasetcontentversionvalue-datasetname + * New: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-dataset-variable-datasetcontentversionvalue.html#cfn-iotanalytics-dataset-variable-datasetcontentversionvalue-datasetname +* AWS::IoTAnalytics::Dataset.DatasetContentVersionValue DatasetName.Required (__changed__) + * Old: true + * New: false +* AWS::IoTAnalytics::Dataset.OutputFileUriValue FileName.Documentation (__changed__) + * Old: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-dataset-outputfileurivalue.html#cfn-iotanalytics-dataset-outputfileurivalue-filename + * New: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-dataset-variable-outputfileurivalue.html#cfn-iotanalytics-dataset-variable-outputfileurivalue-filename +* AWS::IoTAnalytics::Dataset.OutputFileUriValue FileName.Required (__changed__) + * Old: true + * New: false +* AWS::IoTAnalytics::Dataset.QueryAction Filters.DuplicatesAllowed (__deleted__) +* AWS::IoTAnalytics::Dataset.Schedule ScheduleExpression.Documentation (__changed__) + * Old: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-dataset-schedule.html#cfn-iotanalytics-dataset-schedule-scheduleexpression + * New: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-dataset-trigger-schedule.html#cfn-iotanalytics-dataset-trigger-schedule-scheduleexpression +* AWS::IoTAnalytics::Datastore.DatastorePartitions Partitions.DuplicatesAllowed (__deleted__) +* AWS::IoTAnalytics::Datastore.IotSiteWiseMultiLayerStorage CustomerManagedS3Storage.Required (__changed__) + * Old: false + * New: true +* AWS::IoTAnalytics::Datastore.SchemaDefinition Columns.DuplicatesAllowed (__deleted__) +* AWS::IoTAnalytics::Pipeline.AddAttributes Attributes.PrimitiveItemType (__deleted__) +* AWS::IoTAnalytics::Pipeline.AddAttributes Attributes.Type (__deleted__) +* AWS::IoTAnalytics::Pipeline.AddAttributes Attributes.PrimitiveType (__added__) +* AWS::IoTAnalytics::Pipeline.AddAttributes Attributes.Required (__changed__) + * Old: true + * New: false +* AWS::IoTAnalytics::Pipeline.AddAttributes Name.Required (__changed__) + * Old: true + * New: false +* AWS::IoTAnalytics::Pipeline.Channel ChannelName.Required (__changed__) + * Old: true + * New: false +* AWS::IoTAnalytics::Pipeline.Channel Name.Required (__changed__) + * Old: true + * New: false +* AWS::IoTAnalytics::Pipeline.Datastore DatastoreName.Required (__changed__) + * Old: true + * New: false +* AWS::IoTAnalytics::Pipeline.Datastore Name.Required (__changed__) + * Old: true + * New: false +* AWS::IoTAnalytics::Pipeline.DeviceRegistryEnrich Attribute.Required (__changed__) + * Old: true + * New: false +* AWS::IoTAnalytics::Pipeline.DeviceRegistryEnrich Name.Required (__changed__) + * Old: true + * New: false +* AWS::IoTAnalytics::Pipeline.DeviceRegistryEnrich RoleArn.Required (__changed__) + * Old: true + * New: false +* AWS::IoTAnalytics::Pipeline.DeviceRegistryEnrich ThingName.Required (__changed__) + * Old: true + * New: false +* AWS::IoTAnalytics::Pipeline.DeviceShadowEnrich Attribute.Required (__changed__) + * Old: true + * New: false +* AWS::IoTAnalytics::Pipeline.DeviceShadowEnrich Name.Required (__changed__) + * Old: true + * New: false +* AWS::IoTAnalytics::Pipeline.DeviceShadowEnrich RoleArn.Required (__changed__) + * Old: true + * New: false +* AWS::IoTAnalytics::Pipeline.DeviceShadowEnrich ThingName.Required (__changed__) + * Old: true + * New: false +* AWS::IoTAnalytics::Pipeline.Filter Filter.Required (__changed__) + * Old: true + * New: false +* AWS::IoTAnalytics::Pipeline.Filter Name.Required (__changed__) + * Old: true + * New: false +* AWS::IoTAnalytics::Pipeline.Lambda BatchSize.Required (__changed__) + * Old: true + * New: false +* AWS::IoTAnalytics::Pipeline.Lambda LambdaName.Required (__changed__) + * Old: true + * New: false +* AWS::IoTAnalytics::Pipeline.Lambda Name.Required (__changed__) + * Old: true + * New: false +* AWS::IoTAnalytics::Pipeline.Math Attribute.Required (__changed__) + * Old: true + * New: false +* AWS::IoTAnalytics::Pipeline.Math Math.Required (__changed__) + * Old: true + * New: false +* AWS::IoTAnalytics::Pipeline.Math Name.Required (__changed__) + * Old: true + * New: false +* AWS::IoTAnalytics::Pipeline.RemoveAttributes Attributes.DuplicatesAllowed (__deleted__) +* AWS::IoTAnalytics::Pipeline.RemoveAttributes Attributes.Required (__changed__) + * Old: true + * New: false +* AWS::IoTAnalytics::Pipeline.RemoveAttributes Name.Required (__changed__) + * Old: true + * New: false +* AWS::IoTAnalytics::Pipeline.SelectAttributes Attributes.DuplicatesAllowed (__deleted__) +* AWS::IoTAnalytics::Pipeline.SelectAttributes Attributes.Required (__changed__) + * Old: true + * New: false +* AWS::IoTAnalytics::Pipeline.SelectAttributes Name.Required (__changed__) + * Old: true + * New: false +* AWS::S3ObjectLambda::AccessPoint.TransformationConfiguration Actions.Required (__changed__) + * Old: false + * New: true +* AWS::S3ObjectLambda::AccessPoint.TransformationConfiguration ContentTransformation.Required (__changed__) + * Old: false + * New: true +* AWS::SecretsManager::RotationSchedule.HostedRotationLambda SuperuserSecretArn (__added__) +* AWS::SecretsManager::RotationSchedule.HostedRotationLambda SuperuserSecretKmsKeyArn (__added__) + + # CloudFormation Resource Specification v47.0.0 ## New Resource Types diff --git a/packages/@aws-cdk/cfnspec/cfn.version b/packages/@aws-cdk/cfnspec/cfn.version index 645a41f3b46f7..adb9d57dcf11d 100644 --- a/packages/@aws-cdk/cfnspec/cfn.version +++ b/packages/@aws-cdk/cfnspec/cfn.version @@ -1 +1 @@ -47.0.0 +48.0.0 diff --git a/packages/@aws-cdk/cfnspec/spec-source/000_CloudFormationResourceSpecification.json b/packages/@aws-cdk/cfnspec/spec-source/000_CloudFormationResourceSpecification.json index 5d8a7aab62e63..592ff06561e13 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/000_CloudFormationResourceSpecification.json +++ b/packages/@aws-cdk/cfnspec/spec-source/000_CloudFormationResourceSpecification.json @@ -1783,7 +1783,6 @@ }, "StageVariableOverrides": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-apigateway-stage-canarysetting.html#cfn-apigateway-stage-canarysetting-stagevariableoverrides", - "DuplicatesAllowed": false, "PrimitiveItemType": "String", "Required": false, "Type": "Map", @@ -1798,64 +1797,64 @@ } }, "AWS::ApiGateway::Stage.MethodSetting": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-apitgateway-stage-methodsetting.html", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-apigateway-stage-methodsetting.html", "Properties": { "CacheDataEncrypted": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-apitgateway-stage-methodsetting.html#cfn-apigateway-stage-methodsetting-cachedataencrypted", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-apigateway-stage-methodsetting.html#cfn-apigateway-stage-methodsetting-cachedataencrypted", "PrimitiveType": "Boolean", "Required": false, "UpdateType": "Mutable" }, "CacheTtlInSeconds": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-apitgateway-stage-methodsetting.html#cfn-apigateway-stage-methodsetting-cachettlinseconds", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-apigateway-stage-methodsetting.html#cfn-apigateway-stage-methodsetting-cachettlinseconds", "PrimitiveType": "Integer", "Required": false, "UpdateType": "Mutable" }, "CachingEnabled": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-apitgateway-stage-methodsetting.html#cfn-apigateway-stage-methodsetting-cachingenabled", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-apigateway-stage-methodsetting.html#cfn-apigateway-stage-methodsetting-cachingenabled", "PrimitiveType": "Boolean", "Required": false, "UpdateType": "Mutable" }, "DataTraceEnabled": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-apitgateway-stage-methodsetting.html#cfn-apigateway-stage-methodsetting-datatraceenabled", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-apigateway-stage-methodsetting.html#cfn-apigateway-stage-methodsetting-datatraceenabled", "PrimitiveType": "Boolean", "Required": false, "UpdateType": "Mutable" }, "HttpMethod": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-apitgateway-stage-methodsetting.html#cfn-apigateway-stage-methodsetting-httpmethod", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-apigateway-stage-methodsetting.html#cfn-apigateway-stage-methodsetting-httpmethod", "PrimitiveType": "String", "Required": false, "UpdateType": "Mutable" }, "LoggingLevel": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-apitgateway-stage-methodsetting.html#cfn-apigateway-stage-methodsetting-logginglevel", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-apigateway-stage-methodsetting.html#cfn-apigateway-stage-methodsetting-logginglevel", "PrimitiveType": "String", "Required": false, "UpdateType": "Mutable" }, "MetricsEnabled": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-apitgateway-stage-methodsetting.html#cfn-apigateway-stage-methodsetting-metricsenabled", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-apigateway-stage-methodsetting.html#cfn-apigateway-stage-methodsetting-metricsenabled", "PrimitiveType": "Boolean", "Required": false, "UpdateType": "Mutable" }, "ResourcePath": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-apitgateway-stage-methodsetting.html#cfn-apigateway-stage-methodsetting-resourcepath", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-apigateway-stage-methodsetting.html#cfn-apigateway-stage-methodsetting-resourcepath", "PrimitiveType": "String", "Required": false, "UpdateType": "Mutable" }, "ThrottlingBurstLimit": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-apitgateway-stage-methodsetting.html#cfn-apigateway-stage-methodsetting-throttlingburstlimit", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-apigateway-stage-methodsetting.html#cfn-apigateway-stage-methodsetting-throttlingburstlimit", "PrimitiveType": "Integer", "Required": false, "UpdateType": "Mutable" }, "ThrottlingRateLimit": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-apitgateway-stage-methodsetting.html#cfn-apigateway-stage-methodsetting-throttlingratelimit", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-apigateway-stage-methodsetting.html#cfn-apigateway-stage-methodsetting-throttlingratelimit", "PrimitiveType": "Double", "Required": false, "UpdateType": "Mutable" @@ -4353,6 +4352,12 @@ "Required": false, "Type": "HttpGatewayRoute", "UpdateType": "Mutable" + }, + "Priority": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-appmesh-gatewayroute-gatewayroutespec.html#cfn-appmesh-gatewayroute-gatewayroutespec-priority", + "PrimitiveType": "Integer", + "Required": false, + "UpdateType": "Mutable" } } }, @@ -10616,6 +10621,47 @@ } } }, + "AWS::Batch::SchedulingPolicy.FairsharePolicy": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-batch-schedulingpolicy-fairsharepolicy.html", + "Properties": { + "ComputeReservation": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-batch-schedulingpolicy-fairsharepolicy.html#cfn-batch-schedulingpolicy-fairsharepolicy-computereservation", + "PrimitiveType": "Double", + "Required": false, + "UpdateType": "Mutable" + }, + "ShareDecaySeconds": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-batch-schedulingpolicy-fairsharepolicy.html#cfn-batch-schedulingpolicy-fairsharepolicy-sharedecayseconds", + "PrimitiveType": "Double", + "Required": false, + "UpdateType": "Mutable" + }, + "ShareDistribution": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-batch-schedulingpolicy-fairsharepolicy.html#cfn-batch-schedulingpolicy-fairsharepolicy-sharedistribution", + "ItemType": "ShareAttributes", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + } + } + }, + "AWS::Batch::SchedulingPolicy.ShareAttributes": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-batch-schedulingpolicy-shareattributes.html", + "Properties": { + "ShareIdentifier": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-batch-schedulingpolicy-shareattributes.html#cfn-batch-schedulingpolicy-shareattributes-shareidentifier", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "WeightFactor": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-batch-schedulingpolicy-shareattributes.html#cfn-batch-schedulingpolicy-shareattributes-weightfactor", + "PrimitiveType": "Double", + "Required": false, + "UpdateType": "Mutable" + } + } + }, "AWS::Budgets::Budget.BudgetData": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-budgets-budget-budgetdata.html", "Properties": { @@ -11525,6 +11571,12 @@ "Required": false, "UpdateType": "Mutable" }, + "ResponseHeadersPolicyId": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cloudfront-distribution-cachebehavior.html#cfn-cloudfront-distribution-cachebehavior-responseheaderspolicyid", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, "SmoothStreaming": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cloudfront-distribution-cachebehavior.html#cfn-cloudfront-distribution-cachebehavior-smoothstreaming", "PrimitiveType": "Boolean", @@ -11741,6 +11793,12 @@ "Required": false, "UpdateType": "Mutable" }, + "ResponseHeadersPolicyId": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cloudfront-distribution-defaultcachebehavior.html#cfn-cloudfront-distribution-defaultcachebehavior-responseheaderspolicyid", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, "SmoothStreaming": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cloudfront-distribution-defaultcachebehavior.html#cfn-cloudfront-distribution-defaultcachebehavior-smoothstreaming", "PrimitiveType": "Boolean", @@ -13186,6 +13244,113 @@ } } }, + "AWS::CloudWatch::AnomalyDetector.Metric": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cloudwatch-anomalydetector-metric.html", + "Properties": { + "Dimensions": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cloudwatch-anomalydetector-metric.html#cfn-cloudwatch-anomalydetector-metric-dimensions", + "ItemType": "Dimension", + "Required": false, + "Type": "List", + "UpdateType": "Immutable" + }, + "MetricName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cloudwatch-anomalydetector-metric.html#cfn-cloudwatch-anomalydetector-metric-metricname", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + }, + "Namespace": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cloudwatch-anomalydetector-metric.html#cfn-cloudwatch-anomalydetector-metric-namespace", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + } + } + }, + "AWS::CloudWatch::AnomalyDetector.MetricDataQueries": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cloudwatch-anomalydetector-metricdataqueries.html", + "ItemType": "MetricDataQuery", + "Required": false, + "Type": "List", + "UpdateType": "Immutable" + }, + "AWS::CloudWatch::AnomalyDetector.MetricDataQuery": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cloudwatch-anomalydetector-metricdataquery.html", + "Properties": { + "AccountId": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cloudwatch-anomalydetector-metricdataquery.html#cfn-cloudwatch-anomalydetector-metricdataquery-accountid", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, + "Expression": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cloudwatch-anomalydetector-metricdataquery.html#cfn-cloudwatch-anomalydetector-metricdataquery-expression", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, + "Id": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cloudwatch-anomalydetector-metricdataquery.html#cfn-cloudwatch-anomalydetector-metricdataquery-id", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + }, + "Label": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cloudwatch-anomalydetector-metricdataquery.html#cfn-cloudwatch-anomalydetector-metricdataquery-label", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "MetricStat": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cloudwatch-anomalydetector-metricdataquery.html#cfn-cloudwatch-anomalydetector-metricdataquery-metricstat", + "Required": false, + "Type": "MetricStat", + "UpdateType": "Immutable" + }, + "Period": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cloudwatch-anomalydetector-metricdataquery.html#cfn-cloudwatch-anomalydetector-metricdataquery-period", + "PrimitiveType": "Integer", + "Required": false, + "UpdateType": "Immutable" + }, + "ReturnData": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cloudwatch-anomalydetector-metricdataquery.html#cfn-cloudwatch-anomalydetector-metricdataquery-returndata", + "PrimitiveType": "Boolean", + "Required": false, + "UpdateType": "Mutable" + } + } + }, + "AWS::CloudWatch::AnomalyDetector.MetricStat": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cloudwatch-anomalydetector-metricstat.html", + "Properties": { + "Metric": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cloudwatch-anomalydetector-metricstat.html#cfn-cloudwatch-anomalydetector-metricstat-metric", + "Required": true, + "Type": "Metric", + "UpdateType": "Immutable" + }, + "Period": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cloudwatch-anomalydetector-metricstat.html#cfn-cloudwatch-anomalydetector-metricstat-period", + "PrimitiveType": "Integer", + "Required": true, + "UpdateType": "Immutable" + }, + "Stat": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cloudwatch-anomalydetector-metricstat.html#cfn-cloudwatch-anomalydetector-metricstat-stat", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + }, + "Unit": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cloudwatch-anomalydetector-metricstat.html#cfn-cloudwatch-anomalydetector-metricstat-unit", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + } + } + }, "AWS::CloudWatch::AnomalyDetector.Range": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cloudwatch-anomalydetector-range.html", "Properties": { @@ -22782,16 +22947,16 @@ } }, "AWS::EC2::NetworkInterface.PrivateIpAddressSpecification": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-networkinterface-privateipaddressspecification.html", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-network-interface-privateipspec.html", "Properties": { "Primary": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-networkinterface-privateipaddressspecification.html#cfn-ec2-networkinterface-privateipaddressspecification-primary", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-network-interface-privateipspec.html#cfn-ec2-networkinterface-privateipspecification-primary", "PrimitiveType": "Boolean", "Required": true, "UpdateType": "Mutable" }, "PrivateIpAddress": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-networkinterface-privateipaddressspecification.html#cfn-ec2-networkinterface-privateipaddressspecification-privateipaddress", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-network-interface-privateipspec.html#cfn-ec2-networkinterface-privateipspecification-privateipaddress", "PrimitiveType": "String", "Required": true, "UpdateType": "Mutable" @@ -29822,6 +29987,23 @@ } } }, + "AWS::FSx::FileSystem.DiskIopsConfiguration": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-fsx-filesystem-ontapconfiguration-diskiopsconfiguration.html", + "Properties": { + "Iops": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-fsx-filesystem-ontapconfiguration-diskiopsconfiguration.html#cfn-fsx-filesystem-ontapconfiguration-diskiopsconfiguration-iops", + "PrimitiveType": "Integer", + "Required": false, + "UpdateType": "Immutable" + }, + "Mode": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-fsx-filesystem-ontapconfiguration-diskiopsconfiguration.html#cfn-fsx-filesystem-ontapconfiguration-diskiopsconfiguration-mode", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + } + } + }, "AWS::FSx::FileSystem.LustreConfiguration": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-fsx-filesystem-lustreconfiguration.html", "Properties": { @@ -29899,6 +30081,72 @@ } } }, + "AWS::FSx::FileSystem.OntapConfiguration": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-fsx-filesystem-ontapconfiguration.html", + "Properties": { + "AutomaticBackupRetentionDays": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-fsx-filesystem-ontapconfiguration.html#cfn-fsx-filesystem-ontapconfiguration-automaticbackupretentiondays", + "PrimitiveType": "Integer", + "Required": false, + "UpdateType": "Mutable" + }, + "DailyAutomaticBackupStartTime": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-fsx-filesystem-ontapconfiguration.html#cfn-fsx-filesystem-ontapconfiguration-dailyautomaticbackupstarttime", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "DeploymentType": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-fsx-filesystem-ontapconfiguration.html#cfn-fsx-filesystem-ontapconfiguration-deploymenttype", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + }, + "DiskIopsConfiguration": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-fsx-filesystem-ontapconfiguration.html#cfn-fsx-filesystem-ontapconfiguration-diskiopsconfiguration", + "Required": false, + "Type": "DiskIopsConfiguration", + "UpdateType": "Mutable" + }, + "EndpointIpAddressRange": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-fsx-filesystem-ontapconfiguration.html#cfn-fsx-filesystem-ontapconfiguration-endpointipaddressrange", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, + "FsxAdminPassword": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-fsx-filesystem-ontapconfiguration.html#cfn-fsx-filesystem-ontapconfiguration-fsxadminpassword", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "PreferredSubnetId": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-fsx-filesystem-ontapconfiguration.html#cfn-fsx-filesystem-ontapconfiguration-preferredsubnetid", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, + "RouteTableIds": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-fsx-filesystem-ontapconfiguration.html#cfn-fsx-filesystem-ontapconfiguration-routetableids", + "PrimitiveItemType": "String", + "Required": false, + "Type": "List", + "UpdateType": "Immutable" + }, + "ThroughputCapacity": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-fsx-filesystem-ontapconfiguration.html#cfn-fsx-filesystem-ontapconfiguration-throughputcapacity", + "PrimitiveType": "Integer", + "Required": false, + "UpdateType": "Immutable" + }, + "WeeklyMaintenanceStartTime": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-fsx-filesystem-ontapconfiguration.html#cfn-fsx-filesystem-ontapconfiguration-weeklymaintenancestarttime", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + } + } + }, "AWS::FSx::FileSystem.SelfManagedActiveDirectoryConfiguration": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-fsx-filesystem-windowsconfiguration-selfmanagedactivedirectoryconfiguration.html", "Properties": { @@ -30054,6 +30302,29 @@ } } }, + "AWS::FinSpace::Environment.SuperuserParameters": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-finspace-environment-superuserparameters.html", + "Properties": { + "EmailAddress": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-finspace-environment-superuserparameters.html#cfn-finspace-environment-superuserparameters-emailaddress", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, + "FirstName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-finspace-environment-superuserparameters.html#cfn-finspace-environment-superuserparameters-firstname", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, + "LastName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-finspace-environment-superuserparameters.html#cfn-finspace-environment-superuserparameters-lastname", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + } + } + }, "AWS::FraudDetector::Detector.EntityType": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-frauddetector-detector-entitytype.html", "Properties": { @@ -36567,7 +36838,8 @@ } }, "AWS::IoTAnalytics::Channel.ServiceManagedS3": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-channel-servicemanageds3.html" + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-channel-servicemanageds3.html", + "Properties": {} }, "AWS::IoTAnalytics::Dataset.Action": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-dataset-action.html", @@ -36615,7 +36887,6 @@ }, "Variables": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-dataset-containeraction.html#cfn-iotanalytics-dataset-containeraction-variables", - "DuplicatesAllowed": true, "ItemType": "Variable", "Required": false, "Type": "List", @@ -36658,12 +36929,12 @@ } }, "AWS::IoTAnalytics::Dataset.DatasetContentVersionValue": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-dataset-datasetcontentversionvalue.html", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-dataset-variable-datasetcontentversionvalue.html", "Properties": { "DatasetName": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-dataset-datasetcontentversionvalue.html#cfn-iotanalytics-dataset-datasetcontentversionvalue-datasetname", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-dataset-variable-datasetcontentversionvalue.html#cfn-iotanalytics-dataset-variable-datasetcontentversionvalue-datasetname", "PrimitiveType": "String", - "Required": true, + "Required": false, "UpdateType": "Mutable" } } @@ -36770,12 +37041,12 @@ } }, "AWS::IoTAnalytics::Dataset.OutputFileUriValue": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-dataset-outputfileurivalue.html", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-dataset-variable-outputfileurivalue.html", "Properties": { "FileName": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-dataset-outputfileurivalue.html#cfn-iotanalytics-dataset-outputfileurivalue-filename", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-dataset-variable-outputfileurivalue.html#cfn-iotanalytics-dataset-variable-outputfileurivalue-filename", "PrimitiveType": "String", - "Required": true, + "Required": false, "UpdateType": "Mutable" } } @@ -36785,7 +37056,6 @@ "Properties": { "Filters": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-dataset-queryaction.html#cfn-iotanalytics-dataset-queryaction-filters", - "DuplicatesAllowed": true, "ItemType": "Filter", "Required": false, "Type": "List", @@ -36863,10 +37133,10 @@ } }, "AWS::IoTAnalytics::Dataset.Schedule": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-dataset-schedule.html", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-dataset-trigger-schedule.html", "Properties": { "ScheduleExpression": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-dataset-schedule.html#cfn-iotanalytics-dataset-schedule-scheduleexpression", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-dataset-trigger-schedule.html#cfn-iotanalytics-dataset-trigger-schedule-scheduleexpression", "PrimitiveType": "String", "Required": true, "UpdateType": "Mutable" @@ -37032,7 +37302,6 @@ "Properties": { "Partitions": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-datastore-datastorepartitions.html#cfn-iotanalytics-datastore-datastorepartitions-partitions", - "DuplicatesAllowed": true, "ItemType": "DatastorePartition", "Required": false, "Type": "List", @@ -37085,14 +37354,15 @@ "Properties": { "CustomerManagedS3Storage": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-datastore-iotsitewisemultilayerstorage.html#cfn-iotanalytics-datastore-iotsitewisemultilayerstorage-customermanageds3storage", - "Required": false, + "Required": true, "Type": "CustomerManagedS3Storage", "UpdateType": "Mutable" } } }, "AWS::IoTAnalytics::Datastore.JsonConfiguration": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-datastore-jsonconfiguration.html" + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-datastore-jsonconfiguration.html", + "Properties": {} }, "AWS::IoTAnalytics::Datastore.ParquetConfiguration": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-datastore-parquetconfiguration.html", @@ -37138,7 +37408,6 @@ "Properties": { "Columns": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-datastore-schemadefinition.html#cfn-iotanalytics-datastore-schemadefinition-columns", - "DuplicatesAllowed": true, "ItemType": "Column", "Required": false, "Type": "List", @@ -37147,7 +37416,8 @@ } }, "AWS::IoTAnalytics::Datastore.ServiceManagedS3": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-datastore-servicemanageds3.html" + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-datastore-servicemanageds3.html", + "Properties": {} }, "AWS::IoTAnalytics::Datastore.TimestampPartition": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-datastore-timestamppartition.html", @@ -37236,15 +37506,14 @@ "Properties": { "Attributes": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-pipeline-addattributes.html#cfn-iotanalytics-pipeline-addattributes-attributes", - "PrimitiveItemType": "String", - "Required": true, - "Type": "Map", + "PrimitiveType": "Json", + "Required": false, "UpdateType": "Mutable" }, "Name": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-pipeline-addattributes.html#cfn-iotanalytics-pipeline-addattributes-name", "PrimitiveType": "String", - "Required": true, + "Required": false, "UpdateType": "Mutable" }, "Next": { @@ -37261,13 +37530,13 @@ "ChannelName": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-pipeline-channel.html#cfn-iotanalytics-pipeline-channel-channelname", "PrimitiveType": "String", - "Required": true, + "Required": false, "UpdateType": "Mutable" }, "Name": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-pipeline-channel.html#cfn-iotanalytics-pipeline-channel-name", "PrimitiveType": "String", - "Required": true, + "Required": false, "UpdateType": "Mutable" }, "Next": { @@ -37284,13 +37553,13 @@ "DatastoreName": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-pipeline-datastore.html#cfn-iotanalytics-pipeline-datastore-datastorename", "PrimitiveType": "String", - "Required": true, + "Required": false, "UpdateType": "Mutable" }, "Name": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-pipeline-datastore.html#cfn-iotanalytics-pipeline-datastore-name", "PrimitiveType": "String", - "Required": true, + "Required": false, "UpdateType": "Mutable" } } @@ -37301,13 +37570,13 @@ "Attribute": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-pipeline-deviceregistryenrich.html#cfn-iotanalytics-pipeline-deviceregistryenrich-attribute", "PrimitiveType": "String", - "Required": true, + "Required": false, "UpdateType": "Mutable" }, "Name": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-pipeline-deviceregistryenrich.html#cfn-iotanalytics-pipeline-deviceregistryenrich-name", "PrimitiveType": "String", - "Required": true, + "Required": false, "UpdateType": "Mutable" }, "Next": { @@ -37319,13 +37588,13 @@ "RoleArn": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-pipeline-deviceregistryenrich.html#cfn-iotanalytics-pipeline-deviceregistryenrich-rolearn", "PrimitiveType": "String", - "Required": true, + "Required": false, "UpdateType": "Mutable" }, "ThingName": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-pipeline-deviceregistryenrich.html#cfn-iotanalytics-pipeline-deviceregistryenrich-thingname", "PrimitiveType": "String", - "Required": true, + "Required": false, "UpdateType": "Mutable" } } @@ -37336,13 +37605,13 @@ "Attribute": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-pipeline-deviceshadowenrich.html#cfn-iotanalytics-pipeline-deviceshadowenrich-attribute", "PrimitiveType": "String", - "Required": true, + "Required": false, "UpdateType": "Mutable" }, "Name": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-pipeline-deviceshadowenrich.html#cfn-iotanalytics-pipeline-deviceshadowenrich-name", "PrimitiveType": "String", - "Required": true, + "Required": false, "UpdateType": "Mutable" }, "Next": { @@ -37354,13 +37623,13 @@ "RoleArn": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-pipeline-deviceshadowenrich.html#cfn-iotanalytics-pipeline-deviceshadowenrich-rolearn", "PrimitiveType": "String", - "Required": true, + "Required": false, "UpdateType": "Mutable" }, "ThingName": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-pipeline-deviceshadowenrich.html#cfn-iotanalytics-pipeline-deviceshadowenrich-thingname", "PrimitiveType": "String", - "Required": true, + "Required": false, "UpdateType": "Mutable" } } @@ -37371,13 +37640,13 @@ "Filter": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-pipeline-filter.html#cfn-iotanalytics-pipeline-filter-filter", "PrimitiveType": "String", - "Required": true, + "Required": false, "UpdateType": "Mutable" }, "Name": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-pipeline-filter.html#cfn-iotanalytics-pipeline-filter-name", "PrimitiveType": "String", - "Required": true, + "Required": false, "UpdateType": "Mutable" }, "Next": { @@ -37394,19 +37663,19 @@ "BatchSize": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-pipeline-lambda.html#cfn-iotanalytics-pipeline-lambda-batchsize", "PrimitiveType": "Integer", - "Required": true, + "Required": false, "UpdateType": "Mutable" }, "LambdaName": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-pipeline-lambda.html#cfn-iotanalytics-pipeline-lambda-lambdaname", "PrimitiveType": "String", - "Required": true, + "Required": false, "UpdateType": "Mutable" }, "Name": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-pipeline-lambda.html#cfn-iotanalytics-pipeline-lambda-name", "PrimitiveType": "String", - "Required": true, + "Required": false, "UpdateType": "Mutable" }, "Next": { @@ -37423,19 +37692,19 @@ "Attribute": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-pipeline-math.html#cfn-iotanalytics-pipeline-math-attribute", "PrimitiveType": "String", - "Required": true, + "Required": false, "UpdateType": "Mutable" }, "Math": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-pipeline-math.html#cfn-iotanalytics-pipeline-math-math", "PrimitiveType": "String", - "Required": true, + "Required": false, "UpdateType": "Mutable" }, "Name": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-pipeline-math.html#cfn-iotanalytics-pipeline-math-name", "PrimitiveType": "String", - "Required": true, + "Required": false, "UpdateType": "Mutable" }, "Next": { @@ -37451,16 +37720,15 @@ "Properties": { "Attributes": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-pipeline-removeattributes.html#cfn-iotanalytics-pipeline-removeattributes-attributes", - "DuplicatesAllowed": true, "PrimitiveItemType": "String", - "Required": true, + "Required": false, "Type": "List", "UpdateType": "Mutable" }, "Name": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-pipeline-removeattributes.html#cfn-iotanalytics-pipeline-removeattributes-name", "PrimitiveType": "String", - "Required": true, + "Required": false, "UpdateType": "Mutable" }, "Next": { @@ -37476,16 +37744,15 @@ "Properties": { "Attributes": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-pipeline-selectattributes.html#cfn-iotanalytics-pipeline-selectattributes-attributes", - "DuplicatesAllowed": true, "PrimitiveItemType": "String", - "Required": true, + "Required": false, "Type": "List", "UpdateType": "Mutable" }, "Name": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-pipeline-selectattributes.html#cfn-iotanalytics-pipeline-selectattributes-name", "PrimitiveType": "String", - "Required": true, + "Required": false, "UpdateType": "Mutable" }, "Next": { @@ -38651,6 +38918,52 @@ } } }, + "AWS::IoTWireless::FuotaTask.LoRaWAN": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotwireless-fuotatask-lorawan.html", + "Properties": { + "RfRegion": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotwireless-fuotatask-lorawan.html#cfn-iotwireless-fuotatask-lorawan-rfregion", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + }, + "StartTime": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotwireless-fuotatask-lorawan.html#cfn-iotwireless-fuotatask-lorawan-starttime", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + } + } + }, + "AWS::IoTWireless::MulticastGroup.LoRaWAN": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotwireless-multicastgroup-lorawan.html", + "Properties": { + "DlClass": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotwireless-multicastgroup-lorawan.html#cfn-iotwireless-multicastgroup-lorawan-dlclass", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + }, + "NumberOfDevicesInGroup": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotwireless-multicastgroup-lorawan.html#cfn-iotwireless-multicastgroup-lorawan-numberofdevicesingroup", + "PrimitiveType": "Integer", + "Required": false, + "UpdateType": "Mutable" + }, + "NumberOfDevicesRequested": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotwireless-multicastgroup-lorawan.html#cfn-iotwireless-multicastgroup-lorawan-numberofdevicesrequested", + "PrimitiveType": "Integer", + "Required": false, + "UpdateType": "Mutable" + }, + "RfRegion": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotwireless-multicastgroup-lorawan.html#cfn-iotwireless-multicastgroup-lorawan-rfregion", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + } + } + }, "AWS::IoTWireless::PartnerAccount.SidewalkAccountInfo": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotwireless-partneraccount-sidewalkaccountinfo.html", "Properties": { @@ -59118,14 +59431,14 @@ "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-s3objectlambda-accesspoint-transformationconfiguration.html#cfn-s3objectlambda-accesspoint-transformationconfiguration-actions", "DuplicatesAllowed": false, "PrimitiveItemType": "String", - "Required": false, + "Required": true, "Type": "List", "UpdateType": "Mutable" }, "ContentTransformation": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-s3objectlambda-accesspoint-transformationconfiguration.html#cfn-s3objectlambda-accesspoint-transformationconfiguration-contenttransformation", "PrimitiveType": "Json", - "Required": false, + "Required": true, "UpdateType": "Mutable" } } @@ -62910,6 +63223,18 @@ "Required": true, "UpdateType": "Mutable" }, + "SuperuserSecretArn": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-secretsmanager-rotationschedule-hostedrotationlambda.html#cfn-secretsmanager-rotationschedule-hostedrotationlambda-superusersecretarn", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "SuperuserSecretKmsKeyArn": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-secretsmanager-rotationschedule-hostedrotationlambda.html#cfn-secretsmanager-rotationschedule-hostedrotationlambda-superusersecretkmskeyarn", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, "VpcSecurityGroupIds": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-secretsmanager-rotationschedule-hostedrotationlambda.html#cfn-secretsmanager-rotationschedule-hostedrotationlambda-vpcsecuritygroupids", "PrimitiveType": "String", @@ -66092,7 +66417,7 @@ } } }, - "ResourceSpecificationVersion": "47.0.0", + "ResourceSpecificationVersion": "48.0.0", "ResourceTypes": { "AWS::ACMPCA::Certificate": { "Attributes": { @@ -67658,7 +67983,6 @@ }, "Variables": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-apigateway-stage.html#cfn-apigateway-stage-variables", - "DuplicatesAllowed": false, "PrimitiveItemType": "String", "Required": false, "Type": "Map", @@ -68564,6 +68888,12 @@ "Type": "List", "UpdateType": "Mutable" }, + "Type": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-appconfig-configurationprofile.html#cfn-appconfig-configurationprofile-type", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, "Validators": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-appconfig-configurationprofile.html#cfn-appconfig-configurationprofile-validators", "ItemType": "Validators", @@ -71501,6 +71831,35 @@ } } }, + "AWS::Batch::SchedulingPolicy": { + "Attributes": { + "Arn": { + "PrimitiveType": "String" + } + }, + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-batch-schedulingpolicy.html", + "Properties": { + "FairsharePolicy": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-batch-schedulingpolicy.html#cfn-batch-schedulingpolicy-fairsharepolicy", + "Required": false, + "Type": "FairsharePolicy", + "UpdateType": "Mutable" + }, + "Name": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-batch-schedulingpolicy.html#cfn-batch-schedulingpolicy-name", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, + "Tags": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-batch-schedulingpolicy.html#cfn-batch-schedulingpolicy-tags", + "PrimitiveItemType": "String", + "Required": false, + "Type": "Map", + "UpdateType": "Immutable" + } + } + }, "AWS::Budgets::Budget": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-budgets-budget.html", "Properties": { @@ -72683,12 +73042,6 @@ "Type": "FunctionConfig", "UpdateType": "Mutable" }, - "FunctionMetadata": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cloudfront-function.html#cfn-cloudfront-function-functionmetadata", - "Required": false, - "Type": "FunctionMetadata", - "UpdateType": "Mutable" - }, "Name": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cloudfront-function.html#cfn-cloudfront-function-name", "PrimitiveType": "String", @@ -73108,19 +73461,19 @@ "MetricName": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cloudwatch-anomalydetector.html#cfn-cloudwatch-anomalydetector-metricname", "PrimitiveType": "String", - "Required": true, + "Required": false, "UpdateType": "Immutable" }, "Namespace": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cloudwatch-anomalydetector.html#cfn-cloudwatch-anomalydetector-namespace", "PrimitiveType": "String", - "Required": true, + "Required": false, "UpdateType": "Immutable" }, "Stat": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cloudwatch-anomalydetector.html#cfn-cloudwatch-anomalydetector-stat", "PrimitiveType": "String", - "Required": true, + "Required": false, "UpdateType": "Immutable" } } @@ -78027,6 +78380,18 @@ "Required": true, "UpdateType": "Immutable" }, + "OutPostArn": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-capacityreservation.html#cfn-ec2-capacityreservation-outpostarn", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, + "PlacementGroupArn": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-capacityreservation.html#cfn-ec2-capacityreservation-placementgrouparn", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, "TagSpecifications": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-capacityreservation.html#cfn-ec2-capacityreservation-tagspecifications", "ItemType": "TagSpecification", @@ -79353,48 +79718,44 @@ }, "AWS::EC2::NetworkInterface": { "Attributes": { - "Id": { - "PrimitiveType": "String" - }, "PrimaryPrivateIpAddress": { "PrimitiveType": "String" }, "SecondaryPrivateIpAddresses": { - "DuplicatesAllowed": true, "PrimitiveItemType": "String", "Type": "List" } }, - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-networkinterface.html", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-network-interface.html", "Properties": { "Description": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-networkinterface.html#cfn-ec2-networkinterface-description", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-network-interface.html#cfn-awsec2networkinterface-description", "PrimitiveType": "String", "Required": false, "UpdateType": "Mutable" }, "GroupSet": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-networkinterface.html#cfn-ec2-networkinterface-groupset", - "DuplicatesAllowed": true, + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-network-interface.html#cfn-awsec2networkinterface-groupset", + "DuplicatesAllowed": false, "PrimitiveItemType": "String", "Required": false, "Type": "List", "UpdateType": "Mutable" }, "InterfaceType": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-networkinterface.html#cfn-ec2-networkinterface-interfacetype", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-network-interface.html#cfn-ec2-networkinterface-interfacetype", "PrimitiveType": "String", "Required": false, "UpdateType": "Immutable" }, "Ipv6AddressCount": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-networkinterface.html#cfn-ec2-networkinterface-ipv6addresscount", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-network-interface.html#cfn-ec2-networkinterface-ipv6addresscount", "PrimitiveType": "Integer", "Required": false, "UpdateType": "Mutable" }, "Ipv6Addresses": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-networkinterface.html#cfn-ec2-networkinterface-ipv6addresses", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-network-interface.html#cfn-ec2-networkinterface-ipv6addresses", "DuplicatesAllowed": false, "ItemType": "InstanceIpv6Address", "Required": false, @@ -79402,39 +79763,39 @@ "UpdateType": "Mutable" }, "PrivateIpAddress": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-networkinterface.html#cfn-ec2-networkinterface-privateipaddress", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-network-interface.html#cfn-awsec2networkinterface-privateipaddress", "PrimitiveType": "String", "Required": false, "UpdateType": "Immutable" }, "PrivateIpAddresses": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-networkinterface.html#cfn-ec2-networkinterface-privateipaddresses", - "DuplicatesAllowed": true, + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-network-interface.html#cfn-awsec2networkinterface-privateipaddresses", + "DuplicatesAllowed": false, "ItemType": "PrivateIpAddressSpecification", "Required": false, "Type": "List", - "UpdateType": "Mutable" + "UpdateType": "Conditional" }, "SecondaryPrivateIpAddressCount": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-networkinterface.html#cfn-ec2-networkinterface-secondaryprivateipaddresscount", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-network-interface.html#cfn-awsec2networkinterface-secondaryprivateipcount", "PrimitiveType": "Integer", "Required": false, "UpdateType": "Mutable" }, "SourceDestCheck": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-networkinterface.html#cfn-ec2-networkinterface-sourcedestcheck", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-network-interface.html#cfn-awsec2networkinterface-sourcedestcheck", "PrimitiveType": "Boolean", "Required": false, "UpdateType": "Mutable" }, "SubnetId": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-networkinterface.html#cfn-ec2-networkinterface-subnetid", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-network-interface.html#cfn-awsec2networkinterface-subnetid", "PrimitiveType": "String", "Required": true, "UpdateType": "Immutable" }, "Tags": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-networkinterface.html#cfn-ec2-networkinterface-tags", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-network-interface.html#cfn-awsec2networkinterface-tags", "DuplicatesAllowed": true, "ItemType": "Tag", "Required": false, @@ -84802,6 +85163,12 @@ "Required": true, "UpdateType": "Immutable" }, + "FileSystemTypeVersion": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-fsx-filesystem.html#cfn-fsx-filesystem-filesystemtypeversion", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, "KmsKeyId": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-fsx-filesystem.html#cfn-fsx-filesystem-kmskeyid", "PrimitiveType": "String", @@ -84814,6 +85181,12 @@ "Type": "LustreConfiguration", "UpdateType": "Mutable" }, + "OntapConfiguration": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-fsx-filesystem.html#cfn-fsx-filesystem-ontapconfiguration", + "Required": false, + "Type": "OntapConfiguration", + "UpdateType": "Mutable" + }, "SecurityGroupIds": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-fsx-filesystem.html#cfn-fsx-filesystem-securitygroupids", "PrimitiveItemType": "String", @@ -84881,6 +85254,14 @@ }, "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-finspace-environment.html", "Properties": { + "DataBundles": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-finspace-environment.html#cfn-finspace-environment-databundles", + "DuplicatesAllowed": true, + "PrimitiveItemType": "String", + "Required": false, + "Type": "List", + "UpdateType": "Immutable" + }, "Description": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-finspace-environment.html#cfn-finspace-environment-description", "PrimitiveType": "String", @@ -84910,6 +85291,12 @@ "PrimitiveType": "String", "Required": true, "UpdateType": "Mutable" + }, + "SuperuserParameters": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-finspace-environment.html#cfn-finspace-environment-superuserparameters", + "Required": false, + "Type": "SuperuserParameters", + "UpdateType": "Immutable" } } }, @@ -89700,17 +90087,12 @@ } }, "AWS::IoTAnalytics::Channel": { - "Attributes": { - "Id": { - "PrimitiveType": "String" - } - }, "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iotanalytics-channel.html", "Properties": { "ChannelName": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iotanalytics-channel.html#cfn-iotanalytics-channel-channelname", "PrimitiveType": "String", - "Required": true, + "Required": false, "UpdateType": "Immutable" }, "ChannelStorage": { @@ -89727,7 +90109,6 @@ }, "Tags": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iotanalytics-channel.html#cfn-iotanalytics-channel-tags", - "DuplicatesAllowed": true, "ItemType": "Tag", "Required": false, "Type": "List", @@ -89736,16 +90117,10 @@ } }, "AWS::IoTAnalytics::Dataset": { - "Attributes": { - "Id": { - "PrimitiveType": "String" - } - }, "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iotanalytics-dataset.html", "Properties": { "Actions": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iotanalytics-dataset.html#cfn-iotanalytics-dataset-actions", - "DuplicatesAllowed": true, "ItemType": "Action", "Required": true, "Type": "List", @@ -89753,7 +90128,6 @@ }, "ContentDeliveryRules": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iotanalytics-dataset.html#cfn-iotanalytics-dataset-contentdeliveryrules", - "DuplicatesAllowed": true, "ItemType": "DatasetContentDeliveryRule", "Required": false, "Type": "List", @@ -89762,12 +90136,11 @@ "DatasetName": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iotanalytics-dataset.html#cfn-iotanalytics-dataset-datasetname", "PrimitiveType": "String", - "Required": true, + "Required": false, "UpdateType": "Immutable" }, "LateDataRules": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iotanalytics-dataset.html#cfn-iotanalytics-dataset-latedatarules", - "DuplicatesAllowed": true, "ItemType": "LateDataRule", "Required": false, "Type": "List", @@ -89781,7 +90154,6 @@ }, "Tags": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iotanalytics-dataset.html#cfn-iotanalytics-dataset-tags", - "DuplicatesAllowed": true, "ItemType": "Tag", "Required": false, "Type": "List", @@ -89789,7 +90161,6 @@ }, "Triggers": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iotanalytics-dataset.html#cfn-iotanalytics-dataset-triggers", - "DuplicatesAllowed": true, "ItemType": "Trigger", "Required": false, "Type": "List", @@ -89804,11 +90175,6 @@ } }, "AWS::IoTAnalytics::Datastore": { - "Attributes": { - "Id": { - "PrimitiveType": "String" - } - }, "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iotanalytics-datastore.html", "Properties": { "DatastoreName": { @@ -89843,7 +90209,6 @@ }, "Tags": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iotanalytics-datastore.html#cfn-iotanalytics-datastore-tags", - "DuplicatesAllowed": true, "ItemType": "Tag", "Required": false, "Type": "List", @@ -89852,16 +90217,10 @@ } }, "AWS::IoTAnalytics::Pipeline": { - "Attributes": { - "Id": { - "PrimitiveType": "String" - } - }, "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iotanalytics-pipeline.html", "Properties": { "PipelineActivities": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iotanalytics-pipeline.html#cfn-iotanalytics-pipeline-pipelineactivities", - "DuplicatesAllowed": true, "ItemType": "Activity", "Required": true, "Type": "List", @@ -89870,12 +90229,11 @@ "PipelineName": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iotanalytics-pipeline.html#cfn-iotanalytics-pipeline-pipelinename", "PrimitiveType": "String", - "Required": true, + "Required": false, "UpdateType": "Immutable" }, "Tags": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iotanalytics-pipeline.html#cfn-iotanalytics-pipeline-tags", - "DuplicatesAllowed": true, "ItemType": "Tag", "Required": false, "Type": "List", @@ -90470,6 +90828,147 @@ } } }, + "AWS::IoTWireless::FuotaTask": { + "Attributes": { + "Arn": { + "PrimitiveType": "String" + }, + "FuotaTaskStatus": { + "PrimitiveType": "String" + }, + "Id": { + "PrimitiveType": "String" + }, + "LoRaWAN.StartTime": { + "PrimitiveType": "String" + } + }, + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iotwireless-fuotatask.html", + "Properties": { + "AssociateMulticastGroup": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iotwireless-fuotatask.html#cfn-iotwireless-fuotatask-associatemulticastgroup", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "AssociateWirelessDevice": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iotwireless-fuotatask.html#cfn-iotwireless-fuotatask-associatewirelessdevice", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "Description": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iotwireless-fuotatask.html#cfn-iotwireless-fuotatask-description", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "DisassociateMulticastGroup": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iotwireless-fuotatask.html#cfn-iotwireless-fuotatask-disassociatemulticastgroup", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "DisassociateWirelessDevice": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iotwireless-fuotatask.html#cfn-iotwireless-fuotatask-disassociatewirelessdevice", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "FirmwareUpdateImage": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iotwireless-fuotatask.html#cfn-iotwireless-fuotatask-firmwareupdateimage", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + }, + "FirmwareUpdateRole": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iotwireless-fuotatask.html#cfn-iotwireless-fuotatask-firmwareupdaterole", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + }, + "LoRaWAN": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iotwireless-fuotatask.html#cfn-iotwireless-fuotatask-lorawan", + "Required": true, + "Type": "LoRaWAN", + "UpdateType": "Mutable" + }, + "Name": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iotwireless-fuotatask.html#cfn-iotwireless-fuotatask-name", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "Tags": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iotwireless-fuotatask.html#cfn-iotwireless-fuotatask-tags", + "DuplicatesAllowed": false, + "ItemType": "Tag", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + } + } + }, + "AWS::IoTWireless::MulticastGroup": { + "Attributes": { + "Arn": { + "PrimitiveType": "String" + }, + "Id": { + "PrimitiveType": "String" + }, + "LoRaWAN.NumberOfDevicesInGroup": { + "PrimitiveType": "Integer" + }, + "LoRaWAN.NumberOfDevicesRequested": { + "PrimitiveType": "Integer" + }, + "Status": { + "PrimitiveType": "String" + } + }, + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iotwireless-multicastgroup.html", + "Properties": { + "AssociateWirelessDevice": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iotwireless-multicastgroup.html#cfn-iotwireless-multicastgroup-associatewirelessdevice", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "Description": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iotwireless-multicastgroup.html#cfn-iotwireless-multicastgroup-description", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "DisassociateWirelessDevice": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iotwireless-multicastgroup.html#cfn-iotwireless-multicastgroup-disassociatewirelessdevice", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "LoRaWAN": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iotwireless-multicastgroup.html#cfn-iotwireless-multicastgroup-lorawan", + "Required": true, + "Type": "LoRaWAN", + "UpdateType": "Mutable" + }, + "Name": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iotwireless-multicastgroup.html#cfn-iotwireless-multicastgroup-name", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "Tags": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iotwireless-multicastgroup.html#cfn-iotwireless-multicastgroup-tags", + "DuplicatesAllowed": false, + "ItemType": "Tag", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + } + } + }, "AWS::IoTWireless::PartnerAccount": { "Attributes": { "Arn": { @@ -92430,24 +92929,12 @@ "Required": false, "UpdateType": "Mutable" }, - "Location": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lightsail-instance.html#cfn-lightsail-instance-location", - "Required": false, - "Type": "Location", - "UpdateType": "Mutable" - }, "Networking": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lightsail-instance.html#cfn-lightsail-instance-networking", "Required": false, "Type": "Networking", "UpdateType": "Mutable" }, - "State": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lightsail-instance.html#cfn-lightsail-instance-state", - "Required": false, - "Type": "State", - "UpdateType": "Mutable" - }, "Tags": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lightsail-instance.html#cfn-lightsail-instance-tags", "DuplicatesAllowed": false, @@ -94592,12 +95079,6 @@ "Required": false, "UpdateType": "Mutable" }, - "ClusterEndpoint": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-memorydb-cluster.html#cfn-memorydb-cluster-clusterendpoint", - "Required": false, - "Type": "Endpoint", - "UpdateType": "Mutable" - }, "ClusterName": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-memorydb-cluster.html#cfn-memorydb-cluster-clustername", "PrimitiveType": "String", @@ -99728,12 +100209,6 @@ "Required": false, "UpdateType": "Mutable" }, - "Endpoint": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-redshift-cluster.html#cfn-redshift-cluster-endpoint", - "Required": false, - "Type": "Endpoint", - "UpdateType": "Mutable" - }, "EnhancedVpcRouting": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-redshift-cluster.html#cfn-redshift-cluster-enhancedvpcrouting", "PrimitiveType": "Boolean", @@ -101858,7 +102333,7 @@ }, "ObjectLambdaConfiguration": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-s3objectlambda-accesspoint.html#cfn-s3objectlambda-accesspoint-objectlambdaconfiguration", - "Required": false, + "Required": true, "Type": "ObjectLambdaConfiguration", "UpdateType": "Mutable" } diff --git a/packages/@aws-cdk/cloudformation-include/README.md b/packages/@aws-cdk/cloudformation-include/README.md index 2c67aeb4d554b..560718ea1449c 100644 --- a/packages/@aws-cdk/cloudformation-include/README.md +++ b/packages/@aws-cdk/cloudformation-include/README.md @@ -46,8 +46,6 @@ Resources: It can be included in a CDK application with the following code: ```ts -import * as cfn_inc from '@aws-cdk/cloudformation-include'; - const cfnTemplate = new cfn_inc.CfnInclude(this, 'Template', { templateFile: 'my-template.json', }); @@ -82,8 +80,7 @@ If you know the class of the CDK object that corresponds to that resource, you can cast the returned object to the correct type: ```ts -import * as s3 from '@aws-cdk/aws-s3'; - +declare const cfnTemplate: cfn_inc.CfnInclude; const cfnBucket = cfnTemplate.getResource('Bucket') as s3.CfnBucket; // cfnBucket is of type s3.CfnBucket ``` @@ -98,6 +95,8 @@ Any modifications made to that resource will be reflected in the resulting CDK t for example, the name of the bucket can be changed: ```ts +declare const cfnTemplate: cfn_inc.CfnInclude; +const cfnBucket = cfnTemplate.getResource('Bucket') as s3.CfnBucket; cfnBucket.bucketName = 'my-bucket-name'; ``` @@ -107,7 +106,8 @@ including the higher-level ones for example: ```ts -import * as iam from '@aws-cdk/aws-iam'; +declare const cfnTemplate: cfn_inc.CfnInclude; +const cfnBucket = cfnTemplate.getResource('Bucket') as s3.CfnBucket; const role = new iam.Role(this, 'Role', { assumedBy: new iam.AnyPrincipal(), @@ -136,8 +136,7 @@ for example, for KMS Keys, that would be the `Kms.fromCfnKey()` method - and passing the L1 instance as an argument: ```ts -import * as kms from '@aws-cdk/aws-kms'; - +declare const cfnTemplate: cfn_inc.CfnInclude; const cfnKey = cfnTemplate.getResource('Key') as kms.CfnKey; const key = kms.Key.fromCfnKey(cfnKey); ``` @@ -203,16 +202,23 @@ Each L2 class has static factory methods with names like `from*Name()`, You can obtain an L2 resource from an L1 by passing the correct properties of the L1 as the arguments to those methods: ```ts +declare const cfnTemplate: cfn_inc.CfnInclude; + // using from*Name() +const cfnBucket = cfnTemplate.getResource('Bucket') as s3.CfnBucket; const bucket = s3.Bucket.fromBucketName(this, 'L2Bucket', cfnBucket.ref); // using from*Arn() +const cfnKey = cfnTemplate.getResource('Key') as kms.CfnKey; const key = kms.Key.fromKeyArn(this, 'L2Key', cfnKey.attrArn); // using from*Attributes() +declare const privateCfnSubnet1: ec2.CfnSubnet; +declare const privateCfnSubnet2: ec2.CfnSubnet; +const cfnVpc = cfnTemplate.getResource('Vpc') as ec2.CfnVPC; const vpc = ec2.Vpc.fromVpcAttributes(this, 'L2Vpc', { vpcId: cfnVpc.ref, - availabilityZones: cdk.Fn.getAzs(), + availabilityZones: core.Fn.getAzs(), privateSubnetIds: [privateCfnSubnet1.ref, privateCfnSubnet2.ref], }); ``` @@ -231,71 +237,68 @@ you can also retrieve and mutate all other template elements: * [Parameters](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/parameters-section-structure.html): - ```ts - import * as core from '@aws-cdk/core'; - - const param: core.CfnParameter = cfnTemplate.getParameter('MyParameter'); + ```ts + declare const cfnTemplate: cfn_inc.CfnInclude; + const param: core.CfnParameter = cfnTemplate.getParameter('MyParameter'); - // mutating the parameter - param.default = 'MyDefault'; - ``` + // mutating the parameter + param.default = 'MyDefault'; + ``` * [Conditions](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/conditions-section-structure.html): - ```ts - import * as core from '@aws-cdk/core'; + ```ts + declare const cfnTemplate: cfn_inc.CfnInclude; + const condition: core.CfnCondition = cfnTemplate.getCondition('MyCondition'); - const condition: core.CfnCondition = cfnTemplate.getCondition('MyCondition'); - - // mutating the condition - condition.expression = core.Fn.conditionEquals(1, 2); - ``` + // mutating the condition + condition.expression = core.Fn.conditionEquals(1, 2); + ``` * [Mappings](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/mappings-section-structure.html): - ```ts - import * as core from '@aws-cdk/core'; - - const mapping: core.CfnMapping = cfnTemplate.getMapping('MyMapping'); + ```ts + declare const cfnTemplate: cfn_inc.CfnInclude; + const mapping: core.CfnMapping = cfnTemplate.getMapping('MyMapping'); - // mutating the mapping - mapping.setValue('my-region', 'AMI', 'ami-04681a1dbd79675a5'); - ``` + // mutating the mapping + mapping.setValue('my-region', 'AMI', 'ami-04681a1dbd79675a5'); + ``` * [Service Catalog template Rules](https://docs.aws.amazon.com/servicecatalog/latest/adminguide/reference-template_constraint_rules.html): - ```ts - import * as core from '@aws-cdk/core'; - - const rule: core.CfnRule = cfnTemplate.getRule('MyRule'); + ```ts + declare const cfnTemplate: cfn_inc.CfnInclude; + const rule: core.CfnRule = cfnTemplate.getRule('MyRule'); - // mutating the rule - rule.addAssertion(core.Fn.conditionContains(['m1.small'], myParameter.value), - 'MyParameter has to be m1.small'); - ``` + // mutating the rule + declare const myParameter: core.CfnParameter; + rule.addAssertion(core.Fn.conditionContains(['m1.small'], myParameter.valueAsString), + 'MyParameter has to be m1.small'); + ``` * [Outputs](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/outputs-section-structure.html): - ```ts - import * as core from '@aws-cdk/core'; + ```ts + declare const cfnTemplate: cfn_inc.CfnInclude; + const output: core.CfnOutput = cfnTemplate.getOutput('MyOutput'); - const output: core.CfnOutput = cfnTemplate.getOutput('MyOutput'); - - // mutating the output - output.value = cfnBucket.attrArn; - ``` + // mutating the output + declare const cfnBucket: s3.CfnBucket; + output.value = cfnBucket.attrArn; + ``` * [Hooks for blue-green deployments](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/blue-green.html): - ```ts - import * as core from '@aws-cdk/core'; - - const hook: core.CfnHook = cfnTemplate.getHook('MyOutput'); + ```ts + declare const cfnTemplate: cfn_inc.CfnInclude; + const hook: core.CfnHook = cfnTemplate.getHook('MyOutput'); - // mutating the hook - const codeDeployHook = hook as core.CfnCodeDeployBlueGreenHook; - codeDeployHook.serviceRole = myRole.roleArn; - ``` + // mutating the hook + declare const myRole: iam.Role; + const codeDeployHook = hook as core.CfnCodeDeployBlueGreenHook; + codeDeployHook.serviceRole = myRole.roleArn; + ``` ## Parameter replacement @@ -304,7 +307,7 @@ you may want to remove them in favor of build-time values. You can do that using the `parameters` property: ```ts -new inc.CfnInclude(this, 'includeTemplate', { +new cfn_inc.CfnInclude(this, 'includeTemplate', { templateFile: 'path/to/my/template', parameters: { 'MyParam': 'my-value', @@ -350,7 +353,7 @@ You can include both the parent stack, and the nested stack in your CDK application as follows: ```ts -const parentTemplate = new inc.CfnInclude(this, 'ParentStack', { +const parentTemplate = new cfn_inc.CfnInclude(this, 'ParentStack', { templateFile: 'path/to/my-parent-template.json', loadNestedStacks: { 'ChildStack': { @@ -371,6 +374,8 @@ will be modified to point to that asset. The included nested stack can be accessed with the `getNestedStack` method: ```ts +declare const parentTemplate: cfn_inc.CfnInclude; + const includedChildStack = parentTemplate.getNestedStack('ChildStack'); const childStack: core.NestedStack = includedChildStack.stack; const childTemplate: cfn_inc.CfnInclude = includedChildStack.includedTemplate; @@ -380,10 +385,12 @@ Now you can reference resources from `ChildStack`, and modify them like any other included template: ```ts +declare const childTemplate: cfn_inc.CfnInclude; + const cfnBucket = childTemplate.getResource('MyBucket') as s3.CfnBucket; cfnBucket.bucketName = 'my-new-bucket-name'; -const role = new iam.Role(childStack, 'MyRole', { +const role = new iam.Role(this, 'MyRole', { assumedBy: new iam.AccountRootPrincipal(), }); @@ -401,6 +408,7 @@ You can also include the nested stack after the `CfnInclude` object was created, instead of doing it on construction: ```ts +declare const parentTemplate: cfn_inc.CfnInclude; const includedChildStack = parentTemplate.loadNestedStack('ChildTemplate', { templateFile: 'path/to/my-nested-template.json', }); @@ -413,7 +421,9 @@ but more like specialized fragments, implementing a particular pattern or best p If you have templates like that, you can use the `CfnInclude` class to vend them as CDK Constructs: -```ts +```ts nofixture +import { Construct } from 'constructs'; +import * as cfn_inc from '@aws-cdk/cloudformation-include'; import * as path from 'path'; export class MyConstruct extends Construct { diff --git a/packages/@aws-cdk/cloudformation-include/package.json b/packages/@aws-cdk/cloudformation-include/package.json index a526eb3bf61d1..fcaecdf08c0c5 100644 --- a/packages/@aws-cdk/cloudformation-include/package.json +++ b/packages/@aws-cdk/cloudformation-include/package.json @@ -28,7 +28,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", diff --git a/packages/@aws-cdk/cloudformation-include/rosetta/default.ts-fixture b/packages/@aws-cdk/cloudformation-include/rosetta/default.ts-fixture new file mode 100644 index 0000000000000..307736228527e --- /dev/null +++ b/packages/@aws-cdk/cloudformation-include/rosetta/default.ts-fixture @@ -0,0 +1,18 @@ +// Fixture with packages imported, but nothing else +import { Construct } from 'constructs'; +import { Stack } from '@aws-cdk/core'; +import * as core from '@aws-cdk/core'; +import * as path from 'path'; +import * as cfn_inc from '@aws-cdk/cloudformation-include'; +import * as s3 from '@aws-cdk/aws-s3'; +import * as iam from '@aws-cdk/aws-iam'; +import * as kms from '@aws-cdk/aws-kms'; +import * as ec2 from '@aws-cdk/aws-ec2'; + +class Fixture extends Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + + /// here + } +} diff --git a/packages/@aws-cdk/core/lib/nested-stack.ts b/packages/@aws-cdk/core/lib/nested-stack.ts index 82a123fb9243b..347731894d794 100644 --- a/packages/@aws-cdk/core/lib/nested-stack.ts +++ b/packages/@aws-cdk/core/lib/nested-stack.ts @@ -1,4 +1,5 @@ import * as crypto from 'crypto'; +import * as cxapi from '@aws-cdk/cx-api'; import { Construct, Node } from 'constructs'; import { FileAssetPackaging } from './assets'; import { Fn } from './cfn-fn'; @@ -213,6 +214,8 @@ export class NestedStack extends Stack { fileName: this.templateFile, }); + this.addResourceMetadata(this.resource, 'TemplateURL'); + // if bucketName/objectKey are cfn parameters from a stack other than the parent stack, they will // be resolved as cross-stack references like any other (see "multi" tests). this._templateUrl = `https://s3.${this._parentStack.region}.${this._parentStack.urlSuffix}/${templateLocation.bucketName}/${templateLocation.objectKey}`; @@ -230,6 +233,18 @@ export class NestedStack extends Stack { }, }); } + + private addResourceMetadata(resource: CfnResource, resourceProperty: string) { + if (!this.node.tryGetContext(cxapi.ASSET_RESOURCE_METADATA_ENABLED_CONTEXT)) { + return; // not enabled + } + + // tell tools such as SAM CLI that the "TemplateURL" property of this resource + // points to the nested stack template for local emulation + resource.cfnOptions.metadata = resource.cfnOptions.metadata || { }; + resource.cfnOptions.metadata[cxapi.ASSET_RESOURCE_METADATA_PATH_KEY] = this.templateFile; + resource.cfnOptions.metadata[cxapi.ASSET_RESOURCE_METADATA_PROPERTY_KEY] = resourceProperty; + } } /** diff --git a/packages/@aws-cdk/core/test/stack.test.ts b/packages/@aws-cdk/core/test/stack.test.ts index 7c67f545d4b87..cd7186fea9f7d 100644 --- a/packages/@aws-cdk/core/test/stack.test.ts +++ b/packages/@aws-cdk/core/test/stack.test.ts @@ -661,6 +661,29 @@ describe('stack', () => { })); }); + test('asset metadata added to NestedStack resource that contains asset path and property', () => { + const app = new App(); + + // WHEN + const parentStack = new Stack(app, 'parent'); + parentStack.node.setContext(cxapi.ASSET_RESOURCE_METADATA_ENABLED_CONTEXT, true); + const childStack = new NestedStack(parentStack, 'child'); + new CfnResource(childStack, 'ChildResource', { type: 'Resource::Child' }); + + const assembly = app.synth(); + expect(assembly.getStackByName(parentStack.stackName).template).toEqual(expect.objectContaining({ + Resources: { + childNestedStackchildNestedStackResource7408D03F: expect.objectContaining({ + Metadata: { + 'aws:asset:path': 'parentchild13F9359B.nested.template.json', + 'aws:asset:property': 'TemplateURL', + }, + }), + }, + })); + + }); + test('cross-stack reference (substack references parent stack)', () => { // GIVEN const app = new App(); diff --git a/packages/@aws-cdk/cx-api/lib/assets.ts b/packages/@aws-cdk/cx-api/lib/assets.ts index c15dbcd82776f..0b3eaa52cefb5 100644 --- a/packages/@aws-cdk/cx-api/lib/assets.ts +++ b/packages/@aws-cdk/cx-api/lib/assets.ts @@ -10,6 +10,9 @@ export const ASSET_RESOURCE_METADATA_ENABLED_CONTEXT = 'aws:cdk:enable-asset-met * to resources. */ export const ASSET_RESOURCE_METADATA_PATH_KEY = 'aws:asset:path'; +export const ASSET_RESOURCE_METADATA_DOCKERFILE_PATH_KEY = 'aws:asset:dockerfile-path'; +export const ASSET_RESOURCE_METADATA_DOCKER_BUILD_ARGS_KEY = 'aws:asset:docker-build-args'; +export const ASSET_RESOURCE_METADATA_DOCKER_BUILD_TARGET_KEY = 'aws:asset:docker-build-target'; export const ASSET_RESOURCE_METADATA_PROPERTY_KEY = 'aws:asset:property'; /** diff --git a/packages/@aws-cdk/lambda-layer-awscli/README.md b/packages/@aws-cdk/lambda-layer-awscli/README.md index baad6362f5e21..e36677c222de0 100644 --- a/packages/@aws-cdk/lambda-layer-awscli/README.md +++ b/packages/@aws-cdk/lambda-layer-awscli/README.md @@ -15,8 +15,11 @@ This module exports a single class called `AwsCliLayer` which is a `lambda.Layer Usage: ```ts -const fn = new lambda.Function(...); -fn.addLayers(new AwsCliLayer(stack, 'AwsCliLayer')); +// AwsCliLayer bundles the AWS CLI in a lambda layer +import { AwsCliLayer } from '@aws-cdk/lambda-layer-awscli'; + +declare const fn: lambda.Function; +fn.addLayers(new AwsCliLayer(this, 'AwsCliLayer')); ``` The CLI will be installed under `/opt/awscli/aws`. diff --git a/packages/@aws-cdk/lambda-layer-awscli/package.json b/packages/@aws-cdk/lambda-layer-awscli/package.json index 6868ae29a97e8..1a66cd1bae0fb 100644 --- a/packages/@aws-cdk/lambda-layer-awscli/package.json +++ b/packages/@aws-cdk/lambda-layer-awscli/package.json @@ -29,7 +29,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", diff --git a/packages/@aws-cdk/lambda-layer-awscli/rosetta/default.ts-fixture b/packages/@aws-cdk/lambda-layer-awscli/rosetta/default.ts-fixture new file mode 100644 index 0000000000000..d3534321d7f54 --- /dev/null +++ b/packages/@aws-cdk/lambda-layer-awscli/rosetta/default.ts-fixture @@ -0,0 +1,12 @@ +// Fixture with packages imported, but nothing else +import { Construct } from 'constructs'; +import { Stack } from '@aws-cdk/core'; +import * as lambda from '@aws-cdk/aws-lambda'; + +class Fixture extends Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + + /// here + } +} diff --git a/packages/@aws-cdk/lambda-layer-kubectl/README.md b/packages/@aws-cdk/lambda-layer-kubectl/README.md index 97329d16c8c50..2a90c534c9f24 100644 --- a/packages/@aws-cdk/lambda-layer-kubectl/README.md +++ b/packages/@aws-cdk/lambda-layer-kubectl/README.md @@ -18,8 +18,11 @@ This module exports a single class called `KubectlLayer` which is a `lambda.Laye Usage: ```ts -const fn = new lambda.Function(...); -fn.addLayers(new KubectlLayer(stack, 'KubectlLayer')); +// KubectlLayer bundles the 'kubectl' and 'helm' command lines +import { KubectlLayer } from '@aws-cdk/lambda-layer-kubectl'; + +declare const fn: lambda.Function; +fn.addLayers(new KubectlLayer(this, 'KubectlLayer')); ``` `kubectl` will be installed under `/opt/kubectl/kubectl`, and `helm` will be installed under `/opt/helm/helm`. diff --git a/packages/@aws-cdk/lambda-layer-kubectl/package.json b/packages/@aws-cdk/lambda-layer-kubectl/package.json index eb1c41990eff8..b62ace7ad6246 100644 --- a/packages/@aws-cdk/lambda-layer-kubectl/package.json +++ b/packages/@aws-cdk/lambda-layer-kubectl/package.json @@ -29,7 +29,14 @@ ] } }, - "projectReferences": true + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } }, "repository": { "type": "git", diff --git a/packages/@aws-cdk/lambda-layer-kubectl/rosetta/default.ts-fixture b/packages/@aws-cdk/lambda-layer-kubectl/rosetta/default.ts-fixture new file mode 100644 index 0000000000000..d3534321d7f54 --- /dev/null +++ b/packages/@aws-cdk/lambda-layer-kubectl/rosetta/default.ts-fixture @@ -0,0 +1,12 @@ +// Fixture with packages imported, but nothing else +import { Construct } from 'constructs'; +import { Stack } from '@aws-cdk/core'; +import * as lambda from '@aws-cdk/aws-lambda'; + +class Fixture extends Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + + /// here + } +} diff --git a/packages/@aws-cdk/region-info/lib/default.ts b/packages/@aws-cdk/region-info/lib/default.ts index abd1001678bf2..c0ae0cc2b28d5 100644 --- a/packages/@aws-cdk/region-info/lib/default.ts +++ b/packages/@aws-cdk/region-info/lib/default.ts @@ -82,6 +82,7 @@ export class Default { // Services with a regional principal case 'states': + case 'ssm': return `${service}.${region}.amazonaws.com`; // Services with a partitional principal diff --git a/packages/@aws-cdk/region-info/test/default.test.ts b/packages/@aws-cdk/region-info/test/default.test.ts index b39952842d75a..1e7c1b166c9b6 100644 --- a/packages/@aws-cdk/region-info/test/default.test.ts +++ b/packages/@aws-cdk/region-info/test/default.test.ts @@ -5,7 +5,7 @@ const urlSuffix = '.nowhere.null'; describe('servicePrincipal', () => { for (const suffix of ['', '.amazonaws.com', '.amazonaws.com.cn']) { - for (const service of ['states']) { + for (const service of ['states', 'ssm']) { test(`${service}${suffix}`, () => { expect(Default.servicePrincipal(`${service}${suffix}`, region, urlSuffix)).toBe(`${service}.${region}.amazonaws.com`); }); diff --git a/packages/aws-cdk/lib/init-templates/v1/app/csharp/cdk.template.json b/packages/aws-cdk/lib/init-templates/v1/app/csharp/cdk.template.json index 94c37dee310c0..6711bc81bde11 100644 --- a/packages/aws-cdk/lib/init-templates/v1/app/csharp/cdk.template.json +++ b/packages/aws-cdk/lib/init-templates/v1/app/csharp/cdk.template.json @@ -1,3 +1,15 @@ { - "app": "dotnet run -p src/%name.PascalCased%/%name.PascalCased%.csproj" + "app": "dotnet run -p src/%name.PascalCased%/%name.PascalCased%.csproj", + "watch": { + "include": ["**"], + "exclude": [ + "README.md", + "cdk*.json", + "src/*/obj", + "src/*/bin", + "src/*.sln", + "src/*/GlobalSuppressions.cs", + "src/*/*.csproj" + ] + } } diff --git a/packages/aws-cdk/lib/init-templates/v1/app/fsharp/cdk.template.json b/packages/aws-cdk/lib/init-templates/v1/app/fsharp/cdk.template.json index a08c461d2a2e2..040844e83e006 100644 --- a/packages/aws-cdk/lib/init-templates/v1/app/fsharp/cdk.template.json +++ b/packages/aws-cdk/lib/init-templates/v1/app/fsharp/cdk.template.json @@ -1,3 +1,14 @@ { - "app": "dotnet run -p src/%name.PascalCased%/%name.PascalCased%.fsproj" + "app": "dotnet run -p src/%name.PascalCased%/%name.PascalCased%.fsproj", + "watch": { + "include": ["**"], + "exclude": [ + "README.md", + "cdk*.json", + "src/*/obj", + "src/*/bin", + "src/*.sln", + "src/*/*.fsproj" + ] + } } diff --git a/packages/aws-cdk/lib/init-templates/v1/app/go/cdk.template.json b/packages/aws-cdk/lib/init-templates/v1/app/go/cdk.template.json index ad88cd7ef75f3..a25485ed0951b 100644 --- a/packages/aws-cdk/lib/init-templates/v1/app/go/cdk.template.json +++ b/packages/aws-cdk/lib/init-templates/v1/app/go/cdk.template.json @@ -1,3 +1,13 @@ { - "app": "go mod download && go run %name%.go" -} \ No newline at end of file + "app": "go mod download && go run %name%.go", + "watch": { + "include": ["**"], + "exclude": [ + "README.md", + "cdk*.json", + "go.mod", + "go.sum", + "**/*test.go" + ] + } +} diff --git a/packages/aws-cdk/lib/init-templates/v1/app/java/cdk.json b/packages/aws-cdk/lib/init-templates/v1/app/java/cdk.json index b112918622f63..b21c3e47a9552 100644 --- a/packages/aws-cdk/lib/init-templates/v1/app/java/cdk.json +++ b/packages/aws-cdk/lib/init-templates/v1/app/java/cdk.json @@ -1,3 +1,13 @@ { - "app": "mvn -e -q compile exec:java" + "app": "mvn -e -q compile exec:java", + "watch": { + "include": ["**"], + "exclude": [ + "README.md", + "cdk*.json", + "target", + "pom.xml", + "src/test" + ] + } } diff --git a/packages/aws-cdk/lib/init-templates/v1/app/javascript/cdk.template.json b/packages/aws-cdk/lib/init-templates/v1/app/javascript/cdk.template.json index ca1d40ed37e2d..6056727247dff 100644 --- a/packages/aws-cdk/lib/init-templates/v1/app/javascript/cdk.template.json +++ b/packages/aws-cdk/lib/init-templates/v1/app/javascript/cdk.template.json @@ -1,3 +1,15 @@ { - "app": "node bin/%name%.js" + "app": "node bin/%name%.js", + "watch": { + "include": ["**"], + "exclude": [ + "README.md", + "cdk*.json", + "jest.config.js", + "package*.json", + "yarn.lock", + "node_modules", + "test" + ] + } } diff --git a/packages/aws-cdk/lib/init-templates/v1/app/python/cdk.template.json b/packages/aws-cdk/lib/init-templates/v1/app/python/cdk.template.json index d7293493c4415..1c467275741e1 100644 --- a/packages/aws-cdk/lib/init-templates/v1/app/python/cdk.template.json +++ b/packages/aws-cdk/lib/init-templates/v1/app/python/cdk.template.json @@ -1,3 +1,15 @@ { - "app": "%python-executable% app.py" + "app": "%python-executable% app.py", + "watch": { + "include": ["**"], + "exclude": [ + "README.md", + "cdk*.json", + "requirements*.txt", + "source.bat", + "**/__init__.py", + "python/__pycache__", + "tests" + ] + } } diff --git a/packages/aws-cdk/lib/init-templates/v1/app/typescript/cdk.template.json b/packages/aws-cdk/lib/init-templates/v1/app/typescript/cdk.template.json index 4b132c728abd7..e9b5bea306944 100644 --- a/packages/aws-cdk/lib/init-templates/v1/app/typescript/cdk.template.json +++ b/packages/aws-cdk/lib/init-templates/v1/app/typescript/cdk.template.json @@ -1,3 +1,17 @@ { - "app": "npx ts-node --prefer-ts-exts bin/%name%.ts" + "app": "npx ts-node --prefer-ts-exts bin/%name%.ts", + "watch": { + "include": ["**"], + "exclude": [ + "README.md", + "cdk*.json", + "**/*.d.ts", + "**/*.js", + "tsconfig.json", + "package*.json", + "yarn.lock", + "node_modules", + "test" + ] + } } diff --git a/packages/aws-cdk/lib/init-templates/v1/sample-app/csharp/cdk.template.json b/packages/aws-cdk/lib/init-templates/v1/sample-app/csharp/cdk.template.json index 94c37dee310c0..6711bc81bde11 100644 --- a/packages/aws-cdk/lib/init-templates/v1/sample-app/csharp/cdk.template.json +++ b/packages/aws-cdk/lib/init-templates/v1/sample-app/csharp/cdk.template.json @@ -1,3 +1,15 @@ { - "app": "dotnet run -p src/%name.PascalCased%/%name.PascalCased%.csproj" + "app": "dotnet run -p src/%name.PascalCased%/%name.PascalCased%.csproj", + "watch": { + "include": ["**"], + "exclude": [ + "README.md", + "cdk*.json", + "src/*/obj", + "src/*/bin", + "src/*.sln", + "src/*/GlobalSuppressions.cs", + "src/*/*.csproj" + ] + } } diff --git a/packages/aws-cdk/lib/init-templates/v1/sample-app/fsharp/cdk.template.json b/packages/aws-cdk/lib/init-templates/v1/sample-app/fsharp/cdk.template.json index a08c461d2a2e2..040844e83e006 100644 --- a/packages/aws-cdk/lib/init-templates/v1/sample-app/fsharp/cdk.template.json +++ b/packages/aws-cdk/lib/init-templates/v1/sample-app/fsharp/cdk.template.json @@ -1,3 +1,14 @@ { - "app": "dotnet run -p src/%name.PascalCased%/%name.PascalCased%.fsproj" + "app": "dotnet run -p src/%name.PascalCased%/%name.PascalCased%.fsproj", + "watch": { + "include": ["**"], + "exclude": [ + "README.md", + "cdk*.json", + "src/*/obj", + "src/*/bin", + "src/*.sln", + "src/*/*.fsproj" + ] + } } diff --git a/packages/aws-cdk/lib/init-templates/v1/sample-app/go/cdk.template.json b/packages/aws-cdk/lib/init-templates/v1/sample-app/go/cdk.template.json index ad88cd7ef75f3..a25485ed0951b 100644 --- a/packages/aws-cdk/lib/init-templates/v1/sample-app/go/cdk.template.json +++ b/packages/aws-cdk/lib/init-templates/v1/sample-app/go/cdk.template.json @@ -1,3 +1,13 @@ { - "app": "go mod download && go run %name%.go" -} \ No newline at end of file + "app": "go mod download && go run %name%.go", + "watch": { + "include": ["**"], + "exclude": [ + "README.md", + "cdk*.json", + "go.mod", + "go.sum", + "**/*test.go" + ] + } +} diff --git a/packages/aws-cdk/lib/init-templates/v1/sample-app/java/cdk.json b/packages/aws-cdk/lib/init-templates/v1/sample-app/java/cdk.json index b112918622f63..b21c3e47a9552 100644 --- a/packages/aws-cdk/lib/init-templates/v1/sample-app/java/cdk.json +++ b/packages/aws-cdk/lib/init-templates/v1/sample-app/java/cdk.json @@ -1,3 +1,13 @@ { - "app": "mvn -e -q compile exec:java" + "app": "mvn -e -q compile exec:java", + "watch": { + "include": ["**"], + "exclude": [ + "README.md", + "cdk*.json", + "target", + "pom.xml", + "src/test" + ] + } } diff --git a/packages/aws-cdk/lib/init-templates/v1/sample-app/javascript/cdk.template.json b/packages/aws-cdk/lib/init-templates/v1/sample-app/javascript/cdk.template.json index ca1d40ed37e2d..6056727247dff 100644 --- a/packages/aws-cdk/lib/init-templates/v1/sample-app/javascript/cdk.template.json +++ b/packages/aws-cdk/lib/init-templates/v1/sample-app/javascript/cdk.template.json @@ -1,3 +1,15 @@ { - "app": "node bin/%name%.js" + "app": "node bin/%name%.js", + "watch": { + "include": ["**"], + "exclude": [ + "README.md", + "cdk*.json", + "jest.config.js", + "package*.json", + "yarn.lock", + "node_modules", + "test" + ] + } } diff --git a/packages/aws-cdk/lib/init-templates/v1/sample-app/python/cdk.template.json b/packages/aws-cdk/lib/init-templates/v1/sample-app/python/cdk.template.json index d7293493c4415..1c467275741e1 100644 --- a/packages/aws-cdk/lib/init-templates/v1/sample-app/python/cdk.template.json +++ b/packages/aws-cdk/lib/init-templates/v1/sample-app/python/cdk.template.json @@ -1,3 +1,15 @@ { - "app": "%python-executable% app.py" + "app": "%python-executable% app.py", + "watch": { + "include": ["**"], + "exclude": [ + "README.md", + "cdk*.json", + "requirements*.txt", + "source.bat", + "**/__init__.py", + "python/__pycache__", + "tests" + ] + } } diff --git a/packages/aws-cdk/lib/init-templates/v1/sample-app/typescript/cdk.template.json b/packages/aws-cdk/lib/init-templates/v1/sample-app/typescript/cdk.template.json index 4b132c728abd7..e9b5bea306944 100644 --- a/packages/aws-cdk/lib/init-templates/v1/sample-app/typescript/cdk.template.json +++ b/packages/aws-cdk/lib/init-templates/v1/sample-app/typescript/cdk.template.json @@ -1,3 +1,17 @@ { - "app": "npx ts-node --prefer-ts-exts bin/%name%.ts" + "app": "npx ts-node --prefer-ts-exts bin/%name%.ts", + "watch": { + "include": ["**"], + "exclude": [ + "README.md", + "cdk*.json", + "**/*.d.ts", + "**/*.js", + "tsconfig.json", + "package*.json", + "yarn.lock", + "node_modules", + "test" + ] + } } diff --git a/packages/aws-cdk/lib/init-templates/v2/app/csharp/cdk.template.json b/packages/aws-cdk/lib/init-templates/v2/app/csharp/cdk.template.json index 94c37dee310c0..6711bc81bde11 100644 --- a/packages/aws-cdk/lib/init-templates/v2/app/csharp/cdk.template.json +++ b/packages/aws-cdk/lib/init-templates/v2/app/csharp/cdk.template.json @@ -1,3 +1,15 @@ { - "app": "dotnet run -p src/%name.PascalCased%/%name.PascalCased%.csproj" + "app": "dotnet run -p src/%name.PascalCased%/%name.PascalCased%.csproj", + "watch": { + "include": ["**"], + "exclude": [ + "README.md", + "cdk*.json", + "src/*/obj", + "src/*/bin", + "src/*.sln", + "src/*/GlobalSuppressions.cs", + "src/*/*.csproj" + ] + } } diff --git a/packages/aws-cdk/lib/init-templates/v2/app/fsharp/cdk.template.json b/packages/aws-cdk/lib/init-templates/v2/app/fsharp/cdk.template.json index a08c461d2a2e2..040844e83e006 100644 --- a/packages/aws-cdk/lib/init-templates/v2/app/fsharp/cdk.template.json +++ b/packages/aws-cdk/lib/init-templates/v2/app/fsharp/cdk.template.json @@ -1,3 +1,14 @@ { - "app": "dotnet run -p src/%name.PascalCased%/%name.PascalCased%.fsproj" + "app": "dotnet run -p src/%name.PascalCased%/%name.PascalCased%.fsproj", + "watch": { + "include": ["**"], + "exclude": [ + "README.md", + "cdk*.json", + "src/*/obj", + "src/*/bin", + "src/*.sln", + "src/*/*.fsproj" + ] + } } diff --git a/packages/aws-cdk/lib/init-templates/v2/app/go/cdk.template.json b/packages/aws-cdk/lib/init-templates/v2/app/go/cdk.template.json index ad88cd7ef75f3..a25485ed0951b 100644 --- a/packages/aws-cdk/lib/init-templates/v2/app/go/cdk.template.json +++ b/packages/aws-cdk/lib/init-templates/v2/app/go/cdk.template.json @@ -1,3 +1,13 @@ { - "app": "go mod download && go run %name%.go" -} \ No newline at end of file + "app": "go mod download && go run %name%.go", + "watch": { + "include": ["**"], + "exclude": [ + "README.md", + "cdk*.json", + "go.mod", + "go.sum", + "**/*test.go" + ] + } +} diff --git a/packages/aws-cdk/lib/init-templates/v2/app/java/cdk.json b/packages/aws-cdk/lib/init-templates/v2/app/java/cdk.json index b112918622f63..b21c3e47a9552 100644 --- a/packages/aws-cdk/lib/init-templates/v2/app/java/cdk.json +++ b/packages/aws-cdk/lib/init-templates/v2/app/java/cdk.json @@ -1,3 +1,13 @@ { - "app": "mvn -e -q compile exec:java" + "app": "mvn -e -q compile exec:java", + "watch": { + "include": ["**"], + "exclude": [ + "README.md", + "cdk*.json", + "target", + "pom.xml", + "src/test" + ] + } } diff --git a/packages/aws-cdk/lib/init-templates/v2/app/javascript/cdk.template.json b/packages/aws-cdk/lib/init-templates/v2/app/javascript/cdk.template.json index ca1d40ed37e2d..6056727247dff 100644 --- a/packages/aws-cdk/lib/init-templates/v2/app/javascript/cdk.template.json +++ b/packages/aws-cdk/lib/init-templates/v2/app/javascript/cdk.template.json @@ -1,3 +1,15 @@ { - "app": "node bin/%name%.js" + "app": "node bin/%name%.js", + "watch": { + "include": ["**"], + "exclude": [ + "README.md", + "cdk*.json", + "jest.config.js", + "package*.json", + "yarn.lock", + "node_modules", + "test" + ] + } } diff --git a/packages/aws-cdk/lib/init-templates/v2/app/python/cdk.template.json b/packages/aws-cdk/lib/init-templates/v2/app/python/cdk.template.json index d7293493c4415..1c467275741e1 100644 --- a/packages/aws-cdk/lib/init-templates/v2/app/python/cdk.template.json +++ b/packages/aws-cdk/lib/init-templates/v2/app/python/cdk.template.json @@ -1,3 +1,15 @@ { - "app": "%python-executable% app.py" + "app": "%python-executable% app.py", + "watch": { + "include": ["**"], + "exclude": [ + "README.md", + "cdk*.json", + "requirements*.txt", + "source.bat", + "**/__init__.py", + "python/__pycache__", + "tests" + ] + } } diff --git a/packages/aws-cdk/lib/init-templates/v2/app/typescript/cdk.template.json b/packages/aws-cdk/lib/init-templates/v2/app/typescript/cdk.template.json index 4b132c728abd7..e9b5bea306944 100644 --- a/packages/aws-cdk/lib/init-templates/v2/app/typescript/cdk.template.json +++ b/packages/aws-cdk/lib/init-templates/v2/app/typescript/cdk.template.json @@ -1,3 +1,17 @@ { - "app": "npx ts-node --prefer-ts-exts bin/%name%.ts" + "app": "npx ts-node --prefer-ts-exts bin/%name%.ts", + "watch": { + "include": ["**"], + "exclude": [ + "README.md", + "cdk*.json", + "**/*.d.ts", + "**/*.js", + "tsconfig.json", + "package*.json", + "yarn.lock", + "node_modules", + "test" + ] + } } diff --git a/packages/aws-cdk/lib/init-templates/v2/sample-app/csharp/cdk.template.json b/packages/aws-cdk/lib/init-templates/v2/sample-app/csharp/cdk.template.json index 94c37dee310c0..6711bc81bde11 100644 --- a/packages/aws-cdk/lib/init-templates/v2/sample-app/csharp/cdk.template.json +++ b/packages/aws-cdk/lib/init-templates/v2/sample-app/csharp/cdk.template.json @@ -1,3 +1,15 @@ { - "app": "dotnet run -p src/%name.PascalCased%/%name.PascalCased%.csproj" + "app": "dotnet run -p src/%name.PascalCased%/%name.PascalCased%.csproj", + "watch": { + "include": ["**"], + "exclude": [ + "README.md", + "cdk*.json", + "src/*/obj", + "src/*/bin", + "src/*.sln", + "src/*/GlobalSuppressions.cs", + "src/*/*.csproj" + ] + } } diff --git a/packages/aws-cdk/lib/init-templates/v2/sample-app/fsharp/cdk.template.json b/packages/aws-cdk/lib/init-templates/v2/sample-app/fsharp/cdk.template.json index a08c461d2a2e2..040844e83e006 100644 --- a/packages/aws-cdk/lib/init-templates/v2/sample-app/fsharp/cdk.template.json +++ b/packages/aws-cdk/lib/init-templates/v2/sample-app/fsharp/cdk.template.json @@ -1,3 +1,14 @@ { - "app": "dotnet run -p src/%name.PascalCased%/%name.PascalCased%.fsproj" + "app": "dotnet run -p src/%name.PascalCased%/%name.PascalCased%.fsproj", + "watch": { + "include": ["**"], + "exclude": [ + "README.md", + "cdk*.json", + "src/*/obj", + "src/*/bin", + "src/*.sln", + "src/*/*.fsproj" + ] + } } diff --git a/packages/aws-cdk/lib/init-templates/v2/sample-app/go/cdk.template.json b/packages/aws-cdk/lib/init-templates/v2/sample-app/go/cdk.template.json index ad88cd7ef75f3..a25485ed0951b 100644 --- a/packages/aws-cdk/lib/init-templates/v2/sample-app/go/cdk.template.json +++ b/packages/aws-cdk/lib/init-templates/v2/sample-app/go/cdk.template.json @@ -1,3 +1,13 @@ { - "app": "go mod download && go run %name%.go" -} \ No newline at end of file + "app": "go mod download && go run %name%.go", + "watch": { + "include": ["**"], + "exclude": [ + "README.md", + "cdk*.json", + "go.mod", + "go.sum", + "**/*test.go" + ] + } +} diff --git a/packages/aws-cdk/lib/init-templates/v2/sample-app/java/cdk.json b/packages/aws-cdk/lib/init-templates/v2/sample-app/java/cdk.json index b112918622f63..b21c3e47a9552 100644 --- a/packages/aws-cdk/lib/init-templates/v2/sample-app/java/cdk.json +++ b/packages/aws-cdk/lib/init-templates/v2/sample-app/java/cdk.json @@ -1,3 +1,13 @@ { - "app": "mvn -e -q compile exec:java" + "app": "mvn -e -q compile exec:java", + "watch": { + "include": ["**"], + "exclude": [ + "README.md", + "cdk*.json", + "target", + "pom.xml", + "src/test" + ] + } } diff --git a/packages/aws-cdk/lib/init-templates/v2/sample-app/javascript/cdk.template.json b/packages/aws-cdk/lib/init-templates/v2/sample-app/javascript/cdk.template.json index ca1d40ed37e2d..6056727247dff 100644 --- a/packages/aws-cdk/lib/init-templates/v2/sample-app/javascript/cdk.template.json +++ b/packages/aws-cdk/lib/init-templates/v2/sample-app/javascript/cdk.template.json @@ -1,3 +1,15 @@ { - "app": "node bin/%name%.js" + "app": "node bin/%name%.js", + "watch": { + "include": ["**"], + "exclude": [ + "README.md", + "cdk*.json", + "jest.config.js", + "package*.json", + "yarn.lock", + "node_modules", + "test" + ] + } } diff --git a/packages/aws-cdk/lib/init-templates/v2/sample-app/python/cdk.template.json b/packages/aws-cdk/lib/init-templates/v2/sample-app/python/cdk.template.json index d7293493c4415..1c467275741e1 100644 --- a/packages/aws-cdk/lib/init-templates/v2/sample-app/python/cdk.template.json +++ b/packages/aws-cdk/lib/init-templates/v2/sample-app/python/cdk.template.json @@ -1,3 +1,15 @@ { - "app": "%python-executable% app.py" + "app": "%python-executable% app.py", + "watch": { + "include": ["**"], + "exclude": [ + "README.md", + "cdk*.json", + "requirements*.txt", + "source.bat", + "**/__init__.py", + "python/__pycache__", + "tests" + ] + } } diff --git a/packages/aws-cdk/lib/init-templates/v2/sample-app/typescript/cdk.template.json b/packages/aws-cdk/lib/init-templates/v2/sample-app/typescript/cdk.template.json index 4b132c728abd7..e9b5bea306944 100644 --- a/packages/aws-cdk/lib/init-templates/v2/sample-app/typescript/cdk.template.json +++ b/packages/aws-cdk/lib/init-templates/v2/sample-app/typescript/cdk.template.json @@ -1,3 +1,17 @@ { - "app": "npx ts-node --prefer-ts-exts bin/%name%.ts" + "app": "npx ts-node --prefer-ts-exts bin/%name%.ts", + "watch": { + "include": ["**"], + "exclude": [ + "README.md", + "cdk*.json", + "**/*.d.ts", + "**/*.js", + "tsconfig.json", + "package*.json", + "yarn.lock", + "node_modules", + "test" + ] + } } diff --git a/packages/aws-cdk/lib/settings.ts b/packages/aws-cdk/lib/settings.ts index 6182ecc26e0c3..38723bffc0bc3 100644 --- a/packages/aws-cdk/lib/settings.ts +++ b/packages/aws-cdk/lib/settings.ts @@ -30,6 +30,7 @@ export enum Command { METADATA = 'metadata', INIT = 'init', VERSION = 'version', + WATCH = 'watch', } const BUNDLING_COMMANDS = [ @@ -37,6 +38,7 @@ const BUNDLING_COMMANDS = [ Command.DIFF, Command.SYNTH, Command.SYNTHESIZE, + Command.WATCH, ]; export type Arguments = { @@ -251,7 +253,7 @@ export class Settings { // Determine bundling stacks let bundlingStacks: string[]; if (BUNDLING_COMMANDS.includes(argv._[0])) { - // If we deploy, diff or synth a list of stacks exclusively we skip + // If we deploy, diff, synth or watch a list of stacks exclusively we skip // bundling for all other stacks. bundlingStacks = argv.exclusively ? argv.STACKS ?? ['*'] diff --git a/packages/aws-cdk/test/integ/test-cli-regression.bash b/packages/aws-cdk/test/integ/test-cli-regression.bash index 12cbe81791d65..1770ee269d87f 100644 --- a/packages/aws-cdk/test/integ/test-cli-regression.bash +++ b/packages/aws-cdk/test/integ/test-cli-regression.bash @@ -54,12 +54,13 @@ function run_regression_against_framework_version() { echo "Downloading aws-cdk ${PREVIOUS_VERSION} tarball from npm" npm pack aws-cdk@${PREVIOUS_VERSION} - tar -zxvf aws-cdk-${PREVIOUS_VERSION}.tgz + tar -zxf aws-cdk-${PREVIOUS_VERSION}.tgz rm -rf ${integ_under_test} echo "Copying integration tests of version ${PREVIOUS_VERSION} to ${integ_under_test} (dont worry, its gitignored)" cp -r ${temp_dir}/package/test/integ/cli "${integ_under_test}" + cp -r ${temp_dir}/package/test/integ/helpers "${integ_under_test}" patch_dir="${integdir}/cli-regression-patches/v${PREVIOUS_VERSION}" # delete possibly stale junit.xml file @@ -73,5 +74,10 @@ function run_regression_against_framework_version() { # the framework version to use is determined by the caller as the first argument. # its a variable name indirection. - FRAMEWORK_VERSION=${!FRAMEWORK_VERSION_IDENTIFIER} ${integ_under_test}/test.sh + export FRAMEWORK_VERSION=${!FRAMEWORK_VERSION_IDENTIFIER} + + # Show the versions we settled on + echo "♈️ Regression testing [cli $(cdk --version)] against [framework ${FRAMEWORK_VERSION}] using [tests ${PREVIOUS_VERSION}}]" + + ${integ_under_test}/test.sh } diff --git a/packages/aws-cdk/test/settings.test.ts b/packages/aws-cdk/test/settings.test.ts index 0f283b386006b..aef16e6bac946 100644 --- a/packages/aws-cdk/test/settings.test.ts +++ b/packages/aws-cdk/test/settings.test.ts @@ -100,6 +100,16 @@ test('bundling stacks defaults to * for deploy', () => { expect(settings.get(['bundlingStacks'])).toEqual(['*']); }); +test('bundling stacks defaults to * for watch', () => { + // GIVEN + const settings = Settings.fromCommandLineArguments({ + _: [Command.WATCH], + }); + + // THEN + expect(settings.get(['bundlingStacks'])).toEqual(['*']); +}); + test('bundling stacks with deploy exclusively', () => { // GIVEN const settings = Settings.fromCommandLineArguments({ @@ -112,6 +122,18 @@ test('bundling stacks with deploy exclusively', () => { expect(settings.get(['bundlingStacks'])).toEqual(['cool-stack']); }); +test('bundling stacks with watch exclusively', () => { + // GIVEN + const settings = Settings.fromCommandLineArguments({ + _: [Command.WATCH], + exclusively: true, + STACKS: ['cool-stack'], + }); + + // THEN + expect(settings.get(['bundlingStacks'])).toEqual(['cool-stack']); +}); + test('should include outputs-file in settings', () => { // GIVEN const settings = Settings.fromCommandLineArguments({ diff --git a/scripts/run-rosetta.sh b/scripts/run-rosetta.sh index 8e12df3da420d..1bec4074305e4 100755 --- a/scripts/run-rosetta.sh +++ b/scripts/run-rosetta.sh @@ -2,7 +2,18 @@ # # Run jsii-rosetta on all jsii packages, using the S3 build cache if available. # -# Usage: run-rosetta [PKGSFILE] +# Usage: run-rosetta [--infuse] [--pkgs-from PKGSFILE] +# +# Performs three steps, in that order: +# +# 1. Run `rosetta extract` to read and translate all examples from all JSII +# assemblies. +# +# 2. Run `rosetta infuse` to traverse all examples we have, and copy them +# to classes that don't have an example yet. +# +# 3. Run `tools/@aws-cdk/generate-examples` to find all types that *still* +# don't have examples associated with tme, and generate synthetic examples. # # If you already have a file with a list of all the JSII package directories # in it, pass it as the first argument. Otherwise, this script will run @@ -12,28 +23,75 @@ scriptdir=$(cd $(dirname $0) && pwd) ROSETTA=${ROSETTA:-npx jsii-rosetta} -if [[ "${1:-}" = "" ]]; then +infuse=false +jsii_pkgs_file="" +while [[ $# -gt 0 ]]; do + case $1 in + --infuse) + infuse="true" + ;; + --pkgs-from) + jsii_pkgs_file="$2" + shift + ;; + -h|--help) + echo "Usage: run-rosetta.sh [--infuse] [--pkgs-from FILE]" >&2 + exit 1 + ;; + *) + echo "Unrecognized argument: $1" >&2 + exit 1 + ;; + esac + shift +done + + +if [[ "${jsii_pkgs_file}" = "" ]]; then echo "Collecting package list..." >&2 TMPDIR=${TMPDIR:-$(dirname $(mktemp -u))} node $scriptdir/list-packages $TMPDIR/jsii.txt $TMPDIR/nonjsii.txt jsii_pkgs_file=$TMPDIR/jsii.txt -else - jsii_pkgs_file=$1 fi rosetta_cache_file=$HOME/.s3buildcache/rosetta-cache.tabl.json rosetta_cache_opts="" +genexample_cache_opts="" if [[ -f $rosetta_cache_file ]]; then rosetta_cache_opts="--cache-from ${rosetta_cache_file}" + genexample_cache_opts="--cache-from ${rosetta_cache_file}" fi +#---------------------------------------------------------------------- + +# Compile examples with respect to "decdk" directory, as all packages will +# be symlinked there so they can all be included. +echo "💎 Extracting code samples" >&2 $ROSETTA \ - --compile \ - --verbose \ - --output samples.tabl.json \ - $rosetta_cache_opts \ - --directory packages/decdk \ - $(cat $jsii_pkgs_file) + --compile \ + --verbose \ + --output samples.tabl.json \ + $rosetta_cache_opts \ + --directory packages/decdk \ + $(cat $jsii_pkgs_file) + + +if $infuse; then + echo "💎 Infusing examples back into assemblies" >&2 + $ROSETTA infuse \ + --verbose \ + samples.tabl.json \ + $(cat $jsii_pkgs_file) + + echo "💎 Generating synthetic examples for the remainder" >&2 + time $scriptdir/../tools/@aws-cdk/generate-examples/bin/generate-examples \ + $genexample_cache_opts \ + --append-to samples.tabl.json \ + $(cat $jsii_pkgs_file) + +fi + +#---------------------------------------------------------------------- if [[ -d $(dirname $rosetta_cache_file) ]]; then # If the cache directory is available, copy the current tablet into it diff --git a/tools/@aws-cdk/generate-examples/.eslintrc.js b/tools/@aws-cdk/generate-examples/.eslintrc.js new file mode 100644 index 0000000000000..2658ee8727166 --- /dev/null +++ b/tools/@aws-cdk/generate-examples/.eslintrc.js @@ -0,0 +1,3 @@ +const baseConfig = require('@aws-cdk/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; +module.exports = baseConfig; diff --git a/tools/@aws-cdk/generate-examples/.gitignore b/tools/@aws-cdk/generate-examples/.gitignore new file mode 100644 index 0000000000000..405a05419d4b6 --- /dev/null +++ b/tools/@aws-cdk/generate-examples/.gitignore @@ -0,0 +1,10 @@ +*.js +*.js.map +*.d.ts +dist + +*.snk +!.eslintrc.js +!config/*.js +junit.xml +!jest.config.js diff --git a/tools/@aws-cdk/generate-examples/.npmignore b/tools/@aws-cdk/generate-examples/.npmignore new file mode 100644 index 0000000000000..9aca5ee7d9678 --- /dev/null +++ b/tools/@aws-cdk/generate-examples/.npmignore @@ -0,0 +1,14 @@ +# Don't include original .ts files when doing `npm pack` +*.ts +!*.d.ts +coverage +.nyc_output +*.tgz + +.LAST_BUILD +*.snk +.eslintrc.js + +# exclude cdk artifacts +**/cdk.out +junit.xml \ No newline at end of file diff --git a/tools/@aws-cdk/generate-examples/LICENSE b/tools/@aws-cdk/generate-examples/LICENSE new file mode 100644 index 0000000000000..28e4bdcec77ec --- /dev/null +++ b/tools/@aws-cdk/generate-examples/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2018-2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/tools/@aws-cdk/generate-examples/NOTICE b/tools/@aws-cdk/generate-examples/NOTICE new file mode 100644 index 0000000000000..5fc3826926b5b --- /dev/null +++ b/tools/@aws-cdk/generate-examples/NOTICE @@ -0,0 +1,2 @@ +AWS Cloud Development Kit (AWS CDK) +Copyright 2018-2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. diff --git a/tools/@aws-cdk/generate-examples/README.md b/tools/@aws-cdk/generate-examples/README.md new file mode 100644 index 0000000000000..a363579acebc0 --- /dev/null +++ b/tools/@aws-cdk/generate-examples/README.md @@ -0,0 +1,9 @@ +# Generate synthetic examples + +This tool is designed to run during the build. It will find all classes in the +JSII assembly that don't yet have any example code associated with them, and +will generate a synthetic example that shows how to instantiate the type. + +This is a method of last resort: we'd obviously prefer hand-written examples, +but this will make sure L1s will at least get something usable (which otherwise +would not have any examples at all). diff --git a/tools/@aws-cdk/generate-examples/bin/generate-examples b/tools/@aws-cdk/generate-examples/bin/generate-examples new file mode 100755 index 0000000000000..8950f29a1fcbf --- /dev/null +++ b/tools/@aws-cdk/generate-examples/bin/generate-examples @@ -0,0 +1,2 @@ +#!/usr/bin/env node +require('./generate-examples.js'); \ No newline at end of file diff --git a/tools/@aws-cdk/generate-examples/bin/generate-examples.ts b/tools/@aws-cdk/generate-examples/bin/generate-examples.ts new file mode 100644 index 0000000000000..d03462865612b --- /dev/null +++ b/tools/@aws-cdk/generate-examples/bin/generate-examples.ts @@ -0,0 +1,54 @@ +import * as yargs from 'yargs'; + +import { generateMissingExamples } from '../lib/generate-missing-examples'; + +async function main() { + const args = yargs + .usage('$0 [ASSEMBLY..]') + .option('cache-from', { + alias: 'C', + type: 'string', + describe: 'Reuse translations from the given tablet file', + requiresArg: true, + default: undefined, + }) + .option('append-to', { + alias: 'a', + type: 'string', + describe: 'Append translations to the given tablet file', + requiresArg: true, + default: undefined, + }) + .option('directory', { + alias: 'd', + type: 'string', + describe: 'Directory to run the compilation in (with dependencies set up)', + requiresArg: true, + default: undefined, + }) + .option('strict', { + alias: 's', + type: 'boolean', + describe: 'Whether to exit with an error if there are diagnostics', + default: false, + }) + .help() + .strict() + .showHelpOnFail(false) + .argv; + + const assemblyDirs = args._.map(x => `${x}`); + + await generateMissingExamples(assemblyDirs.length > 0 ? assemblyDirs : ['.'], { + cacheFromTablet: args['cache-from'], + appendToTablet: args['append-to'], + directory: args.directory, + strict: args.strict, + }); +} + +main().catch(e => { + // eslint-disable-next-line no-console + console.error(e); + process.exitCode = 1; +}); \ No newline at end of file diff --git a/tools/@aws-cdk/generate-examples/jest.config.js b/tools/@aws-cdk/generate-examples/jest.config.js new file mode 100644 index 0000000000000..c4f65f19ab3d7 --- /dev/null +++ b/tools/@aws-cdk/generate-examples/jest.config.js @@ -0,0 +1,10 @@ +const baseConfig = require('../cdk-build-tools/config/jest.config'); +module.exports = { + ...baseConfig, + coverageThreshold: { + global: { + ...baseConfig.coverageThreshold.global, + branches: 60, + }, + }, +}; diff --git a/tools/@aws-cdk/generate-examples/lib/assemblies.ts b/tools/@aws-cdk/generate-examples/lib/assemblies.ts new file mode 100644 index 0000000000000..5efa530849ea2 --- /dev/null +++ b/tools/@aws-cdk/generate-examples/lib/assemblies.ts @@ -0,0 +1,40 @@ +import * as path from 'path'; +import * as spec from '@jsii/spec'; +import * as fs from 'fs-extra'; +import { TypeScriptSnippet } from 'jsii-rosetta'; + +/** + * Replaces the file where the original assembly file *should* be found with a new assembly file. + * Recalculates the fingerprint of the assembly to avoid tampering detection. + */ +export async function replaceAssembly(assembly: spec.Assembly, directory: string): Promise { + const fileName = path.join(directory, '.jsii'); + await fs.writeJson(fileName, _fingerprint(assembly), { + encoding: 'utf8', + spaces: 2, + }); +} + +/** + * Replaces the old fingerprint with '***********'. + * + * @rmuller says fingerprinting is useless, as we do not actually check + * if an assembly is changed. Instead of keeping the old (wrong) fingerprint + * or spending extra time calculating a new fingerprint, we replace with '**********' + * that demonstrates the fingerprint has changed. + */ +function _fingerprint(assembly: spec.Assembly): spec.Assembly { + assembly.fingerprint = '*'.repeat(10); + return assembly; +} + +/** + * Insert an example into the docs of a type + */ +export function insertExample(example: TypeScriptSnippet, type: spec.Type): void { + if (type.docs) { + type.docs.example = example.visibleSource; + } else { + type.docs = { example: example.visibleSource }; + } +} diff --git a/tools/@aws-cdk/generate-examples/lib/code.ts b/tools/@aws-cdk/generate-examples/lib/code.ts new file mode 100644 index 0000000000000..ae5e75082be19 --- /dev/null +++ b/tools/@aws-cdk/generate-examples/lib/code.ts @@ -0,0 +1,83 @@ +import { Declaration } from './declaration'; +import { sortBy } from './utils'; + +/** + * Information on a segment of code and the declarations necessary to make the code valid. + */ +export class Code { + public static concatAll(...xs: Array): Code { + return xs.map(Code.force).reduce((a, b) => a.append(b), new Code('')); + } + + private static force(x: Code | string): Code { + if (x instanceof Code) { + return x; + } + return new Code(x); + } + + /** + * Construct a Code, consisting of a code fragment and a list of declarations that are meant + * to be rendered at the top of the code snippet. + */ + constructor(public readonly code: string, public readonly declarations: Declaration[] = []) { + } + + /** + * Appends and returns a new Code that safely combines two code fragments along + * with their declarations. + */ + public append(rhs: Code | string): Code { + if (typeof rhs === 'string') { + return new Code(this.code + rhs, this.declarations); + } + + return new Code(this.code + rhs.code, [...this.declarations, ...rhs.declarations]); + } + + public toString() { + return this.render(); + } + + private render(separator = '\n\n') { + return (this.renderDeclarations().join('\n') + separator + this.code).trimStart(); + } + + /** + * Renders variable declarations. Assumes that there are no duplicates in the declarations. + */ + public renderDeclarations(): string[] { + sortBy(this.declarations, (d) => d.sortKey); + const decs = deduplicate(this.declarations); + // Add separator only if necessary + const decStrings = [...decs.map((d) => d.render())]; + // only supports two groups and not more + for (let i = 0; i < decs.length-1; i++) { + if (decs[i].sortKey[0] !== decs[i+1].sortKey[0]) { + decStrings.splice(i+1, 0, ''); + break; + } + } + return decStrings; + } + + public renderCode(): string { + return this.code; + } +} + +/** + * Deduplicates a sorted array of Declarations. + */ +function deduplicate(declarations: Declaration[]): Declaration[] { + if (declarations.length === 0) { return declarations; } + + const newDeclarations: Declaration[] = []; + newDeclarations.push(declarations[0]); + for (let i = 1; i < declarations.length; i++) { + if (!declarations[i].equals(declarations[i-1])) { + newDeclarations.push(declarations[i]); + } + } + return newDeclarations; +} diff --git a/tools/@aws-cdk/generate-examples/lib/declaration.ts b/tools/@aws-cdk/generate-examples/lib/declaration.ts new file mode 100644 index 0000000000000..df4282db96096 --- /dev/null +++ b/tools/@aws-cdk/generate-examples/lib/declaration.ts @@ -0,0 +1,82 @@ +import * as reflect from 'jsii-reflect'; + +import { module, typeNamespacedName } from './module-utils'; + +export abstract class Declaration { + constructor(public readonly sortKey: Array) {} + + public abstract equals(rhs: Declaration): boolean; + public abstract render(): string; +} + +/** + * An Import statement that will get rendered at the top of the code snippet. + */ +export class Import extends Declaration { + public readonly importName: string; + public readonly moduleName: string; + public readonly submoduleName?: string; + public readonly type: reflect.Type; + + public constructor(type: reflect.Type) { + const { importName, moduleName, submoduleName } = module(type); + + super([0, moduleName]); + + this.importName = importName; + this.moduleName = moduleName; + this.submoduleName = submoduleName; + this.type = type; + } + + public equals(rhs: Declaration): boolean { + return this.render() === rhs.render(); + } + + public render(): string { + let what; + if (!this.submoduleName) { + what = `* as ${this.importName}`; + } else if (this.submoduleName === this.importName) { + what = `{ ${this.importName} }`; + } else { + what = `{ ${this.submoduleName} as ${this.importName} }`; + } + return `import ${what} from '${this.moduleName}';`; + } +} + +/** + * A declared constant that will be rendered at the top of the code snippet after the imports. + */ +export class Assumption extends Declaration { + public constructor(private readonly type: reflect.Type, private readonly name: string) { + super([1, name]); + } + + public equals(rhs: Declaration): boolean { + return this.render() === rhs.render(); + } + + public render(): string { + return `declare const ${this.name}: ${module(this.type).importName}.${typeNamespacedName(this.type)};`; + } +} + +/** + * An assumption for an 'any' time. This will be treated the same as 'Assumption' but with a + * different render result. + */ +export class AnyAssumption extends Declaration { + public constructor(private readonly name: string) { + super([1, name]); + } + + public equals(rhs: Declaration): boolean { + return this.render() === rhs.render(); + } + + public render(): string { + return `declare const ${this.name}: any;`; + } +} \ No newline at end of file diff --git a/tools/@aws-cdk/generate-examples/lib/generate-missing-examples.ts b/tools/@aws-cdk/generate-examples/lib/generate-missing-examples.ts new file mode 100644 index 0000000000000..3d3e6f74531bf --- /dev/null +++ b/tools/@aws-cdk/generate-examples/lib/generate-missing-examples.ts @@ -0,0 +1,155 @@ +/* eslint-disable no-console */ +import { promises as fs } from 'fs'; +import { Assembly, TypeSystem } from 'jsii-reflect'; + +// This import should come from @jsii/spec. Replace when that is possible. +import { LanguageTablet, RosettaTranslator, SnippetLocation, SnippetParameters, TypeScriptSnippet, typeScriptSnippetFromCompleteSource } from 'jsii-rosetta'; +import { insertExample, replaceAssembly } from './assemblies'; +import { generateAssignmentStatement } from './generate'; + +const COMMENT_WARNING = [ + '// The code below shows an example of how to instantiate this type.', + '// The values are placeholders you should change.', +]; + +export interface GenerateExamplesOptions { + readonly cacheFromTablet?: string; + readonly appendToTablet?: string; + readonly directory?: string; + readonly strict?: boolean; +} + +export async function generateMissingExamples(assemblyLocations: string[], options: GenerateExamplesOptions) { + const typesystem = new TypeSystem(); + + // load all assemblies into typesystem + const loadedAssemblies = await Promise.all(assemblyLocations.map(async (assemblyLocation) => { + if (!(await statFile(assemblyLocation))?.isDirectory) { + throw new Error(`Assembly location not a directory: ${assemblyLocation}`); + } + + return { assemblyLocation, assembly: await typesystem.load(assemblyLocation, { validate: false }) }; + })); + + const snippets = loadedAssemblies.flatMap(({ assembly }) => { + // Classes and structs + const documentableTypes = [ + ...assembly.classes.filter(c => !c.docs.example), + ...assembly.interfaces.filter(i => !i.docs.example && i.datatype), + ]; + + console.log(`${assembly.name}: ${documentableTypes.length} classes to document`); + if (documentableTypes.length === 0) { return []; } + + const failed = new Array(); + const generatedSnippets = documentableTypes.flatMap((classType) => { + const example = generateAssignmentStatement(classType); + if (!example) { + failed.push(classType.name); + return []; + } + + // To successfully compile, we need to generate the right 'Construct' import + const completeSource = [ + ...COMMENT_WARNING, + ...example.renderDeclarations(), + '', + '/// !hide', + correctConstructImport(assembly), + 'class MyConstruct extends Construct {', + 'constructor(scope: Construct, id: string) {', + 'super(scope, id);', + '/// !show', + example.renderCode(), + '/// !hide', + '} }', + ].join('\n').trimLeft(); + const location: SnippetLocation = { api: { api: 'type', fqn: classType.fqn }, field: { field: 'example' } }; + + const tsSnippet: TypeScriptSnippet = typeScriptSnippetFromCompleteSource( + completeSource, + location, + true, + { + [SnippetParameters.$COMPILATION_DIRECTORY]: options.directory ?? process.cwd(), + }); + + insertExample(tsSnippet, classType.spec); + return [tsSnippet]; + }); + + console.log([ + `${assembly.name}: annotated ${generatedSnippets.length} classes`, + ...(failed.length > 0 ? [`failed: ${failed.join(', ')}`] : []), + ].join(', ')); + + return generatedSnippets; + }); + + const rosetta = new RosettaTranslator({ + includeCompilerDiagnostics: true, + assemblies: loadedAssemblies.map(({ assembly }) => assembly.spec), + }); + + if (options.cacheFromTablet) { + await rosetta.loadCache(options.cacheFromTablet); + } + + // Will mutate the 'snippets' array + const { remaining } = rosetta.readFromCache(snippets); + + console.log(`Translating ${remaining.length} snippets`); + const results = await rosetta.translateAll(remaining); + if (results.diagnostics.length > 0) { + for (const diag of results.diagnostics) { + console.log(diag.formattedMessage); + } + + if (options.strict) { + process.exitCode = 1; + } + } + + // Copy everything from the rosetta tablet into our output tablet + if (options.appendToTablet) { + console.log(`Appending to ${options.appendToTablet}`); + const outputTablet = new LanguageTablet(); + if ((await statFile(options.appendToTablet)) !== undefined) { + await outputTablet.load(options.appendToTablet); + } + + for (const key of rosetta.tablet.snippetKeys) { + const snip = rosetta.tablet.tryGetSnippet(key); + if (snip) { + outputTablet.addSnippet(snip); + } + } + + await outputTablet.save(options.appendToTablet); + } + + console.log(`Saving ${loadedAssemblies.length} assemblies`); + await Promise.all((loadedAssemblies).map(({ assembly, assemblyLocation }) => + replaceAssembly(assembly.spec, assemblyLocation))); +} + +async function statFile(fileName: string) { + try { + return await fs.stat(fileName); + } catch (e) { + if (e.code === 'ENOENT') { return undefined; } + throw e; + } +} + +function correctConstructImport(assembly: Assembly) { + if (assembly.name === 'monocdk') { + return 'import { Construct } from "monocdk";'; + } + + if (assembly.dependencies.some(d => d.assembly.name === '@aws-cdk/core')) { + return 'import { Construct } from "@aws-cdk/core";'; + } + + return 'import { Construct } from "constructs";'; +} \ No newline at end of file diff --git a/tools/@aws-cdk/generate-examples/lib/generate.ts b/tools/@aws-cdk/generate-examples/lib/generate.ts new file mode 100644 index 0000000000000..728b6cd3d7f1d --- /dev/null +++ b/tools/@aws-cdk/generate-examples/lib/generate.ts @@ -0,0 +1,387 @@ +import * as spec from '@jsii/spec'; +import * as reflect from 'jsii-reflect'; +import { TypeSystem } from 'jsii-reflect'; + +import { Code } from './code'; +import { AnyAssumption, Assumption, Import } from './declaration'; +import { escapeIdentifier, typeReference } from './module-utils'; +import { sortBy } from './utils'; + +/** + * Special types that have a standard way of coming up with an example value + */ +const SPECIAL_TYPE_EXAMPLES: Record = { + '@aws-cdk/core.Duration': 'cdk.Duration.minutes(30)', + 'aws-cdk-lib.Duration': 'cdk.Duration.minutes(30)', +}; + +/** + * Context on the example that we are building. + * This object persists throughout the recursive call + * and provides the function with the same information + * on the typesystem and which types have already been + * rendered. This helps to prevent infinite recursion. + */ +class ExampleContext { + private readonly _typeSystem: TypeSystem; + private readonly _rendered: Set = new Set(); + + constructor(typeSystem: TypeSystem) { + this._typeSystem = typeSystem; + } + + public get typeSystem() { + return this._typeSystem; + } + + public get rendered() { + return this._rendered; + } +} + +export function generateAssignmentStatement(type: reflect.ClassType | reflect.InterfaceType): Code | undefined { + const context = new ExampleContext(type.system); + + if (type.isClassType()) { + const expression = exampleValueForClass(context, type, 0); + if (!expression) { return undefined; } + return Code.concatAll( + `const ${lowercaseFirstLetter(type.name)} = `, + expression, + ';', + ); + } + + if (type.isInterfaceType()) { + const expression = exampleValueForStruct(context, type, 0); + if (!expression) { return undefined; } + + return Code.concatAll( + `const ${lowercaseFirstLetter(type.name)}: `, + typeReference(type), + ' = ', + expression, + ';', + ); + } + + return undefined; +} + +function exampleValueForClass(context: ExampleContext, classType: reflect.ClassType, level: number): Code | undefined { + const staticFactoryMethods = getStaticFactoryMethods(classType); + const staticFactoryProperties = getStaticFactoryProperties(classType); + const initializer = getAccessibleConstructor(classType); + + if (initializer && initializer.parameters.length >= 3) { + return generateClassInstantiationExample(context, initializer, level); + } + + if (staticFactoryMethods.length >= 3) { + return generateStaticFactoryMethodExample(context, staticFactoryMethods[0], level); + } + + if (staticFactoryProperties.length >= 3) { + return generateStaticFactoryPropertyExample(staticFactoryProperties[0]); + } + + if (initializer) { + return generateClassInstantiationExample(context, initializer, level); + } + + if (staticFactoryMethods.length >= 1) { + return generateStaticFactoryMethodExample(context, staticFactoryMethods[0], level); + } + + if (staticFactoryProperties.length >= 1) { + return generateStaticFactoryPropertyExample(staticFactoryProperties[0]); + } + + return undefined; +} + +function getAccessibleConstructor(classType: reflect.ClassType): reflect.Initializer | undefined { + if (classType.abstract || !classType.initializer || classType.initializer.protected) { + return undefined; + } + return classType.initializer; +} + +/** + * Return the list of static methods on classtype that return either classtype or a supertype of classtype. + */ +function getStaticFactoryMethods(classType: reflect.ClassType): reflect.Method[] { + return classType.allMethods.filter(method => + method.static && extendsRef(classType, method.returns.type), + ); +} + +/** + * Return the list of static methods on classtype that return either classtype or a supertype of classtype. + */ +function getStaticFactoryProperties(classType: reflect.ClassType): reflect.Property[] { + return classType.allProperties.filter(prop => + prop.static && extendsRef(classType, prop.type), + ); +} + +function generateClassInstantiationExample(context: ExampleContext, initializer: reflect.Initializer, level: number): Code { + return Code.concatAll( + 'new ', + typeReference(initializer.parentType), + '(', + parameterList(context, initializer.parameters, level), + ')', + ); +} + +function parameterList(context: ExampleContext, parameters: reflect.Parameter[], level: number) { + const length = parameters.length; + return Code.concatAll( + ...parameters.map((p, i) => { + if (length - 1 === i) { + return exampleValueForParameter(context, p, i, level); + } else { + return exampleValueForParameter(context, p, i, level).append(', '); + } + }), + ); +} + +function generateStaticFactoryMethodExample( + context: ExampleContext, + staticFactoryMethod: reflect.Method, + level: number, +) { + return Code.concatAll( + typeReference(staticFactoryMethod.parentType), + '.', + staticFactoryMethod.name, + '(', + parameterList(context, staticFactoryMethod.parameters, level), + ')', + ); +} + +function generateStaticFactoryPropertyExample(staticFactoryProperty: reflect.Property) { + return Code.concatAll( + typeReference(staticFactoryProperty.parentType), + '.', + staticFactoryProperty.name, + ); +} + +/** + * Generate an example value of the given parameter. + */ +function exampleValueForParameter(context: ExampleContext, param: reflect.Parameter, position: number, level: number): Code { + if (param.name === 'scope' && position === 0) { + return new Code('this'); + } + + if (param.name === 'id' && position === 1) { + return new Code(`'My${param.parentType.name}'`); + } + if (param.optional) { + return new Code('/* all optional props */ ').append(exampleValue(context, param.type, param.name, level)); + } + return exampleValue(context, param.type, param.name, level); +} + +/** + * Generate an example value of the given type. + */ +function exampleValue(context: ExampleContext, typeRef: reflect.TypeReference, name: string, level: number): Code { + // Process primitive types, base case + if (typeRef.primitive !== undefined) { + switch (typeRef.primitive) { + case spec.PrimitiveType.String: + return new Code(`'${name}'`); + case spec.PrimitiveType.Number: + return new Code('123'); + case spec.PrimitiveType.Boolean: + return new Code('false'); + case spec.PrimitiveType.Date: + return new Code('new Date()'); + default: + return new Code(name, [new AnyAssumption(name)]); + } + } + + // Just pick the first type if it is a union type + if (typeRef.unionOfTypes !== undefined) { + const newType = getBaseUnionType(typeRef.unionOfTypes); + return exampleValue(context, newType, name, level); + } + // If its a collection create a collection of one element + if (typeRef.arrayOfType !== undefined) { + return Code.concatAll('[', exampleValue(context, typeRef.arrayOfType, name, level), ']'); + } + + if (typeRef.mapOfType !== undefined) { + return exampleValueForMap(context, typeRef.mapOfType, name, level); + } + + if (typeRef.fqn) { + const fqn = typeRef.fqn; + // See if we have information on this type in the assembly + const newType = context.typeSystem.findFqn(fqn); + + if (fqn in SPECIAL_TYPE_EXAMPLES) { + return new Code(SPECIAL_TYPE_EXAMPLES[fqn], [new Import(newType)]); + } + + if (newType.isEnumType()) { + return Code.concatAll( + typeReference(newType), + '.', + newType.members[0].name); + } + + // If this is struct and we're not already rendering it (recursion breaker), expand + if (isStructType(newType)) { + if (context.rendered.has(newType.fqn)) { + // Recursion breaker -- if we go by the default behavior end up saying something like: + // + // const myProperty = { + // stringProp: 'stringProp', + // deepProp: myProperty, // <-- value recursion! + // }; + // + // Which TypeScript's type analyzer can't automatically derive a type for. We need to + // annotate SOMETHING. A simple fix is to use a different variable name so the value + // isn't self-recursive. + + return addAssumedVariableDeclaration(newType, '_'); + } + + + context.rendered.add(newType.fqn); + const ret = exampleValueForStruct(context, newType, level); + context.rendered.delete(newType.fqn); + return ret; + } + + // For all other types we will assume you already have a variable of the appropriate type. + return addAssumedVariableDeclaration(newType); + } + + throw new Error('If this happens, then reflect.typeRefernce must have a new value'); +} + +function getBaseUnionType(types: reflect.TypeReference[]): reflect.TypeReference { + for (const newType of types) { + if (newType.fqn?.endsWith('.IResolvable')) { + continue; + } + return newType; + } + return types[0]; +} + +/** + * Add an assumption and import for a variable that will be declared as a constant. + * If the variable is an IXxx Interface, guess a possible implementation of that interface + * by checking if stripping the I results in an Xxx type that extends IXxx. + */ +function addAssumedVariableDeclaration(type: reflect.Type, suffix = ''): Code { + let newType = type; + if (type.isInterfaceType() && !type.datatype) { + // guess corresponding non-interface type if possible + newType = guessConcreteType(type); + } + const variableName = escapeIdentifier(lowercaseFirstLetter(stripLeadingI(newType.name))) + suffix; + return new Code(variableName, [new Assumption(newType, variableName), new Import(newType)]); +} + +/** + * Remove a leading 'I' from a name, if it's being followed by another capital letter + */ +function stripLeadingI(name: string) { + return name.replace(/^I([A-Z])/, '$1'); +} + +/** + * This function tries to guess the corresponding type to an IXxx Interface. + * If it does not find that this type exists, it will return the original type. + */ +function guessConcreteType(type: reflect.InterfaceType): reflect.Type { + const concreteClassName = type.name.substr(1); // Strip off the leading 'I' + + const parts = type.fqn.split('.'); + parts[parts.length - 1] = concreteClassName; + const newFqn = parts.join('.'); + + const newType = type.system.tryFindFqn(newFqn); + return newType && newType.extends(type) ? newType : type; +} + +/** + * Helper function to generate an example value for a map. + */ +function exampleValueForMap(context: ExampleContext, map: reflect.TypeReference, name: string, level: number): Code { + return Code.concatAll( + '{\n', + new Code(`${tab(level + 1)}${name}Key: `).append(exampleValue(context, map, name, level + 1)).append(',\n'), + `${tab(level)}}`, + ); +} + +/** + * Helper function to generate an example value for a struct. + */ +function exampleValueForStruct(context: ExampleContext, struct: reflect.InterfaceType, level: number): Code { + if (struct.allProperties.length === 0) { + return new Code('{ }'); + } + + const properties = [...struct.allProperties]; // Make a copy that we can sort + sortBy(properties, (p) => [p.optional ? 1 : 0, p.name]); + + const renderedProperties = properties.map((p) => + Code.concatAll( + `${tab(level + 1)}${p.name}: `, + exampleValue(context, p.type, p.name, level + 1), + ',\n', + ), + ); + + // Add an empty line between required and optional properties + for (let i = 0; i < properties.length - 1; i++) { + if (properties[i].optional !== properties[i + 1].optional) { + renderedProperties.splice(i + 1, 0, new Code(`\n${tab(level+1)}// the properties below are optional\n`)); + break; + } + } + + return Code.concatAll( + '{\n', + ...renderedProperties, + `${tab(level)}}`, + ); +} + +/** + * Returns whether the given type represents a struct + */ +function isStructType(type: reflect.Type): type is reflect.InterfaceType { + return type.isInterfaceType() && type.datatype; +} + +function extendsRef(subtype: reflect.ClassType, supertypeRef: reflect.TypeReference): boolean { + if (!supertypeRef.fqn) { + // Not a named type, can never extend + return false; + } + + const superType = subtype.system.findFqn(supertypeRef.fqn); + return subtype.extends(superType); +} + +function lowercaseFirstLetter(str: string): string { + return str.charAt(0).toLowerCase() + str.slice(1); +} + +function tab(level: number): string { + return ' '.repeat(level); +} \ No newline at end of file diff --git a/tools/@aws-cdk/generate-examples/lib/index.ts b/tools/@aws-cdk/generate-examples/lib/index.ts new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/tools/@aws-cdk/generate-examples/lib/module-utils.ts b/tools/@aws-cdk/generate-examples/lib/module-utils.ts new file mode 100644 index 0000000000000..e3151ab51c88e --- /dev/null +++ b/tools/@aws-cdk/generate-examples/lib/module-utils.ts @@ -0,0 +1,115 @@ +import * as reflect from 'jsii-reflect'; +import { Code } from './code'; +import { Import } from './declaration'; + +/** + * Customary module import names that differ from what would be automatically generated. + */ +const SPECIAL_PACKAGE_ROOT_IMPORT_NAMES: Record = { + 'aws-cdk-lib': 'cdk', + '@aws-cdk/core': 'cdk', + '@aws-cdk/aws-applicationautoscaling': 'appscaling', + '@aws-cdk/aws-elasticloadbalancing': 'elb', + '@aws-cdk/aws-elasticloadbalancingv2': 'elbv2', +}; + +const SPECIAL_NAMESPACE_IMPORT_NAMES: Record = { + 'aws-cdk-lib.aws_applicationautoscaling': 'appscaling', + 'aws-cdk-lib.aws_elasticloadbalancing': 'elb', + 'aws-cdk-lib.aws_elasticloadbalancingv2': 'elbv2', +}; + +interface ImportedModule { + readonly importName: string; + readonly moduleName: string; + readonly submoduleName?: string; +} + +/** + * Parses the given type for human-readable information on the module + * that the type is from. Meant to serve as a single source of truth + * for parsing the type for module information. + */ +export function module(type: reflect.Type): ImportedModule { + const parts = analyzeTypeName(type); + + if (parts.submoduleNameParts.length > 0) { + const specialNameKey = [parts.assemblyName, ...parts.submoduleNameParts].join('.'); + + const importName = SPECIAL_NAMESPACE_IMPORT_NAMES[specialNameKey] ?? parts.submoduleNameParts.join('.'); + return { + importName: escapeIdentifier(importName.replace(/^aws_/g, '').replace(/[^a-z0-9_]/g, '_')), + moduleName: parts.assemblyName, + submoduleName: parts.submoduleNameParts.join('.'), + }; + } + + // Split '@aws-cdk/aws-s3' into ['@aws-cdk', 'aws-s3'] + const slashParts = type.assembly.name.split('/'); + const nonNamespacedPart = SPECIAL_PACKAGE_ROOT_IMPORT_NAMES[parts.assemblyName] ?? slashParts[1] ?? slashParts[0]; + return { + importName: escapeIdentifier(nonNamespacedPart.replace(/^aws-/g, '').replace(/[^a-z0-9_]/g, '_')), + moduleName: type.assembly.name, + }; +} + +/** + * Namespaced name inside a module + */ +export function typeNamespacedName(type: reflect.Type): string { + const parts = analyzeTypeName(type); + + return [ + ...parts.namespaceNameParts, + parts.simpleName, + ].join('.'); +} + +const KEYWORDS = ['function', 'default']; + +export function escapeIdentifier(ident: string): string { + return KEYWORDS.includes(ident) ? `${ident}_` : ident; +} + +export function moduleReference(type: reflect.Type) { + const imp = new Import(type); + return new Code(imp.importName, [imp]); +} + +export function typeReference(type: reflect.Type) { + return Code.concatAll( + moduleReference(type), + '.', + typeNamespacedName(type)); +} + +/** + * A type name consists of 4 parts which are all treated differently + */ +interface TypeNameParts { + readonly assemblyName: string; + readonly submoduleNameParts: string[]; + readonly namespaceNameParts: string[]; + readonly simpleName: string; +} + +function analyzeTypeName(type: reflect.Type): TypeNameParts { + // Need to divide the namespace into submodule and non-submodule + + // For type 'asm.b.c.d.Type' contains ['asm', 'b', 'c', 'd'] + const nsParts = type.fqn.split('.').slice(0, -1); + + const moduleFqns = new Set(type.assembly.allSubmodules.map((s) => s.fqn)); + + let split = nsParts.length; + while (split > 1 && !moduleFqns.has(nsParts.slice(0, split).join('.'))) { + split--; + } + + return { + assemblyName: type.assembly.name, + submoduleNameParts: nsParts.slice(1, split), + namespaceNameParts: nsParts.slice(split, nsParts.length), + simpleName: type.name, + }; +} \ No newline at end of file diff --git a/tools/@aws-cdk/generate-examples/lib/utils.ts b/tools/@aws-cdk/generate-examples/lib/utils.ts new file mode 100644 index 0000000000000..526664f645a22 --- /dev/null +++ b/tools/@aws-cdk/generate-examples/lib/utils.ts @@ -0,0 +1,28 @@ +export function sortBy(xs: A[], keyFn: (x: A) => Array) { + return xs.sort((a, b) => { + const aKey = keyFn(a); + const bKey = keyFn(b); + + for (let i = 0; i < Math.min(aKey.length, bKey.length); i++) { + // Compare aKey[i] to bKey[i] + const av = aKey[i]; + const bv = bKey[i]; + + if (av === bv) { continue; } + + if (typeof av !== typeof bv) { + throw new Error(`Type of sort key ${JSON.stringify(aKey)} not same as ${JSON.stringify(bKey)}`); + } + + if (typeof av === 'number' && typeof bv === 'number') { + return av - bv; + } + + if (typeof av === 'string' && typeof bv === 'string') { + return av.localeCompare(bv); + } + } + + return aKey.length - bKey.length; + }); +} \ No newline at end of file diff --git a/tools/@aws-cdk/generate-examples/package.json b/tools/@aws-cdk/generate-examples/package.json new file mode 100644 index 0000000000000..e2be3f5d8d8c2 --- /dev/null +++ b/tools/@aws-cdk/generate-examples/package.json @@ -0,0 +1,51 @@ +{ + "name": "@aws-cdk/generate-examples", + "version": "0.0.0", + "private": true, + "description": "Generate missing examples", + "main": "lib/index.js", + "types": "lib/index.d.ts", + "repository": { + "type": "git", + "url": "git://github.com/aws/aws-cdk" + }, + "pkglint": { + "ignore": true + }, + "bin": { + "generate-examples": "bin/generate-examples" + }, + "scripts": { + "build": "tsc -b && chmod +x bin/generate-examples", + "test": "jest", + "build+test": "npm run build && npm test", + "build+test+package": "npm run build && npm test", + "watch": "tsc -b -w", + "lint": "tsc -b && eslint . --ext=.ts" + }, + "keywords": [ + "aws", + "cdk" + ], + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com" + }, + "license": "Apache-2.0", + "devDependencies": { + "@types/jest": "^26.0.24", + "@types/yargs": "^15.0.14", + "jest": "^26.6.3", + "typescript": "~3.9.10" + }, + "nozem": { + "ostools": ["chmod", "cp"] + }, + "dependencies": { + "@jsii/spec": "1.44.0", + "jsii-reflect": "1.44.0", + "jsii-rosetta": "1.44.0", + "fs-extra": "^9.1.0", + "yargs": "^16.2.0" + } +} diff --git a/tools/@aws-cdk/generate-examples/test/code.test.ts b/tools/@aws-cdk/generate-examples/test/code.test.ts new file mode 100644 index 0000000000000..114cc8858faa1 --- /dev/null +++ b/tools/@aws-cdk/generate-examples/test/code.test.ts @@ -0,0 +1,51 @@ +import { Code } from '../lib/code'; +import { AnyAssumption } from '../lib/declaration'; + +test('created successfully', () => { + const code = new Code('hello world', [new AnyAssumption('from Mars')]); + + expect(code.code).toEqual('hello world'); + expect(code.declarations.length).toEqual(1); + expect(code.declarations[0]).toBeInstanceOf(AnyAssumption); +}); + +test('can append', () => { + const code = new Code('hello ', [new AnyAssumption('from Mars')]) + .append('world') + .append(new Code('.', [new AnyAssumption('from Jupiter')])); + + expect(code.code).toEqual('hello world.'); + expect(code.declarations.length).toEqual(2); +}); + +test('concatAll works as expected', () => { + const code = Code.concatAll( + 'hello', + new Code(' world', [new AnyAssumption('from Mars')]), + ); + + expect(code.code).toEqual('hello world'); + expect(code.declarations.length).toEqual(1); +}); + +describe('code declarations on toString', () => { + test('deduplicated', () => { + const code = new Code('', [ + new AnyAssumption('duplicate'), + new AnyAssumption('duplicate'), + new AnyAssumption('unique'), + ]); + + expect(code.toString()).toEqual('declare const duplicate: any;\ndeclare const unique: any;\n\n'); + }); + + test('sorted', () => { + const code = new Code('', [ + new AnyAssumption('third'), + new AnyAssumption('second'), + new AnyAssumption('first'), + ]); + + expect(code.toString()).toEqual('declare const first: any;\ndeclare const second: any;\ndeclare const third: any;\n\n'); + }); +}); diff --git a/tools/@aws-cdk/generate-examples/test/generate-missing-examples.test.ts b/tools/@aws-cdk/generate-examples/test/generate-missing-examples.test.ts new file mode 100644 index 0000000000000..963b98bdc586b --- /dev/null +++ b/tools/@aws-cdk/generate-examples/test/generate-missing-examples.test.ts @@ -0,0 +1,60 @@ +import * as path from 'path'; + +import { LanguageTablet, TargetLanguage } from 'jsii-rosetta'; +import { generateMissingExamples } from '../lib/generate-missing-examples'; +import { DUMMY_ASSEMBLY_TARGETS, AssemblyFixture } from './testutil'; + +test('test end-to-end and translation to Python', async () => { + const assembly = await AssemblyFixture.fromSource( + { + 'index.ts': ` + export interface MyClassProps { + readonly someString: string; + readonly someNumber: number; + } + + export class MyClass { + constructor(value: string, props: MyClassProps) { + Array.isArray(value); + Array.isArray(props); + } + } + `, + }, + { + name: 'my_assembly', + jsii: DUMMY_ASSEMBLY_TARGETS, + }, + ); + try { + const outputTablet = path.join(assembly.directory, 'test.tbl.json'); + + await generateMissingExamples([ + assembly.directory, + ], { + directory: assembly.directory, + appendToTablet: outputTablet, + }); + + const tablet = await LanguageTablet.fromFile(outputTablet); + + const pythons = tablet.snippetKeys + .map((key) => tablet.tryGetSnippet(key)!) + .map((snip) => snip.get(TargetLanguage.PYTHON)?.source); + + const classInstantiation = pythons.find((s) => s?.includes('= my_assembly.MyClass(')); + + expect(classInstantiation).toEqual([ + '# The code below shows an example of how to instantiate this type.', + '# The values are placeholders you should change.', + 'import my_assembly as my_assembly', + '', + 'my_class = my_assembly.MyClass(\"value\",', + ' some_number=123,', + ' some_string=\"someString\"', + ')', + ].join('\n')); + } finally { + await assembly.cleanup(); + } +}); \ No newline at end of file diff --git a/tools/@aws-cdk/generate-examples/test/generate.test.ts b/tools/@aws-cdk/generate-examples/test/generate.test.ts new file mode 100644 index 0000000000000..b15e3e8a31d04 --- /dev/null +++ b/tools/@aws-cdk/generate-examples/test/generate.test.ts @@ -0,0 +1,323 @@ +import * as reflect from 'jsii-reflect'; + +import { generateAssignmentStatement } from '../lib/generate'; +import { AssemblyFixture, DUMMY_ASSEMBLY_TARGETS, MultipleSources } from './testutil'; + +describe('generateClassAssignment ', () => { + test('generates example for class with static methods', + expectedDocTest({ + sources: { + 'index.ts': ` + export class ClassA { + public static firstMethod() { return new ClassA(); } + public static secondMethod() { return new ClassA(); } + public static thirdMethod() { return new ClassA(); } + private constructor() {} + }`, + }, + typeName: 'ClassA', + expected: [ + 'import * as my_assembly from \'my_assembly\';', + '', + 'const classA = my_assembly.ClassA.firstMethod();', + ], + }), + ); + + test('generates example for class with static properties', + expectedDocTest({ + sources: { + 'index.ts': ` + export class ClassA { + public static readonly FIRST_PROPERTY = new ClassA(); + public static readonly SECOND_PROPERTY = new ClassA(); + public static readonly THIRD_PROPERTY = new ClassA(); + private constructor() {} + }`, + }, + typeName: 'ClassA', + expected: [ + 'import * as my_assembly from \'my_assembly\';', + '', + 'const classA = my_assembly.ClassA.FIRST_PROPERTY;', + ], + }), + ); + + test('generates example for class instantiation', + expectedDocTest({ + sources: { + 'index.ts': ` + export class ClassA { + public constructor(public readonly a: string, public readonly b: string) {} + }`, + }, + typeName: 'ClassA', + expected: [ + 'import * as my_assembly from \'my_assembly\';', + '', + 'const classA = new my_assembly.ClassA(\'a\', \'b\');', + ], + }), + ); + + test('generates example for more complicated class instantiation', + expectedDocTest({ + sources: { + 'index.ts': ` + export class ClassA { + public constructor(public readonly scope: string, public readonly id: string, public readonly props: ClassAProps) {} + } + export interface ClassAProps { + readonly prop1: number, + readonly prop2: IProperty, + readonly prop3: string[], + readonly prop4: number | string, + readonly prop5?: any, + readonly prop6?: boolean, + readonly prop7?: { [key: string]: string }, + } + export interface IProperty { + readonly prop: string, + } + export class Property implements IProperty { + readonly prop: string; + public constructor() { this.prop = 'a'; } + } + `, + }, + typeName: 'ClassA', + expected: [ + 'import * as my_assembly from \'my_assembly\';', + '', + 'declare const prop5: any;', + 'declare const property: my_assembly.Property;', + '', + 'const classA = new my_assembly.ClassA(this, \'MyClassA\', {', + ' prop1: 123,', + ' prop2: property,', + ' prop3: [\'prop3\'],', + ' prop4: \'prop4\',', + '', + ' // the properties below are optional', + ' prop5: prop5,', + ' prop6: false,', + ' prop7: {', + ' prop7Key: \'prop7\',', + ' },', + '});', + ], + }), + ); + + test('returns undefined if class has no statics and private initializer', async () => { + const assembly = await AssemblyFixture.fromSource( + { + 'index.ts': ` + export class ClassA { + private constructor() {} + }`, + }, + { + name: 'my_assembly', + jsii: DUMMY_ASSEMBLY_TARGETS, + }, + ); + + const ts = new reflect.TypeSystem(); + await ts.load(assembly.directory); + + const type = ts.findClass('my_assembly.ClassA'); + expect(generateAssignmentStatement(type)).toBeUndefined(); + + await assembly.cleanup(); + }); + + test('optional properties are added in the correct spot', + expectedDocTest({ + sources: { + 'index.ts': ` + export class ClassA { + public constructor(public readonly scope: string, public readonly id: string, public readonly props: ClassAProps) {} + } + export interface ClassAProps { + readonly prop1: number, + readonly prop2?: number, + } + `, + }, + typeName: 'ClassA', + expected: [ + 'import * as my_assembly from \'my_assembly\';', + '', + 'const classA = new my_assembly.ClassA(this, \'MyClassA\', {', + ' prop1: 123,', + '', + ' // the properties below are optional', + ' prop2: 123,', + '});', + ], + }), + ); + + test( + 'comment added when all properties are optional', + expectedDocTest({ + sources: { + 'index.ts': ` + export class ClassA { + public constructor(public readonly scope: string, public readonly id: string, public readonly props: ClassAProps = {}) {} + } + export interface ClassAProps { + readonly prop1?: number, + readonly prop2?: number, + } + `, + }, + typeName: 'ClassA', + expected: [ + 'import * as my_assembly from \'my_assembly\';', + '', + 'const classA = new my_assembly.ClassA(this, \'MyClassA\', /* all optional props */ {', + ' prop1: 123,', + ' prop2: 123,', + '});', + ], + }), + ); +}); + +test( + 'generate example for struct', + expectedDocTest({ + sources: { + 'index.ts': ` + export interface SomeStruct { + readonly required: string; + readonly optional?: number; + } + `, + }, + typeName: 'SomeStruct', + expected: [ + 'import * as my_assembly from \'my_assembly\';', + '', + 'const someStruct: my_assembly.SomeStruct = {', + ' required: \'required\',', + '', + ' // the properties below are optional', + ' optional: 123,', + '};', + ], + }), +); + +test( + 'rendering an enum value', + expectedDocTest({ + sources: { + 'index.ts': ` + export interface SomeStruct { + readonly someEnum: MyEnum; + } + export enum MyEnum { + VALUE1 = 1, + VALUE2 = 2, + } + `, + }, + typeName: 'SomeStruct', + expected: [ + 'import * as my_assembly from \'my_assembly\';', + '', + 'const someStruct: my_assembly.SomeStruct = {', + ' someEnum: my_assembly.MyEnum.VALUE1,', + '};', + ], + }), +); + +test('rendering types in namespaces', expectedDocTest({ + sources: { + // This merges a class and a namespace (making the struct appear + // namespaced inside the class -- we do this for L1 structs) + 'index.ts': ` + export class SomeClass { + constructor(props: SomeClass.SomeStruct) { + Array.isArray(props); + } + } + + export namespace SomeClass { + export interface SomeStruct { + readonly someEnum: MyEnum; + } + export enum MyEnum { + VALUE1 = 1, + VALUE2 = 2, + } + } + `, + }, + typeName: 'SomeClass.SomeStruct', + expected: [ + 'import * as my_assembly from \'my_assembly\';', + '', + 'const someStruct: my_assembly.SomeClass.SomeStruct = {', + ' someEnum: my_assembly.SomeClass.MyEnum.VALUE1,', + '};', + ], +})); + +test('rendering types in submodules', expectedDocTest({ + sources: { + 'index.ts': 'export * as sub from \'./other\';', + 'other.ts': ` + export interface SomeStruct { + readonly someEnum: MyEnum; + } + export enum MyEnum { + VALUE1 = 1, + VALUE2 = 2, + } + `, + }, + typeName: 'sub.SomeStruct', + expected: [ + 'import { sub } from \'my_assembly\';', + '', + 'const someStruct: sub.SomeStruct = {', + ' someEnum: sub.MyEnum.VALUE1,', + '};', + ], +})); + +interface DocTest { + readonly sources: MultipleSources; + readonly typeName: string; + readonly expected: string[]; +} + +function expectedDocTest(testParams: DocTest) { + return async () => { + const assembly = await AssemblyFixture.fromSource( + testParams.sources, + { + name: 'my_assembly', + jsii: DUMMY_ASSEMBLY_TARGETS, + }, + ); + try { + const ts = new reflect.TypeSystem(); + await ts.load(assembly.directory); + + const type = ts.findFqn(`my_assembly.${testParams.typeName}`); + if (!type.isClassType() && !type.isInterfaceType()) { + throw new Error('Expecting class or interface'); + } + expect(generateAssignmentStatement(type)?.toString()?.split('\n')).toEqual(testParams.expected); + } finally { + await assembly.cleanup(); + } + }; +} \ No newline at end of file diff --git a/tools/@aws-cdk/generate-examples/test/module-utils.test.ts b/tools/@aws-cdk/generate-examples/test/module-utils.test.ts new file mode 100644 index 0000000000000..ecb9bea9326ce --- /dev/null +++ b/tools/@aws-cdk/generate-examples/test/module-utils.test.ts @@ -0,0 +1,161 @@ +import * as reflect from 'jsii-reflect'; + +import { module } from '../lib/module-utils'; +import { AssemblyFixture, DUMMY_ASSEMBLY_TARGETS } from './testutil'; + +describe('v1 names are correct: ', () => { + test('core', async () => { + // GIVEN + const mod = '@aws-cdk/core'; + const { ts, assembly } = await v1BuildAssemblyHelper(mod); + + // THEN + const { importName, moduleName } = module(ts.findClass(`${mod}.ClassA`)); + expect(importName).toEqual('cdk'); + expect(moduleName).toEqual(mod); + + await assembly.cleanup(); + }); + + test('special package root', async () => { + // GIVEN + const mod = '@aws-cdk/aws-elasticloadbalancingv2'; + const { ts, assembly } = await v1BuildAssemblyHelper(mod); + + // THEN + const { importName, moduleName } = module(ts.findClass(`${mod}.ClassA`)); + expect(importName).toEqual('elbv2'); + expect(moduleName).toEqual(mod); + + await assembly.cleanup(); + }); + + test('with "aws-"', async () => { + // GIVEN + const mod = '@aws-cdk/aws-s3'; + const { ts, assembly } = await v1BuildAssemblyHelper(mod); + + // THEN + const { importName, moduleName } = module(ts.findClass(`${mod}.ClassA`)); + expect(importName).toEqual('s3'); + expect(moduleName).toEqual(mod); + + await assembly.cleanup(); + }); + + test('without "aws-"', async () => { + // GIVEN + const mod = '@aws-cdk/pipelines'; + const { ts, assembly } = await v1BuildAssemblyHelper(mod); + + // THEN + const { importName, moduleName } = module(ts.findClass(`${mod}.ClassA`)); + expect(importName).toEqual('pipelines'); + expect(moduleName).toEqual(mod); + + await assembly.cleanup(); + }); +}); + +describe('v2 names are correct: ', () => { + test('core', async () => { + // GIVEN + const mod = 'aws-cdk-lib'; + const { ts, assembly } = await v2BuildAssemblyHelper(mod); + + // THEN + const { importName, moduleName } = module(ts.findClass(`${mod}.ClassA`)); + expect(importName).toEqual('cdk'); + expect(moduleName).toEqual(mod); + + await assembly.cleanup(); + }); + + test('special namespace', async () => { + // GIVEN + const mod = 'aws-cdk-lib/aws_elasticloadbalancingv2'; + const { ts, assembly } = await v2BuildAssemblyHelper(mod); + + // THEN + expect(module( + ts.findClass('aws-cdk-lib.aws_elasticloadbalancingv2.ClassB'), + )).toEqual({ + moduleName: 'aws-cdk-lib', + submoduleName: 'aws_elasticloadbalancingv2', + importName: 'elbv2', + }); + + await assembly.cleanup(); + }); + + test('with "aws_"', async () => { + // GIVEN + const mod = 'aws-cdk-lib/aws_s3'; + const { ts, assembly } = await v2BuildAssemblyHelper(mod); + + // THEN + expect(module(ts.findClass('aws-cdk-lib.aws_s3.ClassB'))).toEqual({ + moduleName: 'aws-cdk-lib', + submoduleName: 'aws_s3', + importName: 's3', + }); + + await assembly.cleanup(); + }); + + test('without "aws_"', async () => { + // GIVEN + const mod = 'aws-cdk-lib/pipelines'; + const { ts, assembly } = await v2BuildAssemblyHelper(mod); + + // THEN + expect(module(ts.findClass('aws-cdk-lib.pipelines.ClassB'))).toEqual({ + moduleName: 'aws-cdk-lib', + submoduleName: 'pipelines', + importName: 'pipelines', + }); + + await assembly.cleanup(); + }); +}); + +async function v1BuildAssemblyHelper(name: string) { + const assembly = await AssemblyFixture.fromSource( + { + 'index.ts': ` + export class ClassA { } + `, + }, + { + name, + jsii: DUMMY_ASSEMBLY_TARGETS, + }, + ); + + const ts = new reflect.TypeSystem(); + await ts.load(assembly.directory); + return { ts, assembly }; +} + +async function v2BuildAssemblyHelper(name: string) { + const [assemblyName, submoduleName] = name.split('/'); + const assembly = await AssemblyFixture.fromSource( + { + 'index.ts': ` + export * as ${submoduleName ?? 'dummy'} from "./submod"; + export class ClassA { } + `, + 'submod.ts': ` + export class ClassB { } + `, + }, + { + name: assemblyName, + jsii: DUMMY_ASSEMBLY_TARGETS, + }, + ); + + const ts = new reflect.TypeSystem(); + await ts.load(assembly.directory); + return { ts, assembly }; +} \ No newline at end of file diff --git a/tools/@aws-cdk/generate-examples/test/testutil.ts b/tools/@aws-cdk/generate-examples/test/testutil.ts new file mode 100644 index 0000000000000..f4cee0fe3bd5f --- /dev/null +++ b/tools/@aws-cdk/generate-examples/test/testutil.ts @@ -0,0 +1,65 @@ +import * as os from 'os'; +import * as path from 'path'; +import * as fs from 'fs-extra'; +import { PackageInfo, compileJsiiForTest } from 'jsii'; + +export type MultipleSources = { [key: string]: string; 'index.ts': string }; + +export class AssemblyFixture { + public static async fromSource( + source: string | MultipleSources, + packageInfo: Partial & { name: string }, + ) { + const { assembly, files } = await compileJsiiForTest(source, (pi) => { + Object.assign(pi, packageInfo); + }); + + // The following is silly, however: the helper has compiled the given source to + // an assembly, and output files, and then removed their traces from disk. + // But for the purposes of Rosetta, we need those files back on disk. So write + // them back out again >_< + // + // In fact we will drop them in 'node_modules/' so they can be imported + // as if they were installed. + const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'jsii-rosetta')); + const modDir = path.join(tmpDir, 'node_modules', packageInfo.name); + await fs.ensureDir(modDir); + + await fs.writeJSON(path.join(modDir, '.jsii'), assembly); + await fs.writeJSON(path.join(modDir, 'package.json'), { + name: packageInfo.name, + jsii: packageInfo.jsii, + }); + for (const [fileName, fileContents] of Object.entries(files)) { + // eslint-disable-next-line no-await-in-loop + await fs.writeFile(path.join(modDir, fileName), fileContents); + } + + return new AssemblyFixture(modDir); + } + + private constructor(public readonly directory: string) {} + + public async cleanup() { + await fs.remove(this.directory); + } +} + +export const DUMMY_ASSEMBLY_TARGETS = { + dotnet: { + namespace: 'Example.Test.Demo', + packageId: 'Example.Test.Demo', + }, + go: { moduleName: 'example.test/demo' }, + java: { + maven: { + groupId: 'example.test', + artifactId: 'demo', + }, + package: 'example.test.demo', + }, + python: { + distName: 'example-test.demo', + module: 'example_test_demo', + }, +}; diff --git a/tools/@aws-cdk/generate-examples/test/utils.test.ts b/tools/@aws-cdk/generate-examples/test/utils.test.ts new file mode 100644 index 0000000000000..51913f88869c5 --- /dev/null +++ b/tools/@aws-cdk/generate-examples/test/utils.test.ts @@ -0,0 +1,7 @@ +import { sortBy } from '../lib/utils'; + +test('sortBy sorts successfully', () => { + const alist = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + sortBy(alist, (i) => [i*-1]); + expect(alist).toEqual([10, 9, 8, 7, 6, 5, 4, 3, 2, 1]); +}); \ No newline at end of file diff --git a/tools/@aws-cdk/generate-examples/tsconfig.json b/tools/@aws-cdk/generate-examples/tsconfig.json new file mode 100644 index 0000000000000..c321dc85b07b0 --- /dev/null +++ b/tools/@aws-cdk/generate-examples/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2018", + "module": "commonjs", + "lib": ["es2018"], + "strict": true, + "alwaysStrict": true, + "declaration": true, + "inlineSourceMap": true, + "inlineSources": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "resolveJsonModule": true, + "composite": true, + "incremental": true, + "experimentalDecorators": true + }, + "include": ["**/*.ts"] +} \ No newline at end of file diff --git a/tools/@aws-cdk/pkglint/lib/rules.ts b/tools/@aws-cdk/pkglint/lib/rules.ts index eda6436884d77..dab684878d98f 100644 --- a/tools/@aws-cdk/pkglint/lib/rules.ts +++ b/tools/@aws-cdk/pkglint/lib/rules.ts @@ -1653,7 +1653,7 @@ export class NoExperimentalDependents extends ValidationRule { ['@aws-cdk/aws-apigatewayv2-authorizers', ['@aws-cdk/aws-apigatewayv2']], ['@aws-cdk/aws-events-targets', ['@aws-cdk/aws-kinesisfirehose']], ['@aws-cdk/aws-kinesisfirehose-destinations', ['@aws-cdk/aws-kinesisfirehose']], - ['@aws-cdk/aws-iot-actions', ['@aws-cdk/aws-iot']], + ['@aws-cdk/aws-iot-actions', ['@aws-cdk/aws-iot', '@aws-cdk/aws-kinesisfirehose']], ]); private readonly excludedModules = ['@aws-cdk/cloudformation-include'];