diff --git a/packages/aws-cdk-lib/core/lib/asset-staging.ts b/packages/aws-cdk-lib/core/lib/asset-staging.ts index 37c8cee5ddf93..9db51fdb9f073 100644 --- a/packages/aws-cdk-lib/core/lib/asset-staging.ts +++ b/packages/aws-cdk-lib/core/lib/asset-staging.ts @@ -6,18 +6,19 @@ import type { AssetOptions } from './assets'; import { AssetHashType, FileAssetPackaging } from './assets'; import type { BundlingOptions } from './bundling'; import { BundlingFileAccess, BundlingOutput, PERF_BUNDLING_SRC_SYM } from './bundling'; -import { AssumptionError, ValidationError } from './errors'; +import { AssumptionError, UnscopedValidationError, ValidationError } from './errors'; import type { FingerprintOptions } from './fs'; -import { FileSystem } from './fs'; +import { FileSystem, SymlinkFollowMode } from './fs'; import { clearLargeFileFingerprintCache } from './fs/fingerprint'; import { Names } from './names'; import { AssetBundlingVolumeCopy, AssetBundlingBindMount } from './private/asset-staging'; import { Cache } from './private/cache'; import { stackOf, stageOf } from './private/core-construct-finders'; -import type { Stack } from './stack'; -import * as cxapi from '../../cx-api'; import { lit } from './private/literal-string'; import { profileSpan } from './private/perf'; +import type { Stack } from './stack'; +import * as cxapi from '../../cx-api'; +import { isInternalPath, resolveLinkTarget } from './fs/utils'; const ARCHIVE_EXTENSIONS = ['.tar.gz', '.zip', '.jar', '.tar', '.tgz']; @@ -181,6 +182,11 @@ export class AssetStaging extends Construct { throw new ValidationError(lit`CannotFindAsset`, `Cannot find asset at ${this.sourcePath}`, this); } + // look for invalid (external) symlinks + if (props.follow == SymlinkFollowMode.BLOCK_EXTERNAL) { + findInvalidSymlinks(this.sourcePath); + } + this._sourceStats = fs.statSync(this.sourcePath); const outdir = stageOf(this)?.assetOutdir; @@ -573,6 +579,31 @@ function determineHashType(scope: Construct, assetHashType?: AssetHashType, cust return hashType; } +/** + * Walk the directory tree, throw if we find external symlinks + * @param root true root of the directory + * @param subRoot used for walking subdirectories + */ +function findInvalidSymlinks(root: string, subRoot: string = root) { + const entries = fs.readdirSync(root, { withFileTypes: true }); + for (const entry of entries) { + const childPath = path.join(subRoot, entry.name); + if (entry.isSymbolicLink()) { + // we check whether this is internal or external, throw on external + const linkPath = fs.readlinkSync(childPath); + const resolvedPath = resolveLinkTarget(childPath, linkPath); + if (!isInternalPath(root, resolvedPath)) { + throw new UnscopedValidationError( + lit`BundlingFileSymlinkForbidden`, + `The file ${resolvedPath} is a symbolic link that is forbidden due to follow mode internal-only. Set \`follow\` to a mode that will follow symlinks (ALWAYS or EXTERNAL) or emit a regular file`, + ); + } + } else if (entry.isDirectory()) { + findInvalidSymlinks(root, childPath); + } + } +} + /** * Calculates a cache key from the props. Normalize by sorting keys. */ @@ -636,6 +667,7 @@ function findSingleFile(scope: Construct, directory: string, archiveOnly: boolea if (content.length === 1) { const file = path.join(directory, content[0]); const extension = getExtension(content[0]).toLowerCase(); + if (fs.statSync(file).isFile() && (!archiveOnly || ARCHIVE_EXTENSIONS.includes(extension))) { return file; } diff --git a/packages/aws-cdk-lib/core/lib/fs/fingerprint.ts b/packages/aws-cdk-lib/core/lib/fs/fingerprint.ts index f8ff46a0403c7..02fffd0e8f753 100644 --- a/packages/aws-cdk-lib/core/lib/fs/fingerprint.ts +++ b/packages/aws-cdk-lib/core/lib/fs/fingerprint.ts @@ -5,7 +5,7 @@ import { FingerprintDiskCache } from './fingerprint-disk-cache'; import { IgnoreStrategy } from './ignore'; import type { FingerprintOptions } from './options'; import { IgnoreMode, SymlinkFollowMode } from './options'; -import { isInternalPath } from './utils'; +import { isInternalPath, resolveLinkTarget } from './utils'; import { UnscopedValidationError } from '../errors'; import { lit } from '../private/literal-string'; @@ -98,12 +98,6 @@ export function fingerprint(fileOrDirectory: string, options: FingerprintOptions } } - function _resolveLinkTarget(realPath: string, linkTarget: string): string { - return path.isAbsolute(linkTarget) - ? path.resolve(linkTarget) - : path.resolve(path.dirname(realPath), linkTarget); - } - // --- Core traversal --- function _processDirectory(symbolicPath: string, realPath: string) { @@ -131,7 +125,7 @@ export function fingerprint(fileOrDirectory: string, options: FingerprintOptions function _processSymlink(symbolicPath: string, realPath: string) { const linkTarget = fs.readlinkSync(realPath); - const resolvedLinkTarget = _resolveLinkTarget(realPath, linkTarget); + const resolvedLinkTarget = resolveLinkTarget(realPath, linkTarget); if (!_shouldFollowLink(resolvedLinkTarget)) { // Not following — hash the link target string itself diff --git a/packages/aws-cdk-lib/core/lib/fs/utils.ts b/packages/aws-cdk-lib/core/lib/fs/utils.ts index b2f944fa17db1..02618495defef 100644 --- a/packages/aws-cdk-lib/core/lib/fs/utils.ts +++ b/packages/aws-cdk-lib/core/lib/fs/utils.ts @@ -35,3 +35,9 @@ export function shouldFollow(mode: SymlinkFollowMode, sourceRoot: string, realPa export function isInternalPath(rootPath: string, targetPath: string): boolean { return rootPath === targetPath || targetPath.startsWith(rootPath + path.sep); } + +export function resolveLinkTarget(realPath: string, linkTarget: string): string { + return path.isAbsolute(linkTarget) + ? path.resolve(linkTarget) + : path.resolve(path.dirname(realPath), linkTarget); +} diff --git a/packages/aws-cdk-lib/core/test/fs/fixtures.tar.gz b/packages/aws-cdk-lib/core/test/fs/fixtures.tar.gz index 50f5ba0f4a259..bcc1f2f12fdab 100644 Binary files a/packages/aws-cdk-lib/core/test/fs/fixtures.tar.gz and b/packages/aws-cdk-lib/core/test/fs/fixtures.tar.gz differ diff --git a/packages/aws-cdk-lib/core/test/fs/fixtures/test1/subdir4/file4.txt b/packages/aws-cdk-lib/core/test/fs/fixtures/test1/subdir4/file4.txt new file mode 100644 index 0000000000000..eed67803db0d2 --- /dev/null +++ b/packages/aws-cdk-lib/core/test/fs/fixtures/test1/subdir4/file4.txt @@ -0,0 +1 @@ +file4 \ No newline at end of file diff --git a/packages/aws-cdk-lib/core/test/fs/fixtures/test1/subdir4/local-link4.txt b/packages/aws-cdk-lib/core/test/fs/fixtures/test1/subdir4/local-link4.txt new file mode 120000 index 0000000000000..7fd9270e4704f --- /dev/null +++ b/packages/aws-cdk-lib/core/test/fs/fixtures/test1/subdir4/local-link4.txt @@ -0,0 +1 @@ +file4.txt \ No newline at end of file diff --git a/packages/aws-cdk-lib/core/test/fs/fs-copy.test.ts b/packages/aws-cdk-lib/core/test/fs/fs-copy.test.ts index 720c82c7cb8bd..387b983f8eb11 100644 --- a/packages/aws-cdk-lib/core/test/fs/fs-copy.test.ts +++ b/packages/aws-cdk-lib/core/test/fs/fs-copy.test.ts @@ -29,6 +29,9 @@ describe('fs copy', () => { ' .hidden', ' subdir3 (D)', ' file3.txt', + 'subdir4 (D)', + ' file4.txt', + ' local-link4.txt => file4.txt', ]); }); diff --git a/packages/aws-cdk-lib/core/test/fs/utils.test.ts b/packages/aws-cdk-lib/core/test/fs/utils.test.ts index 1b4b1fc66b8cd..62b9bf9a83be8 100644 --- a/packages/aws-cdk-lib/core/test/fs/utils.test.ts +++ b/packages/aws-cdk-lib/core/test/fs/utils.test.ts @@ -205,4 +205,32 @@ describe('utils', () => { expect(util.isInternalPath(root, path.resolve(path.join('source', 'elsewhere', 'file.txt')))).toEqual(false); }); }); + + describe('resolveLinkTarget', () => { + test('an absolute link target is resolved as-is', () => { + const realPath = path.join('source', 'root', 'link'); + const linkTarget = path.resolve(path.join('somewhere', 'else', 'referent')); + + expect(util.resolveLinkTarget(realPath, linkTarget)).toEqual(path.resolve(linkTarget)); + }); + + test('a relative link target is resolved against the directory of the link', () => { + const realPath = path.join('source', 'root', 'link'); + const linkTarget = 'referent'; + + // Resolved relative to the link's directory ('source/root'), not the cwd. + expect(util.resolveLinkTarget(realPath, linkTarget)).toEqual( + path.resolve(path.join('source', 'root'), 'referent'), + ); + }); + + test('a relative link target with parent segments is normalized', () => { + const realPath = path.join('source', 'root', 'nested', 'link'); + const linkTarget = path.join('..', 'sibling', 'referent'); + + expect(util.resolveLinkTarget(realPath, linkTarget)).toEqual( + path.resolve(path.join('source', 'root', 'sibling', 'referent')), + ); + }); + }); }); diff --git a/packages/aws-cdk-lib/core/test/staging.test.ts b/packages/aws-cdk-lib/core/test/staging.test.ts index f723fb672ced4..7e47a4eab29d2 100644 --- a/packages/aws-cdk-lib/core/test/staging.test.ts +++ b/packages/aws-cdk-lib/core/test/staging.test.ts @@ -8,7 +8,7 @@ import sinon from 'sinon'; import { FileAssetPackaging } from '../../cloud-assembly-schema'; import * as cxapi from '../../cx-api'; import type { BundlingOptions } from '../lib'; -import { App, AssetHashType, AssetStaging, DockerImage, BundlingOutput, FileSystem, Stack, NestedStack, Stage, BundlingFileAccess } from '../lib'; +import { App, AssetHashType, AssetStaging, DockerImage, BundlingOutput, FileSystem, Stack, NestedStack, Stage, BundlingFileAccess, SymlinkFollowMode } from '../lib'; const STUB_INPUT_FILE = '/tmp/docker-stub.input'; const STUB_INPUT_CONCAT_FILE = '/tmp/docker-stub.input.concat'; @@ -28,7 +28,7 @@ enum DockerStubCommand { } const FIXTURE_TEST1_DIR = path.join(__dirname, 'fs', 'fixtures', 'test1'); -const FIXTURE_TEST1_HASH = '2f37f937c51e2c191af66acf9b09f548926008ec68c575bd2ee54b6e997c0e00'; +const FIXTURE_TEST1_HASH = '0ed6a91d0269df25c265bdeb5c55dca958a86769bcc97e406a3d16ba1a08985a'; const FIXTURE_TARBALL = path.join(__dirname, 'fs', 'fixtures.tar.gz'); const NOT_ARCHIVED_ZIP_TXT_HASH = '95c924c84f5d023be4edee540cb2cb401a49f115d01ed403b288f6cb412771df'; const ARCHIVE_TARBALL_TEST_HASH = '3e948ff54a277d6001e2452fdbc4a9ef61f916ff662ba5e05ece1e2ec6dec9f5'; @@ -228,7 +228,7 @@ describe('staging', () => { const assembly = app.synth(); expect(fs.readdirSync(assembly.directory)).toEqual([ `asset.${FIXTURE_TEST1_HASH}`, - 'asset.af10ac04b3b607b0f8659c8f0cee8c343025ee75baf0b146f10f0e5311d2c46b.tar.gz', + 'asset.39def77ac40e423b62f5529cfb8c9ae54b9d6ecd344f49770f27bc81fac36a6c.tar.gz', 'cdk.out', 'manifest.json', 'stack.metadata.json', @@ -276,7 +276,7 @@ describe('staging', () => { // THEN expect(withoutExtra.assetHash).not.toEqual(withExtra.assetHash); expect(withoutExtra.assetHash).toEqual(FIXTURE_TEST1_HASH); - expect(withExtra.assetHash).toEqual('c95c915a5722bb9019e2c725d11868e5a619b55f36172f76bcbcaa8bb2d10c5f'); + expect(withExtra.assetHash).toEqual('546e4a1731df2753503162a5a260ae037df45ce6dc49f0a98711f5824cddec02'); }); test('can specify extra asset salt via context key', () => { @@ -320,7 +320,7 @@ describe('staging', () => { `run --rm ${USER_ARG} -v /input:/asset-input:${delegated} -v /output:/asset-output:${delegated} -w /asset-input alpine DOCKER_STUB_SUCCESS`, ); expect(fs.readdirSync(assembly.directory)).toEqual([ - 'asset.b1e32e86b3523f2fa512eb99180ee2975a50a4439e63e8badd153f2a68d61aa4', + 'asset.73f25aa93681e01831ecafe334b79916f3cead51b5bc3cadbfc4459dbafd4a3c', 'cdk.out', 'manifest.json', 'stack.metadata.json', @@ -353,7 +353,7 @@ describe('staging', () => { const assembly = app.synth(); expect(fs.readdirSync(assembly.directory)).toEqual([ - 'asset.b1e32e86b3523f2fa512eb99180ee2975a50a4439e63e8badd153f2a68d61aa4', + 'asset.73f25aa93681e01831ecafe334b79916f3cead51b5bc3cadbfc4459dbafd4a3c', 'cdk.out', 'manifest.json', 'stack.metadata.json', @@ -362,13 +362,13 @@ describe('staging', () => { 'validation-report.json', ]); - expect(asset.assetHash).toEqual('b1e32e86b3523f2fa512eb99180ee2975a50a4439e63e8badd153f2a68d61aa4'); + expect(asset.assetHash).toEqual('73f25aa93681e01831ecafe334b79916f3cead51b5bc3cadbfc4459dbafd4a3c'); expect(asset.sourcePath).toEqual(directory); const resolvedStagePath = asset.relativeStagedPath(stack); // absolute path ending with bundling dir expect(path.isAbsolute(resolvedStagePath)).toEqual(true); - expect(new RegExp('asset.b1e32e86b3523f2fa512eb99180ee2975a50a4439e63e8badd153f2a68d61aa4$').test(resolvedStagePath)).toEqual(true); + expect(new RegExp('asset.73f25aa93681e01831ecafe334b79916f3cead51b5bc3cadbfc4459dbafd4a3c$').test(resolvedStagePath)).toEqual(true); }); test('bundler reuses its output when it can', () => { @@ -404,7 +404,7 @@ describe('staging', () => { ); expect(fs.readdirSync(assembly.directory)).toEqual([ - 'asset.b1e32e86b3523f2fa512eb99180ee2975a50a4439e63e8badd153f2a68d61aa4', + 'asset.73f25aa93681e01831ecafe334b79916f3cead51b5bc3cadbfc4459dbafd4a3c', 'cdk.out', 'manifest.json', 'stack.metadata.json', @@ -502,8 +502,8 @@ describe('staging', () => { ); expect(fs.readdirSync(assembly.directory)).toEqual([ - 'asset.b1e32e86b3523f2fa512eb99180ee2975a50a4439e63e8badd153f2a68d61aa4', // 'Asset' - 'asset.e80bb8f931b87e84975de193f5a7ecddd7558d3caf3d35d3a536d9ae6539234f', // 'AssetWithDifferentBundlingOptions' + 'asset.73f25aa93681e01831ecafe334b79916f3cead51b5bc3cadbfc4459dbafd4a3c', // 'Asset' + 'asset.c16eb04e5e6f7a2e62afaf27abee04bc36da8999efc73d6853ac3dcdd04b6a98', // 'AssetWithDifferentBundlingOptions' 'cdk.out', 'manifest.json', 'stack.metadata.json', @@ -552,7 +552,7 @@ describe('staging', () => { ); expect(fs.readdirSync(assembly.directory)).toEqual([ - 'asset.2de2347dd01e3f43a463652635acaae09539cdf32769d9a60ac0ad4622b1e943', // 'Asset' + 'asset.feebd77844651944d530c7600e6f3dc440a9bad66abf76ab95a0a7d168aabca0', // 'Asset' 'cdk.out', 'manifest.json', 'stack.metadata.json', @@ -688,7 +688,7 @@ describe('staging', () => { expect(appAssembly.directory).toEqual(app2Assembly.directory); expect(fs.readdirSync(appAssembly.directory)).toEqual([ - 'asset.b1e32e86b3523f2fa512eb99180ee2975a50a4439e63e8badd153f2a68d61aa4', + 'asset.73f25aa93681e01831ecafe334b79916f3cead51b5bc3cadbfc4459dbafd4a3c', 'cdk.out', 'manifest.json', 'stack.metadata.json', @@ -790,7 +790,7 @@ describe('staging', () => { expect(appAssembly.directory).toEqual(app2Assembly.directory); expect(fs.readdirSync(appAssembly.directory)).toEqual([ - 'asset.ec1d4062c578dacd630d64166a7d1efcd472e570e085a63f8857f6c674491bac', + 'asset.2a30c05d99c7036854ab5df0a01e8fbf1fad277598e2b0b6a2a29c7e09ebb4dc', 'cdk.out', 'manifest.json', 'stack.metadata.json', @@ -1388,8 +1388,8 @@ describe('staging', () => { // THEN const assembly = app.synth(); expect(fs.readdirSync(assembly.directory)).toEqual([ - 'asset.f43148c61174f444925231b5849b468f21e93b5d1469cd07c53625ffd039ef48', // this is the bundle dir - 'asset.f43148c61174f444925231b5849b468f21e93b5d1469cd07c53625ffd039ef48.zip', + 'asset.ab484104b6aae238176ac35262399d0db8aee2c7d385b27a10f8e0722b3512a9', // this is the bundle dir + 'asset.ab484104b6aae238176ac35262399d0db8aee2c7d385b27a10f8e0722b3512a9.zip', 'cdk.out', 'manifest.json', 'stack.metadata.json', @@ -1397,7 +1397,7 @@ describe('staging', () => { 'tree.json', 'validation-report.json', ]); - expect(fs.readdirSync(path.join(assembly.directory, 'asset.f43148c61174f444925231b5849b468f21e93b5d1469cd07c53625ffd039ef48'))).toEqual([ + expect(fs.readdirSync(path.join(assembly.directory, 'asset.ab484104b6aae238176ac35262399d0db8aee2c7d385b27a10f8e0722b3512a9'))).toEqual([ 'test.zip', // bundle dir with "touched" bundled output file ]); expect(staging.packaging).toEqual(FileAssetPackaging.FILE); @@ -1469,7 +1469,7 @@ describe('staging', () => { // THEN const assembly = app.synth(); expect(fs.readdirSync(assembly.directory)).toEqual([ - 'asset.86ec07746e1d859290cfd8b9c648e581555649c75f51f741f11e22cab6775abc', + 'asset.7c7d7f5e01d066e4167fee3b098f209de7f45e1be53b77e2b757df73a749f1ea', 'cdk.out', 'manifest.json', 'stack.metadata.json', @@ -1583,8 +1583,8 @@ describe('staging', () => { // THEN const assembly = app.synth(); expect(fs.readdirSync(assembly.directory)).toEqual([ - 'asset.ef734136dc22840a94140575a2f98cbc061074e09535589d1cd2c11a4ac2fd75', - 'asset.ef734136dc22840a94140575a2f98cbc061074e09535589d1cd2c11a4ac2fd75_noext', + 'asset.390ed165e2a0a8741f7c86d1c9cd5c0c5aa251e234f5785cda765b25611e1df4', + 'asset.390ed165e2a0a8741f7c86d1c9cd5c0c5aa251e234f5785cda765b25611e1df4_noext', 'cdk.out', 'manifest.json', 'stack.metadata.json', @@ -1629,6 +1629,128 @@ describe('staging', () => { expect(staging.packaging).toEqual(FileAssetPackaging.FILE); expect(staging.isArchive).toEqual(false); }); + + describe('bundling output that is a single symbolic link', () => { + const SYMLINK_THROW = /is a symbolic link that is forbidden due to follow mode .*/; + + test.each([ + [undefined], // EXTERNAL is also the default when `follow` is unset + [SymlinkFollowMode.EXTERNAL], + [SymlinkFollowMode.ALWAYS], + [SymlinkFollowMode.NEVER], + ])('follows an external symlink under mode %s and uses it as a single-file asset', (follow) => { + // GIVEN + const app = new App({ context: { [cxapi.NEW_STYLE_STACK_SYNTHESIS_CONTEXT]: false } }); + const stack = new Stack(app, 'stack'); + const directory = path.join(__dirname, 'fs', 'fixtures', 'test1'); + + // WHEN + const staging = new AssetStaging(stack, 'Asset', { + sourcePath: directory, + assetHashType: AssetHashType.OUTPUT, + follow, + bundling: { + image: DockerImage.fromRegistry('alpine'), + command: [DockerStubCommand.SINGLE_FILE], + outputType: BundlingOutput.SINGLE_FILE, + }, + }); + + expect(staging.packaging).toEqual(FileAssetPackaging.FILE); + expect(staging.isArchive).toEqual(false); + }); + + test('fails under mode BLOCK_EXTERNAL if there is a symlink in the directory being bundled and AssetHashType is Source', () => { + // GIVEN + const app = new App({ context: { [cxapi.NEW_STYLE_STACK_SYNTHESIS_CONTEXT]: false } }); + const stack = new Stack(app, 'stack'); + const directory = path.join(__dirname, 'fs', 'fixtures', 'test1'); + + // WHEN - we should throw because there is an external symlink in the /test1 fixture + expect(() => new AssetStaging(stack, 'Asset', { + sourcePath: directory, + assetHashType: AssetHashType.SOURCE, + follow: SymlinkFollowMode.BLOCK_EXTERNAL, + bundling: { + image: DockerImage.fromRegistry('alpine'), + command: [DockerStubCommand.SINGLE_FILE], + outputType: BundlingOutput.SINGLE_FILE, + }, + })).toThrow(SYMLINK_THROW); + }); + + test('fails under mode BLOCK_EXTERNAL if there is a symlink in the directory being bundled and AssetHashType is Output', () => { + // GIVEN + const app = new App({ context: { [cxapi.NEW_STYLE_STACK_SYNTHESIS_CONTEXT]: false } }); + const stack = new Stack(app, 'stack'); + const directory = path.join(__dirname, 'fs', 'fixtures', 'test1'); + + // WHEN - we should throw no matter the Asset Hash Type + expect(() => new AssetStaging(stack, 'Asset', { + sourcePath: directory, + assetHashType: AssetHashType.OUTPUT, + follow: SymlinkFollowMode.BLOCK_EXTERNAL, + bundling: { + image: DockerImage.fromRegistry('alpine'), + command: [DockerStubCommand.SINGLE_FILE], + outputType: BundlingOutput.SINGLE_FILE, + }, + })).toThrow(SYMLINK_THROW); + }); + + test('fails under mode BLOCK_EXTERNAL if there is a symlink, using more complicated directory layout', () => { + // GIVEN + const app = new App({ context: { [cxapi.NEW_STYLE_STACK_SYNTHESIS_CONTEXT]: false } }); + const stack = new Stack(app, 'stack'); + const directory = path.join(__dirname, 'fs', 'fixtures', 'symlinks'); + + // WHEN - we should throw no matter the Asset Hash Type + expect(() => new AssetStaging(stack, 'Asset', { + sourcePath: directory, + assetHashType: AssetHashType.OUTPUT, + follow: SymlinkFollowMode.BLOCK_EXTERNAL, + bundling: { + image: DockerImage.fromRegistry('alpine'), + command: [DockerStubCommand.SINGLE_FILE], + outputType: BundlingOutput.SINGLE_FILE, + }, + })).toThrow(SYMLINK_THROW); + }); + + test('does not fail if there is a local link', () => { + // GIVEN + const app = new App({ context: { [cxapi.NEW_STYLE_STACK_SYNTHESIS_CONTEXT]: false } }); + const stack = new Stack(app, 'stack'); + const directory = path.join(__dirname, 'fs', 'fixtures', 'test1', 'subdir4'); + + // WHEN - we should throw no matter the Asset Hash Type + const staging = new AssetStaging(stack, 'Asset', { + sourcePath: directory, + assetHashType: AssetHashType.OUTPUT, + follow: SymlinkFollowMode.BLOCK_EXTERNAL, + bundling: { + image: DockerImage.fromRegistry('alpine'), + command: [DockerStubCommand.SINGLE_FILE], + outputType: BundlingOutput.SINGLE_FILE, + }, + }); + + const assembly = app.synth(); + + expect(staging.packaging).toEqual(FileAssetPackaging.FILE); + expect(staging.isArchive).toEqual(false); + + expect(fs.readdirSync(assembly.directory)).toEqual([ + 'asset.1e4cb66c62741f7b9a5dbb596b25efe18984ba847949c85108f1e5f661ba0152.txt', + 'cdk.out', + 'manifest.json', + 'stack.metadata.json', + 'stack.template.json', + 'tree.json', + 'validation-report.json', + ]); + }); + }); }); describe('staging with docker cp', () => { @@ -1671,8 +1793,8 @@ describe('staging with docker cp', () => { // THEN const assembly = app.synth(); expect(fs.readdirSync(assembly.directory)).toEqual([ - 'asset.0ec371a2022d29dfd83f5df104e0f01b34233a4e3e839c3c4ec62008f0b9a0e8', // this is the bundle dir - 'asset.0ec371a2022d29dfd83f5df104e0f01b34233a4e3e839c3c4ec62008f0b9a0e8.zip', + 'asset.c0a5fa22d478764f48802d4ff41174892273b02445015e8e8e08a9596792550c', // this is the bundle dir + 'asset.c0a5fa22d478764f48802d4ff41174892273b02445015e8e8e08a9596792550c.zip', 'cdk.out', 'manifest.json', 'stack.metadata.json', @@ -1680,7 +1802,7 @@ describe('staging with docker cp', () => { 'tree.json', 'validation-report.json', ]); - expect(fs.readdirSync(path.join(assembly.directory, 'asset.0ec371a2022d29dfd83f5df104e0f01b34233a4e3e839c3c4ec62008f0b9a0e8'))).toEqual([ + expect(fs.readdirSync(path.join(assembly.directory, 'asset.c0a5fa22d478764f48802d4ff41174892273b02445015e8e8e08a9596792550c'))).toEqual([ 'test.zip', // bundle dir with "touched" bundled output file ]); expect(staging.packaging).toEqual(FileAssetPackaging.FILE); @@ -1720,8 +1842,8 @@ describe('staging with docker cp', () => { // THEN const assembly = app.synth(); expect(fs.readdirSync(assembly.directory)).toEqual([ - 'asset.93bd4079bff7440a725991ecf249416ae9ad73cb639f4a8d9e8f3ad8d491e89f', - 'asset.93bd4079bff7440a725991ecf249416ae9ad73cb639f4a8d9e8f3ad8d491e89f_noext', + 'asset.4697ea6b345c96a20246f80b874dfd6d640c6d0fd9c097d02ede1f45d37e1732', + 'asset.4697ea6b345c96a20246f80b874dfd6d640c6d0fd9c097d02ede1f45d37e1732_noext', 'cdk.out', 'manifest.json', 'stack.metadata.json', @@ -1754,6 +1876,7 @@ describe('staging with docker cp', () => { // THEN const assembly = app.synth(); + expect(fs.readdirSync(assembly.directory)).toEqual([ 'asset.53a51b4c68874a8e831e24e8982120be2a608f50b2e05edb8501143b3305baa8', 'asset.53a51b4c68874a8e831e24e8982120be2a608f50b2e05edb8501143b3305baa8_noext',