diff --git a/.github/workflows/codebuild-pr-build.yml b/.github/workflows/codebuild-pr-build.yml index 4236351c0008b..2937476ea7882 100644 --- a/.github/workflows/codebuild-pr-build.yml +++ b/.github/workflows/codebuild-pr-build.yml @@ -70,7 +70,7 @@ jobs: sudo sysctl -w vm.max_map_count=2251954 - name: Build - run: /bin/bash ./build.sh --ci + run: /bin/bash ./build.sh --ci --concurrency 10 - name: Run Rosetta run: /bin/bash ./scripts/run-rosetta.sh diff --git a/.github/workflows/pr-build.yml b/.github/workflows/pr-build.yml index adf1edcbfe7f1..abadbb14084da 100644 --- a/.github/workflows/pr-build.yml +++ b/.github/workflows/pr-build.yml @@ -65,7 +65,7 @@ jobs: sudo sysctl -w vm.max_map_count=2251954 - name: Build - run: /bin/bash ./build.sh --ci --concurrency=10 + run: /bin/bash ./build.sh --ci --concurrency 10 - name: Run Rosetta run: /bin/bash ./scripts/run-rosetta.sh diff --git a/tools/@aws-cdk/spec2cdk/lib/cdk/ast.ts b/tools/@aws-cdk/spec2cdk/lib/cdk/ast.ts index c68dd7adc36ef..035b8548c2688 100644 --- a/tools/@aws-cdk/spec2cdk/lib/cdk/ast.ts +++ b/tools/@aws-cdk/spec2cdk/lib/cdk/ast.ts @@ -3,6 +3,7 @@ import { Module } from '@cdklabs/typewriter'; import { AugmentationsModule } from './augmentation-generator'; import { CannedMetricsModule } from './canned-metrics'; import { CDK_CORE, CONSTRUCTS, ModuleImportLocations } from './cdk'; +import { SelectiveImport } from './relationship-decider'; import { ResourceClass } from './resource-class'; /** @@ -59,7 +60,7 @@ export class AstBuilder { for (const link of resources) { ast.addResource(link.entity); } - + ast.renderImports(); return ast; } @@ -74,6 +75,7 @@ export class AstBuilder { const ast = new AstBuilder(scope, props, aug, metrics); ast.addResource(resource); + ast.renderImports(); return ast; } @@ -85,6 +87,8 @@ export class AstBuilder { public readonly resources: Record = {}; private nameSuffix?: string; private deprecated?: string; + public readonly selectiveImports = new Array(); + private readonly modulesRootLocation: string; protected constructor( public readonly module: T, @@ -95,6 +99,7 @@ export class AstBuilder { this.db = props.db; this.nameSuffix = props.nameSuffix; this.deprecated = props.deprecated; + this.modulesRootLocation = props.importLocations?.modulesRoot ?? '../..'; CDK_CORE.import(this.module, 'cdk', { fromLocation: props.importLocations?.core }); CONSTRUCTS.import(this.module, 'constructs'); @@ -111,6 +116,35 @@ export class AstBuilder { resourceClass.build(); + this.addImports(resourceClass); this.augmentations?.augmentResource(resource, resourceClass); } + + private addImports(resourceClass: ResourceClass) { + for (const selectiveImport of resourceClass.imports) { + const existingModuleImport = this.selectiveImports.find( + (imp) => imp.moduleName === selectiveImport.moduleName, + ); + if (!existingModuleImport) { + this.selectiveImports.push(selectiveImport); + } else { + // We need to avoid importing the same reference multiple times + for (const type of selectiveImport.types) { + if (!existingModuleImport.types.find((t) => + t.originalType === type.originalType && t.aliasedType === type.aliasedType, + )) { + existingModuleImport.types.push(type); + } + } + } + } + } + + public renderImports() { + const sortedImports = this.selectiveImports.sort((a, b) => a.moduleName.localeCompare(b.moduleName)); + for (const selectiveImport of sortedImports) { + const sourceModule = new Module(selectiveImport.moduleName); + sourceModule.importSelective(this.module, selectiveImport.types.map((t) => `${t.originalType} as ${t.aliasedType}`), { fromLocation: `${this.modulesRootLocation}/${sourceModule.name}` }); + } + } } diff --git a/tools/@aws-cdk/spec2cdk/lib/cdk/cdk.ts b/tools/@aws-cdk/spec2cdk/lib/cdk/cdk.ts index 6fc5365d5a2f8..5921b2b441220 100644 --- a/tools/@aws-cdk/spec2cdk/lib/cdk/cdk.ts +++ b/tools/@aws-cdk/spec2cdk/lib/cdk/cdk.ts @@ -22,6 +22,11 @@ export interface ModuleImportLocations { * @default 'aws-cdk-lib/aws-cloudwatch' */ readonly cloudwatch?: string; + /** + * The root location of all the modules + * @default '../..' + */ + readonly modulesRoot?: string; } export class CdkCore extends ExternalModule { diff --git a/tools/@aws-cdk/spec2cdk/lib/cdk/reference-props.ts b/tools/@aws-cdk/spec2cdk/lib/cdk/reference-props.ts new file mode 100644 index 0000000000000..3d7c7125a0dd4 --- /dev/null +++ b/tools/@aws-cdk/spec2cdk/lib/cdk/reference-props.ts @@ -0,0 +1,86 @@ +import { Resource } from '@aws-cdk/service-spec-types'; +import { $E, expr, Expression, PropertySpec, Type } from '@cdklabs/typewriter'; +import { attributePropertyName, referencePropertyName } from '../naming'; +import { CDK_CORE } from './cdk'; + +export interface ReferenceProp { + readonly declaration: PropertySpec; + readonly cfnValue: Expression; +} + +// Convenience typewriter builder +const $this = $E(expr.this_()); + +export function getReferenceProps(resource: Resource): ReferenceProp[] { + const referenceProps = []; + // Primary identifier. We assume all parts are strings. + const primaryIdentifier = resource.primaryIdentifier ?? []; + if (primaryIdentifier.length === 1) { + referenceProps.push({ + declaration: { + name: referencePropertyName(primaryIdentifier[0], resource.name), + type: Type.STRING, + immutable: true, + docs: { + summary: `The ${primaryIdentifier[0]} of the ${resource.name} resource.`, + }, + }, + cfnValue: $this.ref, + }); + } else if (primaryIdentifier.length > 1) { + for (const [i, cfnName] of enumerate(primaryIdentifier)) { + referenceProps.push({ + declaration: { + name: referencePropertyName(cfnName, resource.name), + type: Type.STRING, + immutable: true, + docs: { + summary: `The ${cfnName} of the ${resource.name} resource.`, + }, + }, + cfnValue: splitSelect('|', i, $this.ref), + }); + } + } + + const arnProp = findArnProperty(resource); + if (arnProp) { + referenceProps.push({ + declaration: { + name: referencePropertyName(arnProp, resource.name), + type: Type.STRING, + immutable: true, + docs: { + summary: `The ARN of the ${resource.name} resource.`, + }, + }, + cfnValue: $this[attributePropertyName(arnProp)], + }); + } + return referenceProps; +} + +/** + * Find an ARN property for a given resource + * + * Returns `undefined` if no ARN property is found, or if the ARN property is already + * included in the primary identifier. + */ +export function findArnProperty(resource: Resource) { + const possibleArnNames = ['Arn', `${resource.name}Arn`]; + for (const name of possibleArnNames) { + const prop = resource.attributes[name]; + if (prop && !resource.primaryIdentifier?.includes(name)) { + return name; + } + } + return undefined; +} + +function splitSelect(sep: string, n: number, base: Expression) { + return CDK_CORE.Fn.select(expr.lit(n), CDK_CORE.Fn.split(expr.lit(sep), base)); +} + +function enumerate(xs: A[]): Array<[number, A]> { + return xs.map((x, i) => [i, x]); +} diff --git a/tools/@aws-cdk/spec2cdk/lib/cdk/relationship-decider.ts b/tools/@aws-cdk/spec2cdk/lib/cdk/relationship-decider.ts new file mode 100644 index 0000000000000..b4df6af5b207d --- /dev/null +++ b/tools/@aws-cdk/spec2cdk/lib/cdk/relationship-decider.ts @@ -0,0 +1,172 @@ +import { Property, RelationshipRef, Resource, RichProperty, SpecDatabase } from '@aws-cdk/service-spec-types'; +import { namespaceFromResource, referenceInterfaceName, referenceInterfaceAttributeName, referencePropertyName, typeAliasPrefixFromResource } from '../naming'; +import { getReferenceProps } from './reference-props'; +import { createModuleDefinitionFromCfnNamespace } from '../cfn2ts/pkglint'; +import { log } from '../util'; + +// For now we want relationships to be applied only for these services +export const RELATIONSHIP_SERVICES: string[] = []; + +/** + * Represents a cross-service property relationship that enables references + * between resources from different AWS services. + */ +export interface Relationship { + /** The TypeScript interface type that provides the reference (e.g. "IRoleRef") */ + readonly referenceType: string; + /** The property name on the reference interface that holds the reference object (e.g. "roleRef") */ + readonly referenceName: string; + /** The property to extract from the reference object (e.g. "roleArn") */ + readonly propName: string; +} + +/** + * Represents a selective import statement for cross-module type references. + * Used to import specific types from other CDK modules when relationships + * are between different modules. + */ +export interface SelectiveImport { + /** The module name to import from */ + readonly moduleName: string; + /** Array of types that need to be imported */ + readonly types: { + /** The original type name in the source module */ + originalType: string; + /** The aliased name to avoid naming conflicts */ + aliasedType: string; + }[]; +} + +/** + * Extracts resource relationship information from the database for cross-service property references. + */ +export class RelationshipDecider { + private readonly namespace: string; + public readonly imports = new Array(); + + constructor(readonly resource: Resource, private readonly db: SpecDatabase) { + this.namespace = namespaceFromResource(resource); + } + + private registerRequiredImport({ namespace, originalType, aliasedType }: { + namespace: string; + originalType: string; + aliasedType: string; + }) { + const moduleName = createModuleDefinitionFromCfnNamespace(namespace).moduleName; + const moduleImport = this.imports.find(i => i.moduleName === moduleName); + if (!moduleImport) { + this.imports.push({ + moduleName, + types: [{ originalType, aliasedType }], + }); + } else { + if (!moduleImport.types.find(t => + t.originalType === originalType && t.aliasedType === aliasedType, + )) { + moduleImport.types.push({ originalType, aliasedType }); + } + } + } + + /** + * Retrieves the target resource for a relationship. + * Returns undefined if the target property cannot be found in the reference + * 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}::`))) { + return undefined; + } + const targetResource = this.db.lookup('resource', 'cloudFormationType', 'equals', relationship.cloudFormationType).only(); + const refProps = getReferenceProps(targetResource); + const expectedPropName = referencePropertyName(relationship.propertyName, targetResource.name); + const prop = refProps.find(p => p.declaration.name === expectedPropName); + if (!prop) { + log.debug( + 'Could not find target prop for relationship:', + `${this.resource.cloudFormationType} ${sourcePropName}`, + `=> ${targetResource.cloudFormationType} ${relationship.propertyName}`, + ); + return undefined; + } + return targetResource; + } + + public parseRelationship(sourcePropName: string, relationships?: RelationshipRef[]) { + const parsedRelationships: Relationship[] = []; + if (!relationships) { + return parsedRelationships; + } + for (const relationship of relationships) { + const targetResource = this.findTargetResource(sourcePropName, relationship); + if (!targetResource) { + continue; + } + // Ignore the suffix part because it's an edge case that happens only for one module + const interfaceName = referenceInterfaceName(targetResource.name); + const refPropStructName = referenceInterfaceAttributeName(targetResource.name); + + const targetNamespace = namespaceFromResource(targetResource); + let aliasedTypeName = undefined; + if (this.namespace !== targetNamespace) { + // If this is not in our namespace we need to alias the import type + aliasedTypeName = `${typeAliasPrefixFromResource(targetResource)}${interfaceName}`; + this.registerRequiredImport({ namespace: targetNamespace, originalType: interfaceName, aliasedType: aliasedTypeName }); + } + parsedRelationships.push({ + referenceType: aliasedTypeName ?? interfaceName, + referenceName: refPropStructName, + propName: referencePropertyName(relationship.propertyName, targetResource.name), + }); + } + return parsedRelationships; + } + + /** + * Extracts the referenced type from a property's type, for direct refs and array element refs. + */ + private getReferencedType(prop: Property) { + // Use the oldest type for backwards compatibility + const type = new RichProperty(prop).types()[0]; + if (type.type === 'ref') { + return this.db.get('typeDefinition', type.reference.$ref); + } else if (type.type === 'array' && type.element.type === 'ref') { + return this.db.get('typeDefinition', type.element.reference.$ref); + } + return undefined; + } + + private hasValidRelationships(sourcePropName: string, relationships?: RelationshipRef[]): boolean { + if (!relationships) { + return false; + } + return relationships.some(rel => this.findTargetResource(sourcePropName, rel) !== undefined); + } + + /** + * Checks if a given property needs a flattening function or not + */ + public needsFlatteningFunction(propName: string, prop: Property, visited = new Set()): boolean { + if (this.hasValidRelationships(propName, prop.relationshipRefs)) { + return true; + } + + const referencedTypeDef = this.getReferencedType(prop); + if (!referencedTypeDef) { + return false; + } + + if (visited.has(referencedTypeDef.$id)) { + return false; + } + visited.add(referencedTypeDef.$id); + + for (const [nestedPropName, nestedProp] of Object.entries(referencedTypeDef.properties)) { + if (this.needsFlatteningFunction(nestedPropName, nestedProp, visited)) { + return true; + } + } + return false; + } +} diff --git a/tools/@aws-cdk/spec2cdk/lib/cdk/resolver-builder.ts b/tools/@aws-cdk/spec2cdk/lib/cdk/resolver-builder.ts new file mode 100644 index 0000000000000..1dca3489dc9e1 --- /dev/null +++ b/tools/@aws-cdk/spec2cdk/lib/cdk/resolver-builder.ts @@ -0,0 +1,129 @@ +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 { NON_RESOLVABLE_PROPERTY_NAMES } from './tagging'; +import { TypeConverter } from './type-converter'; +import { flattenFunctionNameFromType, propertyNameFromCloudFormation } from '../naming'; + +export interface ResolverResult { + /** Property name */ + name: string; + /** Property type augmented with the relationships type information */ + propType: Type; + /** Property type without relationship type information */ + resolvableType: Type; + /** Same as propType without IResolvable */ + baseType: Type; + resolver: (props: Expression) => Expression; +} + +/** + * Builds property resolvers that handle relationships and nested property flattening + */ +export class ResolverBuilder { + constructor( + private readonly converter: TypeConverter, + private readonly relationshipDecider: RelationshipDecider, + private readonly module: Module, + ) {} + + public buildResolver(prop: Property, cfnName: string): ResolverResult { + const name = propertyNameFromCloudFormation(cfnName); + const baseType = this.converter.typeFromProperty(prop); + + // Whether or not a property is made `IResolvable` originally depended on + // the name of the property. These conditions were probably expected to coincide, + // 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 }); + } + + 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 }); + } + } + + return { + name, + propType: resolvableType, + resolvableType, + baseType, + resolver: (props: Expression) => expr.get(props, name), + }; + } + + private buildRelationshipResolver({ relationships, baseType, name, resolvableType }: { + relationships: Relationship[]; + baseType: Type; + name: string; + resolvableType: Type; + }): ResolverResult { + if (!(baseType === Type.STRING || baseType.arrayOfType === Type.STRING)) { + throw Error('Trying to map to a non string property'); + } + const newTypes = relationships.map(t => Type.fromName(this.module, t.referenceType)); + const propType = resolvableType.arrayOfType + ? Type.arrayOf(Type.distinctUnionOf(resolvableType.arrayOfType, ...newTypes)) + : Type.distinctUnionOf(resolvableType, ...newTypes); + + // 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) + // 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')); + + const buildChain = (itemName: string) => [ + ...[...arnRels, ...otherRels] + .map(r => `(${itemName} as ${r.referenceType})?.${r.referenceName}?.${r.propName}`), + itemName, + ].join(' ?? '); + const resolver = (_: Expression) => { + if (resolvableType.arrayOfType) { + return expr.directCode(`props.${name}?.map((item: any) => ${ buildChain('item') })`); + } else { + return expr.directCode(buildChain(`props.${name}`)); + } + }; + + return { name, propType, resolvableType, baseType, resolver }; + } + + private buildNestedResolver({ name, baseType, typeRef, resolvableType, optional }: { + name: string; + baseType: Type; + typeRef: DefinitionReference; + resolvableType: Type; + optional: boolean; + }): ResolverResult { + const referencedTypeDef = this.converter.db.get('typeDefinition', typeRef.reference.$ref); + const referencedStruct = this.converter.convertTypeDefinitionType(referencedTypeDef); + const functionName = flattenFunctionNameFromType(referencedStruct); + + const resolver = (props: Expression) => { + const propValue = expr.get(props, name); + const isArray = baseType.arrayOfType !== undefined; + + const flattenCall = isArray + ? propValue.callMethod('map', expr.ident(functionName)) + : expr.ident(functionName).call(propValue); + + const condition = optional + ? expr.cond(propValue).then(flattenCall).else(expr.UNDEFINED) + : flattenCall; + + return isArray + ? expr.cond(CDK_CORE.isResolvableObject(propValue)).then(propValue).else(condition) + : condition; + }; + return { name, propType: resolvableType, resolvableType, baseType, resolver }; + } +} diff --git a/tools/@aws-cdk/spec2cdk/lib/cdk/resource-class.ts b/tools/@aws-cdk/spec2cdk/lib/cdk/resource-class.ts index 60dbc1222683f..92bc2d7dd405b 100644 --- a/tools/@aws-cdk/spec2cdk/lib/cdk/resource-class.ts +++ b/tools/@aws-cdk/spec2cdk/lib/cdk/resource-class.ts @@ -28,18 +28,20 @@ import { } from '@cdklabs/typewriter'; import { CDK_CORE, CONSTRUCTS } from './cdk'; import { CloudFormationMapping } from './cloudformation-mapping'; -import { ResourceDecider, shouldBuildReferenceInterface } from './resource-decider'; +import { ResourceDecider } from './resource-decider'; import { TypeConverter } from './type-converter'; import { cfnParserNameFromType, cfnProducerNameFromType, classNameFromResource, cloudFormationDocLink, propertyNameFromCloudFormation, - propStructNameFromResource, referencePropertyName, + propStructNameFromResource, referenceInterfaceName, referenceInterfaceAttributeName, referencePropertyName, staticRequiredTransform, staticResourceTypeName, } from '../naming'; import { splitDocumentation } from '../util'; +import { findArnProperty } from './reference-props'; +import { SelectiveImport, RelationshipDecider } from './relationship-decider'; export interface ITypeHost { typeFromSpecType(type: PropertyType): Type; @@ -55,11 +57,13 @@ export interface ResourceClassProps { export class ResourceClass extends ClassType { private readonly propsType: StructType; - private readonly refInterface?: InterfaceType; + private readonly refInterface: InterfaceType; private readonly decider: ResourceDecider; + private readonly relationshipDecider: RelationshipDecider; private readonly converter: TypeConverter; private readonly module: Module; private referenceStruct?: StructType; + public readonly imports = new Array(); constructor( scope: IScope, @@ -67,20 +71,17 @@ export class ResourceClass extends ClassType { private readonly resource: Resource, private readonly props: ResourceClassProps = {}, ) { - let refInterface: InterfaceType | undefined; - if (shouldBuildReferenceInterface(resource)) { - // IBucketRef { bucketRef: BucketRef } - refInterface = new InterfaceType(scope, { - export: true, - name: `I${resource.name}${props.suffix ?? ''}Ref`, - extends: [CONSTRUCTS.IConstruct, CDK_CORE.IEnvironmentAware], - docs: { - summary: `Indicates that this resource can be referenced as a ${resource.name}.`, - stability: Stability.Experimental, - ...maybeDeprecated(props.deprecated), - }, - }); - } + // IBucketRef { bucketRef: BucketRef } + const refInterface = new InterfaceType(scope, { + export: true, + name: referenceInterfaceName(resource.name, props.suffix), + extends: [CONSTRUCTS.IConstruct, CDK_CORE.IEnvironmentAware], + docs: { + summary: `Indicates that this resource can be referenced as a ${resource.name}.`, + stability: Stability.Experimental, + ...maybeDeprecated(props.deprecated), + }, + }); super(scope, { export: true, @@ -95,7 +96,7 @@ export class ResourceClass extends ClassType { ...maybeDeprecated(props.deprecated), }, extends: CDK_CORE.CfnResource, - implements: [CDK_CORE.IInspectable, refInterface?.type, ...ResourceDecider.taggabilityInterfaces(resource)].filter(isDefined), + implements: [CDK_CORE.IInspectable, refInterface.type, ...ResourceDecider.taggabilityInterfaces(resource)].filter(isDefined), }); this.refInterface = refInterface; @@ -114,13 +115,16 @@ export class ResourceClass extends ClassType { }, }); + this.relationshipDecider = new RelationshipDecider(this.resource, db); this.converter = TypeConverter.forResource({ db: db, resource: this.resource, resourceClass: this, + relationshipDecider: this.relationshipDecider, }); - this.decider = new ResourceDecider(this.resource, this.converter); + this.imports = this.relationshipDecider.imports; + this.decider = new ResourceDecider(this.resource, this.converter, this.relationshipDecider); } /** @@ -191,10 +195,6 @@ export class ResourceClass extends ClassType { * Build the reference interface for this resource */ private buildReferenceInterface() { - if (!shouldBuildReferenceInterface(this.resource)) { - return; - } - // BucketRef { bucketName, bucketArn } this.referenceStruct = new StructType(this.scope, { export: true, @@ -211,8 +211,8 @@ export class ResourceClass extends ClassType { this.referenceStruct.addProperty(declaration); } - const refProperty = this.refInterface!.addProperty({ - name: `${this.decider.camelResourceName}Ref`, + const refProperty = this.refInterface.addProperty({ + name: referenceInterfaceAttributeName(this.decider.camelResourceName), type: this.referenceStruct.type, immutable: true, docs: { @@ -232,12 +232,12 @@ export class ResourceClass extends ClassType { private makeFromArnFactory() { const arnTemplate = this.resource.arnTemplate; - if (!(arnTemplate && this.refInterface && this.referenceStruct)) { + if (!(arnTemplate && this.referenceStruct)) { // We don't have enough information to build this factory return; } - const cfnArnProperty = this.decider.findArnProperty(); + const cfnArnProperty = findArnProperty(this.resource); if (cfnArnProperty == null) { return; } @@ -260,7 +260,7 @@ export class ResourceClass extends ClassType { } const innerClass = mkImportClass(this.scope); - const refAttributeName = `${this.decider.camelResourceName}Ref`; + const refAttributeName = referenceInterfaceAttributeName(this.decider.camelResourceName); innerClass.addProperty({ name: refAttributeName, @@ -287,9 +287,9 @@ export class ResourceClass extends ClassType { const factory = this.addMethod({ name: `from${this.resource.name}Arn`, static: true, - returnType: this.refInterface?.type, + returnType: this.refInterface.type, docs: { - summary: `Creates a new ${this.refInterface?.name} from an ARN`, + summary: `Creates a new ${this.refInterface.name} from an ARN`, }, }); factory.addParameter({ name: 'scope', type: CONSTRUCTS.Construct }); @@ -320,7 +320,7 @@ export class ResourceClass extends ClassType { private makeFromNameFactory() { const arnTemplate = this.resource.arnTemplate; - if (!(arnTemplate && this.refInterface && this.referenceStruct)) { + if (!(arnTemplate && this.referenceStruct)) { // We don't have enough information to build this factory return; } @@ -344,7 +344,7 @@ export class ResourceClass extends ClassType { const innerClass = mkImportClass(this.scope); - const refAttributeName = `${this.decider.camelResourceName}Ref`; + const refAttributeName = referenceInterfaceAttributeName(this.decider.camelResourceName); innerClass.addProperty({ name: refAttributeName, type: this.referenceStruct!.type, @@ -396,9 +396,9 @@ export class ResourceClass extends ClassType { const factory = this.addMethod({ name: `from${variableName}`, static: true, - returnType: this.refInterface!.type, + returnType: this.refInterface.type, docs: { - summary: `Creates a new ${this.refInterface!.name} from a ${propName}`, + summary: `Creates a new ${this.refInterface.name} from a ${propName}`, }, }); factory.addParameter({ name: 'scope', type: CONSTRUCTS.Construct }); diff --git a/tools/@aws-cdk/spec2cdk/lib/cdk/resource-decider.ts b/tools/@aws-cdk/spec2cdk/lib/cdk/resource-decider.ts index 6a8a4a627ca2a..f67567b577889 100644 --- a/tools/@aws-cdk/spec2cdk/lib/cdk/resource-decider.ts +++ b/tools/@aws-cdk/spec2cdk/lib/cdk/resource-decider.ts @@ -2,30 +2,17 @@ import { Deprecation, Property, Resource, RichProperty, TagVariant } from '@aws- import { $E, $T, Expression, PropertySpec, Type, expr } from '@cdklabs/typewriter'; import { CDK_CORE } from './cdk'; import { PropertyMapping } from './cloudformation-mapping'; +import { RelationshipDecider } from './relationship-decider'; +import { ResolverBuilder } from './resolver-builder'; import { NON_RESOLVABLE_PROPERTY_NAMES, TaggabilityStyle, resourceTaggabilityStyle } from './tagging'; import { TypeConverter } from './type-converter'; -import { attributePropertyName, camelcasedResourceName, cloudFormationDocLink, propertyNameFromCloudFormation, referencePropertyName } from '../naming'; +import { attributePropertyName, camelcasedResourceName, cloudFormationDocLink, propertyNameFromCloudFormation } from '../naming'; import { splitDocumentation } from '../util'; +import { getReferenceProps, ReferenceProp } from './reference-props'; // This convenience typewriter builder is used all over the place const $this = $E(expr.this_()); -interface ReferenceProp { - readonly declaration: PropertySpec; - readonly cfnValue: Expression; -} - -// We temporarily only do this for a few services, to experiment -const REFERENCE_PROP_SERVICES = [ - 'iam', - 'apigateway', - 'ec2', - 'cloudfront', - 'kms', - 's3', - 'lambda', -]; - /** * Decide how properties get mapped between model types, Typescript types, and CloudFormation */ @@ -40,6 +27,7 @@ export class ResourceDecider { } private readonly taggability?: TaggabilityStyle; + private readonly resolverBuilder: ResolverBuilder; public readonly referenceProps = new Array(); public readonly propsProperties = new Array(); @@ -47,14 +35,19 @@ export class ResourceDecider { public readonly classAttributeProperties = new Array(); public readonly camelResourceName: string; - constructor(private readonly resource: Resource, private readonly converter: TypeConverter) { + constructor( + private readonly resource: Resource, + private readonly converter: TypeConverter, + private readonly relationshipDecider: RelationshipDecider, + ) { this.camelResourceName = camelcasedResourceName(resource); this.taggability = resourceTaggabilityStyle(this.resource); + this.resolverBuilder = new ResolverBuilder(this.converter, this.relationshipDecider, this.converter.module); this.convertProperties(); this.convertAttributes(); - this.convertReferenceProps(); + this.referenceProps.push(...getReferenceProps(resource)); this.propsProperties.sort((p1, p2) => p1.propertySpec.name.localeCompare(p2.propertySpec.name)); this.classProperties.sort((p1, p2) => p1.propertySpec.name.localeCompare(p2.propertySpec.name)); @@ -80,104 +73,39 @@ export class ResourceDecider { } } - /** - * Find an ARN property for this resource - * - * Returns `undefined` if no ARN property is found, or if the ARN property is already - * included in the primary identifier. - */ - public findArnProperty() { - const possibleArnNames = ['Arn', `${this.resource.name}Arn`]; - for (const name of possibleArnNames) { - const prop = this.resource.attributes[name]; - if (prop && !this.resource.primaryIdentifier?.includes(name)) { - return name; - } - } - return undefined; - } - - private convertReferenceProps() { - // Primary identifier. We assume all parts are strings. - const primaryIdentifier = this.resource.primaryIdentifier ?? []; - if (primaryIdentifier.length === 1) { - this.referenceProps.push({ - declaration: { - name: referencePropertyName(primaryIdentifier[0], this.resource.name), - type: Type.STRING, - immutable: true, - docs: { - summary: `The ${primaryIdentifier[0]} of the ${this.resource.name} resource.`, - }, - }, - cfnValue: $this.ref, - }); - } else if (primaryIdentifier.length > 1) { - for (const [i, cfnName] of enumerate(primaryIdentifier)) { - this.referenceProps.push({ - declaration: { - name: referencePropertyName(cfnName, this.resource.name), - type: Type.STRING, - immutable: true, - docs: { - summary: `The ${cfnName} of the ${this.resource.name} resource.`, - }, - }, - cfnValue: splitSelect('|', i, $this.ref), - }); - } - } - - const arnProp = this.findArnProperty(); - if (arnProp) { - this.referenceProps.push({ - declaration: { - name: referencePropertyName(arnProp, this.resource.name), - type: Type.STRING, - immutable: true, - docs: { - summary: `The ARN of the ${this.resource.name} resource.`, - }, - }, - cfnValue: $this[attributePropertyName(arnProp)], - }); - } - } - /** * Default mapping for a property */ private handlePropertyDefault(cfnName: string, prop: Property) { - const name = propertyNameFromCloudFormation(cfnName); - - const { type, baseType } = this.legacyCompatiblePropType(cfnName, prop); const optional = !prop.required; + const resolverResult = this.resolverBuilder.buildResolver(prop, cfnName); + this.propsProperties.push({ propertySpec: { - name, - type, + name: resolverResult.name, + type: resolverResult.propType, optional, docs: this.defaultPropDocs(cfnName, prop), }, validateRequiredInConstructor: !!prop.required, cfnMapping: { cfnName, - propName: name, - baseType, + propName: resolverResult.name, + baseType: resolverResult.baseType, optional, }, }); this.classProperties.push({ propertySpec: { - name, - type, + name: resolverResult.name, + type: resolverResult.resolvableType, optional, immutable: false, docs: this.defaultClassPropDocs(cfnName, prop), }, - initializer: (props: Expression) => expr.get(props, name), - cfnValueToRender: { [name]: $this[name] }, + initializer: resolverResult.resolver, + cfnValueToRender: { [resolverResult.name]: $this[resolverResult.name] }, }); } @@ -466,14 +394,3 @@ export function deprecationMessage(property: Property): string | undefined { return undefined; } -function splitSelect(sep: string, n: number, base: Expression) { - return CDK_CORE.Fn.select(expr.lit(n), CDK_CORE.Fn.split(expr.lit(sep), base)); -} - -function enumerate(xs: A[]): Array<[number, A]> { - return xs.map((x, i) => [i, x]); -} - -export function shouldBuildReferenceInterface(resource: Resource) { - return true || REFERENCE_PROP_SERVICES.some(s => resource.cloudFormationType.toLowerCase().startsWith(`aws::${s}::`)); -} diff --git a/tools/@aws-cdk/spec2cdk/lib/cdk/type-converter.ts b/tools/@aws-cdk/spec2cdk/lib/cdk/type-converter.ts index 5accddfe15da2..155e62fb278a7 100644 --- a/tools/@aws-cdk/spec2cdk/lib/cdk/type-converter.ts +++ b/tools/@aws-cdk/spec2cdk/lib/cdk/type-converter.ts @@ -8,6 +8,7 @@ import { } from '@aws-cdk/service-spec-types'; import { ClassType, Module, PrimitiveType, RichScope, StructType, Type, TypeDeclaration } from '@cdklabs/typewriter'; import { CDK_CORE } from './cdk'; +import { RelationshipDecider } from './relationship-decider'; import { TypeDefinitionStruct } from './typedefinition-struct'; import { structNameFromTypeDefinition } from '../naming/conventions'; @@ -31,6 +32,7 @@ export type TypeDefinitionConverter = ( export interface TypeConverterForResourceOptions extends Omit { readonly resource: Resource; readonly resourceClass: ClassType; + readonly relationshipDecider: RelationshipDecider; } /** @@ -64,6 +66,7 @@ export class TypeConverter { resourceClass: opts.resourceClass, converter, typeDefinition, + relationshipDecider: opts.relationshipDecider, }); return { diff --git a/tools/@aws-cdk/spec2cdk/lib/cdk/typedefinition-decider.ts b/tools/@aws-cdk/spec2cdk/lib/cdk/typedefinition-decider.ts index cb8715183e992..8980570ba84c9 100644 --- a/tools/@aws-cdk/spec2cdk/lib/cdk/typedefinition-decider.ts +++ b/tools/@aws-cdk/spec2cdk/lib/cdk/typedefinition-decider.ts @@ -1,10 +1,11 @@ import { Property, Resource, TypeDefinition } from '@aws-cdk/service-spec-types'; -import { PropertySpec, Type } from '@cdklabs/typewriter'; +import { Expression, PropertySpec, Type } from '@cdklabs/typewriter'; import { PropertyMapping } from './cloudformation-mapping'; +import { RelationshipDecider } from './relationship-decider'; +import { ResolverBuilder } from './resolver-builder'; import { deprecationMessage } from './resource-decider'; -import { NON_RESOLVABLE_PROPERTY_NAMES } from './tagging'; import { TypeConverter } from './type-converter'; -import { cloudFormationDocLink, propertyNameFromCloudFormation } from '../naming'; +import { cloudFormationDocLink } from '../naming'; import { splitDocumentation } from '../util'; /** @@ -12,12 +13,15 @@ import { splitDocumentation } from '../util'; */ export class TypeDefinitionDecider { public readonly properties = new Array(); + private readonly resolverBuilder: ResolverBuilder; constructor( private readonly resource: Resource, private readonly typeDefinition: TypeDefinition, private readonly converter: TypeConverter, + private readonly relationshipDecider: RelationshipDecider, ) { + this.resolverBuilder = new ResolverBuilder(this.converter, this.relationshipDecider, this.converter.module); this.convertProperties(); this.properties.sort((p1, p2) => p1.propertySpec.name.localeCompare(p2.propertySpec.name)); } @@ -32,19 +36,14 @@ export class TypeDefinitionDecider { * Default mapping for a property */ private handlePropertyDefault(cfnName: string, prop: Property) { - const name = propertyNameFromCloudFormation(cfnName); - const baseType = this.converter.typeFromProperty(prop); - - // Whether or not a property is made `IResolvable` originally depended on - // the name of the property. These conditions were probably expected to coincide, - // but didn't. - const type = cfnName in NON_RESOLVABLE_PROPERTY_NAMES ? baseType : this.converter.makeTypeResolvable(baseType); const optional = !prop.required; + const resolverResult = this.resolverBuilder.buildResolver(prop, cfnName); + this.properties.push({ propertySpec: { - name, - type, + name: resolverResult.name, + type: resolverResult.propType, optional, docs: { ...splitDocumentation(prop.documentation), @@ -57,13 +56,14 @@ export class TypeDefinitionDecider { deprecated: deprecationMessage(prop), }, }, - baseType, + baseType: resolverResult.baseType, cfnMapping: { cfnName, - propName: name, - baseType, + propName: resolverResult.name, + baseType: resolverResult.baseType, optional, }, + resolver: resolverResult.resolver, }); } } @@ -73,4 +73,5 @@ export interface TypeDefProperty { /** The type that was converted (does not have the IResolvable union) */ readonly baseType: Type; readonly cfnMapping: PropertyMapping; + readonly resolver: (_: Expression) => Expression; } diff --git a/tools/@aws-cdk/spec2cdk/lib/cdk/typedefinition-struct.ts b/tools/@aws-cdk/spec2cdk/lib/cdk/typedefinition-struct.ts index ba0ad951da6db..11eb590ad3859 100644 --- a/tools/@aws-cdk/spec2cdk/lib/cdk/typedefinition-struct.ts +++ b/tools/@aws-cdk/spec2cdk/lib/cdk/typedefinition-struct.ts @@ -1,16 +1,19 @@ import { Resource, TypeDefinition } from '@aws-cdk/service-spec-types'; -import { ClassType, Module, Stability, StructType } from '@cdklabs/typewriter'; +import { ClassType, expr, FreeFunction, Module, Stability, stmt, StructType, Type } from '@cdklabs/typewriter'; import { CloudFormationMapping } from './cloudformation-mapping'; +import { RelationshipDecider } from './relationship-decider'; import { TypeConverter } from './type-converter'; import { TypeDefinitionDecider } from './typedefinition-decider'; -import { cloudFormationDocLink, structNameFromTypeDefinition } from '../naming'; +import { cloudFormationDocLink, flattenFunctionNameFromType, structNameFromTypeDefinition } from '../naming'; import { splitDocumentation } from '../util'; +import { CDK_CORE } from './cdk'; export interface TypeDefinitionStructOptions { readonly typeDefinition: TypeDefinition; readonly converter: TypeConverter; readonly resource: Resource; readonly resourceClass: ClassType; + readonly relationshipDecider: RelationshipDecider; } /** @@ -23,6 +26,7 @@ export class TypeDefinitionStruct extends StructType { private readonly converter: TypeConverter; private readonly resource: Resource; private readonly module: Module; + private readonly relationshipDecider: RelationshipDecider; constructor(options: TypeDefinitionStructOptions) { super(options.resourceClass, { @@ -41,6 +45,7 @@ export class TypeDefinitionStruct extends StructType { this.typeDefinition = options.typeDefinition; this.converter = options.converter; this.resource = options.resource; + this.relationshipDecider = options.relationshipDecider; this.module = Module.of(this); } @@ -48,13 +53,43 @@ export class TypeDefinitionStruct extends StructType { public build() { const cfnMapping = new CloudFormationMapping(this.module, this.converter); - const decider = new TypeDefinitionDecider(this.resource, this.typeDefinition, this.converter); + const decider = new TypeDefinitionDecider(this.resource, this.typeDefinition, this.converter, this.relationshipDecider); for (const prop of decider.properties) { this.addProperty(prop.propertySpec); cfnMapping.add(prop.cfnMapping); } + let needsResolverFunction = false; + for (const [propName, prop] of Object.entries(this.typeDefinition.properties)) { + needsResolverFunction = needsResolverFunction + ? needsResolverFunction + : this.relationshipDecider.needsFlatteningFunction(propName, prop); + } + + if (needsResolverFunction) { + const resolverFunction = new FreeFunction(this.module, { + name: flattenFunctionNameFromType(this), + returnType: Type.unionOf(this.type, CDK_CORE.IResolvable), + parameters: [{ name: 'props', type: Type.unionOf(this.type, CDK_CORE.IResolvable) }], + }); + + const propsParam = resolverFunction.parameters[0]; + resolverFunction.addBody( + stmt.if_(CDK_CORE.isResolvableObject(propsParam)) + .then(stmt.ret(propsParam)), + + stmt.ret(expr.object( + Object.fromEntries( + decider.properties.map(prop => [ + prop.propertySpec.name, + prop.resolver(propsParam), + ]), + ), + )), + ); + } + cfnMapping.makeCfnProducer(this.module, this); cfnMapping.makeCfnParser(this.module, this); } diff --git a/tools/@aws-cdk/spec2cdk/lib/naming/conventions.ts b/tools/@aws-cdk/spec2cdk/lib/naming/conventions.ts index e290eb1ae1cdf..e0b69e5251bdb 100644 --- a/tools/@aws-cdk/spec2cdk/lib/naming/conventions.ts +++ b/tools/@aws-cdk/spec2cdk/lib/naming/conventions.ts @@ -63,6 +63,17 @@ export function interfaceNameFromResource(res: Resource, suffix?: string) { return `I${classNameFromResource(res, suffix)}`; } +export function namespaceFromResource(res: Resource) { + return res.cloudFormationType.split('::').slice(0, 2).join('::'); +} + +/** + * Get the AWS namespace prefix from a resource in PascalCase for use as a type alias prefix. + */ +export function typeAliasPrefixFromResource(res: Resource) { + return camelcase(res.cloudFormationType.split('::')[1], { pascalCase: true }); +} + export function cfnProducerNameFromType(struct: TypeDeclaration) { return `convert${qualifiedName(struct)}ToCloudFormation`; } @@ -75,6 +86,10 @@ export function cfnPropsValidatorNameFromType(struct: TypeDeclaration) { return `${qualifiedName(struct)}Validator`; } +export function flattenFunctionNameFromType(struct: TypeDeclaration) { + return `flatten${qualifiedName(struct)}`; +} + export function metricsClassNameFromService(namespace: string) { return `${namespace.replace(/^AWS\//, '').replace('/', '')}Metrics`; } @@ -110,6 +125,14 @@ export function referencePropertyName(propName: string, resourceName: string) { return camelcase(propName); } +export function referenceInterfaceName(resourceName: string, suffix?: string) { + return `I${resourceName}${suffix ?? ''}Ref`; +} + +export function referenceInterfaceAttributeName(resourceName: string) { + return `${camelcase(resourceName)}Ref`; +} + /** * Generate a name for the given declaration so that we can generate helper symbols for it that won't class * diff --git a/tools/@aws-cdk/spec2cdk/test/__snapshots__/relationships.test.ts.snap b/tools/@aws-cdk/spec2cdk/test/__snapshots__/relationships.test.ts.snap new file mode 100644 index 0000000000000..a1f1b2c0dd21b --- /dev/null +++ b/tools/@aws-cdk/spec2cdk/test/__snapshots__/relationships.test.ts.snap @@ -0,0 +1,1992 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`resource with array of nested properties with relationship 1`] = ` +"/* eslint-disable prettier/prettier, @stylistic/max-len */ +import * as cdk from "aws-cdk-lib"; +import * as constructs from "constructs"; +import * as cfn_parse from "aws-cdk-lib/core/lib/helpers-internal"; +import * as cdk_errors from "aws-cdk-lib/core/lib/errors"; + +/** + * Indicates that this resource can be referenced as a Role. + * + * @stability experimental + */ +export interface IRoleRef extends constructs.IConstruct { + /** + * A reference to a Role resource. + */ + readonly roleRef: RoleReference; +} + +/** + * @cloudformationResource AWS::IAM::Role + * @stability external + * @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iam-role.html + */ +export class CfnRole extends cdk.CfnResource implements cdk.IInspectable, IRoleRef, cdk.ITaggable { + /** + * The CloudFormation resource type name for this resource class. + */ + public static readonly CFN_RESOURCE_TYPE_NAME: string = "AWS::IAM::Role"; + + /** + * Build a CfnRole from CloudFormation properties + * + * A factory method that creates a new instance of this class from an object + * containing the CloudFormation properties of this resource. + * Used in the @aws-cdk/cloudformation-include module. + * + * @internal + */ + public static _fromCloudFormation(scope: constructs.Construct, id: string, resourceAttributes: any, options: cfn_parse.FromCloudFormationOptions): CfnRole { + resourceAttributes = resourceAttributes || {}; + const resourceProperties = options.parser.parseValue(resourceAttributes.Properties); + const propsResult = CfnRolePropsFromCloudFormation(resourceProperties); + if (cdk.isResolvableObject(propsResult.value)) { + throw new cdk_errors.ValidationError("Unexpected IResolvable", scope); + } + const ret = new CfnRole(scope, id, propsResult.value); + for (const [propKey, propVal] of Object.entries(propsResult.extraProperties)) { + ret.addPropertyOverride(propKey, propVal); + } + options.parser.handleAttributes(ret, resourceAttributes, id); + return ret; + } + + /** + * @cloudformationAttribute RoleArn + */ + public readonly attrRoleArn: string; + + /** + * @param scope Scope in which this resource is defined + * @param id Construct identifier for this resource (unique in its scope) + * @param props Resource properties + */ + public constructor(scope: constructs.Construct, id: string, props: CfnRoleProps = {}) { + super(scope, id, { + "type": CfnRole.CFN_RESOURCE_TYPE_NAME, + "properties": props + }); + + this.attrRoleArn = cdk.Token.asString(this.getAtt("RoleArn", cdk.ResolutionTypeHint.STRING)); + } + + public get roleRef(): RoleReference { + return { + "roleArn": this.attrRoleArn + }; + } + + protected get cfnProperties(): Record { + return {}; + } + + /** + * Examines the CloudFormation resource and discloses attributes + * + * @param inspector tree inspector to collect and process attributes + */ + public inspect(inspector: cdk.TreeInspector): void { + inspector.addAttribute("aws:cdk:cloudformation:type", CfnRole.CFN_RESOURCE_TYPE_NAME); + inspector.addAttribute("aws:cdk:cloudformation:props", this.cfnProperties); + } + + protected renderProperties(props: Record): Record { + return convertCfnRolePropsToCloudFormation(props); + } +} + +/** + * Properties for defining a \`CfnRole\` + * + * @struct + * @stability external + * @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iam-role.html + */ +export interface CfnRoleProps { + +} + +/** + * A reference to a Role resource. + * + * @struct + * @stability external + */ +export interface RoleReference { + /** + * The ARN of the Role resource. + */ + readonly roleArn: string; +} + +/** + * Determine whether the given properties match those of a \`CfnRoleProps\` + * + * @param properties - the TypeScript properties of a \`CfnRoleProps\` + * + * @returns the result of the validation. + */ +// @ts-ignore TS6133 +function CfnRolePropsValidator(properties: any): cdk.ValidationResult { + if (!cdk.canInspect(properties)) return cdk.VALIDATION_SUCCESS; + const errors = new cdk.ValidationResults(); + if (!(properties && typeof properties == 'object' && !Array.isArray(properties))) { + errors.collect(new cdk.ValidationResult("Expected an object, but received: " + JSON.stringify(properties))); + } + return errors.wrap("supplied properties not correct for \\"CfnRoleProps\\""); +} + +// @ts-ignore TS6133 +function convertCfnRolePropsToCloudFormation(properties: any): any { + if (!cdk.canInspect(properties)) return properties; + CfnRolePropsValidator(properties).assertSuccess(); + return {}; +} + +// @ts-ignore TS6133 +function CfnRolePropsFromCloudFormation(properties: any): cfn_parse.FromCloudFormationResult { + if (cdk.isResolvableObject(properties)) { + return new cfn_parse.FromCloudFormationResult(properties); + } + properties = ((properties == null) ? {} : properties); + if (!(properties && typeof properties == 'object' && !Array.isArray(properties))) { + return new cfn_parse.FromCloudFormationResult(properties); + } + const ret = new cfn_parse.FromCloudFormationPropertyObject(); + ret.addUnrecognizedPropertiesAsExtra(properties); + return ret; +} + +/** + * Indicates that this resource can be referenced as a Resource. + * + * @stability experimental + */ +export interface IResourceRef extends constructs.IConstruct { + /** + * A reference to a Resource resource. + */ + readonly resourceRef: ResourceReference; +} + +/** + * @cloudformationResource AWS::IAM::Resource + * @stability external + * @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iam-resource.html + */ +export class CfnResource extends cdk.CfnResource implements cdk.IInspectable, IResourceRef { + /** + * The CloudFormation resource type name for this resource class. + */ + public static readonly CFN_RESOURCE_TYPE_NAME: string = "AWS::IAM::Resource"; + + /** + * Build a CfnResource from CloudFormation properties + * + * A factory method that creates a new instance of this class from an object + * containing the CloudFormation properties of this resource. + * Used in the @aws-cdk/cloudformation-include module. + * + * @internal + */ + public static _fromCloudFormation(scope: constructs.Construct, id: string, resourceAttributes: any, options: cfn_parse.FromCloudFormationOptions): CfnResource { + resourceAttributes = resourceAttributes || {}; + const resourceProperties = options.parser.parseValue(resourceAttributes.Properties); + const propsResult = CfnResourcePropsFromCloudFormation(resourceProperties); + if (cdk.isResolvableObject(propsResult.value)) { + throw new cdk_errors.ValidationError("Unexpected IResolvable", scope); + } + const ret = new CfnResource(scope, id, propsResult.value); + for (const [propKey, propVal] of Object.entries(propsResult.extraProperties)) { + ret.addPropertyOverride(propKey, propVal); + } + options.parser.handleAttributes(ret, resourceAttributes, id); + return ret; + } + + public permissions?: Array | cdk.IResolvable; + + /** + * @param scope Scope in which this resource is defined + * @param id Construct identifier for this resource (unique in its scope) + * @param props Resource properties + */ + public constructor(scope: constructs.Construct, id: string, props: CfnResourceProps = {}) { + super(scope, id, { + "type": CfnResource.CFN_RESOURCE_TYPE_NAME, + "properties": props + }); + + this.permissions = (cdk.isResolvableObject(props.permissions) ? props.permissions : (props.permissions ? props.permissions.map(flattenCfnResourcePermissionProperty) : undefined)); + } + + public get resourceRef(): ResourceReference { + return {}; + } + + protected get cfnProperties(): Record { + return { + "permissions": this.permissions + }; + } + + /** + * Examines the CloudFormation resource and discloses attributes + * + * @param inspector tree inspector to collect and process attributes + */ + public inspect(inspector: cdk.TreeInspector): void { + inspector.addAttribute("aws:cdk:cloudformation:type", CfnResource.CFN_RESOURCE_TYPE_NAME); + inspector.addAttribute("aws:cdk:cloudformation:props", this.cfnProperties); + } + + protected renderProperties(props: Record): Record { + return convertCfnResourcePropsToCloudFormation(props); + } +} + +export namespace CfnResource { + /** + * @struct + * @stability external + * @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iam-resource-permission.html + */ + export interface PermissionProperty { + /** + * @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iam-resource-permission.html#cfn-iam-resource-permission-rolearn + */ + readonly roleArn?: IRoleRef | string; + } +} + +/** + * Properties for defining a \`CfnResource\` + * + * @struct + * @stability external + * @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iam-resource.html + */ +export interface CfnResourceProps { + /** + * @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iam-resource.html#cfn-iam-resource-permissions + */ + readonly permissions?: Array | cdk.IResolvable; +} + +// @ts-ignore TS6133 +function flattenCfnResourcePermissionProperty(props: cdk.IResolvable | CfnResource.PermissionProperty): cdk.IResolvable | CfnResource.PermissionProperty { + if (cdk.isResolvableObject(props)) return props; + return { + "roleArn": (props.roleArn as IRoleRef)?.roleRef?.roleArn ?? props.roleArn + }; +} + +/** + * Determine whether the given properties match those of a \`PermissionProperty\` + * + * @param properties - the TypeScript properties of a \`PermissionProperty\` + * + * @returns the result of the validation. + */ +// @ts-ignore TS6133 +function CfnResourcePermissionPropertyValidator(properties: any): cdk.ValidationResult { + if (!cdk.canInspect(properties)) return cdk.VALIDATION_SUCCESS; + const errors = new cdk.ValidationResults(); + if (!(properties && typeof properties == 'object' && !Array.isArray(properties))) { + errors.collect(new cdk.ValidationResult("Expected an object, but received: " + JSON.stringify(properties))); + } + errors.collect(cdk.propertyValidator("roleArn", cdk.validateString)(properties.roleArn)); + return errors.wrap("supplied properties not correct for \\"PermissionProperty\\""); +} + +// @ts-ignore TS6133 +function convertCfnResourcePermissionPropertyToCloudFormation(properties: any): any { + if (!cdk.canInspect(properties)) return properties; + CfnResourcePermissionPropertyValidator(properties).assertSuccess(); + return { + "RoleArn": cdk.stringToCloudFormation(properties.roleArn) + }; +} + +// @ts-ignore TS6133 +function CfnResourcePermissionPropertyFromCloudFormation(properties: any): cfn_parse.FromCloudFormationResult { + if (cdk.isResolvableObject(properties)) { + return new cfn_parse.FromCloudFormationResult(properties); + } + properties = ((properties == null) ? {} : properties); + if (!(properties && typeof properties == 'object' && !Array.isArray(properties))) { + return new cfn_parse.FromCloudFormationResult(properties); + } + const ret = new cfn_parse.FromCloudFormationPropertyObject(); + ret.addPropertyResult("roleArn", "RoleArn", (properties.RoleArn != null ? cfn_parse.FromCloudFormation.getString(properties.RoleArn) : undefined)); + ret.addUnrecognizedPropertiesAsExtra(properties); + return ret; +} + +/** + * A reference to a Resource resource. + * + * @struct + * @stability external + */ +export interface ResourceReference { + +} + +/** + * Determine whether the given properties match those of a \`CfnResourceProps\` + * + * @param properties - the TypeScript properties of a \`CfnResourceProps\` + * + * @returns the result of the validation. + */ +// @ts-ignore TS6133 +function CfnResourcePropsValidator(properties: any): cdk.ValidationResult { + if (!cdk.canInspect(properties)) return cdk.VALIDATION_SUCCESS; + const errors = new cdk.ValidationResults(); + if (!(properties && typeof properties == 'object' && !Array.isArray(properties))) { + errors.collect(new cdk.ValidationResult("Expected an object, but received: " + JSON.stringify(properties))); + } + errors.collect(cdk.propertyValidator("permissions", cdk.listValidator(CfnResourcePermissionPropertyValidator))(properties.permissions)); + return errors.wrap("supplied properties not correct for \\"CfnResourceProps\\""); +} + +// @ts-ignore TS6133 +function convertCfnResourcePropsToCloudFormation(properties: any): any { + if (!cdk.canInspect(properties)) return properties; + CfnResourcePropsValidator(properties).assertSuccess(); + return { + "Permissions": cdk.listMapper(convertCfnResourcePermissionPropertyToCloudFormation)(properties.permissions) + }; +} + +// @ts-ignore TS6133 +function CfnResourcePropsFromCloudFormation(properties: any): cfn_parse.FromCloudFormationResult { + if (cdk.isResolvableObject(properties)) { + return new cfn_parse.FromCloudFormationResult(properties); + } + properties = ((properties == null) ? {} : properties); + if (!(properties && typeof properties == 'object' && !Array.isArray(properties))) { + return new cfn_parse.FromCloudFormationResult(properties); + } + const ret = new cfn_parse.FromCloudFormationPropertyObject(); + ret.addPropertyResult("permissions", "Permissions", (properties.Permissions != null ? cfn_parse.FromCloudFormation.getArray(CfnResourcePermissionPropertyFromCloudFormation)(properties.Permissions) : undefined)); + ret.addUnrecognizedPropertiesAsExtra(properties); + return ret; +}" +`; + +exports[`resource with multiple relationship references 1`] = ` +"/* eslint-disable prettier/prettier, @stylistic/max-len */ +import * as cdk from "aws-cdk-lib"; +import * as constructs from "constructs"; +import * as cfn_parse from "aws-cdk-lib/core/lib/helpers-internal"; +import * as cdk_errors from "aws-cdk-lib/core/lib/errors"; + +/** + * Indicates that this resource can be referenced as a Role. + * + * @stability experimental + */ +export interface IRoleRef extends constructs.IConstruct { + /** + * A reference to a Role resource. + */ + readonly roleRef: RoleReference; +} + +/** + * @cloudformationResource AWS::IAM::Role + * @stability external + * @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iam-role.html + */ +export class CfnRole extends cdk.CfnResource implements cdk.IInspectable, IRoleRef, cdk.ITaggable { + /** + * The CloudFormation resource type name for this resource class. + */ + public static readonly CFN_RESOURCE_TYPE_NAME: string = "AWS::IAM::Role"; + + /** + * Build a CfnRole from CloudFormation properties + * + * A factory method that creates a new instance of this class from an object + * containing the CloudFormation properties of this resource. + * Used in the @aws-cdk/cloudformation-include module. + * + * @internal + */ + public static _fromCloudFormation(scope: constructs.Construct, id: string, resourceAttributes: any, options: cfn_parse.FromCloudFormationOptions): CfnRole { + resourceAttributes = resourceAttributes || {}; + const resourceProperties = options.parser.parseValue(resourceAttributes.Properties); + const propsResult = CfnRolePropsFromCloudFormation(resourceProperties); + if (cdk.isResolvableObject(propsResult.value)) { + throw new cdk_errors.ValidationError("Unexpected IResolvable", scope); + } + const ret = new CfnRole(scope, id, propsResult.value); + for (const [propKey, propVal] of Object.entries(propsResult.extraProperties)) { + ret.addPropertyOverride(propKey, propVal); + } + options.parser.handleAttributes(ret, resourceAttributes, id); + return ret; + } + + /** + * @cloudformationAttribute RoleArn + */ + public readonly attrRoleArn: string; + + /** + * @param scope Scope in which this resource is defined + * @param id Construct identifier for this resource (unique in its scope) + * @param props Resource properties + */ + public constructor(scope: constructs.Construct, id: string, props: CfnRoleProps = {}) { + super(scope, id, { + "type": CfnRole.CFN_RESOURCE_TYPE_NAME, + "properties": props + }); + + this.attrRoleArn = cdk.Token.asString(this.getAtt("RoleArn", cdk.ResolutionTypeHint.STRING)); + } + + public get roleRef(): RoleReference { + return { + "roleArn": this.attrRoleArn + }; + } + + protected get cfnProperties(): Record { + return {}; + } + + /** + * Examines the CloudFormation resource and discloses attributes + * + * @param inspector tree inspector to collect and process attributes + */ + public inspect(inspector: cdk.TreeInspector): void { + inspector.addAttribute("aws:cdk:cloudformation:type", CfnRole.CFN_RESOURCE_TYPE_NAME); + inspector.addAttribute("aws:cdk:cloudformation:props", this.cfnProperties); + } + + protected renderProperties(props: Record): Record { + return convertCfnRolePropsToCloudFormation(props); + } +} + +/** + * Properties for defining a \`CfnRole\` + * + * @struct + * @stability external + * @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iam-role.html + */ +export interface CfnRoleProps { + +} + +/** + * A reference to a Role resource. + * + * @struct + * @stability external + */ +export interface RoleReference { + /** + * The ARN of the Role resource. + */ + readonly roleArn: string; +} + +/** + * Determine whether the given properties match those of a \`CfnRoleProps\` + * + * @param properties - the TypeScript properties of a \`CfnRoleProps\` + * + * @returns the result of the validation. + */ +// @ts-ignore TS6133 +function CfnRolePropsValidator(properties: any): cdk.ValidationResult { + if (!cdk.canInspect(properties)) return cdk.VALIDATION_SUCCESS; + const errors = new cdk.ValidationResults(); + if (!(properties && typeof properties == 'object' && !Array.isArray(properties))) { + errors.collect(new cdk.ValidationResult("Expected an object, but received: " + JSON.stringify(properties))); + } + return errors.wrap("supplied properties not correct for \\"CfnRoleProps\\""); +} + +// @ts-ignore TS6133 +function convertCfnRolePropsToCloudFormation(properties: any): any { + if (!cdk.canInspect(properties)) return properties; + CfnRolePropsValidator(properties).assertSuccess(); + return {}; +} + +// @ts-ignore TS6133 +function CfnRolePropsFromCloudFormation(properties: any): cfn_parse.FromCloudFormationResult { + if (cdk.isResolvableObject(properties)) { + return new cfn_parse.FromCloudFormationResult(properties); + } + properties = ((properties == null) ? {} : properties); + if (!(properties && typeof properties == 'object' && !Array.isArray(properties))) { + return new cfn_parse.FromCloudFormationResult(properties); + } + const ret = new cfn_parse.FromCloudFormationPropertyObject(); + ret.addUnrecognizedPropertiesAsExtra(properties); + return ret; +} + +/** + * Indicates that this resource can be referenced as a User. + * + * @stability experimental + */ +export interface IUserRef extends constructs.IConstruct { + /** + * A reference to a User resource. + */ + readonly userRef: UserReference; +} + +/** + * @cloudformationResource AWS::IAM::User + * @stability external + * @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iam-user.html + */ +export class CfnUser extends cdk.CfnResource implements cdk.IInspectable, IUserRef, cdk.ITaggable { + /** + * The CloudFormation resource type name for this resource class. + */ + public static readonly CFN_RESOURCE_TYPE_NAME: string = "AWS::IAM::User"; + + /** + * Build a CfnUser from CloudFormation properties + * + * A factory method that creates a new instance of this class from an object + * containing the CloudFormation properties of this resource. + * Used in the @aws-cdk/cloudformation-include module. + * + * @internal + */ + public static _fromCloudFormation(scope: constructs.Construct, id: string, resourceAttributes: any, options: cfn_parse.FromCloudFormationOptions): CfnUser { + resourceAttributes = resourceAttributes || {}; + const resourceProperties = options.parser.parseValue(resourceAttributes.Properties); + const propsResult = CfnUserPropsFromCloudFormation(resourceProperties); + if (cdk.isResolvableObject(propsResult.value)) { + throw new cdk_errors.ValidationError("Unexpected IResolvable", scope); + } + const ret = new CfnUser(scope, id, propsResult.value); + for (const [propKey, propVal] of Object.entries(propsResult.extraProperties)) { + ret.addPropertyOverride(propKey, propVal); + } + options.parser.handleAttributes(ret, resourceAttributes, id); + return ret; + } + + /** + * @cloudformationAttribute UserArn + */ + public readonly attrUserArn: string; + + /** + * @param scope Scope in which this resource is defined + * @param id Construct identifier for this resource (unique in its scope) + * @param props Resource properties + */ + public constructor(scope: constructs.Construct, id: string, props: CfnUserProps = {}) { + super(scope, id, { + "type": CfnUser.CFN_RESOURCE_TYPE_NAME, + "properties": props + }); + + this.attrUserArn = cdk.Token.asString(this.getAtt("UserArn", cdk.ResolutionTypeHint.STRING)); + } + + public get userRef(): UserReference { + return { + "userArn": this.attrUserArn + }; + } + + protected get cfnProperties(): Record { + return {}; + } + + /** + * Examines the CloudFormation resource and discloses attributes + * + * @param inspector tree inspector to collect and process attributes + */ + public inspect(inspector: cdk.TreeInspector): void { + inspector.addAttribute("aws:cdk:cloudformation:type", CfnUser.CFN_RESOURCE_TYPE_NAME); + inspector.addAttribute("aws:cdk:cloudformation:props", this.cfnProperties); + } + + protected renderProperties(props: Record): Record { + return convertCfnUserPropsToCloudFormation(props); + } +} + +/** + * Properties for defining a \`CfnUser\` + * + * @struct + * @stability external + * @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iam-user.html + */ +export interface CfnUserProps { + +} + +/** + * A reference to a User resource. + * + * @struct + * @stability external + */ +export interface UserReference { + /** + * The ARN of the User resource. + */ + readonly userArn: string; +} + +/** + * Determine whether the given properties match those of a \`CfnUserProps\` + * + * @param properties - the TypeScript properties of a \`CfnUserProps\` + * + * @returns the result of the validation. + */ +// @ts-ignore TS6133 +function CfnUserPropsValidator(properties: any): cdk.ValidationResult { + if (!cdk.canInspect(properties)) return cdk.VALIDATION_SUCCESS; + const errors = new cdk.ValidationResults(); + if (!(properties && typeof properties == 'object' && !Array.isArray(properties))) { + errors.collect(new cdk.ValidationResult("Expected an object, but received: " + JSON.stringify(properties))); + } + return errors.wrap("supplied properties not correct for \\"CfnUserProps\\""); +} + +// @ts-ignore TS6133 +function convertCfnUserPropsToCloudFormation(properties: any): any { + if (!cdk.canInspect(properties)) return properties; + CfnUserPropsValidator(properties).assertSuccess(); + return {}; +} + +// @ts-ignore TS6133 +function CfnUserPropsFromCloudFormation(properties: any): cfn_parse.FromCloudFormationResult { + if (cdk.isResolvableObject(properties)) { + return new cfn_parse.FromCloudFormationResult(properties); + } + properties = ((properties == null) ? {} : properties); + if (!(properties && typeof properties == 'object' && !Array.isArray(properties))) { + return new cfn_parse.FromCloudFormationResult(properties); + } + const ret = new cfn_parse.FromCloudFormationPropertyObject(); + ret.addUnrecognizedPropertiesAsExtra(properties); + return ret; +} + +/** + * Indicates that this resource can be referenced as a Policy. + * + * @stability experimental + */ +export interface IPolicyRef extends constructs.IConstruct { + /** + * A reference to a Policy resource. + */ + readonly policyRef: PolicyReference; +} + +/** + * @cloudformationResource AWS::IAM::Policy + * @stability external + * @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iam-policy.html + */ +export class CfnPolicy extends cdk.CfnResource implements cdk.IInspectable, IPolicyRef { + /** + * The CloudFormation resource type name for this resource class. + */ + public static readonly CFN_RESOURCE_TYPE_NAME: string = "AWS::IAM::Policy"; + + /** + * Build a CfnPolicy from CloudFormation properties + * + * A factory method that creates a new instance of this class from an object + * containing the CloudFormation properties of this resource. + * Used in the @aws-cdk/cloudformation-include module. + * + * @internal + */ + public static _fromCloudFormation(scope: constructs.Construct, id: string, resourceAttributes: any, options: cfn_parse.FromCloudFormationOptions): CfnPolicy { + resourceAttributes = resourceAttributes || {}; + const resourceProperties = options.parser.parseValue(resourceAttributes.Properties); + const propsResult = CfnPolicyPropsFromCloudFormation(resourceProperties); + if (cdk.isResolvableObject(propsResult.value)) { + throw new cdk_errors.ValidationError("Unexpected IResolvable", scope); + } + const ret = new CfnPolicy(scope, id, propsResult.value); + for (const [propKey, propVal] of Object.entries(propsResult.extraProperties)) { + ret.addPropertyOverride(propKey, propVal); + } + options.parser.handleAttributes(ret, resourceAttributes, id); + return ret; + } + + public principalArn?: string; + + /** + * @param scope Scope in which this resource is defined + * @param id Construct identifier for this resource (unique in its scope) + * @param props Resource properties + */ + public constructor(scope: constructs.Construct, id: string, props: CfnPolicyProps = {}) { + super(scope, id, { + "type": CfnPolicy.CFN_RESOURCE_TYPE_NAME, + "properties": props + }); + + this.principalArn = (props.principalArn as IRoleRef)?.roleRef?.roleArn ?? (props.principalArn as IUserRef)?.userRef?.userArn ?? props.principalArn; + } + + public get policyRef(): PolicyReference { + return {}; + } + + protected get cfnProperties(): Record { + return { + "principalArn": this.principalArn + }; + } + + /** + * Examines the CloudFormation resource and discloses attributes + * + * @param inspector tree inspector to collect and process attributes + */ + public inspect(inspector: cdk.TreeInspector): void { + inspector.addAttribute("aws:cdk:cloudformation:type", CfnPolicy.CFN_RESOURCE_TYPE_NAME); + inspector.addAttribute("aws:cdk:cloudformation:props", this.cfnProperties); + } + + protected renderProperties(props: Record): Record { + return convertCfnPolicyPropsToCloudFormation(props); + } +} + +/** + * Properties for defining a \`CfnPolicy\` + * + * @struct + * @stability external + * @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iam-policy.html + */ +export interface CfnPolicyProps { + /** + * @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iam-policy.html#cfn-iam-policy-principalarn + */ + readonly principalArn?: IRoleRef | IUserRef | string; +} + +/** + * A reference to a Policy resource. + * + * @struct + * @stability external + */ +export interface PolicyReference { + +} + +/** + * Determine whether the given properties match those of a \`CfnPolicyProps\` + * + * @param properties - the TypeScript properties of a \`CfnPolicyProps\` + * + * @returns the result of the validation. + */ +// @ts-ignore TS6133 +function CfnPolicyPropsValidator(properties: any): cdk.ValidationResult { + if (!cdk.canInspect(properties)) return cdk.VALIDATION_SUCCESS; + const errors = new cdk.ValidationResults(); + if (!(properties && typeof properties == 'object' && !Array.isArray(properties))) { + errors.collect(new cdk.ValidationResult("Expected an object, but received: " + JSON.stringify(properties))); + } + errors.collect(cdk.propertyValidator("principalArn", cdk.validateString)(properties.principalArn)); + return errors.wrap("supplied properties not correct for \\"CfnPolicyProps\\""); +} + +// @ts-ignore TS6133 +function convertCfnPolicyPropsToCloudFormation(properties: any): any { + if (!cdk.canInspect(properties)) return properties; + CfnPolicyPropsValidator(properties).assertSuccess(); + return { + "PrincipalArn": cdk.stringToCloudFormation(properties.principalArn) + }; +} + +// @ts-ignore TS6133 +function CfnPolicyPropsFromCloudFormation(properties: any): cfn_parse.FromCloudFormationResult { + if (cdk.isResolvableObject(properties)) { + return new cfn_parse.FromCloudFormationResult(properties); + } + properties = ((properties == null) ? {} : properties); + if (!(properties && typeof properties == 'object' && !Array.isArray(properties))) { + return new cfn_parse.FromCloudFormationResult(properties); + } + const ret = new cfn_parse.FromCloudFormationPropertyObject(); + ret.addPropertyResult("principalArn", "PrincipalArn", (properties.PrincipalArn != null ? cfn_parse.FromCloudFormation.getString(properties.PrincipalArn) : undefined)); + ret.addUnrecognizedPropertiesAsExtra(properties); + return ret; +}" +`; + +exports[`resource with nested relationship requiring flattening 1`] = ` +"/* eslint-disable prettier/prettier, @stylistic/max-len */ +import * as cdk from "aws-cdk-lib"; +import * as constructs from "constructs"; +import * as cfn_parse from "aws-cdk-lib/core/lib/helpers-internal"; +import * as cdk_errors from "aws-cdk-lib/core/lib/errors"; + +/** + * Indicates that this resource can be referenced as a Role. + * + * @stability experimental + */ +export interface IRoleRef extends constructs.IConstruct { + /** + * A reference to a Role resource. + */ + readonly roleRef: RoleReference; +} + +/** + * @cloudformationResource AWS::IAM::Role + * @stability external + * @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iam-role.html + */ +export class CfnRole extends cdk.CfnResource implements cdk.IInspectable, IRoleRef, cdk.ITaggable { + /** + * The CloudFormation resource type name for this resource class. + */ + public static readonly CFN_RESOURCE_TYPE_NAME: string = "AWS::IAM::Role"; + + /** + * Build a CfnRole from CloudFormation properties + * + * A factory method that creates a new instance of this class from an object + * containing the CloudFormation properties of this resource. + * Used in the @aws-cdk/cloudformation-include module. + * + * @internal + */ + public static _fromCloudFormation(scope: constructs.Construct, id: string, resourceAttributes: any, options: cfn_parse.FromCloudFormationOptions): CfnRole { + resourceAttributes = resourceAttributes || {}; + const resourceProperties = options.parser.parseValue(resourceAttributes.Properties); + const propsResult = CfnRolePropsFromCloudFormation(resourceProperties); + if (cdk.isResolvableObject(propsResult.value)) { + throw new cdk_errors.ValidationError("Unexpected IResolvable", scope); + } + const ret = new CfnRole(scope, id, propsResult.value); + for (const [propKey, propVal] of Object.entries(propsResult.extraProperties)) { + ret.addPropertyOverride(propKey, propVal); + } + options.parser.handleAttributes(ret, resourceAttributes, id); + return ret; + } + + /** + * @cloudformationAttribute RoleArn + */ + public readonly attrRoleArn: string; + + /** + * @param scope Scope in which this resource is defined + * @param id Construct identifier for this resource (unique in its scope) + * @param props Resource properties + */ + public constructor(scope: constructs.Construct, id: string, props: CfnRoleProps = {}) { + super(scope, id, { + "type": CfnRole.CFN_RESOURCE_TYPE_NAME, + "properties": props + }); + + this.attrRoleArn = cdk.Token.asString(this.getAtt("RoleArn", cdk.ResolutionTypeHint.STRING)); + } + + public get roleRef(): RoleReference { + return { + "roleArn": this.attrRoleArn + }; + } + + protected get cfnProperties(): Record { + return {}; + } + + /** + * Examines the CloudFormation resource and discloses attributes + * + * @param inspector tree inspector to collect and process attributes + */ + public inspect(inspector: cdk.TreeInspector): void { + inspector.addAttribute("aws:cdk:cloudformation:type", CfnRole.CFN_RESOURCE_TYPE_NAME); + inspector.addAttribute("aws:cdk:cloudformation:props", this.cfnProperties); + } + + protected renderProperties(props: Record): Record { + return convertCfnRolePropsToCloudFormation(props); + } +} + +/** + * Properties for defining a \`CfnRole\` + * + * @struct + * @stability external + * @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iam-role.html + */ +export interface CfnRoleProps { + +} + +/** + * A reference to a Role resource. + * + * @struct + * @stability external + */ +export interface RoleReference { + /** + * The ARN of the Role resource. + */ + readonly roleArn: string; +} + +/** + * Determine whether the given properties match those of a \`CfnRoleProps\` + * + * @param properties - the TypeScript properties of a \`CfnRoleProps\` + * + * @returns the result of the validation. + */ +// @ts-ignore TS6133 +function CfnRolePropsValidator(properties: any): cdk.ValidationResult { + if (!cdk.canInspect(properties)) return cdk.VALIDATION_SUCCESS; + const errors = new cdk.ValidationResults(); + if (!(properties && typeof properties == 'object' && !Array.isArray(properties))) { + errors.collect(new cdk.ValidationResult("Expected an object, but received: " + JSON.stringify(properties))); + } + return errors.wrap("supplied properties not correct for \\"CfnRoleProps\\""); +} + +// @ts-ignore TS6133 +function convertCfnRolePropsToCloudFormation(properties: any): any { + if (!cdk.canInspect(properties)) return properties; + CfnRolePropsValidator(properties).assertSuccess(); + return {}; +} + +// @ts-ignore TS6133 +function CfnRolePropsFromCloudFormation(properties: any): cfn_parse.FromCloudFormationResult { + if (cdk.isResolvableObject(properties)) { + return new cfn_parse.FromCloudFormationResult(properties); + } + properties = ((properties == null) ? {} : properties); + if (!(properties && typeof properties == 'object' && !Array.isArray(properties))) { + return new cfn_parse.FromCloudFormationResult(properties); + } + const ret = new cfn_parse.FromCloudFormationPropertyObject(); + ret.addUnrecognizedPropertiesAsExtra(properties); + return ret; +} + +/** + * Indicates that this resource can be referenced as a Task. + * + * @stability experimental + */ +export interface ITaskRef extends constructs.IConstruct { + /** + * A reference to a Task resource. + */ + readonly taskRef: TaskReference; +} + +/** + * @cloudformationResource AWS::IAM::Task + * @stability external + * @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iam-task.html + */ +export class CfnTask extends cdk.CfnResource implements cdk.IInspectable, ITaskRef { + /** + * The CloudFormation resource type name for this resource class. + */ + public static readonly CFN_RESOURCE_TYPE_NAME: string = "AWS::IAM::Task"; + + /** + * Build a CfnTask from CloudFormation properties + * + * A factory method that creates a new instance of this class from an object + * containing the CloudFormation properties of this resource. + * Used in the @aws-cdk/cloudformation-include module. + * + * @internal + */ + public static _fromCloudFormation(scope: constructs.Construct, id: string, resourceAttributes: any, options: cfn_parse.FromCloudFormationOptions): CfnTask { + resourceAttributes = resourceAttributes || {}; + const resourceProperties = options.parser.parseValue(resourceAttributes.Properties); + const propsResult = CfnTaskPropsFromCloudFormation(resourceProperties); + if (cdk.isResolvableObject(propsResult.value)) { + throw new cdk_errors.ValidationError("Unexpected IResolvable", scope); + } + const ret = new CfnTask(scope, id, propsResult.value); + for (const [propKey, propVal] of Object.entries(propsResult.extraProperties)) { + ret.addPropertyOverride(propKey, propVal); + } + options.parser.handleAttributes(ret, resourceAttributes, id); + return ret; + } + + public executionConfig?: CfnTask.ExecutionConfigProperty | cdk.IResolvable; + + /** + * @param scope Scope in which this resource is defined + * @param id Construct identifier for this resource (unique in its scope) + * @param props Resource properties + */ + public constructor(scope: constructs.Construct, id: string, props: CfnTaskProps = {}) { + super(scope, id, { + "type": CfnTask.CFN_RESOURCE_TYPE_NAME, + "properties": props + }); + + this.executionConfig = (props.executionConfig ? flattenCfnTaskExecutionConfigProperty(props.executionConfig) : undefined); + } + + public get taskRef(): TaskReference { + return {}; + } + + protected get cfnProperties(): Record { + return { + "executionConfig": this.executionConfig + }; + } + + /** + * Examines the CloudFormation resource and discloses attributes + * + * @param inspector tree inspector to collect and process attributes + */ + public inspect(inspector: cdk.TreeInspector): void { + inspector.addAttribute("aws:cdk:cloudformation:type", CfnTask.CFN_RESOURCE_TYPE_NAME); + inspector.addAttribute("aws:cdk:cloudformation:props", this.cfnProperties); + } + + protected renderProperties(props: Record): Record { + return convertCfnTaskPropsToCloudFormation(props); + } +} + +export namespace CfnTask { + /** + * @struct + * @stability external + * @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iam-task-executionconfig.html + */ + export interface ExecutionConfigProperty { + /** + * @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iam-task-executionconfig.html#cfn-iam-task-executionconfig-rolearn + */ + readonly roleArn?: IRoleRef | string; + } +} + +/** + * Properties for defining a \`CfnTask\` + * + * @struct + * @stability external + * @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iam-task.html + */ +export interface CfnTaskProps { + /** + * @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iam-task.html#cfn-iam-task-executionconfig + */ + readonly executionConfig?: CfnTask.ExecutionConfigProperty | cdk.IResolvable; +} + +// @ts-ignore TS6133 +function flattenCfnTaskExecutionConfigProperty(props: CfnTask.ExecutionConfigProperty | cdk.IResolvable): CfnTask.ExecutionConfigProperty | cdk.IResolvable { + if (cdk.isResolvableObject(props)) return props; + return { + "roleArn": (props.roleArn as IRoleRef)?.roleRef?.roleArn ?? props.roleArn + }; +} + +/** + * Determine whether the given properties match those of a \`ExecutionConfigProperty\` + * + * @param properties - the TypeScript properties of a \`ExecutionConfigProperty\` + * + * @returns the result of the validation. + */ +// @ts-ignore TS6133 +function CfnTaskExecutionConfigPropertyValidator(properties: any): cdk.ValidationResult { + if (!cdk.canInspect(properties)) return cdk.VALIDATION_SUCCESS; + const errors = new cdk.ValidationResults(); + if (!(properties && typeof properties == 'object' && !Array.isArray(properties))) { + errors.collect(new cdk.ValidationResult("Expected an object, but received: " + JSON.stringify(properties))); + } + errors.collect(cdk.propertyValidator("roleArn", cdk.validateString)(properties.roleArn)); + return errors.wrap("supplied properties not correct for \\"ExecutionConfigProperty\\""); +} + +// @ts-ignore TS6133 +function convertCfnTaskExecutionConfigPropertyToCloudFormation(properties: any): any { + if (!cdk.canInspect(properties)) return properties; + CfnTaskExecutionConfigPropertyValidator(properties).assertSuccess(); + return { + "RoleArn": cdk.stringToCloudFormation(properties.roleArn) + }; +} + +// @ts-ignore TS6133 +function CfnTaskExecutionConfigPropertyFromCloudFormation(properties: any): cfn_parse.FromCloudFormationResult { + if (cdk.isResolvableObject(properties)) { + return new cfn_parse.FromCloudFormationResult(properties); + } + properties = ((properties == null) ? {} : properties); + if (!(properties && typeof properties == 'object' && !Array.isArray(properties))) { + return new cfn_parse.FromCloudFormationResult(properties); + } + const ret = new cfn_parse.FromCloudFormationPropertyObject(); + ret.addPropertyResult("roleArn", "RoleArn", (properties.RoleArn != null ? cfn_parse.FromCloudFormation.getString(properties.RoleArn) : undefined)); + ret.addUnrecognizedPropertiesAsExtra(properties); + return ret; +} + +/** + * A reference to a Task resource. + * + * @struct + * @stability external + */ +export interface TaskReference { + +} + +/** + * Determine whether the given properties match those of a \`CfnTaskProps\` + * + * @param properties - the TypeScript properties of a \`CfnTaskProps\` + * + * @returns the result of the validation. + */ +// @ts-ignore TS6133 +function CfnTaskPropsValidator(properties: any): cdk.ValidationResult { + if (!cdk.canInspect(properties)) return cdk.VALIDATION_SUCCESS; + const errors = new cdk.ValidationResults(); + if (!(properties && typeof properties == 'object' && !Array.isArray(properties))) { + errors.collect(new cdk.ValidationResult("Expected an object, but received: " + JSON.stringify(properties))); + } + errors.collect(cdk.propertyValidator("executionConfig", CfnTaskExecutionConfigPropertyValidator)(properties.executionConfig)); + return errors.wrap("supplied properties not correct for \\"CfnTaskProps\\""); +} + +// @ts-ignore TS6133 +function convertCfnTaskPropsToCloudFormation(properties: any): any { + if (!cdk.canInspect(properties)) return properties; + CfnTaskPropsValidator(properties).assertSuccess(); + return { + "ExecutionConfig": convertCfnTaskExecutionConfigPropertyToCloudFormation(properties.executionConfig) + }; +} + +// @ts-ignore TS6133 +function CfnTaskPropsFromCloudFormation(properties: any): cfn_parse.FromCloudFormationResult { + if (cdk.isResolvableObject(properties)) { + return new cfn_parse.FromCloudFormationResult(properties); + } + properties = ((properties == null) ? {} : properties); + if (!(properties && typeof properties == 'object' && !Array.isArray(properties))) { + return new cfn_parse.FromCloudFormationResult(properties); + } + const ret = new cfn_parse.FromCloudFormationPropertyObject(); + ret.addPropertyResult("executionConfig", "ExecutionConfig", (properties.ExecutionConfig != null ? CfnTaskExecutionConfigPropertyFromCloudFormation(properties.ExecutionConfig) : undefined)); + ret.addUnrecognizedPropertiesAsExtra(properties); + return ret; +}" +`; + +exports[`resource with nested relationship with type history 1`] = ` +"/* eslint-disable prettier/prettier, @stylistic/max-len */ +import * as cdk from "aws-cdk-lib"; +import * as constructs from "constructs"; +import * as cfn_parse from "aws-cdk-lib/core/lib/helpers-internal"; +import * as cdk_errors from "aws-cdk-lib/core/lib/errors"; + +/** + * Indicates that this resource can be referenced as a Role. + * + * @stability experimental + */ +export interface IRoleRef extends constructs.IConstruct { + /** + * A reference to a Role resource. + */ + readonly roleRef: RoleReference; +} + +/** + * @cloudformationResource AWS::IAM::Role + * @stability external + * @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iam-role.html + */ +export class CfnRole extends cdk.CfnResource implements cdk.IInspectable, IRoleRef, cdk.ITaggable { + /** + * The CloudFormation resource type name for this resource class. + */ + public static readonly CFN_RESOURCE_TYPE_NAME: string = "AWS::IAM::Role"; + + /** + * Build a CfnRole from CloudFormation properties + * + * A factory method that creates a new instance of this class from an object + * containing the CloudFormation properties of this resource. + * Used in the @aws-cdk/cloudformation-include module. + * + * @internal + */ + public static _fromCloudFormation(scope: constructs.Construct, id: string, resourceAttributes: any, options: cfn_parse.FromCloudFormationOptions): CfnRole { + resourceAttributes = resourceAttributes || {}; + const resourceProperties = options.parser.parseValue(resourceAttributes.Properties); + const propsResult = CfnRolePropsFromCloudFormation(resourceProperties); + if (cdk.isResolvableObject(propsResult.value)) { + throw new cdk_errors.ValidationError("Unexpected IResolvable", scope); + } + const ret = new CfnRole(scope, id, propsResult.value); + for (const [propKey, propVal] of Object.entries(propsResult.extraProperties)) { + ret.addPropertyOverride(propKey, propVal); + } + options.parser.handleAttributes(ret, resourceAttributes, id); + return ret; + } + + /** + * @cloudformationAttribute RoleArn + */ + public readonly attrRoleArn: string; + + /** + * @param scope Scope in which this resource is defined + * @param id Construct identifier for this resource (unique in its scope) + * @param props Resource properties + */ + public constructor(scope: constructs.Construct, id: string, props: CfnRoleProps = {}) { + super(scope, id, { + "type": CfnRole.CFN_RESOURCE_TYPE_NAME, + "properties": props + }); + + this.attrRoleArn = cdk.Token.asString(this.getAtt("RoleArn", cdk.ResolutionTypeHint.STRING)); + } + + public get roleRef(): RoleReference { + return { + "roleArn": this.attrRoleArn + }; + } + + protected get cfnProperties(): Record { + return {}; + } + + /** + * Examines the CloudFormation resource and discloses attributes + * + * @param inspector tree inspector to collect and process attributes + */ + public inspect(inspector: cdk.TreeInspector): void { + inspector.addAttribute("aws:cdk:cloudformation:type", CfnRole.CFN_RESOURCE_TYPE_NAME); + inspector.addAttribute("aws:cdk:cloudformation:props", this.cfnProperties); + } + + protected renderProperties(props: Record): Record { + return convertCfnRolePropsToCloudFormation(props); + } +} + +/** + * Properties for defining a \`CfnRole\` + * + * @struct + * @stability external + * @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iam-role.html + */ +export interface CfnRoleProps { + +} + +/** + * A reference to a Role resource. + * + * @struct + * @stability external + */ +export interface RoleReference { + /** + * The ARN of the Role resource. + */ + readonly roleArn: string; +} + +/** + * Determine whether the given properties match those of a \`CfnRoleProps\` + * + * @param properties - the TypeScript properties of a \`CfnRoleProps\` + * + * @returns the result of the validation. + */ +// @ts-ignore TS6133 +function CfnRolePropsValidator(properties: any): cdk.ValidationResult { + if (!cdk.canInspect(properties)) return cdk.VALIDATION_SUCCESS; + const errors = new cdk.ValidationResults(); + if (!(properties && typeof properties == 'object' && !Array.isArray(properties))) { + errors.collect(new cdk.ValidationResult("Expected an object, but received: " + JSON.stringify(properties))); + } + return errors.wrap("supplied properties not correct for \\"CfnRoleProps\\""); +} + +// @ts-ignore TS6133 +function convertCfnRolePropsToCloudFormation(properties: any): any { + if (!cdk.canInspect(properties)) return properties; + CfnRolePropsValidator(properties).assertSuccess(); + return {}; +} + +// @ts-ignore TS6133 +function CfnRolePropsFromCloudFormation(properties: any): cfn_parse.FromCloudFormationResult { + if (cdk.isResolvableObject(properties)) { + return new cfn_parse.FromCloudFormationResult(properties); + } + properties = ((properties == null) ? {} : properties); + if (!(properties && typeof properties == 'object' && !Array.isArray(properties))) { + return new cfn_parse.FromCloudFormationResult(properties); + } + const ret = new cfn_parse.FromCloudFormationPropertyObject(); + ret.addUnrecognizedPropertiesAsExtra(properties); + return ret; +} + +/** + * Indicates that this resource can be referenced as a Job. + * + * @stability experimental + */ +export interface IJobRef extends constructs.IConstruct { + /** + * A reference to a Job resource. + */ + readonly jobRef: JobReference; +} + +/** + * @cloudformationResource AWS::IAM::Job + * @stability external + * @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iam-job.html + */ +export class CfnJob extends cdk.CfnResource implements cdk.IInspectable, IJobRef { + /** + * The CloudFormation resource type name for this resource class. + */ + public static readonly CFN_RESOURCE_TYPE_NAME: string = "AWS::IAM::Job"; + + /** + * Build a CfnJob from CloudFormation properties + * + * A factory method that creates a new instance of this class from an object + * containing the CloudFormation properties of this resource. + * Used in the @aws-cdk/cloudformation-include module. + * + * @internal + */ + public static _fromCloudFormation(scope: constructs.Construct, id: string, resourceAttributes: any, options: cfn_parse.FromCloudFormationOptions): CfnJob { + resourceAttributes = resourceAttributes || {}; + const resourceProperties = options.parser.parseValue(resourceAttributes.Properties); + const propsResult = CfnJobPropsFromCloudFormation(resourceProperties); + if (cdk.isResolvableObject(propsResult.value)) { + throw new cdk_errors.ValidationError("Unexpected IResolvable", scope); + } + const ret = new CfnJob(scope, id, propsResult.value); + for (const [propKey, propVal] of Object.entries(propsResult.extraProperties)) { + ret.addPropertyOverride(propKey, propVal); + } + options.parser.handleAttributes(ret, resourceAttributes, id); + return ret; + } + + public config?: cdk.IResolvable | CfnJob.OldConfigProperty; + + /** + * @param scope Scope in which this resource is defined + * @param id Construct identifier for this resource (unique in its scope) + * @param props Resource properties + */ + public constructor(scope: constructs.Construct, id: string, props: CfnJobProps = {}) { + super(scope, id, { + "type": CfnJob.CFN_RESOURCE_TYPE_NAME, + "properties": props + }); + + this.config = (props.config ? flattenCfnJobOldConfigProperty(props.config) : undefined); + } + + public get jobRef(): JobReference { + return {}; + } + + protected get cfnProperties(): Record { + return { + "config": this.config + }; + } + + /** + * Examines the CloudFormation resource and discloses attributes + * + * @param inspector tree inspector to collect and process attributes + */ + public inspect(inspector: cdk.TreeInspector): void { + inspector.addAttribute("aws:cdk:cloudformation:type", CfnJob.CFN_RESOURCE_TYPE_NAME); + inspector.addAttribute("aws:cdk:cloudformation:props", this.cfnProperties); + } + + protected renderProperties(props: Record): Record { + return convertCfnJobPropsToCloudFormation(props); + } +} + +export namespace CfnJob { + /** + * @struct + * @stability external + * @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iam-job-config.html + */ + export interface ConfigProperty { + /** + * @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iam-job-config.html#cfn-iam-job-config-rolearn + */ + readonly roleArn?: IRoleRef | string; + + /** + * @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iam-job-config.html#cfn-iam-job-config-timeout + */ + readonly timeout?: number; + } + + /** + * @struct + * @stability external + * @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iam-job-oldconfig.html + */ + export interface OldConfigProperty { + /** + * @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iam-job-oldconfig.html#cfn-iam-job-oldconfig-rolearn + */ + readonly roleArn?: IRoleRef | string; + } +} + +/** + * Properties for defining a \`CfnJob\` + * + * @struct + * @stability external + * @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iam-job.html + */ +export interface CfnJobProps { + /** + * @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iam-job.html#cfn-iam-job-config + */ + readonly config?: cdk.IResolvable | CfnJob.OldConfigProperty; +} + +// @ts-ignore TS6133 +function flattenCfnJobConfigProperty(props: CfnJob.ConfigProperty | cdk.IResolvable): CfnJob.ConfigProperty | cdk.IResolvable { + if (cdk.isResolvableObject(props)) return props; + return { + "roleArn": (props.roleArn as IRoleRef)?.roleRef?.roleArn ?? props.roleArn, + "timeout": props.timeout + }; +} + +/** + * Determine whether the given properties match those of a \`ConfigProperty\` + * + * @param properties - the TypeScript properties of a \`ConfigProperty\` + * + * @returns the result of the validation. + */ +// @ts-ignore TS6133 +function CfnJobConfigPropertyValidator(properties: any): cdk.ValidationResult { + if (!cdk.canInspect(properties)) return cdk.VALIDATION_SUCCESS; + const errors = new cdk.ValidationResults(); + if (!(properties && typeof properties == 'object' && !Array.isArray(properties))) { + errors.collect(new cdk.ValidationResult("Expected an object, but received: " + JSON.stringify(properties))); + } + errors.collect(cdk.propertyValidator("roleArn", cdk.validateString)(properties.roleArn)); + errors.collect(cdk.propertyValidator("timeout", cdk.validateNumber)(properties.timeout)); + return errors.wrap("supplied properties not correct for \\"ConfigProperty\\""); +} + +// @ts-ignore TS6133 +function convertCfnJobConfigPropertyToCloudFormation(properties: any): any { + if (!cdk.canInspect(properties)) return properties; + CfnJobConfigPropertyValidator(properties).assertSuccess(); + return { + "RoleArn": cdk.stringToCloudFormation(properties.roleArn), + "Timeout": cdk.numberToCloudFormation(properties.timeout) + }; +} + +// @ts-ignore TS6133 +function CfnJobConfigPropertyFromCloudFormation(properties: any): cfn_parse.FromCloudFormationResult { + if (cdk.isResolvableObject(properties)) { + return new cfn_parse.FromCloudFormationResult(properties); + } + properties = ((properties == null) ? {} : properties); + if (!(properties && typeof properties == 'object' && !Array.isArray(properties))) { + return new cfn_parse.FromCloudFormationResult(properties); + } + const ret = new cfn_parse.FromCloudFormationPropertyObject(); + ret.addPropertyResult("roleArn", "RoleArn", (properties.RoleArn != null ? cfn_parse.FromCloudFormation.getString(properties.RoleArn) : undefined)); + ret.addPropertyResult("timeout", "Timeout", (properties.Timeout != null ? cfn_parse.FromCloudFormation.getNumber(properties.Timeout) : undefined)); + ret.addUnrecognizedPropertiesAsExtra(properties); + return ret; +} + +// @ts-ignore TS6133 +function flattenCfnJobOldConfigProperty(props: cdk.IResolvable | CfnJob.OldConfigProperty): cdk.IResolvable | CfnJob.OldConfigProperty { + if (cdk.isResolvableObject(props)) return props; + return { + "roleArn": (props.roleArn as IRoleRef)?.roleRef?.roleArn ?? props.roleArn + }; +} + +/** + * Determine whether the given properties match those of a \`OldConfigProperty\` + * + * @param properties - the TypeScript properties of a \`OldConfigProperty\` + * + * @returns the result of the validation. + */ +// @ts-ignore TS6133 +function CfnJobOldConfigPropertyValidator(properties: any): cdk.ValidationResult { + if (!cdk.canInspect(properties)) return cdk.VALIDATION_SUCCESS; + const errors = new cdk.ValidationResults(); + if (!(properties && typeof properties == 'object' && !Array.isArray(properties))) { + errors.collect(new cdk.ValidationResult("Expected an object, but received: " + JSON.stringify(properties))); + } + errors.collect(cdk.propertyValidator("roleArn", cdk.validateString)(properties.roleArn)); + return errors.wrap("supplied properties not correct for \\"OldConfigProperty\\""); +} + +// @ts-ignore TS6133 +function convertCfnJobOldConfigPropertyToCloudFormation(properties: any): any { + if (!cdk.canInspect(properties)) return properties; + CfnJobOldConfigPropertyValidator(properties).assertSuccess(); + return { + "RoleArn": cdk.stringToCloudFormation(properties.roleArn) + }; +} + +// @ts-ignore TS6133 +function CfnJobOldConfigPropertyFromCloudFormation(properties: any): cfn_parse.FromCloudFormationResult { + if (cdk.isResolvableObject(properties)) { + return new cfn_parse.FromCloudFormationResult(properties); + } + properties = ((properties == null) ? {} : properties); + if (!(properties && typeof properties == 'object' && !Array.isArray(properties))) { + return new cfn_parse.FromCloudFormationResult(properties); + } + const ret = new cfn_parse.FromCloudFormationPropertyObject(); + ret.addPropertyResult("roleArn", "RoleArn", (properties.RoleArn != null ? cfn_parse.FromCloudFormation.getString(properties.RoleArn) : undefined)); + ret.addUnrecognizedPropertiesAsExtra(properties); + return ret; +} + +/** + * A reference to a Job resource. + * + * @struct + * @stability external + */ +export interface JobReference { + +} + +/** + * Determine whether the given properties match those of a \`CfnJobProps\` + * + * @param properties - the TypeScript properties of a \`CfnJobProps\` + * + * @returns the result of the validation. + */ +// @ts-ignore TS6133 +function CfnJobPropsValidator(properties: any): cdk.ValidationResult { + if (!cdk.canInspect(properties)) return cdk.VALIDATION_SUCCESS; + const errors = new cdk.ValidationResults(); + if (!(properties && typeof properties == 'object' && !Array.isArray(properties))) { + errors.collect(new cdk.ValidationResult("Expected an object, but received: " + JSON.stringify(properties))); + } + errors.collect(cdk.propertyValidator("config", CfnJobOldConfigPropertyValidator)(properties.config)); + return errors.wrap("supplied properties not correct for \\"CfnJobProps\\""); +} + +// @ts-ignore TS6133 +function convertCfnJobPropsToCloudFormation(properties: any): any { + if (!cdk.canInspect(properties)) return properties; + CfnJobPropsValidator(properties).assertSuccess(); + return { + "Config": convertCfnJobOldConfigPropertyToCloudFormation(properties.config) + }; +} + +// @ts-ignore TS6133 +function CfnJobPropsFromCloudFormation(properties: any): cfn_parse.FromCloudFormationResult { + if (cdk.isResolvableObject(properties)) { + return new cfn_parse.FromCloudFormationResult(properties); + } + properties = ((properties == null) ? {} : properties); + if (!(properties && typeof properties == 'object' && !Array.isArray(properties))) { + return new cfn_parse.FromCloudFormationResult(properties); + } + const ret = new cfn_parse.FromCloudFormationPropertyObject(); + ret.addPropertyResult("config", "Config", (properties.Config != null ? CfnJobOldConfigPropertyFromCloudFormation(properties.Config) : undefined)); + ret.addUnrecognizedPropertiesAsExtra(properties); + return ret; +}" +`; + +exports[`resource with relationship reference 1`] = ` +"/* eslint-disable prettier/prettier, @stylistic/max-len */ +import * as cdk from "aws-cdk-lib"; +import * as constructs from "constructs"; +import * as cfn_parse from "aws-cdk-lib/core/lib/helpers-internal"; +import * as cdk_errors from "aws-cdk-lib/core/lib/errors"; + +/** + * Indicates that this resource can be referenced as a Role. + * + * @stability experimental + */ +export interface IRoleRef extends constructs.IConstruct { + /** + * A reference to a Role resource. + */ + readonly roleRef: RoleReference; +} + +/** + * @cloudformationResource AWS::IAM::Role + * @stability external + * @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iam-role.html + */ +export class CfnRole extends cdk.CfnResource implements cdk.IInspectable, IRoleRef, cdk.ITaggable { + /** + * The CloudFormation resource type name for this resource class. + */ + public static readonly CFN_RESOURCE_TYPE_NAME: string = "AWS::IAM::Role"; + + /** + * Build a CfnRole from CloudFormation properties + * + * A factory method that creates a new instance of this class from an object + * containing the CloudFormation properties of this resource. + * Used in the @aws-cdk/cloudformation-include module. + * + * @internal + */ + public static _fromCloudFormation(scope: constructs.Construct, id: string, resourceAttributes: any, options: cfn_parse.FromCloudFormationOptions): CfnRole { + resourceAttributes = resourceAttributes || {}; + const resourceProperties = options.parser.parseValue(resourceAttributes.Properties); + const propsResult = CfnRolePropsFromCloudFormation(resourceProperties); + if (cdk.isResolvableObject(propsResult.value)) { + throw new cdk_errors.ValidationError("Unexpected IResolvable", scope); + } + const ret = new CfnRole(scope, id, propsResult.value); + for (const [propKey, propVal] of Object.entries(propsResult.extraProperties)) { + ret.addPropertyOverride(propKey, propVal); + } + options.parser.handleAttributes(ret, resourceAttributes, id); + return ret; + } + + /** + * @cloudformationAttribute RoleArn + */ + public readonly attrRoleArn: string; + + /** + * @param scope Scope in which this resource is defined + * @param id Construct identifier for this resource (unique in its scope) + * @param props Resource properties + */ + public constructor(scope: constructs.Construct, id: string, props: CfnRoleProps = {}) { + super(scope, id, { + "type": CfnRole.CFN_RESOURCE_TYPE_NAME, + "properties": props + }); + + this.attrRoleArn = cdk.Token.asString(this.getAtt("RoleArn", cdk.ResolutionTypeHint.STRING)); + } + + public get roleRef(): RoleReference { + return { + "roleArn": this.attrRoleArn + }; + } + + protected get cfnProperties(): Record { + return {}; + } + + /** + * Examines the CloudFormation resource and discloses attributes + * + * @param inspector tree inspector to collect and process attributes + */ + public inspect(inspector: cdk.TreeInspector): void { + inspector.addAttribute("aws:cdk:cloudformation:type", CfnRole.CFN_RESOURCE_TYPE_NAME); + inspector.addAttribute("aws:cdk:cloudformation:props", this.cfnProperties); + } + + protected renderProperties(props: Record): Record { + return convertCfnRolePropsToCloudFormation(props); + } +} + +/** + * Properties for defining a \`CfnRole\` + * + * @struct + * @stability external + * @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iam-role.html + */ +export interface CfnRoleProps { + +} + +/** + * A reference to a Role resource. + * + * @struct + * @stability external + */ +export interface RoleReference { + /** + * The ARN of the Role resource. + */ + readonly roleArn: string; +} + +/** + * Determine whether the given properties match those of a \`CfnRoleProps\` + * + * @param properties - the TypeScript properties of a \`CfnRoleProps\` + * + * @returns the result of the validation. + */ +// @ts-ignore TS6133 +function CfnRolePropsValidator(properties: any): cdk.ValidationResult { + if (!cdk.canInspect(properties)) return cdk.VALIDATION_SUCCESS; + const errors = new cdk.ValidationResults(); + if (!(properties && typeof properties == 'object' && !Array.isArray(properties))) { + errors.collect(new cdk.ValidationResult("Expected an object, but received: " + JSON.stringify(properties))); + } + return errors.wrap("supplied properties not correct for \\"CfnRoleProps\\""); +} + +// @ts-ignore TS6133 +function convertCfnRolePropsToCloudFormation(properties: any): any { + if (!cdk.canInspect(properties)) return properties; + CfnRolePropsValidator(properties).assertSuccess(); + return {}; +} + +// @ts-ignore TS6133 +function CfnRolePropsFromCloudFormation(properties: any): cfn_parse.FromCloudFormationResult { + if (cdk.isResolvableObject(properties)) { + return new cfn_parse.FromCloudFormationResult(properties); + } + properties = ((properties == null) ? {} : properties); + if (!(properties && typeof properties == 'object' && !Array.isArray(properties))) { + return new cfn_parse.FromCloudFormationResult(properties); + } + const ret = new cfn_parse.FromCloudFormationPropertyObject(); + ret.addUnrecognizedPropertiesAsExtra(properties); + return ret; +} + +/** + * Indicates that this resource can be referenced as a Function. + * + * @stability experimental + */ +export interface IFunctionRef extends constructs.IConstruct { + /** + * A reference to a Function resource. + */ + readonly functionRef: FunctionReference; +} + +/** + * @cloudformationResource AWS::IAM::Function + * @stability external + * @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iam-function.html + */ +export class CfnFunction extends cdk.CfnResource implements cdk.IInspectable, IFunctionRef { + /** + * The CloudFormation resource type name for this resource class. + */ + public static readonly CFN_RESOURCE_TYPE_NAME: string = "AWS::IAM::Function"; + + /** + * Build a CfnFunction from CloudFormation properties + * + * A factory method that creates a new instance of this class from an object + * containing the CloudFormation properties of this resource. + * Used in the @aws-cdk/cloudformation-include module. + * + * @internal + */ + public static _fromCloudFormation(scope: constructs.Construct, id: string, resourceAttributes: any, options: cfn_parse.FromCloudFormationOptions): CfnFunction { + resourceAttributes = resourceAttributes || {}; + const resourceProperties = options.parser.parseValue(resourceAttributes.Properties); + const propsResult = CfnFunctionPropsFromCloudFormation(resourceProperties); + if (cdk.isResolvableObject(propsResult.value)) { + throw new cdk_errors.ValidationError("Unexpected IResolvable", scope); + } + const ret = new CfnFunction(scope, id, propsResult.value); + for (const [propKey, propVal] of Object.entries(propsResult.extraProperties)) { + ret.addPropertyOverride(propKey, propVal); + } + options.parser.handleAttributes(ret, resourceAttributes, id); + return ret; + } + + public roleArn?: string; + + /** + * @param scope Scope in which this resource is defined + * @param id Construct identifier for this resource (unique in its scope) + * @param props Resource properties + */ + public constructor(scope: constructs.Construct, id: string, props: CfnFunctionProps = {}) { + super(scope, id, { + "type": CfnFunction.CFN_RESOURCE_TYPE_NAME, + "properties": props + }); + + this.roleArn = (props.roleArn as IRoleRef)?.roleRef?.roleArn ?? props.roleArn; + } + + public get functionRef(): FunctionReference { + return {}; + } + + protected get cfnProperties(): Record { + return { + "roleArn": this.roleArn + }; + } + + /** + * Examines the CloudFormation resource and discloses attributes + * + * @param inspector tree inspector to collect and process attributes + */ + public inspect(inspector: cdk.TreeInspector): void { + inspector.addAttribute("aws:cdk:cloudformation:type", CfnFunction.CFN_RESOURCE_TYPE_NAME); + inspector.addAttribute("aws:cdk:cloudformation:props", this.cfnProperties); + } + + protected renderProperties(props: Record): Record { + return convertCfnFunctionPropsToCloudFormation(props); + } +} + +/** + * Properties for defining a \`CfnFunction\` + * + * @struct + * @stability external + * @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iam-function.html + */ +export interface CfnFunctionProps { + /** + * @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iam-function.html#cfn-iam-function-rolearn + */ + readonly roleArn?: IRoleRef | string; +} + +/** + * A reference to a Function resource. + * + * @struct + * @stability external + */ +export interface FunctionReference { + +} + +/** + * Determine whether the given properties match those of a \`CfnFunctionProps\` + * + * @param properties - the TypeScript properties of a \`CfnFunctionProps\` + * + * @returns the result of the validation. + */ +// @ts-ignore TS6133 +function CfnFunctionPropsValidator(properties: any): cdk.ValidationResult { + if (!cdk.canInspect(properties)) return cdk.VALIDATION_SUCCESS; + const errors = new cdk.ValidationResults(); + if (!(properties && typeof properties == 'object' && !Array.isArray(properties))) { + errors.collect(new cdk.ValidationResult("Expected an object, but received: " + JSON.stringify(properties))); + } + errors.collect(cdk.propertyValidator("roleArn", cdk.validateString)(properties.roleArn)); + return errors.wrap("supplied properties not correct for \\"CfnFunctionProps\\""); +} + +// @ts-ignore TS6133 +function convertCfnFunctionPropsToCloudFormation(properties: any): any { + if (!cdk.canInspect(properties)) return properties; + CfnFunctionPropsValidator(properties).assertSuccess(); + return { + "RoleArn": cdk.stringToCloudFormation(properties.roleArn) + }; +} + +// @ts-ignore TS6133 +function CfnFunctionPropsFromCloudFormation(properties: any): cfn_parse.FromCloudFormationResult { + if (cdk.isResolvableObject(properties)) { + return new cfn_parse.FromCloudFormationResult(properties); + } + properties = ((properties == null) ? {} : properties); + if (!(properties && typeof properties == 'object' && !Array.isArray(properties))) { + return new cfn_parse.FromCloudFormationResult(properties); + } + const ret = new cfn_parse.FromCloudFormationPropertyObject(); + ret.addPropertyResult("roleArn", "RoleArn", (properties.RoleArn != null ? cfn_parse.FromCloudFormation.getString(properties.RoleArn) : undefined)); + ret.addUnrecognizedPropertiesAsExtra(properties); + return ret; +}" +`; diff --git a/tools/@aws-cdk/spec2cdk/test/relationships.test.ts b/tools/@aws-cdk/spec2cdk/test/relationships.test.ts new file mode 100644 index 0000000000000..b127d8f4386ee --- /dev/null +++ b/tools/@aws-cdk/spec2cdk/test/relationships.test.ts @@ -0,0 +1,336 @@ +import { Service, SpecDatabase, emptyDatabase } from '@aws-cdk/service-spec-types'; +import { TypeScriptRenderer } from '@cdklabs/typewriter'; +import { AstBuilder } from '../lib/cdk/ast'; +import { RELATIONSHIP_SERVICES } from '../lib/cdk/relationship-decider'; + +const renderer = new TypeScriptRenderer(); +let db: SpecDatabase; +let service: Service; + +// Only run these tests if we're rendering relationships for IAM +const iamHasRelationships = RELATIONSHIP_SERVICES.includes('iam'); +const maybeTest = iamHasRelationships ? test : test.skip; + +beforeEach(() => { + db = emptyDatabase(); + + service = db.allocate('service', { + name: 'aws-iam', + shortName: 'iam', + capitalized: 'IAM', + cloudFormationNamespace: 'AWS::IAM', + }); +}); + +maybeTest('resource with relationship reference', () => { + // Target resource + const targetResource = db.allocate('resource', { + name: 'Role', + attributes: { + RoleArn: { + type: { type: 'string' }, + }, + }, + properties: {}, + cloudFormationType: 'AWS::IAM::Role', + }); + db.link('hasResource', service, targetResource); + + // Source resource with relationship + const sourceResource = db.allocate('resource', { + name: 'Function', + attributes: {}, + properties: { + RoleArn: { + type: { type: 'string' }, + relationshipRefs: [{ + cloudFormationType: 'AWS::IAM::Role', + propertyName: 'RoleArn', + }], + }, + }, + cloudFormationType: 'AWS::IAM::Function', + }); + db.link('hasResource', service, sourceResource); + + const ast = AstBuilder.forService(service, { db }); + const rendered = renderer.render(ast.module); + + expect(rendered).toMatchSnapshot(); +}); + +maybeTest('resource with multiple relationship references', () => { + // Target resource 1 + const roleResource = db.allocate('resource', { + name: 'Role', + attributes: { + RoleArn: { + type: { type: 'string' }, + }, + }, + properties: {}, + cloudFormationType: 'AWS::IAM::Role', + }); + db.link('hasResource', service, roleResource); + + // Target resource 2 + const userResource = db.allocate('resource', { + name: 'User', + attributes: { + UserArn: { + type: { type: 'string' }, + }, + }, + properties: {}, + cloudFormationType: 'AWS::IAM::User', + }); + db.link('hasResource', service, userResource); + + // Source resource with multiple relationships + const policyResource = db.allocate('resource', { + name: 'Policy', + attributes: {}, + properties: { + PrincipalArn: { + type: { type: 'string' }, + relationshipRefs: [ + { + cloudFormationType: 'AWS::IAM::Role', + propertyName: 'RoleArn', + }, + { + cloudFormationType: 'AWS::IAM::User', + propertyName: 'UserArn', + }, + ], + }, + }, + cloudFormationType: 'AWS::IAM::Policy', + }); + db.link('hasResource', service, policyResource); + + const ast = AstBuilder.forService(service, { db }); + const rendered = renderer.render(ast.module); + + expect(rendered).toMatchSnapshot(); +}); + +maybeTest('resource with nested relationship requiring flattening', () => { + // Target resource + const roleResource = db.allocate('resource', { + name: 'Role', + attributes: { + RoleArn: { + type: { type: 'string' }, + }, + }, + properties: {}, + cloudFormationType: 'AWS::IAM::Role', + }); + db.link('hasResource', service, roleResource); + + // Type definition with relationship + const configType = db.allocate('typeDefinition', { + name: 'ExecutionConfig', + properties: { + RoleArn: { + type: { type: 'string' }, + relationshipRefs: [{ + cloudFormationType: 'AWS::IAM::Role', + propertyName: 'RoleArn', + }], + }, + }, + }); + + // Source resource with nested property + const taskResource = db.allocate('resource', { + name: 'Task', + attributes: {}, + properties: { + ExecutionConfig: { + type: { type: 'ref', reference: { $ref: configType.$id } }, + }, + }, + cloudFormationType: 'AWS::IAM::Task', + }); + db.link('hasResource', service, taskResource); + db.link('usesType', taskResource, configType); + + const ast = AstBuilder.forService(service, { db }); + const rendered = renderer.render(ast.module); + + expect(rendered).toMatchSnapshot(); +}); + +maybeTest('resource with array of nested properties with relationship', () => { + // Target resource + const roleResource = db.allocate('resource', { + name: 'Role', + attributes: { + RoleArn: { + type: { type: 'string' }, + }, + }, + properties: {}, + cloudFormationType: 'AWS::IAM::Role', + }); + db.link('hasResource', service, roleResource); + + // Type definition with relationship + const permissionType = db.allocate('typeDefinition', { + name: 'Permission', + properties: { + RoleArn: { + type: { type: 'string' }, + relationshipRefs: [{ + cloudFormationType: 'AWS::IAM::Role', + propertyName: 'RoleArn', + }], + }, + }, + }); + + // Source resource with array of nested properties + const resourceResource = db.allocate('resource', { + name: 'Resource', + attributes: {}, + properties: { + Permissions: { + type: { type: 'array', element: { type: 'ref', reference: { $ref: permissionType.$id } } }, + }, + }, + cloudFormationType: 'AWS::IAM::Resource', + }); + db.link('hasResource', service, resourceResource); + db.link('usesType', resourceResource, permissionType); + + const ast = AstBuilder.forService(service, { db }); + const rendered = renderer.render(ast.module); + + expect(rendered).toMatchSnapshot(); +}); + +maybeTest('resource with nested relationship with type history', () => { + // Target resource + const roleResource = db.allocate('resource', { + name: 'Role', + attributes: { + RoleArn: { + type: { type: 'string' }, + }, + }, + properties: {}, + cloudFormationType: 'AWS::IAM::Role', + }); + db.link('hasResource', service, roleResource); + + // Old type definition + const oldConfigType = db.allocate('typeDefinition', { + name: 'OldConfig', + properties: { + RoleArn: { + type: { type: 'string' }, + relationshipRefs: [{ + cloudFormationType: 'AWS::IAM::Role', + propertyName: 'RoleArn', + }], + }, + }, + }); + + // New type definition + const configType = db.allocate('typeDefinition', { + name: 'Config', + properties: { + RoleArn: { + type: { type: 'string' }, + relationshipRefs: [{ + cloudFormationType: 'AWS::IAM::Role', + propertyName: 'RoleArn', + }], + }, + Timeout: { + type: { type: 'integer' }, + }, + }, + }); + + // Source resource + const jobResource = db.allocate('resource', { + name: 'Job', + attributes: {}, + properties: { + Config: { + type: { type: 'ref', reference: { $ref: configType.$id } }, + previousTypes: [{ type: 'ref', reference: { $ref: oldConfigType.$id } }], + }, + }, + cloudFormationType: 'AWS::IAM::Job', + }); + db.link('hasResource', service, jobResource); + db.link('usesType', jobResource, configType); + db.link('usesType', jobResource, oldConfigType); + + const ast = AstBuilder.forService(service, { db }); + const rendered = renderer.render(ast.module); + + expect(rendered).toMatchSnapshot(); +}); + +maybeTest('relationship have arns appear first in the constructor chain', () => { + // Target resource + const roleResource = db.allocate('resource', { + name: 'Role', + primaryIdentifier: ['RoleName', 'OtherPrimaryId'], + attributes: { + RoleArn: { + type: { type: 'string' }, + }, + }, + properties: {}, + cloudFormationType: 'AWS::IAM::Role', + }); + db.link('hasResource', service, roleResource); + + // Type definition with relationship + const configType = db.allocate('typeDefinition', { + name: 'ExecutionConfig', + properties: { + RoleArn: { + type: { type: 'string' }, + relationshipRefs: [{ + cloudFormationType: 'AWS::IAM::Role', + propertyName: 'RoleName', + }, { + cloudFormationType: 'AWS::IAM::Role', + propertyName: 'RoleArn', + }, { + cloudFormationType: 'AWS::IAM::Role', + propertyName: 'OtherPrimaryId', + }], + }, + }, + }); + + // Source resource with nested property + const taskResource = db.allocate('resource', { + name: 'Task', + attributes: {}, + properties: { + ExecutionConfig: { + type: { type: 'ref', reference: { $ref: configType.$id } }, + }, + }, + cloudFormationType: 'AWS::IAM::Task', + }); + db.link('hasResource', service, taskResource); + db.link('usesType', taskResource, configType); + + const ast = AstBuilder.forService(service, { db }); + const rendered = renderer.render(ast.module); + + const chain = '"roleArn": (props.roleArn as IRoleRef)?.roleRef?.roleArn ?? (props.roleArn as IRoleRef)?.roleRef?.roleName ?? (props.roleArn as IRoleRef)?.roleRef?.otherPrimaryId ?? props.roleArn'; + + expect(rendered).toContain(chain); +});