Skip to content

Commit c5e7f21

Browse files
authored
feat(core): cfn constructs (L1s) can now accept constructs as parameters for known resource relationships (#35713)
### Issue # (if applicable) Closes #<issue number here>. ### Reason for this change Allow passing L2s and L1s to L1s ### Description of changes Added a new relationship decider to parse the relationship information from the database and allow the other deciders to modify the properties / constructors accordingly. The relationship deciders assumes that: - All modules will be built - The properties in the relationship will exist (and have the same naming) #### This generates code that looks like this for non nested properties: In the properties: `readonly role: IamIRoleRef | string;` This is then used in the constructor: `this.role = (props.role as IamIRoleRef)?.roleRef?.roleArn ?? props.role;` If there were multiple possible IxxRef: `(props.targetArn as SqsIQueueRef)?.queueRef?.queueArn ?? (props.targetArn as SnsITopicRef)?.topicRef?.topicArn ?? props.targetArn` #### For nested properties The props behave the same way, "flatten" functions are generated to recursively perform the same role that was done in the constructor for non nested properties: ``` function flattenCfnIdentityPoolRoleAttachmentRoleMappingProperty(props: CfnIdentityPoolRoleAttachment.RoleMappingProperty): CfnIdentityPoolRoleAttachment.RoleMappingProperty { return { "ambiguousRoleResolution": props.ambiguousRoleResolution, "identityProvider": props.identityProvider, "rulesConfiguration": (cdk.isResolvableObject(props.rulesConfiguration) ? props.rulesConfiguration : (props.rulesConfiguration ? flattenCfnIdentityPoolRoleAttachmentRulesConfigurationTypeProperty(props.rulesConfiguration) : undefined)), "type": props.type }; } ``` #### For arrays ``` // @ts-ignore TS6133 function flattenCfnCodeSigningConfigAllowedPublishersProperty(props: CfnCodeSigningConfig.AllowedPublishersProperty | cdk.IResolvable): CfnCodeSigningConfig.AllowedPublishersProperty | cdk.IResolvable { if (cdk.isResolvableObject(props)) return props; return { "signingProfileVersionArns": props.signingProfileVersionArns?.map((item: any) => (item as SignerISigningProfileRef)?.signingProfileRef?.signingProfileArn ?? item) }; } ``` #### For arrays of nested props: ``` this.fileSystemConfigs = (cdk.isResolvableObject(props.fileSystemConfigs) ? props.fileSystemConfigs : (props.fileSystemConfigs ? props.fileSystemConfigs.map(flattenCfnFunctionFileSystemConfigProperty) : undefined)); ``` ### Database assumptions Properties (the properties in sense of props of a resource but also prop of a type) have a a `relationshipRefs` array containing the relationship information: ```relationshipRefs: [{ typeName: "AWS::IAM::Role", propertyPath: "Arn" }, ...]``` ### Description of how you validated changes - Checked the diffs between the previously generated code and the new one. - Added snapshot tests - Added unit tests for lambda - Deployed a stack manually consisting of mixes of L1 and L2 resources using the new capabilities this PR adds ### Checklist - [X] My code adheres to the [CONTRIBUTING GUIDE](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md) and [DESIGN GUIDELINES](https://github.com/aws/aws-cdk/blob/main/docs/DESIGN_GUIDELINES.md) ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent 66ffa6c commit c5e7f21

File tree

16 files changed

+3056
-175
lines changed

16 files changed

+3056
-175
lines changed

packages/aws-cdk-lib/aws-cloudfront/lib/distribution.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { CacheBehavior } from './private/cache-behavior';
1717
import { formatDistributionArn, grant } from './private/utils';
1818
import * as acm from '../../aws-certificatemanager';
1919
import * as cloudwatch from '../../aws-cloudwatch';
20+
import * as elasticloadbalancingv2 from '../../aws-elasticloadbalancingv2';
2021
import * as iam from '../../aws-iam';
2122
import * as lambda from '../../aws-lambda';
2223
import * as s3 from '../../aws-s3';
@@ -716,7 +717,9 @@ export class Distribution extends Resource implements IDistribution {
716717
const generatedId = Names.uniqueId(scope).slice(-ORIGIN_ID_MAX_LENGTH);
717718
const distributionId = this.distributionId;
718719
const originBindConfig = origin.bind(scope, { originId: generatedId, distributionId: Lazy.string({ produce: () => this.distributionId }) });
719-
const originId = originBindConfig.originProperty?.id ?? generatedId;
720+
const originId = (originBindConfig.originProperty?.id as elasticloadbalancingv2.ILoadBalancerRef)?.loadBalancerRef?.loadBalancerArn
721+
?? originBindConfig.originProperty?.id
722+
?? generatedId;
720723
const duplicateId = this.boundOrigins.find(boundOrigin => boundOrigin.originProperty?.id === originBindConfig.originProperty?.id);
721724
if (duplicateId) {
722725
throw new ValidationError(`Origin with id ${duplicateId.originProperty?.id} already exists. OriginIds must be unique within a distribution`, this);
@@ -741,7 +744,8 @@ export class Distribution extends Resource implements IDistribution {
741744
);
742745
return originGroupId;
743746
}
744-
return originBindConfig.originProperty?.id ?? originId;
747+
return (originBindConfig.originProperty?.id as elasticloadbalancingv2.ILoadBalancerRef)?.loadBalancerRef?.loadBalancerArn
748+
?? originBindConfig.originProperty?.id ?? originId;
745749
}
746750
}
747751

