Skip to content

Commit 73f0f0e

Browse files
authored
chore(cfnspec): consume CloudFormation specification in parts (#18210)
Whenever there are errors in the CloudFormation specification, we currently have to fail the build and can't consume anything. To ensure we make some progress, apply the following strategy instead: - Split the spec into fragments, on a per-service basis. - Consume those per-service spec updates that are valid; if updates are invalid, we will leave them at the old version. This will produce an always-building spec, of which certain parts may be outdated. Report the outdated parts in the CHANGELOG. Notifying the CloudFormation team about spec errors is an out-of-band process, and out of scope of this PR. As a side effect of this work, formalize the spec manipulation we do with JSON and patch files into a mini-standard called "JSON Patch Stacks", and add some tools to operate on them. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent 37537fe commit 73f0f0e

File tree

242 files changed

+115036
-112765
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

242 files changed

+115036
-112765
lines changed

packages/@aws-cdk/cfnspec/build-tools/build.ts

Lines changed: 6 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,18 @@
66
*/
77

88
import * as path from 'path';
9-
import * as fs from 'fs-extra';
109
import * as md5 from 'md5';
1110
import { schema } from '../lib';
12-
import { decorateResourceTypes, forEachSection, massageSpec, merge, normalize, patch } from './massage-spec';
11+
import { massageSpec, normalize } from './massage-spec';;
12+
import { writeSorted, applyPatchSet, applyAndWrite } from './patch-set';
1313

1414
async function main() {
1515
const inputDir = path.join(process.cwd(), 'spec-source');
1616
const outDir = path.join(process.cwd(), 'spec');
1717

1818
await generateResourceSpecification(inputDir, path.join(outDir, 'specification.json'));
19-
await mergeSpecificationFromDirs(path.join(inputDir, 'cfn-lint'), path.join(outDir, 'cfn-lint.json'));
20-
await fs.copyFile(path.join(inputDir, 'cfn-docs', 'cfn-docs.json'), path.join(outDir, 'cfn-docs.json'));
19+
await applyAndWrite(path.join(outDir, 'cfn-lint.json'), path.join(inputDir, 'cfn-lint'));
20+
await applyAndWrite(path.join(outDir, 'cfn-docs.json'), path.join(inputDir, 'cfn-docs'));
2121
}
2222

2323
/**
@@ -26,64 +26,11 @@ async function main() {
2626
async function generateResourceSpecification(inputDir: string, outFile: string) {
2727
const spec: schema.Specification = { PropertyTypes: {}, ResourceTypes: {}, Fingerprint: '' };
2828

29-
const files = await fs.readdir(inputDir);
30-
for (const file of files.filter(n => n.endsWith('.json')).sort()) {
31-
const data = await fs.readJson(path.join(inputDir, file));
32-
if (file.indexOf('patch') === -1) {
33-
decorateResourceTypes(data);
34-
forEachSection(spec, data, merge);
35-
} else {
36-
forEachSection(spec, data, patch);
37-
}
38-
}
39-
29+
Object.assign(spec, await applyPatchSet(path.join(inputDir, 'specification')));
4030
massageSpec(spec);
41-
4231
spec.Fingerprint = md5(JSON.stringify(normalize(spec)));
4332

44-
await fs.mkdirp(path.dirname(outFile));
45-
await fs.writeJson(outFile, spec, { spaces: 2 });
46-
}
47-
48-
/**
49-
* Generate Cfnlint spec annotations from sources and patches
50-
*/
51-
async function mergeSpecificationFromDirs(inputDir: string, outFile: string) {
52-
const spec: any = {};
53-
54-
for (const child of await fs.readdir(inputDir)) {
55-
const fullPath = path.join(inputDir, child);
56-
if (!(await fs.stat(fullPath)).isDirectory()) { continue; }
57-
58-
const subspec = await loadMergedSpec(fullPath);
59-
spec[child] = subspec;
60-
}
61-
62-
await fs.mkdirp(path.dirname(outFile));
63-
await fs.writeJson(outFile, spec, { spaces: 2 });
64-
}
65-
66-
/**
67-
* Load all files in the given directory, merge them and apply patches in the order found
68-
*
69-
* The base structure is always an empty object
70-
*/
71-
async function loadMergedSpec(inputDir: string) {
72-
const structure: any = {};
73-
74-
const files = await fs.readdir(inputDir);
75-
for (const file of files.filter(n => n.endsWith('.json')).sort()) {
76-
const data = await fs.readJson(path.join(inputDir, file));
77-
if (file.indexOf('patch') === -1) {
78-
// Copy properties from current object into structure, adding/overwriting whatever is found
79-
Object.assign(structure, data);
80-
} else {
81-
// Apply the loaded file as a patch onto the current structure
82-
patch(structure, data);
83-
}
84-
}
85-
86-
return structure;
33+
await writeSorted(outFile, spec);
8734
}
8835

8936
main()

packages/@aws-cdk/cfnspec/build-tools/massage-spec.ts

Lines changed: 0 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import * as fastJsonPatch from 'fast-json-patch';
21
import { schema } from '../lib';
32
import { detectScrutinyTypes } from './scrutiny';
43

@@ -8,22 +7,6 @@ export function massageSpec(spec: schema.Specification) {
87
dropTypelessAttributes(spec);
98
}
109

11-
export function forEachSection(spec: schema.Specification, data: any, cb: (spec: any, fragment: any, path: string[]) => void) {
12-
cb(spec.PropertyTypes, data.PropertyTypes, ['PropertyTypes']);
13-
cb(spec.ResourceTypes, data.ResourceTypes, ['ResourceTypes']);
14-
// Per-resource specs are keyed on ResourceType (singular), but we want it in ResourceTypes (plural)
15-
cb(spec.ResourceTypes, data.ResourceType, ['ResourceType']);
16-
}
17-
18-
export function decorateResourceTypes(data: any) {
19-
const requiredTransform = data.ResourceSpecificationTransform as string | undefined;
20-
if (!requiredTransform) { return; }
21-
const resourceTypes = data.ResourceTypes || data.ResourceType;
22-
for (const name of Object.keys(resourceTypes)) {
23-
resourceTypes[name].RequiredTransform = requiredTransform;
24-
}
25-
}
26-
2710
/**
2811
* Fix incomplete type definitions in PropertyTypes
2912
*
@@ -63,40 +46,6 @@ function dropTypelessAttributes(spec: schema.Specification) {
6346
});
6447
}
6548

66-
export function merge(spec: any, fragment: any, jsonPath: string[]) {
67-
if (!fragment) { return; }
68-
for (const key of Object.keys(fragment)) {
69-
if (key in spec) {
70-
const specVal = spec[key];
71-
const fragVal = fragment[key];
72-
if (typeof specVal !== typeof fragVal) {
73-
// eslint-disable-next-line max-len
74-
throw new Error(`Attempted to merge ${JSON.stringify(fragVal)} into incompatible ${JSON.stringify(specVal)} at path ${jsonPath.join('/')}/${key}`);
75-
}
76-
if (typeof specVal !== 'object') {
77-
// eslint-disable-next-line max-len
78-
throw new Error(`Conflict when attempting to merge ${JSON.stringify(fragVal)} into ${JSON.stringify(specVal)} at path ${jsonPath.join('/')}/${key}`);
79-
}
80-
merge(specVal, fragVal, [...jsonPath, key]);
81-
} else {
82-
spec[key] = fragment[key];
83-
}
84-
}
85-
}
86-
87-
export function patch(spec: any, fragment: any) {
88-
if (!fragment) { return; }
89-
if ('patch' in fragment) {
90-
// eslint-disable-next-line no-console
91-
console.log(`Applying patch: ${fragment.patch.description}`);
92-
fastJsonPatch.applyPatch(spec, fragment.patch.operations);
93-
} else {
94-
for (const key of Object.keys(fragment)) {
95-
patch(spec[key], fragment[key]);
96-
}
97-
}
98-
}
99-
10049
/**
10150
* Modifies the provided specification so that ``ResourceTypes`` and ``PropertyTypes`` are listed in alphabetical order.
10251
*

0 commit comments

Comments
 (0)