Skip to content

Commit 5e6c194

Browse files
committed
fix: generate addArnFor<Resource> method for more resources
1 parent 10de047 commit 5e6c194

File tree

12 files changed

+414
-141
lines changed

12 files changed

+414
-141
lines changed

aws-cdk.code-workspace

Lines changed: 4 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -7,30 +7,14 @@
77
"type": "on-demand"
88
},
99
"jest.virtualFolders": [
10-
{
11-
"name": "aws-cdk",
12-
"rootPath": "packages/aws-cdk"
13-
},
1410
{
1511
"name": "aws-cdk-lib",
1612
"rootPath": "packages/aws-cdk-lib"
1713
},
18-
{
19-
"name": "cli-lib-alpha",
20-
"rootPath": "packages/@aws-cdk/cli-lib-alpha"
21-
},
22-
{
23-
"name": "cloudformation-diff",
24-
"rootPath": "packages/@aws-cdk/cloudformation-diff"
25-
},
2614
{
2715
"name": "custom-resource-handlers",
2816
"rootPath": "packages/@aws-cdk/custom-resource-handlers"
2917
},
30-
{
31-
"name": "integ-runner",
32-
"rootPath": "packages/@aws-cdk/integ-runner"
33-
},
3418
{
3519
"name": "integ-tests-alpha",
3620
"rootPath": "packages/@aws-cdk/integ-tests-alpha"
@@ -39,17 +23,13 @@
3923
"name": "aws-custom-resource-sdk-adapter",
4024
"rootPath": "packages/@aws-cdk/aws-custom-resource-sdk-adapter"
4125
},
42-
{
43-
"name": "toolkit",
44-
"rootPath": "packages/@aws-cdk/toolkit"
45-
},
46-
{
47-
"name": "user-input-gen",
48-
"rootPath": "tools/@aws-cdk/user-input-gen"
49-
},
5026
{
5127
"name": "mixins",
5228
"rootPath": "packages/@aws-cdk/mixins-preview"
29+
},
30+
{
31+
"name": "spec2cdk",
32+
"rootPath": "tools/@aws-cdk/spec2cdk"
5333
}
5434
]
5535
},

packages/@aws-cdk/mixins-preview/scripts/spec2logs/builder.ts

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,7 @@ import { BaseServiceSubmodule, relativeImportPath } from '@aws-cdk/spec2cdk/lib/
99
import type { AddServiceProps, LibraryBuilderProps } from '@aws-cdk/spec2cdk/lib/cdk/library-builder';
1010
import { LibraryBuilder } from '@aws-cdk/spec2cdk/lib/cdk/library-builder';
1111
import { MIXINS_CORE } from '../spec2mixins/helpers';
12-
13-
// we cannot currently get an Arn for these
14-
const EXCLUDE: string[] = [
15-
'AWS::BedrockAgentCore::Runtime',
16-
'AWS::ElastiCache::CacheCluster',
17-
'AWS::Grafana::Workspace',
18-
'AWS::RUM::AppMonitor',
19-
'AWS::SageMaker::Workteam',
20-
];
12+
import { ResourceReference } from '@aws-cdk/spec2cdk/lib/cdk/reference-props';
2113

2214
class LogsDeliveryBuilderServiceModule extends BaseServiceSubmodule {
2315
public readonly constructLibModule: ExternalModule;
@@ -47,7 +39,8 @@ export class LogsDeliveryBuilder extends LibraryBuilder<LogsDeliveryBuilderServi
4739
}
4840

