Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit dca97c6

Browse files
author
Elad Ben-Israel
committedFeb 27, 2019
start establishing the concept of "artifacts"
Start moving towards the cloud-assembly specification where an output of a CDK program is a bunch of artifacts and those are processed by the toolkit. Related #956 Related #233 Related #1119
1 parent 297b66b commit dca97c6

File tree

10 files changed

+932
-267
lines changed

10 files changed

+932
-267
lines changed
 

‎design/cloud-assembly.md

+473
Large diffs are not rendered by default.

‎packages/@aws-cdk/cdk/lib/app.ts

+13-137
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import cxapi = require('@aws-cdk/cx-api');
2-
import { Stack } from './cloudformation/stack';
3-
import { IConstruct, Root } from './core/construct';
4-
import { InMemorySynthesisSession, ISynthesisSession, SynthesisSession } from './synthesis';
2+
import { Root } from './core/construct';
3+
import { FileSystemStore, InMemoryStore, ISynthesisSession, SynthesisSession } from './synthesis';
54

65
/**
76
* Represents a CDK program.
@@ -18,22 +17,6 @@ export class App extends Root {
1817
this.loadContext();
1918
}
2019

21-
private get stacks() {
22-
const out: { [name: string]: Stack } = { };
23-
collectStacks(this);
24-
return out;
25-
26-
function collectStacks(c: IConstruct) {
27-
for (const child of c.node.children) {
28-
if (Stack.isStack(child)) {
29-
out[child.node.id] = child; // TODO: this should probably be changed to uniqueId
30-
}
31-
32-
collectStacks(child);
33-
}
34-
}
35-
}
36-
3720
/**
3821
* Runs the program. Output is written to output directory as specified in the request.
3922
*/
@@ -44,13 +27,14 @@ export class App extends Root {
4427
}
4528

4629
const outdir = process.env[cxapi.OUTDIR_ENV];
30+
let store;
4731
if (outdir) {
48-
this._session = new SynthesisSession({ outdir });
32+
store = new FileSystemStore({ outdir });
4933
} else {
50-
this._session = new InMemorySynthesisSession();
34+
store = new InMemoryStore();
5135
}
5236

53-
const session = this._session;
37+
const session = this._session = new SynthesisSession(store);
5438

5539
// the three holy phases of synthesis: prepare, validate and synthesize
5640

