Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .projenrc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -681,11 +681,14 @@ const tmpToolkitHelpers = configureProject(
deps: [
cloudAssemblySchema.name,
cloudFormationDiff,
cxApi,
`@aws-sdk/client-cloudformation@${CLI_SDK_V3_RANGE}`,
'archiver',
'chalk@4',
'glob',
'semver',
'uuid',
'wrap-ansi@^7', // Last non-ESM version
'yaml@^1',
],
tsconfig: {
Expand Down
14 changes: 14 additions & 0 deletions packages/@aws-cdk/tmp-toolkit-helpers/.projen/deps.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/@aws-cdk/tmp-toolkit-helpers/.projen/tasks.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions packages/@aws-cdk/tmp-toolkit-helpers/package.json

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
@@ -1,8 +1,8 @@
import type { CloudFormationStackArtifact } from '@aws-cdk/cx-api';
import { type StackActivity, type StackProgress } from '../../../../@aws-cdk/tmp-toolkit-helpers/src/api';
import { IO } from '../../../../@aws-cdk/tmp-toolkit-helpers/src/api/io/private';
import type { IoMessage } from '../../api/io';
import { type StackActivity, type StackProgress } from '../../api/io/payloads';
import { IO } from '../../api/io/private';
import { maxResourceTypeLength, stackEventHasErrorMessage } from '../../util';
import type { IoMessage } from '../io-host/cli-io-host';

export interface IActivityPrinter {
notify(msg: IoMessage<unknown>): void;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import * as util from 'util';
import type { StackActivity } from '@aws-cdk/tmp-toolkit-helpers';
import * as chalk from 'chalk';
import type { ActivityPrinterProps } from './base';
import { ActivityPrinterBase } from './base';
import { RewritableBlock } from './display';
import type { StackActivity } from '../../api/io/payloads';
import { padLeft, padRight, stackEventHasErrorMessage } from '../../util';

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import * as util from 'util';
import type { StackActivity } from '@aws-cdk/tmp-toolkit-helpers';
import * as chalk from 'chalk';
import type { ActivityPrinterProps } from './base';
import { ActivityPrinterBase } from './base';
import type { StackActivity } from '../../api/io/payloads';
import { padRight } from '../../util';

/**
Expand Down
1 change: 1 addition & 0 deletions packages/@aws-cdk/tmp-toolkit-helpers/src/private/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './activity-printer';
165 changes: 165 additions & 0 deletions packages/@aws-cdk/tmp-toolkit-helpers/test/_helpers/assembly.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
import * as fs from 'fs';
import * as path from 'path';
import { ArtifactMetadataEntryType, ArtifactType, type AssetManifest, type AssetMetadataEntry, type AwsCloudFormationStackProperties, type MetadataEntry, type MissingContext } from '@aws-cdk/cloud-assembly-schema';
import { CloudAssembly, CloudAssemblyBuilder, type CloudFormationStackArtifact, type StackMetadata } from '@aws-cdk/cx-api';

export const DEFAULT_FAKE_TEMPLATE = { No: 'Resources' };

const SOME_RECENT_SCHEMA_VERSION = '30.0.0';

export interface TestStackArtifact {
stackName: string;
template?: any;
env?: string;
depends?: string[];
metadata?: StackMetadata;
notificationArns?: string[];

/** Old-style assets */
assets?: AssetMetadataEntry[];
properties?: Partial<AwsCloudFormationStackProperties>;
terminationProtection?: boolean;
displayName?: string;

/** New-style assets */
assetManifest?: AssetManifest;
}

export interface TestAssembly {
stacks: TestStackArtifact[];
missing?: MissingContext[];
nestedAssemblies?: TestAssembly[];
schemaVersion?: string;
}

function clone(obj: any) {
return JSON.parse(JSON.stringify(obj));
}

function addAttributes(assembly: TestAssembly, builder: CloudAssemblyBuilder) {
for (const stack of assembly.stacks) {
const templateFile = `${stack.stackName}.template.json`;
const template = stack.template ?? DEFAULT_FAKE_TEMPLATE;
fs.writeFileSync(path.join(builder.outdir, templateFile), JSON.stringify(template, undefined, 2));
addNestedStacks(templateFile, builder.outdir, template);

// we call patchStackTags here to simulate the tags formatter
// that is used when building real manifest files.
const metadata: { [path: string]: MetadataEntry[] } = patchStackTags({ ...stack.metadata });
for (const asset of stack.assets || []) {
metadata[asset.id] = [{ type: ArtifactMetadataEntryType.ASSET, data: asset }];
}

for (const missing of assembly.missing || []) {
builder.addMissing(missing);
}

const dependencies = [...(stack.depends ?? [])];

if (stack.assetManifest) {
const manifestFile = `${stack.stackName}.assets.json`;
fs.writeFileSync(path.join(builder.outdir, manifestFile), JSON.stringify(stack.assetManifest, undefined, 2));
dependencies.push(`${stack.stackName}.assets`);
builder.addArtifact(`${stack.stackName}.assets`, {
type: ArtifactType.ASSET_MANIFEST,
environment: stack.env || 'aws://123456789012/here',
properties: {
file: manifestFile,
},
});
}

builder.addArtifact(stack.stackName, {
type: ArtifactType.AWS_CLOUDFORMATION_STACK,
environment: stack.env || 'aws://123456789012/here',

dependencies,
metadata,
properties: {
...stack.properties,
templateFile,
terminationProtection: stack.terminationProtection,
notificationArns: stack.notificationArns,
},
displayName: stack.displayName,
});
}
}

function addNestedStacks(templatePath: string, outdir: string, rootStackTemplate?: any) {
let template = rootStackTemplate;

if (!template) {
const templatePathWithDir = path.join('nested-stack-templates', templatePath);
template = JSON.parse(fs.readFileSync(path.join(__dirname, '..', templatePathWithDir)).toString());
fs.writeFileSync(path.join(outdir, templatePath), JSON.stringify(template, undefined, 2));
}

for (const logicalId of Object.keys(template.Resources ?? {})) {
if (template.Resources[logicalId].Type === 'AWS::CloudFormation::Stack') {
if (template.Resources[logicalId].Metadata && template.Resources[logicalId].Metadata['aws:asset:path']) {
const nestedTemplatePath = template.Resources[logicalId].Metadata['aws:asset:path'];
addNestedStacks(nestedTemplatePath, outdir);
}
}
}
}

function rewriteManifestVersion(directory: string, version: string) {
const manifestFile = `${directory}/manifest.json`;
const contents = JSON.parse(fs.readFileSync(`${directory}/manifest.json`, 'utf-8'));
contents.version = version;
fs.writeFileSync(manifestFile, JSON.stringify(contents, undefined, 2));
}

function cxapiAssemblyWithForcedVersion(asm: CloudAssembly, version: string) {
rewriteManifestVersion(asm.directory, version);
return new CloudAssembly(asm.directory, { skipVersionCheck: true });
}

export function testAssembly(assembly: TestAssembly): CloudAssembly {
const builder = new CloudAssemblyBuilder();
addAttributes(assembly, builder);

if (assembly.nestedAssemblies != null && assembly.nestedAssemblies.length > 0) {
assembly.nestedAssemblies?.forEach((nestedAssembly: TestAssembly, i: number) => {
const nestedAssemblyBuilder = builder.createNestedAssembly(`nested${i}`, `nested${i}`);
addAttributes(nestedAssembly, nestedAssemblyBuilder);
nestedAssemblyBuilder.buildAssembly();
});
}

const asm = builder.buildAssembly();
return cxapiAssemblyWithForcedVersion(asm, assembly.schemaVersion ?? SOME_RECENT_SCHEMA_VERSION);
}

/**
* Transform stack tags from how they are decalred in source code (lower cased)
* to how they are stored on disk (upper cased). In real synthesis this is done
* by a special tags formatter.
*
* @see aws-cdk-lib/lib/stack.ts
*/
function patchStackTags(metadata: { [path: string]: MetadataEntry[] }): {
[path: string]: MetadataEntry[];
} {
const cloned = clone(metadata) as { [path: string]: MetadataEntry[] };

for (const metadataEntries of Object.values(cloned)) {
for (const metadataEntry of metadataEntries) {
if (metadataEntry.type === ArtifactMetadataEntryType.STACK_TAGS && metadataEntry.data) {
const metadataAny = metadataEntry as any;

metadataAny.data = metadataAny.data.map((t: any) => {
return { Key: t.key, Value: t.value };
});
}
}
}
return cloned;
}

export function testStack(stack: TestStackArtifact): CloudFormationStackArtifact {
const assembly = testAssembly({ stacks: [stack] });
return assembly.getStackByName(stack.stackName);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { EventEmitter } from 'events';

export type Output = ReadonlyArray<string>;

export interface Options {
isTTY?: boolean;
}

export interface Inspector {
output: Output;
restore: () => void;
}

class ConsoleListener {
private _stream: NodeJS.WriteStream;
private _options?: Options;

constructor(stream: NodeJS.WriteStream, options?: Options) {
this._stream = stream;
this._options = options;
}

inspect(): Inspector {
let isTTY;
if (this._options && this._options.isTTY !== undefined) {
isTTY = this._options.isTTY;
}

const output: string[] = [];
const stream = this._stream;
const res: EventEmitter & Partial<Inspector> = new EventEmitter();

// eslint-disable-next-line @typescript-eslint/unbound-method
const originalWrite = stream.write;
stream.write = (string: string) => {
output.push(string);
return res.emit('data', string);
};

const originalIsTTY = stream.isTTY;
if (isTTY === true) {
stream.isTTY = isTTY;
}

res.output = output;
res.restore = () => {
stream.write = originalWrite;
stream.isTTY = originalIsTTY;
};
return (res as Inspector);
}

inspectSync(fn: (output: Output) => void): Output {
const inspect = this.inspect();
try {
fn(inspect.output);
} finally {
inspect.restore();
}
return inspect.output;
}
}

export const stdout = new ConsoleListener(process.stdout);
export const stderr = new ConsoleListener(process.stderr);
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
/* eslint-disable import/order */
import { RewritableBlock } from '../../../lib/cli/activity-printer/display';
import { RewritableBlock } from '../../src/private/activity-printer/display';
import { stderr } from '../_helpers/console-listener';

describe('Rewritable Block Tests', () => {
Expand Down
Loading