packages/@aws-cdk/integ-runner/lib/runner/private/cloud-assembly.ts (191 lines of code) (raw):

import * as path from 'path'; import type { AssemblyManifest, AwsCloudFormationStackProperties, ArtifactManifest, MetadataEntry, AssetManifestProperties, ContainerImageAssetMetadataEntry, FileAssetMetadataEntry } from '@aws-cdk/cloud-assembly-schema'; import { Manifest, ArtifactType, ArtifactMetadataEntryType } from '@aws-cdk/cloud-assembly-schema'; import type { FileManifestEntry, DockerImageManifestEntry } from 'cdk-assets/lib/asset-manifest'; import { AssetManifest } from 'cdk-assets/lib/asset-manifest'; import * as fs from 'fs-extra'; /** * Trace information for stack * map of resource logicalId to trace message */ export type StackTrace = Map<string, string>; /** * Trace information for a assembly * * map of stackId to StackTrace */ export type ManifestTrace = Map<string, StackTrace>; /** * Reads a Cloud Assembly manifest */ export class AssemblyManifestReader { public static readonly DEFAULT_FILENAME = 'manifest.json'; /** * Reads a Cloud Assembly manifest from a file */ public static fromFile(fileName: string): AssemblyManifestReader { try { const obj = Manifest.loadAssemblyManifest(fileName); return new AssemblyManifestReader(path.dirname(fileName), obj, fileName); } catch (e: any) { throw new Error(`Cannot read integ manifest '${fileName}': ${e.message}`); } } /** * Reads a Cloud Assembly manifest from a file or a directory * If the given filePath is a directory then it will look for * a file within the directory with the DEFAULT_FILENAME */ public static fromPath(filePath: string): AssemblyManifestReader { let st; try { st = fs.statSync(filePath); } catch (e: any) { throw new Error(`Cannot read integ manifest at '${filePath}': ${e.message}`); } if (st.isDirectory()) { return AssemblyManifestReader.fromFile(path.join(filePath, AssemblyManifestReader.DEFAULT_FILENAME)); } return AssemblyManifestReader.fromFile(filePath); } /** * The directory where the manifest was found */ public readonly directory: string; constructor(directory: string, private readonly manifest: AssemblyManifest, private readonly manifestFileName: string) { this.directory = directory; } /** * Get the stacks from the manifest * returns a map of artifactId to CloudFormation template */ public get stacks(): Record<string, any> { const stacks: Record<string, any> = {}; for (const [artifactId, artifact] of Object.entries(this.manifest.artifacts ?? {})) { if (artifact.type !== ArtifactType.AWS_CLOUDFORMATION_STACK) { continue; } const props = artifact.properties as AwsCloudFormationStackProperties; const template = fs.readJSONSync(path.resolve(this.directory, props.templateFile)); stacks[artifactId] = template; } return stacks; } /** * Get the nested stacks for a given stack * returns a map of artifactId to CloudFormation template */ public getNestedStacksForStack(stackId: string): Record<string, any> { const nestedTemplates: string[] = this.getAssetManifestsForStack(stackId).flatMap( manifest => manifest.files .filter(asset => asset.source.path?.endsWith('.nested.template.json')) .map(asset => asset.source.path!), ); const nestedStacks: Record<string, any> = Object.fromEntries(nestedTemplates.map(templateFile => ([ templateFile.split('.', 1)[0], fs.readJSONSync(path.resolve(this.directory, templateFile)), ]))); return nestedStacks; } /** * Write trace data to the assembly manifest metadata */ public recordTrace(trace: ManifestTrace): void { const newManifest = { ...this.manifest, artifacts: this.renderArtifacts(trace), }; Manifest.saveAssemblyManifest(newManifest, this.manifestFileName); } /** * Return a list of assets for a given stack */ public getAssetIdsForStack(stackId: string): string[] { const assets: string[] = []; for (const artifact of Object.values(this.manifest.artifacts ?? {})) { if (artifact.type === ArtifactType.ASSET_MANIFEST && (artifact.properties as AssetManifestProperties)?.file === `${stackId}.assets.json`) { assets.push(...this.assetsFromAssetManifest(artifact).map(asset => asset.id.assetId)); } else if (artifact.type === ArtifactType.AWS_CLOUDFORMATION_STACK) { assets.push(...this.assetsFromAssemblyManifest(artifact).map(asset => asset.id)); } } return assets; } /** * For a given stackId return a list of assets that belong to the stack */ public getAssetLocationsForStack(stackId: string): string[] { const assets: string[] = []; for (const artifact of Object.values(this.manifest.artifacts ?? {})) { if (artifact.type === ArtifactType.ASSET_MANIFEST && (artifact.properties as AssetManifestProperties)?.file === `${stackId}.assets.json`) { assets.push(...this.assetsFromAssetManifest(artifact).flatMap(asset => { if (asset.type === 'file' && !asset.source.path?.endsWith('nested.template.json')) { return asset.source.path!; } else if (asset.type !== 'file') { return asset.source.directory!; } return []; })); } else if (artifact.type === ArtifactType.AWS_CLOUDFORMATION_STACK) { assets.push(...this.assetsFromAssemblyManifest(artifact).map(asset => asset.path)); } } return assets; } /** * Return a list of asset artifacts for a given stack */ public getAssetManifestsForStack(stackId: string): AssetManifest[] { return Object.values(this.manifest.artifacts ?? {}) .filter(artifact => artifact.type === ArtifactType.ASSET_MANIFEST && (artifact.properties as AssetManifestProperties)?.file === `${stackId}.assets.json`) .map(artifact => { const fileName = (artifact.properties as AssetManifestProperties).file; return AssetManifest.fromFile(path.join(this.directory, fileName)); }); } /** * Get a list of assets from the assembly manifest */ private assetsFromAssemblyManifest(artifact: ArtifactManifest): (ContainerImageAssetMetadataEntry | FileAssetMetadataEntry)[] { const assets: (ContainerImageAssetMetadataEntry | FileAssetMetadataEntry)[] = []; for (const metadata of Object.values(artifact.metadata ?? {})) { metadata.forEach(data => { if (data.type === ArtifactMetadataEntryType.ASSET) { const asset = (data.data as ContainerImageAssetMetadataEntry | FileAssetMetadataEntry); if (asset.path.startsWith('asset.')) { assets.push(asset); } } }); } return assets; } /** * Get a list of assets from the asset manifest */ private assetsFromAssetManifest(artifact: ArtifactManifest): (FileManifestEntry | DockerImageManifestEntry)[] { const assets: (FileManifestEntry | DockerImageManifestEntry)[] = []; const fileName = (artifact.properties as AssetManifestProperties).file; const assetManifest = AssetManifest.fromFile(path.join(this.directory, fileName)); assetManifest.entries.forEach(entry => { if (entry.type === 'file') { const source = (entry as FileManifestEntry).source; if (source.path && (source.path.startsWith('asset.') || source.path.endsWith('nested.template.json'))) { assets.push(entry as FileManifestEntry); } } else if (entry.type === 'docker-image') { const source = (entry as DockerImageManifestEntry).source; if (source.directory && source.directory.startsWith('asset.')) { assets.push(entry as DockerImageManifestEntry); } } }); return assets; } /** * Clean the manifest of any unneccesary data. Currently that includes * the metadata trace information since this includes trace information like * file system locations and file lines that will change depending on what machine the test is run on */ public cleanManifest(): void { const newManifest = { ...this.manifest, artifacts: this.renderArtifacts(), }; Manifest.saveAssemblyManifest(newManifest, this.manifestFileName); } private renderArtifactMetadata(artifact: ArtifactManifest, trace?: StackTrace): { [id: string]: MetadataEntry[] } | undefined { const newMetadata: { [id: string]: MetadataEntry[] } = {}; if (!artifact.metadata) return artifact.metadata; for (const [metadataId, metadataEntry] of Object.entries(artifact.metadata ?? {})) { newMetadata[metadataId] = metadataEntry.map((meta: MetadataEntry) => { if (meta.type === 'aws:cdk:logicalId' && trace && meta.data) { const traceData = trace.get(meta.data.toString()); if (traceData) { trace.delete(meta.data.toString()); return { type: meta.type, data: meta.data, trace: [traceData], }; } } // return metadata without the trace data return { type: meta.type, data: meta.data, }; }); } if (trace && trace.size > 0) { for (const [id, data] of trace.entries()) { newMetadata[id] = [{ type: 'aws:cdk:logicalId', data: id, trace: [data], }]; } } return newMetadata; } private renderArtifacts(trace?: ManifestTrace): { [id: string]: ArtifactManifest } | undefined { const newArtifacts: { [id: string]: ArtifactManifest } = {}; for (const [artifactId, artifact] of Object.entries(this.manifest.artifacts ?? {})) { let stackTrace: StackTrace | undefined = undefined; if (artifact.type === ArtifactType.AWS_CLOUDFORMATION_STACK && trace) { stackTrace = trace.get(artifactId); } newArtifacts[artifactId] = { ...artifact, metadata: this.renderArtifactMetadata(artifact, stackTrace), }; } return newArtifacts; } }