packages/cdk-assets/lib/publishing.ts (173 lines of code) (raw):

import type { AssetManifest, IManifestEntry } from './asset-manifest'; import type { IAws } from './aws'; import type { IAssetHandler, IHandlerHost, SubprocessOutputDestination, PublishOptions, } from './private/asset-handler'; import { DockerFactory } from './private/docker'; import { makeAssetHandler } from './private/handlers'; import { pLimit } from './private/p-limit'; import type { IPublishProgress, IPublishProgressListener } from './progress'; import { EventType } from './progress'; export interface AssetPublishingOptions { /** * Entry point for AWS client */ readonly aws: IAws; /** * Listener for progress events * * @default No listener */ readonly progressListener?: IPublishProgressListener; /** * Whether to throw at the end if there were errors * * @default true */ readonly throwOnError?: boolean; /** * Whether to publish in parallel, when 'publish()' is called * * @default false */ readonly publishInParallel?: boolean; /** * Whether to build assets, when 'publish()' is called * * @default true */ readonly buildAssets?: boolean; /** * Whether to publish assets, when 'publish()' is called * * @default true */ readonly publishAssets?: boolean; /** * @deprecated use {@link #subprocessOutputDestination} instead */ readonly quiet?: boolean; /** * Where to send output of a subprocesses * * @default 'stdio' */ subprocessOutputDestination?: SubprocessOutputDestination; } /** * A failure to publish an asset */ export interface FailedAsset { /** * The asset that failed to publish */ readonly asset: IManifestEntry; /** * The failure that occurred */ readonly error: Error; } export class AssetPublishing implements IPublishProgress { /** * The message for the IPublishProgress interface */ public message: string = 'Starting'; /** * The current asset for the IPublishProgress interface */ public currentAsset?: IManifestEntry; public readonly failures = new Array<FailedAsset>(); private readonly assets: IManifestEntry[]; private readonly totalOperations: number; private completedOperations: number = 0; private aborted = false; private readonly handlerHost: IHandlerHost; private readonly publishInParallel: boolean; private readonly buildAssets: boolean; private readonly publishAssets: boolean; private readonly handlerCache = new Map<IManifestEntry, IAssetHandler>(); constructor( private readonly manifest: AssetManifest, private readonly options: AssetPublishingOptions, ) { this.assets = manifest.entries; this.totalOperations = this.assets.length; this.publishInParallel = options.publishInParallel ?? false; this.buildAssets = options.buildAssets ?? true; this.publishAssets = options.publishAssets ?? true; const self = this; this.handlerHost = { aws: this.options.aws, get aborted() { return self.aborted; }, emitMessage(t, m) { self.progressEvent(t, m); }, dockerFactory: new DockerFactory(), }; } /** * Publish all assets from the manifest */ public async publish(options: PublishOptions = {}): Promise<void> { if (this.publishInParallel) { const limit = pLimit(20); // eslint-disable-next-line @cdklabs/promiseall-no-unbounded-parallelism await Promise.all( this.assets.map((asset) => limit(async () => this.publishAsset(asset, options))), ); } else { for (const asset of this.assets) { if (!(await this.publishAsset(asset, options))) { break; } } } if ((this.options.throwOnError ?? true) && this.failures.length > 0) { throw new Error(`Error publishing: ${this.failures.map((e) => e.error.message)}`); } } /** * Build a single asset from the manifest */ public async buildEntry(asset: IManifestEntry) { try { if (this.progressEvent(EventType.START, `Building ${asset.displayName(false)}`)) { return false; } const handler = this.assetHandler(asset); await handler.build(); if (this.aborted) { throw new Error('Aborted'); } this.completedOperations++; if (this.progressEvent(EventType.SUCCESS, `Built ${asset.displayName(false)}`)) { return false; } } catch (e: any) { this.failures.push({ asset, error: e }); this.completedOperations++; if (this.progressEvent(EventType.FAIL, e.message)) { return false; } } return true; } /** * Publish a single asset from the manifest */ public async publishEntry(asset: IManifestEntry, options: PublishOptions = {}) { try { if (this.progressEvent(EventType.START, `Publishing ${asset.displayName(true)}`)) { return false; } const handler = this.assetHandler(asset); await handler.publish(options); if (this.aborted) { throw new Error('Aborted'); } this.completedOperations++; if (this.progressEvent(EventType.SUCCESS, `Published ${asset.displayName(true)}`)) { return false; } } catch (e: any) { this.failures.push({ asset, error: e }); this.completedOperations++; if (this.progressEvent(EventType.FAIL, e.message)) { return false; } } return true; } /** * Return whether a single asset is published */ public isEntryPublished(asset: IManifestEntry) { const handler = this.assetHandler(asset); return handler.isPublished(); } /** * publish an asset (used by 'publish()') * @param asset The asset to publish * @returns false when publishing should stop */ private async publishAsset(asset: IManifestEntry, options: PublishOptions = {}) { try { if (this.progressEvent(EventType.START, `Publishing ${asset.displayName(true)}`)) { return false; } const handler = this.assetHandler(asset); if (this.buildAssets) { await handler.build(); } if (this.publishAssets) { await handler.publish(options); } if (this.aborted) { throw new Error('Aborted'); } this.completedOperations++; if (this.progressEvent(EventType.SUCCESS, `Published ${asset.displayName(true)}`)) { return false; } } catch (e: any) { this.failures.push({ asset, error: e }); this.completedOperations++; if (this.progressEvent(EventType.FAIL, e.message)) { return false; } } return true; } public get percentComplete() { if (this.totalOperations === 0) { return 100; } return Math.floor((this.completedOperations / this.totalOperations) * 100); } public abort(): void { this.aborted = true; } public get hasFailures() { return this.failures.length > 0; } /** * Publish a progress event to the listener, if present. * * Returns whether an abort is requested. Helper to get rid of repetitive code in publish(). */ private progressEvent(event: EventType, message: string): boolean { this.message = message; if (this.options.progressListener) { this.options.progressListener.onPublishEvent(event, this); } return this.aborted; } private assetHandler(asset: IManifestEntry) { const existing = this.handlerCache.get(asset); if (existing) { return existing; } if (this.options.quiet !== undefined && this.options.subprocessOutputDestination) { throw new Error( 'Cannot set both quiet and subprocessOutputDestination. Please use only subprocessOutputDestination', ); } const subprocessOutputDestination = this.options.subprocessOutputDestination ?? (this.options.quiet ? 'ignore' : 'stdio'); const ret = makeAssetHandler(this.manifest, asset, this.handlerHost, { subprocessOutputDestination, }); this.handlerCache.set(asset, ret); return ret; } }