diff --git a/packages/@aws-cdk/cloudwatch/.gitignore b/packages/@aws-cdk/cloudwatch/.gitignore index 541a7635fd070..9178900e205b8 100644 --- a/packages/@aws-cdk/cloudwatch/.gitignore +++ b/packages/@aws-cdk/cloudwatch/.gitignore @@ -5,3 +5,4 @@ tslint.json *.d.ts dist lib/generated/resources.ts +*.tgz diff --git a/packages/@aws-cdk/codepipeline/.gitignore b/packages/@aws-cdk/codepipeline/.gitignore index 541a7635fd070..9178900e205b8 100644 --- a/packages/@aws-cdk/codepipeline/.gitignore +++ b/packages/@aws-cdk/codepipeline/.gitignore @@ -5,3 +5,4 @@ tslint.json *.d.ts dist lib/generated/resources.ts +*.tgz diff --git a/packages/aws-cdk-java/generate.sh b/packages/aws-cdk-java/generate.sh index bd9481fc9cfc1..6b84fa6d694d7 100755 --- a/packages/aws-cdk-java/generate.sh +++ b/packages/aws-cdk-java/generate.sh @@ -3,6 +3,11 @@ set -euo pipefail mkdir -p project +# deploy jsii-runtime to the local maven repo so it will be discoverable +jsii_runtime_repo=$(node -e "console.log(path.join(path.dirname(require.resolve('jsii-java-runtime/package.json')), 'maven-repo'))") +mkdir -p ~/.m2/repository +rsync -av ${jsii_runtime_repo}/ ~/.m2/repository/ + echo "Generating pom.xml..." node ./pom.xml.t.js > project/pom.xml diff --git a/packages/aws-cdk-java/package.json b/packages/aws-cdk-java/package.json index 40184c4b4c6e3..132f9814ccd02 100644 --- a/packages/aws-cdk-java/package.json +++ b/packages/aws-cdk-java/package.json @@ -20,10 +20,8 @@ "url": "https://aws.amazon.com" }, "license": "Apache-2.0", - "dependencies": { - "aws-cdk-all": "^0.7.2-beta" - }, "devDependencies": { + "aws-cdk-all": "^0.7.2-beta", "pkgtools": "^0.7.2-beta" }, "keywords": [ diff --git a/packages/aws-cdk-java/pom.xml.t.js b/packages/aws-cdk-java/pom.xml.t.js index f4b218bc06763..e7ce6ce8e3374 100644 --- a/packages/aws-cdk-java/pom.xml.t.js +++ b/packages/aws-cdk-java/pom.xml.t.js @@ -1,10 +1,7 @@ const path = require('path'); const version = require('./package.json').version.replace(/\+.+$/, ''); // omit "+build" postfix -const jsiiRuntime = { - version: require('jsii-java-runtime/package.json').version, - repo: path.join(path.dirname(require.resolve('jsii-java-runtime')), 'maven-repo'), -}; +const jsiiRuntimeVersion = require('jsii-java-runtime/package.json').version; process.stdout.write(` UTF-8 - - - jsii - file://${jsiiRuntime.repo} - - - @@ -75,6 +65,7 @@ process.stdout.write(` \${project.groupId} \${project.artifactId} \${project.version} + \${project.basedir}/pom.xml jar @@ -88,7 +79,7 @@ process.stdout.write(` com.amazonaws jsii-runtime - ${jsiiRuntime.version} + ${jsiiRuntimeVersion} diff --git a/packages/aws-cdk/lib/init-templates/app/java/.gitignore b/packages/aws-cdk/lib/init-templates/app/java/.gitignore new file mode 100644 index 0000000000000..977763f5766de --- /dev/null +++ b/packages/aws-cdk/lib/init-templates/app/java/.gitignore @@ -0,0 +1,8 @@ +.classpath.txt +target +.classpath +.project +.idea +.settings +.vscode +*.iml diff --git a/packages/aws-cdk/lib/init-templates/app/java/README.md b/packages/aws-cdk/lib/init-templates/app/java/README.md new file mode 100644 index 0000000000000..b74767ee1b0f8 --- /dev/null +++ b/packages/aws-cdk/lib/init-templates/app/java/README.md @@ -0,0 +1,24 @@ + +Welcome to your CDK Java project! + +It is a Maven-based project, so you can open this directory with any Maven-compatible Java IDE, +and you should be able to build and run tests from your IDE. + +You should explore the contents of this template. It demonstrates a CDK app with two instances of +a stack (`HelloStack`) which also uses a user-defined construct (`HelloConstruct`). + +The `cdk.json` file tells the CDK Toolkit how to execute your app. It uses a script called `app.sh` +to do that. Note that this script expects a local file called `.classpath.txt` to exist. This file +is automatically created by `mvn package`. + +# Useful commands + + * `mvn package` compile and run tests + * `cdk ls` list all stacks in the app + * `cdk synth` emits the synthesized CloudFormation template + * `cdk deploy` deploy this stack to your default AWS account/region + * `cdk diff` compare deployed stack with current state + * `cdk docs` open CDK documentation + +Enjoy! + diff --git a/packages/aws-cdk/lib/init-templates/app/java/app.sh b/packages/aws-cdk/lib/init-templates/app/java/app.sh new file mode 100755 index 0000000000000..210e90b76344a --- /dev/null +++ b/packages/aws-cdk/lib/init-templates/app/java/app.sh @@ -0,0 +1,6 @@ +#!/bin/bash +# This script is configured in cdk.json to be used to execute +# the CDK java app by the command-line toolkit. +# The file .classpath.txt is created by when `mvn package` is called +# The first argument will be used as argv[0] +exec java -cp target/classes:$(cat .classpath.txt) com.myorg.HelloApp hello-cdk $@ diff --git a/packages/aws-cdk/lib/init-templates/app/java/cdk.json b/packages/aws-cdk/lib/init-templates/app/java/cdk.json new file mode 100644 index 0000000000000..676ae2b6bd071 --- /dev/null +++ b/packages/aws-cdk/lib/init-templates/app/java/cdk.json @@ -0,0 +1,3 @@ +{ + "app": "./app.sh" +} diff --git a/packages/aws-cdk/lib/init-templates/app/java/pom.template.xml b/packages/aws-cdk/lib/init-templates/app/java/pom.template.xml new file mode 100644 index 0000000000000..2e60478c6f621 --- /dev/null +++ b/packages/aws-cdk/lib/init-templates/app/java/pom.template.xml @@ -0,0 +1,69 @@ + + + 4.0.0 + + + + cdk + file://%cdk-home%/repo/maven + + + + com.myorg + %name% + 0.1 + + + UTF-8 + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.7.0 + + 1.8 + 1.8 + + + + + + org.apache.maven.plugins + maven-dependency-plugin + 2.8 + + + build-classpath + generate-sources + + build-classpath + + + .classpath.txt + + + + + + + + + + + com.amazonaws.cdk + aws-cdk + LATEST + + + + + junit + junit + 4.12 + test + + + diff --git a/packages/aws-cdk/lib/init-templates/app/java/src/main/java/com/myorg/HelloApp.java b/packages/aws-cdk/lib/init-templates/app/java/src/main/java/com/myorg/HelloApp.java new file mode 100644 index 0000000000000..d0debb8f5ed38 --- /dev/null +++ b/packages/aws-cdk/lib/init-templates/app/java/src/main/java/com/myorg/HelloApp.java @@ -0,0 +1,16 @@ +package com.myorg; + +import com.amazonaws.cdk.App; + +import java.util.Arrays; + +public class HelloApp { + public static void main(final String argv[]) { + App app = new App(Arrays.asList(argv)); + + new HelloStack(app, "hello-cdk-1"); + new HelloStack(app, "hello-cdk-2"); + + System.out.println(app.run()); + } +} \ No newline at end of file diff --git a/packages/aws-cdk/lib/init-templates/app/java/src/main/java/com/myorg/HelloConstruct.java b/packages/aws-cdk/lib/init-templates/app/java/src/main/java/com/myorg/HelloConstruct.java new file mode 100644 index 0000000000000..aaf2bb40550f9 --- /dev/null +++ b/packages/aws-cdk/lib/init-templates/app/java/src/main/java/com/myorg/HelloConstruct.java @@ -0,0 +1,31 @@ +package com.myorg; + +import com.amazonaws.cdk.Construct; +import com.amazonaws.cdk.iam.IIdentityResource; +import com.amazonaws.cdk.s3.Bucket; + +import java.util.ArrayList; +import java.util.List; + +/** + * Example of a reusable construct. This one defines N buckets. + */ +public class HelloConstruct extends Construct { + private final List buckets = new ArrayList<>(); + + public HelloConstruct(final Construct parent, final String name, final HelloConstructProps props) { + super(parent, name); + + for (int i = 0; i < props.getBucketCount(); ++i) { + buckets.add(new Bucket(this, "Bucket" + String.valueOf(i))); + } + } + + /** + * Given an principal, grants it READ access on all buckets. + * @param principal The principal (User, Group, Role) + */ + public void grantRead(final IIdentityResource principal) { + buckets.forEach(b -> b.grantRead(principal)); + } +} diff --git a/packages/aws-cdk/lib/init-templates/app/java/src/main/java/com/myorg/HelloConstructProps.java b/packages/aws-cdk/lib/init-templates/app/java/src/main/java/com/myorg/HelloConstructProps.java new file mode 100644 index 0000000000000..592a64e45b173 --- /dev/null +++ b/packages/aws-cdk/lib/init-templates/app/java/src/main/java/com/myorg/HelloConstructProps.java @@ -0,0 +1,40 @@ +package com.myorg; + +public class HelloConstructProps { + private int bucketCount; + + public static HelloConstructPropsBuilder builder() { + return new HelloConstructPropsBuilder(); + } + + public int getBucketCount() { + return bucketCount; + } + + public void setBucketCount(int bucketCount) { + this.bucketCount = bucketCount; + } + + + public static final class HelloConstructPropsBuilder { + private int bucketCount; + + private HelloConstructPropsBuilder() { + } + + public static HelloConstructPropsBuilder aHelloConstructProps() { + return new HelloConstructPropsBuilder(); + } + + public HelloConstructPropsBuilder withBucketCount(int bucketCount) { + this.bucketCount = bucketCount; + return this; + } + + public HelloConstructProps build() { + HelloConstructProps helloConstructProps = new HelloConstructProps(); + helloConstructProps.setBucketCount(bucketCount); + return helloConstructProps; + } + } +} diff --git a/packages/aws-cdk/lib/init-templates/app/java/src/main/java/com/myorg/HelloStack.java b/packages/aws-cdk/lib/init-templates/app/java/src/main/java/com/myorg/HelloStack.java new file mode 100644 index 0000000000000..4a55d78209a8b --- /dev/null +++ b/packages/aws-cdk/lib/init-templates/app/java/src/main/java/com/myorg/HelloStack.java @@ -0,0 +1,37 @@ +package com.myorg; + +import com.amazonaws.cdk.App; +import com.amazonaws.cdk.Stack; +import com.amazonaws.cdk.StackProps; +import com.amazonaws.cdk.iam.User; +import com.amazonaws.cdk.sns.Topic; +import com.amazonaws.cdk.sns.TopicProps; +import com.amazonaws.cdk.sqs.Queue; +import com.amazonaws.cdk.sqs.QueueProps; + +public class HelloStack extends Stack { + public HelloStack(final App parent, final String name) { + this(parent, name, null); + } + + public HelloStack(final App parent, final String name, final StackProps props) { + super(parent, name, props); + + Queue queue = new Queue(this, "MyFirstQueue", QueueProps.builder() + .withVisibilityTimeoutSec(300) + .build()); + + Topic topic = new Topic(this, "MyFirstTopic", TopicProps.builder() + .withDisplayName("My First Topic Yeah") + .build()); + + topic.subscribeQueue(queue); + + HelloConstruct hello = new HelloConstruct(this, "Buckets", HelloConstructProps.builder() + .withBucketCount(5) + .build()); + + User user = new User(this, "MyUser"); + hello.grantRead(user); + } +} diff --git a/packages/aws-cdk/lib/init-templates/app/java/src/test/java/com/myorg/HelloStackTest.java b/packages/aws-cdk/lib/init-templates/app/java/src/test/java/com/myorg/HelloStackTest.java new file mode 100644 index 0000000000000..b998dd795af0a --- /dev/null +++ b/packages/aws-cdk/lib/init-templates/app/java/src/test/java/com/myorg/HelloStackTest.java @@ -0,0 +1,28 @@ +package com.myorg; + +import com.amazonaws.cdk.App; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import org.junit.Test; + +import java.io.IOException; + +import static junit.framework.TestCase.assertEquals; + +public class HelloStackTest { + private final static ObjectMapper JSON = + new ObjectMapper().configure(SerializationFeature.INDENT_OUTPUT, true); + + @Test + public void testStack() throws IOException { + App app = new App(); + HelloStack stack = new HelloStack(app, "test"); + + // synthesize the stack to a CloudFormation template and compare against + // a checked-in JSON file. + JsonNode actual = JSON.valueToTree(app.synthesizeStack(stack.getName()).getTemplate()); + JsonNode expected = JSON.readTree(getClass().getResource("expected.cfn.json")); + assertEquals(expected, actual); + } +} \ No newline at end of file diff --git a/packages/aws-cdk/lib/init-templates/app/java/src/test/resources/com/myorg/expected.cfn.json b/packages/aws-cdk/lib/init-templates/app/java/src/test/resources/com/myorg/expected.cfn.json new file mode 100644 index 0000000000000..97c1ad4d15727 --- /dev/null +++ b/packages/aws-cdk/lib/init-templates/app/java/src/test/resources/com/myorg/expected.cfn.json @@ -0,0 +1,255 @@ +{ + "Resources": { + "MyFirstTopic0ED1F8A4": { + "Type": "AWS::SNS::Topic", + "Properties": { + "DisplayName": "My First Topic Yeah" + } + }, + "MyFirstQueuePolicy596EEC78": { + "Type": "AWS::SQS::QueuePolicy", + "Properties": { + "PolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Condition": { + "ArnEquals": { + "aws:SourceArn": { + "Ref": "MyFirstTopic0ED1F8A4" + } + } + }, + "Action": "sqs:SendMessage", + "Resource": { + "Fn::GetAtt": [ + "MyFirstQueueFF09316A", + "Arn" + ] + }, + "Effect": "Allow", + "Principal": { + "Service": "sns.amazonaws.com" + } + } + ] + }, + "Queues": [ + { + "Ref": "MyFirstQueueFF09316A" + } + ] + } + }, + "BucketsBucket1B98A5B53": { + "Type": "AWS::S3::Bucket" + }, + "MyFirstQueueFF09316A": { + "Type": "AWS::SQS::Queue", + "Properties": { + "VisibilityTimeout": 300 + } + }, + "MyFirstTopicMyFirstQueueSubscription04CBE05C": { + "Type": "AWS::SNS::Subscription", + "Properties": { + "Endpoint": { + "Fn::GetAtt": [ + "MyFirstQueueFF09316A", + "Arn" + ] + }, + "TopicArn": { + "Ref": "MyFirstTopic0ED1F8A4" + }, + "Protocol": "sqs" + } + }, + "BucketsBucket351AD8978": { + "Type": "AWS::S3::Bucket" + }, + "BucketsBucket237F4E9C5": { + "Type": "AWS::S3::Bucket" + }, + "BucketsBucket4AEE1AAD9": { + "Type": "AWS::S3::Bucket" + }, + "MyUserDC45028B": { + "Type": "AWS::IAM::User" + }, + "MyUserDefaultPolicy7B897426": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyName": "MyUserDefaultPolicy7B897426", + "PolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*" + ], + "Resource": [ + { + "Fn::GetAtt": [ + "BucketsBucket04F23AD6D", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "BucketsBucket04F23AD6D", + "Arn" + ] + }, + "/", + "*" + ] + ] + } + ], + "Effect": "Allow" + }, + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*" + ], + "Resource": [ + { + "Fn::GetAtt": [ + "BucketsBucket1B98A5B53", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "BucketsBucket1B98A5B53", + "Arn" + ] + }, + "/", + "*" + ] + ] + } + ], + "Effect": "Allow" + }, + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*" + ], + "Resource": [ + { + "Fn::GetAtt": [ + "BucketsBucket237F4E9C5", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "BucketsBucket237F4E9C5", + "Arn" + ] + }, + "/", + "*" + ] + ] + } + ], + "Effect": "Allow" + }, + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*" + ], + "Resource": [ + { + "Fn::GetAtt": [ + "BucketsBucket351AD8978", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "BucketsBucket351AD8978", + "Arn" + ] + }, + "/", + "*" + ] + ] + } + ], + "Effect": "Allow" + }, + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*" + ], + "Resource": [ + { + "Fn::GetAtt": [ + "BucketsBucket4AEE1AAD9", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "BucketsBucket4AEE1AAD9", + "Arn" + ] + }, + "/", + "*" + ] + ] + } + ], + "Effect": "Allow" + } + ] + }, + "Users": [ + { + "Ref": "MyUserDC45028B" + } + ] + } + }, + "BucketsBucket04F23AD6D": { + "Type": "AWS::S3::Bucket" + } + } +} \ No newline at end of file diff --git a/packages/aws-cdk/lib/init.ts b/packages/aws-cdk/lib/init.ts index 1330819266b86..b7ba863a02882 100644 --- a/packages/aws-cdk/lib/init.ts +++ b/packages/aws-cdk/lib/init.ts @@ -11,6 +11,7 @@ const decamelize = require('decamelize'); // tslint:enable:no-var-requires const TEMPLATES_DIR = path.join(__dirname, 'init-templates'); +const CDK_HOME = process.env.CDK_HOME ? path.resolve(process.env.CDK_HOME) : path.join(os.homedir(), '.cdk'); /** * Initialize a CDK package in the current directory @@ -102,11 +103,13 @@ export class InitTemplate { } private expand(template: string, project: ProjectInfo) { - const cdkVersion = require('@aws-cdk/core/package.json').version.replace(/\+[a-f0-9]+$/, ''); + const MATCH_VER_BUILD = /\+[a-f0-9]+$/; // Matches "+BUILD" in "x.y.z-beta+BUILD" + const cdkVersion = require('@aws-cdk/core/package.json').version.replace(MATCH_VER_BUILD, ''); return template.replace(/%name%/g, project.name) .replace(/%name\.camelCased%/g, camelCase(project.name)) .replace(/%name\.PascalCased%/g, camelCase(project.name, { pascalCase: true })) - .replace(/%cdk-version%/g, cdkVersion); + .replace(/%cdk-version%/g, cdkVersion) + .replace(/%cdk-home%/g, CDK_HOME); } } @@ -192,19 +195,27 @@ async function postInstall(language: string) { switch (language) { case 'typescript': return await postInstallTypescript(); + case 'java': + return await postInstallJava(); } } async function postInstallTypescript() { - const cdkHome = process.env.CDK_HOME ? path.resolve(process.env.CDK_HOME) : path.join(os.homedir(), '.cdk'); - const yNpm = path.join(cdkHome, 'bin', 'y-npm'); + const yNpm = path.join(CDK_HOME, 'bin', 'y-npm'); const command = await fs.pathExists(yNpm) ? yNpm : 'npm'; print(`Executing ${colors.green(`${command} install`)}...`); - if (await execute(command, 'install') !== 0) { - throw new Error(`${colors.green(`${command} install`)} failed!`); + try { + await execute(command, 'install'); + } catch (e) { + throw new Error(`${colors.green(`${command} install`)} failed: ` + e.message); } } +async function postInstallJava() { + print(`Executing ${colors.green('mvn package')}...`); + await execute('mvn', 'package'); +} + /** * @param dir a directory to be checked * @returns true if ``dir`` is within a git repository. @@ -225,19 +236,27 @@ function isRoot(dir: string) { return path.dirname(dir) === dir; } -function execute(cmd: string, ...args: string[]): Promise { - return new Promise((resolve, reject) => { - try { - const child = spawn(cmd, args, { stdio: 'inherit' }); - child.on('exit', (code, _signal) => { - if (code != null) { - resolve(code); - } else { - resolve(128); - } - }); - } catch (e) { - reject(e); - } +/** + * Executes `command`. STDERR is emitted in real-time. + * + * If command exits with non-zero exit code, an exceprion is thrown and includes + * the contents of STDOUT. + * + * @returns STDOUT (if successful). + */ +async function execute(cmd: string, ...args: string[]) { + const child = spawn(cmd, args, { stdio: [ 'ignore', 'pipe', 'inherit' ] }); + let stdout = ''; + child.stdout.on('data', chunk => stdout += chunk.toString()); + return new Promise((ok, fail) => { + child.once('error', err => fail(err)); + child.once('exit', status => { + if (status === 0) { + return ok(stdout); + } else { + process.stderr.write(stdout); + return fail(new Error(`${cmd} exited with status ${status}`)); + } + }); }); }