Skip to content

Commit

Permalink
start establishing the concept of "artifacts"
Browse files Browse the repository at this point in the history
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
  • Loading branch information
Elad Ben-Israel committed Feb 27, 2019
1 parent 297b66b commit dca97c6
Show file tree
Hide file tree
Showing 10 changed files with 932 additions and 267 deletions.
473 changes: 473 additions & 0 deletions design/cloud-assembly.md

Large diffs are not rendered by default.

150 changes: 13 additions & 137 deletions packages/@aws-cdk/cdk/lib/app.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import cxapi = require('@aws-cdk/cx-api');
import { Stack } from './cloudformation/stack';
import { IConstruct, Root } from './core/construct';
import { InMemorySynthesisSession, ISynthesisSession, SynthesisSession } from './synthesis';
import { Root } from './core/construct';
import { FileSystemStore, InMemoryStore, ISynthesisSession, SynthesisSession } from './synthesis';

/**
* Represents a CDK program.
Expand All @@ -18,22 +17,6 @@ export class App extends Root {
this.loadContext();
}

private get stacks() {
const out: { [name: string]: Stack } = { };
collectStacks(this);
return out;

function collectStacks(c: IConstruct) {
for (const child of c.node.children) {
if (Stack.isStack(child)) {
out[child.node.id] = child; // TODO: this should probably be changed to uniqueId
}

collectStacks(child);
}
}
}

/**
* Runs the program. Output is written to output directory as specified in the request.
*/
Expand All @@ -44,13 +27,14 @@ export class App extends Root {
}

const outdir = process.env[cxapi.OUTDIR_ENV];
let store;
if (outdir) {
this._session = new SynthesisSession({ outdir });
store = new FileSystemStore({ outdir });
} else {
this._session = new InMemorySynthesisSession();
store = new InMemoryStore();
}

const session = this._session;
const session = this._session = new SynthesisSession(store);

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

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

// write the entrypoint/manifest of this app. It includes a *copy* of the
// synthesized stack output for backwards compatibility

const manifest: cxapi.SynthesizeResponse = {
version: cxapi.PROTO_RESPONSE_VERSION,
stacks: Object.values(this.stacks).map(s => this.readSynthesizedStack(session, s.artifactName)),
runtime: this.collectRuntimeInformation()
};

session.writeFile(cxapi.OUTFILE_NAME, JSON.stringify(manifest, undefined, 2));

// lock session - cannot emit more artifacts
// write session manifest and lock store
session.finalize();

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

return res;
}

/**
Expand All @@ -107,46 +84,6 @@ export class App extends Root {
return ret;
}

private readSynthesizedStack(session: ISynthesisSession, artifactName: string) {
return JSON.parse(session.readFile(artifactName).toString());
}

private collectRuntimeInformation(): cxapi.AppRuntime {
const libraries: { [name: string]: string } = {};

for (const fileName of Object.keys(require.cache)) {
const pkg = findNpmPackage(fileName);
if (pkg && !pkg.private) {
libraries[pkg.name] = pkg.version;
}
}

// include only libraries that are in the @aws-cdk npm scope
for (const name of Object.keys(libraries)) {
if (!name.startsWith('@aws-cdk/')) {
delete libraries[name];
}
}

// add jsii runtime version
libraries['jsii-runtime'] = getJsiiAgentVersion();

return { libraries };
}

private getStack(stackname: string) {
if (stackname == null) {
throw new Error('Stack name must be defined');
}

const stack = this.stacks[stackname];

if (!stack) {
throw new Error(`Cannot find stack ${stackname}`);
}
return stack;
}

private loadContext() {
const contextJson = process.env[cxapi.CONTEXT_ENV];
const context = !contextJson ? { } : JSON.parse(contextJson);
Expand All @@ -155,64 +92,3 @@ export class App extends Root {
}
}
}

/**
* Determines which NPM module a given loaded javascript file is from.
*
* The only infromation that is available locally is a list of Javascript files,
* and every source file is associated with a search path to resolve the further
* ``require`` calls made from there, which includes its own directory on disk,
* and parent directories - for example:
*
* [ '...repo/packages/aws-cdk-resources/lib/cfn/node_modules',
* '...repo/packages/aws-cdk-resources/lib/node_modules',
* '...repo/packages/aws-cdk-resources/node_modules',
* '...repo/packages/node_modules',
* // etc...
* ]
*
* We are looking for ``package.json`` that is anywhere in the tree, except it's
* in the parent directory, not in the ``node_modules`` directory. For this
* reason, we strip the ``/node_modules`` suffix off each path and use regular
* module resolution to obtain a reference to ``package.json``.
*
* @param fileName a javascript file name.
* @returns the NPM module infos (aka ``package.json`` contents), or
* ``undefined`` if the lookup was unsuccessful.
*/
function findNpmPackage(fileName: string): { name: string, version: string, private?: boolean } | undefined {
const mod = require.cache[fileName];
const paths = mod.paths.map(stripNodeModules);

try {
const packagePath = require.resolve('package.json', { paths });
return require(packagePath);
} catch (e) {
return undefined;
}

/**
* @param s a path.
* @returns ``s`` with any terminating ``/node_modules``
* (or ``\\node_modules``) stripped off.)
*/
function stripNodeModules(s: string): string {
if (s.endsWith('/node_modules') || s.endsWith('\\node_modules')) {
// /node_modules is 13 characters
return s.substr(0, s.length - 13);
}
return s;
}
}

