Skip to content

Commit e0f9a6f

Browse files
authored
feat(toolkit-lib): default to using a non interactive IoHost (#306)
Replaces the current default `IoHost` which is the same as the one used by the CLI with a strictly non-interactive version. This is more in accordance with the goals of a library providing actions. Fixes #157 Closes #289 BREAKING CHANGE: This change updates the default `IoHost` implementation used by `Toolkit` to a version that is strictly non-interactive, i.e. there is no expectation anymore that users will respond to command-line prompts. To restore previous behavior, you will can provide a custom `IoHost` implementation to your `Toolkit` instance. You may consider extending the new `NonInteractiveIoHost` class with desired interactive prompts. --- By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license
1 parent 73e5ad4 commit e0f9a6f

File tree

22 files changed

+486
-48
lines changed

22 files changed

+486
-48
lines changed

Diff for: .projenrc.ts

+3
Original file line numberDiff line numberDiff line change
@@ -681,11 +681,14 @@ const tmpToolkitHelpers = configureProject(
681681
deps: [
682682
cloudAssemblySchema.name,
683683
cloudFormationDiff,
684+
cxApi,
685+
`@aws-sdk/client-cloudformation@${CLI_SDK_V3_RANGE}`,
684686
'archiver',
685687
'chalk@4',
686688
'glob',
687689
'semver',
688690
'uuid',
691+
'wrap-ansi@^7', // Last non-ESM version
689692
'yaml@^1',
690693
],
691694
tsconfig: {

Diff for: packages/@aws-cdk/tmp-toolkit-helpers/.projen/deps.json

+14
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: packages/@aws-cdk/tmp-toolkit-helpers/.projen/tasks.json

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: packages/@aws-cdk/tmp-toolkit-helpers/package.json

+3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: packages/aws-cdk/lib/cli/activity-printer/base.ts renamed to packages/@aws-cdk/tmp-toolkit-helpers/src/private/activity-printer/base.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import type { CloudFormationStackArtifact } from '@aws-cdk/cx-api';
2-
import { type StackActivity, type StackProgress } from '../../../../@aws-cdk/tmp-toolkit-helpers/src/api';
3-
import { IO } from '../../../../@aws-cdk/tmp-toolkit-helpers/src/api/io/private';
2+
import type { IoMessage } from '../../api/io';
3+
import { type StackActivity, type StackProgress } from '../../api/io/payloads';
4+
import { IO } from '../../api/io/private';
45
import { maxResourceTypeLength, stackEventHasErrorMessage } from '../../util';
5-
import type { IoMessage } from '../io-host/cli-io-host';
66

77
export interface IActivityPrinter {
88
notify(msg: IoMessage<unknown>): void;

Diff for: packages/aws-cdk/lib/cli/activity-printer/current.ts renamed to packages/@aws-cdk/tmp-toolkit-helpers/src/private/activity-printer/current.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import * as util from 'util';
2-
import type { StackActivity } from '@aws-cdk/tmp-toolkit-helpers';
32
import * as chalk from 'chalk';
43
import type { ActivityPrinterProps } from './base';
54
import { ActivityPrinterBase } from './base';
65
import { RewritableBlock } from './display';
6+
import type { StackActivity } from '../../api/io/payloads';
77
import { padLeft, padRight, stackEventHasErrorMessage } from '../../util';
88

99
/**

Diff for: packages/aws-cdk/lib/cli/activity-printer/history.ts renamed to packages/@aws-cdk/tmp-toolkit-helpers/src/private/activity-printer/history.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import * as util from 'util';
2-
import type { StackActivity } from '@aws-cdk/tmp-toolkit-helpers';
32
import * as chalk from 'chalk';
43
import type { ActivityPrinterProps } from './base';
54
import { ActivityPrinterBase } from './base';
5+
import type { StackActivity } from '../../api/io/payloads';
66
import { padRight } from '../../util';
77

88
/**
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './activity-printer';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
import * as fs from 'fs';
2+
import * as path from 'path';
3+
import { ArtifactMetadataEntryType, ArtifactType, type AssetManifest, type AssetMetadataEntry, type AwsCloudFormationStackProperties, type MetadataEntry, type MissingContext } from '@aws-cdk/cloud-assembly-schema';
4+
import { CloudAssembly, CloudAssemblyBuilder, type CloudFormationStackArtifact, type StackMetadata } from '@aws-cdk/cx-api';
5+
6+
export const DEFAULT_FAKE_TEMPLATE = { No: 'Resources' };
7+
8+
const SOME_RECENT_SCHEMA_VERSION = '30.0.0';
9+
10+
export interface TestStackArtifact {
11+
stackName: string;
12+
template?: any;
13+
env?: string;
14+
depends?: string[];
15+
metadata?: StackMetadata;
16+
notificationArns?: string[];
17+
18+
/** Old-style assets */
19+
assets?: AssetMetadataEntry[];
20+
properties?: Partial<AwsCloudFormationStackProperties>;
21+
terminationProtection?: boolean;
22+
displayName?: string;
23+
24+
/** New-style assets */
25+
assetManifest?: AssetManifest;
26+
}
27+
28+
export interface TestAssembly {
29+
stacks: TestStackArtifact[];
30+
missing?: MissingContext[];
31+
nestedAssemblies?: TestAssembly[];
32+
schemaVersion?: string;
33+
}
34+
35+
function clone(obj: any) {
36+
return JSON.parse(JSON.stringify(obj));
37+
}
38+
39+
function addAttributes(assembly: TestAssembly, builder: CloudAssemblyBuilder) {
40+
for (const stack of assembly.stacks) {
41+
const templateFile = `${stack.stackName}.template.json`;
42+
const template = stack.template ?? DEFAULT_FAKE_TEMPLATE;
43+
fs.writeFileSync(path.join(builder.outdir, templateFile), JSON.stringify(template, undefined, 2));
44+
addNestedStacks(templateFile, builder.outdir, template);
45+
46+
// we call patchStackTags here to simulate the tags formatter
47+
// that is used when building real manifest files.
48+
const metadata: { [path: string]: MetadataEntry[] } = patchStackTags({ ...stack.metadata });
49+
for (const asset of stack.assets || []) {
50+
metadata[asset.id] = [{ type: ArtifactMetadataEntryType.ASSET, data: asset }];
51+
}
52+
53+
for (const missing of assembly.missing || []) {
54+
builder.addMissing(missing);
55+
}
56+
57+
const dependencies = [...(stack.depends ?? [])];
58+
59+
if (stack.assetManifest) {
60+
const manifestFile = `${stack.stackName}.assets.json`;
61+
fs.writeFileSync(path.join(builder.outdir, manifestFile), JSON.stringify(stack.assetManifest, undefined, 2));
62+
dependencies.push(`${stack.stackName}.assets`);
63+
builder.addArtifact(`${stack.stackName}.assets`, {
64+
type: ArtifactType.ASSET_MANIFEST,
65+
environment: stack.env || 'aws://123456789012/here',
66+
properties: {
67+
file: manifestFile,
68+
},
69+
});
70+
}
71+
72+
builder.addArtifact(stack.stackName, {
73+
type: ArtifactType.AWS_CLOUDFORMATION_STACK,
74+
environment: stack.env || 'aws://123456789012/here',
75+
76+
dependencies,
77+
metadata,
78+
properties: {
79+
...stack.properties,
80+
templateFile,
81+
terminationProtection: stack.terminationProtection,
82+
notificationArns: stack.notificationArns,
83+
},
84+
displayName: stack.displayName,
85+
});
86+
}
87+
}
88+
89+
function addNestedStacks(templatePath: string, outdir: string, rootStackTemplate?: any) {
90+
let template = rootStackTemplate;
91+
92+
if (!template) {
93+
const templatePathWithDir = path.join('nested-stack-templates', templatePath);
94+
template = JSON.parse(fs.readFileSync(path.join(__dirname, '..', templatePathWithDir)).toString());
95+
fs.writeFileSync(path.join(outdir, templatePath), JSON.stringify(template, undefined, 2));
96+
}
97+
98+
for (const logicalId of Object.keys(template.Resources ?? {})) {
99+
if (template.Resources[logicalId].Type === 'AWS::CloudFormation::Stack') {
100+
if (template.Resources[logicalId].Metadata && template.Resources[logicalId].Metadata['aws:asset:path']) {
101+
const nestedTemplatePath = template.Resources[logicalId].Metadata['aws:asset:path'];
102+
addNestedStacks(nestedTemplatePath, outdir);
103+
}
104+
}
105+
}
106+
}
107+
108+
function rewriteManifestVersion(directory: string, version: string) {
109+
const manifestFile = `${directory}/manifest.json`;
110+
const contents = JSON.parse(fs.readFileSync(`${directory}/manifest.json`, 'utf-8'));
111+
contents.version = version;
112+
fs.writeFileSync(manifestFile, JSON.stringify(contents, undefined, 2));
113+
}
114+
115+
function cxapiAssemblyWithForcedVersion(asm: CloudAssembly, version: string) {
116+
rewriteManifestVersion(asm.directory, version);
117+
return new CloudAssembly(asm.directory, { skipVersionCheck: true });
118+
}
119+
120+
export function testAssembly(assembly: TestAssembly): CloudAssembly {
121+
const builder = new CloudAssemblyBuilder();
122+
addAttributes(assembly, builder);
123+
124+
if (assembly.nestedAssemblies != null && assembly.nestedAssemblies.length > 0) {
125+
assembly.nestedAssemblies?.forEach((nestedAssembly: TestAssembly, i: number) => {
126+
const nestedAssemblyBuilder = builder.createNestedAssembly(`nested${i}`, `nested${i}`);
127+
addAttributes(nestedAssembly, nestedAssemblyBuilder);
128+
nestedAssemblyBuilder.buildAssembly();
129+
});
130+
}
131+
132+
const asm = builder.buildAssembly();
133+
return cxapiAssemblyWithForcedVersion(asm, assembly.schemaVersion ?? SOME_RECENT_SCHEMA_VERSION);
134+
}
135+
136+
/**
137+
* Transform stack tags from how they are decalred in source code (lower cased)
138+
* to how they are stored on disk (upper cased). In real synthesis this is done
139+
* by a special tags formatter.
140+
*
141+
* @see aws-cdk-lib/lib/stack.ts
142+
*/
143+
function patchStackTags(metadata: { [path: string]: MetadataEntry[] }): {
144+
[path: string]: MetadataEntry[];
145+
} {
146+
const cloned = clone(metadata) as { [path: string]: MetadataEntry[] };
147+
148+
for (const metadataEntries of Object.values(cloned)) {
149+
for (const metadataEntry of metadataEntries) {
150+
if (metadataEntry.type === ArtifactMetadataEntryType.STACK_TAGS && metadataEntry.data) {
151+
const metadataAny = metadataEntry as any;
152+
153+
metadataAny.data = metadataAny.data.map((t: any) => {
154+
return { Key: t.key, Value: t.value };
155+
});
156+
}
157+
}
158+
}
159+
return cloned;
160+
}
161+
162+
export function testStack(stack: TestStackArtifact): CloudFormationStackArtifact {
163+
const assembly = testAssembly({ stacks: [stack] });
164+
return assembly.getStackByName(stack.stackName);
165+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { EventEmitter } from 'events';
2+
3+
export type Output = ReadonlyArray<string>;
4+
5+
export interface Options {
6+
isTTY?: boolean;
7+
}
8+
9+
export interface Inspector {
10+
output: Output;
11+
restore: () => void;
12+
}
13+
14+
class ConsoleListener {
15+
private _stream: NodeJS.WriteStream;
16+
private _options?: Options;
17+
18+
constructor(stream: NodeJS.WriteStream, options?: Options) {
19+
this._stream = stream;
20+
this._options = options;
21+
}
22+
23+
inspect(): Inspector {
24+
let isTTY;
25+
if (this._options && this._options.isTTY !== undefined) {
26+
isTTY = this._options.isTTY;
27+
}
28+
29+
const output: string[] = [];
30+
const stream = this._stream;
31+
const res: EventEmitter & Partial<Inspector> = new EventEmitter();
32+
33+
// eslint-disable-next-line @typescript-eslint/unbound-method
34+
const originalWrite = stream.write;
35+
stream.write = (string: string) => {
36+
output.push(string);
37+
return res.emit('data', string);
38+
};
39+
40+
const originalIsTTY = stream.isTTY;
41+
if (isTTY === true) {
42+
stream.isTTY = isTTY;
43+
}
44+
45+
res.output = output;
46+
res.restore = () => {
47+
stream.write = originalWrite;
48+
stream.isTTY = originalIsTTY;
49+
};
50+
return (res as Inspector);
51+
}
52+
53+
inspectSync(fn: (output: Output) => void): Output {
54+
const inspect = this.inspect();
55+
try {
56+
fn(inspect.output);
57+
} finally {
58+
inspect.restore();
59+
}
60+
return inspect.output;
61+
}
62+
}
63+
64+
export const stdout = new ConsoleListener(process.stdout);
65+
export const stderr = new ConsoleListener(process.stderr);

Diff for: packages/aws-cdk/test/cli/activity-monitor/display.test.ts renamed to packages/@aws-cdk/tmp-toolkit-helpers/test/activity-monitor/display.test.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
/* eslint-disable import/order */
2-
import { RewritableBlock } from '../../../lib/cli/activity-printer/display';
1+
import { RewritableBlock } from '../../src/private/activity-printer/display';
32
import { stderr } from '../_helpers/console-listener';
43

54
describe('Rewritable Block Tests', () => {

0 commit comments

Comments
 (0)