From 9b4db425f69f4104b1664052252fa8ce05618710 Mon Sep 17 00:00:00 2001 From: Romain Marcadier-Muller Date: Thu, 23 May 2019 17:48:43 +0200 Subject: [PATCH] feat(assets): Add deploy-time content hash (#2334) Introduces an `IAsset` interface that centralizes common aspects about assets, such as the `sourceHash` and `bundleHash` properties. The `sourceHash` fingerprints the objects that are used as the source for the asset bundling logic, and is available at synthesis time (it can for example be injected in construct IDs when it one wants to ensure a new logical ID is issued for every new version of the asset). The `bundleHash` fingerprints the result of the bundling logic, and is more accurate than `sourceHash` (in that, if the same source can produce different artifacts at different points in time, the `sourceHash` will remain un-changed, but the `bundleHash` will change. The `bundleHash` is however a deploy-time value and thus cannot be used in construct IDs. Fixes #1400 --- .gitignore | 28 +- .../lib/adopt-repository/handler.js | 6 +- .../assets-docker/lib/adopted-repository.ts | 4 + .../@aws-cdk/assets-docker/lib/image-asset.ts | 22 +- .../test/integ.assets-docker.expected.json | 326 ++++++++++++++++++ .../assets-docker/test/integ.assets-docker.ts | 15 + .../assets-docker/test/test.image-asset.ts | 6 +- packages/@aws-cdk/assets/lib/asset.ts | 39 ++- .../@aws-cdk/assets/lib/fs/copy-options.ts | 20 ++ packages/@aws-cdk/assets/lib/fs/copy.ts | 42 +-- .../@aws-cdk/assets/lib/fs/fingerprint.ts | 103 +++--- .../@aws-cdk/assets/lib/fs/follow-mode.ts | 2 +- packages/@aws-cdk/assets/lib/fs/index.ts | 3 +- packages/@aws-cdk/assets/lib/fs/utils.ts | 60 ++++ packages/@aws-cdk/assets/lib/index.ts | 2 + packages/@aws-cdk/assets/lib/staging.ts | 20 +- packages/@aws-cdk/assets/package-lock.json | 150 ++++++++ packages/@aws-cdk/assets/package.json | 5 +- .../assets/test/fs/test.fs-fingerprint.ts | 242 +++++++------ .../@aws-cdk/assets/test/fs/test.utils.ts | 195 +++++++++++ .../integ.assets.directory.lit.expected.json | 4 + .../test/integ.assets.file.lit.expected.json | 4 + ...integ.assets.permissions.lit.expected.json | 4 + .../test/integ.assets.refs.lit.expected.json | 6 +- .../test/integ.multi-assets.expected.json | 13 + .../assets/test/integ.multi-assets.ts | 6 +- packages/@aws-cdk/assets/test/test.asset.ts | 27 +- .../test/integ.docker-asset.lit.expected.json | 18 +- .../test/integ.project-shell.expected.json | 4 + .../test/integ.project-vpc.expected.json | 4 + .../aws-codebuild/test/test.project.ts | 5 +- .../integ.deployment-group.expected.json | 14 +- ...eg.pipeline-cfn-cross-region.expected.json | 2 +- ...ipeline-cfn-wtih-action-role.expected.json | 2 +- .../test/integ.pipeline-cfn.expected.json | 2 +- ...g.pipeline-code-commit-build.expected.json | 2 +- .../integ.pipeline-ecs-deploy.expected.json | 2 +- .../test/integ.pipeline-jenkins.expected.json | 2 +- ...teg.pipeline-manual-approval.expected.json | 2 +- .../test/integ.dynamodb.global.expected.json | 4 + .../test/ec2/integ.sd-awsvpc-nw.expected.json | 36 +- .../test/ec2/integ.sd-bridge-nw.expected.json | 2 +- .../fargate/integ.asset-image.expected.json | 18 +- .../ecs/integ.event-task.lit.expected.json | 18 +- .../test/integ.assets.file.expected.json | 4 + .../test/integ.assets.lit.expected.json | 4 + .../integ.layer-version.lit.expected.json | 4 + .../test/integ.log-retention.expected.json | 4 + .../integ.bucket-deployment.expected.json | 12 + .../test/integ.ec2-task.expected.json | 20 +- .../test/integ.fargate-task.expected.json | 20 +- .../@aws-cdk/cx-api/lib/metadata/assets.ts | 40 ++- packages/aws-cdk/lib/api/toolkit-info.ts | 13 +- packages/aws-cdk/lib/archive.ts | 2 +- packages/aws-cdk/lib/assets.ts | 4 +- packages/aws-cdk/lib/cdk-toolkit.ts | 2 +- packages/aws-cdk/lib/docker.ts | 222 ++---------- packages/aws-cdk/lib/util/please-hold.ts | 26 -- packages/aws-cdk/test/test.archive.ts | 6 +- packages/aws-cdk/test/test.assets.ts | 11 +- packages/aws-cdk/test/test.docker.ts | 4 +- packages/decdk/lib/declarative-stack.ts | 21 +- .../test/__snapshots__/synth.test.js.snap | 12 + packages/decdk/test/synth.test.ts | 4 +- 64 files changed, 1382 insertions(+), 544 deletions(-) create mode 100644 packages/@aws-cdk/assets-docker/test/integ.assets-docker.expected.json create mode 100644 packages/@aws-cdk/assets-docker/test/integ.assets-docker.ts create mode 100644 packages/@aws-cdk/assets/lib/fs/copy-options.ts create mode 100644 packages/@aws-cdk/assets/lib/fs/utils.ts create mode 100644 packages/@aws-cdk/assets/test/fs/test.utils.ts delete mode 100644 packages/aws-cdk/lib/util/please-hold.ts 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 +}