4941
protected addResourceToSubmodule(submodule: LogsDeliveryBuilderServiceModule, resource: Resource, _props?: AddServiceProps): void {
50-
if (resource.vendedLogs && !EXCLUDE.includes(resource.cloudFormationType)) {
42+
const resourceReference = new ResourceReference(resource);
43+
if (resource.vendedLogs && resourceReference.hasArnGetter) {
5144
const service = this.db.incoming('hasResource', resource).only().entity;
5245
const logsModule = this.obtainLogsDeliveryModule(submodule, service);
5346

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

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export function findNonIdentifierArnProperty(resource: Resource) {
1010
return findArnProperty(resource, (name) => !resource.primaryIdentifier?.includes(name));
1111
}
1212

13-
export function findArnProperty(resource: Resource, filter: (name: string) => boolean = () => true): any {
13+
export function findArnProperty(resource: Resource, filter: (name: string) => boolean = () => true): string | undefined {
1414
const possibleArnNames = ['Arn', `${resource.name}Arn`];
1515
for (const name of possibleArnNames) {
1616
const prop = resource.attributes[name];
@@ -21,3 +21,27 @@ export function findArnProperty(resource: Resource, filter: (name: string) => bo
2121
return undefined;
2222
}
2323

24+
/**
25+
* Extracts all variables from an ARN Format template
26+
*
27+
* @example
28+
* extractVariablesFromArnFormat("arn:${Partition}:sagemaker:${Region}:${Account}:workteam/${WorkteamName}")
29+
* // returns ['Partition', 'Region', 'Account', 'WorkteamName']
30+
*/
31+
export function extractVariablesFromArnFormat(format: string): string[] {
32+
return (format.match(/\${([^{}]+)}/g) || []).map(match => match.slice(2, -1));
33+
}
34+
35+
/**
36+
* Extracts all resource specific variables from an ARN Format template.
37+
*
38+
* To get all variables, use `extractVariablesFromArnFormat(format)`
39+
*
40+
* @example
41+
* extractVariablesFromArnFormat("arn:${Partition}:sagemaker:${Region}:${Account}:workteam/${WorkteamName}")
42+
* // returns ['WorkteamName']
43+
*/
44+
export function extractResourceVariablesFromArnFormat(format: string) {
45+
const [_arn, _partition, _service, _region, _account, ...rest] = format.split(':');
46+
return extractVariablesFromArnFormat(rest.join(':'));
47+
}

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

Lines changed: 124 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,70 +1,151 @@
11
import { Resource } from '@aws-cdk/service-spec-types';
2-
import { $E, expr, Expression, PropertySpec, Type } from '@cdklabs/typewriter';
2+
import { $this, expr, Expression, PropertySpec, Type } from '@cdklabs/typewriter';
33
import { attributePropertyName, referencePropertyName } from '../naming';
4-
import { findNonIdentifierArnProperty } from './arn';
4+
import { extractResourceVariablesFromArnFormat, findArnProperty, findNonIdentifierArnProperty } from './arn';
55
import { CDK_CORE } from './cdk';
66

77
export interface ReferenceProp {
88
readonly declaration: PropertySpec;
99
readonly cfnValue: Expression;
1010
}
1111

12-
// Convenience typewriter builder
13-
const $this = $E(expr.this_());
14-
15-
export function getReferenceProps(resource: Resource): ReferenceProp[] {
16-
const referenceProps = [];
17-
// Primary identifier. We assume all parts are strings.
18-
const primaryIdentifier = resource.primaryIdentifier ?? [];
19-
if (primaryIdentifier.length === 1) {
20-
referenceProps.push({
21-
declaration: {
22-
name: referencePropertyName(primaryIdentifier[0], resource.name),
23-
type: Type.STRING,
24-
immutable: true,
25-
docs: {
26-
summary: `The ${primaryIdentifier[0]} of the ${resource.name} resource.`,
12+
export class ResourceReference {
13+
public readonly resource: Resource;
14+
public readonly arnPropertyName?: string;
15+
16+
public get referenceProps(): ReferenceProp[] {
17+
return [...this._referenceProps.values()];
18+
}
19+
20+
public get arnVariables(): Record<string, string> | undefined {
21+
return this._arnVariables.length ? Object.fromEntries(this._arnVariables) : undefined;
22+
}
23+
24+
public get hasArnGetter(): boolean {
25+
return this.arnPropertyName != null || this.arnVariables != null;
26+
}
27+
28+
private _arnVariables: [string, string][] = [];
29+
private readonly _referenceProps = new FirstOccurrenceMap<string, ReferenceProp>();
30+
31+
public constructor(resource: Resource) {
32+
this.resource = resource;
33+
this.arnPropertyName = this.findArnPropertyName();
34+
this._referenceProps = new FirstOccurrenceMap<string, ReferenceProp>();
35+
this.collectReferencesProps();
36+
}
37+
38+
private findArnPropertyName(): string | undefined {
39+
const arnProp = findArnProperty(this.resource);
40+
if (!arnProp) {
41+
return;
42+
}
43+
return referencePropertyName(arnProp, this.resource.name);
44+
}
45+
46+
private collectReferencesProps() {
47+
// Primary identifier. We assume all parts are strings.
48+
const primaryIdentifier = this.resource.primaryIdentifier ?? [];
49+
if (primaryIdentifier.length === 1) {
50+
const name = referencePropertyName(primaryIdentifier[0], this.resource.name);
51+
this._referenceProps.setIfAbsent(name, {
52+
declaration: {
53+
name,
54+
type: Type.STRING,
55+
immutable: true,
56+
docs: {
57+
summary: `The ${primaryIdentifier[0]} of the ${this.resource.name} resource.`,
58+
},
2759
},
28-
},
29-
cfnValue: $this.ref,
30-
});
31-
} else if (primaryIdentifier.length > 1) {
32-
for (const [i, cfnName] of enumerate(primaryIdentifier)) {
33-
referenceProps.push({
60+
cfnValue: $this.ref,
61+
});
62+
} else if (primaryIdentifier.length > 1) {
63+
for (const [i, cfnName] of primaryIdentifier.entries()) {
64+
const name = referencePropertyName(cfnName, this.resource.name);
65+
this._referenceProps.setIfAbsent(name, {
66+
declaration: {
67+
name,
68+
type: Type.STRING,
69+
immutable: true,
70+
docs: {
71+
summary: `The ${cfnName} of the ${this.resource.name} resource.`,
72+
},
73+
},
74+
cfnValue: splitSelect('|', i, $this.ref),
75+
});
76+
}
77+
}
78+
79+
// Arn identifier
80+
const arnProp = findNonIdentifierArnProperty(this.resource);
81+
if (arnProp) {
82+
const name = referencePropertyName(arnProp, this.resource.name);
83+
this._referenceProps.setIfAbsent(name, {
3484
declaration: {
35-
name: referencePropertyName(cfnName, resource.name),
85+
name,
3686
type: Type.STRING,
3787
immutable: true,
3888
docs: {
39-
summary: `The ${cfnName} of the ${resource.name} resource.`,
89+
summary: `The ARN of the ${this.resource.name} resource.`,
4090
},
4191
},
42-
cfnValue: splitSelect('|', i, $this.ref),
92+
cfnValue: $this[attributePropertyName(arnProp)],
4393
});
4494
}
95+
96+
// If we do not have an ARN prop, see if we can construct it from the arn template
97+
if (this.resource.arnTemplate && !this.arnPropertyName) {
98+
const variables = extractResourceVariablesFromArnFormat(this.resource.arnTemplate);
99+
const allResolved = variables.every(v => this.tryFindUsableValue(v));
100+
if (allResolved) {
101+
for (const variable of variables) {
102+
const access = this.tryFindUsableValue(variable)!;
103+
const refPropName = referencePropertyName(variable, this.resource.name);
104+
this._arnVariables.push([variable, refPropName]);
105+
this._referenceProps.setIfAbsent(refPropName, {
106+
declaration: {
107+
name: refPropName,
108+
type: Type.STRING,
109+
immutable: true,
110+
docs: {
111+
summary: `The ${variable} of the ${this.resource.name} resource.`,
112+
},
113+
},
114+
cfnValue: access,
115+
});
116+
}
117+
}
118+
}
45119
}
46120

47-
const arnProp = findNonIdentifierArnProperty(resource);
48-
if (arnProp) {
49-
referenceProps.push({
50-
declaration: {
51-
name: referencePropertyName(arnProp, resource.name),
52-
type: Type.STRING,
53-
immutable: true,
54-
docs: {
55-
summary: `The ARN of the ${resource.name} resource.`,
56-
},
57-
},
58-
cfnValue: $this[attributePropertyName(arnProp)],
59-
});
121+
public tryFindUsableValue(name: string): Expression| undefined {
122+
const refPropName = referencePropertyName(name, this.resource.name);
123+
124+
// an already discovered reference prop
125+
if (this._referenceProps.has(refPropName)) {
126+
return this._referenceProps.get(refPropName)!.cfnValue;
127+
}
128+
// an attribute
129+
if (this.resource.attributes[name]) {
130+
return $this[attributePropertyName(name)];
131+
}
132+
// a required prop
133+
if (this.resource.properties[name]?.required) {
134+
return $this[refPropName];
135+
}
136+
137+
// we don't have a matching value
138+
return undefined;
139+
}
140+
}
141+
142+
class FirstOccurrenceMap<K, V> extends Map<K, V> {
143+
setIfAbsent(key: K, value: V): void {
144+
if (!this.has(key)) this.set(key, value);
60145
}
61-
return referenceProps;
62146
}
63147

64148
function splitSelect(sep: string, n: number, base: Expression) {
65149
return CDK_CORE.Fn.select(expr.lit(n), CDK_CORE.Fn.split(expr.lit(sep), base));
66150
}
67151

68-
function enumerate<A>(xs: A[]): Array<[number, A]> {
69-
return xs.map((x, i) => [i, x]);
70-
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Property, RelationshipRef, Resource, RichProperty, SpecDatabase } from '@aws-cdk/service-spec-types';
22
import * as naming from '../naming';
33
import { namespaceFromResource, referenceInterfaceName, referenceInterfaceAttributeName, referencePropertyName, typeAliasPrefixFromResource } from '../naming';
4-
import { getReferenceProps } from './reference-props';
4+
import { ResourceReference } from './reference-props';
55
import { log } from '../util';
66
import { SelectiveImport } from './service-submodule';
77

@@ -76,7 +76,7 @@ export class RelationshipDecider {
7676
return undefined;
7777
}
7878
const targetResource = this.db.lookup('resource', 'cloudFormationType', 'equals', relationship.cloudFormationType).only();
79-
const refProps = getReferenceProps(targetResource);
79+
const refProps = new ResourceReference(targetResource).referenceProps;
8080
const expectedPropName = referencePropertyName(relationship.propertyName, targetResource.name);
8181
const prop = refProps.find(p => p.declaration.name === expectedPropName);
8282
if (!prop) {

0 commit comments

Comments
 (0)