function getJsiiAgentVersion() {
let jsiiAgent = process.env.JSII_AGENT;

// if JSII_AGENT is not specified, we will assume this is a node.js runtime
// and plug in our node.js version
if (!jsiiAgent) {
jsiiAgent = `node.js/${process.version}`;
}

return jsiiAgent;
}
35 changes: 12 additions & 23 deletions packages/@aws-cdk/cdk/lib/cloudformation/stack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,11 +78,6 @@ export class Stack extends Construct {
*/
public readonly name: string;

/**
* The name of the CDK artifact produced by this stack.
*/
public readonly artifactName: string;

/*
* Used to determine if this construct is a stack.
*/
Expand Down Expand Up @@ -112,8 +107,6 @@ export class Stack extends Construct {

this.logicalIds = new LogicalIDs(props && props.namingScheme ? props.namingScheme : new HashedAddressingScheme());
this.name = this.node.id;

this.artifactName = `${this.node.uniqueId}.stack.json`;
}

/**
Expand Down Expand Up @@ -428,25 +421,21 @@ export class Stack extends Construct {
protected synthesize(session: ISynthesisSession): void {
const account = this.env.account || 'unknown-account';
const region = this.env.region || 'unknown-region';

const environment: cxapi.Environment = {
name: `${account}/${region}`,
account,
region
};

const missing = Object.keys(this.missingContext).length ? this.missingContext : undefined;
const template = `${this.node.id}.template.json`;

const output: cxapi.SynthesizedStack = {
name: this.node.id,
template: this.toCloudFormation(),
environment,
missing,
metadata: this.collectMetadata(),
dependsOn: noEmptyArray(this.dependencies().map(s => s.node.id)),
};
// write the CloudFormation template as a JSON file
session.store.writeJson(template, this.toCloudFormation());

session.writeFile(this.artifactName, JSON.stringify(output, undefined, 2));
// add an artifact that represents this stack
session.addArtifact(this.node.id, {
type: cxapi.ArtifactType.CloudFormationStack,
dependencies: noEmptyArray(this.dependencies().map(s => s.node.id)),
environment: `aws://${account}/${region}`,
metadata: this.collectMetadata(),
missing,
properties: { template }
});
}

/**
Expand Down
85 changes: 85 additions & 0 deletions packages/@aws-cdk/cdk/lib/runtime-info.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import cxapi = require('@aws-cdk/cx-api');

export function collectRuntimeInformation(): cxapi.AppRuntime {
const libraries: { [name: string]: string } = {};

for (const fileName of Object.keys(require.cache)) {
const pkg = findNpmPackage(fileName);
if (pkg && !pkg.private) {
libraries[pkg.name] = pkg.version;
}
}

// include only libraries that are in the @aws-cdk npm scope
for (const name of Object.keys(libraries)) {
if (!name.startsWith('@aws-cdk/')) {
delete libraries[name];
}
}

// add jsii runtime version
libraries['jsii-runtime'] = getJsiiAgentVersion();

return { libraries };
}

/**
* Determines which NPM module a given loaded javascript file is from.
*
* The only infromation that is available locally is a list of Javascript files,
* and every source file is associated with a search path to resolve the further
* ``require`` calls made from there, which includes its own directory on disk,
* and parent directories - for example:
*
* [ '...repo/packages/aws-cdk-resources/lib/cfn/node_modules',
* '...repo/packages/aws-cdk-resources/lib/node_modules',
* '...repo/packages/aws-cdk-resources/node_modules',
* '...repo/packages/node_modules',
* // etc...
* ]
*
* We are looking for ``package.json`` that is anywhere in the tree, except it's
* in the parent directory, not in the ``node_modules`` directory. For this
* reason, we strip the ``/node_modules`` suffix off each path and use regular
* module resolution to obtain a reference to ``package.json``.
*
* @param fileName a javascript file name.
* @returns the NPM module infos (aka ``package.json`` contents), or
* ``undefined`` if the lookup was unsuccessful.
*/
function findNpmPackage(fileName: string): { name: string, version: string, private?: boolean } | undefined {
const mod = require.cache[fileName];
const paths = mod.paths.map(stripNodeModules);

try {
const packagePath = require.resolve('package.json', { paths });
return require(packagePath);
} catch (e) {
return undefined;
}

/**
* @param s a path.
* @returns ``s`` with any terminating ``/node_modules``
* (or ``\\node_modules``) stripped off.)
*/
function stripNodeModules(s: string): string {
if (s.endsWith('/node_modules') || s.endsWith('\\node_modules')) {
// /node_modules is 13 characters
return s.substr(0, s.length - 13);
}
return s;
}
}

function getJsiiAgentVersion() {
let jsiiAgent = process.env.JSII_AGENT;

// if JSII_AGENT is not specified, we will assume this is a node.js runtime
// and plug in our node.js version
if (!jsiiAgent) {
jsiiAgent = `node.js/${process.version}`;
}

return jsiiAgent;
}
Loading

0 comments on commit dca97c6

Please sign in to comment.