Skip to content

Commit 05df617

Browse files
committed
refactor(cli): resource-import uses IoHost
1 parent c400e0e commit 05df617

File tree

9 files changed

+310
-75
lines changed

9 files changed

+310
-75
lines changed

packages/@aws-cdk/toolkit/lib/api/io/private/codes.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,15 @@ export const CODES = {
1515
// 2: List
1616
CDK_TOOLKIT_I2901: 'Provides details on the selected stacks and their dependencies',
1717

18+
// 3: Import & Migrate
19+
CDK_TOOLKIT_E3900: 'Resource import failed',
20+
1821
// 4: Diff
1922

20-
// 5: Deploy
23+
// 5: Deploy & Watch
2124
CDK_TOOLKIT_I5000: 'Provides deployment times',
2225
CDK_TOOLKIT_I5001: 'Provides total time in deploy action, including synth and rollback',
26+
CDK_TOOLKIT_I5002: 'Provides time for resource migration',
2327
CDK_TOOLKIT_I5031: 'Informs about any log groups that are traced as part of the deployment',
2428
CDK_TOOLKIT_I5050: 'Confirm rollback during deployment',
2529
CDK_TOOLKIT_I5060: 'Confirm deploy security sensitive changes',

packages/@aws-cdk/toolkit/lib/toolkit/toolkit.ts

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import { CachedCloudAssemblySource, IdentityCloudAssemblySource, StackAssembly,
1919
import { ALL_STACKS, CloudAssemblySourceBuilder } from '../api/cloud-assembly/private';
2020
import { ToolkitError } from '../api/errors';
2121
import { IIoHost, IoMessageCode, IoMessageLevel } from '../api/io';
22-
import { asSdkLogger, withAction, Timer, confirm, error, highlight, info, success, warn, ActionAwareIoHost, debug, result, withoutEmojis, withoutColor, withTrimmedWhitespace } from '../api/io/private';
22+
import { asSdkLogger, withAction, Timer, confirm, error, info, success, warn, ActionAwareIoHost, debug, result, withoutEmojis, withoutColor, withTrimmedWhitespace } from '../api/io/private';
2323

2424
/**
2525
* The current action being performed by the CLI. 'none' represents the absence of an action.
@@ -256,10 +256,8 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab
256256
}
257257

258258
const deployments = await this.deploymentsForAction('deploy');
259+
const migrator = new ResourceMigrator({ deployments });
259260

260-
const migrator = new ResourceMigrator({
261-
deployments,
262-
});
263261
await migrator.tryMigrateResources(stackCollection, options);
264262

265263
const requireApproval = options.requireApproval ?? RequireApproval.NEVER;
@@ -275,7 +273,6 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab
275273
}
276274

277275
const stacks = stackCollection.stackArtifacts;
278-
279276
const stackOutputs: { [key: string]: any } = {};
280277
const outputsFile = options.outputsFile;
281278

@@ -303,28 +300,31 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab
303300
const deployStack = async (stackNode: StackNode) => {
304301
const stack = stackNode.stack;
305302
if (stackCollection.stackCount !== 1) {
306-
await ioHost.notify(highlight(stack.displayName));
303+
await ioHost.notify(info(chalk.bold(stack.displayName)));
307304
}
308305

309306
if (!stack.environment) {
310-
// eslint-disable-next-line max-len
311307
throw new ToolkitError(
312308
`Stack ${stack.displayName} does not define an environment, and AWS credentials could not be obtained from standard locations or no region was configured.`,
313309
);
314310
}
315311

312+
// The generated stack has no resources
316313
if (Object.keys(stack.template.Resources || {}).length === 0) {
317-
// The generated stack has no resources
318-
if (!(await deployments.stackExists({ stack }))) {
319-
await ioHost.notify(warn(`${chalk.bold(stack.displayName)}: stack has no resources, skipping deployment.`));
320-
} else {
321-
await ioHost.notify(warn(`${chalk.bold(stack.displayName)}: stack has no resources, deleting existing stack.`));
322-
await this._destroy(assembly, 'deploy', {
323-
stacks: { patterns: [stack.hierarchicalId], strategy: StackSelectionStrategy.PATTERN_MUST_MATCH_SINGLE },
324-
roleArn: options.roleArn,
325-
ci: options.ci,
326-
});
314+
// stack is empty and doesn't exist => do nothing
315+
const stackExists = await deployments.stackExists({ stack });
316+
if (!stackExists) {
317+
return ioHost.notify(warn(`${chalk.bold(stack.displayName)}: stack has no resources, skipping deployment.`));
327318
}
319+
320+
// stack is empty, but exists => delete
321+
await ioHost.notify(warn(`${chalk.bold(stack.displayName)}: stack has no resources, deleting existing stack.`));
322+
await this._destroy(assembly, 'deploy', {
323+
stacks: { patterns: [stack.hierarchicalId], strategy: StackSelectionStrategy.PATTERN_MUST_MATCH_SINGLE },
324+
roleArn: options.roleArn,
325+
ci: options.ci,
326+
});
327+
328328
return;
329329
}
330330

packages/aws-cdk/lib/import.ts renamed to packages/aws-cdk/lib/api/resource-import/importer.ts

Lines changed: 85 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,71 @@
1-
import { DeployOptions } from '@aws-cdk/cloud-assembly-schema';
1+
import { format } from 'util';
22
import * as cfnDiff from '@aws-cdk/cloudformation-diff';
33
import { ResourceDifference } from '@aws-cdk/cloudformation-diff';
44
import * as cxapi from '@aws-cdk/cx-api';
55
import * as chalk from 'chalk';
66
import * as fs from 'fs-extra';
77
import * as promptly from 'promptly';
8-
import { assertIsSuccessfulDeployStackResult, Deployments, DeploymentMethod, ResourceIdentifierProperties, ResourcesToImport } from './api/deployments';
9-
import { Tag } from './api/tags';
10-
import { StackActivityProgress } from './api/util/cloudformation/stack-activity-monitor';
11-
import { error, info, success, warning } from './logging';
12-
import { ToolkitError } from './toolkit/error';
13-
14-
export interface ImportDeploymentOptions extends DeployOptions {
15-
deploymentMethod?: DeploymentMethod;
16-
progress?: StackActivityProgress;
17-
tags?: Tag[];
8+
import { error, info, warn } from '../../cli/messages';
9+
import { IIoHost, ToolkitAction } from '../../toolkit/cli-io-host';
10+
import { ToolkitError } from '../../toolkit/error';
11+
import { assertIsSuccessfulDeployStackResult, Deployments, DeploymentMethod, ResourceIdentifierProperties, ResourcesToImport } from '../deployments';
12+
import { Tag } from '../tags';
13+
import { StackActivityProgress } from '../util/cloudformation/stack-activity-monitor';
14+
15+
export interface ResourceImporterProps {
16+
deployments: Deployments;
17+
ioHost: IIoHost;
18+
action: ToolkitAction;
19+
}
20+
21+
export interface ImportDeploymentOptions {
22+
/**
23+
* Name of the toolkit stack to use/deploy
24+
*
25+
* @default CDKToolkit
26+
*/
27+
readonly toolkitStackName: string;
28+
29+
/**
30+
* Role to pass to CloudFormation for deployment
31+
*
32+
* @default - Default stack role
33+
*/
34+
readonly roleArn?: string;
35+
36+
/**
37+
* Deployment method
38+
*/
39+
readonly deploymentMethod?: DeploymentMethod;
40+
41+
/**
42+
* Stack tags (pass through to CloudFormation)
43+
*/
44+
readonly tags?: Tag[];
45+
46+
/**
47+
* Use previous values for unspecified parameters
48+
*
49+
* If not set, all parameters must be specified for every deployment.
50+
*
51+
* @default true
52+
*/
53+
readonly usePreviousParameters?: boolean;
54+
55+
/**
56+
* Display mode for stack deployment progress.
57+
*
58+
* @default - StackActivityProgress.Bar - stack events will be displayed for
59+
* the resource currently being deployed.
60+
*/
61+
readonly progress?: StackActivityProgress;
62+
63+
/**
64+
* Rollback failed deployments
65+
*
66+
* @default true
67+
*/
68+
readonly rollback?: boolean;
1869
}
1970

2071
/**
@@ -61,9 +112,20 @@ export type ResourceMap = { [logicalResource: string]: ResourceIdentifierPropert
61112
export class ResourceImporter {
62113
private _currentTemplate: any;
63114

115+
private readonly stack: cxapi.CloudFormationStackArtifact;
116+
private readonly cfn: Deployments;
117+
private readonly ioHost: IIoHost;
118+
private readonly action: ToolkitAction;
119+
64120
constructor(
65-
private readonly stack: cxapi.CloudFormationStackArtifact,
66-
private readonly cfn: Deployments) { }
121+
stack: cxapi.CloudFormationStackArtifact,
122+
props: ResourceImporterProps,
123+
) {
124+
this.stack = stack;
125+
this.cfn = props.deployments;
126+
this.ioHost = props.ioHost;
127+
this.action = props.action;
128+
}
67129

68130
/**
69131
* Ask the user for resources to import
@@ -96,19 +158,19 @@ export class ResourceImporter {
96158
const descr = this.describeResource(resource.logicalId);
97159
const idProps = contents[resource.logicalId];
98160
if (idProps) {
99-
info('%s: importing using %s', chalk.blue(descr), chalk.blue(fmtdict(idProps)));
161+
await this.ioHost.notify(info(this.action, format('%s: importing using %s', chalk.blue(descr), chalk.blue(fmtdict(idProps)))));
100162

101163
ret.importResources.push(resource);
102164
ret.resourceMap[resource.logicalId] = idProps;
103165
delete contents[resource.logicalId];
104166
} else {
105-
info('%s: skipping', chalk.blue(descr));
167+
await this.ioHost.notify(info(this.action, format('%s: skipping', chalk.blue(descr))));
106168
}
107169
}
108170

109171
const unknown = Object.keys(contents);
110172
if (unknown.length > 0) {
111-
warning(`Unrecognized resource identifiers in mapping file: ${unknown.join(', ')}`);
173+
await this.ioHost.notify(warn(this.action, `Unrecognized resource identifiers in mapping file: ${unknown.join(', ')}`));
112174
}
113175

114176
return ret;
@@ -158,9 +220,9 @@ export class ResourceImporter {
158220
? ' ✅ %s (no changes)'
159221
: ' ✅ %s';
160222

161-
success('\n' + message, this.stack.displayName);
223+
await this.ioHost.notify(info(this.action, '\n' + chalk.green(format(message, this.stack.displayName))));
162224
} catch (e) {
163-
error('\n ❌ %s failed: %s', chalk.bold(this.stack.displayName), e);
225+
await this.ioHost.notify(error(this.action, format('\n ❌ %s failed: %s', chalk.bold(this.stack.displayName), e), 'CDK_TOOLKIT_E3900'));
164226
throw e;
165227
}
166228
}
@@ -189,7 +251,7 @@ export class ResourceImporter {
189251
const offendingResources = nonAdditions.map(([logId, _]) => this.describeResource(logId));
190252

191253
if (allowNonAdditions) {
192-
warning(`Ignoring updated/deleted resources (--force): ${offendingResources.join(', ')}`);
254+
await this.ioHost.notify(warn(this.action, `Ignoring updated/deleted resources (--force): ${offendingResources.join(', ')}`));
193255
} else {
194256
throw new ToolkitError('No resource updates or deletes are allowed on import operation. Make sure to resolve pending changes ' +
195257
`to existing resources, before attempting an import. Updated/deleted resources: ${offendingResources.join(', ')} (--force to override)`);
@@ -277,7 +339,7 @@ export class ResourceImporter {
277339
// Skip resources that do not support importing
278340
const resourceType = chg.resourceDiff.newResourceType;
279341
if (resourceType === undefined || !(resourceType in resourceIdentifiers)) {
280-
warning(`${resourceName}: unsupported resource type ${resourceType}, skipping import.`);
342+
await this.ioHost.notify(warn(this.action, `${resourceName}: unsupported resource type ${resourceType}, skipping import.`));
281343
return undefined;
282344
}
283345

@@ -303,7 +365,7 @@ export class ResourceImporter {
303365

304366
// If we got here and the user rejected any available identifiers, then apparently they don't want the resource at all
305367
if (satisfiedPropSets.length > 0) {
306-
info(chalk.grey(`Skipping import of ${resourceName}`));
368+
await this.ioHost.notify(info(this.action, chalk.grey(`Skipping import of ${resourceName}`)));
307369
return undefined;
308370
}
309371

@@ -321,7 +383,7 @@ export class ResourceImporter {
321383

322384
// Do the input loop here
323385
if (preamble) {
324-
info(preamble);
386+
await this.ioHost.notify(info(this.action, preamble));
325387
}
326388
for (const idProps of idPropSets) {
327389
const input: Record<string, string> = {};
@@ -356,7 +418,7 @@ export class ResourceImporter {
356418
}
357419
}
358420

359-
info(chalk.grey(`Skipping import of ${resourceName}`));
421+
await this.ioHost.notify(info(this.action, chalk.grey(`Skipping import of ${resourceName}`)));
360422
return undefined;
361423
}
362424

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './importer';
2+
export * from './migrator';

packages/aws-cdk/lib/migrator.ts renamed to packages/aws-cdk/lib/api/resource-import/migrator.ts

Lines changed: 30 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,53 @@
11
import type * as cxapi from '@aws-cdk/cx-api';
22
import * as chalk from 'chalk';
33
import * as fs from 'fs-extra';
4-
import { StackCollection } from './api/cxapp/cloud-assembly';
5-
import { Deployments, ResourcesToImport } from './api/deployments';
6-
import { formatTime } from './api/util/string-manipulation';
7-
import { DeployOptions } from './cli/cdk-toolkit';
8-
import { ResourceImporter } from './import';
9-
import { info } from './logging';
4+
import { ImportDeploymentOptions, ResourceImporter } from './importer';
5+
import { info } from '../../cli/messages';
6+
import type { IIoHost, ToolkitAction } from '../../toolkit/cli-io-host';
7+
import { StackCollection } from '../cxapp/cloud-assembly';
8+
import { Deployments, ResourcesToImport } from '../deployments';
9+
import { formatTime } from '../util/string-manipulation';
1010

1111
export interface ResourceMigratorProps {
1212
deployments: Deployments;
13+
ioHost: IIoHost;
14+
action: ToolkitAction;
1315
}
1416

15-
type ResourceMigratorOptions = Pick<DeployOptions, 'roleArn' | 'toolkitStackName' | 'deploymentMethod' | 'progress' | 'rollback'>
16-
1717
export class ResourceMigrator {
18-
public constructor(private readonly props: ResourceMigratorProps) {}
18+
private readonly props: ResourceMigratorProps;
19+
private readonly ioHost: IIoHost;
20+
private readonly action: ToolkitAction;
21+
22+
public constructor(props: ResourceMigratorProps) {
23+
this.props = props;
24+
this.ioHost = props.ioHost;
25+
this.action = props.action;
26+
}
1927

2028
/**
2129
* Checks to see if a migrate.json file exists. If it does and the source is either `filepath` or
2230
* is in the same environment as the stack deployment, a new stack is created and the resources are
2331
* migrated to the stack using an IMPORT changeset. The normal deployment will resume after this is complete
2432
* to add back in any outputs and the CDKMetadata.
2533
*/
26-
public async tryMigrateResources(stacks: StackCollection, options: ResourceMigratorOptions): Promise<void> {
34+
public async tryMigrateResources(stacks: StackCollection, options: ImportDeploymentOptions): Promise<void> {
2735
const stack = stacks.stackArtifacts[0];
28-
const migrateDeployment = new ResourceImporter(stack, this.props.deployments);
36+
const migrateDeployment = new ResourceImporter(stack, {
37+
deployments: this.props.deployments,
38+
ioHost: this.ioHost,
39+
action: this.action,
40+
});
2941
const resourcesToImport = await this.tryGetResources(await migrateDeployment.resolveEnvironment());
3042

3143
if (resourcesToImport) {
32-
info('%s: creating stack for resource migration...', chalk.bold(stack.displayName));
33-
info('%s: importing resources into stack...', chalk.bold(stack.displayName));
44+
await this.ioHost.notify(info(this.action, `${chalk.bold(stack.displayName)}: creating stack for resource migration...`));
45+
await this.ioHost.notify(info(this.action, `${chalk.bold(stack.displayName)}: importing resources into stack...`));
3446

3547
await this.performResourceMigration(migrateDeployment, resourcesToImport, options);
3648

3749
fs.rmSync('migrate.json');
38-
info('%s: applying CDKMetadata and Outputs to stack (if applicable)...', chalk.bold(stack.displayName));
50+
await this.ioHost.notify(info(this.action, `${chalk.bold(stack.displayName)}: applying CDKMetadata and Outputs to stack (if applicable)...`));
3951
}
4052
}
4153

@@ -45,7 +57,7 @@ export class ResourceMigrator {
4557
private async performResourceMigration(
4658
migrateDeployment: ResourceImporter,
4759
resourcesToImport: ResourcesToImport,
48-
options: ResourceMigratorOptions,
60+
options: ImportDeploymentOptions,
4961
) {
5062
const startDeployTime = new Date().getTime();
5163
let elapsedDeployTime = 0;
@@ -61,7 +73,9 @@ export class ResourceMigrator {
6173
});
6274

6375
elapsedDeployTime = new Date().getTime() - startDeployTime;
64-
info('\n✨ Resource migration time: %ss\n', formatTime(elapsedDeployTime));
76+
await this.ioHost.notify(info(this.action, `'\n✨ Resource migration time: ${formatTime(elapsedDeployTime)}s\n'`, 'CDK_TOOLKIT_I5002', {
77+
duration: elapsedDeployTime,
78+
}));
6579
}
6680

6781
public async tryGetResources(environment: cxapi.Environment): Promise<ResourcesToImport | undefined> {

0 commit comments

Comments
 (0)