Skip to content

Commit 3c124b9

Browse files
authored
Merge pull request #14181 from aws-amplify/gen2-migrations-execute
fix: custom resource error handling, formatting messages
2 parents d071100 + 199b138 commit 3c124b9

4 files changed

Lines changed: 122 additions & 44 deletions

File tree

packages/amplify-migration-template-gen/src/migration-readme-generator.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,13 @@ s3Bucket.bucketName = YOUR_GEN1_BUCKET_NAME;
3131
`## REDEPLOY GEN2 APPLICATION
3232
1.a) Uncomment the following lines in \`amplify/backend.ts\` file
3333
${this.categories.includes('storage') ? s3BucketChanges : ''}
34-
\`\`\`
34+
${
35+
this.categories.includes('auth')
36+
? `\`\`\`
3537
backend.auth.resources.userPool.node.tryRemoveChild('UserPoolDomain');
36-
\`\`\`
38+
\`\`\``
39+
: ''
40+
}
3741
3842
\`\`\`
3943
Tags.of(backend.stack).add("gen1-migrated-app", "true");

packages/amplify-migration-template-gen/src/template-generator.ts

Lines changed: 36 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@ const CATEGORIES: CATEGORY[] = ['auth', 'storage'];
3939
const TEMPLATES_DIR = '.amplify/migration/templates';
4040
const SEPARATOR = ' to ';
4141

42-
const SOURCE_TO_DESTINATION_STACKS = [`Gen1`, `Gen2`];
42+
const GEN1 = 'Gen 1';
43+
const GEN2 = 'Gen 2';
4344
const AUTH_RESOURCES_TO_REFACTOR = [
4445
CFN_AUTH_TYPE.UserPool,
4546
CFN_AUTH_TYPE.UserPoolClient,
@@ -247,6 +248,10 @@ class TemplateGenerator {
247248
);
248249
}
249250

251+
private getStackCategoryName(category: string) {
252+
return !this.isCustomResource(category) ? category : 'custom';
253+
}
254+
250255
private async processGen1Stack(
251256
category: string,
252257
categoryTemplateGenerator: CategoryTemplateGenerator<CFN_CATEGORY_TYPE>,
@@ -257,17 +262,19 @@ class TemplateGenerator {
257262
const { newTemplate, parameters: gen1StackParameters } = await categoryTemplateGenerator.generateGen1PreProcessTemplate();
258263

259264
assert(gen1StackParameters);
260-
updatingGen1CategoryStack = ora(`Updating Gen1 ${category} stack...`).start();
265+
updatingGen1CategoryStack = ora(`Updating Gen 1 ${this.getStackCategoryName(category)} stack...`).start();
261266

262267
const gen1StackUpdateStatus = await tryUpdateStack(this.cfnClient, sourceCategoryStackId, gen1StackParameters, newTemplate);
263268

264269
assert(gen1StackUpdateStatus === CFNStackStatus.UPDATE_COMPLETE);
265-
updatingGen1CategoryStack.succeed(`Updated Gen1 ${category} stack successfully`);
270+
updatingGen1CategoryStack.succeed(`Updated Gen 1 ${this.getStackCategoryName(category)} stack successfully`);
266271

267272
return newTemplate;
268273
} catch (e) {
269274
if (this.isNoResourcesError(e)) {
270-
updatingGen1CategoryStack?.succeed(`No resources found to move in Gen1 ${category} stack. Skipping update.`);
275+
updatingGen1CategoryStack?.succeed(
276+
`No resources found to move in Gen 1 ${this.getStackCategoryName(category)} stack. Skipping update.`,
277+
);
271278
return undefined;
272279
}
273280
throw e;
@@ -286,12 +293,12 @@ class TemplateGenerator {
286293
try {
287294
const { newTemplate, oldTemplate, parameters } = await categoryTemplateGenerator.generateGen2ResourceRemovalTemplate();
288295

289-
const updatingGen2CategoryStack = ora(`Updating Gen2 ${category} stack...`).start();
296+
const updatingGen2CategoryStack = ora(`Updating Gen 2 ${this.getStackCategoryName(category)} stack...`).start();
290297

291298
const gen2StackUpdateStatus = await tryUpdateStack(this.cfnClient, destinationCategoryStackId, parameters ?? [], newTemplate);
292299

293300
assert(gen2StackUpdateStatus === CFNStackStatus.UPDATE_COMPLETE);
294-
updatingGen2CategoryStack.succeed(`Updated Gen2 ${category} stack successfully`);
301+
updatingGen2CategoryStack.succeed(`Updated Gen 2 ${this.getStackCategoryName(category)} stack successfully`);
295302

296303
return { newTemplate, oldTemplate, parameters };
297304
} catch (e) {
@@ -361,6 +368,12 @@ class TemplateGenerator {
361368
);
362369
}
363370

371+
private isCustomResource(category: string) {
372+
return !Object.values(NON_CUSTOM_RESOURCE_CATEGORY)
373+
.map((nonCustomCategory) => nonCustomCategory.valueOf())
374+
.includes(category);
375+
}
376+
364377
private async generateCategoryTemplates(isRevert = false, customResourceMap?: ResourceMapping[]) {
365378
this.initializeCategoryGenerators(customResourceMap);
366379
for (const [category, sourceCategoryStackId, destinationCategoryStackId, categoryTemplateGenerator] of this
@@ -373,7 +386,7 @@ class TemplateGenerator {
373386
let destinationTemplateForRefactor: CFNTemplate | undefined;
374387
let logicalIdMappingForRefactor: Map<string, string> | undefined;
375388

376-
if (customResourceMap && !Object.values(NON_CUSTOM_RESOURCE_CATEGORY).includes(category as NON_CUSTOM_RESOURCE_CATEGORY)) {
389+
if (customResourceMap && this.isCustomResource(category)) {
377390
newSourceTemplate = await this.processGen1Stack(category, categoryTemplateGenerator, sourceCategoryStackId);
378391
if (!newSourceTemplate) continue;
379392
const { newTemplate } = await this.processGen2Stack(category, categoryTemplateGenerator, destinationCategoryStackId);
@@ -447,7 +460,9 @@ class TemplateGenerator {
447460
assert(newSourceTemplate);
448461
assert(newDestinationTemplate);
449462

450-
const refactorResources = ora(`Moving ${category} resources from ${this.getSourceToDestinationMessage(isRevert)} stack...`).start();
463+
const refactorResources = ora(
464+
`Moving ${this.getStackCategoryName(category)} resources from ${this.getSourceToDestinationMessage(isRevert)} stack...`,
465+
).start();
451466
const { success, failedRefactorMetadata } = await this.refactorResources(
452467
logicalIdMappingForRefactor,
453468
sourceCategoryStackId,
@@ -459,17 +474,21 @@ class TemplateGenerator {
459474
);
460475
if (!success) {
461476
refactorResources.fail(
462-
`Moving ${category} resources from ${this.getSourceToDestinationMessage(isRevert)} stack failed. Reason: ${
463-
failedRefactorMetadata?.reason
464-
}. Status: ${failedRefactorMetadata?.status}. RefactorId: ${failedRefactorMetadata?.stackRefactorId}.`,
477+
`Moving ${this.getStackCategoryName(category)} resources from ${this.getSourceToDestinationMessage(
478+
isRevert,
479+
)} stack failed. Reason: ${failedRefactorMetadata?.reason}. Status: ${failedRefactorMetadata?.status}. RefactorId: ${
480+
failedRefactorMetadata?.stackRefactorId
481+
}.`,
465482
);
466483
await pollStackForCompletionState(this.cfnClient, destinationCategoryStackId, 30);
467484
if (!isRevert && oldDestinationTemplate) {
468485
await this.rollbackGen2Stack(category, destinationCategoryStackId, destinationStackParameters, oldDestinationTemplate);
469486
}
470487
return false;
471488
} else {
472-
refactorResources.succeed(`Moved ${category} resources from ${this.getSourceToDestinationMessage(isRevert)} stack successfully`);
489+
refactorResources.succeed(
490+
`Moved ${this.getStackCategoryName(category)} resources from ${this.getSourceToDestinationMessage(isRevert)} stack successfully`,
491+
);
473492
}
474493
}
475494
if (!isRevert) {
@@ -527,10 +546,10 @@ class TemplateGenerator {
527546
gen2StackParameters: Parameter[] | undefined,
528547
oldGen2Template: CFNTemplate,
529548
) {
530-
const rollingBackGen2Stack = ora(`Rolling back Gen2 ${category} stack...`).start();
549+
const rollingBackGen2Stack = ora(`Rolling back Gen 2 ${this.getStackCategoryName(category)} stack...`).start();
531550
const gen2StackUpdateStatus = await tryUpdateStack(this.cfnClient, gen2CategoryStackId, gen2StackParameters ?? [], oldGen2Template);
532-
assert(gen2StackUpdateStatus === CFNStackStatus.UPDATE_COMPLETE, `Gen2 Stack in a failed state: ${gen2StackUpdateStatus}.`);
533-
rollingBackGen2Stack.succeed(`Rolled back Gen2 ${category} stack successfully`);
551+
assert(gen2StackUpdateStatus === CFNStackStatus.UPDATE_COMPLETE, `Gen 2 Stack is in a failed state: ${gen2StackUpdateStatus}.`);
552+
rollingBackGen2Stack.succeed(`Rolled back Gen 2 ${this.getStackCategoryName(category)} stack successfully`);
534553
}
535554

536555
private async generateRefactorTemplatesForRevert(
@@ -621,7 +640,8 @@ class TemplateGenerator {
621640
}
622641

623642
private getSourceToDestinationMessage(revert: boolean) {
624-
return revert ? [...SOURCE_TO_DESTINATION_STACKS].reverse().join(SEPARATOR) : SOURCE_TO_DESTINATION_STACKS.join(SEPARATOR);
643+
const SOURCE_TO_DESTINATION_STACKS = [GEN1, GEN2];
644+
return revert ? SOURCE_TO_DESTINATION_STACKS.reverse().join(SEPARATOR) : SOURCE_TO_DESTINATION_STACKS.join(SEPARATOR);
625645
}
626646

627647
private constructRoleArn(roleName: string) {

packages/amplify-migration/src/command-handler.test.ts

Lines changed: 46 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -270,21 +270,7 @@ describe('updateCdkStackFile', () => {
270270
const mockCustomResourcesPath = 'amplify-gen2/amplify/custom';
271271
const mockProjectRoot = '/mockRootDir';
272272

273-
beforeEach(() => {
274-
jest.clearAllMocks();
275-
jest.mocked(updateCdkStackFile).mockImplementation(actualUpdateCdkStackFile);
276-
jest.mocked(getProjectInfo).mockImplementation(actualGetProjectInfoFile);
277-
jest.mocked(pathManager.findProjectRoot).mockReturnValue('/mockRootDir');
278-
});
279-
280-
afterEach(() => {
281-
// Reset to the mock after each test if needed
282-
jest.mocked(updateCdkStackFile).mockReset();
283-
jest.mocked(getProjectInfo).mockReset();
284-
});
285-
286-
it('should correctly transform CDK stack file content', async () => {
287-
const originalCdkContent = `
273+
const originalCdkContentWithError = `
288274
import * as AmplifyHelpers from '@aws-amplify/cli-extensibility-helper';
289275
import * as cdk from 'aws-cdk-lib';
290276
@@ -307,6 +293,46 @@ describe('updateCdkStackFile', () => {
307293
}
308294
`;
309295

