Skip to content

Commit

Permalink
feat(cloudformation-diff): use awscdk-service-spec as data source
Browse files Browse the repository at this point in the history
  • Loading branch information
mrgrain committed Nov 3, 2023
1 parent 12fe361 commit a103043
Show file tree
Hide file tree
Showing 6 changed files with 109 additions and 59 deletions.
16 changes: 8 additions & 8 deletions packages/@aws-cdk/cloudformation-diff/lib/diff/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as cfnspec from '@aws-cdk/cfnspec';
import { Resource } from '@aws-cdk/service-spec-types';
import * as types from './types';
import { deepEqual, diffKeyedEntities } from './util';
import { database } from '../model';

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

if (resourceType.oldType !== undefined && resourceType.oldType === resourceType.newType) {
// Only makes sense to inspect deeper if the types stayed the same
const typeSpec = cfnspec.filteredSpecification(resourceType.oldType);
const impl = typeSpec.ResourceTypes[resourceType.oldType];
const impl = database().lookup('resource', 'cloudFormationType', 'equals', resourceType.oldType).only();
propertyDiffs = diffKeyedEntities(oldValue!.Properties,
newValue!.Properties,
(oldVal, newVal, key) => _diffProperty(oldVal, newVal, key, impl));
Expand All @@ -50,16 +50,16 @@ export function diffResource(oldValue?: types.Resource, newValue?: types.Resourc
resourceType, propertyDiffs, otherDiffs,
});

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

