From d8bf1e62973853f92a8422f185c59d36f474a18d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9F=91=A8=F0=9F=8F=BC=E2=80=8D=F0=9F=92=BB=20Romain=20M?= =?UTF-8?q?arcadier-Muller?= Date: Wed, 17 Oct 2018 11:20:07 +0200 Subject: [PATCH] feat(toolkit): Produce an Asset manifest document Consumes the metadata produced by `Asset`s into the `SynthesizedStack`s, and builds an asset manifest document and "internalizes" the supporting files. The supporting files are de-duplicated using a `sha-256`, so that the same file(s) referenced from multiple assets or stacks will be presented only once in the output. The asset document maps an asset logical ID to the "internalized" file(s) base path in the output directory, the construct path where the metadata was created, and the output parameters specified by the asset (currently always an S3 bucket and key). --- packages/aws-cdk/bin/cdk.ts | 67 ++++++++++++++++++++++++++++++++++++- 1 file changed, 66 insertions(+), 1 deletion(-) diff --git a/packages/aws-cdk/bin/cdk.ts b/packages/aws-cdk/bin/cdk.ts index db1f37cb63176..5d829c8658870 100644 --- a/packages/aws-cdk/bin/cdk.ts +++ b/packages/aws-cdk/bin/cdk.ts @@ -4,6 +4,7 @@ import 'source-map-support/register'; import cxapi = require('@aws-cdk/cx-api'); import childProcess = require('child_process'); import colors = require('colors/safe'); +import crypto = require('crypto'); import fs = require('fs-extra'); import YAML = require('js-yaml'); import minimatch = require('minimatch'); @@ -347,14 +348,78 @@ async function initCommandLine() { fs.mkdirpSync(outputDir); + const extension = json ? 'json' : 'yaml'; + const hashCache: { [path: string]: string } = {}; for (const stack of stacks) { const finalName = renames.finalName(stack.name); - const fileName = `${outputDir}/${finalName}.template.${json ? 'json' : 'yaml'}`; + const fileName = path.join(outputDir, `${finalName}.template.${extension}`); highlight(fileName); await fs.writeFile(fileName, toJsonOrYaml(stack.template)); + const manifestName = path.join(outputDir, `${finalName}.manifest.${extension}`); + await fs.writeFile(manifestName, toJsonOrYaml(await prepareManifest(stack, outputDir, hashCache))); } return undefined; // Nothing to print + + /** + * Prepares an asset manifest document, staging assets in a specified directory. + * @param stack the stack for which assets are to be manifested. + * @param stackName the name of the stack (accounting for renames) + * @param dir the directory under which to stage the assets (an assets sub-directory will be used). + * @param cache a cache for asset files hashes, improving performance when the same assets are used across multiple stacks + */ + async function prepareManifest(stack: cxapi.SynthesizedStack, dir: string, cache: { [path: string]: string }) { + const manifest = { version: '1', assets: {} as any }; + for (const key of Object.keys(stack.metadata)) { + const entries = stack.metadata[key].filter(entry => entry.type === cxapi.ASSET_METADATA); + for (const entry of entries) { + const asset = entry.data! as cxapi.AssetMetadataEntry; + const basePath = path.join('assets', await hash(asset.path), path.basename(asset.path)); + const fullPath = path.join(dir, basePath); + if (!await fs.pathExists(fullPath)) { + await fs.copy(asset.path, fullPath); + } + manifest.assets[asset.id] = { + constructPath: key, + packaging: asset.packaging, + parameters: { + s3bucket: asset.s3BucketParameter, + s3key: asset.s3KeyParameter, + }, + path: basePath, + }; + } + } + return manifest; + + function hash(filePath: string): Promise { + return new Promise(async (ok, ko) => { + if (filePath in cache) { return ok(cache[filePath]); } + debug(`Hashing asset file ${filePath}`); + const stat = await fs.stat(filePath); + const digest = crypto.createHash('sha256'); + digest.once('readable', () => { + const binaryHash = digest.read() as Buffer; + if (binaryHash) { + const strHash = binaryHash.toString('hex'); + debug(`Hash of asset file ${filePath}: ${strHash}`); + return ok(cache[filePath] = strHash); + } + ko(new Error('No hash was generated!')); + }); + if (stat.isDirectory()) { + for (const element of (await fs.readdir(filePath)).sort()) { + digest.write(await hash(path.join(filePath, element)) + '\0'); + } + digest.end(); + } else { + const reader = fs.createReadStream(filePath); + digest.write(path.basename(filePath) + '\0'); + reader.pipe(digest); + } + }); + } + } } /**