From 9b2158bf9228a876d8f434dd5e025dbb74dbe4d5 Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Fri, 19 Nov 2021 21:00:11 +0100 Subject: [PATCH 1/2] feat(rds): validate backup retention for read replica instances (#17569) Automatic backups of read replica instances are only supported for MySQL and MariaDB. See https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_ReadRepl.html#USER_ReadRepl.Overview.Differences Closes #17356 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-rds/README.md | 4 + .../@aws-cdk/aws-rds/lib/instance-engine.ts | 12 + packages/@aws-cdk/aws-rds/lib/instance.ts | 8 +- .../@aws-cdk/aws-rds/test/instance.test.ts | 20 + .../test/integ.read-replica.expected.json | 502 ++++++++++++++++++ .../aws-rds/test/integ.read-replica.ts | 59 ++ 6 files changed, 604 insertions(+), 1 deletion(-) create mode 100644 packages/@aws-cdk/aws-rds/test/integ.read-replica.expected.json create mode 100644 packages/@aws-cdk/aws-rds/test/integ.read-replica.ts diff --git a/packages/@aws-cdk/aws-rds/README.md b/packages/@aws-cdk/aws-rds/README.md index 9f350d3dd1f5b..acc7f61b6022b 100644 --- a/packages/@aws-cdk/aws-rds/README.md +++ b/packages/@aws-cdk/aws-rds/README.md @@ -129,6 +129,10 @@ new rds.DatabaseInstanceReadReplica(this, 'ReadReplica', { }); ``` +Automatic backups of read replica instances are only supported for MySQL and MariaDB. By default, +automatic backups are disabled for read replicas and can only be enabled (using `backupRetention`) +if also enabled on the source instance. + Creating a "production" Oracle database instance with option and parameter groups: [example of setting up a production oracle instance](test/integ.instance.lit.ts) diff --git a/packages/@aws-cdk/aws-rds/lib/instance-engine.ts b/packages/@aws-cdk/aws-rds/lib/instance-engine.ts index 05d045e56c8f3..81deab4cabbf4 100644 --- a/packages/@aws-cdk/aws-rds/lib/instance-engine.ts +++ b/packages/@aws-cdk/aws-rds/lib/instance-engine.ts @@ -100,6 +100,13 @@ export interface IInstanceEngine extends IEngine { /** The application used by this engine to perform rotation for a multi-user scenario. */ readonly multiUserRotationApplication: secretsmanager.SecretRotationApplication; + /** + * Whether this engine supports automatic backups of a read replica instance. + * + * @default false + */ + readonly supportsReadReplicaBackups?: boolean; + /** * Method called when the engine is used to create a new instance. */ @@ -123,6 +130,7 @@ abstract class InstanceEngineBase implements IInstanceEngine { public readonly singleUserRotationApplication: secretsmanager.SecretRotationApplication; public readonly multiUserRotationApplication: secretsmanager.SecretRotationApplication; public readonly engineFamily?: string; + public readonly supportsReadReplicaBackups?: boolean; private readonly features?: InstanceEngineFeatures; @@ -320,6 +328,8 @@ export interface MariaDbInstanceEngineProps { } class MariaDbInstanceEngine extends InstanceEngineBase { + public readonly supportsReadReplicaBackups = true; + constructor(version?: MariaDbEngineVersion) { super({ engineType: 'mariadb', @@ -533,6 +543,8 @@ export interface MySqlInstanceEngineProps { } class MySqlInstanceEngine extends InstanceEngineBase { + public readonly supportsReadReplicaBackups = true; + constructor(version?: MysqlEngineVersion) { super({ engineType: 'mysql', diff --git a/packages/@aws-cdk/aws-rds/lib/instance.ts b/packages/@aws-cdk/aws-rds/lib/instance.ts index f592e5f4528be..d893720fed46e 100644 --- a/packages/@aws-cdk/aws-rds/lib/instance.ts +++ b/packages/@aws-cdk/aws-rds/lib/instance.ts @@ -380,7 +380,7 @@ export interface DatabaseInstanceNewProps { * When creating a read replica, you must enable automatic backups on the source * database instance by setting the backup retention to a value other than zero. * - * @default Duration.days(1) + * @default - Duration.days(1) for source instances, disabled for read replicas */ readonly backupRetention?: Duration; @@ -1143,6 +1143,12 @@ export class DatabaseInstanceReadReplica extends DatabaseInstanceNew implements constructor(scope: Construct, id: string, props: DatabaseInstanceReadReplicaProps) { super(scope, id, props); + if (props.sourceDatabaseInstance.engine + && !props.sourceDatabaseInstance.engine.supportsReadReplicaBackups + && props.backupRetention) { + throw new Error(`Cannot set 'backupRetention', as engine '${engineDescription(props.sourceDatabaseInstance.engine)}' does not support automatic backups for read replicas`); + } + const instance = new CfnDBInstance(this, 'Resource', { ...this.newCfnProps, // this must be ARN, not ID, because of https://github.com/terraform-providers/terraform-provider-aws/issues/528#issuecomment-391169012 diff --git a/packages/@aws-cdk/aws-rds/test/instance.test.ts b/packages/@aws-cdk/aws-rds/test/instance.test.ts index 65f8ae8112d46..e159a8971b204 100644 --- a/packages/@aws-cdk/aws-rds/test/instance.test.ts +++ b/packages/@aws-cdk/aws-rds/test/instance.test.ts @@ -1583,6 +1583,26 @@ describe('instance', () => { }); }); + test('throws with backupRetention on a read replica if engine does not support it', () => { + // GIVEN + const instanceType = ec2.InstanceType.of(ec2.InstanceClass.T3, ec2.InstanceSize.SMALL); + const backupRetention = cdk.Duration.days(5); + const source = new rds.DatabaseInstance(stack, 'Source', { + engine: rds.DatabaseInstanceEngine.postgres({ version: rds.PostgresEngineVersion.VER_13 }), + backupRetention, + instanceType, + vpc, + }); + + expect(() => { + new rds.DatabaseInstanceReadReplica(stack, 'Replica', { + sourceDatabaseInstance: source, + backupRetention, + instanceType, + vpc, + }); + }).toThrow(/Cannot set 'backupRetention', as engine 'postgres-13' does not support automatic backups for read replicas/); + }); }); test.each([ diff --git a/packages/@aws-cdk/aws-rds/test/integ.read-replica.expected.json b/packages/@aws-cdk/aws-rds/test/integ.read-replica.expected.json new file mode 100644 index 0000000000000..d27f653c3e6ac --- /dev/null +++ b/packages/@aws-cdk/aws-rds/test/integ.read-replica.expected.json @@ -0,0 +1,502 @@ +{ + "Resources": { + "Vpc8378EB38": { + "Type": "AWS::EC2::VPC", + "Properties": { + "CidrBlock": "10.0.0.0/16", + "EnableDnsHostnames": true, + "EnableDnsSupport": true, + "InstanceTenancy": "default", + "Tags": [ + { + "Key": "Name", + "Value": "cdk-rds-read-replica/Vpc" + } + ] + } + }, + "VpcisolatedSubnet1SubnetE62B1B9B": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.0.0/17", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1a", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "isolated" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Isolated" + }, + { + "Key": "Name", + "Value": "cdk-rds-read-replica/Vpc/isolatedSubnet1" + } + ] + } + }, + "VpcisolatedSubnet1RouteTableE442650B": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "cdk-rds-read-replica/Vpc/isolatedSubnet1" + } + ] + } + }, + "VpcisolatedSubnet1RouteTableAssociationD259E31A": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcisolatedSubnet1RouteTableE442650B" + }, + "SubnetId": { + "Ref": "VpcisolatedSubnet1SubnetE62B1B9B" + } + } + }, + "VpcisolatedSubnet2Subnet39217055": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.128.0/17", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1b", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "isolated" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Isolated" + }, + { + "Key": "Name", + "Value": "cdk-rds-read-replica/Vpc/isolatedSubnet2" + } + ] + } + }, + "VpcisolatedSubnet2RouteTable334F9764": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "cdk-rds-read-replica/Vpc/isolatedSubnet2" + } + ] + } + }, + "VpcisolatedSubnet2RouteTableAssociation25A4716F": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcisolatedSubnet2RouteTable334F9764" + }, + "SubnetId": { + "Ref": "VpcisolatedSubnet2Subnet39217055" + } + } + }, + "PostgresSourceSubnetGroupBEEB1740": { + "Type": "AWS::RDS::DBSubnetGroup", + "Properties": { + "DBSubnetGroupDescription": "Subnet group for PostgresSource database", + "SubnetIds": [ + { + "Ref": "VpcisolatedSubnet1SubnetE62B1B9B" + }, + { + "Ref": "VpcisolatedSubnet2Subnet39217055" + } + ] + } + }, + "PostgresSourceSecurityGroup69289E68": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "Security group for PostgresSource database", + "SecurityGroupEgress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow all outbound traffic by default", + "IpProtocol": "-1" + } + ], + "VpcId": { + "Ref": "Vpc8378EB38" + } + } + }, + "PostgresSourceSecret0A09A7AD": { + "Type": "AWS::SecretsManager::Secret", + "Properties": { + "Description": { + "Fn::Join": [ + "", + [ + "Generated by the CDK for stack: ", + { + "Ref": "AWS::StackName" + } + ] + ] + }, + "GenerateSecretString": { + "ExcludeCharacters": " %+~`#$&*()|[]{}:;<>?!'/@\"\\", + "GenerateStringKey": "password", + "PasswordLength": 30, + "SecretStringTemplate": "{\"username\":\"postgres\"}" + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "PostgresSourceSecretAttachmentE3C3B705": { + "Type": "AWS::SecretsManager::SecretTargetAttachment", + "Properties": { + "SecretId": { + "Ref": "PostgresSourceSecret0A09A7AD" + }, + "TargetId": { + "Ref": "PostgresSourceEB66BFC9" + }, + "TargetType": "AWS::RDS::DBInstance" + } + }, + "PostgresSourceEB66BFC9": { + "Type": "AWS::RDS::DBInstance", + "Properties": { + "DBInstanceClass": "db.t3.small", + "AllocatedStorage": "100", + "BackupRetentionPeriod": 5, + "CopyTagsToSnapshot": true, + "DBSubnetGroupName": { + "Ref": "PostgresSourceSubnetGroupBEEB1740" + }, + "Engine": "postgres", + "EngineVersion": "13", + "MasterUsername": { + "Fn::Join": [ + "", + [ + "{{resolve:secretsmanager:", + { + "Ref": "PostgresSourceSecret0A09A7AD" + }, + ":SecretString:username::}}" + ] + ] + }, + "MasterUserPassword": { + "Fn::Join": [ + "", + [ + "{{resolve:secretsmanager:", + { + "Ref": "PostgresSourceSecret0A09A7AD" + }, + ":SecretString:password::}}" + ] + ] + }, + "PubliclyAccessible": false, + "StorageType": "gp2", + "VPCSecurityGroups": [ + { + "Fn::GetAtt": [ + "PostgresSourceSecurityGroup69289E68", + "GroupId" + ] + } + ] + }, + "UpdateReplacePolicy": "Snapshot", + "DeletionPolicy": "Snapshot" + }, + "PostgresReplicaSubnetGroup301B59DA": { + "Type": "AWS::RDS::DBSubnetGroup", + "Properties": { + "DBSubnetGroupDescription": "Subnet group for PostgresReplica database", + "SubnetIds": [ + { + "Ref": "VpcisolatedSubnet1SubnetE62B1B9B" + }, + { + "Ref": "VpcisolatedSubnet2Subnet39217055" + } + ] + } + }, + "PostgresReplicaSecurityGroup5385C4C2": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "Security group for PostgresReplica database", + "SecurityGroupEgress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow all outbound traffic by default", + "IpProtocol": "-1" + } + ], + "VpcId": { + "Ref": "Vpc8378EB38" + } + } + }, + "PostgresReplica23A3C738": { + "Type": "AWS::RDS::DBInstance", + "Properties": { + "DBInstanceClass": "db.t3.small", + "CopyTagsToSnapshot": true, + "DBSubnetGroupName": { + "Ref": "PostgresReplicaSubnetGroup301B59DA" + }, + "PubliclyAccessible": false, + "SourceDBInstanceIdentifier": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":rds:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":db:", + { + "Ref": "PostgresSourceEB66BFC9" + } + ] + ] + }, + "StorageType": "gp2", + "VPCSecurityGroups": [ + { + "Fn::GetAtt": [ + "PostgresReplicaSecurityGroup5385C4C2", + "GroupId" + ] + } + ] + }, + "UpdateReplacePolicy": "Snapshot", + "DeletionPolicy": "Snapshot" + }, + "MysqlSourceSubnetGroup213E979B": { + "Type": "AWS::RDS::DBSubnetGroup", + "Properties": { + "DBSubnetGroupDescription": "Subnet group for MysqlSource database", + "SubnetIds": [ + { + "Ref": "VpcisolatedSubnet1SubnetE62B1B9B" + }, + { + "Ref": "VpcisolatedSubnet2Subnet39217055" + } + ] + } + }, + "MysqlSourceSecurityGroupC691E169": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "Security group for MysqlSource database", + "SecurityGroupEgress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow all outbound traffic by default", + "IpProtocol": "-1" + } + ], + "VpcId": { + "Ref": "Vpc8378EB38" + } + } + }, + "MysqlSourceSecretB727C3F2": { + "Type": "AWS::SecretsManager::Secret", + "Properties": { + "Description": { + "Fn::Join": [ + "", + [ + "Generated by the CDK for stack: ", + { + "Ref": "AWS::StackName" + } + ] + ] + }, + "GenerateSecretString": { + "ExcludeCharacters": " %+~`#$&*()|[]{}:;<>?!'/@\"\\", + "GenerateStringKey": "password", + "PasswordLength": 30, + "SecretStringTemplate": "{\"username\":\"admin\"}" + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "MysqlSourceSecretAttachment5E4EDF73": { + "Type": "AWS::SecretsManager::SecretTargetAttachment", + "Properties": { + "SecretId": { + "Ref": "MysqlSourceSecretB727C3F2" + }, + "TargetId": { + "Ref": "MysqlSource9A10350C" + }, + "TargetType": "AWS::RDS::DBInstance" + } + }, + "MysqlSource9A10350C": { + "Type": "AWS::RDS::DBInstance", + "Properties": { + "DBInstanceClass": "db.t3.small", + "AllocatedStorage": "100", + "BackupRetentionPeriod": 5, + "CopyTagsToSnapshot": true, + "DBSubnetGroupName": { + "Ref": "MysqlSourceSubnetGroup213E979B" + }, + "Engine": "mysql", + "EngineVersion": "8.0", + "MasterUsername": { + "Fn::Join": [ + "", + [ + "{{resolve:secretsmanager:", + { + "Ref": "MysqlSourceSecretB727C3F2" + }, + ":SecretString:username::}}" + ] + ] + }, + "MasterUserPassword": { + "Fn::Join": [ + "", + [ + "{{resolve:secretsmanager:", + { + "Ref": "MysqlSourceSecretB727C3F2" + }, + ":SecretString:password::}}" + ] + ] + }, + "PubliclyAccessible": false, + "StorageType": "gp2", + "VPCSecurityGroups": [ + { + "Fn::GetAtt": [ + "MysqlSourceSecurityGroupC691E169", + "GroupId" + ] + } + ] + }, + "UpdateReplacePolicy": "Snapshot", + "DeletionPolicy": "Snapshot" + }, + "MysqlReplicaSubnetGroup79E1F72A": { + "Type": "AWS::RDS::DBSubnetGroup", + "Properties": { + "DBSubnetGroupDescription": "Subnet group for MysqlReplica database", + "SubnetIds": [ + { + "Ref": "VpcisolatedSubnet1SubnetE62B1B9B" + }, + { + "Ref": "VpcisolatedSubnet2Subnet39217055" + } + ] + } + }, + "MysqlReplicaSecurityGroup169FAFAA": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "Security group for MysqlReplica database", + "SecurityGroupEgress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow all outbound traffic by default", + "IpProtocol": "-1" + } + ], + "VpcId": { + "Ref": "Vpc8378EB38" + } + } + }, + "MysqlReplica87D29F78": { + "Type": "AWS::RDS::DBInstance", + "Properties": { + "DBInstanceClass": "db.t3.small", + "BackupRetentionPeriod": 3, + "CopyTagsToSnapshot": true, + "DBSubnetGroupName": { + "Ref": "MysqlReplicaSubnetGroup79E1F72A" + }, + "PubliclyAccessible": false, + "SourceDBInstanceIdentifier": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":rds:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":db:", + { + "Ref": "MysqlSource9A10350C" + } + ] + ] + }, + "StorageType": "gp2", + "VPCSecurityGroups": [ + { + "Fn::GetAtt": [ + "MysqlReplicaSecurityGroup169FAFAA", + "GroupId" + ] + } + ] + }, + "UpdateReplacePolicy": "Snapshot", + "DeletionPolicy": "Snapshot" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-rds/test/integ.read-replica.ts b/packages/@aws-cdk/aws-rds/test/integ.read-replica.ts new file mode 100644 index 0000000000000..7ebbd176049b4 --- /dev/null +++ b/packages/@aws-cdk/aws-rds/test/integ.read-replica.ts @@ -0,0 +1,59 @@ +import { InstanceClass, InstanceSize, InstanceType, SubnetSelection, SubnetType, Vpc } from '@aws-cdk/aws-ec2'; +import { App, Duration, Stack, StackProps } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import * as rds from '../lib'; + +class TestStack extends Stack { + constructor(scope: Construct, id: string, props?: StackProps) { + super(scope, id, props); + + const vpc = new Vpc(this, 'Vpc', { + maxAzs: 2, + subnetConfiguration: [ + { + name: 'isolated', + subnetType: SubnetType.PRIVATE_ISOLATED, + }, + ], + }); + + const instanceType = InstanceType.of(InstanceClass.T3, InstanceSize.SMALL); + + const vpcSubnets: SubnetSelection = { subnetType: SubnetType.PRIVATE_ISOLATED }; + + const postgresSource = new rds.DatabaseInstance(this, 'PostgresSource', { + engine: rds.DatabaseInstanceEngine.postgres({ version: rds.PostgresEngineVersion.VER_13 }), + backupRetention: Duration.days(5), + instanceType, + vpc, + vpcSubnets, + }); + + new rds.DatabaseInstanceReadReplica(this, 'PostgresReplica', { + sourceDatabaseInstance: postgresSource, + instanceType, + vpc, + vpcSubnets, + }); + + const mysqlSource = new rds.DatabaseInstance(this, 'MysqlSource', { + engine: rds.DatabaseInstanceEngine.mysql({ version: rds.MysqlEngineVersion.VER_8_0 }), + backupRetention: Duration.days(5), + instanceType, + vpc, + vpcSubnets, + }); + + new rds.DatabaseInstanceReadReplica(this, 'MysqlReplica', { + sourceDatabaseInstance: mysqlSource, + backupRetention: Duration.days(3), + instanceType, + vpc, + vpcSubnets, + }); + } +} + +const app = new App(); +new TestStack(app, 'cdk-rds-read-replica'); +app.synth(); From 55df760fdd9514384de019e5ce338d5250c7df97 Mon Sep 17 00:00:00 2001 From: Mohamed Elasmar <71043312+moelasmar@users.noreply.github.com> Date: Fri, 19 Nov 2021 12:42:39 -0800 Subject: [PATCH 2/2] fix(assets): add missing SAM asset metadata information (#17591) Following up on issue #14593 The integration with SAM tool requires to have some more info about the Assets. SAM needs to know if the Asset was already bundled or not, and what is the original asset path before staging. This change is to add the following assets metadata: - aws:asset:is-bundled - aws:asset:original-path ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../@aws-cdk/aws-lambda/test/code.test.ts | 7 ++++++- .../@aws-cdk/aws-lambda/test/layers.test.ts | 5 ++++- .../aws-logs/test/log-retention.test.ts | 2 ++ packages/@aws-cdk/aws-s3-assets/lib/asset.ts | 20 ++++++++++++++++++- .../@aws-cdk/aws-s3-assets/test/asset.test.ts | 8 ++++++++ packages/@aws-cdk/cx-api/lib/assets.ts | 2 ++ 6 files changed, 41 insertions(+), 3 deletions(-) diff --git a/packages/@aws-cdk/aws-lambda/test/code.test.ts b/packages/@aws-cdk/aws-lambda/test/code.test.ts index 40db469cc12d4..be6172c2454fc 100644 --- a/packages/@aws-cdk/aws-lambda/test/code.test.ts +++ b/packages/@aws-cdk/aws-lambda/test/code.test.ts @@ -74,6 +74,8 @@ describe('code', () => { expect(stack).toHaveResource('AWS::Lambda::Function', { Metadata: { [cxapi.ASSET_RESOURCE_METADATA_PATH_KEY]: 'asset.9678c34eca93259d11f2d714177347afd66c50116e1e08996eff893d3ca81232', + [cxapi.ASSET_RESOURCE_METADATA_ORIGINAL_PATH_KEY]: location, + [cxapi.ASSET_RESOURCE_METADATA_IS_BUNDLED_KEY]: false, [cxapi.ASSET_RESOURCE_METADATA_PROPERTY_KEY]: 'Code', }, }, ResourcePart.CompleteDefinition); @@ -462,8 +464,9 @@ describe('code', () => { stack.node.setContext(cxapi.ASSET_RESOURCE_METADATA_ENABLED_CONTEXT, true); // when + const FunctionCodepath = path.join(__dirname, 'docker-build-lambda'); new lambda.Function(stack, 'Fn', { - code: lambda.Code.fromDockerBuild(path.join(__dirname, 'docker-build-lambda')), + code: lambda.Code.fromDockerBuild(FunctionCodepath), handler: 'index.handler', runtime: lambda.Runtime.NODEJS_12_X, }); @@ -472,6 +475,8 @@ describe('code', () => { expect(stack).toHaveResource('AWS::Lambda::Function', { Metadata: { [cxapi.ASSET_RESOURCE_METADATA_PATH_KEY]: 'asset.fbafdbb9ae8d1bae0def415b791a93c486d18ebc63270c748abecc3ac0ab9533', + [cxapi.ASSET_RESOURCE_METADATA_ORIGINAL_PATH_KEY]: FunctionCodepath, + [cxapi.ASSET_RESOURCE_METADATA_IS_BUNDLED_KEY]: false, [cxapi.ASSET_RESOURCE_METADATA_PROPERTY_KEY]: 'Code', }, }, ResourcePart.CompleteDefinition); diff --git a/packages/@aws-cdk/aws-lambda/test/layers.test.ts b/packages/@aws-cdk/aws-lambda/test/layers.test.ts index 1c416236c0980..6ec497848cfdf 100644 --- a/packages/@aws-cdk/aws-lambda/test/layers.test.ts +++ b/packages/@aws-cdk/aws-lambda/test/layers.test.ts @@ -74,14 +74,17 @@ describe('layers', () => { stack.node.setContext(cxapi.ASSET_RESOURCE_METADATA_ENABLED_CONTEXT, true); // WHEN + let layerCodePath = path.join(__dirname, 'layer-code'); new lambda.LayerVersion(stack, 'layer', { - code: lambda.Code.fromAsset(path.join(__dirname, 'layer-code')), + code: lambda.Code.fromAsset(layerCodePath), }); // THEN expect(canonicalizeTemplate(SynthUtils.toCloudFormation(stack))).toHaveResource('AWS::Lambda::LayerVersion', { Metadata: { 'aws:asset:path': 'asset.Asset1Hash', + 'aws:asset:original-path': layerCodePath, + 'aws:asset:is-bundled': false, 'aws:asset:property': 'Content', }, }, ResourcePart.CompleteDefinition); diff --git a/packages/@aws-cdk/aws-logs/test/log-retention.test.ts b/packages/@aws-cdk/aws-logs/test/log-retention.test.ts index af746f956e675..8a5d1241c2f20 100644 --- a/packages/@aws-cdk/aws-logs/test/log-retention.test.ts +++ b/packages/@aws-cdk/aws-logs/test/log-retention.test.ts @@ -196,6 +196,8 @@ describe('log retention', () => { expect(stack).toHaveResource('AWS::Lambda::Function', { Metadata: { 'aws:asset:path': assetLocation, + 'aws:asset:original-path': assetLocation, + 'aws:asset:is-bundled': false, 'aws:asset:property': 'Code', }, }, ResourcePart.CompleteDefinition); diff --git a/packages/@aws-cdk/aws-s3-assets/lib/asset.ts b/packages/@aws-cdk/aws-s3-assets/lib/asset.ts index 0777602e788f6..46f8cb2901e8e 100644 --- a/packages/@aws-cdk/aws-s3-assets/lib/asset.ts +++ b/packages/@aws-cdk/aws-s3-assets/lib/asset.ts @@ -120,13 +120,29 @@ export class Asset extends CoreConstruct implements cdk.IAsset { public readonly assetHash: string; + /** + * The original Asset Path before it got staged. + * + * If asset staging is disabled, this will be same value as assetPath. + * If asset staging is enabled, it will be the Asset original path before staging. + */ + private readonly originalAssetPath: string; + + /** + * Indicates if this asset got bundled before staged, or not. + */ + private readonly isBundled: boolean; + constructor(scope: Construct, id: string, props: AssetProps) { super(scope, id); + this.originalAssetPath = path.resolve(props.path); + this.isBundled = props.bundling != null; + // stage the asset source (conditionally). const staging = new cdk.AssetStaging(this, 'Stage', { ...props, - sourcePath: path.resolve(props.path), + sourcePath: this.originalAssetPath, follow: props.followSymlinks ?? toSymlinkFollow(props.follow), assetHash: props.assetHash ?? props.sourceHash, }); @@ -191,6 +207,8 @@ export class Asset extends CoreConstruct implements cdk.IAsset { // points to a local path in order to enable local invocation of this function. resource.cfnOptions.metadata = resource.cfnOptions.metadata || { }; resource.cfnOptions.metadata[cxapi.ASSET_RESOURCE_METADATA_PATH_KEY] = this.assetPath; + resource.cfnOptions.metadata[cxapi.ASSET_RESOURCE_METADATA_ORIGINAL_PATH_KEY] = this.originalAssetPath; + resource.cfnOptions.metadata[cxapi.ASSET_RESOURCE_METADATA_IS_BUNDLED_KEY] = this.isBundled; resource.cfnOptions.metadata[cxapi.ASSET_RESOURCE_METADATA_PROPERTY_KEY] = resourceProperty; } diff --git a/packages/@aws-cdk/aws-s3-assets/test/asset.test.ts b/packages/@aws-cdk/aws-s3-assets/test/asset.test.ts index cf0a5c7bc03af..4ea59bff3c7ef 100644 --- a/packages/@aws-cdk/aws-s3-assets/test/asset.test.ts +++ b/packages/@aws-cdk/aws-s3-assets/test/asset.test.ts @@ -203,6 +203,8 @@ test('addResourceMetadata can be used to add CFN metadata to resources', () => { expect(stack).toHaveResource('My::Resource::Type', { Metadata: { 'aws:asset:path': 'asset.6b84b87243a4a01c592d78e1fd3855c4bfef39328cd0a450cc97e81717fea2a2', + 'aws:asset:original-path': location, + 'aws:asset:is-bundled': false, 'aws:asset:property': 'PropName', }, }, ResourcePart.CompleteDefinition); @@ -222,6 +224,8 @@ test('asset metadata is only emitted if ASSET_RESOURCE_METADATA_ENABLED_CONTEXT expect(stack).not.toHaveResource('My::Resource::Type', { Metadata: { 'aws:asset:path': SAMPLE_ASSET_DIR, + 'aws:asset:original-path': SAMPLE_ASSET_DIR, + 'aws:asset:is-bundled': false, 'aws:asset:property': 'PropName', }, }, ResourcePart.CompleteDefinition); @@ -351,6 +355,8 @@ describe('staging', () => { const template = SynthUtils.synthesize(stack).template; expect(template.Resources.MyResource.Metadata).toEqual({ 'aws:asset:path': 'asset.6b84b87243a4a01c592d78e1fd3855c4bfef39328cd0a450cc97e81717fea2a2', + 'aws:asset:original-path': SAMPLE_ASSET_DIR, + 'aws:asset:is-bundled': false, 'aws:asset:property': 'PropName', }); }); @@ -377,6 +383,8 @@ describe('staging', () => { const template = SynthUtils.synthesize(stack).template; expect(template.Resources.MyResource.Metadata).toEqual({ 'aws:asset:path': SAMPLE_ASSET_DIR, + 'aws:asset:original-path': SAMPLE_ASSET_DIR, + 'aws:asset:is-bundled': false, 'aws:asset:property': 'PropName', }); }); diff --git a/packages/@aws-cdk/cx-api/lib/assets.ts b/packages/@aws-cdk/cx-api/lib/assets.ts index 0b3eaa52cefb5..010bafc80c3a5 100644 --- a/packages/@aws-cdk/cx-api/lib/assets.ts +++ b/packages/@aws-cdk/cx-api/lib/assets.ts @@ -14,6 +14,8 @@ export const ASSET_RESOURCE_METADATA_DOCKERFILE_PATH_KEY = 'aws:asset:dockerfile export const ASSET_RESOURCE_METADATA_DOCKER_BUILD_ARGS_KEY = 'aws:asset:docker-build-args'; export const ASSET_RESOURCE_METADATA_DOCKER_BUILD_TARGET_KEY = 'aws:asset:docker-build-target'; export const ASSET_RESOURCE_METADATA_PROPERTY_KEY = 'aws:asset:property'; +export const ASSET_RESOURCE_METADATA_IS_BUNDLED_KEY = 'aws:asset:is-bundled'; +export const ASSET_RESOURCE_METADATA_ORIGINAL_PATH_KEY = 'aws:asset:original-path'; /** * Separator string that separates the prefix separator from the object key separator.