diff --git a/.gitignore b/.gitignore index 901c2922699cb..6dda408d30db0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,24 +1,28 @@ -*.tsbuildinfo -.cdk.staging - -.vscode # VSCode extension +.vscode/ /.favorites.json + +# TypeScript incremental build states +*.tsbuildinfo + +# Local state files & OS specifics .DS_Store -node_modules +node_modules/ lerna-debug.log -dist -pack +dist/ +pack/ .BUILD_COMPLETED -.local-npm -.tools -coverage +.local-npm/ +.tools/ +coverage/ .nyc_output .LAST_BUILD *.sw[a-z] *~ -# we don't want tsconfig at the root +# We don't want tsconfig at the root /tsconfig.json + +# CDK Context & Staging files cdk.context.json -tsconfig.tsbuildinfo +.cdk.staging/ diff --git a/packages/@aws-cdk/assets-docker/lib/adopt-repository/handler.js b/packages/@aws-cdk/assets-docker/lib/adopt-repository/handler.js index abc883c0c108d..3d0cb009c8773 100644 --- a/packages/@aws-cdk/assets-docker/lib/adopt-repository/handler.js +++ b/packages/@aws-cdk/assets-docker/lib/adopt-repository/handler.js @@ -26,10 +26,14 @@ exports.handler = async function(event, context, _callback, respond) { } } - const repo = event.ResourceProperties.RepositoryName; + let repo = event.ResourceProperties.RepositoryName; if (!repo) { throw new Error('Missing required property "RepositoryName"'); } + const isRepoUri = repo.match(/^(\d+\.dkr\.ecr\.[^.]+\.[^/]+\/)(.+)$/i); + if (isRepoUri) { + repo = isRepoUri[2]; + } const adopter = await getAdopter(repo); if (event.RequestType === 'Delete') { diff --git a/packages/@aws-cdk/assets-docker/lib/adopted-repository.ts b/packages/@aws-cdk/assets-docker/lib/adopted-repository.ts index 6990201905ae7..397afb6828b0e 100644 --- a/packages/@aws-cdk/assets-docker/lib/adopted-repository.ts +++ b/packages/@aws-cdk/assets-docker/lib/adopted-repository.ts @@ -59,6 +59,10 @@ export class AdoptedRepository extends ecr.RepositoryBase { PolicyDocument: this.policyDocument } }); + if (fn.role) { + // Need to explicitly depend on the role's policies, so they are applied before we try to use them + adopter.node.addDependency(fn.role); + } // we use the Fn::GetAtt with the RepositoryName returned by the custom // resource in order to implicitly create a dependency between consumers diff --git a/packages/@aws-cdk/assets-docker/lib/image-asset.ts b/packages/@aws-cdk/assets-docker/lib/image-asset.ts index 0644af975dfde..3eccf231d8199 100644 --- a/packages/@aws-cdk/assets-docker/lib/image-asset.ts +++ b/packages/@aws-cdk/assets-docker/lib/image-asset.ts @@ -6,7 +6,7 @@ import fs = require('fs'); import path = require('path'); import { AdoptedRepository } from './adopted-repository'; -export interface DockerImageAssetProps { +export interface DockerImageAssetProps extends assets.CopyOptions { /** * The directory where the Dockerfile is stored */ @@ -36,7 +36,7 @@ export interface DockerImageAssetProps { * * The image will be created in build time and uploaded to an ECR repository. */ -export class DockerImageAsset extends cdk.Construct { +export class DockerImageAsset extends cdk.Construct implements assets.IAsset { /** * The full URI of the image (including a tag). Use this reference to pull * the asset. @@ -48,6 +48,9 @@ export class DockerImageAsset extends cdk.Construct { */ public repository: ecr.IRepository; + public readonly sourceHash: string; + public readonly artifactHash: string; + /** * Directory where the source files are stored */ @@ -66,10 +69,12 @@ export class DockerImageAsset extends cdk.Construct { } const staging = new assets.Staging(this, 'Staging', { + ...props, sourcePath: dir }); this.directory = staging.stagedPath; + this.sourceHash = staging.sourceHash; const imageNameParameter = new cdk.CfnParameter(this, 'ImageName', { type: 'String', @@ -77,9 +82,10 @@ export class DockerImageAsset extends cdk.Construct { }); const asset: cxapi.ContainerImageAssetMetadataEntry = { + id: this.node.uniqueId, packaging: 'container-image', path: this.directory, - id: this.node.uniqueId, + sourceHash: this.sourceHash, imageNameParameter: imageNameParameter.logicalId, repositoryName: props.repositoryName, buildArgs: props.buildArgs @@ -87,10 +93,11 @@ export class DockerImageAsset extends cdk.Construct { this.node.addMetadata(cxapi.ASSET_METADATA, asset); - // parse repository name and tag from the parameter (:) - const components = cdk.Fn.split(':', imageNameParameter.stringValue); + // Parse repository name and tag from the parameter (@sha256:) + // Example: cdk/cdkexampleimageb2d7f504@sha256:72c4f956379a43b5623d529ddd969f6826dde944d6221f445ff3e7add9875500 + const components = cdk.Fn.split('@sha256:', imageNameParameter.stringValue); const repositoryName = cdk.Fn.select(0, components).toString(); - const imageTag = cdk.Fn.select(1, components).toString(); + const imageSha = cdk.Fn.select(1, components).toString(); // Require that repository adoption happens first, so we route the // input ARN into the Custom Resource and then get the URI which we use to @@ -99,6 +106,7 @@ export class DockerImageAsset extends cdk.Construct { // If adoption fails (because the repository might be twice-adopted), we // haven't already started using the image. this.repository = new AdoptedRepository(this, 'AdoptRepository', { repositoryName }); - this.imageUri = this.repository.repositoryUriForTag(imageTag); + this.imageUri = `${this.repository.repositoryUri}@sha256:${imageSha}`; + this.artifactHash = imageSha; } } diff --git a/packages/@aws-cdk/assets-docker/test/integ.assets-docker.expected.json b/packages/@aws-cdk/assets-docker/test/integ.assets-docker.expected.json new file mode 100644 index 0000000000000..54d3f8bb9f8c4 --- /dev/null +++ b/packages/@aws-cdk/assets-docker/test/integ.assets-docker.expected.json @@ -0,0 +1,326 @@ +{ + "Parameters": { + "DockerImageImageName266E5998": { + "Type": "String", + "Description": "ECR repository name and tag asset \"integ-assets-docker/DockerImage\"" + }, + "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cCodeS3Bucket92AB06B6": { + "Type": "String", + "Description": "S3 bucket for asset \"integ-assets-docker/AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62c/Code\"" + }, + "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cCodeS3VersionKey393B7276": { + "Type": "String", + "Description": "S3 key for asset version \"integ-assets-docker/AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62c/Code\"" + }, + "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cCodeArtifactHash8BCBAA49": { + "Type": "String", + "Description": "Artifact hash for asset \"integ-assets-docker/AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62c/Code\"" + } + }, + "Resources": { + "DockerImageAdoptRepositoryA86481BC": { + "Type": "Custom::ECRAdoptedRepository", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62c52BE89E9", + "Arn" + ] + }, + "RepositoryName": { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "@sha256:", + { + "Ref": "DockerImageImageName266E5998" + } + ] + } + ] + } + }, + "DependsOn": [ + "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cServiceRoleDefaultPolicy6BC8737C", + "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cServiceRoleD788AA17" + ] + }, + "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cServiceRoleD788AA17": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": { + "Fn::Join": [ + "", + [ + "lambda.", + { + "Ref": "AWS::URLSuffix" + } + ] + ] + } + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + } + }, + "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cServiceRoleDefaultPolicy6BC8737C": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "ecr:GetRepositoryPolicy", + "ecr:SetRepositoryPolicy", + "ecr:DeleteRepository", + "ecr:ListImages", + "ecr:BatchDeleteImage" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ecr:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":repository/", + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "@sha256:", + { + "Ref": "DockerImageImageName266E5998" + } + ] + } + ] + } + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cServiceRoleDefaultPolicy6BC8737C", + "Roles": [ + { + "Ref": "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cServiceRoleD788AA17" + } + ] + } + }, + "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62c52BE89E9": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Ref": "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cCodeS3Bucket92AB06B6" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cCodeS3VersionKey393B7276" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cCodeS3VersionKey393B7276" + } + ] + } + ] + } + ] + ] + } + }, + "Handler": "handler.handler", + "Role": { + "Fn::GetAtt": [ + "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cServiceRoleD788AA17", + "Arn" + ] + }, + "Runtime": "nodejs8.10", + "Timeout": 300 + }, + "DependsOn": [ + "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cServiceRoleDefaultPolicy6BC8737C", + "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cServiceRoleD788AA17" + ] + } + }, + "Outputs": { + "ArtifactHash": { + "Value": { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "@sha256:", + { + "Ref": "DockerImageImageName266E5998" + } + ] + } + ] + } + }, + "ImageUri": { + "Value": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 4, + { + "Fn::Split": [ + ":", + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ecr:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":repository/", + { + "Fn::GetAtt": [ + "DockerImageAdoptRepositoryA86481BC", + "RepositoryName" + ] + } + ] + ] + } + ] + } + ] + }, + ".dkr.ecr.", + { + "Fn::Select": [ + 3, + { + "Fn::Split": [ + ":", + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ecr:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":repository/", + { + "Fn::GetAtt": [ + "DockerImageAdoptRepositoryA86481BC", + "RepositoryName" + ] + } + ] + ] + } + ] + } + ] + }, + ".amazonaws.com/", + { + "Fn::GetAtt": [ + "DockerImageAdoptRepositoryA86481BC", + "RepositoryName" + ] + }, + "@sha256:", + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "@sha256:", + { + "Ref": "DockerImageImageName266E5998" + } + ] + } + ] + } + ] + ] + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/assets-docker/test/integ.assets-docker.ts b/packages/@aws-cdk/assets-docker/test/integ.assets-docker.ts new file mode 100644 index 0000000000000..91fe6afbd384c --- /dev/null +++ b/packages/@aws-cdk/assets-docker/test/integ.assets-docker.ts @@ -0,0 +1,15 @@ +import cdk = require('@aws-cdk/cdk'); +import path = require('path'); +import assets = require('../lib'); + +const app = new cdk.App(); +const stack = new cdk.Stack(app, 'integ-assets-docker'); + +const asset = new assets.DockerImageAsset(stack, 'DockerImage', { + directory: path.join(__dirname, 'demo-image'), +}); + +new cdk.CfnOutput(stack, 'ArtifactHash', { value: asset.artifactHash }); +new cdk.CfnOutput(stack, 'ImageUri', { value: asset.imageUri }); + +app.run(); diff --git a/packages/@aws-cdk/assets-docker/test/test.image-asset.ts b/packages/@aws-cdk/assets-docker/test/test.image-asset.ts index 6374e0381ee15..869b596bf1421 100644 --- a/packages/@aws-cdk/assets-docker/test/test.image-asset.ts +++ b/packages/@aws-cdk/assets-docker/test/test.image-asset.ts @@ -121,7 +121,7 @@ export = { // THEN expect(stack).to(haveResource('Custom::ECRAdoptedRepository', { "RepositoryName": { - "Fn::Select": [ 0, { "Fn::Split": [ ":", { "Ref": "ImageImageName5E684353" } ] } ] + "Fn::Select": [ 0, { "Fn::Split": [ "@sha256:", { "Ref": "ImageImageName5E684353" } ] } ] }, "PolicyDocument": { "Statement": [ @@ -182,8 +182,8 @@ export = { app.run(); - test.ok(fs.existsSync('.stage-me/96e3ffe92a19cbaa6c558942f7a60246/Dockerfile')); - test.ok(fs.existsSync('.stage-me/96e3ffe92a19cbaa6c558942f7a60246/index.py')); + test.ok(fs.existsSync('.stage-me/1a17a141505ac69144931fe263d130f4612251caa4bbbdaf68a44ed0f405439c/Dockerfile')); + test.ok(fs.existsSync('.stage-me/1a17a141505ac69144931fe263d130f4612251caa4bbbdaf68a44ed0f405439c/index.py')); test.done(); } }; diff --git a/packages/@aws-cdk/assets/lib/asset.ts b/packages/@aws-cdk/assets/lib/asset.ts index 26aa2ae915c59..8653bbe04de39 100644 --- a/packages/@aws-cdk/assets/lib/asset.ts +++ b/packages/@aws-cdk/assets/lib/asset.ts @@ -4,6 +4,7 @@ import cdk = require('@aws-cdk/cdk'); import cxapi = require('@aws-cdk/cx-api'); import fs = require('fs'); import path = require('path'); +import { CopyOptions } from './fs/copy-options'; import { Staging } from './staging'; /** @@ -22,7 +23,7 @@ export enum AssetPackaging { File = 'file', } -export interface AssetProps { +export interface AssetProps extends CopyOptions { /** * The disk location of the asset. */ @@ -42,11 +43,29 @@ export interface AssetProps { readonly readers?: iam.IGrantable[]; } +export interface IAsset extends cdk.IConstruct { + /** + * A hash of the source of this asset, which is available at construction time. As this is a plain + * string, it can be used in construct IDs in order to enforce creation of a new resource when + * the content hash has changed. + */ + readonly sourceHash: string; + + /** + * A hash of the bundle for of this asset, which is only available at deployment time. As this is + * a late-bound token, it may not be used in construct IDs, but can be passed as a resource + * property in order to force a change on a resource when an asset is effectively updated. This is + * more reliable than `sourceHash` in particular for assets which bundling phase involve external + * resources that can change over time (such as Docker image builds). + */ + readonly artifactHash: string; +} + /** * 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 { +export class Asset extends cdk.Construct implements IAsset { /** * Attribute that represents the name of the bucket this asset exists in. */ @@ -82,6 +101,9 @@ export class Asset extends cdk.Construct { */ public readonly isZipArchive: boolean; + public readonly sourceHash: string; + public readonly artifactHash: string; + /** * The S3 prefix where all different versions of this asset are stored */ @@ -92,8 +114,10 @@ export class Asset extends cdk.Construct { // stage the asset source (conditionally). const staging = new Staging(this, 'Stage', { - sourcePath: path.resolve(props.path) + ...props, + sourcePath: path.resolve(props.path), }); + this.sourceHash = staging.sourceHash; this.assetPath = staging.stagedPath; @@ -119,10 +143,16 @@ export class Asset extends cdk.Construct { description: `S3 key for asset version "${this.node.path}"` }); + const hashParam = new cdk.CfnParameter(this, 'ArtifactHash', { + description: `Artifact hash for asset "${this.node.path}"`, + type: 'String', + }); + this.s3BucketName = bucketParam.stringValue; this.s3Prefix = cdk.Fn.select(0, cdk.Fn.split(cxapi.ASSET_PREFIX_SEPARATOR, keyParam.stringValue)).toString(); const s3Filename = cdk.Fn.select(1, cdk.Fn.split(cxapi.ASSET_PREFIX_SEPARATOR, keyParam.stringValue)).toString(); this.s3ObjectKey = `${this.s3Prefix}${s3Filename}`; + this.artifactHash = hashParam.stringValue; this.bucket = s3.Bucket.fromBucketName(this, 'AssetBucket', this.s3BucketName); @@ -137,8 +167,11 @@ export class Asset extends cdk.Construct { path: this.assetPath, id: this.node.uniqueId, packaging: props.packaging, + sourceHash: this.sourceHash, + s3BucketParameter: bucketParam.logicalId, s3KeyParameter: keyParam.logicalId, + artifactHashParameter: hashParam.logicalId, }; this.node.addMetadata(cxapi.ASSET_METADATA, asset); diff --git a/packages/@aws-cdk/assets/lib/fs/copy-options.ts b/packages/@aws-cdk/assets/lib/fs/copy-options.ts new file mode 100644 index 0000000000000..ac8d8b5686f0d --- /dev/null +++ b/packages/@aws-cdk/assets/lib/fs/copy-options.ts @@ -0,0 +1,20 @@ +import { FollowMode } from './follow-mode'; + +/** + * Obtains applied when copying directories into the staging location. + */ +export interface CopyOptions { + /** + * A strategy for how to handle symlinks. + * + * @default Never + */ + readonly follow?: FollowMode; + + /** + * Glob patterns to exclude from the copy. + * + * @default nothing is excluded + */ + readonly exclude?: string[]; +} diff --git a/packages/@aws-cdk/assets/lib/fs/copy.ts b/packages/@aws-cdk/assets/lib/fs/copy.ts index 6ea1f2a6e5f8c..ea011bb3a22a6 100644 --- a/packages/@aws-cdk/assets/lib/fs/copy.ts +++ b/packages/@aws-cdk/assets/lib/fs/copy.ts @@ -1,19 +1,8 @@ import fs = require('fs'); -import minimatch = require('minimatch'); import path = require('path'); +import { CopyOptions } from './copy-options'; import { FollowMode } from './follow-mode'; - -export interface CopyOptions { - /** - * @default External only follows symlinks that are external to the source directory - */ - follow?: FollowMode; - - /** - * glob patterns to exclude from the copy. - */ - exclude?: string[]; -} +import { shouldExclude, shouldFollow } from './utils'; export function copyDirectory(srcDir: string, destDir: string, options: CopyOptions = { }, rootDir?: string) { const follow = options.follow !== undefined ? options.follow : FollowMode.External; @@ -29,7 +18,7 @@ export function copyDirectory(srcDir: string, destDir: string, options: CopyOpti for (const file of files) { const sourceFilePath = path.join(srcDir, file); - if (shouldExclude(path.relative(rootDir, sourceFilePath))) { + if (shouldExclude(exclude, path.relative(rootDir, sourceFilePath))) { continue; } @@ -45,10 +34,8 @@ export function copyDirectory(srcDir: string, destDir: string, options: CopyOpti // determine if this is an external link (i.e. the target's absolute path // is outside of the root directory). const targetPath = path.normalize(path.resolve(srcDir, target)); - const rootPath = path.normalize(rootDir); - const external = !targetPath.startsWith(rootPath); - if (follow === FollowMode.External && external) { + if (shouldFollow(follow, rootDir, targetPath)) { stat = fs.statSync(sourceFilePath); } else { fs.symlinkSync(target, destFilePath); @@ -67,23 +54,4 @@ export function copyDirectory(srcDir: string, destDir: string, options: CopyOpti stat = undefined; } } - - function shouldExclude(filePath: string): boolean { - let excludeOutput = false; - - for (const pattern of exclude) { - const negate = pattern.startsWith('!'); - const match = minimatch(filePath, pattern, { matchBase: true, flipNegate: true }); - - if (!negate && match) { - excludeOutput = true; - } - - if (negate && match) { - excludeOutput = false; - } - } - - return excludeOutput; - } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/assets/lib/fs/fingerprint.ts b/packages/@aws-cdk/assets/lib/fs/fingerprint.ts index 06cdb6a0ed2aa..7ff15fe38383c 100644 --- a/packages/@aws-cdk/assets/lib/fs/fingerprint.ts +++ b/packages/@aws-cdk/assets/lib/fs/fingerprint.ts @@ -1,29 +1,21 @@ import crypto = require('crypto'); import fs = require('fs'); import path = require('path'); +import { CopyOptions } from './copy-options'; import { FollowMode } from './follow-mode'; +import { shouldExclude, shouldFollow } from './utils'; const BUFFER_SIZE = 8 * 1024; +const CTRL_SOH = '\x01'; +const CTRL_SOT = '\x02'; +const CTRL_ETX = '\x03'; -export interface FingerprintOptions { +export interface FingerprintOptions extends CopyOptions { /** * Extra information to encode into the fingerprint (e.g. build instructions * and other inputs) */ extra?: string; - - /** - * List of exclude patterns (see `CopyOptions`) - * @default include all files - */ - exclude?: string[]; - - /** - * What to do when we encounter symlinks. - * @default External only follows symlinks that are external to the source - * directory - */ - follow?: FollowMode; } /** @@ -38,49 +30,64 @@ export interface FingerprintOptions { * @param options Fingerprinting options */ export function fingerprint(fileOrDirectory: string, options: FingerprintOptions = { }) { - const follow = options.follow !== undefined ? options.follow : FollowMode.External; - const hash = crypto.createHash('md5'); - addToHash(fileOrDirectory); - - hash.update(`==follow==${follow}==\n\n`); + const hash = crypto.createHash('sha256'); + _hashField(hash, 'options.extra', options.extra || ''); + const follow = options.follow || FollowMode.External; + _hashField(hash, 'options.follow', follow); - if (options.extra) { - hash.update(`==extra==${options.extra}==\n\n`); - } - - for (const ex of options.exclude || []) { - hash.update(`==exclude==${ex}==\n\n`); - } + const rootDirectory = fs.statSync(fileOrDirectory).isDirectory() + ? fileOrDirectory + : path.dirname(fileOrDirectory); + const exclude = options.exclude || []; + _processFileOrDirectory(fileOrDirectory); return hash.digest('hex'); - function addToHash(pathToAdd: string) { - hash.update('==\n'); - const relativePath = path.relative(fileOrDirectory, pathToAdd); - hash.update(relativePath + '\n'); - hash.update('~~~~~~~~~~~~~~~~~~\n'); - const stat = fs.statSync(pathToAdd); + function _processFileOrDirectory(symbolicPath: string, realPath = symbolicPath) { + if (shouldExclude(exclude, symbolicPath)) { + return; + } + + const stat = fs.lstatSync(realPath); + const relativePath = path.relative(fileOrDirectory, symbolicPath); if (stat.isSymbolicLink()) { - const target = fs.readlinkSync(pathToAdd); - hash.update(target); + const linkTarget = fs.readlinkSync(realPath); + const resolvedLinkTarget = path.resolve(path.dirname(realPath), linkTarget); + if (shouldFollow(follow, rootDirectory, resolvedLinkTarget)) { + _processFileOrDirectory(symbolicPath, resolvedLinkTarget); + } else { + _hashField(hash, `link:${relativePath}`, linkTarget); + } + } else if (stat.isFile()) { + _hashField(hash, `file:${relativePath}`, _contentFingerprint(realPath, stat)); } else if (stat.isDirectory()) { - for (const file of fs.readdirSync(pathToAdd)) { - addToHash(path.join(pathToAdd, file)); + for (const item of fs.readdirSync(realPath).sort()) { + _processFileOrDirectory(path.join(symbolicPath, item), path.join(realPath, item)); } } else { - const file = fs.openSync(pathToAdd, 'r'); - const buffer = Buffer.alloc(BUFFER_SIZE); + throw new Error(`Unable to hash ${symbolicPath}: it is neither a file nor a directory`); + } + } +} - try { - let bytesRead; - do { - bytesRead = fs.readSync(file, buffer, 0, BUFFER_SIZE, null); - hash.update(buffer.slice(0, bytesRead)); - } while (bytesRead === BUFFER_SIZE); - } finally { - fs.closeSync(file); - } +function _contentFingerprint(file: string, stat: fs.Stats): string { + const hash = crypto.createHash('sha256'); + const buffer = Buffer.alloc(BUFFER_SIZE); + // tslint:disable-next-line: no-bitwise + const fd = fs.openSync(file, fs.constants.O_DSYNC | fs.constants.O_RDONLY | fs.constants.O_SYNC); + try { + let read = 0; + // tslint:disable-next-line: no-conditional-assignment + while ((read = fs.readSync(fd, buffer, 0, BUFFER_SIZE, null)) !== 0) { + hash.update(buffer.slice(0, read)); } + } finally { + fs.closeSync(fd); } -} \ No newline at end of file + return `${stat.size}:${hash.digest('hex')}`; +} + +function _hashField(hash: crypto.Hash, header: string, value: string | Buffer | DataView) { + hash.update(CTRL_SOH).update(header).update(CTRL_SOT).update(value).update(CTRL_ETX); +} diff --git a/packages/@aws-cdk/assets/lib/fs/follow-mode.ts b/packages/@aws-cdk/assets/lib/fs/follow-mode.ts index 02ecebfaaa0a7..9334328982236 100644 --- a/packages/@aws-cdk/assets/lib/fs/follow-mode.ts +++ b/packages/@aws-cdk/assets/lib/fs/follow-mode.ts @@ -26,4 +26,4 @@ export enum FollowMode { * If the copy operation runs into an external symlink, it will fail. */ BlockExternal = 'internal-only', -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/assets/lib/fs/index.ts b/packages/@aws-cdk/assets/lib/fs/index.ts index 31b1f468bbdfc..a66267535075d 100644 --- a/packages/@aws-cdk/assets/lib/fs/index.ts +++ b/packages/@aws-cdk/assets/lib/fs/index.ts @@ -1,3 +1,4 @@ +export * from './copy'; +export * from './copy-options'; export * from './fingerprint'; export * from './follow-mode'; -export * from './copy'; \ No newline at end of file diff --git a/packages/@aws-cdk/assets/lib/fs/utils.ts b/packages/@aws-cdk/assets/lib/fs/utils.ts new file mode 100644 index 0000000000000..7f5ec315538c7 --- /dev/null +++ b/packages/@aws-cdk/assets/lib/fs/utils.ts @@ -0,0 +1,60 @@ +import fs = require('fs'); +import minimatch = require('minimatch'); +import path = require('path'); +import { FollowMode } from './follow-mode'; + +/** + * Determines whether a given file should be excluded or not based on given + * exclusion glob patterns. + * + * @param exclude exclusion patterns + * @param filePath file apth to be assessed against the pattern + * + * @returns `true` if the file should be excluded + */ +export function shouldExclude(exclude: string[], filePath: string): boolean { + let excludeOutput = false; + + for (const pattern of exclude) { + const negate = pattern.startsWith('!'); + const match = minimatch(filePath, pattern, { matchBase: true, flipNegate: true }); + + if (!negate && match) { + excludeOutput = true; + } + + if (negate && match) { + excludeOutput = false; + } + } + + return excludeOutput; +} + +/** + * Determines whether a symlink should be followed or not, based on a FollowMode. + * + * @param mode the FollowMode. + * @param sourceRoot the root of the source tree. + * @param realPath the real path of the target of the symlink. + * + * @returns true if the link should be followed. + */ +export function shouldFollow(mode: FollowMode, sourceRoot: string, realPath: string): boolean { + switch (mode) { + case FollowMode.Always: + return fs.existsSync(realPath); + case FollowMode.External: + return !_isInternal() && fs.existsSync(realPath); + case FollowMode.BlockExternal: + return _isInternal() && fs.existsSync(realPath); + case FollowMode.Never: + return false; + default: + throw new Error(`Unsupported FollowMode: ${mode}`); + } + + function _isInternal(): boolean { + return path.resolve(realPath).startsWith(path.resolve(sourceRoot)); + } +} diff --git a/packages/@aws-cdk/assets/lib/index.ts b/packages/@aws-cdk/assets/lib/index.ts index 24ddffa892f0e..e57823463b2aa 100644 --- a/packages/@aws-cdk/assets/lib/index.ts +++ b/packages/@aws-cdk/assets/lib/index.ts @@ -1,2 +1,4 @@ export * from './asset'; +export * from './fs/copy-options'; +export * from './fs/follow-mode'; export * from './staging'; diff --git a/packages/@aws-cdk/assets/lib/staging.ts b/packages/@aws-cdk/assets/lib/staging.ts index 9d55e957c95aa..05e465acb4227 100644 --- a/packages/@aws-cdk/assets/lib/staging.ts +++ b/packages/@aws-cdk/assets/lib/staging.ts @@ -2,9 +2,9 @@ import { Construct, Token } from '@aws-cdk/cdk'; import cxapi = require('@aws-cdk/cx-api'); import fs = require('fs'); import path = require('path'); -import { copyDirectory, fingerprint } from './fs'; +import { copyDirectory, CopyOptions, fingerprint } from './fs'; -export interface StagingProps { +export interface StagingProps extends CopyOptions { readonly sourcePath: string; } @@ -41,6 +41,13 @@ export class Staging extends Construct { */ public readonly sourcePath: string; + /** + * A cryptographic hash of the source document(s). + */ + public readonly sourceHash: string; + + private readonly copyOptions: CopyOptions; + /** * The asset path after "prepare" is called. * @@ -53,6 +60,8 @@ export class Staging extends Construct { super(scope, id); this.sourcePath = props.sourcePath; + this.copyOptions = props; + this.sourceHash = fingerprint(this.sourcePath, props); this.stagedPath = new Token(() => this._preparedAssetPath).toString(); } @@ -67,8 +76,7 @@ export class Staging extends Construct { fs.mkdirSync(stagingDir); } - const hash = fingerprint(this.sourcePath); - const targetPath = path.join(stagingDir, hash + path.extname(this.sourcePath)); + const targetPath = path.join(stagingDir, this.sourceHash + path.extname(this.sourcePath)); this._preparedAssetPath = targetPath; @@ -83,9 +91,9 @@ export class Staging extends Construct { fs.copyFileSync(this.sourcePath, targetPath); } else if (stat.isDirectory()) { fs.mkdirSync(targetPath); - copyDirectory(this.sourcePath, targetPath); + copyDirectory(this.sourcePath, targetPath, this.copyOptions); } else { throw new Error(`Unknown file type: ${this.sourcePath}`); } } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/assets/package-lock.json b/packages/@aws-cdk/assets/package-lock.json index c43201dfaeada..82d07dd1e76e1 100644 --- a/packages/@aws-cdk/assets/package-lock.json +++ b/packages/@aws-cdk/assets/package-lock.json @@ -4,12 +4,60 @@ "lockfileVersion": 1, "requires": true, "dependencies": { + "@sinonjs/commons": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.4.0.tgz", + "integrity": "sha512-9jHK3YF/8HtJ9wCAbG+j8cD0i0+ATS9A7gXFqS36TblLPNy6rEEc+SB0imo91eCboGaBYGV/MT1/br/J+EE7Tw==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + }, + "@sinonjs/formatio": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@sinonjs/formatio/-/formatio-3.2.1.tgz", + "integrity": "sha512-tsHvOB24rvyvV2+zKMmPkZ7dXX6LSLKZ7aOtXY6Edklp0uRcgGpOsQTTGTcWViFyx4uhWc6GV8QdnALbIbIdeQ==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1", + "@sinonjs/samsam": "^3.1.0" + } + }, + "@sinonjs/samsam": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-3.3.1.tgz", + "integrity": "sha512-wRSfmyd81swH0hA1bxJZJ57xr22kC07a1N4zuIL47yTS04bDk6AoCkczcqHEjcRPmJ+FruGJ9WBQiJwMtIElFw==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.0.2", + "array-from": "^2.1.1", + "lodash": "^4.17.11" + } + }, + "@sinonjs/text-encoding": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz", + "integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==", + "dev": true + }, "@types/minimatch": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==", "dev": true }, + "@types/sinon": { + "version": "7.0.11", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-7.0.11.tgz", + "integrity": "sha512-6ee09Ugx6GyEr0opUIakmxIWFNmqYPjkqa3/BuxCBokA0klsOLPgMD5K4q40lH7/yZVuJVzOfQpd7pipwjngkQ==", + "dev": true + }, + "array-from": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/array-from/-/array-from-2.1.1.tgz", + "integrity": "sha1-z+nYwmYoudxa7MYqn12PHzUsEZU=", + "dev": true + }, "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", @@ -29,6 +77,42 @@ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" }, + "diff": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", + "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + }, + "just-extend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.0.2.tgz", + "integrity": "sha512-FrLwOgm+iXrPV+5zDU6Jqu4gCRXbWEQg2O3SKONsWE4w7AXFRkryS53bpWdaL9cNol+AmR3AEYz6kn+o0fCPnw==", + "dev": true + }, + "lodash": { + "version": "4.17.11", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", + "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==", + "dev": true + }, + "lolex": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lolex/-/lolex-4.0.1.tgz", + "integrity": "sha512-UHuOBZ5jjsKuzbB/gRNNW8Vg8f00Emgskdq2kvZxgBJCS0aqquAuXai/SkWORlKeZEiNQWZjFZOqIUcH9LqKCw==", + "dev": true + }, "minimatch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", @@ -36,6 +120,72 @@ "requires": { "brace-expansion": "^1.1.7" } + }, + "nise": { + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/nise/-/nise-1.4.10.tgz", + "integrity": "sha512-sa0RRbj53dovjc7wombHmVli9ZihXbXCQ2uH3TNm03DyvOSIQbxg+pbqDKrk2oxMK1rtLGVlKxcB9rrc6X5YjA==", + "dev": true, + "requires": { + "@sinonjs/formatio": "^3.1.0", + "@sinonjs/text-encoding": "^0.7.1", + "just-extend": "^4.0.2", + "lolex": "^2.3.2", + "path-to-regexp": "^1.7.0" + }, + "dependencies": { + "lolex": { + "version": "2.7.5", + "resolved": "https://registry.npmjs.org/lolex/-/lolex-2.7.5.tgz", + "integrity": "sha512-l9x0+1offnKKIzYVjyXU2SiwhXDLekRzKyhnbyldPHvC7BvLPVpdNUNR2KeMAiCN2D/kLNttZgQD5WjSxuBx3Q==", + "dev": true + } + } + }, + "path-to-regexp": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.7.0.tgz", + "integrity": "sha1-Wf3g9DW62suhA6hOnTvGTpa5k30=", + "dev": true, + "requires": { + "isarray": "0.0.1" + } + }, + "sinon": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-7.3.2.tgz", + "integrity": "sha512-thErC1z64BeyGiPvF8aoSg0LEnptSaWE7YhdWWbWXgelOyThent7uKOnnEh9zBxDbKixtr5dEko+ws1sZMuFMA==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.4.0", + "@sinonjs/formatio": "^3.2.1", + "@sinonjs/samsam": "^3.3.1", + "diff": "^3.5.0", + "lolex": "^4.0.1", + "nise": "^1.4.10", + "supports-color": "^5.5.0" + } + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "ts-mock-imports": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/ts-mock-imports/-/ts-mock-imports-1.2.3.tgz", + "integrity": "sha512-pKeHFhlM4s4LvAPiixTsBTzJ65SY0pcXYFQ6nAmDOHl3lYZk4zi2zZFC3et6xX6tKhCCkt2NaYAY+vciPJlo8Q==", + "dev": true + }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true } } } diff --git a/packages/@aws-cdk/assets/package.json b/packages/@aws-cdk/assets/package.json index 3f2cd8b1c9427..a2ac5394ea5c9 100644 --- a/packages/@aws-cdk/assets/package.json +++ b/packages/@aws-cdk/assets/package.json @@ -63,10 +63,13 @@ "devDependencies": { "@aws-cdk/assert": "^0.31.0", "@types/minimatch": "^3.0.3", + "@types/sinon": "^7.0.11", "aws-cdk": "^0.31.0", "cdk-build-tools": "^0.31.0", "cdk-integ-tools": "^0.31.0", - "pkglint": "^0.31.0" + "pkglint": "^0.31.0", + "sinon": "^7.3.2", + "ts-mock-imports": "^1.2.3" }, "dependencies": { "@aws-cdk/aws-iam": "^0.31.0", diff --git a/packages/@aws-cdk/assets/test/fs/test.fs-fingerprint.ts b/packages/@aws-cdk/assets/test/fs/test.fs-fingerprint.ts index 87cf001562055..8d4f76ce617d4 100644 --- a/packages/@aws-cdk/assets/test/fs/test.fs-fingerprint.ts +++ b/packages/@aws-cdk/assets/test/fs/test.fs-fingerprint.ts @@ -2,107 +2,157 @@ import fs = require('fs'); import { Test } from 'nodeunit'; import os = require('os'); import path = require('path'); -import { copyDirectory } from '../../lib/fs/copy'; -import { fingerprint } from '../../lib/fs/fingerprint'; +import libfs = require('../../lib/fs'); export = { - 'single file'(test: Test) { - // GIVEN - const workdir = fs.mkdtempSync(path.join(os.tmpdir(), 'hash-tests')); - const content = 'Hello, world!'; - const input1 = path.join(workdir, 'input1.txt'); - const input2 = path.join(workdir, 'input2.txt'); - const input3 = path.join(workdir, 'input3.txt'); - fs.writeFileSync(input1, content); - fs.writeFileSync(input2, content); - fs.writeFileSync(input3, content + '.'); // add one character, hash should be different - - // WHEN - const hash1 = fingerprint(input1); - const hash2 = fingerprint(input2); - const hash3 = fingerprint(input3); - - // THEN - test.deepEqual(hash1, hash2); - test.notDeepEqual(hash3, hash1); - test.done(); + files: { + 'does not change with the file name'(test: Test) { + // GIVEN + const workdir = fs.mkdtempSync(path.join(os.tmpdir(), 'hash-tests')); + const content = 'Hello, world!'; + const input1 = path.join(workdir, 'input1.txt'); + const input2 = path.join(workdir, 'input2.txt'); + const input3 = path.join(workdir, 'input3.txt'); + fs.writeFileSync(input1, content); + fs.writeFileSync(input2, content); + fs.writeFileSync(input3, content + '.'); // add one character, hash should be different + + // WHEN + const hash1 = libfs.fingerprint(input1); + const hash2 = libfs.fingerprint(input2); + const hash3 = libfs.fingerprint(input3); + + // THEN + test.deepEqual(hash1, hash2); + test.notDeepEqual(hash3, hash1); + test.done(); + }, + + 'works on empty files'(test: Test) { + // GIVEN + const workdir = fs.mkdtempSync(path.join(os.tmpdir(), 'hash-tests')); + const input1 = path.join(workdir, 'empty'); + const input2 = path.join(workdir, 'empty'); + fs.writeFileSync(input1, ''); + fs.writeFileSync(input2, ''); + + // WHEN + const hash1 = libfs.fingerprint(input1); + const hash2 = libfs.fingerprint(input2); + + // THEN + test.deepEqual(hash1, hash2); + test.done(); + }, }, - 'empty file'(test: Test) { - // GIVEN - const workdir = fs.mkdtempSync(path.join(os.tmpdir(), 'hash-tests')); - const input1 = path.join(workdir, 'empty'); - const input2 = path.join(workdir, 'empty'); - fs.writeFileSync(input1, ''); - fs.writeFileSync(input2, ''); - - // WHEN - const hash1 = fingerprint(input1); - const hash2 = fingerprint(input2); - - // THEN - test.deepEqual(hash1, hash2); - test.done(); + directories: { + 'works on directories'(test: Test) { + // GIVEN + const srcdir = path.join(__dirname, 'fixtures', 'symlinks'); + const outdir = fs.mkdtempSync(path.join(os.tmpdir(), 'copy-tests')); + libfs.copyDirectory(srcdir, outdir); + + // WHEN + const hashSrc = libfs.fingerprint(srcdir); + const hashCopy = libfs.fingerprint(outdir); + + // THEN + test.deepEqual(hashSrc, hashCopy); + test.done(); + }, + + 'ignores requested files'(test: Test) { + // GIVEN + const srcdir = path.join(__dirname, 'fixtures', 'symlinks'); + const outdir = fs.mkdtempSync(path.join(os.tmpdir(), 'copy-tests')); + libfs.copyDirectory(srcdir, outdir); + + // WHEN + const hashSrc = libfs.fingerprint(srcdir); + + fs.writeFileSync(path.join(outdir, `${hashSrc}.ignoreme`), 'Ignore me!'); + const hashCopy = libfs.fingerprint(outdir, { exclude: ['*.ignoreme'] }); + + // THEN + test.deepEqual(hashSrc, hashCopy); + test.done(); + }, + + 'changes with file names'(test: Test) { + // GIVEN + const srcdir = path.join(__dirname, 'fixtures', 'symlinks'); + const cpydir = fs.mkdtempSync(path.join(os.tmpdir(), 'fingerprint-tests')); + libfs.copyDirectory(srcdir, cpydir); + + // be careful not to break a symlink + fs.renameSync(path.join(cpydir, 'normal-dir', 'file-in-subdir.txt'), path.join(cpydir, 'move-me.txt')); + + // WHEN + const hashSrc = libfs.fingerprint(srcdir); + const hashCopy = libfs.fingerprint(cpydir); + + // THEN + test.notDeepEqual(hashSrc, hashCopy); + test.done(); + }, }, - 'directory'(test: Test) { - // GIVEN - const srcdir = path.join(__dirname, 'fixtures', 'symlinks'); - const outdir = fs.mkdtempSync(path.join(os.tmpdir(), 'copy-tests')); - copyDirectory(srcdir, outdir); - - // WHEN - const hashSrc = fingerprint(srcdir); - const hashCopy = fingerprint(outdir); - - // THEN - test.deepEqual(hashSrc, hashCopy); - test.done(); - }, - - 'directory, rename files (fingerprint should change)'(test: Test) { - // GIVEN - const srcdir = path.join(__dirname, 'fixtures', 'symlinks'); - const cpydir = fs.mkdtempSync(path.join(os.tmpdir(), 'fingerprint-tests')); - copyDirectory(srcdir, cpydir); - - // be careful not to break a symlink - fs.renameSync(path.join(cpydir, 'normal-dir', 'file-in-subdir.txt'), path.join(cpydir, 'move-me.txt')); - - // WHEN - const hashSrc = fingerprint(srcdir); - const hashCopy = fingerprint(cpydir); - - // THEN - test.notDeepEqual(hashSrc, hashCopy); - test.done(); - }, - - 'external symlink content changes (fingerprint should change)'(test: Test) { - // GIVEN - const dir1 = fs.mkdtempSync(path.join(os.tmpdir(), 'fingerprint-tests')); - const dir2 = fs.mkdtempSync(path.join(os.tmpdir(), 'fingerprint-tests')); - const target = path.join(dir1, 'boom.txt'); - const content = 'boom'; - fs.writeFileSync(target, content); - fs.symlinkSync(target, path.join(dir2, 'link-to-boom.txt')); - - // now dir2 contains a symlink to a file in dir1 - - // WHEN - const original = fingerprint(dir2); - - // now change the contents of the target - fs.writeFileSync(target, 'changning you!'); - const afterChange = fingerprint(dir2); - - // revert the content to original and expect hash to be reverted - fs.writeFileSync(target, content); - const afterRevert = fingerprint(dir2); - - // THEN - test.notDeepEqual(original, afterChange); - test.deepEqual(afterRevert, original); - test.done(); + symlinks: { + 'changes with the contents of followed symlink referent'(test: Test) { + // GIVEN + const dir1 = fs.mkdtempSync(path.join(os.tmpdir(), 'fingerprint-tests')); + const dir2 = fs.mkdtempSync(path.join(os.tmpdir(), 'fingerprint-tests')); + const target = path.join(dir1, 'boom.txt'); + const content = 'boom'; + fs.writeFileSync(target, content); + fs.symlinkSync(target, path.join(dir2, 'link-to-boom.txt')); + + // now dir2 contains a symlink to a file in dir1 + + // WHEN + const original = libfs.fingerprint(dir2); + + // now change the contents of the target + fs.writeFileSync(target, 'changning you!'); + const afterChange = libfs.fingerprint(dir2); + + // revert the content to original and expect hash to be reverted + fs.writeFileSync(target, content); + const afterRevert = libfs.fingerprint(dir2); + + // THEN + test.notDeepEqual(original, afterChange); + test.deepEqual(afterRevert, original); + test.done(); + }, + + 'does not change with the contents of un-followed symlink referent'(test: Test) { + // GIVEN + const dir1 = fs.mkdtempSync(path.join(os.tmpdir(), 'fingerprint-tests')); + const dir2 = fs.mkdtempSync(path.join(os.tmpdir(), 'fingerprint-tests')); + const target = path.join(dir1, 'boom.txt'); + const content = 'boom'; + fs.writeFileSync(target, content); + fs.symlinkSync(target, path.join(dir2, 'link-to-boom.txt')); + + // now dir2 contains a symlink to a file in dir1 + + // WHEN + const original = libfs.fingerprint(dir2, { follow: libfs.FollowMode.Never }); + + // now change the contents of the target + fs.writeFileSync(target, 'changning you!'); + const afterChange = libfs.fingerprint(dir2, { follow: libfs.FollowMode.Never }); + + // revert the content to original and expect hash to be reverted + fs.writeFileSync(target, content); + const afterRevert = libfs.fingerprint(dir2, { follow: libfs.FollowMode.Never }); + + // THEN + test.deepEqual(original, afterChange); + test.deepEqual(afterRevert, original); + test.done(); + } } }; diff --git a/packages/@aws-cdk/assets/test/fs/test.utils.ts b/packages/@aws-cdk/assets/test/fs/test.utils.ts new file mode 100644 index 0000000000000..c0c4107be43e3 --- /dev/null +++ b/packages/@aws-cdk/assets/test/fs/test.utils.ts @@ -0,0 +1,195 @@ +import fs = require('fs'); +import { Test } from 'nodeunit'; +import path = require('path'); +import { ImportMock } from 'ts-mock-imports'; +import { FollowMode } from '../../lib/fs'; +import util = require('../../lib/fs/utils'); + +export = { + shouldExclude: { + 'excludes nothing by default'(test: Test) { + test.ok(!util.shouldExclude([], path.join('some', 'file', 'path'))); + test.done(); + }, + + 'excludes requested files'(test: Test) { + const exclusions = ['*.ignored']; + test.ok(util.shouldExclude(exclusions, path.join('some', 'file.ignored'))); + test.ok(!util.shouldExclude(exclusions, path.join('some', 'important', 'file'))); + test.done(); + }, + + 'does not exclude whitelisted files'(test: Test) { + const exclusions = ['*.ignored', '!important.*']; + test.ok(!util.shouldExclude(exclusions, path.join('some', 'important.ignored'))); + test.done(); + }, + }, + + shouldFollow: { + always: { + 'follows internal'(test: Test) { + const sourceRoot = path.join('source', 'root'); + const linkTarget = path.join(sourceRoot, 'referent'); + + const mockFsExists = ImportMock.mockFunction(fs, 'existsSync', true); + try { + test.ok(util.shouldFollow(FollowMode.Always, sourceRoot, linkTarget)); + test.ok(mockFsExists.calledOnceWith(linkTarget)); + test.done(); + } finally { + mockFsExists.restore(); + } + }, + + 'follows external'(test: Test) { + const sourceRoot = path.join('source', 'root'); + const linkTarget = path.join('alternate', 'referent'); + const mockFsExists = ImportMock.mockFunction(fs, 'existsSync', true); + try { + test.ok(util.shouldFollow(FollowMode.Always, sourceRoot, linkTarget)); + test.ok(mockFsExists.calledOnceWith(linkTarget)); + test.done(); + } finally { + mockFsExists.restore(); + } + }, + + 'does not follow internal when the referent does not exist'(test: Test) { + const sourceRoot = path.join('source', 'root'); + const linkTarget = path.join(sourceRoot, 'referent'); + const mockFsExists = ImportMock.mockFunction(fs, 'existsSync', false); + try { + test.ok(!util.shouldFollow(FollowMode.Always, sourceRoot, linkTarget)); + test.ok(mockFsExists.calledOnceWith(linkTarget)); + test.done(); + } finally { + mockFsExists.restore(); + } + }, + + 'does not follow external when the referent does not exist'(test: Test) { + const sourceRoot = path.join('source', 'root'); + const linkTarget = path.join('alternate', 'referent'); + const mockFsExists = ImportMock.mockFunction(fs, 'existsSync', false); + try { + test.ok(!util.shouldFollow(FollowMode.Always, sourceRoot, linkTarget)); + test.ok(mockFsExists.calledOnceWith(linkTarget)); + test.done(); + } finally { + mockFsExists.restore(); + } + }, + }, + + external: { + 'does not follow internal'(test: Test) { + const sourceRoot = path.join('source', 'root'); + const linkTarget = path.join(sourceRoot, 'referent'); + const mockFsExists = ImportMock.mockFunction(fs, 'existsSync'); + try { + test.ok(!util.shouldFollow(FollowMode.External, sourceRoot, linkTarget)); + test.ok(mockFsExists.notCalled); + test.done(); + } finally { + mockFsExists.restore(); + } + }, + + 'follows external'(test: Test) { + const sourceRoot = path.join('source', 'root'); + const linkTarget = path.join('alternate', 'referent'); + const mockFsExists = ImportMock.mockFunction(fs, 'existsSync', true); + try { + test.ok(util.shouldFollow(FollowMode.External, sourceRoot, linkTarget)); + test.ok(mockFsExists.calledOnceWith(linkTarget)); + test.done(); + } finally { + mockFsExists.restore(); + } + }, + + 'does not follow external when referent does not exist'(test: Test) { + const sourceRoot = path.join('source', 'root'); + const linkTarget = path.join('alternate', 'referent'); + const mockFsExists = ImportMock.mockFunction(fs, 'existsSync', false); + try { + test.ok(!util.shouldFollow(FollowMode.External, sourceRoot, linkTarget)); + test.ok(mockFsExists.calledOnceWith(linkTarget)); + test.done(); + } finally { + mockFsExists.restore(); + } + }, + }, + + blockExternal: { + 'follows internal'(test: Test) { + const sourceRoot = path.join('source', 'root'); + const linkTarget = path.join(sourceRoot, 'referent'); + const mockFsExists = ImportMock.mockFunction(fs, 'existsSync', true); + try { + test.ok(util.shouldFollow(FollowMode.BlockExternal, sourceRoot, linkTarget)); + test.ok(mockFsExists.calledOnceWith(linkTarget)); + test.done(); + } finally { + mockFsExists.restore(); + } + }, + + 'does not follow internal when referent does not exist'(test: Test) { + const sourceRoot = path.join('source', 'root'); + const linkTarget = path.join(sourceRoot, 'referent'); + const mockFsExists = ImportMock.mockFunction(fs, 'existsSync', false); + try { + test.ok(!util.shouldFollow(FollowMode.BlockExternal, sourceRoot, linkTarget)); + test.ok(mockFsExists.calledOnceWith(linkTarget)); + test.done(); + } finally { + mockFsExists.restore(); + } + }, + + 'does not follow external'(test: Test) { + const sourceRoot = path.join('source', 'root'); + const linkTarget = path.join('alternate', 'referent'); + const mockFsExists = ImportMock.mockFunction(fs, 'existsSync'); + try { + test.ok(!util.shouldFollow(FollowMode.BlockExternal, sourceRoot, linkTarget)); + test.ok(mockFsExists.notCalled); + test.done(); + } finally { + mockFsExists.restore(); + } + }, + }, + + never: { + 'does not follow internal'(test: Test) { + const sourceRoot = path.join('source', 'root'); + const linkTarget = path.join(sourceRoot, 'referent'); + const mockFsExists = ImportMock.mockFunction(fs, 'existsSync'); + try { + test.ok(!util.shouldFollow(FollowMode.Never, sourceRoot, linkTarget)); + test.ok(mockFsExists.notCalled); + test.done(); + } finally { + mockFsExists.restore(); + } + }, + + 'does not follow external'(test: Test) { + const sourceRoot = path.join('source', 'root'); + const linkTarget = path.join('alternate', 'referent'); + const mockFsExists = ImportMock.mockFunction(fs, 'existsSync'); + try { + test.ok(!util.shouldFollow(FollowMode.Never, sourceRoot, linkTarget)); + test.ok(mockFsExists.notCalled); + test.done(); + } finally { + mockFsExists.restore(); + } + }, + } + }, +}; diff --git a/packages/@aws-cdk/assets/test/integ.assets.directory.lit.expected.json b/packages/@aws-cdk/assets/test/integ.assets.directory.lit.expected.json index 21728628fc03a..b399c6fa041c6 100644 --- a/packages/@aws-cdk/assets/test/integ.assets.directory.lit.expected.json +++ b/packages/@aws-cdk/assets/test/integ.assets.directory.lit.expected.json @@ -7,6 +7,10 @@ "SampleAssetS3VersionKey3E106D34": { "Type": "String", "Description": "S3 key for asset version \"aws-cdk-asset-test/SampleAsset\"" + }, + "SampleAssetArtifactHashE80944C9": { + "Type": "String", + "Description": "Artifact hash for asset \"aws-cdk-asset-test/SampleAsset\"" } }, "Resources": { diff --git a/packages/@aws-cdk/assets/test/integ.assets.file.lit.expected.json b/packages/@aws-cdk/assets/test/integ.assets.file.lit.expected.json index 33c26403bc539..759ea473c59b4 100644 --- a/packages/@aws-cdk/assets/test/integ.assets.file.lit.expected.json +++ b/packages/@aws-cdk/assets/test/integ.assets.file.lit.expected.json @@ -7,6 +7,10 @@ "SampleAssetS3VersionKey3E106D34": { "Type": "String", "Description": "S3 key for asset version \"aws-cdk-asset-file-test/SampleAsset\"" + }, + "SampleAssetArtifactHashE80944C9": { + "Type": "String", + "Description": "Artifact hash for asset \"aws-cdk-asset-file-test/SampleAsset\"" } }, "Resources": { diff --git a/packages/@aws-cdk/assets/test/integ.assets.permissions.lit.expected.json b/packages/@aws-cdk/assets/test/integ.assets.permissions.lit.expected.json index 6eebb424ee837..0f1462339cfc6 100644 --- a/packages/@aws-cdk/assets/test/integ.assets.permissions.lit.expected.json +++ b/packages/@aws-cdk/assets/test/integ.assets.permissions.lit.expected.json @@ -7,6 +7,10 @@ "MyFileS3VersionKey568C3C9F": { "Type": "String", "Description": "S3 key for asset version \"aws-cdk-asset-refs/MyFile\"" + }, + "MyFileArtifactHashAB5F44E1": { + "Type": "String", + "Description": "Artifact hash for asset \"aws-cdk-asset-refs/MyFile\"" } }, "Resources": { diff --git a/packages/@aws-cdk/assets/test/integ.assets.refs.lit.expected.json b/packages/@aws-cdk/assets/test/integ.assets.refs.lit.expected.json index e7057a36b64a4..e80d3171ab4c7 100644 --- a/packages/@aws-cdk/assets/test/integ.assets.refs.lit.expected.json +++ b/packages/@aws-cdk/assets/test/integ.assets.refs.lit.expected.json @@ -7,6 +7,10 @@ "SampleAssetS3VersionKey3E106D34": { "Type": "String", "Description": "S3 key for asset version \"aws-cdk-asset-refs/SampleAsset\"" + }, + "SampleAssetArtifactHashE80944C9": { + "Type": "String", + "Description": "Artifact hash for asset \"aws-cdk-asset-refs/SampleAsset\"" } }, "Outputs": { @@ -175,4 +179,4 @@ } } } -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/assets/test/integ.multi-assets.expected.json b/packages/@aws-cdk/assets/test/integ.multi-assets.expected.json index a71296157a8a3..e4cdd9326ecc4 100644 --- a/packages/@aws-cdk/assets/test/integ.multi-assets.expected.json +++ b/packages/@aws-cdk/assets/test/integ.multi-assets.expected.json @@ -1,4 +1,9 @@ { + "Resources": { + "DummyResourceF3AB250A": { + "Type": "AWS::IAM::User" + } + }, "Parameters": { "SampleAsset1S3Bucket469E18FF": { "Type": "String", @@ -8,6 +13,10 @@ "Type": "String", "Description": "S3 key for asset version \"aws-cdk-multi-assets/SampleAsset1\"" }, + "SampleAsset1ArtifactHash9E24B5F0": { + "Type": "String", + "Description": "Artifact hash for asset \"aws-cdk-multi-assets/SampleAsset1\"" + }, "SampleAsset2S3BucketC94C651A": { "Type": "String", "Description": "S3 bucket for asset \"aws-cdk-multi-assets/SampleAsset2\"" @@ -15,6 +24,10 @@ "SampleAsset2S3VersionKey3A7E2CC4": { "Type": "String", "Description": "S3 key for asset version \"aws-cdk-multi-assets/SampleAsset2\"" + }, + "SampleAsset2ArtifactHash62F55C83": { + "Type": "String", + "Description": "Artifact hash for asset \"aws-cdk-multi-assets/SampleAsset2\"" } } } \ No newline at end of file diff --git a/packages/@aws-cdk/assets/test/integ.multi-assets.ts b/packages/@aws-cdk/assets/test/integ.multi-assets.ts index 4cf6220d8ca61..e18a0cd48e660 100644 --- a/packages/@aws-cdk/assets/test/integ.multi-assets.ts +++ b/packages/@aws-cdk/assets/test/integ.multi-assets.ts @@ -1,3 +1,4 @@ +import iam = require('@aws-cdk/aws-iam'); import cdk = require('@aws-cdk/cdk'); import path = require('path'); import assets = require('../lib'); @@ -6,6 +7,9 @@ class TestStack extends cdk.Stack { constructor(scope: cdk.App, id: string, props?: cdk.StackProps) { super(scope, id, props); + // The template must contain at least one resource, so there is this... + new iam.User(this, 'DummyResource'); + // Check that the same asset added multiple times is // uploaded and copied. new assets.FileAsset(this, 'SampleAsset1', { @@ -20,4 +24,4 @@ class TestStack extends cdk.Stack { const app = new cdk.App(); new TestStack(app, 'aws-cdk-multi-assets'); -app.run(); \ No newline at end of file +app.run(); diff --git a/packages/@aws-cdk/assets/test/test.asset.ts b/packages/@aws-cdk/assets/test/test.asset.ts index 1d8ad7d56777d..bb92463bd7454 100644 --- a/packages/@aws-cdk/assets/test/test.asset.ts +++ b/packages/@aws-cdk/assets/test/test.asset.ts @@ -30,8 +30,10 @@ export = { path: SAMPLE_ASSET_DIR, id: 'MyAsset', packaging: 'zip', + sourceHash: '6b84b87243a4a01c592d78e1fd3855c4bfef39328cd0a450cc97e81717fea2a2', s3BucketParameter: 'MyAssetS3Bucket68C9B344', s3KeyParameter: 'MyAssetS3VersionKey68E1A45D', + artifactHashParameter: 'MyAssetArtifactHashF518BDDE', }); test.equal(template.Parameters.MyAssetS3Bucket68C9B344.Type, 'String'); @@ -55,8 +57,10 @@ export = { path: dirPath, id: "mystackMyAssetD6B1B593", packaging: "zip", + sourceHash: '6b84b87243a4a01c592d78e1fd3855c4bfef39328cd0a450cc97e81717fea2a2', s3BucketParameter: "MyAssetS3Bucket68C9B344", - s3KeyParameter: "MyAssetS3VersionKey68E1A45D" + s3KeyParameter: "MyAssetS3VersionKey68E1A45D", + artifactHashParameter: 'MyAssetArtifactHashF518BDDE', }); test.done(); @@ -76,8 +80,10 @@ export = { path: filePath, packaging: 'file', id: 'MyAsset', + sourceHash: '78add9eaf468dfa2191da44a7da92a21baba4c686cf6053d772556768ef21197', s3BucketParameter: 'MyAssetS3Bucket68C9B344', s3KeyParameter: 'MyAssetS3VersionKey68E1A45D', + artifactHashParameter: 'MyAssetArtifactHashF518BDDE', }); // verify that now the template contains parameters for this asset @@ -137,8 +143,8 @@ export = { const stack = new cdk.Stack(); // WHEN - new ZipDirectoryAsset(stack, 'MyDirectory1', { path: '.' }); - new ZipDirectoryAsset(stack, 'MyDirectory2', { path: '.' }); + new ZipDirectoryAsset(stack, 'MyDirectory1', { path: path.join(__dirname, 'sample-asset-directory') }); + new ZipDirectoryAsset(stack, 'MyDirectory2', { path: path.join(__dirname, 'sample-asset-directory') }); // THEN: no error @@ -219,7 +225,7 @@ export = { 'staging': { - 'copy file assets under .assets/fingerprint.ext'(test: Test) { + 'copy file assets under .assets/${fingerprint}.ext'(test: Test) { const tempdir = mkdtempSync(); process.chdir(tempdir); // change current directory to somewhere in /tmp @@ -241,7 +247,7 @@ export = { // THEN app.run(); test.ok(fs.existsSync(path.join(tempdir, '.assets'))); - test.ok(fs.existsSync(path.join(tempdir, '.assets', 'fdb4701ff6c99e676018ee2c24a3119b.zip'))); + test.ok(fs.existsSync(path.join(tempdir, '.assets', 'a7a79cdf84b802ea8b198059ff899cffc095a1b9606e919f98e05bf80779756b.zip'))); fs.readdirSync(path.join(tempdir, '.assets')); test.done(); }, @@ -264,8 +270,9 @@ export = { // THEN app.run(); test.ok(fs.existsSync(path.join(tempdir, '.assets'))); - test.ok(fs.existsSync(path.join(tempdir, '.assets', 'b550524e103eb4cf257c594fba5b9fe8', 'sample-asset-file.txt'))); - test.ok(fs.existsSync(path.join(tempdir, '.assets', 'b550524e103eb4cf257c594fba5b9fe8', 'sample-jar-asset.jar'))); + const hash = '6b84b87243a4a01c592d78e1fd3855c4bfef39328cd0a450cc97e81717fea2a2'; + test.ok(fs.existsSync(path.join(tempdir, '.assets', hash, 'sample-asset-file.txt'))); + test.ok(fs.existsSync(path.join(tempdir, '.assets', hash, 'sample-jar-asset.jar'))); fs.readdirSync(path.join(tempdir, '.assets')); test.done(); }, @@ -295,7 +302,7 @@ export = { const template = SynthUtils.templateForStackName(session, stack.name); test.deepEqual(template.Resources.MyResource.Metadata, { - "aws:asset:path": `.my-awesome-staging-directory/b550524e103eb4cf257c594fba5b9fe8`, + "aws:asset:path": `.my-awesome-staging-directory/6b84b87243a4a01c592d78e1fd3855c4bfef39328cd0a450cc97e81717fea2a2`, "aws:asset:property": "PropName" }); test.done(); @@ -323,7 +330,7 @@ export = { const template = SynthUtils.templateForStackName(session, stack.name); test.deepEqual(template.Resources.MyResource.Metadata, { - "aws:asset:path": `${staging}/b550524e103eb4cf257c594fba5b9fe8`, + "aws:asset:path": `${staging}/6b84b87243a4a01c592d78e1fd3855c4bfef39328cd0a450cc97e81717fea2a2`, "aws:asset:property": "PropName" }); test.done(); @@ -351,7 +358,7 @@ export = { const artifact = session.getArtifact(stack.name); const md = Object.values(artifact.metadata || {})[0][0].data; - test.deepEqual(md.path, '.stageme/b550524e103eb4cf257c594fba5b9fe8'); + test.deepEqual(md.path, '.stageme/6b84b87243a4a01c592d78e1fd3855c4bfef39328cd0a450cc97e81717fea2a2'); test.done(); } diff --git a/packages/@aws-cdk/aws-codebuild/test/integ.docker-asset.lit.expected.json b/packages/@aws-cdk/aws-codebuild/test/integ.docker-asset.lit.expected.json index 0fbeaf9ea4e02..04b128f3787bb 100644 --- a/packages/@aws-cdk/aws-codebuild/test/integ.docker-asset.lit.expected.json +++ b/packages/@aws-cdk/aws-codebuild/test/integ.docker-asset.lit.expected.json @@ -11,6 +11,10 @@ "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cCodeS3VersionKey393B7276": { "Type": "String", "Description": "S3 key for asset version \"test-codebuild-docker-asset/AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62c/Code\"" + }, + "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cCodeArtifactHash8BCBAA49": { + "Type": "String", + "Description": "Artifact hash for asset \"test-codebuild-docker-asset/AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62c/Code\"" } }, "Resources": { @@ -28,7 +32,7 @@ 0, { "Fn::Split": [ - ":", + "@sha256:", { "Ref": "MyImageImageName953AD232" } @@ -63,7 +67,11 @@ ], "Version": "2012-10-17" } - } + }, + "DependsOn": [ + "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cServiceRoleDefaultPolicy6BC8737C", + "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cServiceRoleD788AA17" + ] }, "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cServiceRoleD788AA17": { "Type": "AWS::IAM::Role", @@ -142,7 +150,7 @@ 0, { "Fn::Split": [ - ":", + "@sha256:", { "Ref": "MyImageImageName953AD232" } @@ -413,13 +421,13 @@ "RepositoryName" ] }, - ":", + "@sha256:", { "Fn::Select": [ 1, { "Fn::Split": [ - ":", + "@sha256:", { "Ref": "MyImageImageName953AD232" } diff --git a/packages/@aws-cdk/aws-codebuild/test/integ.project-shell.expected.json b/packages/@aws-cdk/aws-codebuild/test/integ.project-shell.expected.json index 976e7c9937dca..23577c4af4fa8 100644 --- a/packages/@aws-cdk/aws-codebuild/test/integ.project-shell.expected.json +++ b/packages/@aws-cdk/aws-codebuild/test/integ.project-shell.expected.json @@ -7,6 +7,10 @@ "BundleS3VersionKey720F2199": { "Type": "String", "Description": "S3 key for asset version \"aws-cdk-codebuild-project-shell/Bundle\"" + }, + "BundleArtifactHashEA214C27": { + "Type": "String", + "Description": "Artifact hash for asset \"aws-cdk-codebuild-project-shell/Bundle\"" } }, "Resources": { diff --git a/packages/@aws-cdk/aws-codebuild/test/integ.project-vpc.expected.json b/packages/@aws-cdk/aws-codebuild/test/integ.project-vpc.expected.json index 0a42b9adfb276..35f355bfeb053 100644 --- a/packages/@aws-cdk/aws-codebuild/test/integ.project-vpc.expected.json +++ b/packages/@aws-cdk/aws-codebuild/test/integ.project-vpc.expected.json @@ -535,6 +535,10 @@ "BundleS3VersionKey720F2199": { "Type": "String", "Description": "S3 key for asset version \"aws-cdk-codebuild-project-vpc/Bundle\"" + }, + "BundleArtifactHashEA214C27": { + "Type": "String", + "Description": "Artifact hash for asset \"aws-cdk-codebuild-project-vpc/Bundle\"" } } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-codebuild/test/test.project.ts b/packages/@aws-cdk/aws-codebuild/test/test.project.ts index 27715110cce8f..174ce65be4eae 100644 --- a/packages/@aws-cdk/aws-codebuild/test/test.project.ts +++ b/packages/@aws-cdk/aws-codebuild/test/test.project.ts @@ -3,6 +3,7 @@ import assets = require('@aws-cdk/assets'); import { Bucket } from '@aws-cdk/aws-s3'; import cdk = require('@aws-cdk/cdk'); import { Test } from 'nodeunit'; +import path = require('path'); import codebuild = require('../lib'); import { Cache, LocalCacheMode } from '../lib/cache'; @@ -129,8 +130,8 @@ export = { // WHEN new codebuild.Project(stack, 'Project', { - buildScriptAsset: new assets.ZipDirectoryAsset(stack, 'Asset', { path: '.' }), - buildScriptAssetEntrypoint: 'hello.sh', + buildScriptAsset: new assets.ZipDirectoryAsset(stack, 'Asset', { path: path.join(__dirname, 'script_bundle') }), + buildScriptAssetEntrypoint: 'build.sh', }); // THEN diff --git a/packages/@aws-cdk/aws-codedeploy/test/lambda/integ.deployment-group.expected.json b/packages/@aws-cdk/aws-codedeploy/test/lambda/integ.deployment-group.expected.json index 27491e271ad0b..d1b1f415cd098 100644 --- a/packages/@aws-cdk/aws-codedeploy/test/lambda/integ.deployment-group.expected.json +++ b/packages/@aws-cdk/aws-codedeploy/test/lambda/integ.deployment-group.expected.json @@ -579,6 +579,10 @@ "Type": "String", "Description": "S3 key for asset version \"aws-cdk-codedeploy-lambda/Handler/Code\"" }, + "HandlerCodeArtifactHashD7814EF8": { + "Type": "String", + "Description": "Artifact hash for asset \"aws-cdk-codedeploy-lambda/Handler/Code\"" + }, "PreHookCodeS3BucketE2616D65": { "Type": "String", "Description": "S3 bucket for asset \"aws-cdk-codedeploy-lambda/PreHook/Code\"" @@ -587,6 +591,10 @@ "Type": "String", "Description": "S3 key for asset version \"aws-cdk-codedeploy-lambda/PreHook/Code\"" }, + "PreHookCodeArtifactHash540B37CB": { + "Type": "String", + "Description": "Artifact hash for asset \"aws-cdk-codedeploy-lambda/PreHook/Code\"" + }, "PostHookCodeS3BucketECF09EB8": { "Type": "String", "Description": "S3 bucket for asset \"aws-cdk-codedeploy-lambda/PostHook/Code\"" @@ -594,6 +602,10 @@ "PostHookCodeS3VersionKey53451C7E": { "Type": "String", "Description": "S3 key for asset version \"aws-cdk-codedeploy-lambda/PostHook/Code\"" + }, + "PostHookCodeArtifactHash73D72B37": { + "Type": "String", + "Description": "Artifact hash for asset \"aws-cdk-codedeploy-lambda/PostHook/Code\"" } } -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-cfn-cross-region.expected.json b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-cfn-cross-region.expected.json index b3f3b9dbd5e79..27580b9b03a0b 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-cfn-cross-region.expected.json +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-cfn-cross-region.expected.json @@ -268,4 +268,4 @@ } } } -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-cfn-wtih-action-role.expected.json b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-cfn-wtih-action-role.expected.json index 04cf136975672..da68e1737ee0e 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-cfn-wtih-action-role.expected.json +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-cfn-wtih-action-role.expected.json @@ -333,4 +333,4 @@ } } } -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-cfn.expected.json b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-cfn.expected.json index 5f21f509449bf..21978f4abc275 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-cfn.expected.json +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-cfn.expected.json @@ -407,4 +407,4 @@ } } } -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-code-commit-build.expected.json b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-code-commit-build.expected.json index 488d9b70a2369..7472a51f700cc 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-code-commit-build.expected.json +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-code-commit-build.expected.json @@ -582,4 +582,4 @@ ] } } -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-ecs-deploy.expected.json b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-ecs-deploy.expected.json index 242911394e9da..0001b338b25da 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-ecs-deploy.expected.json +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-ecs-deploy.expected.json @@ -815,4 +815,4 @@ ] } } -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-jenkins.expected.json b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-jenkins.expected.json index 93e0bd3c13d98..09dde45fc5791 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-jenkins.expected.json +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-jenkins.expected.json @@ -290,4 +290,4 @@ } } } -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-manual-approval.expected.json b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-manual-approval.expected.json index 81dd6190c7421..6df92db3bd42a 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-manual-approval.expected.json +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-manual-approval.expected.json @@ -203,4 +203,4 @@ } } } -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-dynamodb-global/test/integ.dynamodb.global.expected.json b/packages/@aws-cdk/aws-dynamodb-global/test/integ.dynamodb.global.expected.json index 68f0d210b3b7e..2c49fa3273040 100644 --- a/packages/@aws-cdk/aws-dynamodb-global/test/integ.dynamodb.global.expected.json +++ b/packages/@aws-cdk/aws-dynamodb-global/test/integ.dynamodb.global.expected.json @@ -243,6 +243,10 @@ "SingletonLambdaD38B65A66B544FB69BAD9CD40A6DAC12CodeS3VersionKey59DB89A0": { "Type": "String", "Description": "S3 key for asset version \"globdynamodbinteg-CustomResource/SingletonLambdaD38B65A66B544FB69BAD9CD40A6DAC12/Code\"" + }, + "SingletonLambdaD38B65A66B544FB69BAD9CD40A6DAC12CodeArtifactHashCE92982B": { + "Type": "String", + "Description": "Artifact hash for asset \"globdynamodbinteg-CustomResource/SingletonLambdaD38B65A66B544FB69BAD9CD40A6DAC12/Code\"" } } } diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/integ.sd-awsvpc-nw.expected.json b/packages/@aws-cdk/aws-ecs/test/ec2/integ.sd-awsvpc-nw.expected.json index e6cc62dfd2fac..3fb9be342cd98 100644 --- a/packages/@aws-cdk/aws-ecs/test/ec2/integ.sd-awsvpc-nw.expected.json +++ b/packages/@aws-cdk/aws-ecs/test/ec2/integ.sd-awsvpc-nw.expected.json @@ -853,23 +853,6 @@ ] } }, - "FrontendServiceSecurityGroup85470DEC": { - "Type": "AWS::EC2::SecurityGroup", - "Properties": { - "GroupDescription": "aws-ecs-integ-ecs/FrontendService/SecurityGroup", - "SecurityGroupEgress": [ - { - "CidrIp": "0.0.0.0/0", - "Description": "Allow all outbound traffic by default", - "IpProtocol": "-1" - } - ], - "SecurityGroupIngress": [], - "VpcId": { - "Ref": "Vpc8378EB38" - } - } - }, "FrontendServiceCloudmapService6FE76C06": { "Type": "AWS::ServiceDiscovery::Service", "Properties": { @@ -899,6 +882,23 @@ ] } } + }, + "FrontendServiceSecurityGroup85470DEC": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "aws-ecs-integ-ecs/FrontendService/SecurityGroup", + "SecurityGroupEgress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow all outbound traffic by default", + "IpProtocol": "-1" + } + ], + "SecurityGroupIngress": [], + "VpcId": { + "Ref": "Vpc8378EB38" + } + } } } -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/integ.sd-bridge-nw.expected.json b/packages/@aws-cdk/aws-ecs/test/ec2/integ.sd-bridge-nw.expected.json index 6d9188792f1f3..938d4ab9bb5af 100644 --- a/packages/@aws-cdk/aws-ecs/test/ec2/integ.sd-bridge-nw.expected.json +++ b/packages/@aws-cdk/aws-ecs/test/ec2/integ.sd-bridge-nw.expected.json @@ -865,4 +865,4 @@ } } } -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecs/test/fargate/integ.asset-image.expected.json b/packages/@aws-cdk/aws-ecs/test/fargate/integ.asset-image.expected.json index 3b1452b8e2c4e..3aee844753d35 100644 --- a/packages/@aws-cdk/aws-ecs/test/fargate/integ.asset-image.expected.json +++ b/packages/@aws-cdk/aws-ecs/test/fargate/integ.asset-image.expected.json @@ -360,7 +360,7 @@ 0, { "Fn::Split": [ - ":", + "@sha256:", { "Ref": "ImageImageName5E684353" } @@ -368,7 +368,11 @@ } ] } - } + }, + "DependsOn": [ + "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cServiceRoleDefaultPolicy6BC8737C", + "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cServiceRoleD788AA17" + ] }, "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cServiceRoleD788AA17": { "Type": "AWS::IAM::Role", @@ -447,7 +451,7 @@ 0, { "Fn::Split": [ - ":", + "@sha256:", { "Ref": "ImageImageName5E684353" } @@ -743,13 +747,13 @@ "RepositoryName" ] }, - ":", + "@sha256:", { "Fn::Select": [ 1, { "Fn::Split": [ - ":", + "@sha256:", { "Ref": "ImageImageName5E684353" } @@ -1009,6 +1013,10 @@ "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cCodeS3VersionKey393B7276": { "Type": "String", "Description": "S3 key for asset version \"aws-ecs-integ/AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62c/Code\"" + }, + "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cCodeArtifactHash8BCBAA49": { + "Type": "String", + "Description": "Artifact hash for asset \"aws-ecs-integ/AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62c/Code\"" } }, "Outputs": { diff --git a/packages/@aws-cdk/aws-events-targets/test/ecs/integ.event-task.lit.expected.json b/packages/@aws-cdk/aws-events-targets/test/ecs/integ.event-task.lit.expected.json index ed1654619ee50..cf93a767e37b5 100644 --- a/packages/@aws-cdk/aws-events-targets/test/ecs/integ.event-task.lit.expected.json +++ b/packages/@aws-cdk/aws-events-targets/test/ecs/integ.event-task.lit.expected.json @@ -692,13 +692,13 @@ "RepositoryName" ] }, - ":", + "@sha256:", { "Fn::Select": [ 1, { "Fn::Split": [ - ":", + "@sha256:", { "Ref": "EventImageImageNameE972A8B1" } @@ -859,7 +859,7 @@ 0, { "Fn::Split": [ - ":", + "@sha256:", { "Ref": "EventImageImageNameE972A8B1" } @@ -867,7 +867,11 @@ } ] } - } + }, + "DependsOn": [ + "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cServiceRoleDefaultPolicy6BC8737C", + "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cServiceRoleD788AA17" + ] }, "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cServiceRoleD788AA17": { "Type": "AWS::IAM::Role", @@ -946,7 +950,7 @@ 0, { "Fn::Split": [ - ":", + "@sha256:", { "Ref": "EventImageImageNameE972A8B1" } @@ -1143,6 +1147,10 @@ "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cCodeS3VersionKey393B7276": { "Type": "String", "Description": "S3 key for asset version \"aws-ecs-integ-ecs/AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62c/Code\"" + }, + "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cCodeArtifactHash8BCBAA49": { + "Type": "String", + "Description": "Artifact hash for asset \"aws-ecs-integ-ecs/AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62c/Code\"" } } } diff --git a/packages/@aws-cdk/aws-lambda/test/integ.assets.file.expected.json b/packages/@aws-cdk/aws-lambda/test/integ.assets.file.expected.json index de0f8351c070a..8feb8949c3bd0 100644 --- a/packages/@aws-cdk/aws-lambda/test/integ.assets.file.expected.json +++ b/packages/@aws-cdk/aws-lambda/test/integ.assets.file.expected.json @@ -104,6 +104,10 @@ "MyLambdaCodeS3VersionKey47762537": { "Type": "String", "Description": "S3 key for asset version \"lambda-test-assets-file/MyLambda/Code\"" + }, + "MyLambdaCodeArtifactHashF5E94E30": { + "Type": "String", + "Description": "Artifact hash for asset \"lambda-test-assets-file/MyLambda/Code\"" } } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-lambda/test/integ.assets.lit.expected.json b/packages/@aws-cdk/aws-lambda/test/integ.assets.lit.expected.json index fa1bc3f5e8cea..f3801a2eabf33 100644 --- a/packages/@aws-cdk/aws-lambda/test/integ.assets.lit.expected.json +++ b/packages/@aws-cdk/aws-lambda/test/integ.assets.lit.expected.json @@ -104,6 +104,10 @@ "MyLambdaCodeS3VersionKey47762537": { "Type": "String", "Description": "S3 key for asset version \"lambda-test-assets/MyLambda/Code\"" + }, + "MyLambdaCodeArtifactHashF5E94E30": { + "Type": "String", + "Description": "Artifact hash for asset \"lambda-test-assets/MyLambda/Code\"" } } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-lambda/test/integ.layer-version.lit.expected.json b/packages/@aws-cdk/aws-lambda/test/integ.layer-version.lit.expected.json index af2da01f41b56..6021ea98d0d22 100644 --- a/packages/@aws-cdk/aws-lambda/test/integ.layer-version.lit.expected.json +++ b/packages/@aws-cdk/aws-lambda/test/integ.layer-version.lit.expected.json @@ -7,6 +7,10 @@ "MyLayerCodeS3VersionKeyA45254EC": { "Type": "String", "Description": "S3 key for asset version \"aws-cdk-layer-version-1/MyLayer/Code\"" + }, + "MyLayerCodeArtifactHashCCFB62E9": { + "Type": "String", + "Description": "Artifact hash for asset \"aws-cdk-layer-version-1/MyLayer/Code\"" } }, "Resources": { diff --git a/packages/@aws-cdk/aws-lambda/test/integ.log-retention.expected.json b/packages/@aws-cdk/aws-lambda/test/integ.log-retention.expected.json index 9ef732e1d267b..0cdb053275989 100644 --- a/packages/@aws-cdk/aws-lambda/test/integ.log-retention.expected.json +++ b/packages/@aws-cdk/aws-lambda/test/integ.log-retention.expected.json @@ -378,6 +378,10 @@ "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aCodeS3VersionKey10C1B354": { "Type": "String", "Description": "S3 key for asset version \"aws-cdk-lambda-log-retention/LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8a/Code\"" + }, + "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aCodeArtifactHash327647CC": { + "Type": "String", + "Description": "Artifact hash for asset \"aws-cdk-lambda-log-retention/LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8a/Code\"" } } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-s3-deployment/test/integ.bucket-deployment.expected.json b/packages/@aws-cdk/aws-s3-deployment/test/integ.bucket-deployment.expected.json index f3d6889b8f319..69cf9b444c5e8 100644 --- a/packages/@aws-cdk/aws-s3-deployment/test/integ.bucket-deployment.expected.json +++ b/packages/@aws-cdk/aws-s3-deployment/test/integ.bucket-deployment.expected.json @@ -418,6 +418,10 @@ "Type": "String", "Description": "S3 key for asset version \"test-bucket-deployments-1/DeployMe/Asset\"" }, + "DeployMeAssetArtifactHash31436FAA": { + "Type": "String", + "Description": "Artifact hash for asset \"test-bucket-deployments-1/DeployMe/Asset\"" + }, "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CCodeS3Bucket6E5FB2B7": { "Type": "String", "Description": "S3 bucket for asset \"test-bucket-deployments-1/Custom::CDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C/Code\"" @@ -426,6 +430,10 @@ "Type": "String", "Description": "S3 key for asset version \"test-bucket-deployments-1/Custom::CDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C/Code\"" }, + "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CCodeArtifactHashEF37AD24": { + "Type": "String", + "Description": "Artifact hash for asset \"test-bucket-deployments-1/Custom::CDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C/Code\"" + }, "DeployWithPrefixAssetS3Bucket8B33F071": { "Type": "String", "Description": "S3 bucket for asset \"test-bucket-deployments-1/DeployWithPrefix/Asset\"" @@ -433,6 +441,10 @@ "DeployWithPrefixAssetS3VersionKey45049418": { "Type": "String", "Description": "S3 key for asset version \"test-bucket-deployments-1/DeployWithPrefix/Asset\"" + }, + "DeployWithPrefixAssetArtifactHash9495ADE8": { + "Type": "String", + "Description": "Artifact hash for asset \"test-bucket-deployments-1/DeployWithPrefix/Asset\"" } } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/integ.ec2-task.expected.json b/packages/@aws-cdk/aws-stepfunctions-tasks/test/integ.ec2-task.expected.json index 39f4bde65a48a..4d900089ebbca 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/integ.ec2-task.expected.json +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/integ.ec2-task.expected.json @@ -500,13 +500,13 @@ "RepositoryName" ] }, - ":", + "@sha256:", { "Fn::Select": [ 1, { "Fn::Split": [ - ":", + "@sha256:", { "Ref": "EventImageImageNameE972A8B1" } @@ -667,7 +667,7 @@ 0, { "Fn::Split": [ - ":", + "@sha256:", { "Ref": "EventImageImageNameE972A8B1" } @@ -675,7 +675,11 @@ } ] } - } + }, + "DependsOn": [ + "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cServiceRoleDefaultPolicy6BC8737C", + "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cServiceRoleD788AA17" + ] }, "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cServiceRoleD788AA17": { "Type": "AWS::IAM::Role", @@ -754,7 +758,7 @@ 0, { "Fn::Split": [ - ":", + "@sha256:", { "Ref": "EventImageImageNameE972A8B1" } @@ -985,6 +989,10 @@ "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cCodeS3VersionKey393B7276": { "Type": "String", "Description": "S3 key for asset version \"aws-ecs-integ2/AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62c/Code\"" + }, + "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cCodeArtifactHash8BCBAA49": { + "Type": "String", + "Description": "Artifact hash for asset \"aws-ecs-integ2/AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62c/Code\"" } } -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/integ.fargate-task.expected.json b/packages/@aws-cdk/aws-stepfunctions-tasks/test/integ.fargate-task.expected.json index 56f65b123f8fd..03a26b042def5 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/integ.fargate-task.expected.json +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/integ.fargate-task.expected.json @@ -120,13 +120,13 @@ "RepositoryName" ] }, - ":", + "@sha256:", { "Fn::Select": [ 1, { "Fn::Split": [ - ":", + "@sha256:", { "Ref": "EventImageImageNameE972A8B1" } @@ -289,7 +289,7 @@ 0, { "Fn::Split": [ - ":", + "@sha256:", { "Ref": "EventImageImageNameE972A8B1" } @@ -297,7 +297,11 @@ } ] } - } + }, + "DependsOn": [ + "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cServiceRoleDefaultPolicy6BC8737C", + "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cServiceRoleD788AA17" + ] }, "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cServiceRoleD788AA17": { "Type": "AWS::IAM::Role", @@ -376,7 +380,7 @@ 0, { "Fn::Split": [ - ":", + "@sha256:", { "Ref": "EventImageImageNameE972A8B1" } @@ -629,6 +633,10 @@ "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cCodeS3VersionKey393B7276": { "Type": "String", "Description": "S3 key for asset version \"aws-ecs-integ2/AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62c/Code\"" + }, + "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cCodeArtifactHash8BCBAA49": { + "Type": "String", + "Description": "Artifact hash for asset \"aws-ecs-integ2/AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62c/Code\"" } } -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/cx-api/lib/metadata/assets.ts b/packages/@aws-cdk/cx-api/lib/metadata/assets.ts index 98086a44616b1..7103e2cc8aab6 100644 --- a/packages/@aws-cdk/cx-api/lib/metadata/assets.ts +++ b/packages/@aws-cdk/cx-api/lib/metadata/assets.ts @@ -26,21 +26,34 @@ export const ASSET_RESOURCE_METADATA_PROPERTY_KEY = 'aws:asset:property'; */ export const ASSET_PREFIX_SEPARATOR = '||'; -export interface FileAssetMetadataEntry { +interface BaseAssetMetadataEntry { /** * Requested packaging style */ - readonly packaging: 'zip' | 'file'; + readonly packaging: string; + + /** + * Logical identifier for the asset + */ + readonly id: string; + + /** + * The hash of the source directory used to build the asset. + */ + readonly sourceHash: string; /** * Path on disk to the asset */ readonly path: string; +} + +export interface FileAssetMetadataEntry extends BaseAssetMetadataEntry { /** - * Logical identifier for the asset + * Requested packaging style */ - readonly id: string; + readonly packaging: 'zip' | 'file'; /** * Name of parameter where S3 bucket should be passed in @@ -51,26 +64,21 @@ export interface FileAssetMetadataEntry { * Name of parameter where S3 key should be passed in */ readonly s3KeyParameter: string; -} - -export interface ContainerImageAssetMetadataEntry { - /** - * Type of asset - */ - readonly packaging: 'container-image'; /** - * Path on disk to the asset + * The name of the parameter where the hash of the bundled asset should be passed in. */ - readonly path: string; + readonly artifactHashParameter: string; +} +export interface ContainerImageAssetMetadataEntry extends BaseAssetMetadataEntry { /** - * Logical identifier for the asset + * Type of asset */ - readonly id: string; + readonly packaging: 'container-image'; /** - * ECR Repository name and tag (separated by ":") where this asset is stored. + * ECR Repository name and repo digest (separated by "@sha256:") where this image is stored. */ readonly imageNameParameter: string; diff --git a/packages/aws-cdk/lib/api/toolkit-info.ts b/packages/aws-cdk/lib/api/toolkit-info.ts index 9ddb0994d8345..988fdf29a1756 100644 --- a/packages/aws-cdk/lib/api/toolkit-info.ts +++ b/packages/aws-cdk/lib/api/toolkit-info.ts @@ -1,7 +1,7 @@ import cxapi = require('@aws-cdk/cx-api'); import aws = require('aws-sdk'); import colors = require('colors/safe'); -import { md5hash } from '../archive'; +import { contentHash } from '../archive'; import { debug } from '../logging'; import { Mode } from './aws-auth/credentials'; import { BUCKET_DOMAIN_NAME_OUTPUT, BUCKET_NAME_OUTPUT } from './bootstrap-environment'; @@ -17,6 +17,7 @@ export interface UploadProps { export interface Uploaded { filename: string; key: string; + hash: string; changed: boolean; } @@ -47,10 +48,10 @@ export class ToolkitInfo { /** * 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 + * Uses a hash to render the full key and skips upload if an object * already exists by this key. */ - public async uploadIfChanged(data: any, props: UploadProps): Promise { + public async uploadIfChanged(data: string | Buffer | DataView, props: UploadProps): Promise { const s3 = await this.props.sdk.s3(this.props.environment, Mode.ForWriting); const s3KeyPrefix = props.s3KeyPrefix || ''; @@ -58,7 +59,7 @@ export class ToolkitInfo { const bucket = this.props.bucketName; - const hash = md5hash(data); + const hash = contentHash(data); const filename = `${hash}${s3KeySuffix}`; const key = `${s3KeyPrefix}${filename}`; const url = `s3://${bucket}/${key}`; @@ -66,10 +67,10 @@ export class ToolkitInfo { debug(`${url}: checking if already exists`); if (await objectExists(s3, bucket, key)) { debug(`${url}: found (skipping upload)`); - return { filename, key, changed: false }; + return { filename, key, hash, changed: false }; } - const uploaded = { filename, key, changed: true }; + const uploaded = { filename, key, hash, changed: true }; // Upload if it's new or server-side copy if it was already uploaded previously const previous = this.previousUploads[hash]; diff --git a/packages/aws-cdk/lib/archive.ts b/packages/aws-cdk/lib/archive.ts index b7c3e647625ba..c924035bca369 100644 --- a/packages/aws-cdk/lib/archive.ts +++ b/packages/aws-cdk/lib/archive.ts @@ -25,6 +25,6 @@ export function zipDirectory(directory: string, outputFile: string): Promise { + ci?: boolean): Promise<[CloudFormation.Parameter]> { if (reuse) { return [ @@ -41,7 +39,6 @@ export async function prepareContainerAsset(asset: ContainerImageAssetMetadataEn debug(' πŸ‘‘ Preparing Docker image asset:', asset.path); - const buildHold = new PleaseHold(` βŒ› Building Asset Docker image ${asset.id} from ${asset.path}; this may take a while.`); try { const ecr = await toolkitInfo.prepareEcrRepository(asset); const latest = `${ecr.repositoryUri}:latest`; @@ -60,57 +57,39 @@ export async function prepareContainerAsset(asset: ContainerImageAssetMetadataEn } } - buildHold.start(); - - const baseCommand = ['docker', - 'build', - ...Object.entries(asset.buildArgs || {}).map(([k, v]) => `--build-arg ${k}=${v}`), // Pass build args if any - '--quiet', - asset.path]; + const baseCommand = [ + 'docker', 'build', + ...Object.entries(asset.buildArgs || {}).map(([k, v]) => `--build-arg ${k}=${v}`), + '--tag', latest, + asset.path + ]; const command = ci ? [...baseCommand, '--cache-from', latest] // This does not fail if latest is not available : baseCommand; - const imageId = (await shell(command, { quiet: true })).trim(); - - buildHold.stop(); - - const tag = await calculateImageFingerprint(imageId); - - debug(` βŒ› Image has tag ${tag}, checking ECR repository`); - const imageExists = await toolkitInfo.checkEcrImage(ecr.repositoryName, tag); - - if (imageExists) { - debug(' πŸ‘‘ Image already uploaded.'); - } else { - // Login and push - debug(` βŒ› Image needs to be uploaded first.`); - - if (!loggedIn) { // We could be already logged in if in CI - await dockerLogin(toolkitInfo); - loggedIn = true; - } + await shell(command); - const qualifiedImageName = `${ecr.repositoryUri}:${tag}`; - - await shell(['docker', 'tag', imageId, qualifiedImageName]); - - // There's no way to make this quiet, so we can't use a PleaseHold. Print a header message. - print(` βŒ› Pushing Docker image for ${asset.path}; this may take a while.`); - await shell(['docker', 'push', qualifiedImageName]); - debug(` πŸ‘‘ Docker image for ${asset.path} pushed.`); - } - - if (!loggedIn) { // We could be already logged in if in CI or if image did not exist + // Login and push + if (!loggedIn) { // We could be already logged in if in CI await dockerLogin(toolkitInfo); + loggedIn = true; } - // Always tag and push latest - await shell(['docker', 'tag', imageId, latest]); + // There's no way to make this quiet, so we can't use a PleaseHold. Print a header message. + print(` βŒ› Pushing Docker image for ${asset.path}; this may take a while.`); await shell(['docker', 'push', latest]); + debug(` πŸ‘‘ Docker image for ${asset.path} pushed.`); + + // Get the (single) repo-digest for latest, which'll be @sha256: + const repoDigests = (await shell(['docker', 'image', 'inspect', latest, '--format', '{{range .RepoDigests}}{{.}}|{{end}}'])).trim(); + const requiredPrefix = `${ecr.repositoryUri}@sha256:`; + const repoDigest = repoDigests.split('|').find(digest => digest.startsWith(requiredPrefix)); + if (!repoDigest) { + throw new Error(`Unable to identify repository digest (none starts with ${requiredPrefix}) in:\n${repoDigests}`); + } return [ - { ParameterKey: asset.imageNameParameter, ParameterValue: `${ecr.repositoryName}:${tag}` }, + { ParameterKey: asset.imageNameParameter, ParameterValue: repoDigest.replace(ecr.repositoryUri, ecr.repositoryName) }, ]; } catch (e) { if (e.code === 'ENOENT') { @@ -118,8 +97,6 @@ export async function prepareContainerAsset(asset: ContainerImageAssetMetadataEn throw new Error('Error building Docker image asset; you need to have Docker installed in order to be able to build image assets. Please install Docker and try again.'); } throw e; - } finally { - buildHold.stop(); } } @@ -133,156 +110,3 @@ async function dockerLogin(toolkitInfo: ToolkitInfo) { '--password', credentials.password, credentials.endpoint]); } - -/** - * Calculate image fingerprint. - * - * The fingerprint has a high likelihood to be the same across repositories. - * (As opposed to Docker's built-in image digest, which changes as soon - * as the image is uploaded since it includes the tags that an image has). - * - * The fingerprint will be used as a tag to identify a particular image. - */ -async function calculateImageFingerprint(imageId: string) { - const manifestString = await shell(['docker', 'inspect', imageId], { quiet: true }); - const manifest = JSON.parse(manifestString)[0]; - - // Id can change - delete manifest.Id; - - // Repository-based identifiers are out - delete manifest.RepoTags; - delete manifest.RepoDigests; - - // Metadata that has no bearing on the image contents - delete manifest.Created; - - // We're interested in the image itself, not any running instaces of it - delete manifest.Container; - delete manifest.ContainerConfig; - - // We're not interested in the Docker version used to create this image - delete manifest.DockerVersion; - - // On some Docker versions Metadata contains a LastTagTime which updates - // on every push, causing us to miss all cache hits. - delete manifest.Metadata; - - // GraphDriver is about running the image, not about the image itself. - delete manifest.GraphDriver; - - return crypto.createHash('sha256').update(JSON.stringify(manifest)).digest('hex'); -} - -/** - * Example of a Docker manifest - * - * [ - * { - * "Id": "sha256:3a90542991d03007fd1d8f3b3a6ab04ebb02386785430fe48a867768a048d828", - * "RepoTags": [ - * "993655754359.dkr.ecr.us-east-1.amazonaws.com/cdk/awsecsintegimage7c15b8c6:latest" - * ], - * "RepoDigests": [ - * "993655754359.dkr.ecr.us-east-1.amazo....5e50c0cfc3f2355191934b05df68cd3339a044959111ffec2e14765" - * ], - * "Parent": "sha256:465720f8f43c9c0aff5dcc731d4e368a3927cae4e885442d4ba0bf8a867b7561", - * "Comment": "", - * "Created": "2018-10-17T10:16:40.775888476Z", - * "Container": "20f145d2e7fbf126ca9f4422497b932bc96b5faa038dc032de1e246f64e03a66", - * "ContainerConfig": { - * "Hostname": "9b48b580a312", - * "Domainname": "", - * "User": "", - * "AttachStdin": false, - * "AttachStdout": false, - * "AttachStderr": false, - * "ExposedPorts": { - * "8000/tcp": {} - * }, - * "Tty": false, - * "OpenStdin": false, - * "StdinOnce": false, - * "Env": [ - * "PATH=/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", - * "LANG=C.UTF-8", - * "GPG_KEY=0D96DF4D4110E5C43FBFB17F2D347EA6AA65421D", - * "PYTHON_VERSION=3.6.6", - * "PYTHON_PIP_VERSION=18.1" - * ], - * "Cmd": [ - * "/bin/sh", - * "-c", - * "#(nop) ", - * "CMD [\"/bin/sh\" \"-c\" \"python3 index.py\"]" - * ], - * "ArgsEscaped": true, - * "Image": "sha256:465720f8f43c9c0aff5dcc731d4e368a3927cae4e885442d4ba0bf8a867b7561", - * "Volumes": null, - * "WorkingDir": "/code", - * "Entrypoint": null, - * "OnBuild": [], - * "Labels": {} - * }, - * "DockerVersion": "17.03.2-ce", - * "Author": "", - * "Config": { - * "Hostname": "9b48b580a312", - * "Domainname": "", - * "User": "", - * "AttachStdin": false, - * "AttachStdout": false, - * "AttachStderr": false, - * "ExposedPorts": { - * "8000/tcp": {} - * }, - * "Tty": false, - * "OpenStdin": false, - * "StdinOnce": false, - * "Env": [ - * "PATH=/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", - * "LANG=C.UTF-8", - * "GPG_KEY=0D96DF4D4110E5C43FBFB17F2D347EA6AA65421D", - * "PYTHON_VERSION=3.6.6", - * "PYTHON_PIP_VERSION=18.1" - * ], - * "Cmd": [ - * "/bin/sh", - * "-c", - * "python3 index.py" - * ], - * "ArgsEscaped": true, - * "Image": "sha256:465720f8f43c9c0aff5dcc731d4e368a3927cae4e885442d4ba0bf8a867b7561", - * "Volumes": null, - * "WorkingDir": "/code", - * "Entrypoint": null, - * "OnBuild": [], - * "Labels": {} - * }, - * "Architecture": "amd64", - * "Os": "linux", - * "Size": 917730468, - * "VirtualSize": 917730468, - * "GraphDriver": { - * "Name": "aufs", - * "Data": null - * }, - * "RootFS": { - * "Type": "layers", - * "Layers": [ - * "sha256:f715ed19c28b66943ac8bc12dbfb828e8394de2530bbaf1ecce906e748e4fdff", - * "sha256:8bb25f9cdc41e7d085033af15a522973b44086d6eedd24c11cc61c9232324f77", - * "sha256:08a01612ffca33483a1847c909836610610ce523fb7e1aca880140ee84df23e9", - * "sha256:1191b3f5862aa9231858809b7ac8b91c0b727ce85c9b3279932f0baacc92967d", - * "sha256:9978d084fd771e0b3d1acd7f3525d1b25288ababe9ad8ed259b36101e4e3addd", - * "sha256:2f4f74d3821ecbdd60b5d932452ea9e30cecf902334165c4a19837f6ee636377", - * "sha256:003bb6178bc3218242d73e51d5e9ab2f991dc607780194719c6bd4c8c412fe8c", - * "sha256:15b32d849da2239b1af583f9381c7a75d7aceba12f5ddfffa7a059116cf05ab9", - * "sha256:6e5c5f6bf043bc634378b1e4b61af09be74741f2ac80204d7a373713b1fd5a40", - * "sha256:3260e00e353bfb765b25597d13868c2ef64cb3d509875abcfb58c4e9bf7f4ee2", - * "sha256:f3274b75856311e92e14a1270c78737c86456d6353fe4a83bd2e81bcd2a996ea" - * ] - * } - * } - * ] - */ diff --git a/packages/aws-cdk/lib/util/please-hold.ts b/packages/aws-cdk/lib/util/please-hold.ts deleted file mode 100644 index cb6eff963296b..0000000000000 --- a/packages/aws-cdk/lib/util/please-hold.ts +++ /dev/null @@ -1,26 +0,0 @@ -import colors = require('colors/safe'); -import { print } from "../logging"; - -/** - * Print a message to the logger in case the operation takes a long time - */ -export class PleaseHold { - private handle?: NodeJS.Timer; - - constructor(private readonly message: string, private readonly timeoutSec = 10) { - } - - public start() { - this.handle = setTimeout(this.printMessage.bind(this), this.timeoutSec * 1000); - } - - public stop() { - if (this.handle) { - clearTimeout(this.handle); - } - } - - private printMessage() { - print(colors.yellow(this.message)); - } -} diff --git a/packages/aws-cdk/test/test.archive.ts b/packages/aws-cdk/test/test.archive.ts index 86e820204eee5..f26b662e79ca2 100644 --- a/packages/aws-cdk/test/test.archive.ts +++ b/packages/aws-cdk/test/test.archive.ts @@ -4,7 +4,7 @@ import { Test } from 'nodeunit'; import os = require('os'); import path = require('path'); import { promisify } from 'util'; -import { md5hash, zipDirectory } from '../lib/archive'; +import { contentHash, zipDirectory } from '../lib/archive'; const exec = promisify(_exec); export = { @@ -38,8 +38,8 @@ export = { 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)); + const hash1 = contentHash(await fs.readFile(zipFile1)); + const hash2 = contentHash(await fs.readFile(zipFile2)); test.deepEqual(hash1, hash2, 'md5 hash of two zips of the same content are not the same'); test.done(); diff --git a/packages/aws-cdk/test/test.assets.ts b/packages/aws-cdk/test/test.assets.ts index ff1ec67a82bb5..5d4a93fae0d2a 100644 --- a/packages/aws-cdk/test/test.assets.ts +++ b/packages/aws-cdk/test/test.assets.ts @@ -21,7 +21,8 @@ export = { id: 'SomeStackSomeResource4567', packaging: 'file', s3BucketParameter: 'BucketParameter', - s3KeyParameter: 'KeyParameter' + s3KeyParameter: 'KeyParameter', + artifactHashParameter: 'ArtifactHashParameter', } as AssetMetadataEntry, trace: [] }] @@ -43,6 +44,7 @@ export = { test.deepEqual(params, [ { ParameterKey: 'BucketParameter', ParameterValue: 'bucket' }, { ParameterKey: 'KeyParameter', ParameterValue: 'assets/SomeStackSomeResource4567/||12345.js' }, + { ParameterKey: 'ArtifactHashParameter', ParameterValue: '12345' }, ]); test.done(); @@ -65,7 +67,8 @@ export = { id: 'SomeStackSomeResource4567', packaging: 'file', s3BucketParameter: 'BucketParameter', - s3KeyParameter: 'KeyParameter' + s3KeyParameter: 'KeyParameter', + artifactHashParameter: 'ArtifactHashParameter', } as AssetMetadataEntry, trace: [] }] @@ -87,6 +90,7 @@ export = { test.deepEqual(params, [ { ParameterKey: 'BucketParameter', UsePreviousValue: true }, { ParameterKey: 'KeyParameter', UsePreviousValue: true }, + { ParameterKey: 'ArtifactHashParameter', UsePreviousValue: true }, ]); test.done(); @@ -139,8 +143,9 @@ class FakeToolkit { const filename = `12345${props.s3KeySuffix}`; return { filename, + key: `${props.s3KeyPrefix}${filename}`, + hash: '12345', changed: true, - key: `${props.s3KeyPrefix}${filename}` }; } } diff --git a/packages/aws-cdk/test/test.docker.ts b/packages/aws-cdk/test/test.docker.ts index b46d81b8042b8..4b2c4c875373a 100644 --- a/packages/aws-cdk/test/test.docker.ts +++ b/packages/aws-cdk/test/test.docker.ts @@ -40,6 +40,7 @@ export = { packaging: 'container-image', path: '/foo', repositoryName: 'some-name', + sourceHash: '0123456789abcdef', }; try { @@ -76,6 +77,7 @@ export = { imageNameParameter: 'MyParameter', packaging: 'container-image', path: '/foo', + sourceHash: '1234567890abcdef', repositoryName: 'some-name', buildArgs: { a: 'b', @@ -90,7 +92,7 @@ export = { } // THEN - const command = ['docker', 'build', '--build-arg a=b', '--build-arg c=d', '--quiet', '/foo']; + const command = ['docker', 'build', '--build-arg a=b', '--build-arg c=d', '--tag', `uri:latest`, '/foo']; test.ok(shellStub.calledWith(command)); prepareEcrRepositoryStub.restore(); diff --git a/packages/decdk/lib/declarative-stack.ts b/packages/decdk/lib/declarative-stack.ts index c508362714123..385edd2640f9f 100644 --- a/packages/decdk/lib/declarative-stack.ts +++ b/packages/decdk/lib/declarative-stack.ts @@ -7,6 +7,7 @@ import { isConstruct, isDataType, isEnumLikeClass, isSerializableInterface, Sche export interface DeclarativeStackProps extends cdk.StackProps { typeSystem: reflect.TypeSystem; template: any; + workingDirectory?: string; } export class DeclarativeStack extends cdk.Stack { @@ -37,7 +38,12 @@ export class DeclarativeStack extends cdk.Stack { const typeInfo = typeSystem.findFqn(rprops.Type + 'Props'); const typeRef = new reflect.TypeReference(typeSystem, typeInfo); const Ctor = resolveType(rprops.Type); - new Ctor(this, logicalId, deserializeValue(this, typeRef, true, 'Properties', rprops.Properties)); + + // Changing working directory if needed, such that relative paths in the template are resolved relative to the + // template's location, and not to the current process' CWD. + _cwd(props.workingDirectory, () => + new Ctor(this, logicalId, deserializeValue(this, typeRef, true, 'Properties', rprops.Properties))); + delete template.Resources[logicalId]; } @@ -437,4 +443,15 @@ function isCfnResourceType(resourceType: string) { return resourceType.includes('::'); } -class ValidationError extends Error { } \ No newline at end of file +class ValidationError extends Error { } + +function _cwd(workDir: string | undefined, cb: () => T): T { + if (!workDir) { return cb(); } + const prevWd = process.cwd(); + try { + process.chdir(workDir); + return cb(); + } finally { + process.chdir(prevWd); + } +} diff --git a/packages/decdk/test/__snapshots__/synth.test.js.snap b/packages/decdk/test/__snapshots__/synth.test.js.snap index 155f265f0271e..ffc700b178f15 100644 --- a/packages/decdk/test/__snapshots__/synth.test.js.snap +++ b/packages/decdk/test/__snapshots__/synth.test.js.snap @@ -31,6 +31,10 @@ Object { }, }, "Parameters": Object { + "HelloLambdaCodeArtifactHashBB927E34": Object { + "Description": "Artifact hash for asset \\"apigw/HelloLambda/Code\\"", + "Type": "String", + }, "HelloLambdaCodeS3BucketB83F7900": Object { "Description": "S3 bucket for asset \\"apigw/HelloLambda/Code\\"", "Type": "String", @@ -947,6 +951,10 @@ Object { }, }, "Parameters": Object { + "HelloWorldFunctionCodeArtifactHashEF4E01C5": Object { + "Description": "Artifact hash for asset \\"lambda-events/HelloWorldFunction/Code\\"", + "Type": "String", + }, "HelloWorldFunctionCodeS3BucketF87BE172": Object { "Description": "S3 bucket for asset \\"lambda-events/HelloWorldFunction/Code\\"", "Type": "String", @@ -1512,6 +1520,10 @@ Object { exports[`lambda-topic.json: lambda-topic 1`] = ` Object { "Parameters": Object { + "LambdaCodeArtifactHash305E64BB": Object { + "Description": "Artifact hash for asset \\"lambda-topic/Lambda/Code\\"", + "Type": "String", + }, "LambdaCodeS3Bucket65766E44": Object { "Description": "S3 bucket for asset \\"lambda-topic/Lambda/Code\\"", "Type": "String", diff --git a/packages/decdk/test/synth.test.ts b/packages/decdk/test/synth.test.ts index 28324dd00d9d5..6725a3ba9927c 100644 --- a/packages/decdk/test/synth.test.ts +++ b/packages/decdk/test/synth.test.ts @@ -27,6 +27,7 @@ async function obtainTypeSystem() { for (const templateFile of fs.readdirSync(dir)) { test(templateFile, async () => { + const workingDirectory = dir; const template = await readTemplate(path.resolve(dir, templateFile)); const typeSystem = await obtainTypeSystem(); @@ -34,10 +35,11 @@ for (const templateFile of fs.readdirSync(dir)) { const stackName = stackNameFromFileName(templateFile); new DeclarativeStack(app, stackName, { + workingDirectory, template, typeSystem }); expect(app.synthesizeStack(stackName).template).toMatchSnapshot(stackName); }); -} \ No newline at end of file +}