diff --git a/.github/workflows/yarn-upgrade.yml b/.github/workflows/yarn-upgrade.yml index 214f38b73f988..0b7ddb189622a 100644 --- a/.github/workflows/yarn-upgrade.yml +++ b/.github/workflows/yarn-upgrade.yml @@ -43,17 +43,18 @@ jobs: - name: Run "ncu -u" # We special-case @types/node because we want to stay on the current major (minimum supported node release) # We special-case @types/fs-extra because the current major (9.x) is broken with @types/node >= 10 + # We special-case parcel because we are currently on a pre-release and don't want to move to nightlies # We special-case aws-sdk because of breaking changes with TS interface exports in recent minor versions - https://github.com/aws/aws-sdk-js/issues/3453 # We special-case typescript because it's not semantically versionned run: |- # Upgrade dependencies at repository root ncu --upgrade --filter=@types/node,@types/fs-extra --target=minor ncu --upgrade --filter=typescript --target=patch - ncu --upgrade --reject=@types/node,@types/fs-extra,typescript --target=minor + ncu --upgrade --reject=@types/node,@types/fs-extra,parcel,typescript --target=minor # Upgrade all the packages lerna exec --parallel ncu -- --upgrade --filter=@types/node,@types/fs-extra --target=minor lerna exec --parallel ncu -- --upgrade --filter=typescript --target=patch - lerna exec --parallel ncu -- --upgrade --reject='@types/node,@types/fs-extra,typescript,aws-sdk,${{ steps.list-packages.outputs.list }}' --target=minor + lerna exec --parallel ncu -- --upgrade --reject='@types/node,@types/fs-extra,parcel,typescript,aws-sdk,${{ steps.list-packages.outputs.list }}' --target=minor # This will create a brand new `yarn.lock` file (this is more efficient than `yarn install && yarn upgrade`) - name: Run "yarn install --force" run: yarn install --force diff --git a/packages/@aws-cdk/aws-appsync/lib/private.ts b/packages/@aws-cdk/aws-appsync/lib/private.ts index f31ab439bccc8..9e2bd335bed1e 100644 --- a/packages/@aws-cdk/aws-appsync/lib/private.ts +++ b/packages/@aws-cdk/aws-appsync/lib/private.ts @@ -202,7 +202,7 @@ function concatAndDedup(left: T[], right: T[]): T[] { function generateInterfaces(interfaceTypes?: InterfaceType[]): string { if (!interfaceTypes || interfaceTypes.length === 0) return ''; return interfaceTypes.reduce((acc, interfaceType) => - `${acc} ${interfaceType.name},`, ' implements').slice(0, -1); + `${acc} ${interfaceType.name} &`, ' implements').slice(0, -2); } /** diff --git a/packages/@aws-cdk/aws-appsync/test/appsync-object-type.test.ts b/packages/@aws-cdk/aws-appsync/test/appsync-object-type.test.ts index 77da20a58ca4c..0a6c45d24fd85 100644 --- a/packages/@aws-cdk/aws-appsync/test/appsync-object-type.test.ts +++ b/packages/@aws-cdk/aws-appsync/test/appsync-object-type.test.ts @@ -61,7 +61,7 @@ describe('testing Object Type properties', () => { api.addType(objectTest); const gql_interface = 'interface baseTest {\n id: ID\n}\ninterface anotherTest {\n id2: ID\n}\n'; - const gql_object = 'type objectTest implements anotherTest, baseTest {\n id3: ID\n id2: ID\n id: ID\n}\n'; + const gql_object = 'type objectTest implements anotherTest & baseTest {\n id3: ID\n id2: ID\n id: ID\n}\n'; const out = `${gql_interface}${gql_object}`; // THEN diff --git a/packages/@aws-cdk/aws-batch/README.md b/packages/@aws-cdk/aws-batch/README.md index 314d647dec76e..c4a818e7abc6a 100644 --- a/packages/@aws-cdk/aws-batch/README.md +++ b/packages/@aws-cdk/aws-batch/README.md @@ -242,10 +242,27 @@ new batch.JobDefinition(stack, 'batch-job-def-from-local', { ### Importing an existing Job Definition -To import an existing batch job definition, call `JobDefinition.fromJobDefinitionArn()`. +#### From ARN + +To import an existing batch job definition from its ARN, call `JobDefinition.fromJobDefinitionArn()`. Below is an example: ```ts const job = batch.JobDefinition.fromJobDefinitionArn(this, 'imported-job-definition', 'arn:aws:batch:us-east-1:555555555555:job-definition/my-job-definition'); ``` + +#### From Name + +To import an existing batch job definition from its name, call `JobDefinition.fromJobDefinitionName()`. +If name is specified without a revision then the latest active revision is used. + +Below is an example: + +```ts +// Without revision +const job = batch.JobDefinition.fromJobDefinitionName(this, 'imported-job-definition', 'my-job-definition'); + +// With revision +const job = batch.JobDefinition.fromJobDefinitionName(this, 'imported-job-definition', 'my-job-definition:3'); +``` diff --git a/packages/@aws-cdk/aws-batch/lib/job-definition.ts b/packages/@aws-cdk/aws-batch/lib/job-definition.ts index 4de1549bc86e0..a8d2259c08a55 100644 --- a/packages/@aws-cdk/aws-batch/lib/job-definition.ts +++ b/packages/@aws-cdk/aws-batch/lib/job-definition.ts @@ -269,6 +269,31 @@ export class JobDefinition extends Resource implements IJobDefinition { return new Import(scope, id); } + /** + * Imports an existing batch job definition by its name. + * If name is specified without a revision then the latest active revision is used. + * + * @param scope + * @param id + * @param jobDefinitionName + */ + public static fromJobDefinitionName(scope: Construct, id: string, jobDefinitionName: string): IJobDefinition { + const stack = Stack.of(scope); + const jobDefArn = stack.formatArn({ + service: 'batch', + resource: 'job-definition', + sep: '/', + resourceName: jobDefinitionName, + }); + + class Import extends Resource implements IJobDefinition { + public readonly jobDefinitionArn = jobDefArn; + public readonly jobDefinitionName = jobDefinitionName; + } + + return new Import(scope, id); + } + public readonly jobDefinitionArn: string; public readonly jobDefinitionName: string; private readonly imageConfig: JobDefinitionImageConfig; diff --git a/packages/@aws-cdk/aws-batch/test/job-definition.test.ts b/packages/@aws-cdk/aws-batch/test/job-definition.test.ts index de7dc08aea71d..71129c4906c9e 100644 --- a/packages/@aws-cdk/aws-batch/test/job-definition.test.ts +++ b/packages/@aws-cdk/aws-batch/test/job-definition.test.ts @@ -200,4 +200,14 @@ describe('Batch Job Definition', () => { expect(importedJob.jobDefinitionName).toEqual('job-def-name:1'); expect(importedJob.jobDefinitionArn).toEqual('arn:aws:batch:us-east-1:123456789012:job-definition/job-def-name:1'); }); + + test('can be imported from a name', () => { + // WHEN + const importedJob = batch.JobDefinition.fromJobDefinitionName(stack, 'job-def-clone', 'job-def-name'); + + // THEN + expect(importedJob.jobDefinitionName).toEqual('job-def-name'); + expect(importedJob.jobDefinitionArn) + .toEqual('arn:${Token[AWS.Partition.3]}:batch:${Token[AWS.Region.4]}:${Token[AWS.AccountId.0]}:job-definition/job-def-name'); + }); }); diff --git a/packages/@aws-cdk/aws-cognito/README.md b/packages/@aws-cdk/aws-cognito/README.md index 4a2b87840ecbb..41eb58d9b0a22 100644 --- a/packages/@aws-cdk/aws-cognito/README.md +++ b/packages/@aws-cdk/aws-cognito/README.md @@ -566,3 +566,10 @@ const signInUrl = domain.signInUrl(client, { }) ``` +Exisiting domains can be imported into CDK apps using `UserPoolDomain.fromDomainName()` API + +```ts +const stack = new Stack(app, 'my-stack'); + +const myUserPoolDomain = UserPoolDomain.fromDomainName(stack, 'my-user-pool-domain', 'domain-name'); +``` \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito/lib/user-pool-client.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool-client.ts index b799eb2035fa1..ac67257b6a315 100644 --- a/packages/@aws-cdk/aws-cognito/lib/user-pool-client.ts +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool-client.ts @@ -30,12 +30,6 @@ export interface AuthFlow { * @default false */ readonly userSrp?: boolean; - - /** - * Enable authflow to refresh tokens - * @default false - */ - readonly refreshToken?: boolean; } /** @@ -320,7 +314,7 @@ export class UserPoolClient extends Resource implements IUserPoolClient { explicitAuthFlows: this.configureAuthFlows(props), allowedOAuthFlows: props.disableOAuth ? undefined : this.configureOAuthFlows(), allowedOAuthScopes: props.disableOAuth ? undefined : this.configureOAuthScopes(props.oAuth), - callbackUrLs: callbackUrls && callbackUrls.length > 0 ? callbackUrls : undefined, + callbackUrLs: callbackUrls && callbackUrls.length > 0 && !props.disableOAuth ? callbackUrls : undefined, logoutUrLs: props.oAuth?.logoutUrls, allowedOAuthFlowsUserPoolClient: !props.disableOAuth, preventUserExistenceErrors: this.configurePreventUserExistenceErrors(props.preventUserExistenceErrors), @@ -343,12 +337,18 @@ export class UserPoolClient extends Resource implements IUserPoolClient { } private configureAuthFlows(props: UserPoolClientProps): string[] | undefined { + if (!props.authFlows) return undefined; + const authFlows: string[] = []; - if (props.authFlows?.userPassword) { authFlows.push('ALLOW_USER_PASSWORD_AUTH'); } - if (props.authFlows?.adminUserPassword) { authFlows.push('ALLOW_ADMIN_USER_PASSWORD_AUTH'); } - if (props.authFlows?.custom) { authFlows.push('ALLOW_CUSTOM_AUTH'); } - if (props.authFlows?.userSrp) { authFlows.push('ALLOW_USER_SRP_AUTH'); } - if (props.authFlows?.refreshToken) { authFlows.push('ALLOW_REFRESH_TOKEN_AUTH'); } + if (props.authFlows.userPassword) { authFlows.push('ALLOW_USER_PASSWORD_AUTH'); } + if (props.authFlows.adminUserPassword) { authFlows.push('ALLOW_ADMIN_USER_PASSWORD_AUTH'); } + if (props.authFlows.custom) { authFlows.push('ALLOW_CUSTOM_AUTH'); } + if (props.authFlows.userSrp) { authFlows.push('ALLOW_USER_SRP_AUTH'); } + + // refreshToken should always be allowed if authFlows are present + if (authFlows.length > 0) { + authFlows.push('ALLOW_REFRESH_TOKEN_AUTH'); + } if (authFlows.length === 0) { return undefined; diff --git a/packages/@aws-cdk/aws-cognito/lib/user-pool-domain.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool-domain.ts index 548ba52079fd9..a50f80fbf2804 100644 --- a/packages/@aws-cdk/aws-cognito/lib/user-pool-domain.ts +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool-domain.ts @@ -80,6 +80,17 @@ export interface UserPoolDomainProps extends UserPoolDomainOptions { * Define a user pool domain */ export class UserPoolDomain extends Resource implements IUserPoolDomain { + /** + * Import a UserPoolDomain given its domain name + */ + public static fromDomainName(scope: Construct, id: string, userPoolDomainName: string): IUserPoolDomain { + class Import extends Resource implements IUserPoolDomain { + public readonly domainName = userPoolDomainName; + } + + return new Import(scope, id); + } + public readonly domainName: string; private isCognitoDomain: boolean; diff --git a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-client-explicit-props.ts b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-client-explicit-props.ts index 06fbe5b373c81..dbad2591fc1bc 100644 --- a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-client-explicit-props.ts +++ b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-client-explicit-props.ts @@ -11,7 +11,6 @@ userpool.addClient('myuserpoolclient', { authFlows: { adminUserPassword: true, custom: true, - refreshToken: true, userPassword: true, userSrp: true, }, diff --git a/packages/@aws-cdk/aws-cognito/test/user-pool-client.test.ts b/packages/@aws-cdk/aws-cognito/test/user-pool-client.test.ts index 8266b73e61fbb..081d9d9e6526b 100644 --- a/packages/@aws-cdk/aws-cognito/test/user-pool-client.test.ts +++ b/packages/@aws-cdk/aws-cognito/test/user-pool-client.test.ts @@ -78,7 +78,6 @@ describe('User Pool Client', () => { authFlows: { adminUserPassword: true, custom: true, - refreshToken: true, userPassword: true, userSrp: true, }, @@ -95,6 +94,26 @@ describe('User Pool Client', () => { }); }); + test('ExplicitAuthFlows makes refreshToken true by default', () => { + // GIVEN + const stack = new Stack(); + const pool = new UserPool(stack, 'Pool'); + + // WHEN + pool.addClient('Client', { + authFlows: { + userSrp: true, + }, + }); + + expect(stack).toHaveResourceLike('AWS::Cognito::UserPoolClient', { + ExplicitAuthFlows: [ + 'ALLOW_USER_SRP_AUTH', + 'ALLOW_REFRESH_TOKEN_AUTH', + ], + }); + }); + test('AllowedOAuthFlows are correctly named', () => { // GIVEN const stack = new Stack(); @@ -175,6 +194,23 @@ describe('User Pool Client', () => { }); }); + test('callbackUrls are not rendered if OAuth is disabled ', () => { + // GIVEN + const stack = new Stack(); + const pool = new UserPool(stack, 'Pool'); + + // WHEN + new UserPoolClient(stack, 'PoolClient', { + userPool: pool, + disableOAuth: true, + }); + + // THEN + expect(stack).toHaveResourceLike('AWS::Cognito::UserPoolClient', { + CallbackURLs: ABSENT, + }); + }); + test('fails when callbackUrls is empty for codeGrant or implicitGrant', () => { const stack = new Stack(); const pool = new UserPool(stack, 'Pool'); diff --git a/packages/@aws-cdk/aws-cognito/test/user-pool-domain.test.ts b/packages/@aws-cdk/aws-cognito/test/user-pool-domain.test.ts index c7ec0d98ea75a..b981707a90ce6 100644 --- a/packages/@aws-cdk/aws-cognito/test/user-pool-domain.test.ts +++ b/packages/@aws-cdk/aws-cognito/test/user-pool-domain.test.ts @@ -152,6 +152,18 @@ describe('User Pool Client', () => { expect(cfDomainNameSecond).toEqual(cfDomainNameFirst); }); + test('import', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + const client = UserPoolDomain.fromDomainName(stack, 'Domain', 'domain-name-1'); + + // THEN + expect(client.domainName).toEqual('domain-name-1'); + expect(stack).not.toHaveResource('AWS::Cognito::UserPoolDomain'); + }); + describe('signInUrl', () => { test('returns the expected URL', () => { // GIVEN diff --git a/packages/@aws-cdk/aws-ec2/README.md b/packages/@aws-cdk/aws-ec2/README.md index 27dcabd8b69cd..2704685007a0e 100644 --- a/packages/@aws-cdk/aws-ec2/README.md +++ b/packages/@aws-cdk/aws-ec2/README.md @@ -281,8 +281,8 @@ const igwId = vpc.internetGatewayId; For a VPC with only `ISOLATED` subnets, this value will be undefined. -This is only supported for VPC's created in the stack - currently you're -unable to get the ID for imported VPC's. To do that you'd have to specifically +This is only supported for VPCs created in the stack - currently you're +unable to get the ID for imported VPCs. To do that you'd have to specifically look up the Internet Gateway by name, which would require knowing the name beforehand. @@ -700,7 +700,7 @@ ec2.CloudFormationInit.fromElements( ### Bastion Hosts A bastion host functions as an instance used to access servers and resources in a VPC without open up the complete VPC on a network level. -You can use bastion hosts using a standard SSH connection targetting port 22 on the host. As an alternative, you can connect the SSH connection +You can use bastion hosts using a standard SSH connection targeting port 22 on the host. As an alternative, you can connect the SSH connection feature of AWS Systems Manager Session Manager, which does not need an opened security group. (https://aws.amazon.com/about-aws/whats-new/2019/07/session-manager-launches-tunneling-support-for-ssh-and-scp/) A default bastion host for use via SSM can be configured like: diff --git a/packages/@aws-cdk/aws-ec2/lib/instance-types.ts b/packages/@aws-cdk/aws-ec2/lib/instance-types.ts index 28b8c9ee46554..7ad1d2420aa26 100644 --- a/packages/@aws-cdk/aws-ec2/lib/instance-types.ts +++ b/packages/@aws-cdk/aws-ec2/lib/instance-types.ts @@ -198,6 +198,28 @@ export enum InstanceClass { */ C5N = 'c5n', + /** + * Compute optimized instances for high performance computing, 6th generation with Graviton2 processors + */ + COMPUTE6_GRAVITON2 = 'c6g', + + /** + * Compute optimized instances for high performance computing, 6th generation with Graviton2 processors + */ + C6G = 'c6g', + + /** + * Compute optimized instances for high performance computing, 6th generation with Graviton2 processors + * and local NVME drive + */ + COMPUTE6_GRAVITON2_NVME_DRIVE = 'c6gd', + + /** + * Compute optimized instances for high performance computing, 6th generation with Graviton2 processors + * and local NVME drive + */ + C6GD = 'c6gd', + /** * Storage-optimized instances, 2nd generation */ diff --git a/packages/@aws-cdk/aws-ec2/test/instance.test.ts b/packages/@aws-cdk/aws-ec2/test/instance.test.ts index 18e84a910c4f4..f1a0da3ba62f9 100644 --- a/packages/@aws-cdk/aws-ec2/test/instance.test.ts +++ b/packages/@aws-cdk/aws-ec2/test/instance.test.ts @@ -19,12 +19,12 @@ nodeunitShim({ new Instance(stack, 'Instance', { vpc, machineImage: new AmazonLinuxImage(), - instanceType: InstanceType.of(InstanceClass.T3, InstanceSize.LARGE), + instanceType: InstanceType.of(InstanceClass.COMPUTE6_GRAVITON2, InstanceSize.LARGE), }); // THEN cdkExpect(stack).to(haveResource('AWS::EC2::Instance', { - InstanceType: 't3.large', + InstanceType: 'c6g.large', })); test.done(); diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2-actions/test/integ.cognito.lit.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2-actions/test/integ.cognito.lit.ts index f9ab2c015b382..c68562ee6ffcb 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2-actions/test/integ.cognito.lit.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2-actions/test/integ.cognito.lit.ts @@ -32,7 +32,6 @@ class CognitoStack extends Stack { generateSecret: true, authFlows: { userPassword: true, - refreshToken: true, }, oAuth: { flows: { diff --git a/packages/@aws-cdk/aws-lambda-event-sources/README.md b/packages/@aws-cdk/aws-lambda-event-sources/README.md index 5d1287d0ace7f..452fba4bed43a 100644 --- a/packages/@aws-cdk/aws-lambda-event-sources/README.md +++ b/packages/@aws-cdk/aws-lambda-event-sources/README.md @@ -113,8 +113,12 @@ import * as sns from '@aws-cdk/aws-sns'; import { SnsEventSource } from '@aws-cdk/aws-lambda-event-sources'; const topic = new sns.Topic(...); +const deadLetterQueue = new sqs.Queue(this, 'deadLetterQueue'); -lambda.addEventSource(new SnsEventSource(topic)); +lambda.addEventSource(new SnsEventSource(topic, { + filterPolicy: { ... }, + deadLetterQueue: deadLetterQueue +})); ``` When a user calls the SNS Publish API on a topic that your Lambda function is diff --git a/packages/@aws-cdk/aws-lambda-event-sources/lib/sns.ts b/packages/@aws-cdk/aws-lambda-event-sources/lib/sns.ts index cefd7fc20c286..e2f03ca2da4da 100644 --- a/packages/@aws-cdk/aws-lambda-event-sources/lib/sns.ts +++ b/packages/@aws-cdk/aws-lambda-event-sources/lib/sns.ts @@ -2,14 +2,23 @@ import * as lambda from '@aws-cdk/aws-lambda'; import * as sns from '@aws-cdk/aws-sns'; import * as subs from '@aws-cdk/aws-sns-subscriptions'; +/** + * Properties forwarded to the Lambda Subscription. + */ +export interface SnsEventSourceProps extends subs.LambdaSubscriptionProps { +} + /** * Use an Amazon SNS topic as an event source for AWS Lambda. */ export class SnsEventSource implements lambda.IEventSource { - constructor(readonly topic: sns.ITopic) { + private readonly props?: SnsEventSourceProps; + + constructor(readonly topic: sns.ITopic, props?: SnsEventSourceProps) { + this.props = props; } public bind(target: lambda.IFunction) { - this.topic.addSubscription(new subs.LambdaSubscription(target)); + this.topic.addSubscription(new subs.LambdaSubscription(target, this.props)); } } diff --git a/packages/@aws-cdk/aws-lambda-event-sources/test/test.sns.ts b/packages/@aws-cdk/aws-lambda-event-sources/test/test.sns.ts index 12224e95b6040..2c82ea19c42e0 100644 --- a/packages/@aws-cdk/aws-lambda-event-sources/test/test.sns.ts +++ b/packages/@aws-cdk/aws-lambda-event-sources/test/test.sns.ts @@ -1,5 +1,6 @@ import { expect, haveResource } from '@aws-cdk/assert'; import * as sns from '@aws-cdk/aws-sns'; +import * as sqs from '@aws-cdk/aws-sqs'; import * as cdk from '@aws-cdk/core'; import { Test } from 'nodeunit'; import * as sources from '../lib'; @@ -47,4 +48,67 @@ export = { test.done(); }, + + 'props are passed to subscription'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const fn = new TestFunction(stack, 'Fn'); + const topic = new sns.Topic(stack, 'T'); + const queue = new sqs.Queue(stack, 'Q'); + const props: sources.SnsEventSourceProps = { + deadLetterQueue: queue, + filterPolicy: { + Field: sns.SubscriptionFilter.stringFilter({ + whitelist: ['A', 'B'], + }), + }, + }; + + // WHEN + fn.addEventSource(new sources.SnsEventSource(topic, props)); + + // THEN + expect(stack).to(haveResource('AWS::Lambda::Permission', { + 'Action': 'lambda:InvokeFunction', + 'FunctionName': { + 'Fn::GetAtt': [ + 'Fn9270CBC0', + 'Arn', + ], + }, + 'Principal': 'sns.amazonaws.com', + 'SourceArn': { + 'Ref': 'TD925BC7E', + }, + })); + + expect(stack).to(haveResource('AWS::SNS::Subscription', { + 'Endpoint': { + 'Fn::GetAtt': [ + 'Fn9270CBC0', + 'Arn', + ], + }, + 'Protocol': 'lambda', + 'TopicArn': { + 'Ref': 'TD925BC7E', + }, + 'FilterPolicy': { + 'Field': [ + 'A', + 'B', + ], + }, + 'RedrivePolicy': { + 'deadLetterTargetArn': { + 'Fn::GetAtt': [ + 'Q63C6E3AB', + 'Arn', + ], + }, + }, + })); + + test.done(); + }, }; diff --git a/packages/@aws-cdk/aws-lambda/lib/event-source-mapping.ts b/packages/@aws-cdk/aws-lambda/lib/event-source-mapping.ts index 3fdefc2b16e42..be59fdd54f0a1 100644 --- a/packages/@aws-cdk/aws-lambda/lib/event-source-mapping.ts +++ b/packages/@aws-cdk/aws-lambda/lib/event-source-mapping.ts @@ -74,12 +74,14 @@ export interface EventSourceMappingOptions { /** * The maximum number of times to retry when the function returns an error. + * Set to `undefined` if you want lambda to keep retrying infinitely or until + * the record expires. * * Valid Range: * * Minimum value of 0 * * Maximum value of 10000 * - * @default 10000 + * @default - infinite or until the record expires. */ readonly retryAttempts?: number; diff --git a/packages/@aws-cdk/aws-rds/lib/cluster-engine.ts b/packages/@aws-cdk/aws-rds/lib/cluster-engine.ts index 9fb5bf08ccf3a..032c76101798a 100644 --- a/packages/@aws-cdk/aws-rds/lib/cluster-engine.ts +++ b/packages/@aws-cdk/aws-rds/lib/cluster-engine.ts @@ -495,6 +495,7 @@ class AuroraPostgresClusterEngine extends ClusterEngineBase { private static readonly S3_EXPORT_FEATURE_NAME = 's3Export'; public readonly engineFamily = 'POSTGRESQL'; + public readonly defaultUsername = 'postgres'; public readonly supportedLogTypes: string[] = ['postgresql']; constructor(version?: AuroraPostgresEngineVersion) { @@ -553,7 +554,8 @@ export class DatabaseClusterEngine { /** * The unversioned 'aurora' cluster engine. * - * @deprecated using unversioned engines is an availability risk. + * **Note**: we do not recommend using unversioned engines for non-serverless Clusters, + * as that can pose an availability risk. * We recommend using versioned engines created using the {@link aurora()} method */ public static readonly AURORA: IClusterEngine = new AuroraClusterEngine(); @@ -561,7 +563,8 @@ export class DatabaseClusterEngine { /** * The unversioned 'aurora-msql' cluster engine. * - * @deprecated using unversioned engines is an availability risk. + * **Note**: we do not recommend using unversioned engines for non-serverless Clusters, + * as that can pose an availability risk. * We recommend using versioned engines created using the {@link auroraMysql()} method */ public static readonly AURORA_MYSQL: IClusterEngine = new AuroraMysqlClusterEngine(); @@ -569,7 +572,8 @@ export class DatabaseClusterEngine { /** * The unversioned 'aurora-postgresql' cluster engine. * - * @deprecated using unversioned engines is an availability risk. + * **Note**: we do not recommend using unversioned engines for non-serverless Clusters, + * as that can pose an availability risk. * We recommend using versioned engines created using the {@link auroraPostgres()} method */ public static readonly AURORA_POSTGRESQL: IClusterEngine = new AuroraPostgresClusterEngine(); diff --git a/packages/@aws-cdk/aws-rds/lib/cluster.ts b/packages/@aws-cdk/aws-rds/lib/cluster.ts index dda50403436f2..6ebb70fbe8400 100644 --- a/packages/@aws-cdk/aws-rds/lib/cluster.ts +++ b/packages/@aws-cdk/aws-rds/lib/cluster.ts @@ -430,7 +430,7 @@ export interface DatabaseClusterProps extends DatabaseClusterBaseProps { /** * Credentials for the administrative user * - * @default - A username of 'admin' and SecretsManager-generated password + * @default - A username of 'admin' (or 'postgres' for PostgreSQL) and SecretsManager-generated password */ readonly credentials?: Credentials; @@ -488,7 +488,7 @@ export class DatabaseCluster extends DatabaseClusterNew { this.singleUserRotationApplication = props.engine.singleUserRotationApplication; this.multiUserRotationApplication = props.engine.multiUserRotationApplication; - let credentials = props.credentials ?? Credentials.fromUsername('admin'); + let credentials = props.credentials ?? Credentials.fromUsername(props.engine.defaultUsername ?? 'admin'); if (!credentials.secret && !credentials.password) { credentials = Credentials.fromSecret(new DatabaseSecret(this, 'Secret', { username: credentials.username, diff --git a/packages/@aws-cdk/aws-rds/lib/engine.ts b/packages/@aws-cdk/aws-rds/lib/engine.ts index 2feced9927ca2..82a519cc76ff9 100644 --- a/packages/@aws-cdk/aws-rds/lib/engine.ts +++ b/packages/@aws-cdk/aws-rds/lib/engine.ts @@ -39,4 +39,11 @@ export interface IEngine { * @default - the engine doesn't belong to any family */ readonly engineFamily?: string; + + /** + * The default name of the master database user if one was not provided explicitly. + * The global default of 'admin' will be used if this is `undefined`. + * Note that 'admin' is a reserved word in PostgreSQL and cannot be used. + */ + readonly defaultUsername?: string; } diff --git a/packages/@aws-cdk/aws-rds/lib/instance-engine.ts b/packages/@aws-cdk/aws-rds/lib/instance-engine.ts index 25370706664c7..570e7b81a0479 100644 --- a/packages/@aws-cdk/aws-rds/lib/instance-engine.ts +++ b/packages/@aws-cdk/aws-rds/lib/instance-engine.ts @@ -578,6 +578,8 @@ export interface PostgresInstanceEngineProps { * The instance engine for PostgreSQL. */ class PostgresInstanceEngine extends InstanceEngineBase { + public readonly defaultUsername = 'postgres'; + constructor(version?: PostgresEngineVersion) { super({ engineType: 'postgres', diff --git a/packages/@aws-cdk/aws-rds/lib/instance.ts b/packages/@aws-cdk/aws-rds/lib/instance.ts index caf0b7ae45d35..81aa4654a5606 100644 --- a/packages/@aws-cdk/aws-rds/lib/instance.ts +++ b/packages/@aws-cdk/aws-rds/lib/instance.ts @@ -902,7 +902,7 @@ export interface DatabaseInstanceProps extends DatabaseInstanceSourceProps { /** * Credentials for the administrative user * - * @default - A username of 'admin' and SecretsManager-generated password + * @default - A username of 'admin' (or 'postgres' for PostgreSQL) and SecretsManager-generated password */ readonly credentials?: Credentials; @@ -944,7 +944,7 @@ export class DatabaseInstance extends DatabaseInstanceSource implements IDatabas constructor(scope: Construct, id: string, props: DatabaseInstanceProps) { super(scope, id, props); - let credentials = props.credentials ?? Credentials.fromUsername('admin'); + let credentials = props.credentials ?? Credentials.fromUsername(props.engine.defaultUsername ?? 'admin'); if (!credentials.secret && !credentials.password) { credentials = Credentials.fromSecret(new DatabaseSecret(this, 'Secret', { username: credentials.username, diff --git a/packages/@aws-cdk/aws-rds/test/test.cluster.ts b/packages/@aws-cdk/aws-rds/test/test.cluster.ts index 151137bd86d4b..72c2e010b2092 100644 --- a/packages/@aws-cdk/aws-rds/test/test.cluster.ts +++ b/packages/@aws-cdk/aws-rds/test/test.cluster.ts @@ -1394,6 +1394,29 @@ export = { test.done(); }, + "Aurora PostgreSQL cluster uses a different default master username than 'admin', which is a reserved word"(test: Test) { + // GIVEN + const stack = testStack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + + // WHEN + new DatabaseCluster(stack, 'Database', { + engine: DatabaseClusterEngine.auroraPostgres({ + version: AuroraPostgresEngineVersion.VER_9_6_12, + }), + instanceProps: { vpc }, + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::SecretsManager::Secret', { + GenerateSecretString: { + SecretStringTemplate: '{"username":"postgres"}', + }, + })); + + test.done(); + }, + 'MySQL cluster without S3 exports or imports references the correct default ParameterGroup'(test: Test) { // GIVEN const stack = testStack(); diff --git a/packages/@aws-cdk/aws-rds/test/test.instance.ts b/packages/@aws-cdk/aws-rds/test/test.instance.ts index f913c6b858cf0..2af2afaf35d31 100644 --- a/packages/@aws-cdk/aws-rds/test/test.instance.ts +++ b/packages/@aws-cdk/aws-rds/test/test.instance.ts @@ -997,6 +997,25 @@ export = { test.done(); }, + + "PostgreSQL database instance uses a different default master username than 'admin', which is a reserved word"(test: Test) { + new rds.DatabaseInstance(stack, 'Instance', { + vpc, + engine: rds.DatabaseInstanceEngine.postgres({ + version: rds.PostgresEngineVersion.VER_9_5_7, + }), + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::SecretsManager::Secret', { + GenerateSecretString: { + SecretStringTemplate: '{"username":"postgres"}', + }, + })); + + test.done(); + }, + 'S3 Import/Export': { 'instance with s3 import and export buckets'(test: Test) { new rds.DatabaseInstance(stack, 'DB', { diff --git a/packages/@aws-cdk/aws-secretsmanager/lib/secret.ts b/packages/@aws-cdk/aws-secretsmanager/lib/secret.ts index ad8e7d0fcf0ef..4edf692963158 100644 --- a/packages/@aws-cdk/aws-secretsmanager/lib/secret.ts +++ b/packages/@aws-cdk/aws-secretsmanager/lib/secret.ts @@ -327,7 +327,7 @@ export class Secret extends SecretBase { // @see https://docs.aws.amazon.com/kms/latest/developerguide/services-secrets-manager.html#asm-authz const principal = - new kms.ViaServicePrincipal(`secretsmanager.${Stack.of(this).region}.amazonaws.com`, new iam.AccountPrincipal(Stack.of(this).account)); + new kms.ViaServicePrincipal(`secretsmanager.${Stack.of(this).region}.amazonaws.com`, new iam.AccountPrincipal(Stack.of(this).account)); this.encryptionKey?.grantEncryptDecrypt(principal); this.encryptionKey?.grant(principal, 'kms:CreateGrant', 'kms:DescribeKey'); } @@ -605,8 +605,8 @@ function parseSecretName(construct: IConstruct, secretArn: string) { } // Secret resource names are in the format `${secretName}-${SecretsManager suffix}` - const secretNameFromArn = resourceName.substr(0, resourceName.lastIndexOf('-')); - if (secretNameFromArn) { return secretNameFromArn; } + // If there is no hyphen, assume no suffix was provided, and return the whole name. + return resourceName.substr(0, resourceName.lastIndexOf('-')) || resourceName; } throw new Error('invalid ARN format; no secret name provided'); } diff --git a/packages/@aws-cdk/aws-secretsmanager/test/test.secret.ts b/packages/@aws-cdk/aws-secretsmanager/test/test.secret.ts index 523ce501b9126..a7b4482597bca 100644 --- a/packages/@aws-cdk/aws-secretsmanager/test/test.secret.ts +++ b/packages/@aws-cdk/aws-secretsmanager/test/test.secret.ts @@ -474,11 +474,25 @@ export = { // GIVEN const stack = new cdk.Stack(); const arnWithoutResourceName = 'arn:aws:secretsmanager:eu-west-1:111111111111:secret'; - const arnWithoutSecretsManagerSuffix = 'arn:aws:secretsmanager:eu-west-1:111111111111:secret:MySecret'; // WHEN test.throws(() => secretsmanager.Secret.fromSecretArn(stack, 'Secret1', arnWithoutResourceName), /invalid ARN format/); - test.throws(() => secretsmanager.Secret.fromSecretArn(stack, 'Secret2', arnWithoutSecretsManagerSuffix), /invalid ARN format/); + + test.done(); + }, + + 'import by secretArn supports secret ARNs without suffixes'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const arnWithoutSecretsManagerSuffix = 'arn:aws:secretsmanager:eu-west-1:111111111111:secret:MySecret'; + + // WHEN + const secret = secretsmanager.Secret.fromSecretArn(stack, 'Secret', arnWithoutSecretsManagerSuffix); + + // THEN + test.equals(secret.secretArn, arnWithoutSecretsManagerSuffix); + test.equals(secret.secretName, 'MySecret'); + test.done(); }, diff --git a/packages/@aws-cdk/core/lib/cfn-resource.ts b/packages/@aws-cdk/core/lib/cfn-resource.ts index f5049bdcd2326..4cb8934617c2b 100644 --- a/packages/@aws-cdk/core/lib/cfn-resource.ts +++ b/packages/@aws-cdk/core/lib/cfn-resource.ts @@ -306,7 +306,10 @@ export class CfnResource extends CfnRefElement { Condition: this.cfnOptions.condition && this.cfnOptions.condition.logicalId, }, props => { const renderedProps = this.renderProperties(props.Properties || {}); - props.Properties = renderedProps && (Object.values(renderedProps).find(v => !!v) ? renderedProps : undefined); + if (renderedProps) { + const hasDefined = Object.values(renderedProps).find(v => v !== undefined); + props.Properties = hasDefined !== undefined ? renderedProps : undefined; + } return deepMerge(props, this.rawOverrides); }), }, diff --git a/packages/@aws-cdk/core/test/test.cfn-resource.ts b/packages/@aws-cdk/core/test/test.cfn-resource.ts index 19bd494b485d5..03b615c464171 100644 --- a/packages/@aws-cdk/core/test/test.cfn-resource.ts +++ b/packages/@aws-cdk/core/test/test.cfn-resource.ts @@ -25,6 +25,30 @@ export = nodeunit.testCase({ test.done(); }, + + 'renders "Properties" for a resource that has only properties set to "false"'(test: nodeunit.Test) { + const app = new core.App(); + const stack = new core.Stack(app, 'TestStack'); + new core.CfnResource(stack, 'Resource', { + type: 'Test::Resource::Fake', + properties: { + FakeProperty: false, + }, + }); + + test.deepEqual(app.synth().getStackByName(stack.stackName).template, { + Resources: { + Resource: { + Type: 'Test::Resource::Fake', + Properties: { + FakeProperty: false, + }, + }, + }, + }); + + test.done(); + }, }, 'applyRemovalPolicy default includes Update policy'(test: nodeunit.Test) { diff --git a/packages/aws-cdk/lib/api/cxapp/cloud-assembly.ts b/packages/aws-cdk/lib/api/cxapp/cloud-assembly.ts index ca8bf10d96375..2ce9fef9b29ce 100644 --- a/packages/aws-cdk/lib/api/cxapp/cloud-assembly.ts +++ b/packages/aws-cdk/lib/api/cxapp/cloud-assembly.ts @@ -69,6 +69,7 @@ export class CloudAssembly { public async selectStacks(selectors: string[], options: SelectStacksOptions): Promise { selectors = selectors.filter(s => s != null); // filter null/undefined + selectors = [...new Set(selectors)]; // make them unique const stacks = this.assembly.stacks; if (stacks.length === 0) { diff --git a/packages/aws-cdk/test/api/cloud-assembly.test.ts b/packages/aws-cdk/test/api/cloud-assembly.test.ts index 3e192e3514c23..e0882882cc55e 100644 --- a/packages/aws-cdk/test/api/cloud-assembly.test.ts +++ b/packages/aws-cdk/test/api/cloud-assembly.test.ts @@ -60,6 +60,19 @@ test('select behavior: single', async () => { .rejects.toThrow('Since this app includes more than a single stack, specify which stacks to use (wildcards are supported)'); }); +test('select behavior: repeat', async () => { + // GIVEN + const cxasm = await testCloudAssembly(); + + // WHEN + const x = await cxasm.selectStacks(['withouterrors', 'withouterrors'], { + defaultBehavior: DefaultSelection.AllStacks, + }); + + // THEN + expect(x.stackCount).toBe(1); +}); + async function testCloudAssembly({ env }: { env?: string, versionReporting?: boolean } = {}) { const cloudExec = new MockCloudExecutable({ stacks: [{