-
Notifications
You must be signed in to change notification settings - Fork 69
chore(tmp-toolkit-helpers): formatStackDiff and formatSecurityDiff moved to tmp-toolkit-helpers
#278
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
chore(tmp-toolkit-helpers): formatStackDiff and formatSecurityDiff moved to tmp-toolkit-helpers
#278
Changes from all commits
69711eb
98394d5
fd47e70
ebf4338
8b60fef
a006699
12cb879
dfa15e8
e148e1e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| export * from './nested-stack-templates'; | ||
| export * from './stack-helpers'; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| import type { Template } from './stack-helpers'; | ||
|
|
||
| export interface NestedStackTemplates { | ||
| readonly physicalName: string | undefined; | ||
| readonly deployedTemplate: Template; | ||
| readonly generatedTemplate: Template; | ||
| readonly nestedStackTemplates: { | ||
| [nestedStackLogicalId: string]: NestedStackTemplates; | ||
| }; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| export interface Template { | ||
| Parameters?: Record<string, TemplateParameter>; | ||
| [section: string]: any; | ||
| } | ||
|
|
||
| export interface TemplateParameter { | ||
| Type: string; | ||
| Default?: any; | ||
| Description?: string; | ||
| [key: string]: any; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,296 @@ | ||
| import { format } from 'node:util'; | ||
| import { Writable } from 'stream'; | ||
| import * as cxschema from '@aws-cdk/cloud-assembly-schema'; | ||
| import { | ||
| type DescribeChangeSetOutput, | ||
| type TemplateDiff, | ||
| fullDiff, | ||
| formatSecurityChanges, | ||
| formatDifferences, | ||
| mangleLikeCloudFormation, | ||
| } from '@aws-cdk/cloudformation-diff'; | ||
| import type * as cxapi from '@aws-cdk/cx-api'; | ||
| import * as chalk from 'chalk'; | ||
|
|
||
| import { RequireApproval } from '../../api/require-approval'; | ||
| import { ToolkitError } from '../../api/toolkit-error'; | ||
| import type { NestedStackTemplates } from '../cloudformation/nested-stack-templates'; | ||
| import type { IoHelper } from '../io/private'; | ||
| import { IoDefaultMessages } from '../io/private'; | ||
|
|
||
| /* | ||
| * Custom writable stream that collects text into a string buffer. | ||
| * Used on classes that take in and directly write to a stream, but | ||
| * we intend to capture the output rather than print. | ||
| */ | ||
| export class StringWriteStream extends Writable { | ||
| private buffer: string[] = []; | ||
|
|
||
| constructor() { | ||
| super(); | ||
| } | ||
|
|
||
| _write(chunk: any, _encoding: string, callback: (error?: Error | null) => void): void { | ||
| this.buffer.push(chunk.toString()); | ||
| callback(); | ||
| } | ||
|
|
||
| toString(): string { | ||
| return this.buffer.join(''); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Output of formatSecurityDiff | ||
| */ | ||
| export interface FormatSecurityDiffOutput { | ||
| /** | ||
| * Complete formatted security diff, if it is prompt-worthy | ||
| */ | ||
| readonly formattedDiff?: string; | ||
| } | ||
|
|
||
| /** | ||
| * Formats the security changes of this diff, if the change is impactful enough according to the approval level | ||
| * | ||
| * Returns the diff if the changes are prompt-worthy, an empty object otherwise. | ||
| */ | ||
| export function formatSecurityDiff( | ||
| ioHelper: IoHelper, | ||
| oldTemplate: any, | ||
| newTemplate: cxapi.CloudFormationStackArtifact, | ||
| requireApproval: RequireApproval, | ||
| stackName?: string, | ||
| changeSet?: DescribeChangeSetOutput, | ||
| ): FormatSecurityDiffOutput { | ||
| const ioDefaultHelper = new IoDefaultMessages(ioHelper); | ||
|
|
||
| const diff = fullDiff(oldTemplate, newTemplate.template, changeSet); | ||
|
|
||
| if (diffRequiresApproval(diff, requireApproval)) { | ||
| ioDefaultHelper.info(format('Stack %s\n', chalk.bold(stackName))); | ||
|
|
||
| // eslint-disable-next-line max-len | ||
| ioDefaultHelper.warning(`This deployment will make potentially sensitive changes according to your current security approval level (--require-approval ${requireApproval}).`); | ||
| ioDefaultHelper.warning('Please confirm you intend to make the following modifications:\n'); | ||
|
|
||
| // The security diff is formatted via `Formatter`, which takes in a stream | ||
| // and sends its output directly to that stream. To faciliate use of the | ||
| // global CliIoHost, we create our own stream to capture the output of | ||
| // `Formatter` and return the output as a string for the consumer of | ||
| // `formatSecurityDiff` to decide what to do with it. | ||
| const stream = new StringWriteStream(); | ||
| try { | ||
| // formatSecurityChanges updates the stream with the formatted security diff | ||
| formatSecurityChanges(stream, diff, buildLogicalToPathMap(newTemplate)); | ||
| } finally { | ||
| stream.end(); | ||
| } | ||
| // store the stream containing a formatted stack diff | ||
| const formattedDiff = stream.toString(); | ||
| return { formattedDiff }; | ||
| } | ||
| return {}; | ||
| } | ||
|
|
||
| /** | ||
| * Output of formatStackDiff | ||
| */ | ||
| export interface FormatStackDiffOutput { | ||
| /** | ||
| * Number of stacks with diff changes | ||
| */ | ||
| readonly numStacksWithChanges: number; | ||
|
|
||
| /** | ||
| * Complete formatted diff | ||
| */ | ||
| readonly formattedDiff: string; | ||
| } | ||
|
|
||
| /** | ||
| * Formats the differences between two template states and returns it as a string. | ||
| * | ||
| * @param oldTemplate the old/current state of the stack. | ||
| * @param newTemplate the new/target state of the stack. | ||
| * @param strict do not filter out AWS::CDK::Metadata or Rules | ||
| * @param context lines of context to use in arbitrary JSON diff | ||
| * @param quiet silences \'There were no differences\' messages | ||
| * | ||
| * @returns the formatted diff, and the number of stacks in this stack tree that have differences, including the top-level root stack | ||
| */ | ||
| export function formatStackDiff( | ||
| ioHelper: IoHelper, | ||
| oldTemplate: any, | ||
| newTemplate: cxapi.CloudFormationStackArtifact, | ||
| strict: boolean, | ||
| context: number, | ||
| quiet: boolean, | ||
| stackName?: string, | ||
| changeSet?: DescribeChangeSetOutput, | ||
| isImport?: boolean, | ||
| nestedStackTemplates?: { [nestedStackLogicalId: string]: NestedStackTemplates }): FormatStackDiffOutput { | ||
|
Comment on lines
+122
to
+132
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i agree that this sucks and should be part of a class with |
||
| const ioDefaultHelper = new IoDefaultMessages(ioHelper); | ||
|
|
||
| let diff = fullDiff(oldTemplate, newTemplate.template, changeSet, isImport); | ||
|
|
||
| // The stack diff is formatted via `Formatter`, which takes in a stream | ||
| // and sends its output directly to that stream. To faciliate use of the | ||
| // global CliIoHost, we create our own stream to capture the output of | ||
| // `Formatter` and return the output as a string for the consumer of | ||
| // `formatStackDiff` to decide what to do with it. | ||
| const stream = new StringWriteStream(); | ||
|
|
||
| let numStacksWithChanges = 0; | ||
| let formattedDiff = ''; | ||
| let filteredChangesCount = 0; | ||
| try { | ||
| // must output the stack name if there are differences, even if quiet | ||
| if (stackName && (!quiet || !diff.isEmpty)) { | ||
| stream.write(format('Stack %s\n', chalk.bold(stackName))); | ||
| } | ||
|
|
||
| if (!quiet && isImport) { | ||
| stream.write('Parameters and rules created during migration do not affect resource configuration.\n'); | ||
| } | ||
|
|
||
| // detect and filter out mangled characters from the diff | ||
| if (diff.differenceCount && !strict) { | ||
| const mangledNewTemplate = JSON.parse(mangleLikeCloudFormation(JSON.stringify(newTemplate.template))); | ||
| const mangledDiff = fullDiff(oldTemplate, mangledNewTemplate, changeSet); | ||
| filteredChangesCount = Math.max(0, diff.differenceCount - mangledDiff.differenceCount); | ||
| if (filteredChangesCount > 0) { | ||
| diff = mangledDiff; | ||
| } | ||
| } | ||
|
|
||
| // filter out 'AWS::CDK::Metadata' resources from the template | ||
| // filter out 'CheckBootstrapVersion' rules from the template | ||
| if (!strict) { | ||
| obscureDiff(diff); | ||
| } | ||
|
|
||
| if (!diff.isEmpty) { | ||
| numStacksWithChanges++; | ||
|
|
||
| // formatDifferences updates the stream with the formatted stack diff | ||
| formatDifferences(stream, diff, { | ||
| ...logicalIdMapFromTemplate(oldTemplate), | ||
| ...buildLogicalToPathMap(newTemplate), | ||
| }, context); | ||
|
|
||
| // store the stream containing a formatted stack diff | ||
| formattedDiff = stream.toString(); | ||
| } else if (!quiet) { | ||
| ioDefaultHelper.info(chalk.green('There were no differences')); | ||
| } | ||
| } finally { | ||
| stream.end(); | ||
| } | ||
|
|
||
| if (filteredChangesCount > 0) { | ||
| ioDefaultHelper.info(chalk.yellow(`Omitted ${filteredChangesCount} changes because they are likely mangled non-ASCII characters. Use --strict to print them.`)); | ||
| } | ||
|
|
||
| for (const nestedStackLogicalId of Object.keys(nestedStackTemplates ?? {})) { | ||
| if (!nestedStackTemplates) { | ||
| break; | ||
| } | ||
| const nestedStack = nestedStackTemplates[nestedStackLogicalId]; | ||
|
|
||
| (newTemplate as any)._template = nestedStack.generatedTemplate; | ||
| const nextDiff = formatStackDiff( | ||
| ioHelper, | ||
| nestedStack.deployedTemplate, | ||
| newTemplate, | ||
| strict, | ||
| context, | ||
| quiet, | ||
| nestedStack.physicalName ?? nestedStackLogicalId, | ||
| undefined, | ||
| isImport, | ||
| nestedStack.nestedStackTemplates, | ||
| ); | ||
| numStacksWithChanges += nextDiff.numStacksWithChanges; | ||
| formattedDiff += nextDiff.formattedDiff; | ||
| } | ||
|
|
||
| return { | ||
| numStacksWithChanges, | ||
| formattedDiff, | ||
| }; | ||
| } | ||
|
|
||
| /** | ||
| * Return whether the diff has security-impacting changes that need confirmation | ||
| * | ||
| * TODO: Filter the security impact determination based off of an enum that allows | ||
| * us to pick minimum "severities" to alert on. | ||
| */ | ||
| function diffRequiresApproval(diff: TemplateDiff, requireApproval: RequireApproval) { | ||
| switch (requireApproval) { | ||
| case RequireApproval.NEVER: return false; | ||
| case RequireApproval.ANY_CHANGE: return diff.permissionsAnyChanges; | ||
| case RequireApproval.BROADENING: return diff.permissionsBroadened; | ||
| default: throw new ToolkitError(`Unrecognized approval level: ${requireApproval}`); | ||
| } | ||
| } | ||
|
|
||
| export function buildLogicalToPathMap(stack: cxapi.CloudFormationStackArtifact) { | ||
| const map: { [id: string]: string } = {}; | ||
| for (const md of stack.findMetadataByType(cxschema.ArtifactMetadataEntryType.LOGICAL_ID)) { | ||
| map[md.data as string] = md.path; | ||
| } | ||
| return map; | ||
| } | ||
|
|
||
| export function logicalIdMapFromTemplate(template: any) { | ||
| const ret: Record<string, string> = {}; | ||
|
|
||
| for (const [logicalId, resource] of Object.entries(template.Resources ?? {})) { | ||
| const path = (resource as any)?.Metadata?.['aws:cdk:path']; | ||
| if (path) { | ||
| ret[logicalId] = path; | ||
| } | ||
| } | ||
| return ret; | ||
| } | ||
|
|
||
| /** | ||
| * Remove any template elements that we don't want to show users. | ||
| * This is currently: | ||
| * - AWS::CDK::Metadata resource | ||
| * - CheckBootstrapVersion Rule | ||
| */ | ||
| export function obscureDiff(diff: TemplateDiff) { | ||
| if (diff.unknown) { | ||
| // see https://github.com/aws/aws-cdk/issues/17942 | ||
| diff.unknown = diff.unknown.filter(change => { | ||
| if (!change) { | ||
| return true; | ||
| } | ||
| if (change.newValue?.CheckBootstrapVersion) { | ||
| return false; | ||
| } | ||
| if (change.oldValue?.CheckBootstrapVersion) { | ||
| return false; | ||
| } | ||
| return true; | ||
| }); | ||
| } | ||
|
|
||
| if (diff.resources) { | ||
| diff.resources = diff.resources.filter(change => { | ||
| if (!change) { | ||
| return true; | ||
| } | ||
| if (change.newResourceType === 'AWS::CDK::Metadata') { | ||
| return false; | ||
| } | ||
| if (change.oldResourceType === 'AWS::CDK::Metadata') { | ||
| return false; | ||
| } | ||
| return true; | ||
| }); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export * from './diff'; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,6 @@ | ||
| export * from './cloud-assembly'; | ||
| export * from './cloudformation'; | ||
| export * from './diff'; | ||
| export * from './io'; | ||
| export * from './toolkit-error'; | ||
| export * from './require-approval'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
elsewhere we are depending on
cloudformationDiff.customizeReference({ versionType: 'exact' }),and i'm not sure why. just thought i'd flag