packages/aws-cdk-lib/aws-lambda/test/function.test.ts

Lines changed: 155 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5046,19 +5046,27 @@ describe('Lambda Function log group behavior', () => {
50465046
});
50475047

50485048
describe('telemetry metadata', () => {
5049-
it('redaction happens when feature flag is enabled', () => {
5050-
const app = new cdk.App();
5051-
app.node.setContext(cxapi.ENABLE_ADDITIONAL_METADATA_COLLECTION, true);
5052-
const stack = new cdk.Stack(app);
5049+
let getPrototypeOfSpy: jest.SpyInstance;
50535050

5051+
beforeEach(() => {
50545052
const mockConstructor = {
50555053
[JSII_RUNTIME_SYMBOL]: {
50565054
fqn: 'aws-cdk-lib.aws-lambda.Function',
50575055
},
50585056
};
5059-
jest.spyOn(Object, 'getPrototypeOf').mockReturnValue({
5057+
getPrototypeOfSpy = jest.spyOn(Object, 'getPrototypeOf').mockReturnValue({
50605058
constructor: mockConstructor,
50615059
});
5060+
});
5061+
5062+
afterEach(() => {
5063+
getPrototypeOfSpy.mockRestore();
5064+
});
5065+
5066+
it('redaction happens when feature flag is enabled', () => {
5067+
const app = new cdk.App();
5068+
app.node.setContext(cxapi.ENABLE_ADDITIONAL_METADATA_COLLECTION, true);
5069+
const stack = new cdk.Stack(app);
50625070

50635071
const fn = new lambda.Function(stack, 'Lambda', {
50645072
code: lambda.Code.fromInline('foo'),
@@ -5089,15 +5097,6 @@ describe('telemetry metadata', () => {
50895097
app.node.setContext(cxapi.ENABLE_ADDITIONAL_METADATA_COLLECTION, false);
50905098
const stack = new cdk.Stack(app);
50915099

5092-
const mockConstructor = {
5093-
[JSII_RUNTIME_SYMBOL]: {
5094-
fqn: 'aws-cdk-lib.aws-lambda.Function',
5095-
},
5096-
};
5097-
jest.spyOn(Object, 'getPrototypeOf').mockReturnValue({
5098-
constructor: mockConstructor,
5099-
});
5100-
51015100
const fn = new lambda.Function(stack, 'Lambda', {
51025101
code: lambda.Code.fromInline('foo'),
51035102
handler: 'index.handler',
@@ -5107,6 +5106,148 @@ describe('telemetry metadata', () => {
51075106
expect(fn.node.metadata).toStrictEqual([]);
51085107
});
51095108
});
5109+
describe('L1 Relationships', () => {
5110+
it('simple union', () => {
5111+
const stack = new cdk.Stack();
5112+
const role = new iam.Role(stack, 'SomeRole', {
5113+
assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
5114+
});
5115+
new lambda.CfnFunction(stack, 'MyLambda', {
5116+
code: { zipFile: 'foo' },
5117+
role: role, // Simple Union
5118+
});
5119+
Template.fromStack(stack).hasResource('AWS::Lambda::Function', {
5120+
Properties: {
5121+
Role: { 'Fn::GetAtt': ['SomeRole6DDC54DD', 'Arn'] },
5122+
},
5123+
});
5124+
});
5125+
5126+
it('array of unions', () => {
5127+
const stack = new cdk.Stack();
5128+
const role = new iam.Role(stack, 'SomeRole', {
5129+
assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
5130+
});
5131+
const layer1 = new lambda.LayerVersion(stack, 'LayerVersion1', {
5132+
code: lambda.Code.fromAsset(path.join(__dirname, 'my-lambda-handler')),
5133+
compatibleRuntimes: [lambda.Runtime.PYTHON_3_13],
5134+
});
5135+
const layer2 = new lambda.LayerVersion(stack, 'LayerVersion2', {
5136+
code: lambda.Code.fromAsset(path.join(__dirname, 'my-lambda-handler')),
5137+
compatibleRuntimes: [lambda.Runtime.PYTHON_3_13],
5138+
});
5139+
new lambda.CfnFunction(stack, 'MyLambda', {
5140+
code: { zipFile: 'foo' },
5141+
role: role,
5142+
layers: [layer1, layer2], // Array of Unions
5143+
});
5144+
Template.fromStack(stack).hasResource('AWS::Lambda::Function', {
5145+
Properties: {
5146+
Role: { 'Fn::GetAtt': ['SomeRole6DDC54DD', 'Arn'] },
5147+
Layers: [{ Ref: 'LayerVersion139D4D7A8' }, { Ref: 'LayerVersion23E5F3CEA' }],
5148+
},
5149+
});
5150+
});
5151+
5152+
it('nested union', () => {
5153+
const stack = new cdk.Stack();
5154+
const role = new iam.Role(stack, 'SomeRole', {
5155+
assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
5156+
});
5157+
const bucket = new s3.Bucket(stack, 'MyBucket');
5158+
5159+
new lambda.CfnFunction(stack, 'MyLambda', {
5160+
code: {
5161+
s3Bucket: bucket, // Nested union
5162+
},
5163+
role: role,
5164+
});
5165+
Template.fromStack(stack).hasResource('AWS::Lambda::Function', {
5166+
Properties: {
5167+
Role: { 'Fn::GetAtt': ['SomeRole6DDC54DD', 'Arn'] },
5168+
Code: { S3Bucket: { Ref: 'MyBucketF68F3FF0' } },
5169+
},
5170+
});
5171+
});
5172+
5173+
it('deeply nested union', () => {
5174+
const stack = new cdk.Stack();
5175+
const topic = new sns.CfnTopic(stack, 'Topic');
5176+
5177+
new lambda.CfnEventInvokeConfig(stack, 'EventConfig', {
5178+
functionName: 'myFunction',
5179+
qualifier: '$LATEST',
5180+
destinationConfig: {
5181+
onFailure: {
5182+
destination: topic, // Deeply nested: destinationConfig -> onFailure -> destination (union)
5183+
},
5184+
},
5185+
});
5186+
Template.fromStack(stack).hasResource('AWS::Lambda::EventInvokeConfig', {
5187+
Properties: {
5188+
DestinationConfig: {
5189+
OnFailure: {
5190+
Destination: { Ref: 'Topic' },
5191+
},
5192+
},
5193+
},
5194+
});
5195+
});
5196+
5197+
it('nested array of unions', () => {
5198+
const stack = new cdk.Stack();
5199+
const role = new iam.Role(stack, 'SomeRole', {
5200+
assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
5201+
});
5202+
const securityGroup = new ec2.SecurityGroup(stack, 'SG', {
5203+
vpc: new ec2.Vpc(stack, 'VPC'),
5204+
});
5205+
new lambda.CfnFunction(stack, 'MyLambda', {
5206+
code: { zipFile: 'foo' },
5207+
role: role,
5208+
vpcConfig: {
5209+
securityGroupIds: [securityGroup], // Nested array of union
5210+
},
5211+
});
5212+
Template.fromStack(stack).hasResource('AWS::Lambda::Function', {
5213+
Properties: {
5214+
Role: { 'Fn::GetAtt': ['SomeRole6DDC54DD', 'Arn'] },
5215+
VpcConfig: {
5216+
SecurityGroupIds: [{ 'Fn::GetAtt': ['SGADB53937', 'GroupId'] }],
5217+
},
5218+
},
5219+
});
5220+
});
5221+
5222+
it('tokens should be passed as is', () => {
5223+
const stack = new cdk.Stack();
5224+
const role = new iam.Role(stack, 'SomeRole', {
5225+
assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
5226+
});
5227+
const bucket = new s3.Bucket(stack, 'MyBucket');
5228+
5229+
const codeToken = cdk.Token.asAny({
5230+
resolve: () => ({ s3Bucket: bucket.bucketName }),
5231+
});
5232+
5233+
const fsConfigToken = cdk.Token.asAny({
5234+
resolve: () => ([{ arn: 'TestArn', localMountPath: '/mnt' }]),
5235+
});
5236+
5237+
new lambda.CfnFunction(stack, 'MyLambda', {
5238+
code: codeToken,
5239+
role: role,
5240+
fileSystemConfigs: fsConfigToken,
5241+
});
5242+
Template.fromStack(stack).hasResource('AWS::Lambda::Function', {
5243+
Properties: {
5244+
Role: { 'Fn::GetAtt': ['SomeRole6DDC54DD', 'Arn'] },
5245+
Code: { S3Bucket: { Ref: 'MyBucketF68F3FF0' } },
5246+
FileSystemConfigs: [{ Arn: 'TestArn', LocalMountPath: '/mnt' }],
5247+
},
5248+
});
5249+
});
5250+
});
51105251

51115252
function newTestLambda(scope: constructs.Construct) {
51125253
return new lambda.Function(scope, 'MyLambda', {

packages/aws-cdk-lib/aws-s3/lib/bucket.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2354,7 +2354,7 @@ export class Bucket extends BucketBase {
23542354

23552355
const objectLockConfiguration = this.parseObjectLockConfig(props);
23562356
const replicationConfiguration = this.renderReplicationConfiguration(props);
2357-
this.replicationRoleArn = replicationConfiguration?.role;
2357+
this.replicationRoleArn = (replicationConfiguration?.role as iam.IRoleRef)?.roleRef?.roleArn ?? replicationConfiguration?.role;
23582358
this.objectOwnership = props.objectOwnership;
23592359
this.transitionDefaultMinimumObjectSize = props.transitionDefaultMinimumObjectSize;
23602360
const resource = new CfnBucket(this, 'Resource', {

tools/@aws-cdk/spec2cdk/lib/cdk/ast.ts

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Module } from '@cdklabs/typewriter';
33
import { AugmentationsModule } from './augmentation-generator';
44
import { CannedMetricsModule } from './canned-metrics';
55
import { CDK_CORE, CONSTRUCTS, ModuleImportLocations } from './cdk';
6+
import { SelectiveImport } from './relationship-decider';
67
import { ResourceClass } from './resource-class';
78

89
/**
@@ -59,7 +60,7 @@ export class AstBuilder<T extends Module> {
5960
for (const link of resources) {
6061
ast.addResource(link.entity);
6162
}
62-
63+
ast.renderImports();
6364
return ast;
6465
}
6566

@@ -74,6 +75,7 @@ export class AstBuilder<T extends Module> {
7475

7576
const ast = new AstBuilder(scope, props, aug, metrics);
7677
ast.addResource(resource);
78+
ast.renderImports();
7779

7880
return ast;
7981
}
@@ -85,6 +87,8 @@ export class AstBuilder<T extends Module> {
8587
public readonly resources: Record<string, string> = {};
8688
private nameSuffix?: string;
8789
private deprecated?: string;
90+
public readonly selectiveImports = new Array<SelectiveImport>();
91+
private readonly modulesRootLocation: string;
8892

8993
protected constructor(
9094
public readonly module: T,
@@ -95,6 +99,7 @@ export class AstBuilder<T extends Module> {
9599
this.db = props.db;
96100
this.nameSuffix = props.nameSuffix;
97101
this.deprecated = props.deprecated;
102+
this.modulesRootLocation = props.importLocations?.modulesRoot ?? '../..';
98103

99104
CDK_CORE.import(this.module, 'cdk', { fromLocation: props.importLocations?.core });
100105
CONSTRUCTS.import(this.module, 'constructs');
@@ -111,6 +116,35 @@ export class AstBuilder<T extends Module> {
111116

112117
resourceClass.build();
113118

119+
this.addImports(resourceClass);
114120
this.augmentations?.augmentResource(resource, resourceClass);
115121
}
122+
123+
private addImports(resourceClass: ResourceClass) {
124+
for (const selectiveImport of resourceClass.imports) {
125+
const existingModuleImport = this.selectiveImports.find(
126+
(imp) => imp.moduleName === selectiveImport.moduleName,
127+
);
128+
if (!existingModuleImport) {
129+
this.selectiveImports.push(selectiveImport);
130+
} else {
131+
// We need to avoid importing the same reference multiple times
132+
for (const type of selectiveImport.types) {
133+
if (!existingModuleImport.types.find((t) =>
134+
t.originalType === type.originalType && t.aliasedType === type.aliasedType,
135+
)) {
136+
existingModuleImport.types.push(type);
137+
}
138+
}
139+
}
140+
}
141+
}
142+
143+
public renderImports() {
144+
const sortedImports = this.selectiveImports.sort((a, b) => a.moduleName.localeCompare(b.moduleName));
145+
for (const selectiveImport of sortedImports) {
146+
const sourceModule = new Module(selectiveImport.moduleName);
147+
sourceModule.importSelective(this.module, selectiveImport.types.map((t) => `${t.originalType} as ${t.aliasedType}`), { fromLocation: `${this.modulesRootLocation}/${sourceModule.name}` });
148+
}
149+
}
116150
}

tools/@aws-cdk/spec2cdk/lib/cdk/cdk.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ export interface ModuleImportLocations {
2222
* @default 'aws-cdk-lib/aws-cloudwatch'
2323
*/
2424
readonly cloudwatch?: string;
25+
/**
26+
* The root location of all the modules
27+
* @default '../..'
28+
*/
29+
readonly modulesRoot?: string;
2530
}
2631

2732
export class CdkCore extends ExternalModule {

0 commit comments

Comments
 (0)