Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ class L1PropsMixin extends ClassType {
},
});

this.relationshipDecider = new RelationshipDecider(this.resource, db);
this.relationshipDecider = new RelationshipDecider(this.resource, db, false);
this.converter = TypeConverter.forMixin({
db: db,
resource: this.resource,
Expand Down
102 changes: 102 additions & 0 deletions packages/aws-cdk-lib/aws-lambda/test/function.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5046,6 +5046,10 @@ describe('Lambda Function log group behavior', () => {
});

describe('telemetry metadata', () => {
afterEach(() => {
jest.restoreAllMocks();
});

it('redaction happens when feature flag is enabled', () => {
const app = new cdk.App();
app.node.setContext(cxapi.ENABLE_ADDITIONAL_METADATA_COLLECTION, true);
Expand Down Expand Up @@ -5107,6 +5111,104 @@ describe('telemetry metadata', () => {
expect(fn.node.metadata).toStrictEqual([]);
});
});
describe('L1 Relationships', () => {
it('simple union', () => {
const stack = new cdk.Stack();
const role = new iam.Role(stack, 'SomeRole', {
assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
});
new lambda.CfnFunction(stack, 'MyLambda', {
code: { zipFile: 'foo' },
role: role, // Simple Union
});
Template.fromStack(stack).hasResource('AWS::Lambda::Function', {
Properties: {
Role: { 'Fn::GetAtt': ['SomeRole6DDC54DD', 'Arn'] },
},
});
});

it('array of unions', () => {
const stack = new cdk.Stack();
const role = new iam.Role(stack, 'SomeRole', {
assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
});
const layer1 = new lambda.LayerVersion(stack, 'LayerVersion1', {
code: lambda.Code.fromAsset(path.join(__dirname, 'my-lambda-handler')),
compatibleRuntimes: [lambda.Runtime.PYTHON_3_13],
});
const layer2 = new lambda.LayerVersion(stack, 'LayerVersion2', {
code: lambda.Code.fromAsset(path.join(__dirname, 'my-lambda-handler')),
compatibleRuntimes: [lambda.Runtime.PYTHON_3_13],
});
new lambda.CfnFunction(stack, 'MyLambda', {
code: { zipFile: 'foo' },
role: role,
layers: [layer1, layer2], // Array of Unions
});
Template.fromStack(stack).hasResource('AWS::Lambda::Function', {
Properties: {
Role: { 'Fn::GetAtt': ['SomeRole6DDC54DD', 'Arn'] },
Layers: [{ Ref: 'LayerVersion139D4D7A8' }, { Ref: 'LayerVersion23E5F3CEA' }],
},
});
});

it('array reference should be valid', () => {
const stack = new cdk.Stack();
const role = new iam.Role(stack, 'SomeRole', {
assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
});
const layer1 = new lambda.LayerVersion(stack, 'LayerVersion1', {
code: lambda.Code.fromAsset(path.join(__dirname, 'my-lambda-handler')),
compatibleRuntimes: [lambda.Runtime.PYTHON_3_13],
});
const layerArray = [layer1, 'layer2Arn'];
new lambda.CfnFunction(stack, 'MyLambda', {
code: { zipFile: 'foo' },
role: role,
layers: layerArray,
});

layerArray.push('layer3Arn');

Template.fromStack(stack).hasResource('AWS::Lambda::Function', {
Properties: {
Role: { 'Fn::GetAtt': ['SomeRole6DDC54DD', 'Arn'] },
Layers: [{ Ref: 'LayerVersion139D4D7A8' }, 'layer2Arn', 'layer3Arn'],
},
});
});

it('tokens should be passed as is', () => {
const stack = new cdk.Stack();
const role = new iam.Role(stack, 'SomeRole', {
assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
});
const bucket = new s3.Bucket(stack, 'MyBucket');

const codeToken = cdk.Token.asAny({
resolve: () => ({ s3Bucket: bucket.bucketName }),
});

const fsConfigToken = cdk.Token.asAny({
resolve: () => ([{ arn: 'TestArn', localMountPath: '/mnt' }]),
});

new lambda.CfnFunction(stack, 'MyLambda', {
code: codeToken,
role: role,
fileSystemConfigs: fsConfigToken,
});
Template.fromStack(stack).hasResource('AWS::Lambda::Function', {
Properties: {
Role: { 'Fn::GetAtt': ['SomeRole6DDC54DD', 'Arn'] },
Code: { S3Bucket: { Ref: 'MyBucketF68F3FF0' } },
FileSystemConfigs: [{ Arn: 'TestArn', LocalMountPath: '/mnt' }],
},
});
});
});

function newTestLambda(scope: constructs.Construct) {
return new lambda.Function(scope, 'MyLambda', {
Expand Down
17 changes: 9 additions & 8 deletions tools/@aws-cdk/spec2cdk/lib/cdk/aws-cdk-lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,13 +182,6 @@ export class AwsCdkLibBuilder extends LibraryBuilder<AwsCdkLibServiceSubmodule>
submodule.registerResource(resource.cloudFormationType, resourceClass);
submodule.registerSelectiveImports(...resourceClass.imports);
submodule.augmentations?.module.augmentResource(resource, resourceClass);

for (const selectiveImport of submodule.imports) {
const sourceModule = new Module(selectiveImport.moduleName);
sourceModule.importSelective(submodule.resourcesMod.module, selectiveImport.types.map((t) => `${t.originalType} as ${t.aliasedType}`), {
fromLocation: relativeImportPath(submodule.resourcesMod.filePath, sourceModule.name),
});
}
}

private createResourceModule(moduleName: string, service: Service): LocatedModule<Module> {
Expand Down Expand Up @@ -262,7 +255,15 @@ export class AwsCdkLibBuilder extends LibraryBuilder<AwsCdkLibServiceSubmodule>
grantModule.build(Object.fromEntries(submodule.resources), props?.nameSuffix);
}

super.postprocessSubmodule(submodule);
// Apply selective imports only to resources module
for (const selectiveImport of submodule.imports) {
const sourceModule = new Module(selectiveImport.moduleName);
sourceModule.importSelective(
submodule.resourcesMod.module,
selectiveImport.types.map((t) => `${t.originalType} as ${t.aliasedType}`),
{ fromLocation: relativeImportPath(submodule.resourcesMod, sourceModule.name) },
);
}

// Add an import for the interfaces file to the entry point file (make sure not to do it twice)
if (!submodule.interfaces?.module.isEmpty() && this.interfacesEntry && submodule.didCreateInterfaceModule) {
Expand Down
22 changes: 18 additions & 4 deletions tools/@aws-cdk/spec2cdk/lib/cdk/relationship-decider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,19 @@ import { getReferenceProps } from './reference-props';
import { log } from '../util';
import { SelectiveImport } from './service-submodule';

// For now we want relationships to be applied only for these services
export const RELATIONSHIP_SERVICES: string[] = [];
/**
* We currently disable the relationship on the properties of types because they would create a backwards incompatible change
* by broadening the output type as types are used both in input and output. This represents:
* Relationship counts:
* Resource-level (non-nested): 598
* Type-level (nested): 483 <- these are disabled by this flag
* Total: 1081
* Properties with relationships:
* Resource-level (non-nested): 493
* Type-level (nested): 358
* Total: 851
*/
export const GENERATE_RELATIONSHIPS_ON_TYPES = false;

/**
* Represents a cross-service property relationship that enables references
Expand All @@ -30,7 +41,7 @@ export class RelationshipDecider {
private readonly namespace: string;
public readonly imports = new Array<SelectiveImport>();

constructor(readonly resource: Resource, private readonly db: SpecDatabase) {
constructor(readonly resource: Resource, private readonly db: SpecDatabase, private readonly enableRelationships = true) {
this.namespace = namespaceFromResource(resource);
}

Expand Down Expand Up @@ -61,7 +72,7 @@ export class RelationshipDecider {
* properties as a relationship can only target a primary identifier or arn
*/
private findTargetResource(sourcePropName: string, relationship: RelationshipRef) {
if (!RELATIONSHIP_SERVICES.some(s => this.resource.cloudFormationType.toLowerCase().startsWith(`aws::${s}::`))) {
if (!this.enableRelationships) {
return undefined;
}
const targetResource = this.db.lookup('resource', 'cloudFormationType', 'equals', relationship.cloudFormationType).only();
Expand Down Expand Up @@ -135,6 +146,9 @@ export class RelationshipDecider {
* Checks if a given property needs a flattening function or not
*/
public needsFlatteningFunction(propName: string, prop: Property, visited = new Set<string>()): boolean {
if (!GENERATE_RELATIONSHIPS_ON_TYPES) {
return false;
}
if (this.hasValidRelationships(propName, prop.relationshipRefs)) {
return true;
}
Expand Down
39 changes: 22 additions & 17 deletions tools/@aws-cdk/spec2cdk/lib/cdk/resolver-builder.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { DefinitionReference, Property } from '@aws-cdk/service-spec-types';
import { expr, Expression, Module, Type } from '@cdklabs/typewriter';
import { CDK_CORE } from './cdk';
import { RelationshipDecider, Relationship } from './relationship-decider';
import { RelationshipDecider, Relationship, GENERATE_RELATIONSHIPS_ON_TYPES } from './relationship-decider';
import { NON_RESOLVABLE_PROPERTY_NAMES } from './tagging';
import { TypeConverter } from './type-converter';
import { flattenFunctionNameFromType, propertyNameFromCloudFormation } from '../naming';
Expand All @@ -28,7 +28,8 @@ export class ResolverBuilder {
private readonly module: Module,
) {}

public buildResolver(prop: Property, cfnName: string): ResolverResult {
public buildResolver(prop: Property, cfnName: string, isTypeProp = false): ResolverResult {
const shouldGenerateRelationships = isTypeProp ? GENERATE_RELATIONSHIPS_ON_TYPES : true;
const name = propertyNameFromCloudFormation(cfnName);
const baseType = this.converter.typeFromProperty(prop);

Expand All @@ -37,17 +38,19 @@ export class ResolverBuilder {
// but didn't.
const resolvableType = cfnName in NON_RESOLVABLE_PROPERTY_NAMES ? baseType : this.converter.makeTypeResolvable(baseType);

const relationships = this.relationshipDecider.parseRelationship(name, prop.relationshipRefs);
if (relationships.length > 0) {
return this.buildRelationshipResolver({ relationships, baseType, name, resolvableType });
}
if (shouldGenerateRelationships) {
const relationships = this.relationshipDecider.parseRelationship(name, prop.relationshipRefs);
if (relationships.length > 0) {
return this.buildRelationshipResolver({ relationships, baseType, name, resolvableType });
}

const originalType = this.converter.originalType(baseType);
if (this.relationshipDecider.needsFlatteningFunction(name, prop)) {
const optional = !prop.required;
const typeRef = originalType.type === 'array' ? originalType.element : originalType;
if (typeRef.type === 'ref') {
return this.buildNestedResolver({ name, baseType, typeRef: typeRef, resolvableType, optional });
const originalType = this.converter.originalType(baseType);
if (this.relationshipDecider.needsFlatteningFunction(name, prop)) {
const optional = !prop.required;
const typeRef = originalType.type === 'array' ? originalType.element : originalType;
if (typeRef.type === 'ref') {
return this.buildNestedResolver({ name, baseType, typeRef: typeRef, resolvableType, optional });
}
}
}

Expand Down Expand Up @@ -80,8 +83,8 @@ export class ResolverBuilder {
].join(' | ');

// Generates code like:
// For single value: (props.roleArn as IRoleRef)?.roleRef?.roleArn ?? (props.roleArn as IUserRef)?.userRef?.userArn ?? props.roleArn
// For array: props.roleArns?.map((item: any) => (item as IRoleRef)?.roleRef?.roleArn ?? (item as IUserRef)?.userRef?.userArn ?? item)
// For single value T | string : (props.xx as IxxxRef)?.xxxRef?.xxxArn ?? cdk.ensureStringOrUndefined(props.xxx, "xxx", "iam.IxxxRef | string");
// For array <T | string>[]: (props.xx?.forEach((item: T | string, i: number, arr: Array<T | string>) => { arr[i] = (item as T)?.xxxRef?.xx ?? cdk.ensureStringOrUndefined(item, "xxx", "lambda.T | string"); }), props.xxx as Array<string>);
// Ensures that arn properties always appear first in the chain as they are more general
const arnRels = relationships.filter(r => r.propName.toLowerCase().endsWith('arn'));
const otherRels = relationships.filter(r => !r.propName.toLowerCase().endsWith('arn'));
Expand All @@ -93,7 +96,9 @@ export class ResolverBuilder {
].join(' ?? ');
const resolver = (_: Expression) => {
if (resolvableType.arrayOfType) {
return expr.directCode(`props.${name}?.map((item: any) => ${ buildChain('item') })`);
return expr.directCode(
`(props.${name}?.forEach((item: ${propType.arrayOfType!.toString()}, i: number, arr: ${propType.toString()}) => { arr[i] = ${buildChain('item')}; }), props.${name} as ${resolvableType.toString()})`,
);
} else {
return expr.directCode(buildChain(`props.${name}`));
}
Expand All @@ -118,11 +123,11 @@ export class ResolverBuilder {
const isArray = baseType.arrayOfType !== undefined;

const flattenCall = isArray
? propValue.callMethod('map', expr.ident(functionName))
? expr.directCode(`props.${name}.forEach((item: any, i: number, arr: any[]) => { arr[i] = ${functionName}(item) }), props.${name}`)
: expr.ident(functionName).call(propValue);

const condition = optional
? expr.cond(propValue).then(flattenCall).else(expr.UNDEFINED)
? expr.cond(expr.not(propValue)).then(expr.UNDEFINED).else(flattenCall)
: flattenCall;

return isArray
Expand Down
2 changes: 1 addition & 1 deletion tools/@aws-cdk/spec2cdk/lib/cdk/service-submodule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ export class BaseServiceSubmodule {
const existingModuleImport = this.findSelectiveImport(theImport);
if (!existingModuleImport) {
this.selectiveImports.push(theImport);
return;
continue;
}

// We need to avoid importing the same reference multiple times
Expand Down
2 changes: 1 addition & 1 deletion tools/@aws-cdk/spec2cdk/lib/cdk/typedefinition-decider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export class TypeDefinitionDecider {
private handlePropertyDefault(cfnName: string, prop: Property) {
const optional = !prop.required;

const resolverResult = this.resolverBuilder.buildResolver(prop, cfnName);
const resolverResult = this.resolverBuilder.buildResolver(prop, cfnName, true);

this.properties.push({
propertySpec: {
Expand Down
Loading
Loading