Skip to content

Commit d8ebe73

Browse files
leonmk-awsLeon Michalski
authored andcommitted
feat(core): cfn constructs (L1s) can now accept constructs as parameters for known resource relationships (#35713)
Closes #<issue number here>. Allow passing L2s and L1s to L1s 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) 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` 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 }; } ``` ``` // @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) }; } ``` ``` this.fileSystemConfigs = (cdk.isResolvableObject(props.fileSystemConfigs) ? props.fileSystemConfigs : (props.fileSystemConfigs ? props.fileSystemConfigs.map(flattenCfnFunctionFileSystemConfigProperty) : undefined)); ``` 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" }, ...]``` - 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 - [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 fda6e13 commit d8ebe73

File tree

4 files changed

+171
-18
lines changed

4 files changed

+171
-18
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/relationship-decider.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,15 @@ import { createModuleDefinitionFromCfnNamespace } from '../cfn2ts/pkglint';
55
import { log } from '../util';
66

77
// For now we want relationships to be applied only for these services
8-
export const RELATIONSHIP_SERVICES: string[] = [];
8+
const RELATIONSHIP_SERVICES = [
9+
'iam',
10+
'apigateway',
11+
'ec2',
12+
'cloudfront',
13+
'kms',
14+
's3',
15+
'lambda',
16+
];
917

1018
/**
1119
* Represents a cross-service property relationship that enables references

0 commit comments

Comments
 (0)