Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(applets): integrate into toolkit #1039

Merged
merged 7 commits into from
Nov 2, 2018
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 16 additions & 16 deletions packages/@aws-cdk/applet-js/bin/cdk-applet-js.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,18 +26,22 @@ async function main() {
}

// read applet(s) properties from the provided file
let fileContents = YAML.safeLoad(await fs.readFile(appletFile, { encoding: 'utf-8' }));
if (!Array.isArray(fileContents)) {
fileContents = [fileContents];
const fileContents = YAML.safeLoad(await fs.readFile(appletFile, { encoding: 'utf-8' }));
if (typeof fileContents !== 'object') {
throw new Error(`${appletFile}: should contain a YAML object`);
}
const appletMap = fileContents.Applets;
if (!appletMap) {
throw new Error(`${appletFile}: must have an 'Applets' key`);
}

const searchDir = path.dirname(appletFile);
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'cdkapplet'));
try {

const app = new cdk.App();
for (const props of fileContents) {
await constructStack(app, searchDir, tempDir, props);

for (const [name, definition] of Object.entries(appletMap)) {
await constructStack(app, searchDir, tempDir, name, definition);
}
app.run();
} finally {
Expand All @@ -49,19 +53,18 @@ async function main() {
* Construct a stack from the given props
* @param props Const
*/
async function constructStack(app: cdk.App, searchDir: string, tempDir: string, props: any) {
async function constructStack(app: cdk.App, searchDir: string, tempDir: string, stackName: string, spec: any) {
// the 'applet' attribute tells us how to load the applet. in the javascript case
// it will be in the format <module>:<class> where <module> is technically passed to "require"
// and <class> is expected to be exported from the module.
const appletSpec: string = props.applet;
const appletSpec: string | undefined = spec.Type;
if (!appletSpec) {
throw new Error('Applet file missing "applet" attribute');
throw new Error(`Applet ${stackName} missing "Type" attribute`);
}

const applet = parseApplet(appletSpec);

// remove the 'applet' attribute as we pass it along to the applet class.
delete props.applet;
const props = spec.Properties || {};

if (applet.npmPackage) {
// tslint:disable-next-line:no-console
Expand All @@ -79,8 +82,7 @@ async function constructStack(app: cdk.App, searchDir: string, tempDir: string,

// we need to resolve the module name relatively to where the applet file is
// and not relative to this module or cwd.
const resolve = require.resolve as any; // escape type-checking since { paths } is not defined
const modulePath = resolve(applet.moduleName, { paths: [ searchDir ] });
const modulePath = require.resolve(applet.moduleName, { paths: [ searchDir ] });

// load the module
const pkg = require(modulePath);
Expand All @@ -92,8 +94,6 @@ async function constructStack(app: cdk.App, searchDir: string, tempDir: string,
throw new Error(`Cannot find applet class "${applet.className}" in module "${applet.moduleName}"`);
}

const stackName = props.name || applet.className;

if (isStackConstructor(appletConstructor)) {
// add the applet stack into the app.
new appletConstructor(app, stackName, props);
Expand All @@ -102,4 +102,4 @@ async function constructStack(app: cdk.App, searchDir: string, tempDir: string,
const stack = new cdk.Stack(app, stackName, props);
new appletConstructor(stack, 'Default', props);
}
}
}
4 changes: 3 additions & 1 deletion packages/@aws-cdk/applet-js/test/manual-test-npm.yaml
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
applet: npm://@aws-cdk/aws-ecs:Hello
Applets:
Hello:
Type: npm://@aws-cdk/aws-ecs:Hello
4 changes: 3 additions & 1 deletion packages/@aws-cdk/applet-js/test/negative-test4.yaml
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
# malformed applet specification
applet: invalid:number:of:components:in:applet:name
Applets:
Applet:
Type: invalid:number:of:components:in:applet:name
4 changes: 3 additions & 1 deletion packages/@aws-cdk/applet-js/test/negative-test5.yaml
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
# applet module not found
applet: notfound
Applets:
Applet:
Type: notfound
4 changes: 3 additions & 1 deletion packages/@aws-cdk/applet-js/test/negative-test6.yaml
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
# applet class not found
applet: ./test-applet:ClassNotFound
Applets:
Applet:
Type: ./test-applet:ClassNotFound
4 changes: 3 additions & 1 deletion packages/@aws-cdk/applet-js/test/negative-test7.yaml
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
# no module
applet: :TestApplet
Applets:
Applet:
Type: :TestApplet
1 change: 0 additions & 1 deletion packages/@aws-cdk/applet-js/test/test-fromnpm.yaml

This file was deleted.

20 changes: 11 additions & 9 deletions packages/@aws-cdk/applet-js/test/test-multistack.yaml
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
---
- applet: ./test-applet:TestApplet
name: Stack1
prop1: stack1
prop2: 123
- applet: ./test-applet:TestApplet
name: Stack2
prop1: stack2
prop2: 456
Applets:
Stack1:
Type: ./test-applet:TestApplet
Properties:
prop1: stack1
prop2: 123
Stack2:
Type: ./test-applet:TestApplet
Properties:
prop1: stack2
prop2: 456
8 changes: 5 additions & 3 deletions packages/@aws-cdk/applet-js/test/test-nonstack.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
#!/usr/bin/env cdk-applet-js
applet: ./test-applet:NoStackApplet
argument: this should be reflected in the template description
Applets:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any chance I can convince you to use lower case letters for the schema?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can, and I'm not liking this either.

But then let's not mimic the CloudFormation schema at all. Otherwise it's just going to be confusing, don't you think?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both Azure Resource Manager and Google Cloud Deployment Manager use very similar syntaxes. This is basically the standard-de-facto for desired state configuration.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alrighty

NoStackApplet:
Type: ./test-applet:NoStackApplet
Properties:
argument: this should be reflected in the template description

58 changes: 1 addition & 57 deletions packages/@aws-cdk/applet-js/test/test.applets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,60 +113,4 @@ function stripStackMetadata(stack: any) {
}
delete stack.environment;
return stack;
}

// cdk-applet-js test1.yaml '{ "type": "synth", "stacks": ["TestApplet"] }' | node strip-stacktrace.js > /tmp/actual1.json
// expect_success diff expected1.json /tmp/actual1.json

// #!/bin/bash
// set -euo pipefail
// cd $(dirname $0)
// export PATH=../bin:$PATH

// announce() {
// echo "-------------------------------------------------"
// echo "$@"
// }

// expect_success() {
// announce $@
// set +e
// $@
// local exit_code=$?
// set -e

// if [ "${exit_code}" -ne 0 ]; then
// echo "Command expected to succeed: $@"
// exit 1
// fi
// }

// expect_failure() {
// announce $@
// set +e
// $@
// local exit_code=$?
// set -e
// if [ "${exit_code}" -eq 0 ]; then
// echo "Command expected to fail: $@"
// exit 1
// fi
// }

// cdk-applet-js test1.yaml '{ "type": "synth", "stacks": ["TestApplet"] }' | node strip-stacktrace.js > /tmp/actual1.json
// expect_success diff expected1.json /tmp/actual1.json

// cdk-applet-js test2.yaml '{ "type": "synth", "stacks": ["TestApplet"] }' | node strip-stacktrace.js > /tmp/actual2.json
// expect_success diff expected2.json /tmp/actual2.json

// # applets can use the host as shebang
// chmod +x ./test3.yaml # since codebuild loses permissions
// ./test3.yaml '{ "type": "synth", "stacks": ["Applet"] }' | node strip-stacktrace.js > /tmp/actual3.json
// expect_success diff expected3.json /tmp/actual3.json

// expect_failure cdk-applet-js negative-test4.yaml
// expect_failure cdk-applet-js negative-test5.yaml
// expect_failure cdk-applet-js negative-test6.yaml
// expect_failure cdk-applet-js negative-test7.yaml

// echo "PASS"
}
9 changes: 6 additions & 3 deletions packages/@aws-cdk/applet-js/test/test1.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
# applet is loaded from the local ./test-applet.js file
applet: ./test-applet:TestApplet
prop1: hello
prop2: 123
Applets:
TestApplet:
Type: ./test-applet:TestApplet
Properties:
prop1: hello
prop2: 123
21 changes: 12 additions & 9 deletions packages/@aws-cdk/applet-js/test/test2.yaml
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
# applet is loaded from the local ./test-applet.js file
applet: ./test-applet:TestApplet
prop1: hello
prop2: 123
prop3:
- hello
- this
- is
- awesome
- 12345
Applets:
TestApplet:
Type: ./test-applet:TestApplet
Properties:
prop1: hello
prop2: 123
prop3:
- hello
- this
- is
- awesome
- 12345
9 changes: 5 additions & 4 deletions packages/@aws-cdk/applet-js/test/test3.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#!/usr/bin/env cdk-applet-js

# if class name is not specified, 'Applet' is assumed
applet: ./test-applet
desc: this should be reflected in the template description
Applets:
Applet:
Type: ./test-applet
Properties:
desc: this should be reflected in the template description
47 changes: 43 additions & 4 deletions packages/aws-cdk/lib/exec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,14 +141,53 @@ function appToArray(app: any) {
return typeof app === 'string' ? app.split(' ') : app;
}

type CommandGenerator = (file: string) => string[];

/**
* Direct execution of a YAML file, assume that we're deploying an Applet
*/
function executeApplet(appletFile: string): string[] {
const appletBinary = path.resolve(require.resolve('@aws-cdk/applet-js'));
return [process.execPath, appletBinary, appletFile];
}

/**
* Execute the given file with the same 'node' process as is running the current process
*/
function executeNode(scriptFile: string): string[] {
return [process.execPath, scriptFile];
}

/**
* Mapping of extensions to command-line generators
*/
const EXTENSION_MAP = new Map<string, CommandGenerator>([
['.yml', executeApplet],
['.yaml', executeApplet],
['.js', executeNode],
]);

/**
* Guess the executable from the command-line argument
*
* Only do this if the file is NOT marked as executable. If it is,
* we'll defer to the shebang inside the file itself.
*
* If we're on Windows, we ALWAYS take the handler, since it's hard to
* verify if registry associations have or have not been set up for this
* file type, so we'll assume the worst and take control.
*/
async function guessExecutable(commandLine: string[]) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was actually thinking we can design this a bit more generally and make people happy. For example, if app is a .js file, we can determine that it can be executed with node, if it's a .dll, we can use dotnet, etc. What do you think?

I was also thinking that we can introduce auto-detection (i.e. if there's a package.json file, or pom.xml, we can auto-detect where the CDK app is), but that's a later thing.

For now, I think adding support for .js could be nice and maybe implement this a bit more generically (extension to programming mapping).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that seems reasonable.

if (commandLine.length === 1 && ['.yml', '.yaml'].includes(path.extname(commandLine[0]))) {
// Direct execution of a YAML file, assume that we're deploying an Applet
const appletBinary = path.resolve(require.resolve('@aws-cdk/applet-js'));
return [process.execPath, appletBinary, commandLine[0]];
if (commandLine.length === 1) {
const fstat = await fs.stat(commandLine[0]);
// tslint:disable-next-line:no-bitwise
const isExecutable = (fstat.mode & fs.constants.X_OK) !== 0;
const isWindows = process.platform === "win32";

const handler = EXTENSION_MAP.get(path.extname(commandLine[0]));
if (handler && (!isExecutable || isWindows)) {
return handler(commandLine[0]);
}
}
return commandLine;
}