From 51429341c8bcfd7c8e02ae261623d6d77b3288f5 Mon Sep 17 00:00:00 2001 From: Elad Ben-Israel Date: Wed, 18 Jul 2018 13:41:58 +0300 Subject: [PATCH 1/6] Assets Assets represent local files or directories which can be bundled as part of CDK constructs. During deployment, the toolkit will upload assets to the "Toolkit Bucket", and use CloudFormation Parameters to reference the asset in deploy-time. Assets expose the following deploy-time attributes: * s3BucketName * s3ObjectKey * s3Url Furthermore, the `asset.grantRead(principal)` will add IAM read permissions for the asset to the desired principal. --- packages/@aws-cdk/cdk-assets/.gitignore | 13 + packages/@aws-cdk/cdk-assets/.npmignore | 6 + packages/@aws-cdk/cdk-assets/README.md | 61 ++++ packages/@aws-cdk/cdk-assets/lib/asset.ts | 187 ++++++++++++ packages/@aws-cdk/cdk-assets/lib/index.ts | 1 + packages/@aws-cdk/cdk-assets/package.json | 50 ++++ .../@aws-cdk/cdk-assets/test/file-asset.txt | 1 + .../integ.assets.directory.lit.expected.json | 98 +++++++ .../test/integ.assets.directory.lit.ts | 19 ++ .../test/integ.assets.file.lit.expected.json | 98 +++++++ .../cdk-assets/test/integ.assets.file.lit.ts | 19 ++ ...integ.assets.permissions.lit.expected.json | 98 +++++++ .../test/integ.assets.permissions.lit.ts | 23 ++ .../test/integ.assets.refs.lit.expected.json | 145 ++++++++++ .../cdk-assets/test/integ.assets.refs.lit.ts | 23 ++ .../sample-asset-file.txt | 1 + .../@aws-cdk/cdk-assets/test/test.asset.ts | 270 ++++++++++++++++++ packages/@aws-cdk/cx-api/lib/cxapi.ts | 8 + packages/aws-cdk/lib/api/deploy-stack.ts | 28 +- packages/aws-cdk/lib/api/toolkit-info.ts | 80 +++++- packages/aws-cdk/lib/archive.ts | 21 ++ packages/aws-cdk/lib/assets.ts | 111 +++++++ packages/aws-cdk/package-lock.json | 267 ++++++++++++++++- packages/aws-cdk/package.json | 10 +- packages/aws-cdk/test/test-archive/file1.txt | 1 + packages/aws-cdk/test/test-archive/file2.txt | 2 + .../test/test-archive/subdir/file3.txt | 1 + packages/aws-cdk/test/test.archive.ts | 46 +++ 28 files changed, 1656 insertions(+), 32 deletions(-) create mode 100644 packages/@aws-cdk/cdk-assets/.gitignore create mode 100644 packages/@aws-cdk/cdk-assets/.npmignore create mode 100644 packages/@aws-cdk/cdk-assets/README.md create mode 100644 packages/@aws-cdk/cdk-assets/lib/asset.ts create mode 100644 packages/@aws-cdk/cdk-assets/lib/index.ts create mode 100644 packages/@aws-cdk/cdk-assets/package.json create mode 100644 packages/@aws-cdk/cdk-assets/test/file-asset.txt create mode 100644 packages/@aws-cdk/cdk-assets/test/integ.assets.directory.lit.expected.json create mode 100644 packages/@aws-cdk/cdk-assets/test/integ.assets.directory.lit.ts create mode 100644 packages/@aws-cdk/cdk-assets/test/integ.assets.file.lit.expected.json create mode 100644 packages/@aws-cdk/cdk-assets/test/integ.assets.file.lit.ts create mode 100644 packages/@aws-cdk/cdk-assets/test/integ.assets.permissions.lit.expected.json create mode 100644 packages/@aws-cdk/cdk-assets/test/integ.assets.permissions.lit.ts create mode 100644 packages/@aws-cdk/cdk-assets/test/integ.assets.refs.lit.expected.json create mode 100644 packages/@aws-cdk/cdk-assets/test/integ.assets.refs.lit.ts create mode 100644 packages/@aws-cdk/cdk-assets/test/sample-asset-directory/sample-asset-file.txt create mode 100644 packages/@aws-cdk/cdk-assets/test/test.asset.ts create mode 100644 packages/aws-cdk/lib/archive.ts create mode 100644 packages/aws-cdk/lib/assets.ts create mode 100644 packages/aws-cdk/test/test-archive/file1.txt create mode 100644 packages/aws-cdk/test/test-archive/file2.txt create mode 100644 packages/aws-cdk/test/test-archive/subdir/file3.txt create mode 100644 packages/aws-cdk/test/test.archive.ts diff --git a/packages/@aws-cdk/cdk-assets/.gitignore b/packages/@aws-cdk/cdk-assets/.gitignore new file mode 100644 index 0000000000000..6f03998f7c880 --- /dev/null +++ b/packages/@aws-cdk/cdk-assets/.gitignore @@ -0,0 +1,13 @@ +*.js +*.js.map +*.d.ts +node_modules +dist +tsconfig.json +tslint.json + +.LAST_BUILD +.nyc_output +coverage + +.jsii diff --git a/packages/@aws-cdk/cdk-assets/.npmignore b/packages/@aws-cdk/cdk-assets/.npmignore new file mode 100644 index 0000000000000..414172bb772ec --- /dev/null +++ b/packages/@aws-cdk/cdk-assets/.npmignore @@ -0,0 +1,6 @@ +# Don't include original .ts files when doing `npm pack` +*.ts +!*.d.ts +coverage +.nyc_output +*.tgz diff --git a/packages/@aws-cdk/cdk-assets/README.md b/packages/@aws-cdk/cdk-assets/README.md new file mode 100644 index 0000000000000..681a4d2e45dec --- /dev/null +++ b/packages/@aws-cdk/cdk-assets/README.md @@ -0,0 +1,61 @@ +## AWS CDK Assets + +Assets are local files or directories which are needed by a CDK app. A common +example is a directory which contains the handler code for a Lambda function, +but assets can represent any artifact that is needed for the app's operation. + +When deploying a CDK app that includes constructs with assets, the CDK toolkit +will first upload all the assets to S3, and only then deploy the stacks. The S3 +locations of the uploaded assets will be passed in as CloudFormation Parameters +to the relevant stacks. + +The following JavaScript example defines an directory asset which is archived as +a .zip file and uploaded to S3 during deployment. + +[Example of a ZipDirectoryAsset](./test/integ.assets.directory.lit.ts) + +The following JavaScript example defines a file asset, which is uploaded as-is +to an S3 bucket during deployment. + +[Example of a FileAsset](./test/integ.assets.file.lit.ts) + +## Attributes + +`Asset` constructs expose the following deploy-time attributes: + + * `s3BucketName` - the name of the assets S3 bucket. + * `s3ObjectKey` - the S3 object key of the asset file (whether it's a file or a zip archive) + * `s3Url` - the S3 URL of the asset (i.e. https://s3.us-east-1.amazonaws.com/mybucket/mykey.zip) + +In the following example, the various asset attributes are exported as stack outputs: + +[Example of referencing an asset](./test/integ.assets.refs.lit.ts) + +## Permissions + +IAM roles, users or groups which need to be able to read assets in runtime will should be +granted IAM permissions. To do that use the `asset.grantRead(principal)` method: + +The following examples grants an IAM group read permissions on an asset: + +[Example of granting read access to an asset](./test/integ.assets.permissions.lit.ts) + +## How does it work? + +When an asset is defined in a construct, a construct metadata entry +`aws:cdk:asset` is emitted with instructions on where to find the asset and what +type of packaging to perform (`zip` or `file`). Furthermore, the synthesized +CloudFormation template will also include two CloudFormation parameters: one for +the asset's bucket and one for the asset S3 key. Those parameters are used to +reference the deploy-time values of the asset (using `{ Ref: "Param" }`). + +Then, when the stack is deployed, the toolkit will package the asset (i.e. zip +the directory), calculate an MD5 hash of the contents and will render an S3 key +for this asset within the toolkit's asset store. If the file doesn't exist in +the asset store, it is uploaded during deployment. + +> The toolkit's asset store is an S3 bucket created by the toolkit for each + environment the toolkit operates in (environment = account + region). + +Now, when the toolkit deploys the stack, it will set the relevant CloudFormation +Parameters to point to the actual bucket and key for each asset. diff --git a/packages/@aws-cdk/cdk-assets/lib/asset.ts b/packages/@aws-cdk/cdk-assets/lib/asset.ts new file mode 100644 index 0000000000000..f39d8a1841abc --- /dev/null +++ b/packages/@aws-cdk/cdk-assets/lib/asset.ts @@ -0,0 +1,187 @@ +import cdk = require('@aws-cdk/core'); +import cxapi = require('@aws-cdk/cx-api'); +import iam = require('@aws-cdk/iam'); +import s3 = require('@aws-cdk/s3'); +import * as fs from 'fs'; +import * as path from 'path'; + +/** + * Defines the way an asset is packaged before it is uploaded to S3. + */ +export enum AssetPackaging { + /** + * Path refers to a directory on disk, the contents of the directory is + * archived into a .zip. + */ + ZipDirectory = 'zip', + + /** + * Path refers to a single file on disk. The file is uploaded as-is. + */ + File = 'file', +} + +export interface GenericAssetProps { + /** + * The disk location of the asset. + */ + path: string; + + /** + * The packaging type for this asset. + */ + packaging: AssetPackaging; + + /** + * A list of principals that should be able to read this asset from S3. + * You can use `asset.grantRead(principal)` to grant read permissions later. + */ + readers?: iam.IPrincipal[]; +} + +/** + * An asset represents a local file or directory, which is automatically uploaded to S3 + * and then can be referenced within a CDK application. + */ +export class Asset extends cdk.Construct { + /** + * Attribute that represents the name of the bucket this asset exists in. + */ + public readonly s3BucketName: s3.BucketName; + + /** + * Attribute which represents the S3 object key of this asset. + */ + public readonly s3ObjectKey: s3.ObjectKey; + + /** + * Attribute which represents the S3 URL of this asset. + * @example https://s3.us-west-1.amazonaws.com/bucket/key + */ + public readonly s3Url: s3.S3Url; + + /** + * Resolved full-path location of this asset. + */ + public readonly assetPath: string; + + private readonly bucket: s3.BucketRef; + + constructor(parent: cdk.Construct, id: string, props: GenericAssetProps) { + super(parent, id); + + // resolve full path + this.assetPath = path.resolve(props.path); + + if (!fs.existsSync(this.assetPath)) { + throw new Error(`Cannot find asset at ${props.path}`); + } + + if (props.packaging === AssetPackaging.ZipDirectory) { + if (!fs.statSync(this.assetPath).isDirectory()) { + throw new Error(`${this.assetPath} is expected to be a directory when asset packaging is 'zip'`); + } + } else if (props.packaging === AssetPackaging.File) { + if (!fs.statSync(this.assetPath).isFile()) { + throw new Error(`${this.assetPath} is expected to be a regular file when asset packaging is 'file'`); + } + } else { + throw new Error(`Unsupported asset packaging format: ${props.packaging}`); + } + + // add parameters for s3 bucket and s3 key. those will be set by + // the toolkit or by CI/CD when the stack is deployed and will include + // the name of the bucket and the S3 key where the code lives. + + const bucketParam = new cdk.Parameter(this, 'S3Bucket', { + type: 'String', + description: `S3 bucket for asset "${this.path}"`, + }); + + const keyParam = new cdk.Parameter(this, 'S3ObjectKey', { + type: 'String', + description: `S3 object for asset "${this.path}"` + }); + + this.s3BucketName = bucketParam.value; + this.s3ObjectKey = keyParam.value; + + // grant the lambda's role read permissions on the code s3 object + + this.bucket = s3.BucketRef.import(parent, 'AssetBucket', { + bucketName: this.s3BucketName + }); + + // form the s3 URL of the object key + this.s3Url = this.bucket.urlForObject(this.s3ObjectKey); + + // attach metadata to the lambda function which includes information + // for tooling to be able to package and upload a directory to the + // s3 bucket and plug in the bucket name and key in the correct + // parameters. + + const asset: cxapi.AssetMetadataEntry = { + path: this.assetPath, + packaging: props.packaging, + s3BucketParameter: bucketParam.logicalId, + s3KeyParameter: keyParam.logicalId, + }; + + this.addMetadata(cxapi.ASSET_METADATA, asset); + + for (const reader of (props.readers || [])) { + this.grantRead(reader); + } + } + + /** + * Grants read permissions to the principal on the asset's S3 object. + */ + public grantRead(principal?: iam.IPrincipal) { + this.bucket.grantRead(principal, this.s3ObjectKey); + } +} + +export interface FileAssetProps { + /** + * File path. + */ + path: string; + + /** + * A list of principals that should be able to read this file asset from S3. + * You can use `asset.grantRead(principal)` to grant read permissions later. + */ + readers?: iam.IPrincipal[]; +} + +/** + * An asset that represents a file on disk. + */ +export class FileAsset extends Asset { + constructor(parent: cdk.Construct, id: string, props: FileAssetProps) { + super(parent, id, { packaging: AssetPackaging.File, ...props }); + } +} + +export interface ZipDirectoryAssetProps { + /** + * Path of the directory. + */ + path: string; + + /** + * A list of principals that should be able to read this ZIP file from S3. + * You can use `asset.grantRead(principal)` to grant read permissions later. + */ + readers?: iam.IPrincipal[]; +} + +/** + * An asset that represents a ZIP archive of a directory on disk. + */ +export class ZipDirectoryAsset extends Asset { + constructor(parent: cdk.Construct, id: string, props: ZipDirectoryAssetProps) { + super(parent, id, { packaging: AssetPackaging.ZipDirectory, ...props }); + } +} diff --git a/packages/@aws-cdk/cdk-assets/lib/index.ts b/packages/@aws-cdk/cdk-assets/lib/index.ts new file mode 100644 index 0000000000000..946a5dd1a6851 --- /dev/null +++ b/packages/@aws-cdk/cdk-assets/lib/index.ts @@ -0,0 +1 @@ +export * from './asset'; \ No newline at end of file diff --git a/packages/@aws-cdk/cdk-assets/package.json b/packages/@aws-cdk/cdk-assets/package.json new file mode 100644 index 0000000000000..37a9e2fe442cc --- /dev/null +++ b/packages/@aws-cdk/cdk-assets/package.json @@ -0,0 +1,50 @@ +{ + "name": "@aws-cdk/cdk-assets", + "version": "0.7.3-beta", + "description": "Integration of CDK apps with local assets", + "main": "lib/index.js", + "types": "lib/index.d.ts", + "jsii": { + "outdir": "dist", + "names": { + "java": "com.amazonaws.cdk.cdkassets", + "dotnet": "Aws.Cdk.CdkAssets" + } + }, + "repository": { + "type": "git", + "url": "git://github.com/awslabs/aws-cdk" + }, + "scripts": { + "build": "cdk-build", + "watch": "cdk-watch", + "lint": "cdk-lint", + "test": "cdk-test", + "integ": "cdk-integ", + "pkglint": "pkglint -f" + }, + "keywords": [ + "aws", + "cdk", + "constructs", + "assets" + ], + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com" + }, + "license": "LicenseRef-LICENSE", + "devDependencies": { + "@aws-cdk/assert": "^0.7.3-beta", + "aws-cdk": "^0.7.3-beta", + "pkglint": "^0.7.3-beta", + "cdk-build-tools": "^0.7.3-beta", + "cdk-integ-tools": "^0.7.3-beta" + }, + "dependencies": { + "@aws-cdk/s3": "^0.7.3-beta", + "@aws-cdk/core": "^0.7.3-beta", + "@aws-cdk/iam": "^0.7.3-beta", + "@aws-cdk/cx-api": "^0.7.3-beta" + } +} diff --git a/packages/@aws-cdk/cdk-assets/test/file-asset.txt b/packages/@aws-cdk/cdk-assets/test/file-asset.txt new file mode 100644 index 0000000000000..87cda36f4da3e --- /dev/null +++ b/packages/@aws-cdk/cdk-assets/test/file-asset.txt @@ -0,0 +1 @@ +Hello, this is a just a file! \ No newline at end of file diff --git a/packages/@aws-cdk/cdk-assets/test/integ.assets.directory.lit.expected.json b/packages/@aws-cdk/cdk-assets/test/integ.assets.directory.lit.expected.json new file mode 100644 index 0000000000000..af8fa84270de8 --- /dev/null +++ b/packages/@aws-cdk/cdk-assets/test/integ.assets.directory.lit.expected.json @@ -0,0 +1,98 @@ +{ + "Parameters": { + "SampleAssetS3BucketE6B2908E": { + "Type": "String", + "Description": "S3 bucket for asset \"aws-cdk-asset-test/SampleAsset\"" + }, + "SampleAssetS3ObjectKey6F5D200B": { + "Type": "String", + "Description": "S3 object for asset \"aws-cdk-asset-test/SampleAsset\"" + } + }, + "Resources": { + "TestUser6A619381": { + "Type": "AWS::IAM::User" + }, + "TestUserDefaultPolicyFC1191A6": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn", + ":", + { + "Ref": "AWS::Partition" + }, + ":", + "s3", + ":", + "", + ":", + "", + ":", + { + "Ref": "SampleAssetS3BucketE6B2908E" + } + ] + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::Join": [ + "", + [ + "arn", + ":", + { + "Ref": "AWS::Partition" + }, + ":", + "s3", + ":", + "", + ":", + "", + ":", + { + "Ref": "SampleAssetS3BucketE6B2908E" + } + ] + ] + }, + "/", + { + "Ref": "SampleAssetS3ObjectKey6F5D200B" + } + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "TestUserDefaultPolicyFC1191A6", + "Users": [ + { + "Ref": "TestUser6A619381" + } + ] + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/cdk-assets/test/integ.assets.directory.lit.ts b/packages/@aws-cdk/cdk-assets/test/integ.assets.directory.lit.ts new file mode 100644 index 0000000000000..1e6b241233979 --- /dev/null +++ b/packages/@aws-cdk/cdk-assets/test/integ.assets.directory.lit.ts @@ -0,0 +1,19 @@ +import cdk = require('@aws-cdk/core'); +import path = require('path'); +import assets = require('../lib'); + +class TestStack extends cdk.Stack { + constructor(parent: cdk.App, name: string, props?: cdk.StackProps) { + super(parent, name, props); + + /// !show + new assets.ZipDirectoryAsset(this, 'SampleAsset', { + path: path.join(__dirname, 'sample-asset-directory') + }); + /// !hide + } +} + +const app = new cdk.App(process.argv); +new TestStack(app, 'aws-cdk-asset-test'); +process.stdout.write(app.run()); diff --git a/packages/@aws-cdk/cdk-assets/test/integ.assets.file.lit.expected.json b/packages/@aws-cdk/cdk-assets/test/integ.assets.file.lit.expected.json new file mode 100644 index 0000000000000..898e8288c1a16 --- /dev/null +++ b/packages/@aws-cdk/cdk-assets/test/integ.assets.file.lit.expected.json @@ -0,0 +1,98 @@ +{ + "Parameters": { + "SampleAssetS3BucketE6B2908E": { + "Type": "String", + "Description": "S3 bucket for asset \"aws-cdk-asset-file-test/SampleAsset\"" + }, + "SampleAssetS3ObjectKey6F5D200B": { + "Type": "String", + "Description": "S3 object for asset \"aws-cdk-asset-file-test/SampleAsset\"" + } + }, + "Resources": { + "TestUser6A619381": { + "Type": "AWS::IAM::User" + }, + "TestUserDefaultPolicyFC1191A6": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn", + ":", + { + "Ref": "AWS::Partition" + }, + ":", + "s3", + ":", + "", + ":", + "", + ":", + { + "Ref": "SampleAssetS3BucketE6B2908E" + } + ] + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::Join": [ + "", + [ + "arn", + ":", + { + "Ref": "AWS::Partition" + }, + ":", + "s3", + ":", + "", + ":", + "", + ":", + { + "Ref": "SampleAssetS3BucketE6B2908E" + } + ] + ] + }, + "/", + { + "Ref": "SampleAssetS3ObjectKey6F5D200B" + } + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "TestUserDefaultPolicyFC1191A6", + "Users": [ + { + "Ref": "TestUser6A619381" + } + ] + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/cdk-assets/test/integ.assets.file.lit.ts b/packages/@aws-cdk/cdk-assets/test/integ.assets.file.lit.ts new file mode 100644 index 0000000000000..9a577c9b560c7 --- /dev/null +++ b/packages/@aws-cdk/cdk-assets/test/integ.assets.file.lit.ts @@ -0,0 +1,19 @@ +import cdk = require('@aws-cdk/core'); +import path = require('path'); +import assets = require('../lib'); + +class TestStack extends cdk.Stack { + constructor(parent: cdk.App, name: string, props?: cdk.StackProps) { + super(parent, name, props); + + /// !show + new assets.FileAsset(this, 'SampleAsset', { + path: path.join(__dirname, 'file-asset.txt') + }); + /// !hide + } +} + +const app = new cdk.App(process.argv); +new TestStack(app, 'aws-cdk-asset-file-test'); +process.stdout.write(app.run()); diff --git a/packages/@aws-cdk/cdk-assets/test/integ.assets.permissions.lit.expected.json b/packages/@aws-cdk/cdk-assets/test/integ.assets.permissions.lit.expected.json new file mode 100644 index 0000000000000..e7540be27bb73 --- /dev/null +++ b/packages/@aws-cdk/cdk-assets/test/integ.assets.permissions.lit.expected.json @@ -0,0 +1,98 @@ +{ + "Parameters": { + "MyFileS3BucketACE13C36": { + "Type": "String", + "Description": "S3 bucket for asset \"aws-cdk-asset-refs/MyFile\"" + }, + "MyFileS3ObjectKey4641930D": { + "Type": "String", + "Description": "S3 object for asset \"aws-cdk-asset-refs/MyFile\"" + } + }, + "Resources": { + "MyUserGroupDA7A39B2": { + "Type": "AWS::IAM::Group" + }, + "MyUserGroupDefaultPolicy50C5D742": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn", + ":", + { + "Ref": "AWS::Partition" + }, + ":", + "s3", + ":", + "", + ":", + "", + ":", + { + "Ref": "MyFileS3BucketACE13C36" + } + ] + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::Join": [ + "", + [ + "arn", + ":", + { + "Ref": "AWS::Partition" + }, + ":", + "s3", + ":", + "", + ":", + "", + ":", + { + "Ref": "MyFileS3BucketACE13C36" + } + ] + ] + }, + "/", + { + "Ref": "MyFileS3ObjectKey4641930D" + } + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "MyUserGroupDefaultPolicy50C5D742", + "Groups": [ + { + "Ref": "MyUserGroupDA7A39B2" + } + ] + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/cdk-assets/test/integ.assets.permissions.lit.ts b/packages/@aws-cdk/cdk-assets/test/integ.assets.permissions.lit.ts new file mode 100644 index 0000000000000..7de0186fdb08a --- /dev/null +++ b/packages/@aws-cdk/cdk-assets/test/integ.assets.permissions.lit.ts @@ -0,0 +1,23 @@ +import cdk = require('@aws-cdk/core'); +import iam = require('@aws-cdk/iam'); +import path = require('path'); +import assets = require('../lib'); + +class TestStack extends cdk.Stack { + constructor(parent: cdk.App, name: string, props?: cdk.StackProps) { + super(parent, name, props); + + const asset = new assets.FileAsset(this, 'MyFile', { + path: path.join(__dirname, 'file-asset.txt') + }); + + /// !show + const group = new iam.Group(this, 'MyUserGroup'); + asset.grantRead(group); + /// !hide + } +} + +const app = new cdk.App(process.argv); +new TestStack(app, 'aws-cdk-asset-refs'); +process.stdout.write(app.run()); diff --git a/packages/@aws-cdk/cdk-assets/test/integ.assets.refs.lit.expected.json b/packages/@aws-cdk/cdk-assets/test/integ.assets.refs.lit.expected.json new file mode 100644 index 0000000000000..9d10212235190 --- /dev/null +++ b/packages/@aws-cdk/cdk-assets/test/integ.assets.refs.lit.expected.json @@ -0,0 +1,145 @@ +{ + "Parameters": { + "SampleAssetS3BucketE6B2908E": { + "Type": "String", + "Description": "S3 bucket for asset \"aws-cdk-asset-refs/SampleAsset\"" + }, + "SampleAssetS3ObjectKey6F5D200B": { + "Type": "String", + "Description": "S3 object for asset \"aws-cdk-asset-refs/SampleAsset\"" + } + }, + "Outputs": { + "S3BucketName": { + "Value": { + "Ref": "SampleAssetS3BucketE6B2908E" + }, + "Export": { + "Name": "aws-cdk-asset-refs:S3BucketName" + } + }, + "S3ObjectKey": { + "Value": { + "Ref": "SampleAssetS3ObjectKey6F5D200B" + }, + "Export": { + "Name": "aws-cdk-asset-refs:S3ObjectKey" + } + }, + "S3URL": { + "Value": { + "Fn::Join": [ + "", + [ + "https://", + "s3.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Ref": "SampleAssetS3BucketE6B2908E" + }, + "/", + { + "Ref": "SampleAssetS3ObjectKey6F5D200B" + } + ] + ] + }, + "Export": { + "Name": "aws-cdk-asset-refs:S3URL" + } + } + }, + "Resources": { + "TestUser6A619381": { + "Type": "AWS::IAM::User" + }, + "TestUserDefaultPolicyFC1191A6": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn", + ":", + { + "Ref": "AWS::Partition" + }, + ":", + "s3", + ":", + "", + ":", + "", + ":", + { + "Ref": "SampleAssetS3BucketE6B2908E" + } + ] + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::Join": [ + "", + [ + "arn", + ":", + { + "Ref": "AWS::Partition" + }, + ":", + "s3", + ":", + "", + ":", + "", + ":", + { + "Ref": "SampleAssetS3BucketE6B2908E" + } + ] + ] + }, + "/", + { + "Ref": "SampleAssetS3ObjectKey6F5D200B" + } + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "TestUserDefaultPolicyFC1191A6", + "Users": [ + { + "Ref": "TestUser6A619381" + } + ] + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/cdk-assets/test/integ.assets.refs.lit.ts b/packages/@aws-cdk/cdk-assets/test/integ.assets.refs.lit.ts new file mode 100644 index 0000000000000..0b664acaf73ed --- /dev/null +++ b/packages/@aws-cdk/cdk-assets/test/integ.assets.refs.lit.ts @@ -0,0 +1,23 @@ +import cdk = require('@aws-cdk/core'); +import path = require('path'); +import assets = require('../lib'); + +class TestStack extends cdk.Stack { + constructor(parent: cdk.App, name: string, props?: cdk.StackProps) { + super(parent, name, props); + + /// !show + const asset = new assets.ZipDirectoryAsset(this, 'SampleAsset', { + path: path.join(__dirname, 'sample-asset-directory') + }); + + new cdk.Output(this, 'S3BucketName', { value: asset.s3BucketName }); + new cdk.Output(this, 'S3ObjectKey', { value: asset.s3ObjectKey }); + new cdk.Output(this, 'S3URL', { value: asset.s3Url }); + /// !hide + } +} + +const app = new cdk.App(process.argv); +new TestStack(app, 'aws-cdk-asset-refs'); +process.stdout.write(app.run()); diff --git a/packages/@aws-cdk/cdk-assets/test/sample-asset-directory/sample-asset-file.txt b/packages/@aws-cdk/cdk-assets/test/sample-asset-directory/sample-asset-file.txt new file mode 100644 index 0000000000000..a21cdbf7d0691 --- /dev/null +++ b/packages/@aws-cdk/cdk-assets/test/sample-asset-directory/sample-asset-file.txt @@ -0,0 +1 @@ +// hello dear asset \ No newline at end of file diff --git a/packages/@aws-cdk/cdk-assets/test/test.asset.ts b/packages/@aws-cdk/cdk-assets/test/test.asset.ts new file mode 100644 index 0000000000000..49346c0640d6e --- /dev/null +++ b/packages/@aws-cdk/cdk-assets/test/test.asset.ts @@ -0,0 +1,270 @@ +import { expect } from '@aws-cdk/assert'; +import { Stack } from '@aws-cdk/core'; +import { Group, User } from '@aws-cdk/iam'; +import { Test } from 'nodeunit'; +import * as path from 'path'; +import { FileAsset, ZipDirectoryAsset } from '../lib/asset'; + +export = { + 'simple use case'(test: Test) { + const stack = new Stack(); + const dirPath = path.join(__dirname, 'sample-asset-directory'); + const asset = new ZipDirectoryAsset(stack, 'MyAsset', { + path: dirPath + }); + + // verify that metadata contains an "aws:cdk:asset" entry with + // the correct information + const entry = asset.metadata.find(m => m.type === 'aws:cdk:asset'); + test.ok(entry, 'found metadata entry'); + test.deepEqual(entry!.data, { + path: dirPath, + packaging: 'zip', + s3BucketParameter: 'MyAssetS3Bucket68C9B344', + s3KeyParameter: 'MyAssetS3ObjectKeyC07605E4' + }); + + // verify that now the template contains two parameters for this asset + expect(stack).toMatch({ + Parameters: { + MyAssetS3Bucket68C9B344: { + Type: "String", + Description: 'S3 bucket for asset "MyAsset"' + }, + MyAssetS3ObjectKeyC07605E4: { + Type: "String", + Description: 'S3 object for asset "MyAsset"' + } + } + }); + + test.done(); + }, + + '"file" assets'(test: Test) { + const stack = new Stack(); + const filePath = path.join(__dirname, 'file-asset.txt'); + const asset = new FileAsset(stack, 'MyAsset', { path: filePath }); + const entry = asset.metadata.find(m => m.type === 'aws:cdk:asset'); + test.ok(entry, 'found metadata entry'); + test.deepEqual(entry!.data, { + path: filePath, + packaging: 'file', + s3BucketParameter: 'MyAssetS3Bucket68C9B344', + s3KeyParameter: 'MyAssetS3ObjectKeyC07605E4' + }); + + expect(stack).toMatch({ + Parameters: { + MyAssetS3Bucket68C9B344: { + Type: "String", + Description: 'S3 bucket for asset "MyAsset"' + }, + MyAssetS3ObjectKeyC07605E4: { + Type: "String", + Description: 'S3 object for asset "MyAsset"' + } + } + }); + + test.done(); + }, + + '"readers" or "grantRead" can be used to grant read permissions on the asset to a principal'(test: Test) { + const stack = new Stack(); + const user = new User(stack, 'MyUser'); + const group = new Group(stack, 'MyGroup'); + + const asset = new ZipDirectoryAsset(stack, 'MyAsset', { + path: path.join(__dirname, 'sample-asset-directory'), + readers: [ user ] + }); + + asset.grantRead(group); + + expect(stack).toMatch({ + Resources: { + MyUserDC45028B: { + Type: "AWS::IAM::User" + }, + MyUserDefaultPolicy7B897426: { + Type: "AWS::IAM::Policy", + Properties: { + PolicyDocument: { + Statement: [ + { + Action: [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*" + ], + Effect: "Allow", + Resource: [ + { + "Fn::Join": [ + "", + [ + "arn", + ":", + { + Ref: "AWS::Partition" + }, + ":", + "s3", + ":", + "", + ":", + "", + ":", + { + Ref: "MyAssetS3Bucket68C9B344" + } + ] + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::Join": [ + "", + [ + "arn", + ":", + { + Ref: "AWS::Partition" + }, + ":", + "s3", + ":", + "", + ":", + "", + ":", + { + Ref: "MyAssetS3Bucket68C9B344" + } + ] + ] + }, + "/", + { + Ref: "MyAssetS3ObjectKeyC07605E4" + } + ] + ] + } + ] + } + ], + Version: "2012-10-17" + }, + PolicyName: "MyUserDefaultPolicy7B897426", + Users: [ + { + Ref: "MyUserDC45028B" + } + ] + } + }, + MyGroupCBA54B1B: { + Type: "AWS::IAM::Group" + }, + MyGroupDefaultPolicy72C41231: { + Type: "AWS::IAM::Policy", + Properties: { + Groups: [ + { + Ref: "MyGroupCBA54B1B" + } + ], + PolicyDocument: { + Statement: [ + { + Action: [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*" + ], + Effect: "Allow", + Resource: [ + { + "Fn::Join": [ + "", + [ + "arn", + ":", + { + Ref: "AWS::Partition" + }, + ":", + "s3", + ":", + "", + ":", + "", + ":", + { + Ref: "MyAssetS3Bucket68C9B344" + } + ] + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::Join": [ + "", + [ + "arn", + ":", + { + Ref: "AWS::Partition" + }, + ":", + "s3", + ":", + "", + ":", + "", + ":", + { + Ref: "MyAssetS3Bucket68C9B344" + } + ] + ] + }, + "/", + { + Ref: "MyAssetS3ObjectKeyC07605E4" + } + ] + ] + } + ] + } + ], + Version: "2012-10-17" + }, + PolicyName: "MyGroupDefaultPolicy72C41231" + } + } + }, + Parameters: { + MyAssetS3Bucket68C9B344: { + Type: "String", + Description: "S3 bucket for asset \"MyAsset\"" + }, + MyAssetS3ObjectKeyC07605E4: { + Type: "String", + Description: "S3 object for asset \"MyAsset\"" + } + } + }); + + test.done(); + } +}; diff --git a/packages/@aws-cdk/cx-api/lib/cxapi.ts b/packages/@aws-cdk/cx-api/lib/cxapi.ts index acb5ce231a336..9e813f3bd437c 100644 --- a/packages/@aws-cdk/cx-api/lib/cxapi.ts +++ b/packages/@aws-cdk/cx-api/lib/cxapi.ts @@ -95,3 +95,11 @@ export const DEFAULT_ACCOUNT_CONTEXT_KEY = 'aws:cdk:toolkit:default-account'; * Context parameter for the default AWS region to use if a stack's environment is not set. */ export const DEFAULT_REGION_CONTEXT_KEY = 'aws:cdk:toolkit:default-region'; + +export const ASSET_METADATA = 'aws:cdk:asset'; +export interface AssetMetadataEntry { + path: string; + packaging: 'zip' | 'file'; + s3BucketParameter: string; + s3KeyParameter: string; +} \ No newline at end of file diff --git a/packages/aws-cdk/lib/api/deploy-stack.ts b/packages/aws-cdk/lib/api/deploy-stack.ts index 0b99db4f0cb5a..4e5d78027ddad 100644 --- a/packages/aws-cdk/lib/api/deploy-stack.ts +++ b/packages/aws-cdk/lib/api/deploy-stack.ts @@ -1,9 +1,9 @@ import { StackInfo, SynthesizedStack } from '@aws-cdk/cx-api'; import { CloudFormation } from 'aws-sdk'; import * as colors from 'colors/safe'; -import * as crypto from 'crypto'; import * as uuid from 'uuid'; import * as YAML from 'yamljs'; +import { prepareAssets } from '../assets'; import { debug, error } from '../logging'; import { Mode } from './aws-auth/credentials'; import { ToolkitInfo } from './toolkit-info'; @@ -31,12 +31,14 @@ export async function deployStack(stack: SynthesizedStack, throw new Error(`The stack ${stack.name} does not have an environment`); } + const params = await prepareAssets(stack, toolkitInfo); + deployName = deployName || stack.name; const executionId = uuid.v4(); const cfn = await sdk.cloudFormation(stack.environment, Mode.ForWriting); - const bodyParameter = await makeBodyParameter(stack, sdk, toolkitInfo); + const bodyParameter = await makeBodyParameter(stack, toolkitInfo); if (!await stackExists(cfn, deployName)) { await createEmptyStack(cfn, deployName, quiet); @@ -52,6 +54,7 @@ export async function deployStack(stack: SynthesizedStack, Description: `CDK Changeset for execution ${executionId}`, TemplateBody: bodyParameter.TemplateBody, TemplateURL: bodyParameter.TemplateURL, + Parameters: params, Capabilities: [ 'CAPABILITY_IAM', 'CAPABILITY_NAMED_IAM' ] }).promise(); debug('Initiated creation of changeset: %s; waiting for it to finish creating...', changeSet.Id); @@ -110,20 +113,17 @@ async function createEmptyStack(cfn: CloudFormation, stackName: string, quiet: b * @param sdk an AWS SDK to use when interacting with S3 * @param toolkitInfo information about the toolkit stack */ -async function makeBodyParameter(stack: SynthesizedStack, sdk: SDK, toolkitInfo?: ToolkitInfo): Promise { +async function makeBodyParameter(stack: SynthesizedStack, toolkitInfo?: ToolkitInfo): Promise { const templateJson = YAML.stringify(stack.template, 16, 4); if (toolkitInfo) { - const hash = crypto.createHash('sha256').update(templateJson).digest('hex'); - const key = `cdk/${stack.name}/${hash}.yml`; - const s3 = await sdk.s3(stack.environment!, Mode.ForWriting); - await s3.putObject({ - Bucket: toolkitInfo.bucketName, - Key: key, - Body: templateJson, - ContentType: 'application/x-yaml' - }).promise(); - debug('Stored template in S3 at s3://%s/%s', toolkitInfo.bucketName, key); - return { TemplateURL: `https://${toolkitInfo.bucketName}.s3.amazonaws.com/${key}` }; + const s3KeyPrefix = `cdk/${stack.name}/`; + const s3KeySuffix = '.yml'; + const { key } = await toolkitInfo.uploadIfChanged(templateJson, { + s3KeyPrefix, s3KeySuffix, contentType: 'application/x-yaml' + }); + const templateURL = `${toolkitInfo.bucketUrl}/${key}`; + debug('Stored template in S3 at:', templateURL); + return { TemplateURL: templateURL }; } else if (templateJson.length > 51_200) { error('The template for stack %s is %d bytes long, a CDK Toolkit stack is required for deployment of templates larger than 51,200 bytes. ' + 'A CDK Toolkit stack can be created using %s', diff --git a/packages/aws-cdk/lib/api/toolkit-info.ts b/packages/aws-cdk/lib/api/toolkit-info.ts index 91e2927fa8517..dacde51269256 100644 --- a/packages/aws-cdk/lib/api/toolkit-info.ts +++ b/packages/aws-cdk/lib/api/toolkit-info.ts @@ -1,15 +1,82 @@ import { Environment } from '@aws-cdk/cx-api'; -import { CloudFormation } from 'aws-sdk'; +import { CloudFormation, S3 } from 'aws-sdk'; import * as colors from 'colors/safe'; +import { md5hash } from '../archive'; import { debug } from '../logging'; import { Mode } from './aws-auth/credentials'; import { BUCKET_DOMAIN_NAME_OUTPUT, BUCKET_NAME_OUTPUT } from './bootstrap-environment'; import { waitForStack } from './util/cloudformation'; import { SDK } from './util/sdk'; -export interface ToolkitInfo { - bucketName: string - bucketEndpoint: string +export class ToolkitInfo { + constructor(private readonly props: { + sdk: SDK, + bucketName: string, + bucketEndpoint: string, + environment: Environment + }) { } + + public get bucketUrl() { + return `https://${this.props.bucketName}.s3.amazonaws.com`; + } + + public get bucketName() { + return this.props.bucketName; + } + + /** + * Uploads a data blob to S3 under the specified key prefix. + * Uses md5 hash to render the full key and skips upload if an object + * already exists by this key. + */ + public async uploadIfChanged(data: any, props: { + s3KeyPrefix?: string, + s3KeySuffix?: string, + contentType?: string, + }): Promise<{ key: string, changed: boolean }> { + const s3 = await this.props.sdk.s3(this.props.environment, Mode.ForWriting); + + const s3KeyPrefix = props.s3KeyPrefix || ''; + const s3KeySuffix = props.s3KeySuffix || ''; + + const bucket = this.props.bucketName; + + const hash = md5hash(data); + const key = `${s3KeyPrefix}${hash}${s3KeySuffix}`; + const url = `s3://${bucket}/${key}`; + + debug(`${url}: checking if already exists`); + if (await objectExists(s3, bucket, key)) { + debug(`${url}: found (skipping upload)`); + return { key, changed: false }; + } + + debug(`${url}: uploading`); + await s3.putObject({ + Bucket: bucket, + Key: key, + Body: data, + ContentType: props.contentType + }).promise(); + + debug(`${url}: upload complete`); + + return { key, changed: true }; + } + +} + +async function objectExists(s3: S3, bucket: string, key: string) { + try { + await s3.headObject({ Bucket: bucket, Key: key }).promise(); + return true; + } catch (e) { + if (e.code === 'NotFound') { + return false; + } + + throw e; + } } export async function loadToolkitInfo(environment: Environment, sdk: SDK, stackName: string): Promise { @@ -20,10 +87,11 @@ export async function loadToolkitInfo(environment: Environment, sdk: SDK, stackN environment.name, stackName, colors.blue(`cdk bootstrap "${environment.name}"`)); return undefined; } - return { + return new ToolkitInfo({ + sdk, environment, bucketName: getOutputValue(stack, BUCKET_NAME_OUTPUT), bucketEndpoint: getOutputValue(stack, BUCKET_DOMAIN_NAME_OUTPUT) - }; + }); } function getOutputValue(stack: CloudFormation.Stack, output: string): string { diff --git a/packages/aws-cdk/lib/archive.ts b/packages/aws-cdk/lib/archive.ts new file mode 100644 index 0000000000000..d0495a0062b77 --- /dev/null +++ b/packages/aws-cdk/lib/archive.ts @@ -0,0 +1,21 @@ +import archiver = require('archiver'); +import crypto = require('crypto'); +import fs = require('fs-extra'); + +export function zipDirectory(directory: string, outputFile: string): Promise { + return new Promise((ok, fail) => { + const output = fs.createWriteStream(outputFile); + const archive = archiver('zip'); + archive.directory(directory, false); + archive.pipe(output); + archive.finalize(); + + archive.on('warning', fail); + archive.on('error', fail); + output.once('close', () => ok()); + }); +} + +export function md5hash(data: any) { + return crypto.createHash('sha256').update(data).digest('hex'); +} \ No newline at end of file diff --git a/packages/aws-cdk/lib/assets.ts b/packages/aws-cdk/lib/assets.ts new file mode 100644 index 0000000000000..2d3fb15afcc97 --- /dev/null +++ b/packages/aws-cdk/lib/assets.ts @@ -0,0 +1,111 @@ +import { ASSET_METADATA, AssetMetadataEntry, StackMetadata, SynthesizedStack } from "@aws-cdk/cx-api"; +import { CloudFormation } from "aws-sdk"; +import fs = require('fs-extra'); +import path = require('path'); +import { ToolkitInfo } from "./api/toolkit-info"; +import { zipDirectory } from './archive'; +import { debug, success } from "./logging"; + +export async function prepareAssets(stack: SynthesizedStack, toolkitInfo?: ToolkitInfo): Promise { + const assets = findAssets(stack.metadata); + if (assets.length === 0) { + return []; + } + + if (!toolkitInfo) { + throw new Error('Since this stack uses assets, the toolkit stack must be deployed to the environment ("cdk bootstrap")'); + } + + debug('Preparing assets'); + let params = new Array(); + for (const asset of assets) { + debug(` - ${asset.path} (${asset.packaging})`); + + params = params.concat(await prepareAsset(asset, toolkitInfo)); + } + + return params; +} + +async function prepareAsset(asset: AssetMetadataEntry, toolkitInfo: ToolkitInfo): Promise { + debug('Preparing asset', JSON.stringify(asset)); + switch (asset.packaging) { + case 'zip': + return await prepareZipAsset(asset, toolkitInfo); + case 'file': + return await prepareFileAsset(asset, toolkitInfo); + default: + throw new Error(`Unsupported packaging type: ${asset.packaging}`); + } +} + +async function prepareZipAsset(asset: AssetMetadataEntry, toolkitInfo: ToolkitInfo): Promise { + debug('Preparing zip asset from directory:', asset.path); + + const staging = await fs.mkdtemp('/tmp/cdk-assets'); + + try { + const archiveFile = path.join(staging, 'archive.zip'); + await zipDirectory(asset.path, archiveFile); + + debug('zip archive:', archiveFile); + const data = await fs.readFile(archiveFile); + + const { key, changed } = await toolkitInfo.uploadIfChanged(data, { + s3KeyPrefix: 'assets/', + s3KeySuffix: '.zip', + contentType: 'application/zip', + }); + + const s3url = `s3://${toolkitInfo.bucketName}/${key}`; + if (changed) { + success(` πŸ‘œ Asset ${asset.path} (directory zip) uploaded to ${s3url}`); + } else { + success(` πŸ‘œ Asset ${asset.path} (directory zip) already exists in ${s3url}`); + } + + return [ + { ParameterKey: asset.s3BucketParameter, ParameterValue: toolkitInfo.bucketName }, + { ParameterKey: asset.s3KeyParameter, ParameterValue: key } + ]; + } finally { + await fs.remove(staging); + } +} + +async function prepareFileAsset(asset: AssetMetadataEntry, toolkitInfo: ToolkitInfo): Promise { + debug('Preparing file asset:', asset.path); + + const data = await fs.readFile(asset.path); + + const { key, changed } = await toolkitInfo.uploadIfChanged(data, { + s3KeyPrefix: 'assets/', + s3KeySuffix: path.extname(asset.path) + }); + + const s3url = `s3://${toolkitInfo.bucketName}/${key}`; + if (changed) { + success(` πŸ‘œ Asset ${asset.path} uploaded to ${s3url}`); + } else { + success(` πŸ‘œ Asset ${asset.path} already exists in ${s3url}`); + } + + return [ + { ParameterKey: asset.s3BucketParameter, ParameterValue: toolkitInfo.bucketName }, + { ParameterKey: asset.s3KeyParameter, ParameterValue: key } + ]; +} + +function findAssets(metadata: StackMetadata): AssetMetadataEntry[] { + const assets = new Array(); + + for (const k of Object.keys(metadata)) { + for (const entry of metadata[k]) { + if (entry.type === ASSET_METADATA) { + assets.push(entry.data); + } + } + } + + return assets; +} \ No newline at end of file diff --git a/packages/aws-cdk/package-lock.json b/packages/aws-cdk/package-lock.json index 4427cef0316e9..ba923d521dd4e 100644 --- a/packages/aws-cdk/package-lock.json +++ b/packages/aws-cdk/package-lock.json @@ -1,16 +1,35 @@ { - "requires": true, + "name": "aws-cdk", + "version": "0.7.3-beta", "lockfileVersion": 1, + "requires": true, "dependencies": { + "@types/archiver": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@types/archiver/-/archiver-2.1.2.tgz", + "integrity": "sha512-UGcGgeMoGpFh97pQkzR+cgSrJDjVmO+CZ2N+WDI7i0QCOJE+PocpfHEEtePhlttULdbfOrzNi0yexg66E52Prw==", + "dev": true, + "requires": { + "@types/glob": "*" + } + }, "@types/caseless": { "version": "0.12.1", "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.1.tgz", - "integrity": "sha512-FhlMa34NHp9K5MY1Uz8yb+ZvuX0pnvn3jScRSNAb75KHGB8d3rEU6hqMs3Z2vjuytcMfRg6c5CHMc3wtYyD2/A==" + "integrity": "sha512-FhlMa34NHp9K5MY1Uz8yb+ZvuX0pnvn3jScRSNAb75KHGB8d3rEU6hqMs3Z2vjuytcMfRg6c5CHMc3wtYyD2/A==", + "dev": true + }, + "@types/events": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@types/events/-/events-1.2.0.tgz", + "integrity": "sha512-KEIlhXnIutzKwRbQkGWb/I4HFqBuUykAdHgDED6xqwXJfONCjF5VoE0cXEiurh3XauygxzeDzgtXUqvLkxFzzA==", + "dev": true }, "@types/form-data": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/@types/form-data/-/form-data-2.2.1.tgz", "integrity": "sha512-JAMFhOaHIciYVh8fb5/83nmuO/AHwmto+Hq7a9y8FzLDcC1KCU344XDOMEmahnrTFlHjgh4L0WJFczNIX2GxnQ==", + "dev": true, "requires": { "@types/node": "*" } @@ -19,29 +38,45 @@ "version": "4.0.8", "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-4.0.8.tgz", "integrity": "sha512-Z5nu9Pbxj9yNeXIK3UwGlRdJth4cZ5sCq05nI7FaI6B0oz28nxkOtp6Lsz0ZnmLHJGvOJfB/VHxSTbVq/i6ujA==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/glob": { + "version": "5.0.35", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-5.0.35.tgz", + "integrity": "sha512-wc+VveszMLyMWFvXLkloixT4n0harUIVZjnpzztaZ0nKLuul7Z32iMt2fUFGAaZ4y1XWjFRMtCI5ewvyh4aIeg==", + "dev": true, "requires": { + "@types/events": "*", + "@types/minimatch": "*", "@types/node": "*" } }, "@types/minimatch": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", - "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==" + "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==", + "dev": true }, "@types/mockery": { "version": "1.4.29", "resolved": "https://registry.npmjs.org/@types/mockery/-/mockery-1.4.29.tgz", - "integrity": "sha1-m6It838H43gP/4Ux0aOOYz+UV6U=" + "integrity": "sha1-m6It838H43gP/4Ux0aOOYz+UV6U=", + "dev": true }, "@types/node": { "version": "8.10.17", "resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.17.tgz", - "integrity": "sha512-3N3FRd/rA1v5glXjb90YdYUa+sOB7WrkU2rAhKZnF4TKD86Cym9swtulGuH0p9nxo7fP5woRNa8b0oFTpCO1bg==" + "integrity": "sha512-3N3FRd/rA1v5glXjb90YdYUa+sOB7WrkU2rAhKZnF4TKD86Cym9swtulGuH0p9nxo7fP5woRNa8b0oFTpCO1bg==", + "dev": true }, "@types/request": { "version": "2.47.1", "resolved": "https://registry.npmjs.org/@types/request/-/request-2.47.1.tgz", "integrity": "sha512-TV3XLvDjQbIeVxJ1Z3oCTDk/KuYwwcNKVwz2YaT0F5u86Prgc4syDAp6P96rkTQQ4bIdh+VswQIC9zS6NjY7/g==", + "dev": true, "requires": { "@types/caseless": "*", "@types/form-data": "*", @@ -52,12 +87,14 @@ "@types/tough-cookie": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-2.3.3.tgz", - "integrity": "sha512-MDQLxNFRLasqS4UlkWMSACMKeSm1x4Q3TxzUC7KQUsh6RK1ZrQ0VEyE3yzXcBu+K8ejVj4wuX32eUG02yNp+YQ==" + "integrity": "sha512-MDQLxNFRLasqS4UlkWMSACMKeSm1x4Q3TxzUC7KQUsh6RK1ZrQ0VEyE3yzXcBu+K8ejVj4wuX32eUG02yNp+YQ==", + "dev": true }, "@types/uuid": { "version": "3.4.3", "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-3.4.3.tgz", "integrity": "sha512-5fRLCYhLtDb3hMWqQyH10qtF+Ud2JnNCXTCZ+9ktNdCcgslcuXkDTkFcJNk++MT29yDntDnlF1+jD+uVGumsbw==", + "dev": true, "requires": { "@types/node": "*" } @@ -65,12 +102,14 @@ "@types/yamljs": { "version": "0.2.30", "resolved": "https://registry.npmjs.org/@types/yamljs/-/yamljs-0.2.30.tgz", - "integrity": "sha1-0DTh0ynkbo0Pc3yajbl/aPgbU4I=" + "integrity": "sha1-0DTh0ynkbo0Pc3yajbl/aPgbU4I=", + "dev": true }, "@types/yargs": { "version": "8.0.3", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-8.0.3.tgz", - "integrity": "sha512-YdxO7zGQf2qJeMgR0fNO8QTlj88L2zCP5GOddovoTyetgLiNDOUXcWzhWKb4EdZZlOjLQUA0JM8lW7VcKQL+9w==" + "integrity": "sha512-YdxO7zGQf2qJeMgR0fNO8QTlj88L2zCP5GOddovoTyetgLiNDOUXcWzhWKb4EdZZlOjLQUA0JM8lW7VcKQL+9w==", + "dev": true }, "ajv": { "version": "5.5.2", @@ -88,6 +127,34 @@ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" }, + "archiver": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-2.1.1.tgz", + "integrity": "sha1-/2YrSnggFJSj7lRNOjP+dJZQnrw=", + "requires": { + "archiver-utils": "^1.3.0", + "async": "^2.0.0", + "buffer-crc32": "^0.2.1", + "glob": "^7.0.0", + "lodash": "^4.8.0", + "readable-stream": "^2.0.0", + "tar-stream": "^1.5.0", + "zip-stream": "^1.2.0" + } + }, + "archiver-utils": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-1.3.0.tgz", + "integrity": "sha1-5QtMCccL89aA4y/xt5lOn52JUXQ=", + "requires": { + "glob": "^7.0.0", + "graceful-fs": "^4.1.0", + "lazystream": "^1.0.0", + "lodash": "^4.8.0", + "normalize-path": "^2.0.0", + "readable-stream": "^2.0.0" + } + }, "argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -106,6 +173,14 @@ "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" }, + "async": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.1.tgz", + "integrity": "sha512-fNEiL2+AZt6AlAw/29Cr0UDe4sRAHCpEHh54WMz+Bb7QfNcFw4h3loofyJpLeQs4Yx7yuqu/2dLgM5hKOs6HlQ==", + "requires": { + "lodash": "^4.17.10" + } + }, "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -156,6 +231,15 @@ "tweetnacl": "^0.14.3" } }, + "bl": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.2.tgz", + "integrity": "sha512-e8tQYnZodmebYDWGH7KMRvtzKXaJHx3BbilrgZCfvyLUYdKpK1t5PSPmpkny/SgiTSCnjfLW7v5rlONXVFkQEA==", + "requires": { + "readable-stream": "^2.3.5", + "safe-buffer": "^5.1.1" + } + }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -175,6 +259,30 @@ "isarray": "^1.0.0" } }, + "buffer-alloc": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", + "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==", + "requires": { + "buffer-alloc-unsafe": "^1.1.0", + "buffer-fill": "^1.0.0" + } + }, + "buffer-alloc-unsafe": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz", + "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==" + }, + "buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=" + }, + "buffer-fill": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", + "integrity": "sha1-+PeLdniYiO858gXNY39o5wISKyw=" + }, "buffer-from": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.0.0.tgz", @@ -248,6 +356,17 @@ "delayed-stream": "~1.0.0" } }, + "compress-commons": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-1.2.2.tgz", + "integrity": "sha1-UkqfEJA/OoEzibAiXSfEi7dRiQ8=", + "requires": { + "buffer-crc32": "^0.2.1", + "crc32-stream": "^2.0.0", + "normalize-path": "^2.0.0", + "readable-stream": "^2.0.0" + } + }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -258,6 +377,34 @@ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" }, + "crc": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/crc/-/crc-3.7.0.tgz", + "integrity": "sha512-ZwmUex488OBjSVOMxnR/dIa1yxisBMJNEi+UxzXpKhax8MPsQtoRQtl5Qgo+W7pcSVkRXa3BEVjaniaWKtvKvw==", + "requires": { + "buffer": "^5.1.0" + }, + "dependencies": { + "buffer": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.1.0.tgz", + "integrity": "sha512-YkIRgwsZwJWTnyQrsBTWefizHh+8GYj3kbL1BTiAQ/9pwpino0G7B2gp5tx/FUBqUlvtxV85KNR3mwfAtv15Yw==", + "requires": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4" + } + } + } + }, + "crc32-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-2.0.0.tgz", + "integrity": "sha1-483TtN8xaN10494/u8t7KX/pCPQ=", + "requires": { + "crc": "^3.4.4", + "readable-stream": "^2.0.0" + } + }, "cross-spawn": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", @@ -314,6 +461,14 @@ "jsbn": "~0.1.0" } }, + "end-of-stream": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", + "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", + "requires": { + "once": "^1.4.0" + } + }, "error-ex": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.1.tgz", @@ -389,6 +544,11 @@ "mime-types": "^2.1.12" } }, + "fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" + }, "fs-extra": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-4.0.3.tgz", @@ -599,6 +759,14 @@ "verror": "1.10.0" } }, + "lazystream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.0.tgz", + "integrity": "sha1-9plf4PggOS9hOWvolGJAe7dxaOQ=", + "requires": { + "readable-stream": "^2.0.5" + } + }, "lcid": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", @@ -678,7 +846,8 @@ "mockery": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/mockery/-/mockery-2.1.0.tgz", - "integrity": "sha512-9VkOmxKlWXoDO/h1jDZaS4lH33aWfRiJiNT/tKj+8OGzrcFDLo8d0syGdbsc3Bc4GvRXPb+NMMvojotmuGJTvA==" + "integrity": "sha512-9VkOmxKlWXoDO/h1jDZaS4lH33aWfRiJiNT/tKj+8OGzrcFDLo8d0syGdbsc3Bc4GvRXPb+NMMvojotmuGJTvA==", + "dev": true }, "mute-stream": { "version": "0.0.7", @@ -696,6 +865,14 @@ "validate-npm-package-license": "^3.0.1" } }, + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "requires": { + "remove-trailing-separator": "^1.0.1" + } + }, "npm-run-path": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", @@ -799,6 +976,11 @@ "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=" }, + "process-nextick-args": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" + }, "promptly": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/promptly/-/promptly-0.2.1.tgz", @@ -854,6 +1036,25 @@ "read-pkg": "^2.0.0" } }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=" + }, "request": { "version": "2.87.0", "resolved": "https://registry.npmjs.org/request/-/request-2.87.0.tgz", @@ -1020,6 +1221,14 @@ } } }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + }, "strip-ansi": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", @@ -1038,6 +1247,25 @@ "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=" }, + "tar-stream": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.6.1.tgz", + "integrity": "sha512-IFLM5wp3QrJODQFPm6/to3LJZrONdBY/otxcvDIQzu217zKye6yVR3hhi9lAjrC2Z+m/j5oDxMPb1qcd8cIvpA==", + "requires": { + "bl": "^1.0.0", + "buffer-alloc": "^1.1.0", + "end-of-stream": "^1.0.0", + "fs-constants": "^1.0.0", + "readable-stream": "^2.3.0", + "to-buffer": "^1.1.0", + "xtend": "^4.0.0" + } + }, + "to-buffer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.1.1.tgz", + "integrity": "sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg==" + }, "tough-cookie": { "version": "2.3.4", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.4.tgz", @@ -1081,6 +1309,11 @@ "querystring": "0.2.0" } }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, "uuid": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.1.0.tgz", @@ -1171,6 +1404,11 @@ "resolved": "https://registry.npmjs.org/xregexp/-/xregexp-4.0.0.tgz", "integrity": "sha512-PHyM+sQouu7xspQQwELlGwwd05mXUFqwFYfqPO0cC7x4fxyHnnuetmQr6CjJiafIDoH4MogHb9dOoJzR/Y4rFg==" }, + "xtend": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", + "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=" + }, "y18n": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", @@ -1236,6 +1474,17 @@ "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=" } } + }, + "zip-stream": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-1.2.0.tgz", + "integrity": "sha1-qLxF9MG0lpnGuQGYuqyqzbzUugQ=", + "requires": { + "archiver-utils": "^1.3.0", + "compress-commons": "^1.2.0", + "lodash": "^4.8.0", + "readable-stream": "^2.0.0" + } } } } diff --git a/packages/aws-cdk/package.json b/packages/aws-cdk/package.json index b92ee67b9ed2a..52847586b6aae 100644 --- a/packages/aws-cdk/package.json +++ b/packages/aws-cdk/package.json @@ -15,7 +15,9 @@ "test": "cdk-test" }, "cdk-build": { - "pre": ["./generate.sh"] + "pre": [ + "./generate.sh" + ] }, "nyc": { "lines": 8, @@ -27,6 +29,7 @@ }, "license": "LicenseRef-LICENSE", "devDependencies": { + "@types/archiver": "^2.1.2", "@types/fs-extra": "^4.0.8", "@types/minimatch": "^3.0.3", "@types/mockery": "^1.4.29", @@ -34,14 +37,15 @@ "@types/uuid": "^3.4.3", "@types/yamljs": "^0.2.0", "@types/yargs": "^8.0.3", + "cdk-build-tools": "^0.7.3-beta", "mockery": "^2.1.0", - "pkglint": "^0.7.3-beta", - "cdk-build-tools": "^0.7.3-beta" + "pkglint": "^0.7.3-beta" }, "dependencies": { "@aws-cdk/cloudformation-diff": "^0.7.3-beta", "@aws-cdk/cx-api": "^0.7.3-beta", "@aws-cdk/util": "^0.7.3-beta", + "archiver": "^2.1.1", "aws-sdk": "^2.259.1", "camelcase": "^5.0.0", "colors": "^1.2.1", diff --git a/packages/aws-cdk/test/test-archive/file1.txt b/packages/aws-cdk/test/test-archive/file1.txt new file mode 100644 index 0000000000000..7bb7edbd4b634 --- /dev/null +++ b/packages/aws-cdk/test/test-archive/file1.txt @@ -0,0 +1 @@ +I am file1 \ No newline at end of file diff --git a/packages/aws-cdk/test/test-archive/file2.txt b/packages/aws-cdk/test/test-archive/file2.txt new file mode 100644 index 0000000000000..ccb69856e7157 --- /dev/null +++ b/packages/aws-cdk/test/test-archive/file2.txt @@ -0,0 +1,2 @@ +I am file2 +BLA! \ No newline at end of file diff --git a/packages/aws-cdk/test/test-archive/subdir/file3.txt b/packages/aws-cdk/test/test-archive/subdir/file3.txt new file mode 100644 index 0000000000000..976606ef5a8ac --- /dev/null +++ b/packages/aws-cdk/test/test-archive/subdir/file3.txt @@ -0,0 +1 @@ +I am in a subdirectory diff --git a/packages/aws-cdk/test/test.archive.ts b/packages/aws-cdk/test/test.archive.ts new file mode 100644 index 0000000000000..0f381c33a0751 --- /dev/null +++ b/packages/aws-cdk/test/test.archive.ts @@ -0,0 +1,46 @@ +import { exec as _exec } from 'child_process'; +import fs = require('fs-extra'); +import { Test } from 'nodeunit'; +import path = require('path'); +import { promisify } from 'util'; +import { md5hash, zipDirectory } from '../lib/api/archive'; +const exec = promisify(_exec); + +export = { + async 'zipDirectory can take a directory and produce a zip from it'(test: Test) { + const stagingDir = await fs.mkdtemp('/tmp/test.archive'); + const zipFile = path.join(stagingDir, 'output.zip'); + const originalDir = path.join(__dirname, 'test-archive'); + const extractDir = await fs.mkdtemp('/tmp/test.archive.extract'); + await zipDirectory(originalDir, zipFile); + + // unzip and verify that the resulting tree is the same + await exec(`unzip ${zipFile}`, { cwd: extractDir }); + + try { + await exec(`diff -bur ${originalDir} ${extractDir}`); + } catch (e) { + test.ok(false, `extracted directory ${extractDir} differs from original ${originalDir}`); + } + + await fs.remove(stagingDir); + await fs.remove(extractDir); + test.done(); + }, + + async 'md5 hash of a zip stays consistent across invocations'(test: Test) { + const stagingDir = await fs.mkdtemp('/tmp/test.archive'); + const zipFile1 = path.join(stagingDir, 'output.zip'); + const zipFile2 = path.join(stagingDir, 'output.zip'); + const originalDir = path.join(__dirname, 'test-archive'); + await zipDirectory(originalDir, zipFile1); + await new Promise(ok => setTimeout(ok, 2000)); // wait 2s + await zipDirectory(originalDir, zipFile2); + + const hash1 = md5hash(await fs.readFile(zipFile1)); + const hash2 = md5hash(await fs.readFile(zipFile2)); + + test.deepEqual(hash1, hash2, 'md5 hash of two zips of the same content are not the same'); + test.done(); + } +}; \ No newline at end of file From 22128e5ca6f02eb99bc528d90c1edd4606cfa1cd Mon Sep 17 00:00:00 2001 From: Elad Ben-Israel Date: Wed, 18 Jul 2018 15:29:54 +0300 Subject: [PATCH 2/6] Fix test --- packages/aws-cdk/test/test.archive.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/aws-cdk/test/test.archive.ts b/packages/aws-cdk/test/test.archive.ts index 0f381c33a0751..48bdb9849ae99 100644 --- a/packages/aws-cdk/test/test.archive.ts +++ b/packages/aws-cdk/test/test.archive.ts @@ -3,7 +3,7 @@ import fs = require('fs-extra'); import { Test } from 'nodeunit'; import path = require('path'); import { promisify } from 'util'; -import { md5hash, zipDirectory } from '../lib/api/archive'; +import { md5hash, zipDirectory } from '../lib/archive'; const exec = promisify(_exec); export = { @@ -43,4 +43,4 @@ export = { test.deepEqual(hash1, hash2, 'md5 hash of two zips of the same content are not the same'); test.done(); } -}; \ No newline at end of file +}; From 47643c4f3094d2c08626b19b92ac80a283be8a08 Mon Sep 17 00:00:00 2001 From: Elad Ben-Israel Date: Wed, 18 Jul 2018 15:36:02 +0300 Subject: [PATCH 3/6] Fix dotnet namesapce --- packages/@aws-cdk/cdk-assets/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk/cdk-assets/package.json b/packages/@aws-cdk/cdk-assets/package.json index 37a9e2fe442cc..27be41f8a1443 100644 --- a/packages/@aws-cdk/cdk-assets/package.json +++ b/packages/@aws-cdk/cdk-assets/package.json @@ -8,7 +8,7 @@ "outdir": "dist", "names": { "java": "com.amazonaws.cdk.cdkassets", - "dotnet": "Aws.Cdk.CdkAssets" + "dotnet": "Amazon.Cdk.CdkAssets" } }, "repository": { From 05c0e8d669b8e3ef81d4de38dc8650709fbb3029 Mon Sep 17 00:00:00 2001 From: Elad Ben-Israel Date: Wed, 18 Jul 2018 16:13:40 +0300 Subject: [PATCH 4/6] Updating integration test expectations --- .../test/integ.assets.directory.lit.expected.json | 8 ++++---- .../cdk-assets/test/integ.assets.directory.lit.ts | 6 +++++- .../cdk-assets/test/integ.assets.file.lit.expected.json | 8 ++++---- .../@aws-cdk/cdk-assets/test/integ.assets.file.lit.ts | 6 +++++- .../cdk-assets/test/integ.assets.refs.lit.expected.json | 8 ++++---- .../@aws-cdk/cdk-assets/test/integ.assets.refs.lit.ts | 4 ++++ 6 files changed, 26 insertions(+), 14 deletions(-) diff --git a/packages/@aws-cdk/cdk-assets/test/integ.assets.directory.lit.expected.json b/packages/@aws-cdk/cdk-assets/test/integ.assets.directory.lit.expected.json index af8fa84270de8..e3ca3797ddd0c 100644 --- a/packages/@aws-cdk/cdk-assets/test/integ.assets.directory.lit.expected.json +++ b/packages/@aws-cdk/cdk-assets/test/integ.assets.directory.lit.expected.json @@ -10,10 +10,10 @@ } }, "Resources": { - "TestUser6A619381": { + "MyUserDC45028B": { "Type": "AWS::IAM::User" }, - "TestUserDefaultPolicyFC1191A6": { + "MyUserDefaultPolicy7B897426": { "Type": "AWS::IAM::Policy", "Properties": { "PolicyDocument": { @@ -86,10 +86,10 @@ ], "Version": "2012-10-17" }, - "PolicyName": "TestUserDefaultPolicyFC1191A6", + "PolicyName": "MyUserDefaultPolicy7B897426", "Users": [ { - "Ref": "TestUser6A619381" + "Ref": "MyUserDC45028B" } ] } diff --git a/packages/@aws-cdk/cdk-assets/test/integ.assets.directory.lit.ts b/packages/@aws-cdk/cdk-assets/test/integ.assets.directory.lit.ts index 1e6b241233979..ab3b7b642af50 100644 --- a/packages/@aws-cdk/cdk-assets/test/integ.assets.directory.lit.ts +++ b/packages/@aws-cdk/cdk-assets/test/integ.assets.directory.lit.ts @@ -1,4 +1,5 @@ import cdk = require('@aws-cdk/core'); +import iam = require('@aws-cdk/iam'); import path = require('path'); import assets = require('../lib'); @@ -7,10 +8,13 @@ class TestStack extends cdk.Stack { super(parent, name, props); /// !show - new assets.ZipDirectoryAsset(this, 'SampleAsset', { + const asset = new assets.ZipDirectoryAsset(this, 'SampleAsset', { path: path.join(__dirname, 'sample-asset-directory') }); /// !hide + + const user = new iam.User(this, 'MyUser'); + asset.grantRead(user); } } diff --git a/packages/@aws-cdk/cdk-assets/test/integ.assets.file.lit.expected.json b/packages/@aws-cdk/cdk-assets/test/integ.assets.file.lit.expected.json index 898e8288c1a16..99d5cee84363e 100644 --- a/packages/@aws-cdk/cdk-assets/test/integ.assets.file.lit.expected.json +++ b/packages/@aws-cdk/cdk-assets/test/integ.assets.file.lit.expected.json @@ -10,10 +10,10 @@ } }, "Resources": { - "TestUser6A619381": { + "MyUserDC45028B": { "Type": "AWS::IAM::User" }, - "TestUserDefaultPolicyFC1191A6": { + "MyUserDefaultPolicy7B897426": { "Type": "AWS::IAM::Policy", "Properties": { "PolicyDocument": { @@ -86,10 +86,10 @@ ], "Version": "2012-10-17" }, - "PolicyName": "TestUserDefaultPolicyFC1191A6", + "PolicyName": "MyUserDefaultPolicy7B897426", "Users": [ { - "Ref": "TestUser6A619381" + "Ref": "MyUserDC45028B" } ] } diff --git a/packages/@aws-cdk/cdk-assets/test/integ.assets.file.lit.ts b/packages/@aws-cdk/cdk-assets/test/integ.assets.file.lit.ts index 9a577c9b560c7..16b95cb108b83 100644 --- a/packages/@aws-cdk/cdk-assets/test/integ.assets.file.lit.ts +++ b/packages/@aws-cdk/cdk-assets/test/integ.assets.file.lit.ts @@ -1,4 +1,5 @@ import cdk = require('@aws-cdk/core'); +import iam = require('@aws-cdk/iam'); import path = require('path'); import assets = require('../lib'); @@ -7,10 +8,13 @@ class TestStack extends cdk.Stack { super(parent, name, props); /// !show - new assets.FileAsset(this, 'SampleAsset', { + const asset = new assets.FileAsset(this, 'SampleAsset', { path: path.join(__dirname, 'file-asset.txt') }); /// !hide + + const user = new iam.User(this, 'MyUser'); + asset.grantRead(user); } } diff --git a/packages/@aws-cdk/cdk-assets/test/integ.assets.refs.lit.expected.json b/packages/@aws-cdk/cdk-assets/test/integ.assets.refs.lit.expected.json index 9d10212235190..659b218855cc3 100644 --- a/packages/@aws-cdk/cdk-assets/test/integ.assets.refs.lit.expected.json +++ b/packages/@aws-cdk/cdk-assets/test/integ.assets.refs.lit.expected.json @@ -57,10 +57,10 @@ } }, "Resources": { - "TestUser6A619381": { + "MyUserDC45028B": { "Type": "AWS::IAM::User" }, - "TestUserDefaultPolicyFC1191A6": { + "MyUserDefaultPolicy7B897426": { "Type": "AWS::IAM::Policy", "Properties": { "PolicyDocument": { @@ -133,10 +133,10 @@ ], "Version": "2012-10-17" }, - "PolicyName": "TestUserDefaultPolicyFC1191A6", + "PolicyName": "MyUserDefaultPolicy7B897426", "Users": [ { - "Ref": "TestUser6A619381" + "Ref": "MyUserDC45028B" } ] } diff --git a/packages/@aws-cdk/cdk-assets/test/integ.assets.refs.lit.ts b/packages/@aws-cdk/cdk-assets/test/integ.assets.refs.lit.ts index 0b664acaf73ed..769ececac6fe5 100644 --- a/packages/@aws-cdk/cdk-assets/test/integ.assets.refs.lit.ts +++ b/packages/@aws-cdk/cdk-assets/test/integ.assets.refs.lit.ts @@ -1,4 +1,5 @@ import cdk = require('@aws-cdk/core'); +import iam = require('@aws-cdk/iam'); import path = require('path'); import assets = require('../lib'); @@ -15,6 +16,9 @@ class TestStack extends cdk.Stack { new cdk.Output(this, 'S3ObjectKey', { value: asset.s3ObjectKey }); new cdk.Output(this, 'S3URL', { value: asset.s3Url }); /// !hide + + // we need at least one resource + asset.grantRead(new iam.User(this, 'MyUser')); } } From 5502436def5a3390ca2faeaa49de014de11ee4ea Mon Sep 17 00:00:00 2001 From: Elad Ben-Israel Date: Wed, 18 Jul 2018 16:50:24 +0300 Subject: [PATCH 5/6] s3: bucketUrl and urlForObject(key) (#370) The `bucketUrl` returns the URL of the bucket and `urlForObject(key)` returns the URL of an object within the bucket. Furthermore: `iam.IIdentityResource` was soft-renamed to `iam.IPrincipal` (IIdentityResource is still supported). --- .../core/lib/cloudformation/pseudo.ts | 6 + packages/@aws-cdk/iam/lib/policy.ts | 11 +- packages/@aws-cdk/s3/README.md | 13 +++ packages/@aws-cdk/s3/lib/bucket.ts | 90 +++++++++++---- .../s3/test/integ.bucket.expected.json | 4 +- .../test/integ.bucket.url.lit.expected.json | 61 +++++++++++ .../@aws-cdk/s3/test/integ.bucket.url.lit.ts | 19 ++++ packages/@aws-cdk/s3/test/test.bucket.ts | 103 +++++++++++++++++- 8 files changed, 281 insertions(+), 26 deletions(-) create mode 100644 packages/@aws-cdk/s3/test/integ.bucket.url.lit.expected.json create mode 100644 packages/@aws-cdk/s3/test/integ.bucket.url.lit.ts diff --git a/packages/@aws-cdk/core/lib/cloudformation/pseudo.ts b/packages/@aws-cdk/core/lib/cloudformation/pseudo.ts index 29dcf57c0e7d6..59b6edd4c90ee 100644 --- a/packages/@aws-cdk/core/lib/cloudformation/pseudo.ts +++ b/packages/@aws-cdk/core/lib/cloudformation/pseudo.ts @@ -18,6 +18,12 @@ export class AwsDomainSuffix extends PseudoParameter { } } +export class AwsURLSuffix extends PseudoParameter { + constructor() { + super('AWS::URLSuffix'); + } +} + export class AwsNotificationARNs extends PseudoParameter { constructor() { super('AWS::NotificationARNs'); diff --git a/packages/@aws-cdk/iam/lib/policy.ts b/packages/@aws-cdk/iam/lib/policy.ts index 205cef2d56d51..3e98006376600 100644 --- a/packages/@aws-cdk/iam/lib/policy.ts +++ b/packages/@aws-cdk/iam/lib/policy.ts @@ -5,7 +5,10 @@ import { Role } from './role'; import { User } from './user'; import { generatePolicyName, undefinedIfEmpty } from './util'; -export interface IIdentityResource { +/** + * A construct that represents an IAM principal, such as a user, group or role. + */ +export interface IPrincipal { /** * The IAM principal of this identity (i.e. AWS principal, service principal, etc). */ @@ -31,6 +34,12 @@ export interface IIdentityResource { attachManagedPolicy(arn: any): void; } +/** + * @deprecated Use IPrincipal + */ +// tslint:disable-next-line:no-empty-interface +export interface IIdentityResource extends IPrincipal { } + export interface PolicyProps { /** * The name of the policy. If you specify multiple policies for an entity, diff --git a/packages/@aws-cdk/s3/README.md b/packages/@aws-cdk/s3/README.md index 66017fa1b0afe..2eaa3503204e6 100644 --- a/packages/@aws-cdk/s3/README.md +++ b/packages/@aws-cdk/s3/README.md @@ -6,6 +6,19 @@ Define an unencrypted S3 bucket. new Bucket(this, 'MyFirstBucket'); ``` +`Bucket` constructs expose the following deploy-time attributes: + + * `bucketArn` - the ARN of the bucket (i.e. `arn:aws:s3:::bucket_name`) + * `bucketName` - the name of the bucket (i.e. `bucket_name`) + * `bucketUrl` - the URL of the bucket (i.e. + `https://s3.us-west-1.amazonaws.com/onlybucket`) + * `arnForObjects(...pattern)` - the ARN of an object or objects within the + bucket (i.e. + `arn:aws:s3:::my_corporate_bucket/exampleobject.png` or + `arn:aws:s3:::my_corporate_bucket/Development/*`) + * `urlForObject(key)` - the URL of an object within the bucket (i.e. + `https://s3.cn-north-1.amazonaws.com.cn/china-bucket/mykey`) + ### Encryption Define a KMS-encrypted bucket: diff --git a/packages/@aws-cdk/s3/lib/bucket.ts b/packages/@aws-cdk/s3/lib/bucket.ts index 848c0ad39ae8b..cb9a03f24870a 100644 --- a/packages/@aws-cdk/s3/lib/bucket.ts +++ b/packages/@aws-cdk/s3/lib/bucket.ts @@ -1,6 +1,6 @@ -import { applyRemovalPolicy, Arn, Construct, FnConcat, Output, PolicyStatement, RemovalPolicy, Token } from '@aws-cdk/core'; -import { IIdentityResource } from '@aws-cdk/iam'; -import * as kms from '@aws-cdk/kms'; +import cdk = require('@aws-cdk/core'); +import iam = require('@aws-cdk/iam'); +import kms = require('@aws-cdk/kms'); import { BucketPolicy } from './bucket-policy'; import * as perms from './perms'; import { LifecycleRule } from './rule'; @@ -45,7 +45,7 @@ export interface BucketRefProps { * BucketRef.import(this, 'MyImportedBucket', ref); * */ -export abstract class BucketRef extends Construct { +export abstract class BucketRef extends cdk.Construct { /** * Creates a Bucket construct that represents an external bucket. * @@ -54,7 +54,7 @@ export abstract class BucketRef extends Construct { * @param ref A BucketRefProps object. Can be obtained from a call to * `bucket.export()`. */ - public static import(parent: Construct, name: string, props: BucketRefProps): BucketRef { + public static import(parent: cdk.Construct, name: string, props: BucketRefProps): BucketRef { return new ImportedBucketRef(parent, name, props); } @@ -92,8 +92,8 @@ export abstract class BucketRef extends Construct { */ public export(): BucketRefProps { return { - bucketArn: new Output(this, 'BucketArn', { value: this.bucketArn }).makeImportValue(), - bucketName: new Output(this, 'BucketName', { value: this.bucketName }).makeImportValue(), + bucketArn: new cdk.Output(this, 'BucketArn', { value: this.bucketArn }).makeImportValue(), + bucketName: new cdk.Output(this, 'BucketName', { value: this.bucketName }).makeImportValue(), }; } @@ -103,7 +103,7 @@ export abstract class BucketRef extends Construct { * contents. Use `bucketArn` and `arnForObjects(keys)` to obtain ARNs for * this bucket or objects. */ - public addToResourcePolicy(permission: PolicyStatement) { + public addToResourcePolicy(permission: cdk.PolicyStatement) { if (!this.policy && this.autoCreatePolicy) { this.policy = new BucketPolicy(this, 'Policy', { bucket: this }); } @@ -113,6 +113,38 @@ export abstract class BucketRef extends Construct { } } + /** + * The https:// URL of this bucket. + * @example https://s3.us-west-1.amazonaws.com/onlybucket + * Similar to calling `urlForObject` with no object key. + */ + public get bucketUrl() { + return this.urlForObject(); + } + + /** + * The https URL of an S3 object. For example: + * @example https://s3.us-west-1.amazonaws.com/onlybucket + * @example https://s3.us-west-1.amazonaws.com/bucket/key + * @example https://s3.cn-north-1.amazonaws.com.cn/china-bucket/mykey + * @param key The S3 key of the object. If not specified, the URL of the + * bucket is returned. + * @returns an ObjectS3Url token + */ + public urlForObject(key?: any): S3Url { + const components = [ 'https://', 's3.', new cdk.AwsRegion(), '.', new cdk.AwsURLSuffix(), '/', this.bucketName ]; + if (key) { + // trim prepending '/' + if (typeof key === 'string' && key.startsWith('/')) { + key = key.substr(1); + } + components.push('/'); + components.push(key); + } + + return new cdk.FnConcat(...components); + } + /** * Returns an ARN that represents all objects within the bucket that match * the key pattern specified. To represent all keys, specify ``"*"``. @@ -122,8 +154,8 @@ export abstract class BucketRef extends Construct { * arnForObjects('home/', team, '/', user, '/*') * */ - public arnForObjects(...keyPattern: any[]): Arn { - return new FnConcat(this.bucketArn, '/', ...keyPattern); + public arnForObjects(...keyPattern: any[]): cdk.Arn { + return new cdk.FnConcat(this.bucketArn, '/', ...keyPattern); } /** @@ -133,7 +165,7 @@ export abstract class BucketRef extends Construct { * If an encryption key is used, permission to ues the key to decrypt the * contents of the bucket will also be granted. */ - public grantRead(identity?: IIdentityResource, objectsKeyPattern = '*') { + public grantRead(identity?: iam.IPrincipal, objectsKeyPattern: any = '*') { if (!identity) { return; } @@ -147,7 +179,7 @@ export abstract class BucketRef extends Construct { * If an encryption key is used, permission to use the key for * encrypt/decrypt will also be granted. */ - public grantReadWrite(identity?: IIdentityResource, objectsKeyPattern = '*') { + public grantReadWrite(identity?: iam.IPrincipal, objectsKeyPattern: any = '*') { if (!identity) { return; } @@ -156,24 +188,24 @@ export abstract class BucketRef extends Construct { this.grant(identity, objectsKeyPattern, bucketActions, keyActions); } - private grant(identity: IIdentityResource, objectsKeyPattern: string, bucketActions: string[], keyActions: string[]) { + private grant(identity: iam.IPrincipal, objectsKeyPattern: any, bucketActions: string[], keyActions: string[]) { const resources = [ this.bucketArn, this.arnForObjects(objectsKeyPattern) ]; - identity.addToPolicy(new PolicyStatement() + identity.addToPolicy(new cdk.PolicyStatement() .addResources(...resources) .addActions(...bucketActions)); // grant key permissions if there's an associated key. if (this.encryptionKey) { // KMS permissions need to be granted both directions - identity.addToPolicy(new PolicyStatement() + identity.addToPolicy(new cdk.PolicyStatement() .addResource(this.encryptionKey.keyArn) .addActions(...keyActions)); - this.encryptionKey.addToResourcePolicy(new PolicyStatement() + this.encryptionKey.addToResourcePolicy(new cdk.PolicyStatement() .addResource('*') .addPrincipal(identity.principal) .addActions(...keyActions)); @@ -216,7 +248,7 @@ export interface BucketProps { * * @default By default, the bucket will be destroyed if it is removed from the stack. */ - removalPolicy?: RemovalPolicy; + removalPolicy?: cdk.RemovalPolicy; /** * The bucket policy associated with this bucket. @@ -258,7 +290,7 @@ export class Bucket extends BucketRef { private readonly lifecycleRules: LifecycleRule[] = []; private readonly versioned?: boolean; - constructor(parent: Construct, name: string, props: BucketProps = {}) { + constructor(parent: cdk.Construct, name: string, props: BucketProps = {}) { super(parent, name); validateBucketName(props && props.bucketName); @@ -269,10 +301,10 @@ export class Bucket extends BucketRef { bucketName: props && props.bucketName, bucketEncryption, versioningConfiguration: props.versioned ? { status: 'Enabled' } : undefined, - lifecycleConfiguration: new Token(() => this.parseLifecycleConfiguration()), + lifecycleConfiguration: new cdk.Token(() => this.parseLifecycleConfiguration()), }); - applyRemovalPolicy(resource, props.removalPolicy); + cdk.applyRemovalPolicy(resource, props.removalPolicy); this.versioned = props.versioned; this.policy = props.policy; @@ -435,7 +467,21 @@ export enum BucketEncryption { /** * The name of the bucket. */ -export class BucketName extends Token { +export class BucketName extends cdk.Token { + +} + +/** + * A key to an S3 object. + */ +export class ObjectKey extends cdk.Token { + +} + +/** + * The web URL (https://s3.us-west-1.amazonaws.com/bucket/key) of an S3 object. + */ +export class S3Url extends cdk.Token { } @@ -447,7 +493,7 @@ class ImportedBucketRef extends BucketRef { protected policy?: BucketPolicy; protected autoCreatePolicy: boolean; - constructor(parent: Construct, name: string, props: BucketRefProps) { + constructor(parent: cdk.Construct, name: string, props: BucketRefProps) { super(parent, name); this.bucketArn = parseBucketArn(props); diff --git a/packages/@aws-cdk/s3/test/integ.bucket.expected.json b/packages/@aws-cdk/s3/test/integ.bucket.expected.json index 4d166081eef83..ce11fc9d144e1 100644 --- a/packages/@aws-cdk/s3/test/integ.bucket.expected.json +++ b/packages/@aws-cdk/s3/test/integ.bucket.expected.json @@ -3,7 +3,6 @@ "MyBucketKeyC17130CF": { "Type": "AWS::KMS::Key", "Properties": { - "Description": "Created by aws-cdk-s3/MyBucket", "KeyPolicy": { "Statement": [ { @@ -63,7 +62,8 @@ } ], "Version": "2012-10-17" - } + }, + "Description": "Created by aws-cdk-s3/MyBucket" }, "DeletionPolicy": "Retain" }, diff --git a/packages/@aws-cdk/s3/test/integ.bucket.url.lit.expected.json b/packages/@aws-cdk/s3/test/integ.bucket.url.lit.expected.json new file mode 100644 index 0000000000000..5e673cd26b405 --- /dev/null +++ b/packages/@aws-cdk/s3/test/integ.bucket.url.lit.expected.json @@ -0,0 +1,61 @@ +{ + "Resources": { + "MyBucketF68F3FF0": { + "Type": "AWS::S3::Bucket" + } + }, + "Outputs": { + "BucketURL": { + "Value": { + "Fn::Join": [ + "", + [ + "https://", + "s3.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Ref": "MyBucketF68F3FF0" + } + ] + ] + }, + "Export": { + "Name": "aws-cdk-s3-urls:BucketURL" + } + }, + "ObjectURL": { + "Value": { + "Fn::Join": [ + "", + [ + "https://", + "s3.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Ref": "MyBucketF68F3FF0" + }, + "/", + "myfolder/myfile.txt" + ] + ] + }, + "Export": { + "Name": "aws-cdk-s3-urls:ObjectURL" + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/s3/test/integ.bucket.url.lit.ts b/packages/@aws-cdk/s3/test/integ.bucket.url.lit.ts new file mode 100644 index 0000000000000..c4438650aa6a8 --- /dev/null +++ b/packages/@aws-cdk/s3/test/integ.bucket.url.lit.ts @@ -0,0 +1,19 @@ +import cdk = require('@aws-cdk/core'); +import s3 = require('../lib'); + +class TestStack extends cdk.Stack { + constructor(parent: cdk.App, id: string) { + super(parent, id); + + /// !show + const bucket = new s3.Bucket(this, 'MyBucket'); + + new cdk.Output(this, 'BucketURL', { value: bucket.bucketUrl }); + new cdk.Output(this, 'ObjectURL', { value: bucket.urlForObject('myfolder/myfile.txt') }); + /// !hide + } +} + +const app = new cdk.App(process.argv); +new TestStack(app, 'aws-cdk-s3-urls'); +process.stdout.write(app.run()); diff --git a/packages/@aws-cdk/s3/test/test.bucket.ts b/packages/@aws-cdk/s3/test/test.bucket.ts index 1671e2af2dff2..989ba7167811f 100644 --- a/packages/@aws-cdk/s3/test/test.bucket.ts +++ b/packages/@aws-cdk/s3/test/test.bucket.ts @@ -1,9 +1,10 @@ import { expect } from '@aws-cdk/assert'; -import { PolicyStatement, RemovalPolicy, resolve, Stack } from '@aws-cdk/core'; +import { Output, PolicyStatement, RemovalPolicy, resolve, Stack } from '@aws-cdk/core'; import { Group, User } from '@aws-cdk/iam'; import { EncryptionKey } from '@aws-cdk/kms'; import { Test } from 'nodeunit'; import * as s3 from '../lib'; +import { Bucket } from '../lib'; // to make it easy to copy & paste from output: // tslint:disable:object-literal-key-quotes @@ -915,4 +916,104 @@ export = { test.done(); }, + + 'urlForObject returns a token with the S3 URL of the token'(test: Test) { + const stack = new Stack(); + const bucket = new Bucket(stack, 'MyBucket'); + + new Output(stack, 'BucketURL', { value: bucket.bucketUrl }); + new Output(stack, 'MyFileURL', { value: bucket.urlForObject('my/file.txt') }); + new Output(stack, 'YourFileURL', { value: bucket.urlForObject('/your/file.txt') }); // "/" is optional + + expect(stack).toMatch({ + "Resources": { + "MyBucketF68F3FF0": { + "Type": "AWS::S3::Bucket" + } + }, + "Outputs": { + "BucketURL": { + "Value": { + "Fn::Join": [ + "", + [ + "https://", + "s3.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Ref": "MyBucketF68F3FF0" + } + ] + ] + }, + "Export": { + "Name": "BucketURL" + } + }, + "MyFileURL": { + "Value": { + "Fn::Join": [ + "", + [ + "https://", + "s3.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Ref": "MyBucketF68F3FF0" + }, + "/", + "my/file.txt" + ] + ] + }, + "Export": { + "Name": "MyFileURL" + } + }, + "YourFileURL": { + "Value": { + "Fn::Join": [ + "", + [ + "https://", + "s3.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Ref": "MyBucketF68F3FF0" + }, + "/", + "your/file.txt" + ] + ] + }, + "Export": { + "Name": "YourFileURL" + } + } + } + }); + + test.done(); + } }; From 07632827041dcac899062352de6cf006c1844449 Mon Sep 17 00:00:00 2001 From: Elad Ben-Israel Date: Wed, 18 Jul 2018 17:32:27 +0300 Subject: [PATCH 6/6] CR fixes --- packages/@aws-cdk/cdk-assets/lib/asset.ts | 39 ++++++++++++------- .../@aws-cdk/cdk-assets/test/test.asset.ts | 7 ++++ packages/aws-cdk/lib/api/toolkit-info.ts | 2 +- packages/aws-cdk/lib/assets.ts | 30 +++----------- 4 files changed, 37 insertions(+), 41 deletions(-) diff --git a/packages/@aws-cdk/cdk-assets/lib/asset.ts b/packages/@aws-cdk/cdk-assets/lib/asset.ts index f39d8a1841abc..260953680f867 100644 --- a/packages/@aws-cdk/cdk-assets/lib/asset.ts +++ b/packages/@aws-cdk/cdk-assets/lib/asset.ts @@ -73,21 +73,7 @@ export class Asset extends cdk.Construct { // resolve full path this.assetPath = path.resolve(props.path); - if (!fs.existsSync(this.assetPath)) { - throw new Error(`Cannot find asset at ${props.path}`); - } - - if (props.packaging === AssetPackaging.ZipDirectory) { - if (!fs.statSync(this.assetPath).isDirectory()) { - throw new Error(`${this.assetPath} is expected to be a directory when asset packaging is 'zip'`); - } - } else if (props.packaging === AssetPackaging.File) { - if (!fs.statSync(this.assetPath).isFile()) { - throw new Error(`${this.assetPath} is expected to be a regular file when asset packaging is 'file'`); - } - } else { - throw new Error(`Unsupported asset packaging format: ${props.packaging}`); - } + validateAssetOnDisk(this.assetPath, props.packaging); // add parameters for s3 bucket and s3 key. those will be set by // the toolkit or by CI/CD when the stack is deployed and will include @@ -185,3 +171,26 @@ export class ZipDirectoryAsset extends Asset { super(parent, id, { packaging: AssetPackaging.ZipDirectory, ...props }); } } + +function validateAssetOnDisk(assetPath: string, packaging: AssetPackaging) { + if (!fs.existsSync(assetPath)) { + throw new Error(`Cannot find asset at ${assetPath}`); + } + + switch (packaging) { + case AssetPackaging.ZipDirectory: + if (!fs.statSync(assetPath).isDirectory()) { + throw new Error(`${assetPath} is expected to be a directory when asset packaging is 'zip'`); + } + break; + + case AssetPackaging.File: + if (!fs.statSync(assetPath).isFile()) { + throw new Error(`${assetPath} is expected to be a regular file when asset packaging is 'file'`); + } + break; + + default: + throw new Error(`Unsupported asset packaging format: ${packaging}`); + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/cdk-assets/test/test.asset.ts b/packages/@aws-cdk/cdk-assets/test/test.asset.ts index 49346c0640d6e..990abc66800c1 100644 --- a/packages/@aws-cdk/cdk-assets/test/test.asset.ts +++ b/packages/@aws-cdk/cdk-assets/test/test.asset.ts @@ -265,6 +265,13 @@ export = { } }); + test.done(); + }, + 'fails if directory not found'(test: Test) { + const stack = new Stack(); + test.throws(() => new ZipDirectoryAsset(stack, 'MyDirectory', { + path: '/path/not/found/' + Math.random() * 999999 + })); test.done(); } }; diff --git a/packages/aws-cdk/lib/api/toolkit-info.ts b/packages/aws-cdk/lib/api/toolkit-info.ts index dacde51269256..57bbdbe55e6d4 100644 --- a/packages/aws-cdk/lib/api/toolkit-info.ts +++ b/packages/aws-cdk/lib/api/toolkit-info.ts @@ -17,7 +17,7 @@ export class ToolkitInfo { }) { } public get bucketUrl() { - return `https://${this.props.bucketName}.s3.amazonaws.com`; + return `https://${this.props.bucketEndpoint}`; } public get bucketName() { diff --git a/packages/aws-cdk/lib/assets.ts b/packages/aws-cdk/lib/assets.ts index 2d3fb15afcc97..bd4407191b1e2 100644 --- a/packages/aws-cdk/lib/assets.ts +++ b/packages/aws-cdk/lib/assets.ts @@ -41,53 +41,33 @@ async function prepareAsset(asset: AssetMetadataEntry, toolkitInfo: ToolkitInfo) async function prepareZipAsset(asset: AssetMetadataEntry, toolkitInfo: ToolkitInfo): Promise { debug('Preparing zip asset from directory:', asset.path); - const staging = await fs.mkdtemp('/tmp/cdk-assets'); - try { const archiveFile = path.join(staging, 'archive.zip'); await zipDirectory(asset.path, archiveFile); - debug('zip archive:', archiveFile); - const data = await fs.readFile(archiveFile); - - const { key, changed } = await toolkitInfo.uploadIfChanged(data, { - s3KeyPrefix: 'assets/', - s3KeySuffix: '.zip', - contentType: 'application/zip', - }); - - const s3url = `s3://${toolkitInfo.bucketName}/${key}`; - if (changed) { - success(` πŸ‘œ Asset ${asset.path} (directory zip) uploaded to ${s3url}`); - } else { - success(` πŸ‘œ Asset ${asset.path} (directory zip) already exists in ${s3url}`); - } - - return [ - { ParameterKey: asset.s3BucketParameter, ParameterValue: toolkitInfo.bucketName }, - { ParameterKey: asset.s3KeyParameter, ParameterValue: key } - ]; + return await prepareFileAsset(asset, toolkitInfo, 'application/zip'); } finally { await fs.remove(staging); } } -async function prepareFileAsset(asset: AssetMetadataEntry, toolkitInfo: ToolkitInfo): Promise { +async function prepareFileAsset(asset: AssetMetadataEntry, toolkitInfo: ToolkitInfo, contentType?: string): Promise { debug('Preparing file asset:', asset.path); const data = await fs.readFile(asset.path); const { key, changed } = await toolkitInfo.uploadIfChanged(data, { s3KeyPrefix: 'assets/', - s3KeySuffix: path.extname(asset.path) + s3KeySuffix: path.extname(asset.path), + contentType }); const s3url = `s3://${toolkitInfo.bucketName}/${key}`; if (changed) { success(` πŸ‘œ Asset ${asset.path} uploaded to ${s3url}`); } else { - success(` πŸ‘œ Asset ${asset.path} already exists in ${s3url}`); + success(` πŸ‘œ Asset ${asset.path} is already up-to-date in ${s3url}`); } return [