packages/@aws-cdk/toolkit-lib/lib/actions/diff/private/helpers.ts (142 lines of code) (raw):

import type * as cxapi from '@aws-cdk/cx-api'; import * as fs from 'fs-extra'; import * as uuid from 'uuid'; import type { ChangeSetDiffOptions, DiffOptions, LocalFileDiffOptions } from '..'; import { DiffMethod } from '..'; import type { StackCollection } from '../../../api/cloud-assembly/stack-collection'; import type { Deployments } from '../../../api/deployments'; import type { TemplateInfo } from '../../../api/diff'; import type { ResourcesToImport } from '../../../api/resource-import'; import { removeNonImportResources, ResourceMigrator } from '../../../api/resource-import'; import type { IoHelper, SdkProvider } from '../../../api/shared-private'; import { IO, cfnApi } from '../../../api/shared-private'; import { ToolkitError } from '../../../api/shared-public'; import { deserializeStructure, formatErrorMessage } from '../../../private/util'; export function prepareDiff( ioHelper: IoHelper, stacks: StackCollection, deployments: Deployments, sdkProvider: SdkProvider, options: DiffOptions, ): Promise<TemplateInfo[]> { switch (options.method?.method ?? DiffMethod.ChangeSet().method) { case 'local-file': return localFileDiff(stacks, options); case 'template-only': return cfnDiff(ioHelper, stacks, deployments, options, sdkProvider, false); case 'change-set': return cfnDiff(ioHelper, stacks, deployments, options, sdkProvider, true); default: throw new ToolkitError(formatErrorMessage(`Unknown diff method ${options.method}`)); } } async function localFileDiff(stacks: StackCollection, options: DiffOptions): Promise<TemplateInfo[]> { const methodOptions = (options.method?.options ?? {}) as LocalFileDiffOptions; // Compare single stack against fixed template if (stacks.stackCount !== 1) { throw new ToolkitError( 'Can only select one stack when comparing to fixed template. Use --exclusively to avoid selecting multiple stacks.', ); } if (!(await fs.pathExists(methodOptions.path))) { throw new ToolkitError(`There is no file at ${methodOptions.path}`); } const file = fs.readFileSync(methodOptions.path).toString(); const template = deserializeStructure(file); return [{ oldTemplate: template, newTemplate: stacks.firstStack, }]; } async function cfnDiff( ioHelper: IoHelper, stacks: StackCollection, deployments: Deployments, options: DiffOptions, sdkProvider: SdkProvider, includeChangeSet: boolean, ): Promise<TemplateInfo[]> { const templateInfos = []; const methodOptions = (options.method?.options ?? {}) as ChangeSetDiffOptions; // Compare N stacks against deployed templates for (const stack of stacks.stackArtifacts) { const templateWithNestedStacks = await deployments.readCurrentTemplateWithNestedStacks( stack, methodOptions.compareAgainstProcessedTemplate, ); const currentTemplate = templateWithNestedStacks.deployedRootTemplate; const nestedStacks = templateWithNestedStacks.nestedStacks; const migrator = new ResourceMigrator({ deployments, ioHelper }); const resourcesToImport = await migrator.tryGetResources(await deployments.resolveEnvironment(stack)); if (resourcesToImport) { removeNonImportResources(stack); } const changeSet = includeChangeSet ? await changeSetDiff( ioHelper, deployments, stack, sdkProvider, resourcesToImport, methodOptions.parameters, methodOptions.fallbackToTemplate, ) : undefined; templateInfos.push({ oldTemplate: currentTemplate, newTemplate: stack, isImport: !!resourcesToImport, nestedStacks, changeSet, }); } return templateInfos; } async function changeSetDiff( ioHelper: IoHelper, deployments: Deployments, stack: cxapi.CloudFormationStackArtifact, sdkProvider: SdkProvider, resourcesToImport?: ResourcesToImport, parameters: { [name: string]: string | undefined } = {}, fallBackToTemplate: boolean = true, ): Promise<any | undefined> { let stackExists = false; try { stackExists = await deployments.stackExists({ stack, deployName: stack.stackName, tryLookupRole: true, }); } catch (e: any) { if (!fallBackToTemplate) { throw new ToolkitError(`describeStacks call failed with ${e} for ${stack.stackName}, set fallBackToTemplate to true or use DiffMethod.templateOnly to base the diff on template differences.`); } await ioHelper.notify(IO.DEFAULT_TOOLKIT_DEBUG.msg(`Checking if the stack ${stack.stackName} exists before creating the changeset has failed, will base the diff on template differences.\n`)); await ioHelper.notify(IO.DEFAULT_TOOLKIT_DEBUG.msg(formatErrorMessage(e))); stackExists = false; } if (stackExists) { return cfnApi.createDiffChangeSet(ioHelper, { stack, uuid: uuid.v4(), deployments, willExecute: false, sdkProvider, parameters: parameters, resourcesToImport, failOnError: !fallBackToTemplate, }); } else { if (!fallBackToTemplate) { throw new ToolkitError(`the stack '${stack.stackName}' has not been deployed to CloudFormation, set fallBackToTemplate to true or use DiffMethod.templateOnly to base the diff on template differences.`); } await ioHelper.notify(IO.DEFAULT_TOOLKIT_DEBUG.msg(`the stack '${stack.stackName}' has not been deployed to CloudFormation, skipping changeset creation.`)); return; } } /** * Appends all properties from obj2 to obj1. * obj2 values take priority in the case of collisions. * * @param obj1 The object to modify * @param obj2 The object to consume * * @returns obj1 with all properties from obj2 */ export function appendObject<T>( obj1: { [name: string]: T }, obj2: { [name: string]: T }, ): { [name: string]: T } { // Directly modify obj1 by adding all properties from obj2 for (const key in obj2) { obj1[key] = obj2[key]; } // Return the modified obj1 return obj1; }