packages/@aws-cdk/toolkit-lib/lib/api/work-graph/work-graph-builder.ts (140 lines of code) (raw):

import * as cxapi from '@aws-cdk/cx-api'; import { AssetManifest, type IManifestEntry } from 'cdk-assets'; import { WorkGraph } from './work-graph'; import type { AssetBuildNode, WorkNode } from './work-graph-types'; import { DeploymentState } from './work-graph-types'; import { contentHashAny } from '../../util'; import type { IoHelper } from '../io/private'; import { ToolkitError } from '../toolkit-error'; export class WorkGraphBuilder { /** * Default priorities for nodes * * Assets builds have higher priority than the other two operations, to make good on our promise that * '--prebuild-assets' will actually do assets before stacks (if it can). Unfortunately it is the * default :( * * But between stack dependencies and publish dependencies, stack dependencies go first */ public static PRIORITIES: Record<WorkNode['type'], number> = { 'asset-build': 10, 'asset-publish': 0, 'stack': 5, }; private readonly graph: WorkGraph; private readonly ioHelper: IoHelper; constructor( ioHelper: IoHelper, private readonly prebuildAssets: boolean, private readonly idPrefix = '', ) { this.graph = new WorkGraph({}, ioHelper); this.ioHelper = ioHelper; } private addStack(artifact: cxapi.CloudFormationStackArtifact) { this.graph.addNodes({ type: 'stack', id: `${this.idPrefix}${artifact.id}`, dependencies: new Set(this.stackArtifactIds(onlyStacks(artifact.dependencies))), stack: artifact, deploymentState: DeploymentState.PENDING, priority: WorkGraphBuilder.PRIORITIES.stack, }); } /** * Oof, see this parameter list */ // eslint-disable-next-line max-len private addAsset(parentStack: cxapi.CloudFormationStackArtifact, assetManifestArtifact: cxapi.AssetManifestArtifact, assetManifest: AssetManifest, asset: IManifestEntry) { // Just the artifact identifier const assetId = asset.id.assetId; const buildId = `build-${assetId}-${contentHashAny([assetId, asset.genericSource]).substring(0, 10)}`; const publishId = `publish-${assetId}-${contentHashAny([assetId, asset.genericDestination]).substring(0, 10)}`; // Build node only gets added once because they are all the same if (!this.graph.tryGetNode(buildId)) { const node: AssetBuildNode = { type: 'asset-build', id: buildId, note: asset.displayName(false), dependencies: new Set([ ...this.stackArtifactIds(assetManifestArtifact.dependencies), // If we disable prebuild, then assets inherit (stack) dependencies from their parent stack ...!this.prebuildAssets ? this.stackArtifactIds(onlyStacks(parentStack.dependencies)) : [], ]), parentStack: parentStack, assetManifestArtifact, assetManifest, asset, deploymentState: DeploymentState.PENDING, priority: WorkGraphBuilder.PRIORITIES['asset-build'], }; this.graph.addNodes(node); } const publishNode = this.graph.tryGetNode(publishId); if (!publishNode) { this.graph.addNodes({ type: 'asset-publish', id: publishId, note: asset.displayName(true), dependencies: new Set([ buildId, ]), parentStack, assetManifestArtifact, assetManifest, asset, deploymentState: DeploymentState.PENDING, priority: WorkGraphBuilder.PRIORITIES['asset-publish'], }); } for (const inheritedDep of this.stackArtifactIds(onlyStacks(parentStack.dependencies))) { // The asset publish step also depends on the stacks that the parent depends on. // This is purely cosmetic: if we don't do this, the progress printing of asset publishing // is going to interfere with the progress bar of the stack deployment. We could remove this // for overall faster deployments if we ever have a better method of progress displaying. // Note: this may introduce a cycle if one of the parent's dependencies is another stack that // depends on this asset. To workaround this we remove these cycles once all nodes have // been added to the graph. this.graph.addDependency(publishId, inheritedDep); } // This will work whether the stack node has been added yet or not this.graph.addDependency(`${this.idPrefix}${parentStack.id}`, publishId); } public build(artifacts: cxapi.CloudArtifact[]): WorkGraph { const parentStacks = stacksFromAssets(artifacts); for (const artifact of artifacts) { if (cxapi.CloudFormationStackArtifact.isCloudFormationStackArtifact(artifact)) { this.addStack(artifact); } else if (cxapi.AssetManifestArtifact.isAssetManifestArtifact(artifact)) { const manifest = AssetManifest.fromFile(artifact.file); for (const entry of manifest.entries) { const parentStack = parentStacks.get(artifact); if (parentStack === undefined) { throw new ToolkitError('Found an asset manifest that is not associated with a stack'); } this.addAsset(parentStack, artifact, manifest, entry); } } else if (cxapi.NestedCloudAssemblyArtifact.isNestedCloudAssemblyArtifact(artifact)) { const assembly = new cxapi.CloudAssembly(artifact.fullPath, { topoSort: false }); const nestedGraph = new WorkGraphBuilder( this.ioHelper, this.prebuildAssets, `${this.idPrefix}${artifact.id}.`, ).build(assembly.artifacts); this.graph.absorb(nestedGraph); } else { // Ignore whatever else } } this.graph.removeUnavailableDependencies(); // Remove any potentially introduced cycles between asset publishing and the stacks that depend on them. this.removeStackPublishCycles(); return this.graph; } private stackArtifactIds(deps: cxapi.CloudArtifact[]): string[] { return deps.flatMap((d) => cxapi.CloudFormationStackArtifact.isCloudFormationStackArtifact(d) ? [this.stackArtifactId(d)] : []); } private stackArtifactId(artifact: cxapi.CloudArtifact): string { if (!cxapi.CloudFormationStackArtifact.isCloudFormationStackArtifact(artifact)) { throw new ToolkitError(`Can only call this on CloudFormationStackArtifact, got: ${artifact.constructor.name}`); } return `${this.idPrefix}${artifact.id}`; } /** * We may have accidentally introduced cycles in an attempt to make the messages printed to the * console not interfere with each other too much. Remove them again. */ private removeStackPublishCycles() { const publishSteps = this.graph.nodesOfType('asset-publish'); for (const publishStep of publishSteps) { for (const dep of publishStep.dependencies) { if (this.graph.reachable(dep, publishStep.id)) { publishStep.dependencies.delete(dep); } } } } } function stacksFromAssets(artifacts: cxapi.CloudArtifact[]) { const ret = new Map<cxapi.AssetManifestArtifact, cxapi.CloudFormationStackArtifact>(); for (const stack of artifacts.filter(x => cxapi.CloudFormationStackArtifact.isCloudFormationStackArtifact(x))) { const assetArtifacts = stack.dependencies.filter((x) => cxapi.AssetManifestArtifact.isAssetManifestArtifact(x)); for (const art of assetArtifacts) { ret.set(art, stack); } } return ret; } function onlyStacks(artifacts: cxapi.CloudArtifact[]) { return artifacts.filter(x => cxapi.CloudFormationStackArtifact.isCloudFormationStackArtifact(x)); }