diff --git a/.github/workflows/README.md b/.github/workflows/README.md new file mode 100644 index 0000000000000..9d54ed7236ef7 --- /dev/null +++ b/.github/workflows/README.md @@ -0,0 +1,47 @@ +# AWS CDK GitHub Actions + +These workflows and actions are configured in the AWS CDK GitHub repository. + +## Pull Request Triggered + +### Auto Approve +[auto-approve.yml](auto-approve.yml): Approves merging PRs with the +`pr/auto-approve` label. +Owner: Core CDK team + +### PR Linter +[pr-linter.yml](pr-linter.yml): Runs `tools/@aws-cdk-prlint` on each PR to +check for correctness. +Owner: Core CDK team + +### v2-main PR automation +[v2-pull-request.yml](v2-pull-request.yml): Runs `pkglint` on merge forward PRs +and commits the results. +Owner: Core CDK team + +### Label Assigner +[issue-label-assign.yml](issue-label-assign.yml): Github action for automatically adding labels and/or setting assignees when an Issue or PR is opened or edited based on user-defined Area +Owner: CDK support team + +## Issue Triggered + +### Closed Issue Message +[closed-issue-message.yml](closed-issue-message.yml): Adds a reminder message +to issues that are closed. +Owner: CDK support team + +### Label Assigner +[issue-label-assign.yml](issue-label-assign.yml): Github action for automatically adding labels and/or setting assignees when an Issue or PR is opened or edited based on user-defined Area +Owner: CDK support team + +## Scheduled Actions + +### Issue Lifecycle Handling +[close-stale-issues.yml](close-stale-issues.yml): Handles labeling issues and +PRs with `closing-soon`, `response-requested`, etc. +Owner: CDK support team + +### Yarn Upgrader +[yarn-upgrade.yml](yarn-upgrade.yml): Upgrades yarn dependencies and creates a +patch file for downloading. +Owner: Core CDK team \ No newline at end of file diff --git a/.github/workflows/close-stale-issues.yml b/.github/workflows/close-stale-issues.yml index 487a095e0c372..ee427df90ef79 100644 --- a/.github/workflows/close-stale-issues.yml +++ b/.github/workflows/close-stale-issues.yml @@ -4,7 +4,7 @@ name: "Close Stale Issues" on: workflow_dispatch: schedule: - - cron: "0 6 * * *" + - cron: "0 */4 * * *" jobs: cleanup: diff --git a/.yarnrc b/.yarnrc index ac05beb3696fd..019f345f39305 100644 --- a/.yarnrc +++ b/.yarnrc @@ -7,3 +7,4 @@ # be forced to registry.yarnpkg.com. # https://github.com/npm/cli/issues/3783 registry "https://registry.npmjs.org" +ignore-engines true # the 'engines' key for 'aws-cdk-lib' has specifies node14 as min while v1 will remain at node10 diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b0127076effa..f254623e858c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,40 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [1.128.0](https://github.com/aws/aws-cdk/compare/v1.127.0...v1.128.0) (2021-10-14) + + +### ⚠ BREAKING CHANGES TO EXPERIMENTAL FEATURES + +* **assertions:** Starting this release, the `assertions` module will be +published to Maven with the name 'assertions' instead of +'cdk-assertions'. + +### Features + +* **apigatewayv2-integrations:** http api - support for request parameter mapping ([#15630](https://github.com/aws/aws-cdk/issues/15630)) ([0452aed](https://github.com/aws/aws-cdk/commit/0452aed2f00198e05bd65b1d20246f7de0b24e20)) +* **cli:** hotswap deployments for ECS Services ([#16864](https://github.com/aws/aws-cdk/issues/16864)) ([ad7288f](https://github.com/aws/aws-cdk/commit/ad7288f35a17fcfbecd7080e99ece4873fa99ad2)) +* **codepipeline:** add support for string user parameters to the Lambda invoke action ([#16946](https://github.com/aws/aws-cdk/issues/16946)) ([e19ea31](https://github.com/aws/aws-cdk/commit/e19ea31dbf62446edaf5131c75246098ab05da6e)), closes [#16776](https://github.com/aws/aws-cdk/issues/16776) +* **lambda:** docker platform for architecture ([#16858](https://github.com/aws/aws-cdk/issues/16858)) ([5c258a3](https://github.com/aws/aws-cdk/commit/5c258a30367a4922e404eb26e5aa076720846fbe)) +* **lambda-event-sources:** self managed kafka: support sasl/plain authentication ([#16712](https://github.com/aws/aws-cdk/issues/16712)) ([d4ad93f](https://github.com/aws/aws-cdk/commit/d4ad93f30877b26b851caa81d3a4a1d80df55164)) +* **stepfunctions-tasks:** AWS SDK service integrations ([#16746](https://github.com/aws/aws-cdk/issues/16746)) ([ae840ff](https://github.com/aws/aws-cdk/commit/ae840ff1abb8283a1290dae5859f5729a9cf72b1)), closes [#16780](https://github.com/aws/aws-cdk/issues/16780) + + +### Bug Fixes + +* **ecs:** add ASG capacity via Capacity Provider by not specifying machineImageType ([#16361](https://github.com/aws/aws-cdk/issues/16361)) ([93b3fdc](https://github.com/aws/aws-cdk/commit/93b3fdce80f0997d7b809f9ef7e3edd1e75e1f42)), closes [#16360](https://github.com/aws/aws-cdk/issues/16360) +* **servicecatalog:** Allow users to create multiple product versions from assets. ([#16914](https://github.com/aws/aws-cdk/issues/16914)) ([958d755](https://github.com/aws/aws-cdk/commit/958d755ff7acaf016e3f8969bf5ab07d4dc2977b)) +* **codebuild:** add build image AMAZON_LINUX_2_ARM_2 ([#16931](https://github.com/aws/aws-cdk/issues/16931)) ([370cb31](https://github.com/aws/aws-cdk/commit/370cb310cce3fccc5381d8d53130e21b266de868)), closes [#16930](https://github.com/aws/aws-cdk/issues/16930) +* **core:** asset hash is different between linux and windows ([#16945](https://github.com/aws/aws-cdk/issues/16945)) ([59950dd](https://github.com/aws/aws-cdk/commit/59950dd331635fb707aac819529614c0f3e47ee5)), closes [#14555](https://github.com/aws/aws-cdk/issues/14555) [#16928](https://github.com/aws/aws-cdk/issues/16928) +* **ecs-patterns:** minScalingCapacity cannot be set to 0 ([#16961](https://github.com/aws/aws-cdk/issues/16961)) ([589f284](https://github.com/aws/aws-cdk/commit/589f284acec8530aa9824b75a5daef4632e98985)), closes [#15632](https://github.com/aws/aws-cdk/issues/15632) [#14336](https://github.com/aws/aws-cdk/issues/14336) +* **ssm:** StringParameter accepts ParameterType.AWS_EC2_IMAGE_ID as type ([#16884](https://github.com/aws/aws-cdk/issues/16884)) ([2b353be](https://github.com/aws/aws-cdk/commit/2b353be5291cbcdc56a8863038eed4a5f2adc65f)), closes [#16806](https://github.com/aws/aws-cdk/issues/16806) +* use registry.npmjs.com to fix shinkwrap resolves ([#16607](https://github.com/aws/aws-cdk/issues/16607)) ([8f91531](https://github.com/aws/aws-cdk/commit/8f91531c3c25900316d40d5564450566a03e27ee)) + + +### Miscellaneous Chores + +* **assertions:** consistent naming in maven ([#16921](https://github.com/aws/aws-cdk/issues/16921)) ([0dcd9ec](https://github.com/aws/aws-cdk/commit/0dcd9eca3a1014c39f92d9e052b67974fc751af0)) + ## [1.127.0](https://github.com/aws/aws-cdk/compare/v1.126.0...v1.127.0) (2021-10-08) diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/http/stage.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/http/stage.ts index 709f207a04435..ca40349975ec1 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/http/stage.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/http/stage.ts @@ -18,6 +18,11 @@ export interface IHttpStage extends IStage { */ readonly api: IHttpApi; + /** + * The custom domain URL to this stage + */ + readonly domainUrl: string; + /** * Metric for the number of client-side errors captured in a given period. * @@ -96,6 +101,7 @@ export interface HttpStageAttributes extends StageAttributes { } abstract class HttpStageBase extends StageBase implements IHttpStage { + public abstract readonly domainUrl: string; public abstract readonly api: IHttpApi; public metricClientError(props?: MetricOptions): Metric { @@ -140,6 +146,10 @@ export class HttpStage extends HttpStageBase { get url(): string { throw new Error('url is not available for imported stages.'); } + + get domainUrl(): string { + throw new Error('domainUrl is not available for imported stages.'); + } } return new Import(scope, id); } @@ -177,9 +187,6 @@ export class HttpStage extends HttpStageBase { return `https://${this.api.apiId}.execute-api.${s.region}.${s.urlSuffix}/${urlPath}`; } - /** - * The custom domain URL to this stage - */ public get domainUrl(): string { if (!this._apiMapping) { throw new Error('domainUrl is not available when no API mapping is associated with the Stage'); diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/http/api.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/http/api.test.ts index 5b7f1052bfe35..200933eefbca9 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/test/http/api.test.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/test/http/api.test.ts @@ -1,9 +1,10 @@ import { Match, Template } from '@aws-cdk/assertions'; +import { Certificate } from '@aws-cdk/aws-certificatemanager'; import { Metric } from '@aws-cdk/aws-cloudwatch'; import * as ec2 from '@aws-cdk/aws-ec2'; import { Duration, Stack } from '@aws-cdk/core'; import { - CorsHttpMethod, + CorsHttpMethod, DomainName, HttpApi, HttpAuthorizer, HttpIntegrationType, HttpMethod, HttpRouteAuthorizerBindOptions, HttpRouteAuthorizerConfig, HttpRouteIntegrationBindOptions, HttpRouteIntegrationConfig, IHttpRouteAuthorizer, IHttpRouteIntegration, HttpNoneAuthorizer, PayloadFormatVersion, } from '../../lib'; @@ -374,6 +375,27 @@ describe('HttpApi', () => { expect(() => api.apiEndpoint).toThrow(/apiEndpoint is not configured/); }); + test('domainUrl can be retrieved for default stage', () => { + const stack = new Stack(); + const dn = new DomainName(stack, 'DN', { + domainName: 'example.com', + certificate: Certificate.fromCertificateArn(stack, 'cert', 'arn:aws:acm:us-east-1:111111111111:certificate'), + }); + + const api = new HttpApi(stack, 'Api', { + createDefaultStage: true, + defaultDomainMapping: { + domainName: dn, + }, + }); + + expect(stack.resolve(api.defaultStage?.domainUrl)).toEqual({ + 'Fn::Join': ['', [ + 'https://', { Ref: 'DNFDC76583' }, '/', + ]], + }); + }); + describe('default authorization settings', () => { test('can add default authorizer', () => { diff --git a/packages/@aws-cdk/aws-appsync/README.md b/packages/@aws-cdk/aws-appsync/README.md index a191b51a86483..8e78c92bd4b7b 100644 --- a/packages/@aws-cdk/aws-appsync/README.md +++ b/packages/@aws-cdk/aws-appsync/README.md @@ -24,6 +24,10 @@ The `@aws-cdk/aws-appsync` package contains constructs for building flexible APIs that use GraphQL. +```ts nofixture +import * as appsync from '@aws-cdk/aws-appsync'; +``` + ## Example ### DynamoDB @@ -52,24 +56,21 @@ type Mutation { CDK stack file `app-stack.ts`: ```ts -import * as appsync from '@aws-cdk/aws-appsync'; -import * as db from '@aws-cdk/aws-dynamodb'; - -const api = new appsync.GraphqlApi(stack, 'Api', { +const api = new appsync.GraphqlApi(this, 'Api', { name: 'demo', - schema: appsync.Schema.fromAsset(join(__dirname, 'schema.graphql')), + schema: appsync.Schema.fromAsset(path.join(__dirname, 'schema.graphql')), authorizationConfig: { defaultAuthorization: { - authorizationType: appsync.AuthorizationType.IAM + authorizationType: appsync.AuthorizationType.IAM, }, }, xrayEnabled: true, }); -const demoTable = new db.Table(stack, 'DemoTable', { +const demoTable = new dynamodb.Table(this, 'DemoTable', { partitionKey: { name: 'id', - type: db.AttributeType.STRING, + type: dynamodb.AttributeType.STRING, }, }); @@ -89,7 +90,7 @@ demoDS.createResolver({ fieldName: 'addDemo', requestMappingTemplate: appsync.MappingTemplate.dynamoDbPutItem( appsync.PrimaryKey.partition('id').auto(), - appsync.Values.projecting('input') + appsync.Values.projecting('input'), ), responseMappingTemplate: appsync.MappingTemplate.dynamoDbResultItem(), }); @@ -103,15 +104,15 @@ against the Data API with GraphQL queries, mutations, and subscriptions. ```ts // Create username and password secret for DB Cluster -const secret = new rds.DatabaseSecret(stack, 'AuroraSecret', { +const secret = new rds.DatabaseSecret(this, 'AuroraSecret', { username: 'clusteradmin', }); // The VPC to place the cluster in -const vpc = new ec2.Vpc(stack, 'AuroraVpc'); +const vpc = new ec2.Vpc(this, 'AuroraVpc'); // Create the serverless cluster, provide all values needed to customise the database. -const cluster = new rds.ServerlessCluster(stack, 'AuroraCluster', { +const cluster = new rds.ServerlessCluster(this, 'AuroraCluster', { engine: rds.DatabaseClusterEngine.AURORA_MYSQL, vpc, credentials: { username: 'clusteradmin' }, @@ -120,13 +121,14 @@ const cluster = new rds.ServerlessCluster(stack, 'AuroraCluster', { }); // Build a data source for AppSync to access the database. +declare const api: appsync.GraphqlApi; const rdsDS = api.addRdsDataSource('rds', cluster, secret, 'demos'); // Set up a resolver for an RDS query. rdsDS.createResolver({ typeName: 'Query', fieldName: 'getDemosRds', - requestMappingTemplate: MappingTemplate.fromString(` + requestMappingTemplate: appsync.MappingTemplate.fromString(` { "version": "2018-05-29", "statements": [ @@ -134,7 +136,7 @@ rdsDS.createResolver({ ] } `), - responseMappingTemplate: MappingTemplate.fromString(` + responseMappingTemplate: appsync.MappingTemplate.fromString(` $utils.toJson($utils.rds.toJsonObject($ctx.result)[0]) `), }); @@ -143,7 +145,7 @@ rdsDS.createResolver({ rdsDS.createResolver({ typeName: 'Mutation', fieldName: 'addDemoRds', - requestMappingTemplate: MappingTemplate.fromString(` + requestMappingTemplate: appsync.MappingTemplate.fromString(` { "version": "2018-05-29", "statements": [ @@ -156,7 +158,7 @@ rdsDS.createResolver({ } } `), - responseMappingTemplate: MappingTemplate.fromString(` + responseMappingTemplate: appsync.MappingTemplate.fromString(` $utils.toJson($utils.rds.toJsonObject($ctx.result)[1][0]) `), }); @@ -212,11 +214,9 @@ GraphQL response mapping template `response.vtl`: CDK stack file `app-stack.ts`: ```ts -import * as appsync from '@aws-cdk/aws-appsync'; - -const api = new appsync.GraphqlApi(scope, 'api', { +const api = new appsync.GraphqlApi(this, 'api', { name: 'api', - schema: appsync.Schema.fromFile(join(__dirname, 'schema.graphql')), + schema: appsync.Schema.fromAsset(path.join(__dirname, 'schema.graphql')), }); const httpDs = api.addHttpDataSource( @@ -227,7 +227,7 @@ const httpDs = api.addHttpDataSource( description: 'from appsync to StepFunctions Workflow', authorizationConfig: { signingRegion: 'us-east-1', - signingServiceName: 'states' + signingServiceName: 'states', } } ); @@ -235,8 +235,8 @@ const httpDs = api.addHttpDataSource( httpDs.createResolver({ typeName: 'Mutation', fieldName: 'callStepFunction', - requestMappingTemplate: MappingTemplate.fromFile('request.vtl'), - responseMappingTemplate: MappingTemplate.fromFile('response.vtl') + requestMappingTemplate: appsync.MappingTemplate.fromFile('request.vtl'), + responseMappingTemplate: appsync.MappingTemplate.fromFile('response.vtl'), }); ``` @@ -247,16 +247,19 @@ through your AWS account. You can use AppSync resolvers to perform GraphQL opera such as queries, mutations, and subscriptions. ```ts -const user = new User(stack, 'User'); -const domain = new es.Domain(stack, 'Domain', { +import * as es from '@aws-cdk/aws-elasticsearch'; + +const user = new iam.User(this, 'User'); +const domain = new es.Domain(this, 'Domain', { version: es.ElasticsearchVersion.V7_1, - removalPolicy: cdk.RemovalPolicy.DESTROY, + removalPolicy: RemovalPolicy.DESTROY, fineGrainedAccessControl: { masterUserArn: user.userArn }, encryptionAtRest: { enabled: true }, nodeToNodeEncryption: true, enforceHttps: true, }); +declare const api: appsync.GraphqlApi; const ds = api.addElasticsearchDataSource('ds', domain); ds.createResolver({ @@ -293,23 +296,23 @@ When declaring your GraphQL Api, CDK defaults to a code-first approach if the `schema` property is not configured. ```ts -const api = new appsync.GraphqlApi(stack, 'api', { name: 'myApi' }); +const api = new appsync.GraphqlApi(this, 'api', { name: 'myApi' }); ``` CDK will declare a `Schema` class that will give your Api access functions to -define your schema code-first: `addType`, `addObjectType`, `addToSchema`, etc. +define your schema code-first: `addType`, `addToSchema`, etc. You can also declare your `Schema` class outside of your CDK stack, to define your schema externally. ```ts const schema = new appsync.Schema(); -schema.addObjectType('demo', { +schema.addType(new appsync.ObjectType('demo', { definition: { id: appsync.GraphqlType.id() }, -}); -const api = new appsync.GraphqlApi(stack, 'api', { +})); +const api = new appsync.GraphqlApi(this, 'api', { name: 'myApi', - schema + schema, }); ``` @@ -321,9 +324,9 @@ You can define your GraphQL Schema from a file on disk. For convenience, use the `appsync.Schema.fromAsset` to specify the file representing your schema. ```ts -const api = appsync.GraphqlApi(stack, 'api', { +const api = new appsync.GraphqlApi(this, 'api', { name: 'myApi', - schema: appsync.Schema.fromAsset(join(__dirname, 'schema.graphl')), + schema: appsync.Schema.fromAsset(path.join(__dirname, 'schema.graphl')), }); ``` @@ -334,9 +337,11 @@ another stack into your CDK app. Utilizing the `fromXxx` function, you have the ability to add data sources and resolvers through a `IGraphqlApi` interface. ```ts -const importedApi = appsync.GraphqlApi.fromGraphqlApiAttributes(stack, 'IApi', { +declare const api: appsync.GraphqlApi; +declare const table: dynamodb.Table; +const importedApi = appsync.GraphqlApi.fromGraphqlApiAttributes(this, 'IApi', { graphqlApiId: api.apiId, - graphqlArn: api.arn, + graphqlApiArn: api.arn, }); importedApi.addDynamoDbDataSource('TableDataSource', table); ``` @@ -362,9 +367,10 @@ authorization mode to finish defining your authorization. For example, this is a with AWS Lambda Authorization. ```ts -authFunction = new lambda.Function(stack, 'auth-function', {}); +import * as lambda from '@aws-cdk/aws-lambda'; +declare const authFunction: lambda.Function; -new appsync.GraphqlApi(stack, 'api', { +new appsync.GraphqlApi(this, 'api', { name: 'api', schema: appsync.Schema.fromAsset(path.join(__dirname, 'appsync.test.graphql')), authorizationConfig: { @@ -390,7 +396,7 @@ for `IAM` authorized access you would configure the following. In `schema.graphql`: -```ts +```gql type Mutation { updateExample(...): ... @aws_iam @@ -401,18 +407,18 @@ In `IAM`: ```json { - "Version": "2012-10-17", - "Statement": [ - { - "Effect": "Allow", - "Action": [ - "appsync:GraphQL" - ], - "Resource": [ - "arn:aws:appsync:REGION:ACCOUNT_ID:apis/GRAPHQL_ID/types/Mutation/fields/updateExample" - ] - } - ] + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "appsync:GraphQL" + ], + "Resource": [ + "arn:aws:appsync:REGION:ACCOUNT_ID:apis/GRAPHQL_ID/types/Mutation/fields/updateExample" + ] + } + ] } ``` @@ -423,14 +429,12 @@ To make this easier, CDK provides `grant` API. Use the `grant` function for more granular authorization. ```ts -const role = new iam.Role(stack, 'Role', { +const role = new iam.Role(this, 'Role', { assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'), }); -const api = new appsync.GraphqlApi(stack, 'API', { - definition -}); +declare const api: appsync.GraphqlApi; -api.grant(role, appsync.IamResource.custom('types/Mutation/fields/updateExample'), 'appsync:GraphQL') +api.grant(role, appsync.IamResource.custom('types/Mutation/fields/updateExample'), 'appsync:GraphQL'); ``` ### IamResource @@ -454,6 +458,9 @@ These include: - grantSubscription (use to grant access to Subscription fields) ```ts +declare const api: appsync.GraphqlApi; +declare const role: iam.Role; + // For generic types api.grantMutation(role, 'updateExample'); @@ -468,10 +475,12 @@ backend data source. Developers can compose operations (Functions) and execute them in sequence with Pipeline Resolvers. ```ts -const appsyncFunction = new appsync.AppsyncFunction(stack, 'function', { +declare const api: appsync.GraphqlApi; + +const appsyncFunction = new appsync.AppsyncFunction(this, 'function', { name: 'appsync_function', - api: api, - dataSource: apiDataSource, + api, + dataSource: api.addNoneDataSource('none'), requestMappingTemplate: appsync.MappingTemplate.fromFile('request.vtl'), responseMappingTemplate: appsync.MappingTemplate.fromFile('response.vtl'), }); @@ -481,10 +490,14 @@ AppSync Functions are used in tandem with pipeline resolvers to compose multiple operations. ```ts -const pipelineResolver = new appsync.Resolver(stack, 'pipeline', { - name: 'pipeline_resolver', - api: api, - dataSource: apiDataSource, +declare const api: appsync.GraphqlApi; +declare const appsyncFunction: appsync.AppsyncFunction; + +const pipelineResolver = new appsync.Resolver(this, 'pipeline', { + api, + dataSource: api.addNoneDataSource('none'), + typeName: 'typeName', + fieldName: 'fieldName', requestMappingTemplate: appsync.MappingTemplate.fromFile('beforeRequest.vtl'), pipelineConfig: [appsyncFunction], responseMappingTemplate: appsync.MappingTemplate.fromFile('afterResponse.vtl'), @@ -537,48 +550,38 @@ Above we see a schema that allows for generating paginated responses. For exampl we can query `allFilms(first: 100)` since `FilmConnection` acts as an intermediary for holding `FilmEdges` we can write a resolver to return the first 100 films. -In a separate file, we can declare our scalar types: `scalar-types.ts`. - -```ts -import { GraphqlType } from '@aws-cdk/aws-appsync'; - -export const string = appsync.GraphqlType.string(); -export const int = appsync.GraphqlType.int(); -``` - -In another separate file, we can declare our object types and related functions. +In a separate file, we can declare our object types and related functions. We will call this file `object-types.ts` and we will have created it in a way that allows us to generate other `XxxConnection` and `XxxEdges` in the future. -```ts -const pluralize = require('pluralize'); -import * as scalar from './scalar-types.ts'; +```ts nofixture import * as appsync from '@aws-cdk/aws-appsync'; +const pluralize = require('pluralize'); export const args = { - after: scalar.string, - first: scalar.int, - before: scalar.string, - last: scalar.int, + after: appsync.GraphqlType.string(), + first: appsync.GraphqlType.int(), + before: appsync.GraphqlType.string(), + last: appsync.GraphqlType.int(), }; export const Node = new appsync.InterfaceType('Node', { - definition: { id: scalar.string } + definition: { id: appsync.GraphqlType.string() } }); -export const FilmNode = new appsync.ObjectType.implementInterface('FilmNode', { +export const FilmNode = new appsync.ObjectType('FilmNode', { interfaceTypes: [Node], - definition: { filmName: scalar.string } + definition: { filmName: appsync.GraphqlType.string() } }); export function generateEdgeAndConnection(base: appsync.ObjectType) { const edge = new appsync.ObjectType(`${base.name}Edge`, { - definition: { node: base.attribute(), cursor: scalar.string } + definition: { node: base.attribute(), cursor: appsync.GraphqlType.string() } }); const connection = new appsync.ObjectType(`${base.name}Connection`, { definition: { - edges: edges.attribute({ isList: true }), + edges: edge.attribute({ isList: true }), [pluralize(base.name)]: base.attribute({ isList: true }), - totalCount: scalar.int, + totalCount: appsync.GraphqlType.int(), } }); return { edge: edge, connection: connection }; @@ -588,29 +591,30 @@ export function generateEdgeAndConnection(base: appsync.ObjectType) { Finally, we will go to our `cdk-stack` and combine everything together to generate our schema. -```ts -import * as appsync from '@aws-cdk/aws-appsync'; -import * as schema from './object-types'; +```ts fixture=with-objects +declare const dummyRequest: appsync.MappingTemplate; +declare const dummyResponse: appsync.MappingTemplate; -const api = new appsync.GraphqlApi(stack, 'Api', { +const api = new appsync.GraphqlApi(this, 'Api', { name: 'demo', }); -this.objectTypes = [ schema.Node, schema.Film ]; +const objectTypes = [ Node, FilmNode ]; -const filmConnections = schema.generateEdgeAndConnection(schema.Film); +const filmConnections = generateEdgeAndConnection(FilmNode); api.addQuery('allFilms', new appsync.ResolvableField({ - returnType: filmConnections.connection.attribute(), - args: schema.args, - dataSource: dummyDataSource, - requestMappingTemplate: dummyRequest, - responseMappingTemplate: dummyResponse, - }), -}); + returnType: filmConnections.connection.attribute(), + args: args, + dataSource: api.addNoneDataSource('none'), + requestMappingTemplate: dummyRequest, + responseMappingTemplate: dummyResponse, +})); -this.objectTypes.map((t) => api.addType(t)); -Object.keys(filmConnections).forEach((key) => api.addType(filmConnections[key])); +api.addType(Node); +api.addType(FilmNode); +api.addType(filmConnections.edge); +api.addType(filmConnections.connection); ``` Notice how we can utilize the `generateEdgeAndConnection` function to generate @@ -701,6 +705,9 @@ type Info { The CDK code required would be: ```ts +declare const api: appsync.GraphqlApi; +declare const dummyRequest: appsync.MappingTemplate; +declare const dummyResponse: appsync.MappingTemplate; const info = new appsync.ObjectType('Info', { definition: { node: new appsync.ResolvableField({ @@ -729,6 +736,9 @@ type Query { The CDK code required would be: ```ts +declare const api: appsync.GraphqlApi; +declare const dummyRequest: appsync.MappingTemplate; +declare const dummyResponse: appsync.MappingTemplate; const query = new appsync.ObjectType('Query', { definition: { get: new appsync.ResolvableField({ @@ -784,12 +794,12 @@ To learn more about **Interface Types**, read the docs [here](https://graphql.or the `demo` variable is an **Object Type**. **Object Types** are defined by GraphQL Types and are only usable when linked to a GraphQL Api. -You can create Object Types in three ways: +You can create Object Types in two ways: 1. Object Types can be created ***externally***. ```ts - const api = new appsync.GraphqlApi(stack, 'Api', { + const api = new appsync.GraphqlApi(this, 'Api', { name: 'demo', }); const demo = new appsync.ObjectType('Demo', { @@ -799,34 +809,28 @@ You can create Object Types in three ways: }, }); - api.addType(object); + api.addType(demo); ``` > This method allows for reusability and modularity, ideal for larger projects. For example, imagine moving all Object Type definition outside the stack. - `scalar-types.ts` - a file for scalar type definitions - - ```ts - export const required_string = appsync.GraphqlType.string({ isRequired: true }); - ``` - `object-types.ts` - a file for object type definitions - ```ts - import { required_string } from './scalar-types'; + ```ts nofixture + import * as appsync from '@aws-cdk/aws-appsync'; export const demo = new appsync.ObjectType('Demo', { definition: { - id: required_string, - version: required_string, + id: appsync.GraphqlType.string({ isRequired: true }), + version: appsync.GraphqlType.string({ isRequired: true }), }, }); ``` `cdk-stack.ts` - a file containing our cdk stack - ```ts - import { demo } from './object-types'; + ```ts fixture=with-objects + declare const api: appsync.GraphqlApi; api.addType(demo); ``` @@ -869,6 +873,7 @@ enum Episode { The above GraphQL Enumeration Type can be expressed in CDK as the following: ```ts +declare const api: appsync.GraphqlApi; const episode = new appsync.EnumType('Episode', { definition: [ 'NEWHOPE', @@ -896,10 +901,11 @@ input Review { The above GraphQL Input Type can be expressed in CDK as the following: ```ts +declare const api: appsync.GraphqlApi; const review = new appsync.InputType('Review', { definition: { - stars: GraphqlType.int({ isRequired: true }), - commentary: GraphqlType.string(), + stars: appsync.GraphqlType.int({ isRequired: true }), + commentary: appsync.GraphqlType.string(), }, }); api.addType(review); @@ -923,6 +929,7 @@ The above GraphQL Union Type encompasses the Object Types of Human, Droid and St can be expressed in CDK as the following: ```ts +declare const api: appsync.GraphqlApi; const string = appsync.GraphqlType.string(); const human = new appsync.ObjectType('Human', { definition: { name: string } }); const droid = new appsync.ObjectType('Droid', { definition: { name: string } }); @@ -945,6 +952,11 @@ To add fields for these queries, we can simply run the `addQuery` function to ad to the schema's `Query` type. ```ts +declare const api: appsync.GraphqlApi; +declare const filmConnection: appsync.InterfaceType; +declare const dummyRequest: appsync.MappingTemplate; +declare const dummyResponse: appsync.MappingTemplate; + const string = appsync.GraphqlType.string(); const int = appsync.GraphqlType.int(); api.addQuery('allFilms', new appsync.ResolvableField({ @@ -968,10 +980,15 @@ To add fields for these mutations, we can simply run the `addMutation` function to the schema's `Mutation` type. ```ts +declare const api: appsync.GraphqlApi; +declare const filmNode: appsync.ObjectType; +declare const dummyRequest: appsync.MappingTemplate; +declare const dummyResponse: appsync.MappingTemplate; + const string = appsync.GraphqlType.string(); const int = appsync.GraphqlType.int(); api.addMutation('addFilm', new appsync.ResolvableField({ - returnType: film.attribute(), + returnType: filmNode.attribute(), args: { name: string, film_number: int }, dataSource: api.addNoneDataSource('none'), requestMappingTemplate: dummyRequest, @@ -994,10 +1011,13 @@ To add fields for these subscriptions, we can simply run the `addSubscription` f to the schema's `Subscription` type. ```ts +declare const api: appsync.GraphqlApi; +declare const film: appsync.InterfaceType; + api.addSubscription('addedFilm', new appsync.Field({ returnType: film.attribute(), args: { id: appsync.GraphqlType.id({ isRequired: true }) }, - directive: [appsync.Directive.subscribe('addFilm')], + directives: [appsync.Directive.subscribe('addFilm')], })); ``` diff --git a/packages/@aws-cdk/aws-appsync/lib/graphqlapi.ts b/packages/@aws-cdk/aws-appsync/lib/graphqlapi.ts index 93c9078e32358..71cd6e32060c1 100644 --- a/packages/@aws-cdk/aws-appsync/lib/graphqlapi.ts +++ b/packages/@aws-cdk/aws-appsync/lib/graphqlapi.ts @@ -150,7 +150,7 @@ export interface OpenIdConnectConfig { /** * The client identifier of the Relying party at the OpenID identity provider. * A regular expression can be specified so AppSync can validate against multiple client identifiers at a time. - * @example - 'ABCD|CDEF' where ABCD and CDEF are two different clientId + * @example - 'ABCD|CDEF' // where ABCD and CDEF are two different clientId * @default - * (All) */ readonly clientId?: string; diff --git a/packages/@aws-cdk/aws-appsync/rosetta/default.ts-fixture b/packages/@aws-cdk/aws-appsync/rosetta/default.ts-fixture new file mode 100644 index 0000000000000..2b84336958cc7 --- /dev/null +++ b/packages/@aws-cdk/aws-appsync/rosetta/default.ts-fixture @@ -0,0 +1,15 @@ +// Fixture with packages imported, but nothing else +import { Construct, RemovalPolicy, Stack } from '@aws-cdk/core'; +import appsync = require('@aws-cdk/aws-appsync'); +import ec2 = require('@aws-cdk/aws-ec2'); +import dynamodb = require('@aws-cdk/aws-dynamodb'); +import iam = require('@aws-cdk/aws-iam'); +import rds = require('@aws-cdk/aws-rds'); +import path = require('path'); + +class Fixture extends Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + /// here + } +} diff --git a/packages/@aws-cdk/aws-appsync/rosetta/with-objects.ts-fixture b/packages/@aws-cdk/aws-appsync/rosetta/with-objects.ts-fixture new file mode 100644 index 0000000000000..1251aad728423 --- /dev/null +++ b/packages/@aws-cdk/aws-appsync/rosetta/with-objects.ts-fixture @@ -0,0 +1,49 @@ +// Fixture with packages imported, but nothing else +import { Construct, Stack } from '@aws-cdk/core'; +import appsync = require('@aws-cdk/aws-appsync'); +const pluralize = require('pluralize'); + +const args = { + after: appsync.GraphqlType.string(), + first: appsync.GraphqlType.int(), + before: appsync.GraphqlType.string(), + last: appsync.GraphqlType.int(), +}; + +const Node = new appsync.InterfaceType('Node', { + definition: { id: appsync.GraphqlType.string() } +}); + +const FilmNode = new appsync.ObjectType('FilmNode', { + interfaceTypes: [Node], + definition: { filmName: appsync.GraphqlType.string() } +}); + +function generateEdgeAndConnection(base: appsync.ObjectType) { + const edge = new appsync.ObjectType(`${base.name}Edge`, { + definition: { node: base.attribute(), cursor: appsync.GraphqlType.string() } + }); + const connection = new appsync.ObjectType(`${base.name}Connection`, { + definition: { + edges: edge.attribute({ isList: true }), + [pluralize(base.name)]: base.attribute({ isList: true }), + totalCount: appsync.GraphqlType.int(), + } + }); + return { edge: edge, connection: connection }; +} + +const demo = new appsync.ObjectType('Demo', { + definition: { + id: appsync.GraphqlType.string({ isRequired: true }), + version: appsync.GraphqlType.string({ isRequired: true }), + }, +}); + +class Fixture extends Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + + /// here + } +} diff --git a/packages/@aws-cdk/aws-autoscaling/README.md b/packages/@aws-cdk/aws-autoscaling/README.md index 67e55eee91a9f..75aa4f66807e2 100644 --- a/packages/@aws-cdk/aws-autoscaling/README.md +++ b/packages/@aws-cdk/aws-autoscaling/README.md @@ -378,6 +378,32 @@ new autoscaling.AutoScalingGroup(stack, 'ASG', { }); ``` +## Configuring Instance Metadata Service (IMDS) + +### Toggling IMDSv1 + +You can configure [EC2 Instance Metadata Service](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-metadata.html) options to either +allow both IMDSv1 and IMDSv2 or enforce IMDSv2 when interacting with the IMDS. + +To do this for a single `AutoScalingGroup`, you can use set the `requireImdsv2` property. +The example below demonstrates IMDSv2 being required on a single `AutoScalingGroup`: + +```ts +new autoscaling.AutoScalingGroup(stack, 'ASG', { + requireImdsv2: true, + // ... +}); +``` + +You can also use `AutoScalingGroupRequireImdsv2Aspect` to apply the operation to multiple AutoScalingGroups. +The example below demonstrates the `AutoScalingGroupRequireImdsv2Aspect` being used to require IMDSv2 for all AutoScalingGroups in a stack: + +```ts +const aspect = new autoscaling.AutoScalingGroupRequireImdsv2Aspect(); + +Aspects.of(stack).add(aspect); +``` + ## Future work * [ ] CloudWatch Events (impossible to add currently as the AutoScalingGroup ARN is diff --git a/packages/@aws-cdk/aws-autoscaling/lib/aspects/index.ts b/packages/@aws-cdk/aws-autoscaling/lib/aspects/index.ts new file mode 100644 index 0000000000000..31fc534776144 --- /dev/null +++ b/packages/@aws-cdk/aws-autoscaling/lib/aspects/index.ts @@ -0,0 +1 @@ +export * from './require-imdsv2-aspect'; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-autoscaling/lib/aspects/require-imdsv2-aspect.ts b/packages/@aws-cdk/aws-autoscaling/lib/aspects/require-imdsv2-aspect.ts new file mode 100644 index 0000000000000..e399dce585d79 --- /dev/null +++ b/packages/@aws-cdk/aws-autoscaling/lib/aspects/require-imdsv2-aspect.ts @@ -0,0 +1,38 @@ +import * as cdk from '@aws-cdk/core'; +import { AutoScalingGroup } from '../auto-scaling-group'; +import { CfnLaunchConfiguration } from '../autoscaling.generated'; + +/** + * Aspect that makes IMDSv2 required on instances deployed by AutoScalingGroups. + */ +export class AutoScalingGroupRequireImdsv2Aspect implements cdk.IAspect { + constructor() { + } + + public visit(node: cdk.IConstruct): void { + if (!(node instanceof AutoScalingGroup)) { + return; + } + + const launchConfig = node.node.tryFindChild('LaunchConfig') as CfnLaunchConfiguration; + if (cdk.isResolvableObject(launchConfig.metadataOptions)) { + this.warn(node, 'CfnLaunchConfiguration.MetadataOptions field is a CDK token.'); + return; + } + + launchConfig.metadataOptions = { + ...launchConfig.metadataOptions, + httpTokens: 'required', + }; + } + + /** + * Adds a warning annotation to a node. + * + * @param node The scope to add the warning to. + * @param message The warning message. + */ + protected warn(node: cdk.IConstruct, message: string) { + cdk.Annotations.of(node).addWarning(`${AutoScalingGroupRequireImdsv2Aspect.name} failed on node ${node.node.id}: ${message}`); + } +} diff --git a/packages/@aws-cdk/aws-autoscaling/lib/auto-scaling-group.ts b/packages/@aws-cdk/aws-autoscaling/lib/auto-scaling-group.ts index 027034249c4dd..45fd06c478dfc 100644 --- a/packages/@aws-cdk/aws-autoscaling/lib/auto-scaling-group.ts +++ b/packages/@aws-cdk/aws-autoscaling/lib/auto-scaling-group.ts @@ -7,6 +7,7 @@ import * as sns from '@aws-cdk/aws-sns'; import { Annotations, + Aspects, Aws, CfnAutoScalingRollingUpdate, CfnCreationPolicy, CfnUpdatePolicy, Duration, Fn, IResource, Lazy, PhysicalName, Resource, Stack, Tags, @@ -14,6 +15,7 @@ import { Tokenization, withResolved, } from '@aws-cdk/core'; import { Construct } from 'constructs'; +import { AutoScalingGroupRequireImdsv2Aspect } from './aspects'; import { CfnAutoScalingGroup, CfnAutoScalingGroupProps, CfnLaunchConfiguration } from './autoscaling.generated'; import { BasicLifecycleHookProps, LifecycleHook } from './lifecycle-hook'; import { BasicScheduledActionProps, ScheduledAction } from './scheduled-action'; @@ -384,6 +386,13 @@ export interface AutoScalingGroupProps extends CommonAutoScalingGroupProps { * @default - default options */ readonly initOptions?: ApplyCloudFormationInitOptions; + + /** + * Whether IMDSv2 should be required on launched instances. + * + * @default - false + */ + readonly requireImdsv2?: boolean; } /** @@ -1065,6 +1074,10 @@ export class AutoScalingGroup extends AutoScalingGroupBase implements } this.spotPrice = props.spotPrice; + + if (props.requireImdsv2) { + Aspects.of(this).add(new AutoScalingGroupRequireImdsv2Aspect()); + } } /** diff --git a/packages/@aws-cdk/aws-autoscaling/lib/index.ts b/packages/@aws-cdk/aws-autoscaling/lib/index.ts index 69fede92e300b..186d1a3058fae 100644 --- a/packages/@aws-cdk/aws-autoscaling/lib/index.ts +++ b/packages/@aws-cdk/aws-autoscaling/lib/index.ts @@ -1,3 +1,4 @@ +export * from './aspects'; export * from './auto-scaling-group'; export * from './schedule'; export * from './lifecycle-hook'; diff --git a/packages/@aws-cdk/aws-autoscaling/test/aspects/require-imdsv2-aspect.test.ts b/packages/@aws-cdk/aws-autoscaling/test/aspects/require-imdsv2-aspect.test.ts new file mode 100644 index 0000000000000..22a58f097a98b --- /dev/null +++ b/packages/@aws-cdk/aws-autoscaling/test/aspects/require-imdsv2-aspect.test.ts @@ -0,0 +1,79 @@ +import { + expect as expectCDK, + haveResourceLike, +} from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as cdk from '@aws-cdk/core'; +import { + AutoScalingGroup, + AutoScalingGroupRequireImdsv2Aspect, + CfnLaunchConfiguration, +} from '../../lib'; + +describe('AutoScalingGroupRequireImdsv2Aspect', () => { + let app: cdk.App; + let stack: cdk.Stack; + let vpc: ec2.Vpc; + + beforeEach(() => { + app = new cdk.App(); + stack = new cdk.Stack(app, 'Stack'); + vpc = new ec2.Vpc(stack, 'Vpc'); + }); + + test('warns when metadataOptions is a token', () => { + // GIVEN + const asg = new AutoScalingGroup(stack, 'AutoScalingGroup', { + vpc, + instanceType: new ec2.InstanceType('t2.micro'), + machineImage: ec2.MachineImage.latestAmazonLinux(), + }); + const launchConfig = asg.node.tryFindChild('LaunchConfig') as CfnLaunchConfiguration; + launchConfig.metadataOptions = fakeToken(); + const aspect = new AutoScalingGroupRequireImdsv2Aspect(); + + // WHEN + cdk.Aspects.of(stack).add(aspect); + + // THEN + expectCDK(stack).notTo(haveResourceLike('AWS::AutoScaling::LaunchConfiguration', { + MetadataOptions: { + HttpTokens: 'required', + }, + })); + expect(asg.node.metadataEntry).toContainEqual({ + data: expect.stringContaining('CfnLaunchConfiguration.MetadataOptions field is a CDK token.'), + type: 'aws:cdk:warning', + trace: undefined, + }); + }); + + test('requires IMDSv2', () => { + // GIVEN + new AutoScalingGroup(stack, 'AutoScalingGroup', { + vpc, + instanceType: new ec2.InstanceType('t2.micro'), + machineImage: ec2.MachineImage.latestAmazonLinux(), + }); + const aspect = new AutoScalingGroupRequireImdsv2Aspect(); + + // WHEN + cdk.Aspects.of(stack).add(aspect); + + // THEN + expectCDK(stack).to(haveResourceLike('AWS::AutoScaling::LaunchConfiguration', { + MetadataOptions: { + HttpTokens: 'required', + }, + })); + }); +}); + +function fakeToken(): cdk.IResolvable { + return { + creationStack: [], + resolve: (_c) => {}, + toString: () => '', + }; +} diff --git a/packages/@aws-cdk/aws-autoscaling/test/auto-scaling-group.test.ts b/packages/@aws-cdk/aws-autoscaling/test/auto-scaling-group.test.ts index 64795593e8ec4..d74860638fd30 100644 --- a/packages/@aws-cdk/aws-autoscaling/test/auto-scaling-group.test.ts +++ b/packages/@aws-cdk/aws-autoscaling/test/auto-scaling-group.test.ts @@ -1364,6 +1364,27 @@ describe('auto scaling group', () => { }); + + test('requires imdsv2', () => { + // GIVEN + const stack = new cdk.Stack(); + const vpc = mockVpc(stack); + + // WHEN + new autoscaling.AutoScalingGroup(stack, 'MyASG', { + vpc, + instanceType: new ec2.InstanceType('t2.micro'), + machineImage: ec2.MachineImage.latestAmazonLinux(), + requireImdsv2: true, + }); + + // THEN + expect(stack).toHaveResourceLike('AWS::AutoScaling::LaunchConfiguration', { + MetadataOptions: { + HttpTokens: 'required', + }, + }); + }); }); function mockVpc(stack: cdk.Stack) { diff --git a/packages/@aws-cdk/aws-codebuild/lib/project.ts b/packages/@aws-cdk/aws-codebuild/lib/project.ts index 18dbda2cbcd3a..186c58f84138f 100644 --- a/packages/@aws-cdk/aws-codebuild/lib/project.ts +++ b/packages/@aws-cdk/aws-codebuild/lib/project.ts @@ -1656,8 +1656,9 @@ class ArmBuildImage implements IBuildImage { public validate(buildEnvironment: BuildEnvironment): string[] { const ret = []; if (buildEnvironment.computeType && + buildEnvironment.computeType !== ComputeType.SMALL && buildEnvironment.computeType !== ComputeType.LARGE) { - ret.push(`ARM images only support ComputeType '${ComputeType.LARGE}' - ` + + ret.push(`ARM images only support ComputeTypes '${ComputeType.SMALL}' and '${ComputeType.LARGE}' - ` + `'${buildEnvironment.computeType}' was given`); } return ret; diff --git a/packages/@aws-cdk/aws-codebuild/test/codebuild.test.ts b/packages/@aws-cdk/aws-codebuild/test/codebuild.test.ts index cc8f9309bcb44..f5bb3eacda8d1 100644 --- a/packages/@aws-cdk/aws-codebuild/test/codebuild.test.ts +++ b/packages/@aws-cdk/aws-codebuild/test/codebuild.test.ts @@ -1590,17 +1590,21 @@ describe('ARM image', () => { }); }); - test('cannot be used in conjunction with ComputeType SMALL', () => { + test('can be used with ComputeType SMALL', () => { const stack = new cdk.Stack(); + new codebuild.PipelineProject(stack, 'Project', { + environment: { + computeType: codebuild.ComputeType.SMALL, + buildImage: codebuild.LinuxBuildImage.AMAZON_LINUX_2_ARM, + }, + }); - expect(() => { - new codebuild.PipelineProject(stack, 'Project', { - environment: { - buildImage: codebuild.LinuxBuildImage.AMAZON_LINUX_2_ARM, - computeType: codebuild.ComputeType.SMALL, - }, - }); - }).toThrow(/ARM images only support ComputeType 'BUILD_GENERAL1_LARGE' - 'BUILD_GENERAL1_SMALL' was given/); + expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { + 'Environment': { + 'Type': 'ARM_CONTAINER', + 'ComputeType': 'BUILD_GENERAL1_SMALL', + }, + }); }); test('cannot be used in conjunction with ComputeType MEDIUM', () => { @@ -1613,7 +1617,24 @@ describe('ARM image', () => { computeType: codebuild.ComputeType.MEDIUM, }, }); - }).toThrow(/ARM images only support ComputeType 'BUILD_GENERAL1_LARGE' - 'BUILD_GENERAL1_MEDIUM' was given/); + }).toThrow(/ARM images only support ComputeTypes 'BUILD_GENERAL1_SMALL' and 'BUILD_GENERAL1_LARGE' - 'BUILD_GENERAL1_MEDIUM' was given/); + }); + + test('can be used with ComputeType LARGE', () => { + const stack = new cdk.Stack(); + new codebuild.PipelineProject(stack, 'Project', { + environment: { + computeType: codebuild.ComputeType.LARGE, + buildImage: codebuild.LinuxBuildImage.AMAZON_LINUX_2_ARM, + }, + }); + + expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { + 'Environment': { + 'Type': 'ARM_CONTAINER', + 'ComputeType': 'BUILD_GENERAL1_LARGE', + }, + }); }); test('cannot be used in conjunction with ComputeType X2_LARGE', () => { @@ -1626,7 +1647,7 @@ describe('ARM image', () => { computeType: codebuild.ComputeType.X2_LARGE, }, }); - }).toThrow(/ARM images only support ComputeType 'BUILD_GENERAL1_LARGE' - 'BUILD_GENERAL1_2XLARGE' was given/); + }).toThrow(/ARM images only support ComputeTypes 'BUILD_GENERAL1_SMALL' and 'BUILD_GENERAL1_LARGE' - 'BUILD_GENERAL1_2XLARGE' was given/); }); }); }); diff --git a/packages/@aws-cdk/aws-codepipeline-actions/README.md b/packages/@aws-cdk/aws-codepipeline-actions/README.md index e57381c517c1f..05860cfe20233 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/README.md +++ b/packages/@aws-cdk/aws-codepipeline-actions/README.md @@ -11,7 +11,7 @@ This package contains Actions that can be used in a CodePipeline. -```ts +```ts nofixture import * as codepipeline from '@aws-cdk/aws-codepipeline'; import * as codepipeline_actions from '@aws-cdk/aws-codepipeline-actions'; ``` @@ -23,10 +23,8 @@ import * as codepipeline_actions from '@aws-cdk/aws-codepipeline-actions'; To use a CodeCommit Repository in a CodePipeline: ```ts -import * as codecommit from '@aws-cdk/aws-codecommit'; - const repo = new codecommit.Repository(this, 'Repo', { - // ... + repositoryName: 'MyRepo', }); const pipeline = new codepipeline.Pipeline(this, 'MyPipeline', { @@ -49,6 +47,7 @@ You can specify the role object in eventRole property. ```ts const eventRole = iam.Role.fromRoleArn(this, 'Event-role', 'roleArn'); +declare const repo: codecommit.Repository; const sourceAction = new codepipeline_actions.CodeCommitSourceAction({ actionName: 'CodeCommit', repository: repo, @@ -61,6 +60,8 @@ If you want to clone the entire CodeCommit repository (only available for CodeBu you can set the `codeBuildCloneOutput` property to `true`: ```ts +declare const project: codebuild.PipelineProject; +declare const repo: codecommit.Repository; const sourceOutput = new codepipeline.Artifact(); const sourceAction = new codepipeline_actions.CodeCommitSourceAction({ actionName: 'CodeCommit', @@ -80,15 +81,22 @@ const buildAction = new codepipeline_actions.CodeBuildAction({ The CodeCommit source action emits variables: ```ts +declare const project: codebuild.PipelineProject; +declare const repo: codecommit.Repository; +const sourceOutput = new codepipeline.Artifact(); const sourceAction = new codepipeline_actions.CodeCommitSourceAction({ - // ... + actionName: 'CodeCommit', + repository: repo, + output: sourceOutput, variablesNamespace: 'MyNamespace', // optional - by default, a name will be generated for you }); // later: new codepipeline_actions.CodeBuildAction({ - // ... + actionName: 'CodeBuild', + project, + input: sourceOutput, environmentVariables: { COMMIT_ID: { value: sourceAction.variables.commitId, @@ -107,20 +115,21 @@ If you want to use a GitHub repository as the source, you must create: with the value of the **GitHub Access Token**. Pick whatever name you want (for example `my-github-token`). This token can be stored either as Plaintext or as a Secret key/value. If you stored the token as Plaintext, - set `cdk.SecretValue.secretsManager('my-github-token')` as the value of `oauthToken`. + set `SecretValue.secretsManager('my-github-token')` as the value of `oauthToken`. If you stored it as a Secret key/value, - you must set `cdk.SecretValue.secretsManager('my-github-token', { jsonField : 'my-github-token' })` as the value of `oauthToken`. + you must set `SecretValue.secretsManager('my-github-token', { jsonField : 'my-github-token' })` as the value of `oauthToken`. To use GitHub as the source of a CodePipeline: ```ts // Read the secret from Secrets Manager +const pipeline = new codepipeline.Pipeline(this, 'MyPipeline'); const sourceOutput = new codepipeline.Artifact(); const sourceAction = new codepipeline_actions.GitHubSourceAction({ actionName: 'GitHub_Source', owner: 'awslabs', repo: 'aws-cdk', - oauthToken: cdk.SecretValue.secretsManager('my-github-token'), + oauthToken: SecretValue.secretsManager('my-github-token'), output: sourceOutput, branch: 'develop', // default: 'master' }); @@ -133,15 +142,24 @@ pipeline.addStage({ The GitHub source action emits variables: ```ts +declare const sourceOutput: codepipeline.Artifact; +declare const project: codebuild.PipelineProject; + const sourceAction = new codepipeline_actions.GitHubSourceAction({ - // ... + actionName: 'Github_Source', + output: sourceOutput, + owner: 'my-owner', + repo: 'my-repo', + oauthToken: SecretValue.secretsManager('my-github-token'), variablesNamespace: 'MyNamespace', // optional - by default, a name will be generated for you }); // later: new codepipeline_actions.CodeBuildAction({ - // ... + actionName: 'CodeBuild', + project, + input: sourceOutput, environmentVariables: { COMMIT_URL: { value: sourceAction.variables.commitUrl, @@ -185,8 +203,6 @@ You can also use the `CodeStarConnectionsSourceAction` to connect to GitHub, in To use an S3 Bucket as a source in CodePipeline: ```ts -import * as s3 from '@aws-cdk/aws-s3'; - const sourceBucket = new s3.Bucket(this, 'MyBucket', { versioned: true, // a Bucket used as a source in CodePipeline must be versioned }); @@ -229,6 +245,8 @@ You can do it through the CDK: ```ts import * as cloudtrail from '@aws-cdk/aws-cloudtrail'; +declare const sourceBucket: s3.Bucket; +const sourceOutput = new codepipeline.Artifact(); const key = 'some/key.zip'; const trail = new cloudtrail.Trail(this, 'CloudTrail'); trail.addS3EventSelector([{ @@ -249,15 +267,23 @@ const sourceAction = new codepipeline_actions.S3SourceAction({ The S3 source action emits variables: ```ts +const key = 'some/key.zip'; +declare const sourceBucket: s3.Bucket; +const sourceOutput = new codepipeline.Artifact(); const sourceAction = new codepipeline_actions.S3SourceAction({ - // ... + actionName: 'S3Source', + bucketKey: key, + bucket: sourceBucket, + output: sourceOutput, variablesNamespace: 'MyNamespace', // optional - by default, a name will be generated for you }); // later: - +declare const project: codebuild.PipelineProject; new codepipeline_actions.CodeBuildAction({ - // ... + actionName: 'CodeBuild', + project, + input: sourceOutput, environmentVariables: { VERSION_ID: { value: sourceAction.variables.versionId, @@ -273,6 +299,7 @@ To use an ECR Repository as a source in a Pipeline: ```ts import * as ecr from '@aws-cdk/aws-ecr'; +declare const ecrRepository: ecr.Repository; const pipeline = new codepipeline.Pipeline(this, 'MyPipeline'); const sourceOutput = new codepipeline.Artifact(); const sourceAction = new codepipeline_actions.EcrSourceAction({ @@ -290,15 +317,23 @@ pipeline.addStage({ The ECR source action emits variables: ```ts +import * as ecr from '@aws-cdk/aws-ecr'; + +const sourceOutput = new codepipeline.Artifact(); +declare const ecrRepository: ecr.Repository; const sourceAction = new codepipeline_actions.EcrSourceAction({ - // ... + actionName: 'Source', + output: sourceOutput, + repository: ecrRepository, variablesNamespace: 'MyNamespace', // optional - by default, a name will be generated for you }); // later: - +declare const project: codebuild.PipelineProject; new codepipeline_actions.CodeBuildAction({ - // ... + actionName: 'CodeBuild', + project, + input: sourceOutput, environmentVariables: { IMAGE_URI: { value: sourceAction.variables.imageUri, @@ -314,9 +349,7 @@ new codepipeline_actions.CodeBuildAction({ Example of a CodeBuild Project used in a Pipeline, alongside CodeCommit: ```ts -import * as codebuild from '@aws-cdk/aws-codebuild'; -import * as codecommit from '@aws-cdk/aws-codecommit'; - +declare const project: codebuild.PipelineProject; const repository = new codecommit.Repository(this, 'MyRepository', { repositoryName: 'MyRepository', }); @@ -356,6 +389,8 @@ if you want a `Test` Action instead, override the `type` property: ```ts +declare const project: codebuild.PipelineProject; +const sourceOutput = new codepipeline.Artifact(); const testAction = new codepipeline_actions.CodeBuildAction({ actionName: 'IntegrationTest', project, @@ -373,12 +408,14 @@ properties of the `Project` class, you need to use the `extraInputs` and Actions. Example: ```ts +declare const repository1: codecommit.Repository; const sourceOutput1 = new codepipeline.Artifact(); const sourceAction1 = new codepipeline_actions.CodeCommitSourceAction({ actionName: 'Source1', repository: repository1, output: sourceOutput1, }); +declare const repository2: codecommit.Repository; const sourceOutput2 = new codepipeline.Artifact('source2'); const sourceAction2 = new codepipeline_actions.CodeCommitSourceAction({ actionName: 'Source2', @@ -386,6 +423,7 @@ const sourceAction2 = new codepipeline_actions.CodeCommitSourceAction({ output: sourceOutput2, }); +declare const project: codebuild.PipelineProject; const buildAction = new codepipeline_actions.CodeBuildAction({ actionName: 'Build', project, @@ -447,6 +485,7 @@ in the 'exported-variables' subsection of the 'env' section. Example: ```ts +const sourceOutput = new codepipeline.Artifact(); const buildAction = new codepipeline_actions.CodeBuildAction({ actionName: 'Build1', input: sourceOutput, @@ -469,9 +508,11 @@ const buildAction = new codepipeline_actions.CodeBuildAction({ }); // later: - +declare const project: codebuild.PipelineProject; new codepipeline_actions.CodeBuildAction({ - // ... + actionName: 'CodeBuild', + project, + input: sourceOutput, environmentVariables: { MyVar: { value: buildAction.variable('MY_VAR'), @@ -498,7 +539,7 @@ or outside the CDK (in the CodePipeline AWS Console, for example), you can import it: ```ts -const jenkinsProvider = codepipeline_actions.JenkinsProvider.import(this, 'JenkinsProvider', { +const jenkinsProvider = codepipeline_actions.JenkinsProvider.fromJenkinsProviderAttributes(this, 'JenkinsProvider', { providerName: 'MyJenkinsProvider', serverUrl: 'http://my-jenkins.com:8080', version: '2', // optional, default: '1' @@ -514,6 +555,7 @@ With a `JenkinsProvider`, we can create a Jenkins Action: ```ts +declare const jenkinsProvider: codepipeline_actions.JenkinsProvider; const buildAction = new codepipeline_actions.JenkinsAction({ actionName: 'JenkinsBuild', jenkinsProvider: jenkinsProvider, @@ -566,8 +608,12 @@ If you want to update stacks in a different account, pass the `account` property when creating the action: ```ts +const sourceOutput = new codepipeline.Artifact(); new codepipeline_actions.CloudFormationCreateUpdateStackAction({ - // ... + actionName: 'CloudFormationCreateUpdate', + stackName: 'MyStackName', + adminPermissions: true, + templatePath: sourceOutput.atPath('template.yaml'), account: '123456789012', }); ``` @@ -584,15 +630,20 @@ and the action will operate in the same account the role belongs to: import { PhysicalName } from '@aws-cdk/core'; // in stack for account 123456789012... +declare const otherAccountStack: Stack; const actionRole = new iam.Role(otherAccountStack, 'ActionRole', { - assumedBy: new iam.AccountPrincipal(pipelineAccount), + assumedBy: new iam.AccountPrincipal('123456789012'), // the role has to have a physical name set roleName: PhysicalName.GENERATE_IF_NEEDED, }); // in the pipeline stack... +const sourceOutput = new codepipeline.Artifact(); new codepipeline_actions.CloudFormationCreateUpdateStackAction({ - // ... + actionName: 'CloudFormationCreateUpdate', + stackName: 'MyStackName', + adminPermissions: true, + templatePath: sourceOutput.atPath('template.yaml'), role: actionRole, // this action will be cross-account as well }); ``` @@ -604,14 +655,13 @@ new codepipeline_actions.CloudFormationCreateUpdateStackAction({ To use CodeDeploy for EC2/on-premise deployments in a Pipeline: ```ts -import * as codedeploy from '@aws-cdk/aws-codedeploy'; - const pipeline = new codepipeline.Pipeline(this, 'MyPipeline', { pipelineName: 'MyPipeline', }); // add the source and build Stages to the Pipeline... - +const buildOutput = new codepipeline.Artifact(); +declare const deploymentGroup: codedeploy.ServerDeploymentGroup; const deployAction = new codepipeline_actions.CodeDeployServerDeployAction({ actionName: 'CodeDeploy', input: buildOutput, @@ -629,19 +679,19 @@ To use CodeDeploy for blue-green Lambda deployments in a Pipeline: ```ts const lambdaCode = lambda.Code.fromCfnParameters(); -const func = new lambda.Function(lambdaStack, 'Lambda', { +const func = new lambda.Function(this, 'Lambda', { code: lambdaCode, handler: 'index.handler', runtime: lambda.Runtime.NODEJS_12_X, }); // used to make sure each CDK synthesis produces a different Version const version = func.addVersion('NewVersion'); -const alias = new lambda.Alias(lambdaStack, 'LambdaAlias', { +const alias = new lambda.Alias(this, 'LambdaAlias', { aliasName: 'Prod', version, }); -new codedeploy.LambdaDeploymentGroup(lambdaStack, 'DeploymentGroup', { +new codedeploy.LambdaDeploymentGroup(this, 'DeploymentGroup', { alias, deploymentConfig: codedeploy.LambdaDeploymentConfig.LINEAR_10PERCENT_EVERY_1MINUTE, }); @@ -658,6 +708,11 @@ CodePipeline can deploy an ECS service. The deploy Action receives one input Artifact which contains the [image definition file]: ```ts +import * as ecs from '@aws-cdk/aws-ecs'; + +declare const service: ecs.FargateService; +const pipeline = new codepipeline.Pipeline(this, 'MyPipeline'); +const buildOutput = new codepipeline.Artifact(); const deployStage = pipeline.addStage({ stageName: 'Deploy', actions: [ @@ -672,7 +727,7 @@ const deployStage = pipeline.addStage({ // use the `imageFile` property, // and leave out the `input` property imageFile: buildOutput.atPath('imageDef.json'), - deploymentTimeout: cdk.Duration.minutes(60), // optional, default is 60 minutes + deploymentTimeout: Duration.minutes(60), // optional, default is 60 minutes }), ], }); @@ -697,12 +752,12 @@ Here's an example: To use an S3 Bucket as a deployment target in CodePipeline: ```ts -const targetBucket = new s3.Bucket(this, 'MyBucket', {}); +const sourceOutput = new codepipeline.Artifact(); +const targetBucket = new s3.Bucket(this, 'MyBucket'); const pipeline = new codepipeline.Pipeline(this, 'MyPipeline'); const deployAction = new codepipeline_actions.S3DeployAction({ actionName: 'S3Deploy', - stage: deployStage, bucket: targetBucket, input: sourceOutput, }); @@ -720,9 +775,8 @@ and use the AWS CLI to invalidate the cache: ```ts // Create a Cloudfront Web Distribution -const distribution = new cloudfront.Distribution(this, `Distribution`, { - // ... -}); +import * as cloudfront from '@aws-cdk/aws-cloudfront'; +declare const distribution: cloudfront.Distribution; // Create the build project that will invalidate the cache const invalidateBuildProject = new codebuild.PipelineProject(this, `InvalidateProject`, { @@ -752,19 +806,21 @@ invalidateBuildProject.addToRolePolicy(new iam.PolicyStatement({ })); // Create the pipeline (here only the S3 deploy and Invalidate cache build) +const deployBucket = new s3.Bucket(this, 'DeployBucket'); +const deployInput = new codepipeline.Artifact(); new codepipeline.Pipeline(this, 'Pipeline', { stages: [ // ... { stageName: 'Deploy', actions: [ - new codepipelineActions.S3DeployAction({ + new codepipeline_actions.S3DeployAction({ actionName: 'S3Deploy', bucket: deployBucket, input: deployInput, runOrder: 1, }), - new codepipelineActions.CodeBuildAction({ + new codepipeline_actions.CodeBuildAction({ actionName: 'InvalidateCache', project: invalidateBuildProject, input: deployInput, @@ -782,11 +838,12 @@ You can deploy to Alexa using CodePipeline with the following Action: ```ts // Read the secrets from ParameterStore -const clientId = cdk.SecretValue.secretsManager('AlexaClientId'); -const clientSecret = cdk.SecretValue.secretsManager('AlexaClientSecret'); -const refreshToken = cdk.SecretValue.secretsManager('AlexaRefreshToken'); +const clientId = SecretValue.secretsManager('AlexaClientId'); +const clientSecret = SecretValue.secretsManager('AlexaClientSecret'); +const refreshToken = SecretValue.secretsManager('AlexaRefreshToken'); // Add deploy action +const sourceOutput = new codepipeline.Artifact(); new codepipeline_actions.AlexaSkillDeployAction({ actionName: 'DeploySkill', runOrder: 1, @@ -801,20 +858,22 @@ new codepipeline_actions.AlexaSkillDeployAction({ If you need manifest overrides you can specify them as `parameterOverridesArtifact` in the action: ```ts -import * as cloudformation from '@aws-cdk/aws-cloudformation'; - // Deploy some CFN change set and store output const executeOutput = new codepipeline.Artifact('CloudFormation'); const executeChangeSetAction = new codepipeline_actions.CloudFormationExecuteChangeSetAction({ actionName: 'ExecuteChangesTest', runOrder: 2, - stackName, - changeSetName, + stackName: 'MyStack', + changeSetName: 'MyChangeSet', outputFileName: 'overrides.json', output: executeOutput, }); // Provide CFN output as manifest overrides +const clientId = SecretValue.secretsManager('AlexaClientId'); +const clientSecret = SecretValue.secretsManager('AlexaClientSecret'); +const refreshToken = SecretValue.secretsManager('AlexaRefreshToken'); +const sourceOutput = new codepipeline.Artifact(); new codepipeline_actions.AlexaSkillDeployAction({ actionName: 'DeploySkill', runOrder: 1, @@ -832,11 +891,11 @@ new codepipeline_actions.AlexaSkillDeployAction({ You can deploy a CloudFormation template to an existing Service Catalog product with the following Action: ```ts +const cdkBuildOutput = new codepipeline.Artifact(); const serviceCatalogDeployAction = new codepipeline_actions.ServiceCatalogDeployActionBeta1({ actionName: 'ServiceCatalogDeploy', templatePath: cdkBuildOutput.atPath("Sample.template.json"), productVersionName: "Version - " + Date.now.toString, - productType: "CLOUD_FORMATION_TEMPLATE", productVersionDescription: "This is a version from the pipeline with a new description.", productId: "prod-XXXXXXXX", }); @@ -849,6 +908,10 @@ const serviceCatalogDeployAction = new codepipeline_actions.ServiceCatalogDeploy This package contains an Action that stops the Pipeline until someone manually clicks the approve button: ```ts +import * as sns from '@aws-cdk/aws-sns'; + +const pipeline = new codepipeline.Pipeline(this, 'MyPipeline'); +const approveStage = pipeline.addStage({ stageName: 'Approve' }); const manualApprovalAction = new codepipeline_actions.ManualApprovalAction({ actionName: 'Approve', notificationTopic: new sns.Topic(this, 'Topic'), // optional @@ -871,12 +934,14 @@ If you want to grant a principal permissions to approve the changes, you can invoke the method `grantManualApproval` passing it a `IGrantable`: ```ts +const pipeline = new codepipeline.Pipeline(this, 'MyPipeline'); +const approveStage = pipeline.addStage({ stageName: 'Approve' }); const manualApprovalAction = new codepipeline_actions.ManualApprovalAction({ actionName: 'Approve', }); approveStage.addAction(manualApprovalAction); -const role = iam.Role.fromRoleArn(this, 'Admin', Arn.format({ service: 'iam', resource: 'role', resourceName: 'Admin' }, stack)); +const role = iam.Role.fromRoleArn(this, 'Admin', Arn.format({ service: 'iam', resource: 'role', resourceName: 'Admin' }, this)); manualApprovalAction.grantManualApproval(role); ``` @@ -885,8 +950,7 @@ manualApprovalAction.grantManualApproval(role); This module contains an Action that allows you to invoke a Lambda function in a Pipeline: ```ts -import * as lambda from '@aws-cdk/aws-lambda'; - +declare const fn: lambda.Function; const pipeline = new codepipeline.Pipeline(this, 'MyPipeline'); const lambdaAction = new codepipeline_actions.LambdaInvokeAction({ actionName: 'Lambda', @@ -902,7 +966,9 @@ The Lambda Action can have up to 5 inputs, and up to 5 outputs: ```ts - +declare const fn: lambda.Function; +const sourceOutput = new codepipeline.Artifact(); +const buildOutput = new codepipeline.Artifact(); const lambdaAction = new codepipeline_actions.LambdaInvokeAction({ actionName: 'Lambda', inputs: [ @@ -913,7 +979,26 @@ const lambdaAction = new codepipeline_actions.LambdaInvokeAction({ new codepipeline.Artifact('Out1'), new codepipeline.Artifact('Out2'), ], - lambda: fn + lambda: fn, +}); +``` + +The Lambda Action supports custom user parameters that pipeline +will pass to the Lambda function: + +```ts +import * as lambda from '@aws-cdk/aws-lambda'; + +const pipeline = new codepipeline.Pipeline(this, 'MyPipeline'); +const lambdaAction = new codepipeline_actions.LambdaInvokeAction({ + actionName: 'Lambda', + lambda: fn, + userParameters: { + foo: 'bar', + baz: 'qux', + }, + // OR + userParametersString: 'my-parameter-string', }); ``` @@ -924,8 +1009,6 @@ API with the `outputVariables` property filled with the map of variables Example: ```ts -import * as lambda from '@aws-cdk/aws-lambda'; - const lambdaInvokeAction = new codepipeline_actions.LambdaInvokeAction({ actionName: 'Lambda', lambda: new lambda.Function(this, 'Func', { @@ -949,9 +1032,12 @@ const lambdaInvokeAction = new codepipeline_actions.LambdaInvokeAction({ }); // later: - +declare const project: codebuild.PipelineProject; +const sourceOutput = new codepipeline.Artifact(); new codepipeline_actions.CodeBuildAction({ - // ... + actionName: 'CodeBuild', + project, + input: sourceOutput, environmentVariables: { MyVar: { value: lambdaInvokeAction.variable('MY_VAR'), @@ -968,14 +1054,13 @@ on how to write a Lambda function invoked from CodePipeline. This module contains an Action that allows you to invoke a Step Function in a Pipeline: ```ts -import * as stepfunction from '@aws-cdk/aws-stepfunctions'; - +import * as stepfunctions from '@aws-cdk/aws-stepfunctions'; const pipeline = new codepipeline.Pipeline(this, 'MyPipeline'); -const startState = new stepfunction.Pass(stack, 'StartState'); -const simpleStateMachine = new stepfunction.StateMachine(stack, 'SimpleStateMachine', { +const startState = new stepfunctions.Pass(this, 'StartState'); +const simpleStateMachine = new stepfunctions.StateMachine(this, 'SimpleStateMachine', { definition: startState, }); -const stepFunctionAction = new codepipeline_actions.StepFunctionsInvokeAction({ +const stepFunctionAction = new codepipeline_actions.StepFunctionInvokeAction({ actionName: 'Invoke', stateMachine: simpleStateMachine, stateMachineInput: codepipeline_actions.StateMachineInput.literal({ IsHelloWorldExample: true }), @@ -990,15 +1075,15 @@ The `StateMachineInput` can be created with one of 2 static factory methods: `literal`, which takes an arbitrary map as its only argument, or `filePath`: ```ts -import * as stepfunction from '@aws-cdk/aws-stepfunctions'; +import * as stepfunctions from '@aws-cdk/aws-stepfunctions'; const pipeline = new codepipeline.Pipeline(this, 'MyPipeline'); const inputArtifact = new codepipeline.Artifact(); -const startState = new stepfunction.Pass(stack, 'StartState'); -const simpleStateMachine = new stepfunction.StateMachine(stack, 'SimpleStateMachine', { +const startState = new stepfunctions.Pass(this, 'StartState'); +const simpleStateMachine = new stepfunctions.StateMachine(this, 'SimpleStateMachine', { definition: startState, }); -const stepFunctionAction = new codepipeline_actions.StepFunctionsInvokeAction({ +const stepFunctionAction = new codepipeline_actions.StepFunctionInvokeAction({ actionName: 'Invoke', stateMachine: simpleStateMachine, stateMachineInput: codepipeline_actions.StateMachineInput.filePath(inputArtifact.atPath('assets/input.json')), diff --git a/packages/@aws-cdk/aws-codepipeline-actions/lib/lambda/invoke-action.ts b/packages/@aws-cdk/aws-codepipeline-actions/lib/lambda/invoke-action.ts index f8a49d977f6b4..8740d8fafb9ff 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/lib/lambda/invoke-action.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/lib/lambda/invoke-action.ts @@ -38,10 +38,24 @@ export interface LambdaInvokeActionProps extends codepipeline.CommonAwsActionPro * A set of key-value pairs that will be accessible to the invoked Lambda * inside the event that the Pipeline will call it with. * + * Only one of `userParameters` or `userParametersString` can be specified. + * * @see https://docs.aws.amazon.com/codepipeline/latest/userguide/actions-invoke-lambda-function.html#actions-invoke-lambda-function-json-event-example + * @default - no user parameters will be passed */ readonly userParameters?: { [key: string]: any }; + /** + * The string representation of the user parameters that will be + * accessible to the invoked Lambda inside the event + * that the Pipeline will call it with. + * + * Only one of `userParametersString` or `userParameters` can be specified. + * + * @default - no user parameters will be passed + */ + readonly userParametersString?: string; + /** * The lambda function to invoke. */ @@ -71,6 +85,10 @@ export class LambdaInvokeAction extends Action { }); this.props = props; + + if (props.userParameters && props.userParametersString) { + throw new Error('Only one of userParameters or userParametersString can be specified'); + } } /** @@ -121,7 +139,7 @@ export class LambdaInvokeAction extends Action { return { configuration: { FunctionName: this.props.lambda.functionName, - UserParameters: Stack.of(scope).toJsonString(this.props.userParameters), + UserParameters: this.props.userParametersString ?? Stack.of(scope).toJsonString(this.props.userParameters), }, }; } diff --git a/packages/@aws-cdk/aws-codepipeline-actions/lib/servicecatalog/deploy-action-beta1.ts b/packages/@aws-cdk/aws-codepipeline-actions/lib/servicecatalog/deploy-action-beta1.ts index 34b055c14735b..302b82b2a8725 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/lib/servicecatalog/deploy-action-beta1.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/lib/servicecatalog/deploy-action-beta1.ts @@ -35,12 +35,8 @@ export interface ServiceCatalogDeployActionBeta1Props extends codepipeline.Commo /** * CodePipeline action to connect to an existing ServiceCatalog product. -<<<<<<< HEAD:packages/@aws-cdk/aws-codepipeline-actions/lib/servicecatalog/deploy-action.ts * * **Note**: this class is still experimental, and may have breaking changes in the future! - * -======= ->>>>>>> master:packages/@aws-cdk/aws-codepipeline-actions/lib/servicecatalog/deploy-action-beta1.ts */ export class ServiceCatalogDeployActionBeta1 extends Action { private readonly templatePath: string; diff --git a/packages/@aws-cdk/aws-codepipeline-actions/rosetta/default.ts-fixture b/packages/@aws-cdk/aws-codepipeline-actions/rosetta/default.ts-fixture new file mode 100644 index 0000000000000..aba6086a1683e --- /dev/null +++ b/packages/@aws-cdk/aws-codepipeline-actions/rosetta/default.ts-fixture @@ -0,0 +1,17 @@ +// Fixture with packages imported, but nothing else +import { Arn, Construct, Duration, SecretValue, Stack } from '@aws-cdk/core'; +import codebuild = require('@aws-cdk/aws-codebuild'); +import codedeploy = require('@aws-cdk/aws-codedeploy'); +import codepipeline = require('@aws-cdk/aws-codepipeline'); +import codepipeline_actions = require('@aws-cdk/aws-codepipeline-actions'); +import codecommit = require('@aws-cdk/aws-codecommit'); +import iam = require('@aws-cdk/aws-iam'); +import lambda = require('@aws-cdk/aws-lambda'); +import s3 = require('@aws-cdk/aws-s3'); + +class Fixture extends Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + /// here + } +} diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.lambda-deployed-through-codepipeline.lit.ts b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.lambda-deployed-through-codepipeline.lit.ts index e631dd6b10c1b..3f9a337333915 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.lambda-deployed-through-codepipeline.lit.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.lambda-deployed-through-codepipeline.lit.ts @@ -124,9 +124,7 @@ pipeline.addStage({ templatePath: cdkBuildOutput.atPath('LambdaStack.template.yaml'), stackName: 'LambdaStackDeployedName', adminPermissions: true, - parameterOverrides: { - ...lambdaCode.assign(lambdaBuildOutput.s3Location), - }, + parameterOverrides: lambdaCode.assign(lambdaBuildOutput.s3Location), extraInputs: [ lambdaBuildOutput, ], diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/lambda/lambda-invoke-action.test.ts b/packages/@aws-cdk/aws-codepipeline-actions/test/lambda/lambda-invoke-action.test.ts index c9c694b6cedaf..41d1b13563235 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/lambda/lambda-invoke-action.test.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/lambda/lambda-invoke-action.test.ts @@ -3,9 +3,9 @@ import * as codepipeline from '@aws-cdk/aws-codepipeline'; import * as lambda from '@aws-cdk/aws-lambda'; import * as s3 from '@aws-cdk/aws-s3'; import * as sns from '@aws-cdk/aws-sns'; +import { testFutureBehavior } from '@aws-cdk/cdk-build-tools/lib/feature-flag'; import { App, Aws, Lazy, SecretValue, Stack, Token } from '@aws-cdk/core'; import * as cxapi from '@aws-cdk/cx-api'; -import { testFutureBehavior } from '@aws-cdk/cdk-build-tools/lib/feature-flag'; import * as cpactions from '../../lib'; /* eslint-disable quote-props */ @@ -100,6 +100,36 @@ describe('', () => { }); + test('properly assings userParametersString to UserParameters', () => { + const stack = stackIncludingLambdaInvokeCodePipeline({ + userParamsString: '**/*.template.json', + }); + + expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + 'Stages': [ + {}, + { + 'Actions': [ + { + 'Configuration': { + 'UserParameters': '**/*.template.json', + }, + }, + ], + }, + ], + }); + }); + + test('throws if both userParameters and userParametersString are supplied', () => { + expect(() => stackIncludingLambdaInvokeCodePipeline({ + userParams: { + key: Token.asString(null), + }, + userParamsString: '**/*.template.json', + })).toThrow(/Only one of userParameters or userParametersString can be specified/); + }); + test("assigns the Action's Role with read permissions to the Bucket if it has only inputs", () => { const stack = stackIncludingLambdaInvokeCodePipeline({ lambdaInput: new codepipeline.Artifact(), @@ -302,6 +332,7 @@ describe('', () => { interface HelperProps { readonly userParams?: { [key: string]: any }; + readonly userParamsString?: string; readonly lambdaInput?: codepipeline.Artifact; readonly lambdaOutput?: codepipeline.Artifact; } @@ -334,6 +365,7 @@ function stackIncludingLambdaInvokeCodePipeline(props: HelperProps, app?: App) { runtime: lambda.Runtime.NODEJS_10_X, }), userParameters: props.userParams, + userParametersString: props.userParamsString, inputs: props.lambdaInput ? [props.lambdaInput] : undefined, outputs: props.lambdaOutput ? [props.lambdaOutput] : undefined, }), diff --git a/packages/@aws-cdk/aws-dynamodb/lib/replica-handler/index.ts b/packages/@aws-cdk/aws-dynamodb/lib/replica-handler/index.ts index 1554dcc84004d..38109b1a4a614 100644 --- a/packages/@aws-cdk/aws-dynamodb/lib/replica-handler/index.ts +++ b/packages/@aws-cdk/aws-dynamodb/lib/replica-handler/index.ts @@ -49,12 +49,13 @@ export async function isCompleteHandler(event: IsCompleteRequest): Promise r.RegionName === event.ResourceProperties.Region); const replicaActive = !!(regionReplica?.ReplicaStatus === 'ACTIVE'); + const skipReplicationCompletedWait = event.ResourceProperties.SkipReplicationCompletedWait ?? false; switch (event.RequestType) { case 'Create': case 'Update': // Complete when replica is reported as ACTIVE - return { IsComplete: tableActive && replicaActive }; + return { IsComplete: tableActive && (replicaActive || skipReplicationCompletedWait) }; case 'Delete': // Complete when replica is gone return { IsComplete: tableActive && regionReplica === undefined }; diff --git a/packages/@aws-cdk/aws-dynamodb/lib/table.ts b/packages/@aws-cdk/aws-dynamodb/lib/table.ts index a2b1aae5ee2eb..10a7472be3c0d 100644 --- a/packages/@aws-cdk/aws-dynamodb/lib/table.ts +++ b/packages/@aws-cdk/aws-dynamodb/lib/table.ts @@ -231,6 +231,22 @@ export interface TableOptions extends SchemaOptions { */ readonly replicationTimeout?: Duration; + /** + * Indicates whether CloudFormation stack waits for replication to finish. + * If set to false, the CloudFormation resource will mark the resource as + * created and replication will be completed asynchronously. This property is + * ignored if replicationRegions property is not set. + * + * DO NOT UNSET this property if adding/removing multiple replicationRegions + * in one deployment, as CloudFormation only supports one region replication + * at a time. CDK overcomes this limitation by waiting for replication to + * finish before starting new replicationRegion. + * + * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-dynamodb-globaltable.html#cfn-dynamodb-globaltable-replicas + * @default true + */ + readonly waitForReplicationToFinish?: boolean; + /** * Whether CloudWatch contributor insights is enabled. * @@ -1152,7 +1168,7 @@ export class Table extends TableBase { } if (props.replicationRegions && props.replicationRegions.length > 0) { - this.createReplicaTables(props.replicationRegions, props.replicationTimeout); + this.createReplicaTables(props.replicationRegions, props.replicationTimeout, props.waitForReplicationToFinish); } } @@ -1494,7 +1510,7 @@ export class Table extends TableBase { * * @param regions regions where to create tables */ - private createReplicaTables(regions: string[], timeout?: Duration) { + private createReplicaTables(regions: string[], timeout?: Duration, waitForReplicationToFinish?: boolean) { const stack = Stack.of(this); if (!Token.isUnresolved(stack.region) && regions.includes(stack.region)) { @@ -1524,6 +1540,7 @@ export class Table extends TableBase { properties: { TableName: this.tableName, Region: region, + SkipReplicationCompletedWait: waitForReplicationToFinish === undefined ? undefined : !waitForReplicationToFinish, }, }); currentRegion.node.addDependency( diff --git a/packages/@aws-cdk/aws-dynamodb/test/dynamodb.test.ts b/packages/@aws-cdk/aws-dynamodb/test/dynamodb.test.ts index 56ab48f19f501..a095bff84ca25 100644 --- a/packages/@aws-cdk/aws-dynamodb/test/dynamodb.test.ts +++ b/packages/@aws-cdk/aws-dynamodb/test/dynamodb.test.ts @@ -2438,6 +2438,60 @@ describe('global', () => { }); }); + test('create replicas without waiting to finish replication', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + new Table(stack, 'Table', { + partitionKey: { + name: 'id', + type: AttributeType.STRING, + }, + replicationRegions: [ + 'eu-west-2', + 'eu-central-1', + ], + waitForReplicationToFinish: false, + }); + + // THEN + expect(stack).toHaveResource('Custom::DynamoDBReplica', { + Properties: { + TableName: { + Ref: 'TableCD117FA1', + }, + Region: 'eu-west-2', + SkipReplicationCompletedWait: true, + }, + Condition: 'TableStackRegionNotEqualseuwest2A03859E7', + }, ResourcePart.CompleteDefinition); + + expect(stack).toHaveResource('Custom::DynamoDBReplica', { + Properties: { + TableName: { + Ref: 'TableCD117FA1', + }, + Region: 'eu-central-1', + SkipReplicationCompletedWait: true, + }, + Condition: 'TableStackRegionNotEqualseucentral199D46FC0', + }, ResourcePart.CompleteDefinition); + + expect(SynthUtils.toCloudFormation(stack).Conditions).toEqual({ + TableStackRegionNotEqualseuwest2A03859E7: { + 'Fn::Not': [ + { 'Fn::Equals': ['eu-west-2', { Ref: 'AWS::Region' }] }, + ], + }, + TableStackRegionNotEqualseucentral199D46FC0: { + 'Fn::Not': [ + { 'Fn::Equals': ['eu-central-1', { Ref: 'AWS::Region' }] }, + ], + }, + }); + }); + test('grantReadData', () => { const stack = new Stack(); const table = new Table(stack, 'Table', { diff --git a/packages/@aws-cdk/aws-ec2/test/vpc.test.ts b/packages/@aws-cdk/aws-ec2/test/vpc.test.ts index ca375dc7fe321..90942056865c7 100644 --- a/packages/@aws-cdk/aws-ec2/test/vpc.test.ts +++ b/packages/@aws-cdk/aws-ec2/test/vpc.test.ts @@ -361,7 +361,7 @@ describe('vpc', () => { } }); - test('with custom subents and natGateways = 2 there should be only two NATGW', () => { + test('with custom subnets and natGateways = 2 there should be only two NATGW', () => { const stack = getTestStack(); new Vpc(stack, 'TheVPC', { cidr: '10.0.0.0/21', diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/base/queue-processing-service-base.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/base/queue-processing-service-base.ts index 2c293dc3173de..67634b3cc95e0 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/base/queue-processing-service-base.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/base/queue-processing-service-base.ts @@ -320,14 +320,14 @@ export abstract class QueueProcessingServiceBase extends CoreConstruct { // Determine the desired task count (minimum) and maximum scaling capacity if (!this.node.tryGetContext(cxapi.ECS_REMOVE_DEFAULT_DESIRED_COUNT)) { - this.minCapacity = props.minScalingCapacity || this.desiredCount; + this.minCapacity = props.minScalingCapacity ?? this.desiredCount; this.maxCapacity = props.maxScalingCapacity || (2 * this.desiredCount); } else { if (props.desiredTaskCount != null) { - this.minCapacity = props.minScalingCapacity || this.desiredCount; + this.minCapacity = props.minScalingCapacity ?? this.desiredCount; this.maxCapacity = props.maxScalingCapacity || (2 * this.desiredCount); } else { - this.minCapacity = props.minScalingCapacity || 1; + this.minCapacity = props.minScalingCapacity ?? 1; this.maxCapacity = props.maxScalingCapacity || 2; } } diff --git a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.queue-processing-fargate-service.expected.json b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.queue-processing-fargate-service.expected.json index f43e4e5aa4e0b..48bab449fe610 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.queue-processing-fargate-service.expected.json +++ b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.queue-processing-fargate-service.expected.json @@ -642,7 +642,7 @@ "Type": "AWS::ApplicationAutoScaling::ScalableTarget", "Properties": { "MaxCapacity": 2, - "MinCapacity": 1, + "MinCapacity": 0, "ResourceId": { "Fn::Join": [ "", diff --git a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.queue-processing-fargate-service.ts b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.queue-processing-fargate-service.ts index 540b8d2302e9a..a0c679f4afaa8 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.queue-processing-fargate-service.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.queue-processing-fargate-service.ts @@ -15,6 +15,7 @@ new QueueProcessingFargateService(stack, 'QueueProcessingService', { vpc, memoryLimitMiB: 512, image: new ecs.AssetImage(path.join(__dirname, '..', 'sqs-reader')), + minScalingCapacity: 0, }); app.synth(); diff --git a/packages/@aws-cdk/aws-ecs/lib/base/from-service-attributes.ts b/packages/@aws-cdk/aws-ecs/lib/base/from-service-attributes.ts index 80ec042626153..cfaff590b3eb1 100644 --- a/packages/@aws-cdk/aws-ecs/lib/base/from-service-attributes.ts +++ b/packages/@aws-cdk/aws-ecs/lib/base/from-service-attributes.ts @@ -54,5 +54,7 @@ export function fromServiceAtrributes(scope: Construct, id: string, attrs: Servi public readonly serviceName = name; public readonly cluster = attrs.cluster; } - return new Import(scope, id); + return new Import(scope, id, { + environmentFromArn: arn, + }); } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/ec2-service.test.ts b/packages/@aws-cdk/aws-ecs/test/ec2/ec2-service.test.ts index 48da04142ff3c..21d4292346974 100644 --- a/packages/@aws-cdk/aws-ecs/test/ec2/ec2-service.test.ts +++ b/packages/@aws-cdk/aws-ecs/test/ec2/ec2-service.test.ts @@ -3292,6 +3292,8 @@ describe('ec2 service', () => { expect(service.serviceArn).toEqual('arn:aws:ecs:us-west-2:123456789012:service/my-http-service'); expect(service.serviceName).toEqual('my-http-service'); + expect(service.env.account).toEqual('123456789012'); + expect(service.env.region).toEqual('us-west-2'); }); diff --git a/packages/@aws-cdk/aws-ecs/test/fargate/fargate-service.test.ts b/packages/@aws-cdk/aws-ecs/test/fargate/fargate-service.test.ts index ef4ceae4ccedc..71866a116443f 100644 --- a/packages/@aws-cdk/aws-ecs/test/fargate/fargate-service.test.ts +++ b/packages/@aws-cdk/aws-ecs/test/fargate/fargate-service.test.ts @@ -2124,6 +2124,8 @@ describe('fargate service', () => { expect(service.serviceArn).toEqual('arn:aws:ecs:us-west-2:123456789012:service/my-http-service'); expect(service.serviceName).toEqual('my-http-service'); + expect(service.env.account).toEqual('123456789012'); + expect(service.env.region).toEqual('us-west-2'); }); diff --git a/packages/@aws-cdk/aws-eks/lib/cluster-resource.ts b/packages/@aws-cdk/aws-eks/lib/cluster-resource.ts index 662f4e345a24c..88f3cd0138344 100644 --- a/packages/@aws-cdk/aws-eks/lib/cluster-resource.ts +++ b/packages/@aws-cdk/aws-eks/lib/cluster-resource.ts @@ -16,6 +16,7 @@ export interface ClusterResourceProps { readonly resourcesVpcConfig: CfnCluster.ResourcesVpcConfigProperty; readonly roleArn: string; readonly encryptionConfig?: Array; + readonly kubernetesNetworkConfig?: CfnCluster.KubernetesNetworkConfigProperty; readonly name: string; readonly version?: string; readonly endpointPrivateAccess: boolean; @@ -78,6 +79,7 @@ export class ClusterResource extends CoreConstruct { version: props.version, roleArn: props.roleArn, encryptionConfig: props.encryptionConfig, + kubernetesNetworkConfig: props.kubernetesNetworkConfig, resourcesVpcConfig: { subnetIds: (props.resourcesVpcConfig as CfnCluster.ResourcesVpcConfigProperty).subnetIds, securityGroupIds: (props.resourcesVpcConfig as CfnCluster.ResourcesVpcConfigProperty).securityGroupIds, diff --git a/packages/@aws-cdk/aws-eks/lib/cluster.ts b/packages/@aws-cdk/aws-eks/lib/cluster.ts index 2db0438537f97..c1d00a9dcd767 100644 --- a/packages/@aws-cdk/aws-eks/lib/cluster.ts +++ b/packages/@aws-cdk/aws-eks/lib/cluster.ts @@ -540,6 +540,15 @@ export interface ClusterOptions extends CommonClusterOptions { * using AWS-Managed encryption keys. */ readonly secretsEncryptionKey?: kms.IKey; + + /** + * The CIDR block to assign Kubernetes service IP addresses from. + * + * @default - Kubernetes assigns addresses from either the + * 10.100.0.0/16 or 172.20.0.0/16 CIDR blocks + * @see https://docs.aws.amazon.com/eks/latest/APIReference/API_KubernetesNetworkConfigRequest.html#AmazonEKS-Type-KubernetesNetworkConfigRequest-serviceIpv4Cidr + */ + readonly serviceIpv4Cidr?: string; } /** @@ -1182,7 +1191,7 @@ export class Cluster extends ClusterBase { this.onEventLayer = props.onEventLayer; this.kubectlMemory = props.kubectlMemory; - const privateSubents = this.selectPrivateSubnets().slice(0, 16); + const privateSubnets = this.selectPrivateSubnets().slice(0, 16); const publicAccessDisabled = !this.endpointAccess._config.publicAccess; const publicAccessRestricted = !publicAccessDisabled && this.endpointAccess._config.publicCidrs @@ -1190,19 +1199,19 @@ export class Cluster extends ClusterBase { // validate endpoint access configuration - if (privateSubents.length === 0 && publicAccessDisabled) { + if (privateSubnets.length === 0 && publicAccessDisabled) { // no private subnets and no public access at all, no good. throw new Error('Vpc must contain private subnets when public endpoint access is disabled'); } - if (privateSubents.length === 0 && publicAccessRestricted) { - // no private subents and public access is restricted, no good. + if (privateSubnets.length === 0 && publicAccessRestricted) { + // no private subnets and public access is restricted, no good. throw new Error('Vpc must contain private subnets when public endpoint access is restricted'); } const placeClusterHandlerInVpc = props.placeClusterHandlerInVpc ?? false; - if (placeClusterHandlerInVpc && privateSubents.length === 0) { + if (placeClusterHandlerInVpc && privateSubnets.length === 0) { throw new Error('Cannot place cluster handler in the VPC since no private subnets could be selected'); } @@ -1223,16 +1232,19 @@ export class Cluster extends ClusterBase { resources: ['secrets'], }], } : {}), + kubernetesNetworkConfig: props.serviceIpv4Cidr ? { + serviceIpv4Cidr: props.serviceIpv4Cidr, + } : undefined, endpointPrivateAccess: this.endpointAccess._config.privateAccess, endpointPublicAccess: this.endpointAccess._config.publicAccess, publicAccessCidrs: this.endpointAccess._config.publicCidrs, secretsEncryptionKey: props.secretsEncryptionKey, vpc: this.vpc, - subnets: placeClusterHandlerInVpc ? privateSubents : undefined, + subnets: placeClusterHandlerInVpc ? privateSubnets : undefined, onEventLayer: this.onEventLayer, }); - if (this.endpointAccess._config.privateAccess && privateSubents.length !== 0) { + if (this.endpointAccess._config.privateAccess && privateSubnets.length !== 0) { // when private access is enabled and the vpc has private subnets, lets connect // the provider to the vpc so that it will work even when restricting public access. @@ -1242,7 +1254,7 @@ export class Cluster extends ClusterBase { throw new Error('Private endpoint access requires the VPC to have DNS support and DNS hostnames enabled. Use `enableDnsHostnames: true` and `enableDnsSupport: true` when creating the VPC.'); } - this.kubectlPrivateSubnets = privateSubents; + this.kubectlPrivateSubnets = privateSubnets; // the vpc must exist in order to properly delete the cluster (since we run `kubectl delete`). // this ensures that. diff --git a/packages/@aws-cdk/aws-eks/test/cluster.test.ts b/packages/@aws-cdk/aws-eks/test/cluster.test.ts index 14e2bfad0746b..295092509ffd2 100644 --- a/packages/@aws-cdk/aws-eks/test/cluster.test.ts +++ b/packages/@aws-cdk/aws-eks/test/cluster.test.ts @@ -2888,4 +2888,26 @@ describe('cluster', () => { expect(providerNestedStackTemplate?.Resources?.Handler886CB40B?.Properties?.MemorySize).toEqual(4096); }); + + test('create a cluster using custom kubernetes network config', () => { + // GIVEN + const { stack } = testFixture(); + const customCidr = '172.16.0.0/12'; + + // WHEN + new eks.Cluster(stack, 'Cluster', { + version: CLUSTER_VERSION, + serviceIpv4Cidr: customCidr, + }); + + // THEN + expect(stack).toHaveResourceLike('Custom::AWSCDK-EKS-Cluster', { + Config: { + kubernetesNetworkConfig: { + serviceIpv4Cidr: customCidr, + }, + }, + }); + + }); }); diff --git a/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts b/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts index 842071fa0ec68..173b13834d233 100644 --- a/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts +++ b/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts @@ -1540,9 +1540,9 @@ export class Domain extends DomainBase implements IDomain, ec2.IConnectable { if (props.logging?.auditLogEnabled) { this.auditLogGroup = props.logging.auditLogGroup ?? - new logs.LogGroup(this, 'AuditLogs', { - retention: logs.RetentionDays.ONE_MONTH, - }); + new logs.LogGroup(this, 'AuditLogs', { + retention: logs.RetentionDays.ONE_MONTH, + }); logGroups.push(this.auditLogGroup); }; @@ -1692,7 +1692,21 @@ export class Domain extends DomainBase implements IDomain, ec2.IConnectable { if (logGroupResourcePolicy) { this.domain.node.addDependency(logGroupResourcePolicy); } - if (props.domainName) { this.node.addMetadata('aws:cdk:hasPhysicalName', props.domainName); } + if (props.domainName) { + if (!cdk.Token.isUnresolved(props.domainName)) { + // https://docs.aws.amazon.com/opensearch-service/latest/developerguide/configuration-api.html#configuration-api-datatypes-domainname + if (!props.domainName.match(/^[a-z0-9\-]+$/)) { + throw new Error(`Invalid domainName '${props.domainName}'. Valid characters are a-z (lowercase only), 0-9, and – (hyphen).`); + } + if (props.domainName.length < 3 || props.domainName.length > 28) { + throw new Error(`Invalid domainName '${props.domainName}'. It must be between 3 and 28 characters`); + } + if (props.domainName[0] < 'a' || props.domainName[0] > 'z') { + throw new Error(`Invalid domainName '${props.domainName}'. It must start with a lowercase letter`); + } + } + this.node.addMetadata('aws:cdk:hasPhysicalName', props.domainName); + } this.domainName = this.getResourceNameAttribute(this.domain.ref); diff --git a/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts b/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts index 6966882c55549..f1de6b640898b 100644 --- a/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts +++ b/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts @@ -1317,6 +1317,21 @@ describe('custom error responses', () => { })).toThrow(/Unknown Elasticsearch version: 5\.4/); }); + test('error when invalid domain name is given', () => { + expect(() => new Domain(stack, 'Domain1', { + version: ElasticsearchVersion.V7_4, + domainName: 'InvalidName', + })).toThrow(/Valid characters are a-z/); + expect(() => new Domain(stack, 'Domain2', { + version: ElasticsearchVersion.V7_4, + domainName: 'a'.repeat(29), + })).toThrow(/It must be between 3 and 28 characters/); + expect(() => new Domain(stack, 'Domain3', { + version: ElasticsearchVersion.V7_4, + domainName: '123domain', + })).toThrow(/It must start with a lowercase letter/); + }); + test('error when error log publishing is enabled for elasticsearch version < 5.1', () => { const error = /Error logs publishing requires Elasticsearch version 5.1 or later/; expect(() => new Domain(stack, 'Domain1', { diff --git a/packages/@aws-cdk/aws-events-targets/README.md b/packages/@aws-cdk/aws-events-targets/README.md index 994c24ab8a0a5..a5c8fde1d9d98 100644 --- a/packages/@aws-cdk/aws-events-targets/README.md +++ b/packages/@aws-cdk/aws-events-targets/README.md @@ -35,7 +35,7 @@ EventBridge. ## Event retry policy and using dead-letter queues -The Codebuild, CodePipeline, Lambda, StepFunctions and LogGroup targets support attaching a [dead letter queue and setting retry policies](https://docs.aws.amazon.com/eventbridge/latest/userguide/rule-dlq.html). See the [lambda example](#invoke-a-lambda-function). +The Codebuild, CodePipeline, Lambda, StepFunctions, LogGroup and SQSQueue targets support attaching a [dead letter queue and setting retry policies](https://docs.aws.amazon.com/eventbridge/latest/userguide/rule-dlq.html). See the [lambda example](#invoke-a-lambda-function). Use [escape hatches](https://docs.aws.amazon.com/cdk/latest/guide/cfn_layer.html) for the other target types. ## Invoke a Lambda function diff --git a/packages/@aws-cdk/aws-events-targets/lib/sqs.ts b/packages/@aws-cdk/aws-events-targets/lib/sqs.ts index 43fb9b8ed15d0..465f0355516cf 100644 --- a/packages/@aws-cdk/aws-events-targets/lib/sqs.ts +++ b/packages/@aws-cdk/aws-events-targets/lib/sqs.ts @@ -1,11 +1,12 @@ import * as events from '@aws-cdk/aws-events'; import * as iam from '@aws-cdk/aws-iam'; import * as sqs from '@aws-cdk/aws-sqs'; +import { addToDeadLetterQueueResourcePolicy, TargetBaseProps, bindBaseTargetConfig } from './util'; /** * Customize the SQS Queue Event Target */ -export interface SqsQueueProps { +export interface SqsQueueProps extends TargetBaseProps { /** * Message Group ID for messages sent to this queue @@ -24,7 +25,6 @@ export interface SqsQueueProps { * @default the entire EventBridge event */ readonly message?: events.RuleTargetInput; - } /** @@ -62,7 +62,12 @@ export class SqsQueue implements events.IRuleTarget { // deduplicated automatically this.queue.grantSendMessages(new iam.ServicePrincipal('events.amazonaws.com', principalOpts)); + if (this.props.deadLetterQueue) { + addToDeadLetterQueueResourcePolicy(rule, this.props.deadLetterQueue); + } + return { + ...bindBaseTargetConfig(this.props), arn: this.queue.queueArn, input: this.props.message, targetResource: this.queue, diff --git a/packages/@aws-cdk/aws-events-targets/test/sqs/integ.sqs-event-rule-target.expected.json b/packages/@aws-cdk/aws-events-targets/test/sqs/integ.sqs-event-rule-target.expected.json index eb2a7dd26ef5f..f35a7a93b9e42 100644 --- a/packages/@aws-cdk/aws-events-targets/test/sqs/integ.sqs-event-rule-target.expected.json +++ b/packages/@aws-cdk/aws-events-targets/test/sqs/integ.sqs-event-rule-target.expected.json @@ -61,6 +61,14 @@ "Arn" ] }, + "DeadLetterConfig": { + "Arn": { + "Fn::GetAtt": [ + "MyDeadLetterQueueD997968A", + "Arn" + ] + } + }, "Id": "Target0" } ] @@ -110,6 +118,50 @@ } ] } + }, + "MyDeadLetterQueueD997968A": { + "Type": "AWS::SQS::Queue", + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "MyDeadLetterQueuePolicyCC35D52C": { + "Type": "AWS::SQS::QueuePolicy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "sqs:SendMessage", + "Condition": { + "ArnEquals": { + "aws:SourceArn": { + "Fn::GetAtt": [ + "MyRuleA44AB831", + "Arn" + ] + } + } + }, + "Effect": "Allow", + "Principal": { + "Service": "events.amazonaws.com" + }, + "Resource": { + "Fn::GetAtt": [ + "MyDeadLetterQueueD997968A", + "Arn" + ] + }, + "Sid": "AllowEventRuleawscdksqseventtargetMyRule0027A8F4" + } + ], + "Version": "2012-10-17" + }, + "Queues": [ + { + "Ref": "MyDeadLetterQueueD997968A" + } + ] + } } } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-events-targets/test/sqs/integ.sqs-event-rule-target.ts b/packages/@aws-cdk/aws-events-targets/test/sqs/integ.sqs-event-rule-target.ts index b2b8fb334bff6..f2375dd7c2a37 100644 --- a/packages/@aws-cdk/aws-events-targets/test/sqs/integ.sqs-event-rule-target.ts +++ b/packages/@aws-cdk/aws-events-targets/test/sqs/integ.sqs-event-rule-target.ts @@ -24,6 +24,10 @@ const queue = new sqs.Queue(stack, 'MyQueue', { encryptionMasterKey: key, }); -event.addTarget(new targets.SqsQueue(queue)); +const deadLetterQueue = new sqs.Queue(stack, 'MyDeadLetterQueue'); + +event.addTarget(new targets.SqsQueue(queue, { + deadLetterQueue, +})); app.synth(); diff --git a/packages/@aws-cdk/aws-events-targets/test/sqs/sqs.test.ts b/packages/@aws-cdk/aws-events-targets/test/sqs/sqs.test.ts index 8893a8820f37e..ad2a5296714e6 100644 --- a/packages/@aws-cdk/aws-events-targets/test/sqs/sqs.test.ts +++ b/packages/@aws-cdk/aws-events-targets/test/sqs/sqs.test.ts @@ -180,3 +180,75 @@ test('fifo queues are synthesized correctly', () => { ], })); }); + +test('dead letter queue is configured correctly', () => { + const stack = new Stack(); + const queue = new sqs.Queue(stack, 'MyQueue', { fifo: true }); + const deadLetterQueue = new sqs.Queue(stack, 'MyDeadLetterQueue'); + const rule = new events.Rule(stack, 'MyRule', { + schedule: events.Schedule.rate(Duration.hours(1)), + }); + + // WHEN + rule.addTarget(new targets.SqsQueue(queue, { + deadLetterQueue, + })); + + cdkExpect(stack).to(haveResource('AWS::Events::Rule', { + ScheduleExpression: 'rate(1 hour)', + State: 'ENABLED', + Targets: [ + { + Arn: { + 'Fn::GetAtt': [ + 'MyQueueE6CA6235', + 'Arn', + ], + }, + Id: 'Target0', + DeadLetterConfig: { + Arn: { + 'Fn::GetAtt': [ + 'MyDeadLetterQueueD997968A', + 'Arn', + ], + }, + }, + }, + ], + })); +}); + +test('specifying retry policy', () => { + const stack = new Stack(); + const queue = new sqs.Queue(stack, 'MyQueue', { fifo: true }); + const rule = new events.Rule(stack, 'MyRule', { + schedule: events.Schedule.rate(Duration.hours(1)), + }); + + // WHEN + rule.addTarget(new targets.SqsQueue(queue, { + retryAttempts: 2, + maxEventAge: Duration.hours(2), + })); + + cdkExpect(stack).to(haveResource('AWS::Events::Rule', { + ScheduleExpression: 'rate(1 hour)', + State: 'ENABLED', + Targets: [ + { + Arn: { + 'Fn::GetAtt': [ + 'MyQueueE6CA6235', + 'Arn', + ], + }, + Id: 'Target0', + RetryPolicy: { + MaximumEventAgeInSeconds: 7200, + MaximumRetryAttempts: 2, + }, + }, + ], + })); +}); diff --git a/packages/@aws-cdk/aws-events/lib/event-bus.ts b/packages/@aws-cdk/aws-events/lib/event-bus.ts index df0859b3d3259..bdddbc9cceb61 100644 --- a/packages/@aws-cdk/aws-events/lib/event-bus.ts +++ b/packages/@aws-cdk/aws-events/lib/event-bus.ts @@ -276,6 +276,8 @@ export class EventBus extends EventBusBase { ); } return { eventBusName: eventSourceName, eventSourceName }; + } else { + return { eventBusName: props.eventBusName }; } } return { eventBusName: defaultEventBusName }; @@ -311,7 +313,7 @@ export class EventBus extends EventBusBase { super(scope, id, { physicalName: eventBusName }); const eventBus = new CfnEventBus(this, 'Resource', { - name: eventBusName, + name: this.physicalName, eventSourceName, }); diff --git a/packages/@aws-cdk/aws-events/test/event-bus.test.ts b/packages/@aws-cdk/aws-events/test/event-bus.test.ts index b4384255ea7b4..e50f0b24db771 100644 --- a/packages/@aws-cdk/aws-events/test/event-bus.test.ts +++ b/packages/@aws-cdk/aws-events/test/event-bus.test.ts @@ -1,6 +1,6 @@ import '@aws-cdk/assert-internal/jest'; import * as iam from '@aws-cdk/aws-iam'; -import { Aws, CfnResource, Stack, Arn } from '@aws-cdk/core'; +import { Aws, CfnResource, Stack, Arn, App, PhysicalName, CfnOutput } from '@aws-cdk/core'; import { EventBus } from '../lib'; describe('event bus', () => { @@ -515,4 +515,32 @@ describe('event bus', () => { }); + test('cross account event bus uses generated physical name', () => { + // GIVEN + const app = new App(); + const stack1 = new Stack(app, 'Stack1', { + env: { + account: '11111111111', + region: 'us-east-1', + }, + }); + const stack2 = new Stack(app, 'Stack2', { + env: { + account: '22222222222', + region: 'us-east-1', + }, + }); + + // WHEN + const bus1 = new EventBus(stack1, 'Bus', { + eventBusName: PhysicalName.GENERATE_IF_NEEDED, + }); + + new CfnOutput(stack2, 'BusName', { value: bus1.eventBusName }); + + // THEN + expect(stack1).toHaveResource('AWS::Events::EventBus', { + Name: 'stack1stack1busca19bdf8ab2e51b62a5a', + }); + }); }); diff --git a/packages/@aws-cdk/aws-lambda/lib/function.ts b/packages/@aws-cdk/aws-lambda/lib/function.ts index 7505cdf463a1b..fffd20decd8d9 100644 --- a/packages/@aws-cdk/aws-lambda/lib/function.ts +++ b/packages/@aws-cdk/aws-lambda/lib/function.ts @@ -220,6 +220,10 @@ export interface FunctionOptions extends EventInvokeConfigOptions { * Specify the version of CloudWatch Lambda insights to use for monitoring * @see https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/Lambda-Insights.html * + * When used with `DockerImageFunction` or `DockerImageCode`, the Docker image should have + * the Lambda insights agent installed. + * @see https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/Lambda-Insights-Getting-Started-docker.html + * * @default - No Lambda Insights */ readonly insightsVersion?: LambdaInsightsVersion; @@ -782,9 +786,7 @@ export class Function extends FunctionBase { } // Configure Lambda insights - if (props.insightsVersion !== undefined) { - this.configureLambdaInsights(props.insightsVersion); - } + this.configureLambdaInsights(props); } /** @@ -912,8 +914,15 @@ Environment variables can be marked for removal when used in Lambda@Edge by sett * * https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/Lambda-Insights-extension-versions.html */ - private configureLambdaInsights(insightsVersion: LambdaInsightsVersion): void { - this.addLayers(LayerVersion.fromLayerVersionArn(this, 'LambdaInsightsLayer', insightsVersion.layerVersionArn)); + private configureLambdaInsights(props: FunctionProps): void { + if (props.insightsVersion === undefined) { + return; + } + if (props.runtime !== Runtime.FROM_IMAGE) { + // Layers cannot be added to Lambda container images. The image should have the insights agent installed. + // See https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/Lambda-Insights-Getting-Started-docker.html + this.addLayers(LayerVersion.fromLayerVersionArn(this, 'LambdaInsightsLayer', props.insightsVersion.layerVersionArn)); + } this.role?.addManagedPolicy(iam.ManagedPolicy.fromAwsManagedPolicyName('CloudWatchLambdaInsightsExecutionRolePolicy')); } diff --git a/packages/@aws-cdk/aws-lambda/test/lambda-insights.test.ts b/packages/@aws-cdk/aws-lambda/test/lambda-insights.test.ts index 990c0c7fb7405..762df158da6f4 100644 --- a/packages/@aws-cdk/aws-lambda/test/lambda-insights.test.ts +++ b/packages/@aws-cdk/aws-lambda/test/lambda-insights.test.ts @@ -1,5 +1,6 @@ import '@aws-cdk/assert-internal/jest'; import { MatchStyle } from '@aws-cdk/assert-internal'; +import * as ecr from '@aws-cdk/aws-ecr'; import * as cdk from '@aws-cdk/core'; import * as lambda from '../lib'; @@ -313,4 +314,43 @@ describe('lambda-insights', () => { // On synthesis it should not throw an error expect(() => app.synth()).not.toThrow(); }); + + test('insights layer is skipped for container images and the role is updated', () => { + const stack = new cdk.Stack(); + new lambda.DockerImageFunction(stack, 'MyFunction', { + code: lambda.DockerImageCode.fromEcr(ecr.Repository.fromRepositoryArn(stack, 'MyRepo', + 'arn:aws:ecr:us-east-1:0123456789:repository/MyRepo')), + insightsVersion: lambda.LambdaInsightsVersion.VERSION_1_0_98_0, + }); + + expect(stack).toCountResources('AWS::Lambda::LayerVersion', 0); + + expect(stack).toHaveResourceLike('AWS::IAM::Role', { + 'AssumeRolePolicyDocument': { + 'Statement': [ + { + 'Action': 'sts:AssumeRole', + 'Principal': { + 'Service': 'lambda.amazonaws.com', + }, + }, + ], + }, + 'ManagedPolicyArns': [ + { }, + { + 'Fn::Join': [ + '', + [ + 'arn:', + { + 'Ref': 'AWS::Partition', + }, + ':iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy', + ], + ], + }, + ], + }); + }); }); diff --git a/packages/@aws-cdk/aws-msk/README.md b/packages/@aws-cdk/aws-msk/README.md index ce86c6a477c45..664ec4f66c973 100644 --- a/packages/@aws-cdk/aws-msk/README.md +++ b/packages/@aws-cdk/aws-msk/README.md @@ -29,7 +29,7 @@ The following example creates an MSK Cluster. import * as msk from '@aws-cdk/aws-msk'; const cluster = new Cluster(this, 'Cluster', { - kafkaVersion: msk.KafkaVersion.V2_6_1, + kafkaVersion: msk.KafkaVersion.V2_8_1, vpc, }); ``` diff --git a/packages/@aws-cdk/aws-msk/lib/cluster-version.ts b/packages/@aws-cdk/aws-msk/lib/cluster-version.ts index 0d3b511aae59e..b04c154b76054 100644 --- a/packages/@aws-cdk/aws-msk/lib/cluster-version.ts +++ b/packages/@aws-cdk/aws-msk/lib/cluster-version.ts @@ -47,6 +47,11 @@ export class KafkaVersion { */ public static readonly V2_8_0 = KafkaVersion.of('2.8.0'); + /** + * Kafka version 2.8.1 + */ + public static readonly V2_8_1 = KafkaVersion.of('2.8.1'); + /** * Custom cluster version * @param version custom version number diff --git a/packages/@aws-cdk/aws-msk/test/integ.cluster.expected.json b/packages/@aws-cdk/aws-msk/test/integ.cluster.expected.json index 9a0d0db6e2325..2b523706bd3e2 100644 --- a/packages/@aws-cdk/aws-msk/test/integ.cluster.expected.json +++ b/packages/@aws-cdk/aws-msk/test/integ.cluster.expected.json @@ -95,15 +95,15 @@ "VPCPublicSubnet1NATGatewayE0556630": { "Type": "AWS::EC2::NatGateway", "Properties": { + "SubnetId": { + "Ref": "VPCPublicSubnet1SubnetB4246D30" + }, "AllocationId": { "Fn::GetAtt": [ "VPCPublicSubnet1EIP6AD938E8", "AllocationId" ] }, - "SubnetId": { - "Ref": "VPCPublicSubnet1SubnetB4246D30" - }, "Tags": [ { "Key": "Name", @@ -192,15 +192,15 @@ "VPCPublicSubnet2NATGateway3C070193": { "Type": "AWS::EC2::NatGateway", "Properties": { + "SubnetId": { + "Ref": "VPCPublicSubnet2Subnet74179F39" + }, "AllocationId": { "Fn::GetAtt": [ "VPCPublicSubnet2EIP4947BC00", "AllocationId" ] }, - "SubnetId": { - "Ref": "VPCPublicSubnet2Subnet74179F39" - }, "Tags": [ { "Key": "Name", @@ -399,7 +399,7 @@ } }, "ClusterName": "integ-test", - "KafkaVersion": "2.6.1", + "KafkaVersion": "2.8.1", "NumberOfBrokerNodes": 2, "EncryptionInfo": { "EncryptionInTransit": { @@ -524,7 +524,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameters5c61041c12314e1ad8e67a0107fa3733382a206a78cdc1576fffa7e93caca5b4S3BucketB17E5ABD" + "Ref": "AssetParameters1c4eb88f5a8270f387281dcff6e3493840634113c4d57044f4aff74e3ef94c2dS3Bucket4C71F166" }, "S3Key": { "Fn::Join": [ @@ -537,7 +537,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters5c61041c12314e1ad8e67a0107fa3733382a206a78cdc1576fffa7e93caca5b4S3VersionKey77778F6A" + "Ref": "AssetParameters1c4eb88f5a8270f387281dcff6e3493840634113c4d57044f4aff74e3ef94c2dS3VersionKey0124EFC4" } ] } @@ -550,7 +550,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters5c61041c12314e1ad8e67a0107fa3733382a206a78cdc1576fffa7e93caca5b4S3VersionKey77778F6A" + "Ref": "AssetParameters1c4eb88f5a8270f387281dcff6e3493840634113c4d57044f4aff74e3ef94c2dS3VersionKey0124EFC4" } ] } @@ -576,17 +576,17 @@ } }, "Parameters": { - "AssetParameters5c61041c12314e1ad8e67a0107fa3733382a206a78cdc1576fffa7e93caca5b4S3BucketB17E5ABD": { + "AssetParameters1c4eb88f5a8270f387281dcff6e3493840634113c4d57044f4aff74e3ef94c2dS3Bucket4C71F166": { "Type": "String", - "Description": "S3 bucket for asset \"5c61041c12314e1ad8e67a0107fa3733382a206a78cdc1576fffa7e93caca5b4\"" + "Description": "S3 bucket for asset \"1c4eb88f5a8270f387281dcff6e3493840634113c4d57044f4aff74e3ef94c2d\"" }, - "AssetParameters5c61041c12314e1ad8e67a0107fa3733382a206a78cdc1576fffa7e93caca5b4S3VersionKey77778F6A": { + "AssetParameters1c4eb88f5a8270f387281dcff6e3493840634113c4d57044f4aff74e3ef94c2dS3VersionKey0124EFC4": { "Type": "String", - "Description": "S3 key for asset version \"5c61041c12314e1ad8e67a0107fa3733382a206a78cdc1576fffa7e93caca5b4\"" + "Description": "S3 key for asset version \"1c4eb88f5a8270f387281dcff6e3493840634113c4d57044f4aff74e3ef94c2d\"" }, - "AssetParameters5c61041c12314e1ad8e67a0107fa3733382a206a78cdc1576fffa7e93caca5b4ArtifactHash580E429C": { + "AssetParameters1c4eb88f5a8270f387281dcff6e3493840634113c4d57044f4aff74e3ef94c2dArtifactHash6350D824": { "Type": "String", - "Description": "Artifact hash for asset \"5c61041c12314e1ad8e67a0107fa3733382a206a78cdc1576fffa7e93caca5b4\"" + "Description": "Artifact hash for asset \"1c4eb88f5a8270f387281dcff6e3493840634113c4d57044f4aff74e3ef94c2d\"" } }, "Outputs": { diff --git a/packages/@aws-cdk/aws-msk/test/integ.cluster.ts b/packages/@aws-cdk/aws-msk/test/integ.cluster.ts index c05fa496d7210..06ad0893d20d7 100644 --- a/packages/@aws-cdk/aws-msk/test/integ.cluster.ts +++ b/packages/@aws-cdk/aws-msk/test/integ.cluster.ts @@ -10,7 +10,7 @@ const vpc = new ec2.Vpc(stack, 'VPC', { maxAzs: 2 }); const cluster = new msk.Cluster(stack, 'Cluster', { clusterName: 'integ-test', - kafkaVersion: msk.KafkaVersion.V2_6_1, + kafkaVersion: msk.KafkaVersion.V2_8_1, vpc, removalPolicy: cdk.RemovalPolicy.DESTROY, }); diff --git a/packages/@aws-cdk/aws-opensearchservice/lib/domain.ts b/packages/@aws-cdk/aws-opensearchservice/lib/domain.ts index 1279d9525f831..600e1bee8f49a 100644 --- a/packages/@aws-cdk/aws-opensearchservice/lib/domain.ts +++ b/packages/@aws-cdk/aws-opensearchservice/lib/domain.ts @@ -1472,9 +1472,9 @@ export class Domain extends DomainBase implements IDomain, ec2.IConnectable { if (props.logging?.auditLogEnabled) { this.auditLogGroup = props.logging.auditLogGroup ?? - new logs.LogGroup(this, 'AuditLogs', { - retention: logs.RetentionDays.ONE_MONTH, - }); + new logs.LogGroup(this, 'AuditLogs', { + retention: logs.RetentionDays.ONE_MONTH, + }); logGroups.push(this.auditLogGroup); }; @@ -1624,7 +1624,21 @@ export class Domain extends DomainBase implements IDomain, ec2.IConnectable { if (logGroupResourcePolicy) { this.domain.node.addDependency(logGroupResourcePolicy); } - if (props.domainName) { this.node.addMetadata('aws:cdk:hasPhysicalName', props.domainName); } + if (props.domainName) { + if (!cdk.Token.isUnresolved(props.domainName)) { + // https://docs.aws.amazon.com/opensearch-service/latest/developerguide/configuration-api.html#configuration-api-datatypes-domainname + if (!props.domainName.match(/^[a-z0-9\-]+$/)) { + throw new Error(`Invalid domainName '${props.domainName}'. Valid characters are a-z (lowercase only), 0-9, and – (hyphen).`); + } + if (props.domainName.length < 3 || props.domainName.length > 28) { + throw new Error(`Invalid domainName '${props.domainName}'. It must be between 3 and 28 characters`); + } + if (props.domainName[0] < 'a' || props.domainName[0] > 'z') { + throw new Error(`Invalid domainName '${props.domainName}'. It must start with a lowercase letter`); + } + } + this.node.addMetadata('aws:cdk:hasPhysicalName', props.domainName); + } this.domainName = this.getResourceNameAttribute(this.domain.ref); diff --git a/packages/@aws-cdk/aws-opensearchservice/test/domain.test.ts b/packages/@aws-cdk/aws-opensearchservice/test/domain.test.ts index 4ad11ac372f97..80cc7a1473b40 100644 --- a/packages/@aws-cdk/aws-opensearchservice/test/domain.test.ts +++ b/packages/@aws-cdk/aws-opensearchservice/test/domain.test.ts @@ -1319,6 +1319,21 @@ describe('custom error responses', () => { })).toThrow('Unknown Elasticsearch version: 5.4'); }); + test('error when invalid domain name is given', () => { + expect(() => new Domain(stack, 'Domain1', { + version: EngineVersion.OPENSEARCH_1_0, + domainName: 'InvalidName', + })).toThrow(/Valid characters are a-z/); + expect(() => new Domain(stack, 'Domain2', { + version: EngineVersion.OPENSEARCH_1_0, + domainName: 'a'.repeat(29), + })).toThrow(/It must be between 3 and 28 characters/); + expect(() => new Domain(stack, 'Domain3', { + version: EngineVersion.OPENSEARCH_1_0, + domainName: '123domain', + })).toThrow(/It must start with a lowercase letter/); + }); + test('error when error log publishing is enabled for Elasticsearch version < 5.1', () => { const error = /Error logs publishing requires Elasticsearch version 5.1 or later or OpenSearch version 1.0 or later/; expect(() => new Domain(stack, 'Domain1', { diff --git a/packages/@aws-cdk/aws-route53-targets/test/bucket-website-target.test.ts b/packages/@aws-cdk/aws-route53-targets/test/bucket-website-target.test.ts index eebb53b2d488a..ff1938e66447b 100644 --- a/packages/@aws-cdk/aws-route53-targets/test/bucket-website-target.test.ts +++ b/packages/@aws-cdk/aws-route53-targets/test/bucket-website-target.test.ts @@ -74,10 +74,10 @@ test('throws if region agnostic', () => { }).toThrow(/Cannot use an S3 record alias in region-agnostic stacks/); }); -test('throws if bucket website hosting is unavailable (cn-northwest-1)', () => { +test('throws if bucket website hosting is unavailable (cn-north-1)', () => { // GIVEN const app = new App(); - const stack = new Stack(app, 'test', { env: { region: 'cn-northwest-1' } }); + const stack = new Stack(app, 'test', { env: { region: 'cn-north-1' } }); const bucketWebsite = new s3.Bucket(stack, 'Bucket'); diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/README.md b/packages/@aws-cdk/aws-stepfunctions-tasks/README.md index d33d3c7ff801e..a0f47ee7519c1 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/README.md +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/README.md @@ -969,6 +969,10 @@ disable this behavior. Step Functions supports [AWS SageMaker](https://docs.aws.amazon.com/step-functions/latest/dg/connect-sagemaker.html) through the service integration pattern. +If your training job or model uses resources from AWS Marketplace, +[network isolation is required](https://docs.aws.amazon.com/sagemaker/latest/dg/mkt-algo-model-internet-free.html). +To do so, set the `enableNetworkIsolation` property to `true` for `SageMakerCreateModel` or `SageMakerCreateTrainingJob`. + ### Create Training Job You can call the [`CreateTrainingJob`](https://docs.aws.amazon.com/sagemaker/latest/dg/API_CreateTrainingJob.html) API from a `Task` state. diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/sagemaker/create-training-job.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/sagemaker/create-training-job.ts index 0aa9954e859d5..64680dc357747 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/sagemaker/create-training-job.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/sagemaker/create-training-job.ts @@ -32,6 +32,13 @@ export interface SageMakerCreateTrainingJobProps extends sfn.TaskStateBaseProps */ readonly algorithmSpecification: AlgorithmSpecification; + /** + * Isolates the training container. No inbound or outbound network calls can be made to or from the training container. + * + * @default false + */ + readonly enableNetworkIsolation?: boolean; + /** * Algorithm-specific parameters that influence the quality of the model. Set hyperparameters before you start the learning process. * For a list of hyperparameters provided by Amazon SageMaker @@ -217,6 +224,7 @@ export class SageMakerCreateTrainingJob extends sfn.TaskStateBase implements iam private renderParameters(): { [key: string]: any } { return { TrainingJobName: this.props.trainingJobName, + EnableNetworkIsolation: this.props.enableNetworkIsolation, RoleArn: this._role!.roleArn, ...this.renderAlgorithmSpecification(this.algorithmSpecification), ...this.renderInputDataConfig(this.inputDataConfig), diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/sagemaker/create-training-job.test.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/sagemaker/create-training-job.test.ts index 59f5a1bb36229..e16d20f94cfeb 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/sagemaker/create-training-job.test.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/sagemaker/create-training-job.test.ts @@ -142,6 +142,7 @@ test('create complex training job', () => { }, ], }, + enableNetworkIsolation: true, hyperparameters: { lr: '0.1', }, @@ -221,6 +222,7 @@ test('create complex training job', () => { { Name: 'mymetric', Regex: 'regex_pattern' }, ], }, + EnableNetworkIsolation: true, HyperParameters: { lr: '0.1', }, diff --git a/packages/@aws-cdk/cfnspec/test/filtered-specification.test.ts b/packages/@aws-cdk/cfnspec/test/filtered-specification.test.ts index a4bd359517da1..e9f18346699ea 100644 --- a/packages/@aws-cdk/cfnspec/test/filtered-specification.test.ts +++ b/packages/@aws-cdk/cfnspec/test/filtered-specification.test.ts @@ -14,7 +14,7 @@ test('filteredSpecification(s => s.startsWith("AWS::S3::")', () => { }); for (const name of resourceTypes().sort()) { - test(`filteredSpecification(${JSON.stringify(name)})`, () => { + describe(`filteredSpecification(${JSON.stringify(name)})`, () => { const filteredSpec = filteredSpecification(name); expect(filteredSpec).not.toEqual(specification); expect(filteredSpec.ResourceTypes).not.toEqual({}); diff --git a/packages/@aws-cdk/cfnspec/test/spec-validators.ts b/packages/@aws-cdk/cfnspec/test/spec-validators.ts index 0c49c6bfa1a03..c8993885332fb 100644 --- a/packages/@aws-cdk/cfnspec/test/spec-validators.ts +++ b/packages/@aws-cdk/cfnspec/test/spec-validators.ts @@ -1,3 +1,4 @@ +/* eslint-disable jest/no-export */ import * as schema from '../lib/schema'; export function validateSpecification(specification: schema.Specification) { @@ -7,26 +8,30 @@ export function validateSpecification(specification: schema.Specification) { function validateResourceTypes(specification: schema.Specification) { for (const typeName of Object.keys(specification.ResourceTypes)) { - expect(typeName).toBeTruthy(); - const type = specification.ResourceTypes[typeName]; - expect(type.Documentation).not.toBeNull(); - if (type.ScrutinyType) { - expect(schema.isResourceScrutinyType(type.ScrutinyType)).toBeTruthy(); - } - if (type.Properties) { validateProperties(typeName, type.Properties, specification); } - if (type.Attributes) { validateAttributes(typeName, type.Attributes, specification); } + describe(typeName, () => { + expect(typeName).toBeTruthy(); + const type = specification.ResourceTypes[typeName]; + expect(type.Documentation).not.toBeNull(); + if (type.ScrutinyType) { + expect(schema.isResourceScrutinyType(type.ScrutinyType)).toBeTruthy(); + } + if (type.Properties) { validateProperties(typeName, type.Properties, specification); } + if (type.Attributes) { validateAttributes(typeName, type.Attributes, specification); } + }); } } function validatePropertyTypes(specification: schema.Specification) { for (const typeName of Object.keys(specification.PropertyTypes)) { - expect(typeName).toBeTruthy(); - const type = specification.PropertyTypes[typeName]; - if (schema.isRecordType(type)) { - validateProperties(typeName, type.Properties, specification); - } else { - validateProperties(typeName, { '': type }, specification); - } + describe(`PropertyType ${typeName}`, () => { + expect(typeName).toBeTruthy(); + const type = specification.PropertyTypes[typeName]; + if (schema.isRecordType(type)) { + validateProperties(typeName, type.Properties, specification); + } else { + validateProperties(typeName, { '': type }, specification); + } + }); } } @@ -37,85 +42,87 @@ function validateProperties( const expectedKeys = ['Documentation', 'Required', 'UpdateType', 'ScrutinyType']; for (const name of Object.keys(properties)) { - const property = properties[name]; - expect(property.Documentation).not.toEqual(''); - expect(!property.UpdateType || schema.isUpdateType(property.UpdateType)).toBeTruthy(); - if (property.ScrutinyType !== undefined) { - expect(schema.isPropertyScrutinyType(property.ScrutinyType)).toBeTruthy(); - } + test(`Property ${name}`, () => { + const property = properties[name]; + expect(property.Documentation).not.toEqual(''); + expect(!property.UpdateType || schema.isUpdateType(property.UpdateType)).toBeTruthy(); + if (property.ScrutinyType !== undefined) { + expect(schema.isPropertyScrutinyType(property.ScrutinyType)).toBeTruthy(); + } + + if (schema.isPrimitiveProperty(property)) { + expect(schema.isPrimitiveType(property.PrimitiveType)).toBeTruthy(); + expectedKeys.push('PrimitiveType'); - if (schema.isPrimitiveProperty(property)) { - expect(schema.isPrimitiveType(property.PrimitiveType)).toBeTruthy(); - expectedKeys.push('PrimitiveType'); + } else if (schema.isPrimitiveListProperty(property)) { + expectedKeys.push('Type', 'DuplicatesAllowed', 'PrimitiveItemType'); + expect(schema.isPrimitiveType(property.PrimitiveItemType)).toBeTruthy(); - } else if (schema.isPrimitiveListProperty(property)) { - expectedKeys.push('Type', 'DuplicatesAllowed', 'PrimitiveItemType'); - expect(schema.isPrimitiveType(property.PrimitiveItemType)).toBeTruthy(); + } else if (schema.isPrimitiveMapProperty(property)) { + expectedKeys.push('Type', 'DuplicatesAllowed', 'PrimitiveItemType', 'Type'); + expect(schema.isPrimitiveType(property.PrimitiveItemType)).toBeTruthy(); + expect(property.DuplicatesAllowed).toBeFalsy(); - } else if (schema.isPrimitiveMapProperty(property)) { - expectedKeys.push('Type', 'DuplicatesAllowed', 'PrimitiveItemType', 'Type'); - expect(schema.isPrimitiveType(property.PrimitiveItemType)).toBeTruthy(); - expect(property.DuplicatesAllowed).toBeFalsy(); + } else if (schema.isComplexListProperty(property)) { + expectedKeys.push('Type', 'DuplicatesAllowed', 'ItemType', 'Type'); + expect(property.ItemType).toBeTruthy(); + if (property.ItemType !== 'Tag') { + const fqn = `${typeName.split('.')[0]}.${property.ItemType}`; + const resolvedType = specification.PropertyTypes && specification.PropertyTypes[fqn]; + expect(resolvedType).toBeTruthy(); + } - } else if (schema.isComplexListProperty(property)) { - expectedKeys.push('Type', 'DuplicatesAllowed', 'ItemType', 'Type'); - expect(property.ItemType).toBeTruthy(); - if (property.ItemType !== 'Tag') { + } else if (schema.isMapOfStructsProperty(property)) { + expectedKeys.push('Type', 'DuplicatesAllowed', 'ItemType', 'Type'); + expect(property.ItemType).toBeTruthy(); const fqn = `${typeName.split('.')[0]}.${property.ItemType}`; const resolvedType = specification.PropertyTypes && specification.PropertyTypes[fqn]; expect(resolvedType).toBeTruthy(); - } + expect(property.DuplicatesAllowed).toBeFalsy(); + + } else if (schema.isMapOfListsOfPrimitivesProperty(property)) { + expectedKeys.push('Type', 'DuplicatesAllowed', 'ItemType', 'PrimitiveItemItemType', 'Type'); + expect(schema.isPrimitiveType(property.PrimitiveItemItemType)).toBeTruthy(); + expect(property.DuplicatesAllowed).toBeFalsy(); + + } else if (schema.isComplexProperty(property)) { + expectedKeys.push('Type'); + expect(property.Type).toBeTruthy(); + const fqn = `${typeName.split('.')[0]}.${property.Type}`; + const resolvedType = specification.PropertyTypes && specification.PropertyTypes[fqn]; + expect(resolvedType).toBeTruthy(); - } else if (schema.isMapOfStructsProperty(property)) { - expectedKeys.push('Type', 'DuplicatesAllowed', 'ItemType', 'Type'); - expect(property.ItemType).toBeTruthy(); - const fqn = `${typeName.split('.')[0]}.${property.ItemType}`; - const resolvedType = specification.PropertyTypes && specification.PropertyTypes[fqn]; - expect(resolvedType).toBeTruthy(); - expect(property.DuplicatesAllowed).toBeFalsy(); - - } else if (schema.isMapOfListsOfPrimitivesProperty(property)) { - expectedKeys.push('Type', 'DuplicatesAllowed', 'ItemType', 'PrimitiveItemItemType', 'Type'); - expect(schema.isPrimitiveType(property.PrimitiveItemItemType)).toBeTruthy(); - expect(property.DuplicatesAllowed).toBeFalsy(); - - } else if (schema.isComplexProperty(property)) { - expectedKeys.push('Type'); - expect(property.Type).toBeTruthy(); - const fqn = `${typeName.split('.')[0]}.${property.Type}`; - const resolvedType = specification.PropertyTypes && specification.PropertyTypes[fqn]; - expect(resolvedType).toBeTruthy(); - - } else if (schema.isUnionProperty(property)) { - expectedKeys.push('PrimitiveTypes', 'PrimitiveItemTypes', 'ItemTypes', 'Types'); - if (property.PrimitiveTypes) { - for (const type of property.PrimitiveTypes) { - expect(schema.isPrimitiveType(type)).toBeTruthy(); + } else if (schema.isUnionProperty(property)) { + expectedKeys.push('PrimitiveTypes', 'PrimitiveItemTypes', 'ItemTypes', 'Types'); + if (property.PrimitiveTypes) { + for (const type of property.PrimitiveTypes) { + expect(schema.isPrimitiveType(type)).toBeTruthy(); + } } - } - if (property.ItemTypes) { - for (const type of property.ItemTypes) { - const fqn = `${typeName.split('.')[0]}.${type}`; - const resolvedType = specification.PropertyTypes && specification.PropertyTypes[fqn]; - expect(resolvedType).toBeTruthy(); + if (property.ItemTypes) { + for (const type of property.ItemTypes) { + const fqn = `${typeName.split('.')[0]}.${type}`; + const resolvedType = specification.PropertyTypes && specification.PropertyTypes[fqn]; + expect(resolvedType).toBeTruthy(); + } } - } - if (property.Types) { - for (const type of property.Types) { - const fqn = `${typeName.split('.')[0]}.${type}`; - const resolvedType = specification.PropertyTypes && specification.PropertyTypes[fqn]; - expect(resolvedType).toBeTruthy(); + if (property.Types) { + for (const type of property.Types) { + const fqn = `${typeName.split('.')[0]}.${type}`; + const resolvedType = specification.PropertyTypes && specification.PropertyTypes[fqn]; + expect(resolvedType).toBeTruthy(); + } } - } - } else { - // eslint-disable-next-line no-console - console.error(`${typeName}.Properties.${name} does not declare a type.` + - `Property definition is: ${JSON.stringify(property, undefined, 2)}`); - expect(false).toBeTruthy(); - } + } else { + // eslint-disable-next-line no-console + console.error(`${typeName}.Properties.${name} does not declare a type.` + + `Property definition is: ${JSON.stringify(property, undefined, 2)}`); + expect(false).toBeTruthy(); + } - expect(without(Object.keys(property), expectedKeys)).toEqual([]); + expect(without(Object.keys(property), expectedKeys)).toEqual([]); + }); } } @@ -124,29 +131,31 @@ function validateAttributes( attributes: { [name: string]: schema.Attribute }, specification: schema.Specification) { for (const name of Object.keys(attributes)) { - const attribute = attributes[name]; - expect(('Type' in attribute)).not.toEqual(('PrimitiveType' in attribute)); - if (schema.isPrimitiveAttribute(attribute)) { - expect(schema.isListAttribute(attribute)).toBeFalsy(); - expect(schema.isPrimitiveType(attribute.PrimitiveType)).toBeTruthy(); - expect(('PrimitiveItemType' in attribute)).toBeFalsy(); - expect(('ItemType' in attribute)).toBeFalsy(); - } else if (schema.isPrimitiveListAttribute(attribute)) { - expect(schema.isComplexListAttribute(attribute)).toBeFalsy(); - expect(schema.isPrimitiveType(attribute.PrimitiveItemType)).toBeTruthy(); - expect(('ItemType' in attribute)).toBeFalsy(); - } else if (schema.isComplexListAttribute(attribute)) { - expect(attribute.ItemType).toBeTruthy(); - const fqn = `${typeName.split('.')[0]}.${attribute.ItemType}`; - const resolvedType = specification.PropertyTypes && specification.PropertyTypes[fqn]; - expect(resolvedType).toBeTruthy(); - expect(('PrimitiveItemType' in attribute)).toBeFalsy(); - } else if (schema.isPrimitiveMapAttribute(attribute)) { - expect(schema.isPrimitiveType(attribute.PrimitiveItemType)).toBeTruthy(); - expect(('ItemType' in attribute)).toBeFalsy(); - } else { - expect(false).toBeTruthy(); // `${typeName}.Attributes.${name} has a valid type`); - } + test(`Attribute ${name}`, () => { + const attribute = attributes[name]; + expect(('Type' in attribute)).not.toEqual(('PrimitiveType' in attribute)); + if (schema.isPrimitiveAttribute(attribute)) { + expect(schema.isListAttribute(attribute)).toBeFalsy(); + expect(schema.isPrimitiveType(attribute.PrimitiveType)).toBeTruthy(); + expect(('PrimitiveItemType' in attribute)).toBeFalsy(); + expect(('ItemType' in attribute)).toBeFalsy(); + } else if (schema.isPrimitiveListAttribute(attribute)) { + expect(schema.isComplexListAttribute(attribute)).toBeFalsy(); + expect(schema.isPrimitiveType(attribute.PrimitiveItemType)).toBeTruthy(); + expect(('ItemType' in attribute)).toBeFalsy(); + } else if (schema.isComplexListAttribute(attribute)) { + expect(attribute.ItemType).toBeTruthy(); + const fqn = `${typeName.split('.')[0]}.${attribute.ItemType}`; + const resolvedType = specification.PropertyTypes && specification.PropertyTypes[fqn]; + expect(resolvedType).toBeTruthy(); + expect(('PrimitiveItemType' in attribute)).toBeFalsy(); + } else if (schema.isPrimitiveMapAttribute(attribute)) { + expect(schema.isPrimitiveType(attribute.PrimitiveItemType)).toBeTruthy(); + expect(('ItemType' in attribute)).toBeFalsy(); + } else { + expect(false).toBeTruthy(); // `${typeName}.Attributes.${name} has a valid type`); + } + }); } } diff --git a/packages/@aws-cdk/cloudformation-diff/lib/diff-template.ts b/packages/@aws-cdk/cloudformation-diff/lib/diff-template.ts index b3f282802b675..6e06e56f90af1 100644 --- a/packages/@aws-cdk/cloudformation-diff/lib/diff-template.ts +++ b/packages/@aws-cdk/cloudformation-diff/lib/diff-template.ts @@ -99,13 +99,17 @@ function calculateTemplateDiff(currentTemplate: { [key: string]: any }, newTempl for (const key of unionOf(Object.keys(currentTemplate), Object.keys(newTemplate)).sort()) { const oldValue = currentTemplate[key]; const newValue = newTemplate[key]; - if (deepEqual(oldValue, newValue)) { continue; } + if (deepEqual(oldValue, newValue)) { + continue; + } const handler: DiffHandler = DIFF_HANDLERS[key] || ((_diff, oldV, newV) => unknown[key] = impl.diffUnknown(oldV, newV)); handler(differences, oldValue, newValue); } - if (Object.keys(unknown).length > 0) { differences.unknown = new types.DifferenceCollection(unknown); } + if (Object.keys(unknown).length > 0) { + differences.unknown = new types.DifferenceCollection(unknown); + } return new types.TemplateDiff(differences); } diff --git a/packages/@aws-cdk/cloudformation-diff/lib/diff/util.ts b/packages/@aws-cdk/cloudformation-diff/lib/diff/util.ts index 59c8606be0a35..1cbd4b1a111d7 100644 --- a/packages/@aws-cdk/cloudformation-diff/lib/diff/util.ts +++ b/packages/@aws-cdk/cloudformation-diff/lib/diff/util.ts @@ -138,20 +138,10 @@ export function unionOf(lv: string[] | Set, rv: string[] | Set): * A parseFloat implementation that does the right thing for * strings like '0.0.0' * (for which JavaScript's parseFloat() returns 0). + * We return NaN for all of these strings that do not represent numbers, + * and so comparing them fails, + * and doesn't short-circuit the diff logic. */ function safeParseFloat(str: string): number { - const ret = parseFloat(str); - const nonNumericRegex = /\d*\.\d+\./; - if (ret === 0) { - // if the str is exactly '0', that's OK; - // but parseFloat() also returns 0 for things like '0.0'; - // in this case, return NaN, so we'll fall back to string comparison - return str === '0' ? ret : NaN; - } else if (nonNumericRegex.test(str)) { - // if the str contains non-numeric characters, - // return NaN, so we'll fall back to string comparison - return NaN; - } else { - return ret; - } + return Number(str); } diff --git a/packages/@aws-cdk/cloudformation-diff/test/diff-template.test.ts b/packages/@aws-cdk/cloudformation-diff/test/diff-template.test.ts index 9241d0e8e28eb..d43ec99808d31 100644 --- a/packages/@aws-cdk/cloudformation-diff/test/diff-template.test.ts +++ b/packages/@aws-cdk/cloudformation-diff/test/diff-template.test.ts @@ -582,70 +582,57 @@ test('when a property changes including equivalent DependsOn', () => { expect(differences.resources.differenceCount).toBe(1); }); -test('when a property with a number-like format changes', () => { - const bucketName = 'ShineyBucketName'; - const tagChanges = { - '0.31.1-prod': '0.31.2-prod', - '8.0.5.5.4-identifier': '8.0.5.5.5-identifier', - '1.1.1.1': '1.1.2.2', - '1.2.3': '1.2.4', - '2.2.2.2': '2.2.3.2', - '3.3.3.3': '3.4.3.3', - }; - const oldTags = Object.keys(tagChanges); - const newTags = Object.values(tagChanges); +test.each([ + ['0.31.1-prod', '0.31.2-prod'], + ['8.0.5.5.4-identifier', '8.0.5.5.5-identifier'], + ['1.1.1.1', '1.1.1.2'], + ['1.2.3', '1.2.4'], + ['2.2.2.2', '2.2.3.2'], + ['3.3.3.3', '3.4.3.3'], + ['2021-10-23T06:07:08.000Z', '2021-10-23T09:10:11.123Z'], +])("reports a change when a string property with a number-like format changes from '%s' to '%s'", (oldValue, newValue) => { + // GIVEN const currentTemplate = { Resources: { - QueueResource: { - Type: 'AWS::SQS::Queue', - }, BucketResource: { Type: 'AWS::S3::Bucket', Properties: { - BucketName: bucketName, - Tags: oldTags, + Tags: [oldValue], }, }, }, }; const newTemplate = { Resources: { - QueueResource: { - Type: 'AWS::SQS::Queue', - }, BucketResource: { Type: 'AWS::S3::Bucket', Properties: { - BucketName: bucketName, - Tags: newTags, + Tags: [newValue], }, }, }, }; - + // WHEN const differences = diffTemplate(currentTemplate, newTemplate); + + // THEN expect(differences.differenceCount).toBe(1); expect(differences.resources.differenceCount).toBe(1); const difference = differences.resources.changes.BucketResource; expect(difference).not.toBeUndefined(); expect(difference?.oldResourceType).toEqual('AWS::S3::Bucket'); expect(difference?.propertyUpdates).toEqual({ - Tags: { oldValue: oldTags, newValue: newTags, changeImpact: ResourceImpact.WILL_UPDATE, isDifferent: true }, + Tags: { oldValue: [oldValue], newValue: [newValue], changeImpact: ResourceImpact.WILL_UPDATE, isDifferent: true }, }); }); test('when a property with a number-like format doesn\'t change', () => { - const bucketName = 'ShineyBucketName'; const tags = ['0.31.1-prod', '8.0.5.5.4-identifier', '1.1.1.1', '1.2.3']; const currentTemplate = { Resources: { - QueueResource: { - Type: 'AWS::SQS::Queue', - }, BucketResource: { Type: 'AWS::S3::Bucket', Properties: { - BucketName: bucketName, Tags: tags, }, }, @@ -653,13 +640,9 @@ test('when a property with a number-like format doesn\'t change', () => { }; const newTemplate = { Resources: { - QueueResource: { - Type: 'AWS::SQS::Queue', - }, BucketResource: { Type: 'AWS::S3::Bucket', Properties: { - BucketName: bucketName, Tags: tags, }, }, @@ -671,4 +654,4 @@ test('when a property with a number-like format doesn\'t change', () => { expect(differences.resources.differenceCount).toBe(0); const difference = differences.resources.changes.BucketResource; expect(difference).toBeUndefined(); -}); \ No newline at end of file +}); diff --git a/packages/@aws-cdk/cx-api/lib/features.ts b/packages/@aws-cdk/cx-api/lib/features.ts index 554c41929651d..986be7f4e136b 100644 --- a/packages/@aws-cdk/cx-api/lib/features.ts +++ b/packages/@aws-cdk/cx-api/lib/features.ts @@ -175,9 +175,9 @@ export const CLOUDFRONT_DEFAULT_SECURITY_POLICY_TLS_V1_2_2021 = '@aws-cdk/aws-cl */ export const FUTURE_FLAGS: { [key: string]: any } = { [APIGATEWAY_USAGEPLANKEY_ORDERINSENSITIVE_ID]: true, - [ENABLE_STACK_NAME_DUPLICATES_CONTEXT]: 'true', - [ENABLE_DIFF_NO_FAIL_CONTEXT]: 'true', - [STACK_RELATIVE_EXPORTS_CONTEXT]: 'true', + [ENABLE_STACK_NAME_DUPLICATES_CONTEXT]: true, + [ENABLE_DIFF_NO_FAIL_CONTEXT]: true, + [STACK_RELATIVE_EXPORTS_CONTEXT]: true, [DOCKER_IGNORE_SUPPORT]: true, [SECRETS_MANAGER_PARSE_OWNED_SECRET_NAME]: true, [KMS_DEFAULT_KEY_POLICIES]: true, diff --git a/packages/@aws-cdk/pipelines/ORIGINAL_API.md b/packages/@aws-cdk/pipelines/ORIGINAL_API.md index 73ac108d9c67f..447e39bb09bc2 100644 --- a/packages/@aws-cdk/pipelines/ORIGINAL_API.md +++ b/packages/@aws-cdk/pipelines/ORIGINAL_API.md @@ -41,7 +41,9 @@ all commands necessary to do a full CDK build and synth, so do include installing dependencies and running the CDK CLI. For example, the old API: ```ts -SimpleSynthAction.standardNpmSynth({ +const sourceArtifact = new codepipeline.Artifact(); +const cloudAssemblyArtifact = new codepipeline.Artifact(); +pipelines.SimpleSynthAction.standardNpmSynth({ sourceArtifact, cloudAssemblyArtifact, @@ -54,8 +56,10 @@ SimpleSynthAction.standardNpmSynth({ Becomes: ```ts -new ShellStep('Synth', { - input: /* source */, +new pipelines.ShellStep('Synth', { + input: pipelines.CodePipelineSource.connection('my-org/my-app', 'main', { + connectionArn: 'arn:aws:codestar-connections:us-east-1:222222222222:connection/7d2469ff-514a-4e4f-9003-5ca4a43cdc41', // Created using the AWS console * });', + }), commands: [ 'npm ci', 'npm run build', @@ -71,7 +75,7 @@ You can use any of the factory functions on `CodePipelineSource`. For example, for a GitHub source, the following old API: ```ts -sourceAction: new codepipeline_actions.GitHubSourceAction({ +sourceAction: new cpactions.GitHubSourceAction({ actionName: 'GitHub', output: sourceArtifact, // Replace these with your actual GitHub project name @@ -84,8 +88,8 @@ sourceAction: new codepipeline_actions.GitHubSourceAction({ Translates into: ```ts -input: CodePipelineSource.gitHub('OWNER/REPO', 'main', { - authentication: SecretValue.secretsManager('GITHUB_TOKEN_NAME'), +input: pipelines.CodePipelineSource.gitHub('OWNER/REPO', 'main', { + authentication: cdk.SecretValue.secretsManager('GITHUB_TOKEN_NAME'), }), ``` @@ -111,8 +115,9 @@ putting manual approvals in `pre` steps, and automated approvals in `post` steps For example, specifying a manual approval on a stage deployment in old API: ```ts +declare const pipeline: pipelines.CdkPipeline; const stage = pipeline.addApplicationStage(...); -stage.addAction(new ManualApprovalAction({ +stage.addAction(new pipelines.ManualApprovalAction({ actionName: 'ManualApproval', runOrder: testingStage.nextSequentialRunOrder(), })); @@ -121,9 +126,10 @@ stage.addAction(new ManualApprovalAction({ Becomes: ```ts -pipeline.addStage(..., { +const stage = new MyApplicationStage(this, 'MyApplication'); +pipeline.addStage(stage, { pre: [ - new ManualApprovalStep('ManualApproval'), + new pipelines.ManualApprovalStep('ManualApproval'), ], }); ``` @@ -139,7 +145,7 @@ For example, specifying an automated approval after a stage is deployed in the f ```ts const stage = pipeline.addApplicationStage(...); -stage.addActions(new ShellScriptAction({ +stage.addActions(new pipelines.ShellScriptAction({ actionName: 'MyValidation', commands: ['curl -Ssf $VAR'], useOutputs: { @@ -153,10 +159,10 @@ stage.addActions(new ShellScriptAction({ Becomes: ```ts -const stage = new MyStage(...); +const stage = new MyApplicationStage(this, 'MyApplication'); pipeline.addStage(stage, { post: [ - new CodeBuildStep('MyValidation', { + new pipelines.CodeBuildStep('MyValidation', { commands: ['curl -Ssf $VAR'], envFromCfnOutput: { VAR: stage.cfnOutput, @@ -174,7 +180,19 @@ customizations (like `buildEnvironment`). #### Change set approvals In the old API, there were two properties that were used to add actions to the pipeline -in between the `CreateChangeSet` and `ExecuteChangeSet` actions: `manualApprovals` and `extraRunOrderSpace`. These are not supported in the new API. +in between the `CreateChangeSet` and `ExecuteChangeSet` actions: `manualApprovals` and `extraRunOrderSpace`. +This can be achieved in the modern API via the `stackSteps` property, which allows steps to be added +at the stack level: + +```ts +const stage = new MyApplicationStage(this, 'MyApplication'); +pipeline.addStage(stage, { + stackSteps: [{ + stack: stage.stack1, + changeSet: [new pipelines.ManualApprovalStep('ChangeSet Approval')], + }], +}); +``` ### Custom CodePipeline Actions @@ -190,7 +208,6 @@ artifacts: ```ts import { Construct, Stage, Stack, StackProps, StageProps } from '@aws-cdk/core'; -import { CdkPipeline } from '@aws-cdk/pipelines'; import * as codepipeline from '@aws-cdk/aws-codepipeline'; /** @@ -203,20 +220,20 @@ class MyPipelineStack extends Stack { const sourceArtifact = new codepipeline.Artifact(); const cloudAssemblyArtifact = new codepipeline.Artifact(); - const pipeline = new CdkPipeline(this, 'Pipeline', { + const pipeline = new pipelines.CdkPipeline(this, 'Pipeline', { cloudAssemblyArtifact, - sourceAction: new codepipeline_actions.GitHubSourceAction({ + sourceAction: new cpactions.GitHubSourceAction({ actionName: 'GitHub', output: sourceArtifact, - oauthToken: SecretValue.secretsManager('GITHUB_TOKEN_NAME'), + oauthToken: cdk.SecretValue.secretsManager('GITHUB_TOKEN_NAME'), // Replace these with your actual GitHub project name owner: 'OWNER', repo: 'REPO', branch: 'main', // default: 'master' }), - synthAction: SimpleSynthAction.standardNpmSynth({ + synthAction: pipelines.SimpleSynthAction.standardNpmSynth({ sourceArtifact, cloudAssemblyArtifact, @@ -274,21 +291,21 @@ class MyPipelineStack extends Stack { const sourceArtifact = new codepipeline.Artifact(); const cloudAssemblyArtifact = new codepipeline.Artifact(); - const pipeline = new CdkPipeline(this, 'Pipeline', { + const pipeline = new pipelines.CdkPipeline(this, 'Pipeline', { pipelineName: 'MyAppPipeline', cloudAssemblyArtifact, - sourceAction: new codepipeline_actions.GitHubSourceAction({ + sourceAction: new cpactions.GitHubSourceAction({ actionName: 'GitHub', output: sourceArtifact, - oauthToken: SecretValue.secretsManager('GITHUB_TOKEN_NAME'), + oauthToken: cdk.SecretValue.secretsManager('GITHUB_TOKEN_NAME'), // Replace these with your actual GitHub project name owner: 'OWNER', repo: 'REPO', branch: 'main', // default: 'master' }), - synthAction: SimpleSynthAction.standardNpmSynth({ + synthAction: pipelines.SimpleSynthAction.standardNpmSynth({ sourceArtifact, cloudAssemblyArtifact, @@ -316,7 +333,7 @@ If you prefer more control over the underlying CodePipeline object, you can create one yourself, including custom Source and Build stages: ```ts -const codePipeline = new cp.Pipeline(pipelineStack, 'CodePipeline', { +const codePipeline = new codepipeline.Pipeline(pipelineStack, 'CodePipeline', { stages: [ { stageName: 'CustomSource', @@ -330,7 +347,7 @@ const codePipeline = new cp.Pipeline(pipelineStack, 'CodePipeline', { }); const app = new App(); -const cdkPipeline = new CdkPipeline(app, 'CdkPipeline', { +const cdkPipeline = new pipelines.CdkPipeline(app, 'CdkPipeline', { codePipeline, cloudAssemblyArtifact, }); @@ -360,9 +377,9 @@ using these, the source repository does not need to have a `buildspec.yml`. An e of using `SimpleSynthAction` to run a Maven build followed by a CDK synth: ```ts -const pipeline = new CdkPipeline(this, 'Pipeline', { +const pipeline = new pipelines.CdkPipeline(this, 'Pipeline', { // ... - synthAction: new SimpleSynthAction({ + synthAction: new pipelines.SimpleSynthAction({ sourceArtifact, cloudAssemblyArtifact, installCommands: ['npm install -g aws-cdk'], @@ -396,16 +413,16 @@ from the CA repo instead of NPM. class MyPipelineStack extends Stack { constructor(scope: Construct, id: string, props?: StackProps) { ... - const pipeline = new CdkPipeline(this, 'Pipeline', { + const pipeline = new pipelines.CdkPipeline(this, 'Pipeline', { ... - synthAction: SimpleSynthAction.standardNpmSynth({ + synthAction: pipelines.SimpleSynthAction.standardNpmSynth({ sourceArtifact, cloudAssemblyArtifact, // Use this to customize and a permissions required for the build // and synth rolePolicyStatements: [ - new PolicyStatement({ + new iam.PolicyStatement({ actions: ['codeartifact:*', 'sts:GetServiceBearerToken'], resources: ['arn:codeartifact:repo:arn'], }), @@ -477,7 +494,7 @@ const testingStage = pipeline.addApplicationStage(new MyApplication(this, 'Testi // Add a action -- in this case, a Manual Approval action // (for illustration purposes: testingStage.addManualApprovalAction() is a // convenience shorthand that does the same) -testingStage.addAction(new ManualApprovalAction({ +testingStage.addAction(new pipelines.ManualApprovalAction({ actionName: 'ManualApproval', runOrder: testingStage.nextSequentialRunOrder(), })); @@ -522,7 +539,7 @@ In its simplest form, adding validation actions looks like this: ```ts const stage = pipeline.addApplicationStage(new MyApplication(/* ... */)); -stage.addActions(new ShellScriptAction({ +stage.addActions(new pipelines.ShellScriptAction({ actionName: 'MyValidation', commands: ['curl -Ssf https://my.webservice.com/'], // Optionally specify a VPC if, for example, the service is deployed with a private load balancer @@ -563,7 +580,7 @@ const lbApp = new MyLbApplication(this, 'MyApp', { env: { /* ... */ } }); const stage = pipeline.addApplicationStage(lbApp); -stage.addActions(new ShellScriptAction({ +stage.addActions(new pipelines.ShellScriptAction({ // ... useOutputs: { // When the test is executed, this will make $URL contain the @@ -594,7 +611,7 @@ two ways. Either pass additional policy statements in the `rolePolicyStatements` property: ```ts -new ShellScriptAction({ +new pipelines.ShellScriptAction({ // ... rolePolicyStatements: [ new iam.PolicyStatement({ @@ -608,7 +625,7 @@ new ShellScriptAction({ The Action can also be used as a Grantable after having been added to a Pipeline: ```ts -const action = new ShellScriptAction({ /* ... */ }); +const action = new pipelines.ShellScriptAction({ /* ... */ }); pipeline.addStage('Test').addActions(action); bucket.grantRead(action); @@ -623,11 +640,11 @@ if they are executable shell scripts themselves). Pass the `sourceArtifact`: ```ts const sourceArtifact = new codepipeline.Artifact(); -const pipeline = new CdkPipeline(this, 'Pipeline', { +const pipeline = new pipelines.CdkPipeline(this, 'Pipeline', { // ... }); -const validationAction = new ShellScriptAction({ +const validationAction = new pipelines.ShellScriptAction({ actionName: 'TestUsingSourceArtifact', additionalArtifacts: [sourceArtifact], @@ -651,8 +668,8 @@ in the `ShellScriptAction`'s `additionalArtifacts`: const cloudAssemblyArtifact = new codepipeline.Artifact('CloudAsm'); const integTestsArtifact = new codepipeline.Artifact('IntegTests'); -const pipeline = new CdkPipeline(this, 'Pipeline', { - synthAction: SimpleSynthAction.standardNpmSynth({ +const pipeline = new pipelines.CdkPipeline(this, 'Pipeline', { + synthAction: pipelines.SimpleSynthAction.standardNpmSynth({ sourceArtifact, cloudAssemblyArtifact, buildCommands: ['npm run build'], @@ -666,7 +683,7 @@ const pipeline = new CdkPipeline(this, 'Pipeline', { // ... }); -const validationAction = new ShellScriptAction({ +const validationAction = new pipelines.ShellScriptAction({ actionName: 'TestUsingBuildArtifact', additionalArtifacts: [integTestsArtifact], // 'test.js' was produced from 'test/test.ts' during the synth step @@ -715,12 +732,11 @@ create an SNS Topic, subscribe your own email address, and pass it in via ```ts import * as sns from '@aws-cdk/aws-sns'; import * as subscriptions from '@aws-cdk/aws-sns-subscriptions'; -import * as pipelines from '@aws-cdk/pipelines'; const topic = new sns.Topic(this, 'SecurityChangesTopic'); topic.addSubscription(new subscriptions.EmailSubscription('test@email.com')); -const pipeline = new CdkPipeline(app, 'Pipeline', { /* ... */ }); +const pipeline = new pipelines.CdkPipeline(app, 'Pipeline', { /* ... */ }); const stage = pipeline.addApplicationStage(new MyApplication(this, 'PreProd'), { confirmBroadeningPermissions: true, securityNotificationTopic: topic, diff --git a/packages/@aws-cdk/pipelines/README.md b/packages/@aws-cdk/pipelines/README.md index 3013140344bef..bd6525542920b 100644 --- a/packages/@aws-cdk/pipelines/README.md +++ b/packages/@aws-cdk/pipelines/README.md @@ -43,14 +43,31 @@ CodePipeline engine, define a `CodePipeline` construct. The following example creates a CodePipeline that deploys an application from GitHub: ```ts -/** The stacks for our app are defined in my-stacks.ts. The internals of these +/** The stacks for our app are minimally defined here. The internals of these * stacks aren't important, except that DatabaseStack exposes an attribute * "table" for a database table it defines, and ComputeStack accepts a reference * to this table in its properties. */ -import { DatabaseStack, ComputeStack } from '../lib/my-stacks'; -import { Construct, Stage, Stack, StackProps, StageProps } from '@aws-cdk/core'; -import { CodePipeline, CodePipelineSource, ShellStep } from '@aws-cdk/pipelines'; +class DatabaseStack extends Stack { + public readonly table: dynamodb.Table; + + constructor(scope: Construct, id: string) { + super(scope, id); + this.table = new dynamodb.Table(this, 'Table', { + partitionKey: { name: 'id', type: dynamodb.AttributeType.STRING } + }); + } +} + +interface ComputeProps { + readonly table: dynamodb.Table; +} + +class ComputeStack extends Stack { + constructor(scope: Construct, id: string, props: ComputeProps) { + super(scope, id); + } +} /** * Stack to hold the pipeline @@ -59,11 +76,11 @@ class MyPipelineStack extends Stack { constructor(scope: Construct, id: string, props?: StackProps) { super(scope, id, props); - const pipeline = new CodePipeline(this, 'Pipeline', { - synth: new ShellStep('Synth', { + const pipeline = new pipelines.CodePipeline(this, 'Pipeline', { + synth: new pipelines.ShellStep('Synth', { // Use a connection created using the AWS console to authenticate to GitHub // Other sources are available. - input: CodePipelineSource.connection('my-org/my-app', 'main', { + input: pipelines.CodePipelineSource.connection('my-org/my-app', 'main', { connectionArn: 'arn:aws:codestar-connections:us-east-1:222222222222:connection/7d2469ff-514a-4e4f-9003-5ca4a43cdc41', // Created using the AWS console * });', }), commands: [ @@ -81,7 +98,7 @@ class MyPipelineStack extends Stack { env: { account: '123456789012', region: 'eu-west-1', - } + }, })); } } @@ -106,7 +123,7 @@ class MyApplication extends Stage { } // In your main file -new MyPipelineStack(app, 'PipelineStack', { +new MyPipelineStack(this, 'PipelineStack', { env: { account: '123456789012', region: 'eu-west-1', @@ -172,15 +189,25 @@ off temporarily, by passing `selfMutation: false` property, example: ```ts // Modern API -const pipeline = new CodePipeline(this, 'Pipeline', { +const modernPipeline = new pipelines.CodePipeline(this, 'Pipeline', { selfMutation: false, - ... + synth: new pipelines.ShellStep('Synth', { + input: pipelines.CodePipelineSource.connection('my-org/my-app', 'main', { + connectionArn: 'arn:aws:codestar-connections:us-east-1:222222222222:connection/7d2469ff-514a-4e4f-9003-5ca4a43cdc41', // Created using the AWS console * });', + }), + commands: [ + 'npm ci', + 'npm run build', + 'npx cdk synth', + ], + }), }); // Original API -const pipeline = new CdkPipeline(this, 'Pipeline', { +const cloudAssemblyArtifact = new codepipeline.Artifact(); +const originalPipeline = new pipelines.CdkPipeline(this, 'Pipeline', { selfMutating: false, - ... + cloudAssemblyArtifact, }); ``` @@ -204,10 +231,10 @@ commands required will depend on the programming language you are using. For a typical NPM-based project, the synth will look like this: ```ts -const source = /* the repository source */; +declare const source: pipelines.IFileSetProducer; // the repository source -const pipeline = new CodePipeline(this, 'Pipeline', { - synth: new ShellStep('Synth', { +const pipeline = new pipelines.CodePipeline(this, 'Pipeline', { + synth: new pipelines.ShellStep('Synth', { input: source, commands: [ 'npm ci', @@ -224,8 +251,10 @@ CDK project lives in a subdirectory, be sure to adjust the `primaryOutputDirectory` to match: ```ts -const pipeline = new CodePipeline(this, 'Pipeline', { - synth: new ShellStep('Synth', { +declare const source: pipelines.IFileSetProducer; // the repository source + +const pipeline = new pipelines.CodePipeline(this, 'Pipeline', { + synth: new pipelines.ShellStep('Synth', { input: source, commands: [ 'cd mysubdir', @@ -254,8 +283,10 @@ look like in a number of different situations. For Yarn, the install commands are different: ```ts -const pipeline = new CodePipeline(this, 'Pipeline', { - synth: new ShellStep('Synth', { +declare const source: pipelines.IFileSetProducer; // the repository source + +const pipeline = new pipelines.CodePipeline(this, 'Pipeline', { + synth: new pipelines.ShellStep('Synth', { input: source, commands: [ 'yarn install --frozen-lockfile', @@ -270,8 +301,10 @@ For Python projects, remember to install the CDK CLI globally (as there is no `package.json` to automatically install it for you): ```ts -const pipeline = new CodePipeline(this, 'Pipeline', { - synth: new ShellStep('Synth', { +declare const source: pipelines.IFileSetProducer; // the repository source + +const pipeline = new pipelines.CodePipeline(this, 'Pipeline', { + synth: new pipelines.ShellStep('Synth', { input: source, commands: [ 'pip install -r requirements.txt', @@ -288,8 +321,10 @@ and the Maven compilation step is automatically executed for you as you run `cdk synth`: ```ts -const pipeline = new CodePipeline(this, 'Pipeline', { - synth: new ShellStep('Synth', { +declare const source: pipelines.IFileSetProducer; // the repository source + +const pipeline = new pipelines.CodePipeline(this, 'Pipeline', { + synth: new pipelines.ShellStep('Synth', { input: source, commands: [ 'npm install -g aws-cdk', @@ -314,7 +349,7 @@ You will first use the AWS Console to authenticate to the source control provider, and then use the connection ARN in your pipeline definition: ```ts -CodePipelineSource.connection('org/repo', 'branch', { +pipelines.CodePipelineSource.connection('org/repo', 'branch', { connectionArn: 'arn:aws:codestar-connections:us-east-1:222222222222:connection/7d2469ff-514a-4e4f-9003-5ca4a43cdc41', }); ``` @@ -328,9 +363,9 @@ you can change the name. The token should have the **repo** and **admin:repo_hoo scopes. ```ts -CodePipelineSource.gitHub('org/repo', 'branch', { +pipelines.CodePipelineSource.gitHub('org/repo', 'branch', { // This is optional - authentication: SecretValue.secretsManager('my-token'), + authentication: cdk.SecretValue.secretsManager('my-token'), }); ``` @@ -341,8 +376,8 @@ that the CodeCommit repository and then use `CodePipelineSource.codeCommit` to reference it: ```ts -const repository = codecommit.fromRepositoryName(this, 'Repository', 'my-repository'); -CodePipelineSource.codeCommit(repository); +const repository = codecommit.Repository.fromRepositoryName(this, 'Repository', 'my-repository'); +pipelines.CodePipelineSource.codeCommit(repository, 'main'); ``` ##### S3 @@ -352,7 +387,7 @@ triggered every time the file in S3 is changed: ```ts const bucket = s3.Bucket.fromBucketName(this, 'Bucket', 'my-bucket'); -CodePipelineSource.s3(bucket, 'my/source.zip'); +pipelines.CodePipelineSource.s3(bucket, 'my/source.zip'); ``` #### Additional inputs @@ -363,17 +398,17 @@ output file set can be used as an input, such as a `CodePipelineSource`, but also other `ShellStep`: ```ts -const prebuild = new ShellStep('Prebuild', { - input: CodePipelineSource.gitHub('myorg/repo1'), +const prebuild = new pipelines.ShellStep('Prebuild', { + input: pipelines.CodePipelineSource.gitHub('myorg/repo1', 'main'), primaryOutputDirectory: './build', commands: ['./build.sh'], }); -const pipeline = new CodePipeline(this, 'Pipeline', { - synth: new ShellStep('Synth', { - input: CodePipelineSource.gitHub('myorg/repo2'), +const pipeline = new pipelines.CodePipeline(this, 'Pipeline', { + synth: new pipelines.ShellStep('Synth', { + input: pipelines.CodePipelineSource.gitHub('myorg/repo2', 'main'), additionalInputs: { - 'subdir': CodePipelineSource.gitHub('myorg/repo3'), + 'subdir': pipelines.CodePipelineSource.gitHub('myorg/repo3', 'main'), '../siblingdir': prebuild, }, @@ -389,6 +424,7 @@ more CDK `Stages` which will be deployed to their target environments. To do so, call `pipeline.addStage()` on the Stage object: ```ts +declare const pipeline: pipelines.CodePipeline; // Do this as many times as necessary with any account and region // Account and region may different from the pipeline's. pipeline.addStage(new MyApplicationStage(this, 'Prod', { @@ -421,6 +457,7 @@ deployed in sequence. For example, the following will deploy two copies of your application to `eu-west-1` and `eu-central-1` in parallel: ```ts +declare const pipeline: pipelines.CodePipeline; const europeWave = pipeline.addWave('Europe'); europeWave.addStage(new MyApplicationStage(this, 'Ireland', { env: { region: 'eu-west-1' } @@ -445,9 +482,19 @@ KMS key. Example: ```ts -const pipeline = new CodePipeline(this, 'Pipeline', { +const pipeline = new pipelines.CodePipeline(this, 'Pipeline', { // Encrypt artifacts, required for cross-account deployments crossAccountKeys: true, + synth: new pipelines.ShellStep('Synth', { + input: pipelines.CodePipelineSource.connection('my-org/my-app', 'main', { + connectionArn: 'arn:aws:codestar-connections:us-east-1:222222222222:connection/7d2469ff-514a-4e4f-9003-5ca4a43cdc41', // Created using the AWS console * });', + }), + commands: [ + 'npm ci', + 'npm run build', + 'npx cdk synth', + ], + }), }); ``` @@ -464,19 +511,20 @@ a manual approval in the form of a `ManualApprovalStep` added to the pipeline. B pass in order to promote from the `PreProd` to the `Prod` environment: ```ts -const preprod = new MyApplicationStage(this, 'PreProd', { ... }); -const prod = new MyApplicationStage(this, 'Prod', { ... }); +declare const pipeline: pipelines.CodePipeline; +const preprod = new MyApplicationStage(this, 'PreProd'); +const prod = new MyApplicationStage(this, 'Prod'); pipeline.addStage(preprod, { post: [ - new ShellStep('Validate Endpoint', { + new pipelines.ShellStep('Validate Endpoint', { commands: ['curl -Ssf https://my.webservice.com/'], }), ], }); pipeline.addStage(prod, { pre: [ - new ManualApprovalStep('PromoteToProd'), + new pipelines.ManualApprovalStep('PromoteToProd'), ], }); ``` @@ -484,15 +532,29 @@ pipeline.addStage(prod, { You can also specify steps to be executed at the stack level. To achieve this, you can specify the stack and step via the `stackSteps` property: ```ts +class MyStacksStage extends Stage { + public readonly stack1: Stack; + public readonly stack2: Stack; + + constructor(scope: Construct, id: string, props?: StageProps) { + super(scope, id, props); + this.stack1 = new Stack(this, 'stack1'); + this.stack2 = new Stack(this, 'stack2'); + } +} + +declare const pipeline: pipelines.CodePipeline; +const prod = new MyStacksStage(this, 'Prod'); + pipeline.addStage(prod, { stackSteps: [{ stack: prod.stack1, - pre: [new ManualApprovalStep('Pre-Stack Check')], // Executed before stack is prepared - changeSet: [new ManualApprovalStep('ChangeSet Approval')], // Executed after stack is prepared but before the stack is deployed - post: [new ManualApprovalStep('Post-Deploy Check')], // Executed after staack is deployed + pre: [new pipelines.ManualApprovalStep('Pre-Stack Check')], // Executed before stack is prepared + changeSet: [new pipelines.ManualApprovalStep('ChangeSet Approval')], // Executed after stack is prepared but before the stack is deployed + post: [new pipelines.ManualApprovalStep('Post-Deploy Check')], // Executed after staack is deployed }, { stack: prod.stack2, - post: [new ManualApprovalStep('Post-Deploy Check')], // Executed after staack is deployed + post: [new pipelines.ManualApprovalStep('Post-Deploy Check')], // Executed after staack is deployed }], }); ``` @@ -507,21 +569,26 @@ To use Stack Outputs, expose the `CfnOutput` object you're interested in, and pass it to `envFromCfnOutputs` of the `ShellStep`: ```ts -class MyApplicationStage extends Stage { +class MyOutputStage extends Stage { public readonly loadBalancerAddress: CfnOutput; - // ... + + constructor(scope: Construct, id: string, props?: StageProps) { + super(scope, id, props); + this.loadBalancerAddress = new CfnOutput(this, 'Output', {value: 'value'}); + } } -const lbApp = new MyApplicationStage(this, 'MyApp', { /* ... */ }); +const lbApp = new MyOutputStage(this, 'MyApp'); +declare const pipeline: pipelines.CodePipeline; pipeline.addStage(lbApp, { post: [ - new ShellStep('HitEndpoint', { + new pipelines.ShellStep('HitEndpoint', { envFromCfnOutputs: { // Make the load balancer address available as $URL inside the commands URL: lbApp.loadBalancerAddress, }, commands: ['curl -Ssf $URL'], - }); + }), ], }); ``` @@ -539,12 +606,13 @@ Here's an example that captures an additional output directory in the synth step and runs tests from there: ```ts -const synth = new ShellStep('Synth', { /* ... */ }); -const pipeline = new CodePipeline(this, 'Pipeline', { synth }); +declare const synth: pipelines.ShellStep; +const stage = new MyApplicationStage(this, 'MyApplication'); +const pipeline = new pipelines.CodePipeline(this, 'Pipeline', { synth }); -pipeline.addStage(/* ... */, { +pipeline.addStage(stage, { post: [ - new ShellStep('Approve', { + new pipelines.ShellStep('Approve', { // Use the contents of the 'integ' directory from the synth step as the input input: synth.addOutputDirectory('integ'), commands: ['cd integ && ./run.sh'], @@ -562,7 +630,9 @@ generated, use a `CodeBuildStep` instead of a `ShellStep`. This class has a numb of properties that allow you to customize various aspects of the projects: ```ts -new CodeBuildStep('Synth', { +declare const vpc: ec2.Vpc; +declare const mySecurityGroup: ec2.SecurityGroup; +new pipelines.CodeBuildStep('Synth', { // ...standard ShellStep props... commands: [/* ... */], env: { /* ... */ }, @@ -602,8 +672,20 @@ or just for the synth, asset publishing, and self-mutation projects by passing ` `assetPublishingCodeBuildDefaults`, or `selfMutationCodeBuildDefaults`: ```ts -new CodePipeline(this, 'Pipeline', { - // ... +declare const vpc: ec2.Vpc; +declare const mySecurityGroup: ec2.SecurityGroup; +new pipelines.CodePipeline(this, 'Pipeline', { + // Standard CodePipeline properties + synth: new pipelines.ShellStep('Synth', { + input: pipelines.CodePipelineSource.connection('my-org/my-app', 'main', { + connectionArn: 'arn:aws:codestar-connections:us-east-1:222222222222:connection/7d2469ff-514a-4e4f-9003-5ca4a43cdc41', // Created using the AWS console * });', + }), + commands: [ + 'npm ci', + 'npm run build', + 'npx cdk synth', + ], + }), // Defaults for all CodeBuild projects codeBuildDefaults: { @@ -644,15 +726,19 @@ doesn't have a matching class yet, you can define your own step class that exten Here's an example that adds a Jenkins step: ```ts -class MyJenkinsStep extends Step implements ICodePipelineActionFactory { - constructor(private readonly provider: codepipeline_actions.JenkinsProvider, private readonly input: FileSet) { +class MyJenkinsStep extends pipelines.Step implements pipelines.ICodePipelineActionFactory { + constructor( + private readonly provider: cpactions.JenkinsProvider, + private readonly input: pipelines.FileSet, + ) { + super('MyJenkinsStep'); } - public produceAction(stage: codepipeline.IStage, options: ProduceActionOptions): CodePipelineActionFactoryResult { + public produceAction(stage: codepipeline.IStage, options: pipelines.ProduceActionOptions): pipelines.CodePipelineActionFactoryResult { // This is where you control what type of Action gets added to the // CodePipeline - stage.addAction(new codepipeline_actions.JenkinsAction({ + stage.addAction(new cpactions.JenkinsAction({ // Copy 'actionName' and 'runOrder' from the options actionName: options.actionName, runOrder: options.runOrder, @@ -700,8 +786,13 @@ stacks the pipeline is deploying), for example by the use of `LinuxBuildImage.fr you need to pass `dockerEnabledForSelfMutation: true` to the pipeline. For example: ```ts -const pipeline = new CodePipeline(this, 'Pipeline', { - // ... +const pipeline = new pipelines.CodePipeline(this, 'Pipeline', { + synth: new pipelines.ShellStep('Synth', { + input: pipelines.CodePipelineSource.connection('my-org/my-app', 'main', { + connectionArn: 'arn:aws:codestar-connections:us-east-1:222222222222:connection/7d2469ff-514a-4e4f-9003-5ca4a43cdc41', // Created using the AWS console * });', + }), + commands: ['npm ci','npm run build','npx cdk synth'], + }), // Turn this on because the pipeline uses Docker image assets dockerEnabledForSelfMutation: true, @@ -709,16 +800,16 @@ const pipeline = new CodePipeline(this, 'Pipeline', { pipeline.addWave('MyWave', { post: [ - new CodeBuildStep('RunApproval', { + new pipelines.CodeBuildStep('RunApproval', { commands: ['command-from-image'], buildEnvironment: { // The user of a Docker image asset in the pipeline requires turning on // 'dockerEnabledForSelfMutation'. - buildImage: LinuxBuildImage.fromAsset(this, 'Image', { + buildImage: codebuild.LinuxBuildImage.fromAsset(this, 'Image', { directory: './docker-image', - }) + }), }, - }) + }), ], }); ``` @@ -734,8 +825,13 @@ if you add a construct like `@aws-cdk/aws-lambda-nodejs`), you need to pass `dockerEnabledForSynth: true` to the pipeline. For example: ```ts -const pipeline = new CodePipeline(this, 'Pipeline', { - // ... +const pipeline = new pipelines.CodePipeline(this, 'Pipeline', { + synth: new pipelines.ShellStep('Synth', { + input: pipelines.CodePipelineSource.connection('my-org/my-app', 'main', { + connectionArn: 'arn:aws:codestar-connections:us-east-1:222222222222:connection/7d2469ff-514a-4e4f-9003-5ca4a43cdc41', // Created using the AWS console * });', + }), + commands: ['npm ci','npm run build','npx cdk synth'], + }), // Turn this on because the application uses bundled file assets dockerEnabledForSynth: true, @@ -756,16 +852,21 @@ different environment (e.g., ECR repo) or to avoid throttling (e.g., DockerHub). ```ts const dockerHubSecret = secretsmanager.Secret.fromSecretCompleteArn(this, 'DHSecret', 'arn:aws:...'); const customRegSecret = secretsmanager.Secret.fromSecretCompleteArn(this, 'CRSecret', 'arn:aws:...'); -const repo1 = ecr.Repository.fromRepositoryArn(stack, 'Repo', 'arn:aws:ecr:eu-west-1:0123456789012:repository/Repo1'); -const repo2 = ecr.Repository.fromRepositoryArn(stack, 'Repo', 'arn:aws:ecr:eu-west-1:0123456789012:repository/Repo2'); +const repo1 = ecr.Repository.fromRepositoryArn(this, 'Repo', 'arn:aws:ecr:eu-west-1:0123456789012:repository/Repo1'); +const repo2 = ecr.Repository.fromRepositoryArn(this, 'Repo', 'arn:aws:ecr:eu-west-1:0123456789012:repository/Repo2'); -const pipeline = new CodePipeline(this, 'Pipeline', { +const pipeline = new pipelines.CodePipeline(this, 'Pipeline', { dockerCredentials: [ - DockerCredential.dockerHub(dockerHubSecret), - DockerCredential.customRegistry('dockerregistry.example.com', customRegSecret), - DockerCredential.ecr([repo1, repo2]); + pipelines.DockerCredential.dockerHub(dockerHubSecret), + pipelines.DockerCredential.customRegistry('dockerregistry.example.com', customRegSecret), + pipelines.DockerCredential.ecr([repo1, repo2]), ], - // ... + synth: new pipelines.ShellStep('Synth', { + input: pipelines.CodePipelineSource.connection('my-org/my-app', 'main', { + connectionArn: 'arn:aws:codestar-connections:us-east-1:222222222222:connection/7d2469ff-514a-4e4f-9003-5ca4a43cdc41', // Created using the AWS console * });', + }), + commands: ['npm ci','npm run build','npx cdk synth'], + }), }); ``` @@ -784,7 +885,7 @@ the **Synth**, **Self-Update**, and **Asset Publishing** actions within the ```ts const dockerHubSecret = secretsmanager.Secret.fromSecretCompleteArn(this, 'DHSecret', 'arn:aws:...'); // Only the image asset publishing actions will be granted read access to the secret. -const creds = DockerCredential.dockerHub(dockerHubSecret, { usages: [DockerCredentialUsage.ASSET_PUBLISHING] }); +const creds = pipelines.DockerCredential.dockerHub(dockerHubSecret, { usages: [pipelines.DockerCredentialUsage.ASSET_PUBLISHING] }); ``` ## CDK Environment Bootstrapping @@ -953,9 +1054,11 @@ give the synth CodeBuild execution role permissions to assume the bootstrapped lookup roles. As an example, doing so would look like this: ```ts -new CodePipeline(this, 'Pipeline', { - synth: new CodeBuildStep('Synth', { - input: // ...input... +new pipelines.CodePipeline(this, 'Pipeline', { + synth: new pipelines.CodeBuildStep('Synth', { + input: pipelines.CodePipelineSource.connection('my-org/my-app', 'main', { + connectionArn: 'arn:aws:codestar-connections:us-east-1:222222222222:connection/7d2469ff-514a-4e4f-9003-5ca4a43cdc41', // Created using the AWS console * });', + }), commands: [ // Commands to load cdk.context.json from somewhere here '...', @@ -1034,10 +1137,11 @@ Pipeline You can insert the security check by using a `ConfirmPermissionsBroadening` step: ```ts +declare const pipeline: pipelines.CodePipeline; const stage = new MyApplicationStage(this, 'MyApplication'); pipeline.addStage(stage, { pre: [ - new ConfirmPermissionsBroadening('Check', { stage }), + new pipelines.ConfirmPermissionsBroadening('Check', { stage }), ], }); ``` @@ -1047,17 +1151,14 @@ create an SNS Topic, subscribe your own email address, and pass it in as as the `notificationTopic` property: ```ts -import * as sns from '@aws-cdk/aws-sns'; -import * as subscriptions from '@aws-cdk/aws-sns-subscriptions'; -import * as pipelines from '@aws-cdk/pipelines'; - +declare const pipeline: pipelines.CodePipeline; const topic = new sns.Topic(this, 'SecurityChangesTopic'); topic.addSubscription(new subscriptions.EmailSubscription('test@email.com')); const stage = new MyApplicationStage(this, 'MyApplication'); pipeline.addStage(stage, { pre: [ - new ConfirmPermissionsBroadening('Check', { + new pipelines.ConfirmPermissionsBroadening('Check', { stage, notificationTopic: topic, }), @@ -1162,19 +1263,19 @@ that bundles asset using tools run via Docker, like `aws-lambda-nodejs`, `aws-la Make sure you set the `privileged` environment variable to `true` in the synth definition: -```typescript - const pipeline = new CdkPipeline(this, 'MyPipeline', { - ... - - synthAction: SimpleSynthAction.standardNpmSynth({ - sourceArtifact: ..., - cloudAssemblyArtifact: ..., - - environment: { - privileged: true, - }, - }), - }); +```ts +const sourceArtifact = new codepipeline.Artifact(); +const cloudAssemblyArtifact = new codepipeline.Artifact(); +const pipeline = new pipelines.CdkPipeline(this, 'MyPipeline', { + cloudAssemblyArtifact, + synthAction: pipelines.SimpleSynthAction.standardNpmSynth({ + sourceArtifact, + cloudAssemblyArtifact, + environment: { + privileged: true, + }, + }), +}); ``` After turning on `privilegedMode: true`, you will need to do a one-time manual cdk deploy of your @@ -1201,10 +1302,11 @@ This happens because the pipeline is not self-mutating and, as a consequence, th build projects get out-of-sync with the generated templates. To fix this, make sure the `selfMutating` property is set to `true`: -```typescript -const pipeline = new CdkPipeline(this, 'MyPipeline', { +```ts +const cloudAssemblyArtifact = new codepipeline.Artifact(); +const pipeline = new pipelines.CdkPipeline(this, 'MyPipeline', { selfMutating: true, - ... + cloudAssemblyArtifact, }); ``` @@ -1240,9 +1342,9 @@ $ env CDK_NEW_BOOTSTRAP=1 npx cdk bootstrap \ See https://docs.aws.amazon.com/cdk/latest/guide/bootstrapping.html for more info. ```ts -new MyStack(this, 'MyStack', { +new Stack(this, 'MyStack', { // Update this qualifier to match the one used above. - synthesizer: new DefaultStackSynthesizer({ + synthesizer: new cdk.DefaultStackSynthesizer({ qualifier: 'randchars1234', }), }); diff --git a/packages/@aws-cdk/pipelines/lib/blueprint/shell-step.ts b/packages/@aws-cdk/pipelines/lib/blueprint/shell-step.ts index 75c1883d92419..1f03105c78ee9 100644 --- a/packages/@aws-cdk/pipelines/lib/blueprint/shell-step.ts +++ b/packages/@aws-cdk/pipelines/lib/blueprint/shell-step.ts @@ -65,11 +65,11 @@ export interface ShellStepProps { * following configuration: * * ```ts - * const script = new ShellStep('MainScript', { - * // ... - * input: MyEngineSource.gitHub('org/source1'), + * const script = new pipelines.ShellStep('MainScript', { + * commands: ['npm ci','npm run build','npx cdk synth'], + * input: pipelines.CodePipelineSource.gitHub('org/source1', 'main'), * additionalInputs: { - * '../siblingdir': MyEngineSource.gitHub('org/source2'), + * '../siblingdir': pipelines.CodePipelineSource.gitHub('org/source2', 'main'), * } * }); * ``` diff --git a/packages/@aws-cdk/pipelines/lib/codepipeline/codepipeline-source.ts b/packages/@aws-cdk/pipelines/lib/codepipeline/codepipeline-source.ts index b6d10b03f2f67..382bed08028ff 100644 --- a/packages/@aws-cdk/pipelines/lib/codepipeline/codepipeline-source.ts +++ b/packages/@aws-cdk/pipelines/lib/codepipeline/codepipeline-source.ts @@ -26,7 +26,7 @@ export abstract class CodePipelineSource extends Step implements ICodePipelineAc * Pass in the owner and repository in a single string, like this: * * ```ts - * CodePipelineSource.gitHub('owner/repo', 'main'); + * pipelines.CodePipelineSource.gitHub('owner/repo', 'main'); * ``` * * Authentication will be done by a secret called `github-token` in AWS @@ -51,8 +51,8 @@ export abstract class CodePipelineSource extends Step implements ICodePipelineAc * Example: * * ```ts - * const bucket: IBucket = ... - * CodePipelineSource.s3(bucket, { + * declare const bucket: s3.Bucket; + * pipelines.CodePipelineSource.s3(bucket, { * key: 'path/to/file.zip', * }); * ``` @@ -74,7 +74,7 @@ export abstract class CodePipelineSource extends Step implements ICodePipelineAc * Example: * * ```ts - * CodePipelineSource.connection('owner/repo', 'main', { + * pipelines.CodePipelineSource.connection('owner/repo', 'main', { * connectionArn: 'arn:aws:codestar-connections:us-east-1:222222222222:connection/7d2469ff-514a-4e4f-9003-5ca4a43cdc41', // Created using the AWS console * }); * ``` @@ -131,7 +131,6 @@ export interface GitHubSourceOptions { * * ```ts * const oauth = cdk.SecretValue.secretsManager('my-github-token'); - * new GitHubSource(this, 'GitHubSource', { authentication: oauth, ... }); * ``` * * The GitHub Personal Access Token should have these scopes: diff --git a/packages/@aws-cdk/pipelines/lib/legacy/actions/deploy-cdk-stack-action.ts b/packages/@aws-cdk/pipelines/lib/legacy/actions/deploy-cdk-stack-action.ts index af6b7821a308d..1fc92da472ea1 100644 --- a/packages/@aws-cdk/pipelines/lib/legacy/actions/deploy-cdk-stack-action.ts +++ b/packages/@aws-cdk/pipelines/lib/legacy/actions/deploy-cdk-stack-action.ts @@ -16,6 +16,8 @@ import { Construct as CoreConstruct } from '@aws-cdk/core'; /** * Customization options for a DeployCdkStackAction + * + * @deprecated This class is part of the old API. Use the API based on the `CodePipeline` class instead */ export interface DeployCdkStackActionOptions { /** @@ -68,6 +70,8 @@ export interface DeployCdkStackActionOptions { /** * Properties for a DeployCdkStackAction + * + * @deprecated This class is part of the old API. Use the API based on the `CodePipeline` class instead */ export interface DeployCdkStackActionProps extends DeployCdkStackActionOptions { /** @@ -129,6 +133,8 @@ export interface DeployCdkStackActionProps extends DeployCdkStackActionOptions { /** * Options for the 'fromStackArtifact' operation + * + * @deprecated This class is part of the old API. Use the API based on the `CodePipeline` class instead */ export interface CdkStackActionFromArtifactOptions extends DeployCdkStackActionOptions { /** @@ -147,6 +153,8 @@ export interface CdkStackActionFromArtifactOptions extends DeployCdkStackActionO * * You do not need to instantiate this action yourself -- it will automatically * be added by the pipeline when you add stack artifacts or entire stages. + * + * @deprecated This class is part of the old API. Use the API based on the `CodePipeline` class instead */ export class DeployCdkStackAction implements codepipeline.IAction { /** @@ -317,6 +325,8 @@ function roleFromPlaceholderArn(scope: Construct, region: string | undefined, /** * Options for CdkDeployAction.fromStackArtifact + * + * @deprecated This class is part of the old API. Use the API based on the `CodePipeline` class instead */ export interface FromStackArtifactOptions { /** diff --git a/packages/@aws-cdk/pipelines/lib/legacy/actions/publish-assets-action.ts b/packages/@aws-cdk/pipelines/lib/legacy/actions/publish-assets-action.ts index 1c112aec9afbe..055744cb971c2 100644 --- a/packages/@aws-cdk/pipelines/lib/legacy/actions/publish-assets-action.ts +++ b/packages/@aws-cdk/pipelines/lib/legacy/actions/publish-assets-action.ts @@ -17,6 +17,8 @@ import { Construct as CoreConstruct } from '@aws-cdk/core'; /** * Props for a PublishAssetsAction + * + * @deprecated This class is part of the old API. Use the API based on the `CodePipeline` class instead */ export interface PublishAssetsActionProps { /** @@ -111,6 +113,8 @@ export interface PublishAssetsActionProps { * * You do not need to instantiate this action -- it will automatically * be added by the pipeline when you add stacks that use assets. + * + * @deprecated This class is part of the old API. Use the API based on the `CodePipeline` class instead */ export class PublishAssetsAction extends CoreConstruct implements codepipeline.IAction { private readonly action: codepipeline.IAction; diff --git a/packages/@aws-cdk/pipelines/lib/legacy/actions/update-pipeline-action.ts b/packages/@aws-cdk/pipelines/lib/legacy/actions/update-pipeline-action.ts index bc50e80c0aa56..da4278dd6faf2 100644 --- a/packages/@aws-cdk/pipelines/lib/legacy/actions/update-pipeline-action.ts +++ b/packages/@aws-cdk/pipelines/lib/legacy/actions/update-pipeline-action.ts @@ -14,6 +14,8 @@ import { Construct as CoreConstruct } from '@aws-cdk/core'; /** * Props for the UpdatePipelineAction + * + * @deprecated This class is part of the old API. Use the API based on the `CodePipeline` class instead */ export interface UpdatePipelineActionProps { /** @@ -79,6 +81,8 @@ export interface UpdatePipelineActionProps { * * You do not need to instantiate this action -- it will automatically * be added by the pipeline. + * + * @deprecated This class is part of the old API. Use the API based on the `CodePipeline` class instead */ export class UpdatePipelineAction extends CoreConstruct implements codepipeline.IAction { private readonly action: codepipeline.IAction; diff --git a/packages/@aws-cdk/pipelines/lib/legacy/pipeline.ts b/packages/@aws-cdk/pipelines/lib/legacy/pipeline.ts index 60f158f298fce..ef0d1d209619b 100644 --- a/packages/@aws-cdk/pipelines/lib/legacy/pipeline.ts +++ b/packages/@aws-cdk/pipelines/lib/legacy/pipeline.ts @@ -21,6 +21,8 @@ import { Construct as CoreConstruct } from '@aws-cdk/core'; const CODE_BUILD_LENGTH_LIMIT = 100; /** * Properties for a CdkPipeline + * + * @deprecated This class is part of the old API. Use the API based on the `CodePipeline` class instead */ export interface CdkPipelineProps { /** @@ -49,7 +51,7 @@ export interface CdkPipelineProps { * You can choose to not pass this value, in which case a new CodePipeline is * created with default settings. * - * If you pass an existing CodePipeline, it should should have been created + * If you pass an existing CodePipeline, it should have been created * with `restartExecutionOnUpdate: true`. * * [disable-awslint:ref-via-interface] @@ -206,6 +208,8 @@ export interface CdkPipelineProps { * - Asset publishing. * - Keeping the pipeline up-to-date as the CDK apps change. * - Using stack outputs later on in the pipeline. + * + * @deprecated This class is part of the old API. Use the API based on the `CodePipeline` class instead */ export class CdkPipeline extends CoreConstruct { private readonly _pipeline: codepipeline.Pipeline; diff --git a/packages/@aws-cdk/pipelines/lib/legacy/stage.ts b/packages/@aws-cdk/pipelines/lib/legacy/stage.ts index bfb997e908196..c054e9a0592fb 100644 --- a/packages/@aws-cdk/pipelines/lib/legacy/stage.ts +++ b/packages/@aws-cdk/pipelines/lib/legacy/stage.ts @@ -20,6 +20,8 @@ import { Construct as CoreConstruct } from '@aws-cdk/core'; /** * Construction properties for a CdkStage + * + * @deprecated This class is part of the old API. Use the API based on the `CodePipeline` class instead */ export interface CdkStageProps { /** @@ -70,6 +72,8 @@ export interface CdkStageProps { * * You don't need to instantiate this class directly. Use * `cdkPipeline.addStage()` instead. + * + * @deprecated This class is part of the old API. Use the API based on the `CodePipeline` class instead */ export class CdkStage extends CoreConstruct { private _nextSequentialRunOrder = 1; // Must start at 1 eh @@ -396,6 +400,8 @@ export class CdkStage extends CoreConstruct { /** * Additional options for adding a stack deployment + * + * @deprecated This class is part of the old API. Use the API based on the `CodePipeline` class instead */ export interface AddStackOptions { /** @@ -415,6 +421,8 @@ export interface AddStackOptions { /** * A single output of a Stack + * + * @deprecated This class is part of the old API. Use the API based on the `CodePipeline` class instead */ export class StackOutput { /** @@ -448,6 +456,8 @@ function isAssetManifest(s: cxapi.CloudArtifact): s is cxapi.AssetManifestArtifa /** * Features that the Stage needs from its environment + * + * @deprecated This class is part of the old API. Use the API based on the `CodePipeline` class instead */ export interface IStageHost { /** @@ -463,6 +473,8 @@ export interface IStageHost { /** * Instructions to publish certain assets + * + * @deprecated This class is part of the old API. Use the API based on the `CodePipeline` class instead */ export interface AssetPublishingCommand { /** @@ -493,6 +505,8 @@ export interface AssetPublishingCommand { /** * Base options for a pipelines stage + * + * @deprecated This class is part of the old API. Use the API based on the `CodePipeline` class instead */ export interface BaseStageOptions { /** @@ -522,6 +536,8 @@ export interface BaseStageOptions { /** * Options for adding an application stage to a pipeline + * + * @deprecated This class is part of the old API. Use the API based on the `CodePipeline` class instead */ export interface AddStageOptions extends BaseStageOptions { /** @@ -546,6 +562,8 @@ export interface AddStageOptions extends BaseStageOptions { /** * Options for addManualApproval + * + * @deprecated This class is part of the old API. Use the API based on the `CodePipeline` class instead */ export interface AddManualApprovalOptions { /** diff --git a/packages/@aws-cdk/pipelines/lib/legacy/synths/simple-synth-action.ts b/packages/@aws-cdk/pipelines/lib/legacy/synths/simple-synth-action.ts index 1fd0b96bb2db6..226a75d2ed23c 100644 --- a/packages/@aws-cdk/pipelines/lib/legacy/synths/simple-synth-action.ts +++ b/packages/@aws-cdk/pipelines/lib/legacy/synths/simple-synth-action.ts @@ -19,6 +19,8 @@ import { Construct } from '@aws-cdk/core'; /** * Configuration options for a SimpleSynth + * + * @deprecated This class is part of the old API. Use the API based on the `CodePipeline` class instead */ export interface SimpleSynthOptions { /** @@ -128,6 +130,8 @@ export interface SimpleSynthOptions { /** * Construction props for SimpleSynthAction + * + * @deprecated This class is part of the old API. Use the API based on the `CodePipeline` class instead */ export interface SimpleSynthActionProps extends SimpleSynthOptions { /** @@ -192,6 +196,8 @@ export interface SimpleSynthActionProps extends SimpleSynthOptions { /** * Specification of an additional artifact to generate + * + * @deprecated This class is part of the old API. Use the API based on the `CodePipeline` class instead */ export interface AdditionalArtifact { /** @@ -207,6 +213,8 @@ export interface AdditionalArtifact { /** * A standard synth with a generated buildspec + * + * @deprecated This class is part of the old API. Use the API based on the `CodePipeline` class instead */ export class SimpleSynthAction implements codepipeline.IAction, iam.IGrantable { @@ -479,6 +487,8 @@ export class SimpleSynthAction implements codepipeline.IAction, iam.IGrantable { /** * Options for a convention-based synth using NPM + * + * @deprecated This class is part of the old API. Use the API based on the `CodePipeline` class instead */ export interface StandardNpmSynthOptions extends SimpleSynthOptions { /** @@ -520,6 +530,8 @@ export interface StandardNpmSynthOptions extends SimpleSynthOptions { /** * Options for a convention-based synth using Yarn + * + * @deprecated This class is part of the old API. Use the API based on the `CodePipeline` class instead */ export interface StandardYarnSynthOptions extends SimpleSynthOptions { /** diff --git a/packages/@aws-cdk/pipelines/lib/legacy/validation/shell-script-action.ts b/packages/@aws-cdk/pipelines/lib/legacy/validation/shell-script-action.ts index 436f099d3c67d..78f223919cafd 100644 --- a/packages/@aws-cdk/pipelines/lib/legacy/validation/shell-script-action.ts +++ b/packages/@aws-cdk/pipelines/lib/legacy/validation/shell-script-action.ts @@ -13,6 +13,8 @@ import { Construct } from '@aws-cdk/core'; /** * Properties for ShellScriptAction + * + * @deprecated This class is part of the old API. Use the API based on the `CodePipeline` class instead */ export interface ShellScriptActionProps { /** @@ -122,6 +124,8 @@ export interface ShellScriptActionProps { /** * Validate a revision using shell commands + * + * @deprecated This class is part of the old API. Use the API based on the `CodePipeline` class instead */ export class ShellScriptAction implements codepipeline.IAction, iam.IGrantable { private _project?: codebuild.IProject; diff --git a/packages/@aws-cdk/pipelines/lib/main/pipeline-base.ts b/packages/@aws-cdk/pipelines/lib/main/pipeline-base.ts index d69c2a6c89ab1..2f90df9de6f1a 100644 --- a/packages/@aws-cdk/pipelines/lib/main/pipeline-base.ts +++ b/packages/@aws-cdk/pipelines/lib/main/pipeline-base.ts @@ -95,9 +95,11 @@ export abstract class PipelineBase extends CoreConstruct { * Example: * * ```ts + * declare const pipeline: pipelines.CodePipeline; + * * const wave = pipeline.addWave('MyWave'); - * wave.addStage(new MyStage('Stage1', ...)); - * wave.addStage(new MyStage('Stage2', ...)); + * wave.addStage(new MyApplicationStage(this, 'Stage1')); + * wave.addStage(new MyApplicationStage(this, 'Stage2')); * ``` */ public addWave(id: string, options?: WaveOptions) { diff --git a/packages/@aws-cdk/pipelines/rosetta/default.ts-fixture b/packages/@aws-cdk/pipelines/rosetta/default.ts-fixture new file mode 100644 index 0000000000000..61a973840f007 --- /dev/null +++ b/packages/@aws-cdk/pipelines/rosetta/default.ts-fixture @@ -0,0 +1,29 @@ +// Fixture with packages imported, but nothing else +import { Construct, CfnOutput, Stage, Stack, StackProps, StageProps } from '@aws-cdk/core'; +import cdk = require('@aws-cdk/core'); +import codepipeline = require('@aws-cdk/aws-codepipeline'); +import cpactions = require('@aws-cdk/aws-codepipeline-actions'); +import codebuild = require('@aws-cdk/aws-codebuild'); +import codecommit = require('@aws-cdk/aws-codecommit'); +import dynamodb = require('@aws-cdk/aws-dynamodb'); +import ecr = require('@aws-cdk/aws-ecr'); +import ec2 = require('@aws-cdk/aws-ec2'); +import iam = require('@aws-cdk/aws-iam'); +import pipelines = require('@aws-cdk/pipelines'); +import secretsmanager = require('@aws-cdk/aws-secretsmanager'); +import sns = require('@aws-cdk/aws-sns'); +import subscriptions = require('@aws-cdk/aws-sns-subscriptions'); +import s3 = require('@aws-cdk/aws-s3'); + +class MyApplicationStage extends Stage { + constructor(scope: Construct, id: string, props?: StageProps) { + super(scope, id, props); + } +} + +class Fixture extends Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + /// here + } +} diff --git a/packages/@aws-cdk/region-info/build-tools/fact-tables.ts b/packages/@aws-cdk/region-info/build-tools/fact-tables.ts index 28d61f7fd9387..0e1f923dc2a02 100644 --- a/packages/@aws-cdk/region-info/build-tools/fact-tables.ts +++ b/packages/@aws-cdk/region-info/build-tools/fact-tables.ts @@ -43,7 +43,7 @@ export const AWS_CDK_METADATA = new Set([ /** * The hosted zone Id if using an alias record in Route53. * - * @see https://docs.aws.amazon.com/general/latest/gr/rande.html#s3_website_region_endpoints + * @see https://docs.aws.amazon.com/general/latest/gr/s3.html#s3_region */ export const ROUTE_53_BUCKET_WEBSITE_ZONE_IDS: { [region: string]: string } = { 'af-south-1': 'Z11KHD8FBVPUYU', @@ -55,6 +55,7 @@ export const ROUTE_53_BUCKET_WEBSITE_ZONE_IDS: { [region: string]: string } = { 'ap-southeast-1': 'Z3O0J2DXBE1FTB', 'ap-southeast-2': 'Z1WCIGYICN2BYD', 'ca-central-1': 'Z1QDHH18159H29', + 'cn-northwest-1': 'Z282HJ1KT0DH03', 'eu-central-1': 'Z21DNDUVLTQW6Q', 'eu-north-1': 'Z3BAZG2TWCNX0D', 'eu-south-1': 'Z3IXVV8C73GIO3', @@ -135,6 +136,7 @@ export const DLC_REPOSITORY_ACCOUNTS: { [region: string]: string } = { }; // https://docs.aws.amazon.com/app-mesh/latest/userguide/envoy.html +// https://docs.amazonaws.cn/app-mesh/latest/userguide/envoy.html export const APPMESH_ECR_ACCOUNTS: { [region: string]: string } = { 'af-south-1': '924023996002', 'ap-east-1': '856666278305', @@ -145,6 +147,8 @@ export const APPMESH_ECR_ACCOUNTS: { [region: string]: string } = { 'ap-southeast-1': '840364872350', 'ap-southeast-2': '840364872350', 'ca-central-1': '840364872350', + 'cn-north-1': '919366029133', + 'cn-northwest-1': '919830735681', 'eu-central-1': '840364872350', 'eu-north-1': '840364872350', 'eu-south-1': '422531588944', diff --git a/tools/@aws-cdk/individual-pkg-gen/transform-packages.ts b/tools/@aws-cdk/individual-pkg-gen/transform-packages.ts index ece10f8262a86..a5cbcb556ea1b 100644 --- a/tools/@aws-cdk/individual-pkg-gen/transform-packages.ts +++ b/tools/@aws-cdk/individual-pkg-gen/transform-packages.ts @@ -3,6 +3,8 @@ import * as awsCdkMigration from 'aws-cdk-migration'; import * as fs from 'fs-extra'; // eslint-disable-next-line @typescript-eslint/no-require-imports const lerna_project = require('@lerna/project'); +// eslint-disable-next-line @typescript-eslint/no-require-imports +const ver = require('../../../scripts/resolve-version'); /** * @aws-cdk/ scoped packages that may be present in devDependencies and need to @@ -128,6 +130,9 @@ function transformPackageJson(pkg: any, source: string, destination: string, alp const pkgUnscopedName = `${pkg.name.substring('@aws-cdk/'.length)}`; packageJson.name += '-alpha'; + if (ver.alphaVersion) { + packageJson.version = ver.alphaVersion; + } packageJson.repository.directory = `packages/individual-packages/${pkgUnscopedName}`; // All individual packages are public by default on v1, and private by default on v2. @@ -201,7 +206,7 @@ function transformPackageJsonDependencies(packageJson: any, pkg: any, alphaPacka break; default: if (alphaPackages[dependency]) { - alphaDependencies[alphaPackages[dependency]] = pkg.version; + alphaDependencies[alphaPackages[dependency]] = packageJson.version; } else if (v1BundledDependencies.indexOf(dependency) !== -1) { // ...other than third-party dependencies, which are in bundledDependencies bundledDependencies[dependency] = packageJson.dependencies[dependency]; @@ -221,7 +226,7 @@ function transformPackageJsonDependencies(packageJson: any, pkg: any, alphaPacka break; default: if (alphaPackages[v1DevDependency]) { - alphaDevDependencies[alphaPackages[v1DevDependency]] = pkg.version; + alphaDevDependencies[alphaPackages[v1DevDependency]] = packageJson.version; } else if (!v1DevDependency.startsWith('@aws-cdk/') || isRequiredTool(v1DevDependency)) { devDependencies[v1DevDependency] = packageJson.devDependencies[v1DevDependency]; } diff --git a/tools/@aws-cdk/pkglint/lib/rules.ts b/tools/@aws-cdk/pkglint/lib/rules.ts index f991f7b0a2441..e18fcb1086fdb 100644 --- a/tools/@aws-cdk/pkglint/lib/rules.ts +++ b/tools/@aws-cdk/pkglint/lib/rules.ts @@ -1129,7 +1129,11 @@ export class MustHaveNodeEnginesDeclaration extends ValidationRule { public readonly name = 'package-info/engines'; public validate(pkg: PackageJson): void { - expectJSON(this.name, pkg, 'engines.node', '>= 10.13.0 <13 || >=13.7.0'); + if (cdkMajorVersion() === 2) { + expectJSON(this.name, pkg, 'engines.node', '>= 14.15.0'); + } else { + expectJSON(this.name, pkg, 'engines.node', '>= 10.13.0 <13 || >=13.7.0'); + } } } diff --git a/version.v1.json b/version.v1.json index 030861767e22a..c24ba0065c588 100644 --- a/version.v1.json +++ b/version.v1.json @@ -1,3 +1,3 @@ { - "version": "1.127.0" + "version": "1.128.0" } \ No newline at end of file