const spec = resourceSpec && resourceSpec.Properties && resourceSpec.Properties[key];
const spec = resourceSpec && resourceSpec.properties && resourceSpec.properties[key];
if (spec && !deepEqual(oldV, newV)) {
switch (spec.UpdateType) {
case cfnspec.schema.UpdateType.Immutable:
switch (spec.causesReplacement) {
case 'yes':
changeImpact = types.ResourceImpact.WILL_REPLACE;
break;
case cfnspec.schema.UpdateType.Conditional:
case 'maybe':
changeImpact = types.ResourceImpact.MAY_REPLACE;
break;
default:
Expand Down
92 changes: 55 additions & 37 deletions packages/@aws-cdk/cloudformation-diff/lib/diff/types.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { AssertionError } from 'assert';
import * as cfnspec from '@aws-cdk/cfnspec';
import { PropertyScrutinyType, ResourceScrutinyType, Resource as ResourceModel } from '@aws-cdk/service-spec-types';
import { deepEqual } from './util';
import { IamChanges } from '../iam/iam-changes';
import { database } from '../model';
import { SecurityGroupChanges } from '../network/security-group-changes';

export type PropertyMap = {[key: string]: any };
Expand Down Expand Up @@ -55,10 +56,10 @@ export class TemplateDiff implements ITemplateDiff {
});

this.securityGroupChanges = new SecurityGroupChanges({
egressRulePropertyChanges: this.scrutinizablePropertyChanges([cfnspec.schema.PropertyScrutinyType.EgressRules]),
ingressRulePropertyChanges: this.scrutinizablePropertyChanges([cfnspec.schema.PropertyScrutinyType.IngressRules]),
egressRuleResourceChanges: this.scrutinizableResourceChanges([cfnspec.schema.ResourceScrutinyType.EgressRuleResource]),
ingressRuleResourceChanges: this.scrutinizableResourceChanges([cfnspec.schema.ResourceScrutinyType.IngressRuleResource]),
egressRulePropertyChanges: this.scrutinizablePropertyChanges([PropertyScrutinyType.EgressRules]),
ingressRulePropertyChanges: this.scrutinizablePropertyChanges([PropertyScrutinyType.IngressRules]),
egressRuleResourceChanges: this.scrutinizableResourceChanges([ResourceScrutinyType.EgressRuleResource]),
ingressRuleResourceChanges: this.scrutinizableResourceChanges([ResourceScrutinyType.IngressRuleResource]),
});
}

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

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

const props = cfnspec.scrutinizablePropertyNames(resourceChange.newResourceType!, scrutinyTypes);
for (const propertyName of props) {
ret.push({
resourceLogicalId,
propertyName,
resourceType: resourceChange.resourceType,
scrutinyType: cfnspec.propertySpecification(resourceChange.resourceType, propertyName).ScrutinyType!,
oldValue: resourceChange.oldProperties && resourceChange.oldProperties[propertyName],
newValue: resourceChange.newProperties && resourceChange.newProperties[propertyName],
});
if (!resourceChange.newResourceType) {
continue;
}

const newTypeProps = database().lookup('resource', 'cloudFormationType', 'equals', resourceChange.newResourceType).only().properties || {};
for (const [propertyName, prop] of Object.entries(newTypeProps)) {
const propScrutinyType = prop.scrutinizable || PropertyScrutinyType.None;
if (scrutinyTypes.includes(propScrutinyType)) {
ret.push({
resourceLogicalId,
propertyName,
resourceType: resourceChange.resourceType,
scrutinyType: propScrutinyType,
oldValue: resourceChange.oldProperties && resourceChange.oldProperties[propertyName],
newValue: resourceChange.newProperties && resourceChange.newProperties[propertyName],
});
}
}
}

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

const scrutinizableTypes = new Set(cfnspec.scrutinizableResourceTypes(scrutinyTypes));

for (const [resourceLogicalId, resourceChange] of Object.entries(this.resources.changes)) {
if (!resourceChange) { continue; }

Expand All @@ -158,35 +164,47 @@ export class TemplateDiff implements ITemplateDiff {
// changes to the Type of resources can happen when migrating from CFN templates that use Transforms
if (resourceChange.resourceTypeChanged) {
// Treat as DELETE+ADD
if (scrutinizableTypes.has(resourceChange.oldResourceType!)) {
ret.push({
...commonProps,
newProperties: undefined,
resourceType: resourceChange.oldResourceType!,
scrutinyType: cfnspec.resourceSpecification(resourceChange.oldResourceType!).ScrutinyType!,
});
if (resourceChange.oldResourceType) {
const oldResourceModel = database().lookup('resource', 'cloudFormationType', 'equals', resourceChange.oldResourceType).only();
if (this.resourceIsScrutinizable(oldResourceModel, scrutinyTypes)) {
ret.push({
...commonProps,
newProperties: undefined,
resourceType: resourceChange.oldResourceType!,
scrutinyType: oldResourceModel.scrutinizable!,
});
}
}
if (scrutinizableTypes.has(resourceChange.newResourceType!)) {
ret.push({
...commonProps,
oldProperties: undefined,
resourceType: resourceChange.newResourceType!,
scrutinyType: cfnspec.resourceSpecification(resourceChange.newResourceType!).ScrutinyType!,
});

if (resourceChange.newResourceType) {
const newResourceModel = database().lookup('resource', 'cloudFormationType', 'equals', resourceChange.newResourceType).only();
if (this.resourceIsScrutinizable(newResourceModel, scrutinyTypes)) {
ret.push({
...commonProps,
oldProperties: undefined,
resourceType: resourceChange.newResourceType!,
scrutinyType: newResourceModel.scrutinizable!,
});
}
}
} else {
if (scrutinizableTypes.has(resourceChange.resourceType)) {
const resourceModel = database().lookup('resource', 'cloudFormationType', 'equals', resourceChange.resourceType).only();
if (this.resourceIsScrutinizable(resourceModel, scrutinyTypes)) {
ret.push({
...commonProps,
resourceType: resourceChange.resourceType,
scrutinyType: cfnspec.resourceSpecification(resourceChange.resourceType).ScrutinyType!,
scrutinyType: resourceModel.scrutinizable!,
});
}
}
}

return ret;
}

private resourceIsScrutinizable(resource: ResourceModel, scrutinyTypes: Array<ResourceScrutinyType>): boolean {
return scrutinyTypes.includes(resource.scrutinizable || ResourceScrutinyType.None);
}
}

/**
Expand All @@ -211,7 +229,7 @@ export interface PropertyChange {
/**
* Scrutiny type for this property change
*/
scrutinyType: cfnspec.schema.PropertyScrutinyType;
scrutinyType: PropertyScrutinyType;

/**
* Name of the property that is changing
Expand Down Expand Up @@ -243,7 +261,7 @@ export interface ResourceChange {
/**
* Scrutiny type for this resource change
*/
scrutinyType: cfnspec.schema.ResourceScrutinyType;
scrutinyType: ResourceScrutinyType;

/**
* The type of the resource
Expand Down
26 changes: 13 additions & 13 deletions packages/@aws-cdk/cloudformation-diff/lib/iam/iam-changes.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import * as cfnspec from '@aws-cdk/cfnspec';
import { PropertyScrutinyType, ResourceScrutinyType } from '@aws-cdk/service-spec-types';
import * as chalk from 'chalk';
import { ManagedPolicyAttachment, ManagedPolicyJson } from './managed-policy';
import { parseLambdaPermission, parseStatements, Statement, StatementJson } from './statement';
Expand All @@ -18,15 +18,15 @@ export interface IamChangesProps {
*/
export class IamChanges {
public static IamPropertyScrutinies = [
cfnspec.schema.PropertyScrutinyType.InlineIdentityPolicies,
cfnspec.schema.PropertyScrutinyType.InlineResourcePolicy,
cfnspec.schema.PropertyScrutinyType.ManagedPolicies,
PropertyScrutinyType.InlineIdentityPolicies,
PropertyScrutinyType.InlineResourcePolicy,
PropertyScrutinyType.ManagedPolicies,
];

public static IamResourceScrutinies = [
cfnspec.schema.ResourceScrutinyType.ResourcePolicyResource,
cfnspec.schema.ResourceScrutinyType.IdentityPolicyResource,
cfnspec.schema.ResourceScrutinyType.LambdaPermission,
ResourceScrutinyType.ResourcePolicyResource,
ResourceScrutinyType.IdentityPolicyResource,
ResourceScrutinyType.LambdaPermission,
];

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

private readPropertyChange(propertyChange: PropertyChange) {
switch (propertyChange.scrutinyType) {
case cfnspec.schema.PropertyScrutinyType.InlineIdentityPolicies:
case PropertyScrutinyType.InlineIdentityPolicies:
// AWS::IAM::{ Role | User | Group }.Policies
this.statements.addOld(...this.readIdentityPolicies(propertyChange.oldValue, propertyChange.resourceLogicalId));
this.statements.addNew(...this.readIdentityPolicies(propertyChange.newValue, propertyChange.resourceLogicalId));
break;
case cfnspec.schema.PropertyScrutinyType.InlineResourcePolicy:
case PropertyScrutinyType.InlineResourcePolicy:
// Any PolicyDocument on a resource (including AssumeRolePolicyDocument)
this.statements.addOld(...this.readResourceStatements(propertyChange.oldValue, propertyChange.resourceLogicalId));
this.statements.addNew(...this.readResourceStatements(propertyChange.newValue, propertyChange.resourceLogicalId));
break;
case cfnspec.schema.PropertyScrutinyType.ManagedPolicies:
case PropertyScrutinyType.ManagedPolicies:
// Just a list of managed policies
this.managedPolicies.addOld(...this.readManagedPolicies(propertyChange.oldValue, propertyChange.resourceLogicalId));
this.managedPolicies.addNew(...this.readManagedPolicies(propertyChange.newValue, propertyChange.resourceLogicalId));
Expand All @@ -164,17 +164,17 @@ export class IamChanges {

private readResourceChange(resourceChange: ResourceChange) {
switch (resourceChange.scrutinyType) {
case cfnspec.schema.ResourceScrutinyType.IdentityPolicyResource:
case ResourceScrutinyType.IdentityPolicyResource:
// AWS::IAM::Policy
this.statements.addOld(...this.readIdentityPolicyResource(resourceChange.oldProperties));
this.statements.addNew(...this.readIdentityPolicyResource(resourceChange.newProperties));
break;
case cfnspec.schema.ResourceScrutinyType.ResourcePolicyResource:
case ResourceScrutinyType.ResourcePolicyResource:
// AWS::*::{Bucket,Queue,Topic}Policy
this.statements.addOld(...this.readResourcePolicyResource(resourceChange.oldProperties));
this.statements.addNew(...this.readResourcePolicyResource(resourceChange.newProperties));
break;
case cfnspec.schema.ResourceScrutinyType.LambdaPermission:
case ResourceScrutinyType.LambdaPermission:
this.statements.addOld(...this.readLambdaStatements(resourceChange.oldProperties));
this.statements.addNew(...this.readLambdaStatements(resourceChange.newProperties));
break;
Expand Down
11 changes: 11 additions & 0 deletions packages/@aws-cdk/cloudformation-diff/lib/model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { loadAwsServiceSpecSync } from '@aws-cdk/aws-service-spec';
import { SpecDatabase } from '@aws-cdk/service-spec-types';

let DATABASE: SpecDatabase;

export function database(): SpecDatabase {
if (!DATABASE) {
DATABASE = loadAwsServiceSpecSync();
}
return DATABASE;
}
8 changes: 7 additions & 1 deletion packages/@aws-cdk/cloudformation-diff/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@
},
"license": "Apache-2.0",
"dependencies": {
"@aws-cdk/cfnspec": "0.0.0",
"@aws-cdk/aws-service-spec": "^0.0.25",
"@aws-cdk/service-spec-types": "^0.0.25",
"chalk": "^4",
"diff": "^5.1.0",
"fast-deep-equal": "^3.1.3",
Expand Down Expand Up @@ -56,5 +57,10 @@
"maturity": "stable",
"publishConfig": {
"tag": "latest"
},
"pkglint": {
"exclude": [
"dependencies/cdk-point-dependencies"
]
}
}
15 changes: 15 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,14 @@
"@aws-cdk/service-spec-types" "^0.0.24"
"@cdklabs/tskb" "^0.0.2"

"@aws-cdk/aws-service-spec@^0.0.25":
version "0.0.25"
resolved "https://registry.npmjs.org/@aws-cdk/aws-service-spec/-/aws-service-spec-0.0.25.tgz#2f5eda16c882394fb7533a79ab75bb95ab18c342"
integrity sha512-hJiBmbhN9O7iIe4SOs52tSd4bRFlLEFaq3exe+5SrELiPKZsRxQs9FvRFz1ALadbj9Wno/qkd36D7s7jNhpmVQ==
dependencies:
"@aws-cdk/service-spec-types" "^0.0.25"
"@cdklabs/tskb" "^0.0.2"

"@aws-cdk/lambda-layer-kubectl-v24@^2.0.242":
version "2.0.242"
resolved "https://registry.npmjs.org/@aws-cdk/lambda-layer-kubectl-v24/-/lambda-layer-kubectl-v24-2.0.242.tgz#4273a5ad7714f933a7eba155eb9280823086db71"
Expand Down Expand Up @@ -98,6 +106,13 @@
dependencies:
"@cdklabs/tskb" "^0.0.2"

"@aws-cdk/service-spec-types@^0.0.25":
version "0.0.25"
resolved "https://registry.npmjs.org/@aws-cdk/service-spec-types/-/service-spec-types-0.0.25.tgz#971c5539630fb66abb2d685f76dffef33eabbba1"
integrity sha512-urxao77R7+a9c8Mj3RAt2/ee9cY3pm4lrJHdy6b8qavPYgslYYM7ibk67KzR2Uz24CVWUfxHF7N4EqDK+hKKew==
dependencies:
"@cdklabs/tskb" "^0.0.2"

"@aws-crypto/[email protected]":
version "3.0.0"
resolved "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-3.0.0.tgz#07300eca214409c33e3ff769cd5697b57fdd38fa"
Expand Down

0 comments on commit a103043

Please sign in to comment.