Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
Loading