packages/@aws-cdk/toolkit-lib/lib/api/cloud-assembly/private/context-aware-source.ts (81 lines of code) (raw):

import type { MissingContext } from '@aws-cdk/cloud-assembly-schema'; import * as contextproviders from '../../../context-providers'; import type { ToolkitServices } from '../../../toolkit/private'; import { PROJECT_CONTEXT, type Context } from '../../context'; import type { IoHelper } from '../../io/private'; import { IO } from '../../io/private'; import { ToolkitError } from '../../shared-public'; import type { ICloudAssemblySource, IReadableCloudAssembly } from '../types'; export interface ContextAwareCloudAssemblyProps { /** * AWS object (used by contextprovider) * @deprecated context should be moved to the toolkit itself */ readonly services: ToolkitServices; /** * Application context */ readonly context: Context; /** * The file used to store application context in (relative to cwd). * * @default "cdk.context.json" */ readonly contextFile?: string; /** * Enable context lookups. * * Producing a `cxapi.CloudAssembly` will fail if this is disabled and context lookups need to be performed. * * @default true */ readonly lookups?: boolean; } /** * A CloudAssemblySource that wraps another CloudAssemblySource and runs a lookup loop on it * * This means that if the underlying CloudAssemblySource produces a manifest * with provider queries in it, the `ContextAwareCloudAssemblySource` will * perform the necessary context lookups and invoke the underlying * `CloudAssemblySource` again with thew missing context information. * * This is only useful if the underlying `CloudAssemblySource` can respond to * this new context information (it must be a CDK app source); if it is just a * static directory, then the contents of the assembly won't change in response * to context. * * The context is passed between `ContextAwareCloudAssemblySource` and the wrapped * cloud assembly source via a contex file on disk, so the wrapped assembly source * should re-read the context file on every invocation. */ export class ContextAwareCloudAssemblySource implements ICloudAssemblySource { private canLookup: boolean; private context: Context; private contextFile: string; private ioHelper: IoHelper; constructor(private readonly source: ICloudAssemblySource, private readonly props: ContextAwareCloudAssemblyProps) { this.canLookup = props.lookups ?? true; this.context = props.context; this.contextFile = props.contextFile ?? PROJECT_CONTEXT; // @todo new feature not needed right now this.ioHelper = props.services.ioHelper; } /** * Produce a Cloud Assembly, i.e. a set of stacks */ public async produce(): Promise<IReadableCloudAssembly> { // We may need to run the cloud assembly source multiple times in order to satisfy all missing context // (When the source producer runs, it will tell us about context it wants to use // but it missing. We'll then look up the context and run the executable again, and // again, until it doesn't complain anymore or we've stopped making progress). let previouslyMissingKeys: Set<string> | undefined; while (true) { const readableAsm = await this.source.produce(); const assembly = readableAsm.cloudAssembly; if (assembly.manifest.missing && assembly.manifest.missing.length > 0) { const missingKeysSet = missingContextKeys(assembly.manifest.missing); const missingKeys = Array.from(missingKeysSet); if (!this.canLookup) { throw new ToolkitError( 'Context lookups have been disabled. ' + 'Make sure all necessary context is already in \'cdk.context.json\' by running \'cdk synth\' on a machine with sufficient AWS credentials and committing the result. ' + `Missing context keys: '${missingKeys.join(', ')}'`); } let tryLookup = true; if (previouslyMissingKeys && equalSets(missingKeysSet, previouslyMissingKeys)) { await this.ioHelper.notify(IO.CDK_ASSEMBLY_I0240.msg('Not making progress trying to resolve environmental context. Giving up.', { missingKeys })); tryLookup = false; } previouslyMissingKeys = missingKeysSet; if (tryLookup) { await this.ioHelper.notify(IO.CDK_ASSEMBLY_I0241.msg('Some context information is missing. Fetching...', { missingKeys })); await contextproviders.provideContextValues( assembly.manifest.missing, this.context, this.props.services.sdkProvider, this.props.services.pluginHost, this.ioHelper, ); // Cache the new context to disk await this.ioHelper.notify(IO.CDK_ASSEMBLY_I0042.msg(`Writing updated context to ${this.contextFile}...`, { contextFile: this.contextFile, context: this.context.all, })); await this.context.save(this.contextFile); // Execute again. Unlock the assembly here so that the producer can acquire // a read lock on the directory again. await readableAsm._unlock(); continue; } } return readableAsm; } } } /** * Return all keys of missing context items */ function missingContextKeys(missing?: MissingContext[]): Set<string> { return new Set((missing || []).map(m => m.key)); } /** * Are two sets equal to each other */ function equalSets<A>(a: Set<A>, b: Set<A>) { if (a.size !== b.size) { return false; } for (const x of a) { if (!b.has(x)) { return false; } } return true; }