@@ -67,18 +51,7 @@ export class App extends Root {
6751
// synthesize
6852
this.node.synthesizeTree(session);
6953

70-
// write the entrypoint/manifest of this app. It includes a *copy* of the
71-
// synthesized stack output for backwards compatibility
72-
73-
const manifest: cxapi.SynthesizeResponse = {
74-
version: cxapi.PROTO_RESPONSE_VERSION,
75-
stacks: Object.values(this.stacks).map(s => this.readSynthesizedStack(session, s.artifactName)),
76-
runtime: this.collectRuntimeInformation()
77-
};
78-
79-
session.writeFile(cxapi.OUTFILE_NAME, JSON.stringify(manifest, undefined, 2));
80-
81-
// lock session - cannot emit more artifacts
54+
// write session manifest and lock store
8255
session.finalize();
8356

8457
return session;
@@ -90,9 +63,13 @@ export class App extends Root {
9063
* @deprecated This method is going to be deprecated in a future version of the CDK
9164
*/
9265
public synthesizeStack(stackName: string): cxapi.SynthesizedStack {
93-
const stack = this.getStack(stackName);
9466
const session = this.run();
95-
return this.readSynthesizedStack(session, stack.artifactName);
67+
const res = session.manifest.stacks.find(s => s.name === stackName);
68+
if (!res) {
69+
throw new Error(`Stack "${stackName}" not found`);
70+
}
71+
72+
return res;
9673
}
9774

9875
/**
@@ -107,46 +84,6 @@ export class App extends Root {
10784
return ret;
10885
}
10986

110-
private readSynthesizedStack(session: ISynthesisSession, artifactName: string) {
111-
return JSON.parse(session.readFile(artifactName).toString());
112-
}
113-
114-
private collectRuntimeInformation(): cxapi.AppRuntime {
115-
const libraries: { [name: string]: string } = {};
116-
117-
for (const fileName of Object.keys(require.cache)) {
118-
const pkg = findNpmPackage(fileName);
119-
if (pkg && !pkg.private) {
120-
libraries[pkg.name] = pkg.version;
121-
}
122-
}
123-
124-
// include only libraries that are in the @aws-cdk npm scope
125-
for (const name of Object.keys(libraries)) {
126-
if (!name.startsWith('@aws-cdk/')) {
127-
delete libraries[name];
128-
}
129-
}
130-
131-
// add jsii runtime version
132-
libraries['jsii-runtime'] = getJsiiAgentVersion();
133-
134-
return { libraries };
135-
}
136-
137-
private getStack(stackname: string) {
138-
if (stackname == null) {
139-
throw new Error('Stack name must be defined');
140-
}
141-
142-
const stack = this.stacks[stackname];
143-
144-
if (!stack) {
145-
throw new Error(`Cannot find stack ${stackname}`);
146-
}
147-
return stack;
148-
}
149-
15087
private loadContext() {
15188
const contextJson = process.env[cxapi.CONTEXT_ENV];
15289
const context = !contextJson ? { } : JSON.parse(contextJson);
@@ -155,64 +92,3 @@ export class App extends Root {
15592
}
15693
}
15794
}
158-
159-
/**
160-
* Determines which NPM module a given loaded javascript file is from.
161-
*
162-
* The only infromation that is available locally is a list of Javascript files,
163-
* and every source file is associated with a search path to resolve the further
164-
* ``require`` calls made from there, which includes its own directory on disk,
165-
* and parent directories - for example:
166-
*
167-
* [ '...repo/packages/aws-cdk-resources/lib/cfn/node_modules',
168-
* '...repo/packages/aws-cdk-resources/lib/node_modules',
169-
* '...repo/packages/aws-cdk-resources/node_modules',
170-
* '...repo/packages/node_modules',
171-
* // etc...
172-
* ]
173-
*
174-
* We are looking for ``package.json`` that is anywhere in the tree, except it's
175-
* in the parent directory, not in the ``node_modules`` directory. For this
176-
* reason, we strip the ``/node_modules`` suffix off each path and use regular
177-
* module resolution to obtain a reference to ``package.json``.
178-
*
179-
* @param fileName a javascript file name.
180-
* @returns the NPM module infos (aka ``package.json`` contents), or
181-
* ``undefined`` if the lookup was unsuccessful.
182-
*/
183-
function findNpmPackage(fileName: string): { name: string, version: string, private?: boolean } | undefined {
184-
const mod = require.cache[fileName];
185-
const paths = mod.paths.map(stripNodeModules);
186-
187-
try {
188-
const packagePath = require.resolve('package.json', { paths });
189-
return require(packagePath);
190-
} catch (e) {
191-
return undefined;
192-
}
193-
194-
/**
195-
* @param s a path.
196-
* @returns ``s`` with any terminating ``/node_modules``
197-
* (or ``\\node_modules``) stripped off.)
198-
*/
199-
function stripNodeModules(s: string): string {
200-
if (s.endsWith('/node_modules') || s.endsWith('\\node_modules')) {
201-
// /node_modules is 13 characters
202-
return s.substr(0, s.length - 13);
203-
}
204-
return s;
205-
}
206-
}
207-
208-
function getJsiiAgentVersion() {
209-
let jsiiAgent = process.env.JSII_AGENT;
210-
211-
// if JSII_AGENT is not specified, we will assume this is a node.js runtime
212-
// and plug in our node.js version
213-
if (!jsiiAgent) {
214-
jsiiAgent = `node.js/${process.version}`;
215-
}
216-
217-
return jsiiAgent;
218-
}

‎packages/@aws-cdk/cdk/lib/cloudformation/stack.ts

+12-23
Original file line numberDiff line numberDiff line change
@@ -78,11 +78,6 @@ export class Stack extends Construct {
7878
*/
7979
public readonly name: string;
8080

81-
/**
82-
* The name of the CDK artifact produced by this stack.
83-
*/
84-
public readonly artifactName: string;
85-
8681
/*
8782
* Used to determine if this construct is a stack.
8883
*/
@@ -112,8 +107,6 @@ export class Stack extends Construct {
112107

113108
this.logicalIds = new LogicalIDs(props && props.namingScheme ? props.namingScheme : new HashedAddressingScheme());
114109
this.name = this.node.id;
115-
116-
this.artifactName = `${this.node.uniqueId}.stack.json`;
117110
}
118111

119112
/**
@@ -428,25 +421,21 @@ export class Stack extends Construct {
428421
protected synthesize(session: ISynthesisSession): void {
429422
const account = this.env.account || 'unknown-account';
430423
const region = this.env.region || 'unknown-region';
431-
432-
const environment: cxapi.Environment = {
433-
name: `${account}/${region}`,
434-
account,
435-
region
436-
};
437-
438424
const missing = Object.keys(this.missingContext).length ? this.missingContext : undefined;
425+
const template = `${this.node.id}.template.json`;
439426

440-
const output: cxapi.SynthesizedStack = {
441-
name: this.node.id,
442-
template: this.toCloudFormation(),
443-
environment,
444-
missing,
445-
metadata: this.collectMetadata(),
446-
dependsOn: noEmptyArray(this.dependencies().map(s => s.node.id)),
447-
};
427+
// write the CloudFormation template as a JSON file
428+
session.store.writeJson(template, this.toCloudFormation());
448429

449-
session.writeFile(this.artifactName, JSON.stringify(output, undefined, 2));
430+
// add an artifact that represents this stack
431+
session.addArtifact(this.node.id, {
432+
type: cxapi.ArtifactType.CloudFormationStack,
433+
dependencies: noEmptyArray(this.dependencies().map(s => s.node.id)),
434+
environment: `aws://${account}/${region}`,
435+
metadata: this.collectMetadata(),
436+
missing,
437+
properties: { template }
438+
});
450439
}
451440

452441
/**
+85
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import cxapi = require('@aws-cdk/cx-api');
2+
3+
export function collectRuntimeInformation(): cxapi.AppRuntime {
4+
const libraries: { [name: string]: string } = {};
5+
6+
for (const fileName of Object.keys(require.cache)) {
7+
const pkg = findNpmPackage(fileName);
8+
if (pkg && !pkg.private) {
9+
libraries[pkg.name] = pkg.version;
10+
}
11+
}
12+
13+
// include only libraries that are in the @aws-cdk npm scope
14+
for (const name of Object.keys(libraries)) {
15+
if (!name.startsWith('@aws-cdk/')) {
16+
delete libraries[name];
17+
}
18+
}
19+
20+
// add jsii runtime version
21+
libraries['jsii-runtime'] = getJsiiAgentVersion();
22+
23+
return { libraries };
24+
}
25+
26+
/**
27+
* Determines which NPM module a given loaded javascript file is from.
28+
*
29+
* The only infromation that is available locally is a list of Javascript files,
30+
* and every source file is associated with a search path to resolve the further
31+
* ``require`` calls made from there, which includes its own directory on disk,
32+
* and parent directories - for example:
33+
*
34+
* [ '...repo/packages/aws-cdk-resources/lib/cfn/node_modules',
35+
* '...repo/packages/aws-cdk-resources/lib/node_modules',
36+
* '...repo/packages/aws-cdk-resources/node_modules',
37+
* '...repo/packages/node_modules',
38+
* // etc...
39+
* ]
40+
*
41+
* We are looking for ``package.json`` that is anywhere in the tree, except it's
42+
* in the parent directory, not in the ``node_modules`` directory. For this
43+
* reason, we strip the ``/node_modules`` suffix off each path and use regular
44+
* module resolution to obtain a reference to ``package.json``.
45+
*
46+
* @param fileName a javascript file name.
47+
* @returns the NPM module infos (aka ``package.json`` contents), or
48+
* ``undefined`` if the lookup was unsuccessful.
49+
*/
50+
function findNpmPackage(fileName: string): { name: string, version: string, private?: boolean } | undefined {
51+
const mod = require.cache[fileName];
52+
const paths = mod.paths.map(stripNodeModules);
53+
54+
try {
55+
const packagePath = require.resolve('package.json', { paths });
56+
return require(packagePath);
57+
} catch (e) {
58+
return undefined;
59+
}
60+
61+
/**
62+
* @param s a path.
63+
* @returns ``s`` with any terminating ``/node_modules``
64+
* (or ``\\node_modules``) stripped off.)
65+
*/
66+
function stripNodeModules(s: string): string {
67+
if (s.endsWith('/node_modules') || s.endsWith('\\node_modules')) {
68+
// /node_modules is 13 characters
69+
return s.substr(0, s.length - 13);
70+
}
71+
return s;
72+
}
73+
}
74+
75+
function getJsiiAgentVersion() {
76+
let jsiiAgent = process.env.JSII_AGENT;
77+
78+
// if JSII_AGENT is not specified, we will assume this is a node.js runtime
79+
// and plug in our node.js version
80+
if (!jsiiAgent) {
81+
jsiiAgent = `node.js/${process.version}`;
82+
}
83+
84+
return jsiiAgent;
85+
}

‎packages/@aws-cdk/cdk/lib/synthesis.ts

+140-10
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,67 @@
1+
import cxapi = require('@aws-cdk/cx-api');
12
import fs = require('fs');
23
import os = require('os');
34
import path = require('path');
5+
import { collectRuntimeInformation } from './runtime-info';
46

57
export interface ISynthesisSession {
8+
readonly store: ISessionStore;
9+
readonly manifest: cxapi.AssemblyManifest;
10+
addArtifact(id: string, droplet: cxapi.Artifact): void;
11+
tryGetArtifact(id: string): cxapi.Artifact | undefined;
12+
}
13+
14+
export class SynthesisSession implements ISynthesisSession {
15+
private readonly artifacts: { [id: string]: cxapi.Artifact } = { };
16+
private _manifest?: cxapi.AssemblyManifest;
17+
18+
constructor(public readonly store: ISessionStore) {
19+
20+
}
21+
22+
public get manifest() {
23+
if (!this._manifest) {
24+
throw new Error(`Cannot read assembly manifest before the session has been finalized`);
25+
}
26+
27+
return this._manifest;
28+
}
29+
30+
public addArtifact(id: string, artifact: cxapi.Artifact): void {
31+
cxapi.validateArtifact(artifact);
32+
this.store.writeFile(id, JSON.stringify(artifact, undefined, 2));
33+
this.artifacts[id] = artifact;
34+
}
35+
36+
public tryGetArtifact(id: string): cxapi.Artifact | undefined {
37+
if (!this.store.exists(id)) {
38+
return undefined;
39+
}
40+
41+
return JSON.parse(this.store.readFile(id).toString());
42+
}
43+
44+
public finalize(): cxapi.AssemblyManifest {
45+
const manifest: cxapi.SynthesizeResponse = this._manifest = {
46+
version: cxapi.PROTO_RESPONSE_VERSION,
47+
artifacts: this.artifacts,
48+
runtime: collectRuntimeInformation(),
49+
50+
// for backwards compatbility
51+
stacks: renderLegacyStacks(this.artifacts, this.store),
52+
};
53+
54+
// write the manifest (under both legacy and new name)
55+
this.store.writeFile(cxapi.OUTFILE_NAME, JSON.stringify(manifest, undefined, 2));
56+
this.store.writeFile(cxapi.MANIFEST_FILE, JSON.stringify(manifest, undefined, 2));
57+
58+
return manifest;
59+
}
60+
}
61+
62+
export interface ISessionStore {
663
/**
7-
* Creates a directory under the session directory and returns it's full path.
64+
* Creates a directory and returns it's full path.
865
* @param directoryName The name of the directory to create.
966
* @throws if a directory by that name already exists in the session or if the session has already been finalized.
1067
*/
@@ -18,38 +75,56 @@ export interface ISynthesisSession {
1875
readdir(directoryName: string): string[];
1976

2077
/**
21-
* Writes a file into the synthesis session directory.
78+
* Writes a file into the store.
2279
* @param artifactName The name of the file.
2380
* @param data The contents of the file.
2481
*/
2582
writeFile(artifactName: string, data: any): void;
2683

2784
/**
28-
* Reads a file from the synthesis session directory.
85+
* Writes a formatted JSON output file to the store
86+
* @param artifactName the name of the artifact
87+
* @param json the JSON object
88+
*/
89+
writeJson(artifactName: string, json: any): void;
90+
91+
/**
92+
* Reads a file from the store.
2993
* @param fileName The name of the file.
3094
* @throws if the file is not found
3195
*/
3296
readFile(fileName: string): any;
3397

3498
/**
35-
* @returns true if the file `fileName` exists in the session directory.
99+
* Reads a JSON object from the store.
100+
*/
101+
readJson(fileName: string): any;
102+
103+
/**
104+
* @returns true if the file `fileName` exists in the store.
36105
* @param name The name of the file or directory to look up.
37106
*/
38107
exists(name: string): boolean;
39108

40109
/**
41-
* List all artifacts that were emitted to the session.
110+
* List all top-level files that were emitted to the store.
42111
*/
43112
list(): string[];
44113

45114
/**
46-
* Finalizes the session. After this is called, the session will be locked for
47-
* writing.
115+
* Do not allow further writes into the store.
48116
*/
49117
finalize(): void;
50118
}
51119

52120
export interface SynthesisSessionOptions {
121+
/**
122+
* Where to store the
123+
*/
124+
store: ISessionStore;
125+
}
126+
127+
export interface FileSystemStoreOptions {
53128
/**
54129
* The output directory for synthesis artifacts
55130
*/
@@ -59,11 +134,11 @@ export interface SynthesisSessionOptions {
59134
/**
60135
* Can be used to prepare and emit synthesis artifacts into an output directory.
61136
*/
62-
export class SynthesisSession implements ISynthesisSession {
137+
export class FileSystemStore implements ISessionStore {
63138
private readonly outdir: string;
64139
private locked = false;
65140

66-
constructor(options: SynthesisSessionOptions) {
141+
constructor(options: FileSystemStoreOptions) {
67142
this.outdir = options.outdir;
68143
return;
69144
}
@@ -75,6 +150,10 @@ export class SynthesisSession implements ISynthesisSession {
75150
fs.writeFileSync(p, data);
76151
}
77152

153+
public writeJson(fileName: string, json: any) {
154+
this.writeFile(fileName, JSON.stringify(json, undefined, 2));
155+
}
156+
78157
public readFile(fileName: string): any {
79158
const p = this.pathForArtifact(fileName);
80159
if (!fs.existsSync(p)) {
@@ -84,6 +163,10 @@ export class SynthesisSession implements ISynthesisSession {
84163
return fs.readFileSync(p);
85164
}
86165

166+
public readJson(fileName: string): any {
167+
return JSON.parse(this.readFile(fileName).toString());
168+
}
169+
87170
public exists(name: string): boolean {
88171
const p = this.pathForArtifact(name);
89172
return fs.existsSync(p);
@@ -127,7 +210,7 @@ export class SynthesisSession implements ISynthesisSession {
127210
}
128211
}
129212

130-
export class InMemorySynthesisSession implements ISynthesisSession {
213+
export class InMemoryStore implements ISessionStore {
131214
private files: { [fileName: string]: any } = { };
132215
private dirs: { [dirName: string]: string } = { }; // value is path to a temporary directory
133216

@@ -138,13 +221,21 @@ export class InMemorySynthesisSession implements ISynthesisSession {
138221
this.files[fileName] = data;
139222
}
140223

224+
public writeJson(fileName: string, json: any): void {
225+
this.writeFile(fileName, JSON.stringify(json, undefined, 2));
226+
}
227+
141228
public readFile(fileName: string) {
142229
if (!(fileName in this.files)) {
143230
throw new Error(`${fileName} not found`);
144231
}
145232
return this.files[fileName];
146233
}
147234

235+
public readJson(fileName: string): any {
236+
return JSON.parse(this.readFile(fileName).toString());
237+
}
238+
148239
public exists(name: string) {
149240
return name in this.files || name in this.dirs;
150241
}
@@ -182,4 +273,43 @@ export class InMemorySynthesisSession implements ISynthesisSession {
182273
throw new Error('Session has already been finalized');
183274
}
184275
}
276+
}
277+
278+
function renderLegacyStacks(artifacts: { [id: string]: cxapi.Artifact }, store: ISessionStore) {
279+
// special case for backwards compat. build a list of stacks for the manifest
280+
const stacks = new Array<cxapi.SynthesizedStack>();
281+
282+
for (const [ id, artifact ] of Object.entries(artifacts)) {
283+
if (artifact.type === cxapi.ArtifactType.CloudFormationStack) {
284+
const templateFile = (artifact.properties || {}).template;
285+
if (!templateFile) {
286+
throw new Error(`Invalid cloudformation artifact. Missing "template" property`);
287+
}
288+
const template = store.readJson(templateFile);
289+
290+
const match = cxapi.AWS_ENV_REGEX.exec(artifact.environment);
291+
if (!match) {
292+
throw new Error(`"environment" must match regex: ${cxapi.AWS_ENV_REGEX}`);
293+
}
294+
295+
const synthStack: cxapi.SynthesizedStack = {
296+
name: id,
297+
environment: { name: artifact.environment.substr('aws://'.length), account: match[1], region: match[2] },
298+
template,
299+
metadata: artifact.metadata || {},
300+
};
301+
302+
if (artifact.dependencies && artifact.dependencies.length > 0) {
303+
synthStack.dependsOn = artifact.dependencies;
304+
}
305+
306+
if (artifact.missing) {
307+
synthStack.missing = artifact.missing;
308+
}
309+
310+
stacks.push(synthStack);
311+
}
312+
}
313+
314+
return stacks;
185315
}

‎packages/@aws-cdk/cdk/test/test.app.ts

+6-11
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ function withApp(context: { [key: string]: any } | undefined, block: (app: App)
1616

1717
const session = app.run();
1818

19-
return JSON.parse(session.readFile(cxapi.OUTFILE_NAME));
19+
return session.manifest;
2020
}
2121

2222
function synth(context?: { [key: string]: any }): cxapi.SynthesizeResponse {
@@ -59,6 +59,7 @@ export = {
5959
// clean up metadata so assertion will be sane
6060
response.stacks.forEach(s => delete s.metadata);
6161
delete response.runtime;
62+
delete response.artifacts;
6263

6364
test.deepEqual(response, {
6465
version: '0.19.0',
@@ -90,13 +91,7 @@ export = {
9091
const stack = new Stack(prog, 'MyStack');
9192
new Resource(stack, 'MyResource', { type: 'MyResourceType' });
9293

93-
let throws;
94-
try {
95-
prog.synthesizeStacks(['foo']);
96-
} catch (e) {
97-
throws = e.message;
98-
}
99-
test.ok(throws.indexOf('Cannot find stack foo') !== -1);
94+
test.throws(() => prog.synthesizeStacks(['foo']), /foo/);
10095

10196
test.deepEqual(prog.synthesizeStack('MyStack').template,
10297
{ Resources: { MyResource: { Type: 'MyResourceType' } } });
@@ -264,7 +259,7 @@ export = {
264259
new Resource(stack, 'MyResource', { type: 'Resource::Type' });
265260
});
266261

267-
const libs = response.runtime.libraries;
262+
const libs = (response.runtime && response.runtime.libraries) || { };
268263

269264
const version = require('../package.json').version;
270265
test.deepEqual(libs['@aws-cdk/cdk'], version);
@@ -281,7 +276,7 @@ export = {
281276
new Resource(stack, 'MyResource', { type: 'Resource::Type' });
282277
});
283278

284-
const libs = response.runtime.libraries;
279+
const libs = (response.runtime && response.runtime.libraries) || { };
285280
test.deepEqual(libs['jsii-runtime'], `Java/1.2.3.4`);
286281

287282
delete process.env.JSII_AGENT;
@@ -294,7 +289,7 @@ export = {
294289
new Resource(stack, 'MyResource', { type: 'Resource::Type' });
295290
});
296291

297-
const libs = response.runtime.libraries;
292+
const libs = (response.runtime && response.runtime.libraries) || { };
298293

299294
const version = require('../package.json').version;
300295
test.deepEqual(libs, {

‎packages/@aws-cdk/cdk/test/test.synthesis.ts

+145-84
Original file line numberDiff line numberDiff line change
@@ -4,39 +4,12 @@ import { Test } from 'nodeunit';
44
import os = require('os');
55
import path = require('path');
66
import cdk = require('../lib');
7-
import { InMemorySynthesisSession, SynthesisSession } from '../lib';
7+
import { FileSystemStore, InMemoryStore, SynthesisSession } from '../lib';
88

9-
const sessionTestMatix: any = {};
9+
const storeTestMatrix: any = {};
1010

1111
export = {
12-
'constructs that implement "synthesize" can emit artifacts during synthesis'(test: Test) {
13-
// GIVEN
14-
const app = new cdk.App();
15-
new Synthesizer1(app, 'synthe1');
16-
const s2 = new Synthesizer2(app, 'synthe2');
17-
new Synthesizer3(s2, 'synthe3');
18-
19-
// WHEN
20-
const session = app.run();
21-
22-
// THEN
23-
test.deepEqual(session.readFile('s1.txt'), 'hello, s1');
24-
test.deepEqual(session.readFile('s2.txt'), 'hello, s2');
25-
26-
test.deepEqual(session.list(), [
27-
'cdk.out',
28-
's1.txt',
29-
's2.txt',
30-
'synthe2Group0512C945A.txt',
31-
'synthe2Group181E95665.txt',
32-
'synthe2Group20BD1A3CD.txt',
33-
'synthe2synthe30CE80559.txt'
34-
]);
35-
36-
test.done();
37-
},
38-
39-
'cdk.out contains all synthesized stacks'(test: Test) {
12+
'backwards compatibility: cdk.out contains all synthesized stacks'(test: Test) {
4013
// GIVEN
4114
const app = new cdk.App();
4215
const stack1 = new cdk.Stack(app, 'stack1');
@@ -47,7 +20,7 @@ export = {
4720

4821
// WHEN
4922
const session = app.run();
50-
const manifest: cxapi.SynthesizeResponse = JSON.parse(session.readFile(cxapi.OUTFILE_NAME).toString());
23+
const manifest = session.manifest;
5124

5225
// THEN
5326
const t1 = manifest.stacks.find(s => s.name === 'stack1')!.template;
@@ -67,96 +40,184 @@ export = {
6740
test.done();
6841
},
6942

70-
'session': sessionTestMatix
43+
'store': storeTestMatrix
7144
};
7245

73-
const sessionTests = {
74-
'writeFile()/readFile()'(test: Test, session: cdk.ISynthesisSession) {
46+
//
47+
// all these tests will be executed for each type of store
48+
//
49+
const storeTests = {
50+
'writeFile()/readFile()'(test: Test, store: cdk.ISessionStore) {
7551
// WHEN
76-
session.writeFile('bla.txt', 'hello');
77-
session.writeFile('hey.txt', '1234');
52+
store.writeFile('bla.txt', 'hello');
53+
store.writeFile('hey.txt', '1234');
7854

7955
// THEN
80-
test.deepEqual(session.readFile('bla.txt').toString(), 'hello');
81-
test.deepEqual(session.readFile('hey.txt').toString(), '1234');
82-
test.throws(() => session.writeFile('bla.txt', 'override is forbidden'));
56+
test.deepEqual(store.readFile('bla.txt').toString(), 'hello');
57+
test.deepEqual(store.readFile('hey.txt').toString(), '1234');
58+
test.throws(() => store.writeFile('bla.txt', 'override is forbidden'));
8359

8460
// WHEN
85-
session.finalize();
61+
store.finalize();
8662

8763
// THEN
88-
test.throws(() => session.writeFile('another.txt', 'locked!'));
64+
test.throws(() => store.writeFile('another.txt', 'locked!'));
8965
test.done();
9066
},
9167

92-
'exists() for files'(test: Test, session: cdk.ISynthesisSession) {
68+
'exists() for files'(test: Test, store: cdk.ISessionStore) {
9369
// WHEN
94-
session.writeFile('A.txt', 'aaa');
70+
store.writeFile('A.txt', 'aaa');
9571

9672
// THEN
97-
test.ok(session.exists('A.txt'));
98-
test.ok(!session.exists('B.txt'));
73+
test.ok(store.exists('A.txt'));
74+
test.ok(!store.exists('B.txt'));
9975
test.done();
10076
},
10177

102-
'mkdir'(test: Test, session: cdk.ISynthesisSession) {
78+
'mkdir'(test: Test, store: cdk.ISessionStore) {
10379
// WHEN
104-
const dir1 = session.mkdir('dir1');
105-
const dir2 = session.mkdir('dir2');
80+
const dir1 = store.mkdir('dir1');
81+
const dir2 = store.mkdir('dir2');
10682

10783
// THEN
10884
test.ok(fs.statSync(dir1).isDirectory());
10985
test.ok(fs.statSync(dir2).isDirectory());
110-
test.throws(() => session.mkdir('dir1'));
86+
test.throws(() => store.mkdir('dir1'));
11187

11288
// WHEN
113-
session.finalize();
114-
test.throws(() => session.mkdir('dir3'));
89+
store.finalize();
90+
test.throws(() => store.mkdir('dir3'));
11591
test.done();
11692
},
11793

118-
'list'(test: Test, session: cdk.ISynthesisSession) {
94+
'list'(test: Test, store: cdk.ISessionStore) {
11995
// WHEN
120-
session.mkdir('dir1');
121-
session.writeFile('file1.txt', 'boom1');
96+
store.mkdir('dir1');
97+
store.writeFile('file1.txt', 'boom1');
12298

12399
// THEN
124-
test.deepEqual(session.list(), ['dir1', 'file1.txt']);
100+
test.deepEqual(store.list(), ['dir1', 'file1.txt']);
125101
test.done();
126-
}
127-
};
102+
},
128103

129-
for (const [name, fn] of Object.entries(sessionTests)) {
130-
const outdir = fs.mkdtempSync(path.join(os.tmpdir(), 'synthesis-tests'));
131-
const fsSession = new SynthesisSession({ outdir });
132-
const memorySession = new InMemorySynthesisSession();
133-
sessionTestMatix[`SynthesisSession - ${name}`] = (test: Test) => fn(test, fsSession);
134-
sessionTestMatix[`InMemorySession - ${name}`] = (test: Test) => fn(test, memorySession);
135-
}
104+
'SynthesisSession'(test: Test, store: cdk.ISessionStore) {
105+
// GIVEN
106+
const session = new SynthesisSession(store);
107+
const templateFile = 'foo.template.json';
136108

137-
class Synthesizer1 extends cdk.Construct {
138-
public synthesize(s: cdk.ISynthesisSession) {
139-
s.writeFile('s1.txt', 'hello, s1');
140-
}
141-
}
109+
// WHEN
110+
session.addArtifact('my-first-artifact', {
111+
type: cxapi.ArtifactType.CloudFormationStack,
112+
environment: 'aws://1222344/us-east-1',
113+
dependencies: ['a', 'b'],
114+
metadata: {
115+
foo: { bar: 123 }
116+
},
117+
properties: {
118+
template: templateFile,
119+
prop1: 1234,
120+
prop2: 555
121+
},
122+
missing: {
123+
foo: {
124+
provider: 'context-provider',
125+
props: {
126+
a: 'A',
127+
b: 2
128+
}
129+
}
130+
}
131+
});
142132

143-
class Synthesizer2 extends cdk.Construct {
144-
constructor(scope: cdk.Construct, id: string) {
145-
super(scope, id);
133+
session.addArtifact('minimal-artifact', {
134+
type: cxapi.ArtifactType.CloudFormationStack,
135+
environment: 'aws://111/helo-world',
136+
properties: {
137+
template: templateFile
138+
}
139+
});
146140

147-
const group = new cdk.Construct(this, 'Group');
148-
for (let i = 0; i < 3; ++i) {
149-
new Synthesizer3(group, `${i}`);
150-
}
151-
}
141+
session.store.writeJson(templateFile, {
142+
Resources: {
143+
MyTopic: {
144+
Type: 'AWS::S3::Topic'
145+
}
146+
}
147+
});
152148

153-
public synthesize(s: cdk.ISynthesisSession) {
154-
s.writeFile('s2.txt', 'hello, s2');
155-
}
156-
}
149+
session.finalize();
157150

158-
class Synthesizer3 extends cdk.Construct {
159-
public synthesize(s: cdk.ISynthesisSession) {
160-
s.writeFile(this.node.uniqueId + '.txt', 'hello, s3');
151+
// THEN
152+
delete session.manifest.stacks; // remove legacy
153+
delete session.manifest.runtime; // deterministic tests
154+
155+
// verify the manifest looks right
156+
test.deepEqual(session.manifest, {
157+
version: cxapi.PROTO_RESPONSE_VERSION,
158+
artifacts: {
159+
'my-first-artifact': {
160+
type: 'aws:cloudformation:stack',
161+
environment: 'aws://1222344/us-east-1',
162+
dependencies: ['a', 'b'],
163+
metadata: { foo: { bar: 123 } },
164+
properties: { template: 'foo.template.json', prop1: 1234, prop2: 555 },
165+
missing: {
166+
foo: { provider: 'context-provider', props: { a: 'A', b: 2 } }
167+
}
168+
},
169+
'minimal-artifact': {
170+
type: 'aws:cloudformation:stack',
171+
environment: 'aws://111/helo-world',
172+
properties: { template: 'foo.template.json' }
173+
}
174+
}
175+
});
176+
177+
// verify we have a template file
178+
test.deepEqual(session.store.readJson(templateFile), {
179+
Resources: {
180+
MyTopic: {
181+
Type: 'AWS::S3::Topic'
182+
}
183+
}
184+
});
185+
186+
test.done();
161187
}
188+
};
189+
190+
for (const [name, fn] of Object.entries(storeTests)) {
191+
const outdir = fs.mkdtempSync(path.join(os.tmpdir(), 'synthesis-tests'));
192+
const fsStore = new FileSystemStore({ outdir });
193+
const memoryStore = new InMemoryStore();
194+
storeTestMatrix[`FileSystemStore - ${name}`] = (test: Test) => fn(test, fsStore);
195+
storeTestMatrix[`InMemoryStore - ${name}`] = (test: Test) => fn(test, memoryStore);
162196
}
197+
198+
// class Synthesizer1 extends cdk.Construct {
199+
// public synthesize(s: cdk.ISynthesisSession) {
200+
// s.writeFile('s1.txt', 'hello, s1');
201+
// }
202+
// }
203+
204+
// class Synthesizer2 extends cdk.Construct {
205+
// constructor(scope: cdk.Construct, id: string) {
206+
// super(scope, id);
207+
208+
// const group = new cdk.Construct(this, 'Group');
209+
// for (let i = 0; i < 3; ++i) {
210+
// new Synthesizer3(group, `${i}`);
211+
// }
212+
// }
213+
214+
// public synthesize(s: cdk.ISynthesisSession) {
215+
// s.writeFile('s2.txt', 'hello, s2');
216+
// }
217+
// }
218+
219+
// class Synthesizer3 extends cdk.Construct {
220+
// public synthesize(s: cdk.ISynthesisSession) {
221+
// s.writeFile(this.node.uniqueId + '.txt', 'hello, s3');
222+
// }
223+
// }
+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
export const AWS_ENV_REGEX = /aws\:\/\/([0-9]+|unknown-account)\/([a-z\-0-9]+)/;
2+
3+
export enum ArtifactType {
4+
CloudFormationStack = 'aws:cloudformation:stack',
5+
DockerImage = 'aws:docker',
6+
File = 'aws:file'
7+
}
8+
9+
export interface Artifact {
10+
type: ArtifactType;
11+
environment: string; // format: aws://account/region
12+
properties?: { [name: string]: any };
13+
metadata?: { [path: string]: any };
14+
dependencies?: string[];
15+
missing?: { [key: string]: any };
16+
17+
/**
18+
* Build instructions for this artifact (for example, lambda-builders, zip directory, docker build, etc)
19+
*/
20+
build?: any;
21+
}
22+
23+
export function validateArtifact(artifcat: Artifact) {
24+
if (!AWS_ENV_REGEX.test(artifcat.environment)) {
25+
throw new Error(`Artifact "environment" must conform to ${AWS_ENV_REGEX}: ${artifcat.environment}`);
26+
}
27+
}

‎packages/@aws-cdk/cx-api/lib/cxapi.ts

+30-2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
* File with definitions for the interface between the Cloud Executable and the CDK toolkit.
33
*/
44

5+
import { Artifact } from './artifacts';
56
import { Environment } from './environment';
67

78
/**
@@ -22,7 +23,16 @@ import { Environment } from './environment';
2223
*/
2324
export const PROTO_RESPONSE_VERSION = '0.19.0';
2425

26+
/**
27+
* @deprecated Use `MANIFEST_FILE`
28+
*/
2529
export const OUTFILE_NAME = 'cdk.out';
30+
31+
/**
32+
* The name of the root manifest file of the assembly.
33+
*/
34+
export const MANIFEST_FILE = "manifest.json";
35+
2636
export const OUTDIR_ENV = 'CDK_OUTDIR';
2737
export const CONTEXT_ENV = 'CDK_CONTEXT_JSON';
2838

@@ -38,15 +48,33 @@ export interface MissingContext {
3848
};
3949
}
4050

41-
export interface SynthesizeResponse {
51+
export interface AssemblyManifest {
4252
/**
4353
* Protocol version
4454
*/
4555
version: string;
46-
stacks: SynthesizedStack[];
56+
57+
/**
58+
* The set of artifacts in this assembly.
59+
*/
60+
artifacts?: { [id: string]: Artifact };
61+
62+
/**
63+
* Runtime information.
64+
*/
4765
runtime?: AppRuntime;
66+
67+
/**
68+
* @deprecated stacks should be read from `Artifacts`.
69+
*/
70+
stacks: SynthesizedStack[];
4871
}
4972

73+
/**
74+
* @deprecated use `AssemblyManifest`
75+
*/
76+
export type SynthesizeResponse = AssemblyManifest;
77+
5078
/**
5179
* A complete synthesized stack
5280
*/

‎packages/@aws-cdk/cx-api/lib/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ export * from './context/vpc';
55
export * from './context/ssm-parameter';
66
export * from './context/availability-zones';
77
export * from './metadata/assets';
8+
export * from './artifacts';

0 commit comments

Comments
 (0)
Please sign in to comment.