296+
const originalCdkContentWithoutError = `
297+
import * as AmplifyHelpers from '@aws-amplify/cli-extensibility-helper';
298+
import * as cdk from 'aws-cdk-lib';
299+
300+
export class cdkStack extends cdk.Stack {
301+
constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
302+
super(scope, id, props);
303+
304+
const projectInfo = AmplifyHelpers.getProjectInfo();
305+
/* AmplifyHelpers.addResourceDependency(this,
306+
{
307+
category: "custom",
308+
resourceName: "customResource1",
309+
},
310+
{
311+
category: "auth",
312+
resourceName: "authResource1",
313+
}
314+
); */
315+
}
316+
}
317+
`;
318+
319+
beforeEach(() => {
320+
jest.clearAllMocks();
321+
jest.mocked(updateCdkStackFile).mockImplementation(actualUpdateCdkStackFile);
322+
jest.mocked(getProjectInfo).mockImplementation(actualGetProjectInfoFile);
323+
jest.mocked(pathManager.findProjectRoot).mockReturnValue('/mockRootDir');
324+
});
325+
326+
afterEach(() => {
327+
// Reset to the mock after each test if needed
328+
jest.mocked(updateCdkStackFile).mockReset();
329+
jest.mocked(getProjectInfo).mockReset();
330+
});
331+
332+
test.each([
333+
[originalCdkContentWithError, true],
334+
[originalCdkContentWithoutError, false],
335+
])('should correctly transform CDK stack file content', async (originalCdkContent, shouldThrowError) => {
310336
const mockProjectConfig = JSON.stringify({
311337
projectName: 'testProject',
312338
version: '3.1',
@@ -344,9 +370,11 @@ describe('updateCdkStackFile', () => {
344370

345371
// Verify specific transformations
346372
expect(transformedContent).not.toContain('import { AmplifyHelpers }'); // Import removed
347-
expect(transformedContent).toContain(
348-
"throw new Error('Follow https://docs.amplify.aws/react/start/migrate-to-gen2/ to update the resource dependency')",
349-
); // Error added
373+
if (shouldThrowError) {
374+
expect(transformedContent).toContain(
375+
"throw new Error('Follow https://docs.amplify.aws/react/start/migrate-to-gen2/ to update the resource dependency')",
376+
);
377+
} // Error added
350378
expect(transformedContent).toContain("const projectInfo = {envName: `${AMPLIFY_GEN_1_ENV_NAME}`, projectName: 'testProject'}"); // Project info replaced
351379
});
352380
});

packages/amplify-migration/src/command-handlers.ts

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,8 @@ const MIGRATION_DIR = '.amplify/migration';
5050
const GEN1_COMMAND = 'amplifyPush --simple';
5151
const GEN2_COMMAND = 'npx ampx pipeline-deploy --branch $AWS_BRANCH --app-id $AWS_APP_ID';
5252
const GEN2_COMMAND_GENERATION_MESSAGE_SUFFIX = 'your Gen 2 backend code';
53-
const GEN1_REMOVE_CONFIGURATION_MESSAGE_SUFFIX = 'your Gen1 configuration files';
53+
const GEN1_REMOVE_CONFIGURATION_MESSAGE_SUFFIX = 'your Gen 1 configuration files';
54+
const GEN1_CUSTOM_RESOURCES_SUFFIX = 'your Gen 1 custom resources';
5455
export const GEN1_CONFIGURATION_FILES = ['aws-exports.js', 'amplifyconfiguration.json', 'awsconfiguration.json'];
5556
const CUSTOM_DIR = 'custom';
5657
const TYPES_DIR = 'types';
@@ -321,6 +322,7 @@ const getCustomResourceMap = async (): Promise<Map<string, string>> => {
321322
export async function updateCustomResources() {
322323
const customResources = getCustomResources();
323324
if (customResources.length > 0) {
325+
const movingGen1CustomResources = ora(`Moving ${GEN1_CUSTOM_RESOURCES_SUFFIX}`).start();
324326
const rootDir = pathManager.findProjectRoot();
325327
assert(rootDir);
326328
const amplifyGen1BackendDir = path.join(rootDir, AMPLIFY_DIR, BACKEND_DIR);
@@ -344,6 +346,7 @@ export async function updateCustomResources() {
344346
await fs.cp(sourceTypesPath, destinationTypesPath, { recursive: true });
345347

346348
await updateCdkStackFile(customResources, destinationCustomResourcePath, rootDir);
349+
movingGen1CustomResources.succeed(`Moved ${GEN1_CUSTOM_RESOURCES_SUFFIX}`);
347350
}
348351
}
349352

@@ -359,7 +362,7 @@ export async function updateCdkStackFile(customResources: string[], destinationC
359362
let cdkStackContent = await fs.readFile(cdkStackFilePath, { encoding: 'utf-8' });
360363

361364
// Check for existence of AmplifyHelpers.addResourceDependency and throw an error if found
362-
if (cdkStackContent.includes('AmplifyHelpers.addResourceDependency')) {
365+
if (hasUncommentedDependency(cdkStackContent, 'AmplifyHelpers.addResourceDependency')) {
363366
cdkStackContent = cdkStackContent.replace(
364367
/export class/,
365368
`throw new Error('Follow https://docs.amplify.aws/react/start/migrate-to-gen2/ to update the resource dependency');\n\nexport class`,
@@ -375,7 +378,7 @@ export async function updateCdkStackFile(customResources: string[], destinationC
375378

376379
// Replace the cdk.CfnParameter definition to include the default property
377380
cdkStackContent = cdkStackContent.replace(
378-
/new cdk\.CfnParameter\(this, "env", {[\s\S]*?}\);/,
381+
/new cdk\.CfnParameter\(this, ['"]env['"], {[\s\S]*?}\);/,
379382
`new cdk.CfnParameter(this, "env", {
380383
type: "String",
381384
description: "Current Amplify CLI env name",
@@ -402,6 +405,29 @@ export async function updateCdkStackFile(customResources: string[], destinationC
402405
}
403406
}
404407

408+
const hasUncommentedDependency = (fileContent: string, matchString: string) => {
409+
// Split the content into lines
410+
const lines = fileContent.split('\n');
411+
412+
// Check each line
413+
for (const line of lines) {
414+
const trimmedLine = line.trim();
415+
416+
// Check if the line contains the dependency and is not commented
417+
if (
418+
trimmedLine.includes(matchString) &&
419+
!trimmedLine.startsWith('//') &&
420+
!trimmedLine.startsWith('/*') &&
421+
!trimmedLine.includes('*/') &&
422+
!trimmedLine.match(/^\s*\*/)
423+
) {
424+
return true;
425+
}
426+
}
427+
428+
return false;
429+
};
430+
405431
export async function getProjectInfo(rootDir: string) {
406432
const configDir = path.join(rootDir, AMPLIFY_DIR, '.config');
407433
const projectConfigFilePath = path.join(configDir, 'project-config.json');
@@ -477,7 +503,7 @@ export async function prepare() {
477503

478504
await updateCustomResources();
479505

480-
const movingGen1BackendFiles = ora(`Moving your Gen1 backend files to ${format.highlight(MIGRATION_DIR)}`).start();
506+
const movingGen1BackendFiles = ora(`Moving your Gen 1 backend files to ${format.highlight(MIGRATION_DIR)}`).start();
481507
// Move gen1 amplify to .amplify/migrations and move gen2 amplify from amplify-gen2 to amplify dir to convert current app to gen2.
482508
const cwd = process.cwd();
483509
await fs.rm(MIGRATION_DIR, { force: true, recursive: true });
@@ -486,7 +512,7 @@ export async function prepare() {
486512
await fs.rename(`${TEMP_GEN_2_OUTPUT_DIR}/amplify`, `${cwd}/amplify`);
487513
await fs.rename(`${TEMP_GEN_2_OUTPUT_DIR}/package.json`, `${cwd}/package.json`);
488514
await fs.rm(TEMP_GEN_2_OUTPUT_DIR, { recursive: true });
489-
movingGen1BackendFiles.succeed(`Moved your Gen1 backend files to ${format.highlight(MIGRATION_DIR)}`);
515+
movingGen1BackendFiles.succeed(`Moved your Gen 1 backend files to ${format.highlight(MIGRATION_DIR)}`);
490516
}
491517

492518
export async function removeGen1ConfigurationFiles() {
@@ -536,12 +562,12 @@ export async function revertGen2Migration(fromStack: string, toStack: string) {
536562
const success = await templateGenerator.revert();
537563
const usageData = await getUsageDataMetric(envName);
538564
if (success) {
539-
printer.print(format.success(`Moved resources back to Gen1 stack successfully.`));
540-
const movingGen1BackendFiles = ora(`Moving your Gen1 backend files to ${format.highlight(AMPLIFY_DIR)}`).start();
565+
printer.print(format.success(`Moved resources back to Gen 1 stack successfully.`));
566+
const movingGen1BackendFiles = ora(`Moving your Gen 1 backend files to ${format.highlight(AMPLIFY_DIR)}`).start();
541567
// Move gen1 amplify from .amplify/migration/amplify to amplify
542568
await fs.rm(AMPLIFY_DIR, { force: true, recursive: true });
543569
await fs.rename(`${MIGRATION_DIR}/amplify`, AMPLIFY_DIR);
544-
movingGen1BackendFiles.succeed(`Moved your Gen1 backend files to ${format.highlight(AMPLIFY_DIR)}`);
570+
movingGen1BackendFiles.succeed(`Moved your Gen 1 backend files to ${format.highlight(AMPLIFY_DIR)}`);
545571
await usageData.emitSuccess();
546572
} else {
547573
await usageData.emitError(new Error('Failed to run revert command'));

0 commit comments

Comments
 (0)