Skip to content

Commit aac52e5

Browse files
authored
feat(cloudformation-diff): use awscdk-service-spec as data source (#27813)
We already changed the datasource for generated resource to use `@aws-cdk/awscdk-service-spec`. However `cloudformation-diff` was still using the old data source. This PR swaps out the source, with minimal further changes. Hence no additional tests are required. Note that this new implementation does not apply `temporary-schemas` defined in `spec2cdk`. This is because `temporary-schemas` is a mechanism to unblock development of unreleased features, and not a mechanism to publish schema changes. For the latter, changes must be made in [cdklabs/awscdk-service-spec](https://github.com/cdklabs/awscdk-service-spec). I ran a local benchmark and `cdk diff` is now running ~1.03 slower than before. I investigated why and this appears to be mostly due to the gzip compression of the database file. If we load the DB from an uncompressed source, the new code path performs ~1.07 times faster than the old code path. This trade-off is acceptable. Compressed package size of `aws-cdk` is reducing with this change from `11.7MB` to `8.9MB` ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent aa45bfd commit aac52e5

File tree

12 files changed

+132
-73
lines changed

12 files changed

+132
-73
lines changed

aws-cdk.code-workspace

+4
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@
1010
"name": "cli-lib-alpha",
1111
"rootPath": "packages/@aws-cdk/cli-lib-alpha"
1212
},
13+
{
14+
"name": "cloudformation-diff",
15+
"rootPath": "packages/@aws-cdk/cloudformation-diff"
16+
},
1317
{
1418
"name": "custom-resource-handlers",
1519
"rootPath": "packages/@aws-cdk/custom-resource-handlers"
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,18 @@
11
const baseConfig = require('@aws-cdk/cdk-build-tools/config/jest.config');
2+
3+
/** @type {import('ts-jest').JestConfigWithTsJest} */
24
module.exports = {
3-
...baseConfig,
4-
coverageThreshold: {
5-
global: {
6-
statements: 60,
7-
branches: 55,
8-
},
5+
...baseConfig,
6+
7+
// Different than usual
8+
testMatch: [
9+
'<rootDir>/**/test/**/?(*.)+(test).ts',
10+
],
11+
12+
coverageThreshold: {
13+
global: {
14+
branches: 60,
15+
statements: 55,
916
},
17+
},
1018
};

packages/@aws-cdk/cloudformation-diff/lib/diff/index.ts

+8-9
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import * as cfnspec from '@aws-cdk/cfnspec';
1+
import { Resource } from '@aws-cdk/service-spec-types';
22
import * as types from './types';
3-
import { deepEqual, diffKeyedEntities } from './util';
3+
import { deepEqual, diffKeyedEntities, loadResourceModel } from './util';
44

55
export function diffAttribute(oldValue: any, newValue: any): types.Difference<string> {
66
return new types.Difference<string>(_asString(oldValue), _asString(newValue));
@@ -36,8 +36,7 @@ export function diffResource(oldValue?: types.Resource, newValue?: types.Resourc
3636

3737
if (resourceType.oldType !== undefined && resourceType.oldType === resourceType.newType) {
3838
// Only makes sense to inspect deeper if the types stayed the same
39-
const typeSpec = cfnspec.filteredSpecification(resourceType.oldType);
40-
const impl = typeSpec.ResourceTypes[resourceType.oldType];
39+
const impl = loadResourceModel(resourceType.oldType);
4140
propertyDiffs = diffKeyedEntities(oldValue!.Properties,
4241
newValue!.Properties,
4342
(oldVal, newVal, key) => _diffProperty(oldVal, newVal, key, impl));
@@ -50,16 +49,16 @@ export function diffResource(oldValue?: types.Resource, newValue?: types.Resourc
5049
resourceType, propertyDiffs, otherDiffs,
5150
});
5251

53-
function _diffProperty(oldV: any, newV: any, key: string, resourceSpec?: cfnspec.schema.ResourceType) {
52+
function _diffProperty(oldV: any, newV: any, key: string, resourceSpec?: Resource) {
5453
let changeImpact = types.ResourceImpact.NO_CHANGE;
5554

56-
const spec = resourceSpec && resourceSpec.Properties && resourceSpec.Properties[key];
55+
const spec = resourceSpec?.properties?.[key];
5756
if (spec && !deepEqual(oldV, newV)) {
58-
switch (spec.UpdateType) {
59-
case cfnspec.schema.UpdateType.Immutable:
57+
switch (spec.causesReplacement) {
58+
case 'yes':
6059
changeImpact = types.ResourceImpact.WILL_REPLACE;
6160
break;
62-
case cfnspec.schema.UpdateType.Conditional:
61+
case 'maybe':
6362
changeImpact = types.ResourceImpact.MAY_REPLACE;
6463
break;
6564
default:

packages/@aws-cdk/cloudformation-diff/lib/diff/types.ts

+55-38
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { AssertionError } from 'assert';
2-
import * as cfnspec from '@aws-cdk/cfnspec';
3-
import { deepEqual } from './util';
2+
import { PropertyScrutinyType, ResourceScrutinyType, Resource as ResourceModel } from '@aws-cdk/service-spec-types';
3+
import { deepEqual, loadResourceModel } from './util';
44
import { IamChanges } from '../iam/iam-changes';
55
import { SecurityGroupChanges } from '../network/security-group-changes';
66

@@ -55,10 +55,10 @@ export class TemplateDiff implements ITemplateDiff {
5555
});
5656

5757
this.securityGroupChanges = new SecurityGroupChanges({
58-
egressRulePropertyChanges: this.scrutinizablePropertyChanges([cfnspec.schema.PropertyScrutinyType.EgressRules]),
59-
ingressRulePropertyChanges: this.scrutinizablePropertyChanges([cfnspec.schema.PropertyScrutinyType.IngressRules]),
60-
egressRuleResourceChanges: this.scrutinizableResourceChanges([cfnspec.schema.ResourceScrutinyType.EgressRuleResource]),
61-
ingressRuleResourceChanges: this.scrutinizableResourceChanges([cfnspec.schema.ResourceScrutinyType.IngressRuleResource]),
58+
egressRulePropertyChanges: this.scrutinizablePropertyChanges([PropertyScrutinyType.EgressRules]),
59+
ingressRulePropertyChanges: this.scrutinizablePropertyChanges([PropertyScrutinyType.IngressRules]),
60+
egressRuleResourceChanges: this.scrutinizableResourceChanges([ResourceScrutinyType.EgressRuleResource]),
61+
ingressRuleResourceChanges: this.scrutinizableResourceChanges([ResourceScrutinyType.IngressRuleResource]),
6262
});
6363
}
6464

@@ -110,7 +110,7 @@ export class TemplateDiff implements ITemplateDiff {
110110
* We don't just look at property updates; we also look at resource additions and deletions (in which
111111
* case there is no further detail on property values), and resource type changes.
112112
*/
113-
private scrutinizablePropertyChanges(scrutinyTypes: cfnspec.schema.PropertyScrutinyType[]): PropertyChange[] {
113+
private scrutinizablePropertyChanges(scrutinyTypes: PropertyScrutinyType[]): PropertyChange[] {
114114
const ret = new Array<PropertyChange>();
115115

116116
for (const [resourceLogicalId, resourceChange] of Object.entries(this.resources.changes)) {
@@ -119,16 +119,23 @@ export class TemplateDiff implements ITemplateDiff {
119119
continue;
120120
}
121121

122-
const props = cfnspec.scrutinizablePropertyNames(resourceChange.newResourceType!, scrutinyTypes);
123-
for (const propertyName of props) {
124-
ret.push({
125-
resourceLogicalId,
126-
propertyName,
127-
resourceType: resourceChange.resourceType,
128-
scrutinyType: cfnspec.propertySpecification(resourceChange.resourceType, propertyName).ScrutinyType!,
129-
oldValue: resourceChange.oldProperties && resourceChange.oldProperties[propertyName],
130-
newValue: resourceChange.newProperties && resourceChange.newProperties[propertyName],
131-
});
122+
if (!resourceChange.newResourceType) {
123+
continue;
124+
}
125+
126+
const newTypeProps = loadResourceModel(resourceChange.newResourceType)?.properties || {};
127+
for (const [propertyName, prop] of Object.entries(newTypeProps)) {
128+
const propScrutinyType = prop.scrutinizable || PropertyScrutinyType.None;
129+
if (scrutinyTypes.includes(propScrutinyType)) {
130+
ret.push({
131+
resourceLogicalId,
132+
propertyName,
133+
resourceType: resourceChange.resourceType,
134+
scrutinyType: propScrutinyType,
135+
oldValue: resourceChange.oldProperties?.[propertyName],
136+
newValue: resourceChange.newProperties?.[propertyName],
137+
});
138+
}
132139
}
133140
}
134141

@@ -141,11 +148,9 @@ export class TemplateDiff implements ITemplateDiff {
141148
* We don't just look at resource updates; we also look at resource additions and deletions (in which
142149
* case there is no further detail on property values), and resource type changes.
143150
*/
144-
private scrutinizableResourceChanges(scrutinyTypes: cfnspec.schema.ResourceScrutinyType[]): ResourceChange[] {
151+
private scrutinizableResourceChanges(scrutinyTypes: ResourceScrutinyType[]): ResourceChange[] {
145152
const ret = new Array<ResourceChange>();
146153

147-
const scrutinizableTypes = new Set(cfnspec.scrutinizableResourceTypes(scrutinyTypes));
148-
149154
for (const [resourceLogicalId, resourceChange] of Object.entries(this.resources.changes)) {
150155
if (!resourceChange) { continue; }
151156

@@ -158,35 +163,47 @@ export class TemplateDiff implements ITemplateDiff {
158163
// changes to the Type of resources can happen when migrating from CFN templates that use Transforms
159164
if (resourceChange.resourceTypeChanged) {
160165
// Treat as DELETE+ADD
161-
if (scrutinizableTypes.has(resourceChange.oldResourceType!)) {
162-
ret.push({
163-
...commonProps,
164-
newProperties: undefined,
165-
resourceType: resourceChange.oldResourceType!,
166-
scrutinyType: cfnspec.resourceSpecification(resourceChange.oldResourceType!).ScrutinyType!,
167-
});
166+
if (resourceChange.oldResourceType) {
167+
const oldResourceModel = loadResourceModel(resourceChange.oldResourceType);
168+
if (oldResourceModel && this.resourceIsScrutinizable(oldResourceModel, scrutinyTypes)) {
169+
ret.push({
170+
...commonProps,
171+
newProperties: undefined,
172+
resourceType: resourceChange.oldResourceType!,
173+
scrutinyType: oldResourceModel.scrutinizable!,
174+
});
175+
}
168176
}
169-
if (scrutinizableTypes.has(resourceChange.newResourceType!)) {
170-
ret.push({
171-
...commonProps,
172-
oldProperties: undefined,
173-
resourceType: resourceChange.newResourceType!,
174-
scrutinyType: cfnspec.resourceSpecification(resourceChange.newResourceType!).ScrutinyType!,
175-
});
177+
178+
if (resourceChange.newResourceType) {
179+
const newResourceModel = loadResourceModel(resourceChange.newResourceType);
180+
if (newResourceModel && this.resourceIsScrutinizable(newResourceModel, scrutinyTypes)) {
181+
ret.push({
182+
...commonProps,
183+
oldProperties: undefined,
184+
resourceType: resourceChange.newResourceType!,
185+
scrutinyType: newResourceModel.scrutinizable!,
186+
});
187+
}
176188
}
177189
} else {
178-
if (scrutinizableTypes.has(resourceChange.resourceType)) {
190+
const resourceModel = loadResourceModel(resourceChange.resourceType);
191+
if (resourceModel && this.resourceIsScrutinizable(resourceModel, scrutinyTypes)) {
179192
ret.push({
180193
...commonProps,
181194
resourceType: resourceChange.resourceType,
182-
scrutinyType: cfnspec.resourceSpecification(resourceChange.resourceType).ScrutinyType!,
195+
scrutinyType: resourceModel.scrutinizable!,
183196
});
184197
}
185198
}
186199
}
187200

188201
return ret;
189202
}
203+
204+
private resourceIsScrutinizable(res: ResourceModel, scrutinyTypes: Array<ResourceScrutinyType>): boolean {
205+
return scrutinyTypes.includes(res.scrutinizable || ResourceScrutinyType.None);
206+
}
190207
}
191208

192209
/**
@@ -211,7 +228,7 @@ export interface PropertyChange {
211228
/**
212229
* Scrutiny type for this property change
213230
*/
214-
scrutinyType: cfnspec.schema.PropertyScrutinyType;
231+
scrutinyType: PropertyScrutinyType;
215232

216233
/**
217234
* Name of the property that is changing
@@ -243,7 +260,7 @@ export interface ResourceChange {
243260
/**
244261
* Scrutiny type for this resource change
245262
*/
246-
scrutinyType: cfnspec.schema.ResourceScrutinyType;
263+
scrutinyType: ResourceScrutinyType;
247264

248265
/**
249266
* The type of the resource

packages/@aws-cdk/cloudformation-diff/lib/diff/util.ts

+23
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import { loadAwsServiceSpecSync } from '@aws-cdk/aws-service-spec';
2+
import { Resource, SpecDatabase } from '@aws-cdk/service-spec-types';
3+
14
/**
25
* Compares two objects for equality, deeply. The function handles arguments that are
36
* +null+, +undefined+, arrays and objects. For objects, the function will not take the
@@ -163,3 +166,23 @@ export function mangleLikeCloudFormation(payload: string) {
163166
function safeParseFloat(str: string): number {
164167
return Number(str);
165168
}
169+
170+
/**
171+
* Lazily load the service spec database and cache the loaded db
172+
*/
173+
let DATABASE: SpecDatabase | undefined;
174+
function database(): SpecDatabase {
175+
if (!DATABASE) {
176+
DATABASE = loadAwsServiceSpecSync();
177+
}
178+
return DATABASE;
179+
}
180+
181+
/**
182+
* Load a Resource model from the Service Spec Database
183+
*
184+
* The database is loaded lazily and cached across multiple calls to `loadResourceModel`.
185+
*/
186+
export function loadResourceModel(type: string): Resource | undefined {
187+
return database().lookup('resource', 'cloudFormationType', 'equals', type).at(0);
188+
}

packages/@aws-cdk/cloudformation-diff/lib/format.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -305,12 +305,12 @@ class Formatter {
305305
for (const [logicalId, resourceDiff] of Object.entries(templateDiff.resources)) {
306306
if (!resourceDiff) { continue; }
307307

308-
const oldPathMetadata = resourceDiff.oldValue && resourceDiff.oldValue.Metadata && resourceDiff.oldValue.Metadata[PATH_METADATA_KEY];
308+
const oldPathMetadata = resourceDiff.oldValue?.Metadata?.[PATH_METADATA_KEY];
309309
if (oldPathMetadata && !(logicalId in this.logicalToPathMap)) {
310310
this.logicalToPathMap[logicalId] = oldPathMetadata;
311311
}
312312

313-
const newPathMetadata = resourceDiff.newValue && resourceDiff.newValue.Metadata && resourceDiff.newValue.Metadata[PATH_METADATA_KEY];
313+
const newPathMetadata = resourceDiff.newValue?.Metadata?.[PATH_METADATA_KEY];
314314
if (newPathMetadata && !(logicalId in this.logicalToPathMap)) {
315315
this.logicalToPathMap[logicalId] = newPathMetadata;
316316
}

packages/@aws-cdk/cloudformation-diff/lib/iam/iam-changes.ts

+13-13
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import * as cfnspec from '@aws-cdk/cfnspec';
1+
import { PropertyScrutinyType, ResourceScrutinyType } from '@aws-cdk/service-spec-types';
22
import * as chalk from 'chalk';
33
import { ManagedPolicyAttachment, ManagedPolicyJson } from './managed-policy';
44
import { parseLambdaPermission, parseStatements, Statement, StatementJson } from './statement';
@@ -18,15 +18,15 @@ export interface IamChangesProps {
1818
*/
1919
export class IamChanges {
2020
public static IamPropertyScrutinies = [
21-
cfnspec.schema.PropertyScrutinyType.InlineIdentityPolicies,
22-
cfnspec.schema.PropertyScrutinyType.InlineResourcePolicy,
23-
cfnspec.schema.PropertyScrutinyType.ManagedPolicies,
21+
PropertyScrutinyType.InlineIdentityPolicies,
22+
PropertyScrutinyType.InlineResourcePolicy,
23+
PropertyScrutinyType.ManagedPolicies,
2424
];
2525

2626
public static IamResourceScrutinies = [
27-
cfnspec.schema.ResourceScrutinyType.ResourcePolicyResource,
28-
cfnspec.schema.ResourceScrutinyType.IdentityPolicyResource,
29-
cfnspec.schema.ResourceScrutinyType.LambdaPermission,
27+
ResourceScrutinyType.ResourcePolicyResource,
28+
ResourceScrutinyType.IdentityPolicyResource,
29+
ResourceScrutinyType.LambdaPermission,
3030
];
3131

3232
public readonly statements = new DiffableCollection<Statement>();
@@ -144,17 +144,17 @@ export class IamChanges {
144144

145145
private readPropertyChange(propertyChange: PropertyChange) {
146146
switch (propertyChange.scrutinyType) {
147-
case cfnspec.schema.PropertyScrutinyType.InlineIdentityPolicies:
147+
case PropertyScrutinyType.InlineIdentityPolicies:
148148
// AWS::IAM::{ Role | User | Group }.Policies
149149
this.statements.addOld(...this.readIdentityPolicies(propertyChange.oldValue, propertyChange.resourceLogicalId));
150150
this.statements.addNew(...this.readIdentityPolicies(propertyChange.newValue, propertyChange.resourceLogicalId));
151151
break;
152-
case cfnspec.schema.PropertyScrutinyType.InlineResourcePolicy:
152+
case PropertyScrutinyType.InlineResourcePolicy:
153153
// Any PolicyDocument on a resource (including AssumeRolePolicyDocument)
154154
this.statements.addOld(...this.readResourceStatements(propertyChange.oldValue, propertyChange.resourceLogicalId));
155155
this.statements.addNew(...this.readResourceStatements(propertyChange.newValue, propertyChange.resourceLogicalId));
156156
break;
157-
case cfnspec.schema.PropertyScrutinyType.ManagedPolicies:
157+
case PropertyScrutinyType.ManagedPolicies:
158158
// Just a list of managed policies
159159
this.managedPolicies.addOld(...this.readManagedPolicies(propertyChange.oldValue, propertyChange.resourceLogicalId));
160160
this.managedPolicies.addNew(...this.readManagedPolicies(propertyChange.newValue, propertyChange.resourceLogicalId));
@@ -164,17 +164,17 @@ export class IamChanges {
164164

165165
private readResourceChange(resourceChange: ResourceChange) {
166166
switch (resourceChange.scrutinyType) {
167-
case cfnspec.schema.ResourceScrutinyType.IdentityPolicyResource:
167+
case ResourceScrutinyType.IdentityPolicyResource:
168168
// AWS::IAM::Policy
169169
this.statements.addOld(...this.readIdentityPolicyResource(resourceChange.oldProperties));
170170
this.statements.addNew(...this.readIdentityPolicyResource(resourceChange.newProperties));
171171
break;
172-
case cfnspec.schema.ResourceScrutinyType.ResourcePolicyResource:
172+
case ResourceScrutinyType.ResourcePolicyResource:
173173
// AWS::*::{Bucket,Queue,Topic}Policy
174174
this.statements.addOld(...this.readResourcePolicyResource(resourceChange.oldProperties));
175175
this.statements.addNew(...this.readResourcePolicyResource(resourceChange.newProperties));
176176
break;
177-
case cfnspec.schema.ResourceScrutinyType.LambdaPermission:
177+
case ResourceScrutinyType.LambdaPermission:
178178
this.statements.addOld(...this.readLambdaStatements(resourceChange.oldProperties));
179179
this.statements.addNew(...this.readLambdaStatements(resourceChange.newProperties));
180180
break;

packages/@aws-cdk/cloudformation-diff/lib/util.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -46,4 +46,4 @@ export function flatMap<T, U>(xs: T[], f: (x: T) => U[]): U[] {
4646
ret.push(...f(x));
4747
}
4848
return ret;
49-
}
49+
}

packages/@aws-cdk/cloudformation-diff/package.json

+7-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@
2323
},
2424
"license": "Apache-2.0",
2525
"dependencies": {
26-
"@aws-cdk/cfnspec": "0.0.0",
26+
"@aws-cdk/aws-service-spec": "^0.0.26",
27+
"@aws-cdk/service-spec-types": "^0.0.26",
2728
"chalk": "^4",
2829
"diff": "^5.1.0",
2930
"fast-deep-equal": "^3.1.3",
@@ -56,5 +57,10 @@
5657
"maturity": "stable",
5758
"publishConfig": {
5859
"tag": "latest"
60+
},
61+
"pkglint": {
62+
"exclude": [
63+
"dependencies/cdk-point-dependencies"
64+
]
5965
}
6066
}

packages/@aws-cdk/integ-runner/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@
4444
"BSD-2-Clause",
4545
"0BSD"
4646
],
47-
"dontAttribute": "^@aws-cdk/|^cdk-assets$",
47+
"dontAttribute": "^@aws-cdk/|^@cdklabs/|^cdk-assets$",
4848
"test": "bin/integ-runner --version"
4949
}
5050
},

packages/aws-cdk/.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,4 @@ test/integ/cli/*.d.ts
4141
junit.xml
4242

4343
lib/**/*.wasm
44+
db.json.gz

0 commit comments

Comments
 (0)