From 339c4cd35fc7ecf4d579b1685676fb1eafdb2df5 Mon Sep 17 00:00:00 2001 From: rjabhi Date: Thu, 21 Nov 2024 10:10:04 -0800 Subject: [PATCH 1/5] feat: automate preprocessing steps for refactor --- .../package.json | 6 +- .../src/cfn-stack-updater.ts | 70 +++++ .../src/custom-test-matchers.ts | 34 ++ .../src/migration-readme-generator.test.ts | 113 +------ .../src/migration-readme-generator.ts | 212 ++++++------- .../src/setup-jest.ts | 6 + .../src/template-generator.test.ts | 295 +++++++++++++++--- .../src/template-generator.ts | 23 +- .../src/types.ts | 4 + yarn.lock | 1 + 10 files changed, 508 insertions(+), 256 deletions(-) create mode 100644 packages/amplify-migration-template-gen/src/cfn-stack-updater.ts create mode 100644 packages/amplify-migration-template-gen/src/custom-test-matchers.ts create mode 100644 packages/amplify-migration-template-gen/src/setup-jest.ts diff --git a/packages/amplify-migration-template-gen/package.json b/packages/amplify-migration-template-gen/package.json index fed78e667a..805c9883aa 100644 --- a/packages/amplify-migration-template-gen/package.json +++ b/packages/amplify-migration-template-gen/package.json @@ -4,6 +4,7 @@ "type": "commonjs", "main": "lib/index.js", "devDependencies": { + "@jest/globals": "^29.7.0", "jest": "^29.5.0", "typescript": "^5.4.5" }, @@ -31,7 +32,10 @@ "json", "node" ], - "collectCoverage": true + "collectCoverage": true, + "setupFilesAfterEnv": [ + "/src/setup-jest.ts" + ] }, "publishConfig": { "access": "public" diff --git a/packages/amplify-migration-template-gen/src/cfn-stack-updater.ts b/packages/amplify-migration-template-gen/src/cfn-stack-updater.ts new file mode 100644 index 0000000000..0763b9bf5c --- /dev/null +++ b/packages/amplify-migration-template-gen/src/cfn-stack-updater.ts @@ -0,0 +1,70 @@ +import { CloudFormationClient, DescribeStacksCommand, Parameter, UpdateStackCommand } from '@aws-sdk/client-cloudformation'; +import { CFNTemplate } from './types'; +import assert from 'node:assert'; + +const POLL_ATTEMPTS = 30; +const POLL_INTERVAL_MS = 1000; +const NO_UPDATES_MESSAGE = 'No updates are to be performed'; +const CFN_IAM_CAPABILIY = 'CAPABILITY_NAMED_IAM'; +const COMPLETION_STATE = '_COMPLETE'; +export const UPDATE_COMPLETE = 'UPDATE_COMPLETE'; +/** + * Updates a stack with given template. If no updates are present, it no-ops. + * @param cfnClient + * @param stackName + * @param parameters + * @param templateBody + * @param attempts number of attempts to poll CFN stack for update completion state. The interval between the polls is 1 second. + */ +export async function tryUpdateStack( + cfnClient: CloudFormationClient, + stackName: string, + parameters: Parameter[], + templateBody: CFNTemplate, + attempts = POLL_ATTEMPTS, +): Promise { + try { + await cfnClient.send( + new UpdateStackCommand({ + TemplateBody: JSON.stringify(templateBody), + Parameters: parameters, + StackName: stackName, + Capabilities: [CFN_IAM_CAPABILIY], + Tags: [], + }), + ); + return pollStackForCompletionState(cfnClient, stackName, attempts); + } catch (e) { + if (!e.message.includes(NO_UPDATES_MESSAGE)) { + throw e; + } + return UPDATE_COMPLETE; + } +} + +/** + * Polls a stack for completion state + * @param cfnClient + * @param stackName + * @param attempts number of attempts to poll for completion. + * @returns the stack status + */ +async function pollStackForCompletionState(cfnClient: CloudFormationClient, stackName: string, attempts: number): Promise { + do { + const { Stacks } = await cfnClient.send( + new DescribeStacksCommand({ + StackName: stackName, + }), + ); + const stack = Stacks?.[0]; + assert(stack); + const stackStatus = stack.StackStatus; + assert(stackStatus); + if (stackStatus?.endsWith(COMPLETION_STATE)) { + return stackStatus; + } + await new Promise((res) => setTimeout(() => res(''), POLL_INTERVAL_MS)); + attempts--; + } while (attempts > 0); + throw new Error(`Stack ${stackName} did not reach a completion state within the given time period.`); +} diff --git a/packages/amplify-migration-template-gen/src/custom-test-matchers.ts b/packages/amplify-migration-template-gen/src/custom-test-matchers.ts new file mode 100644 index 0000000000..f0281b7a18 --- /dev/null +++ b/packages/amplify-migration-template-gen/src/custom-test-matchers.ts @@ -0,0 +1,34 @@ +import { + DescribeStackResourcesCommand, + DescribeStackResourcesCommandInput, + UpdateStackCommand, + DescribeStacksCommand, + DescribeStacksCommandInput, + UpdateStackCommandInput, +} from '@aws-sdk/client-cloudformation'; + +type CFNCommand = DescribeStackResourcesCommand | DescribeStacksCommand | UpdateStackCommand; +type CFNCommandType = typeof DescribeStackResourcesCommand | typeof DescribeStacksCommand | typeof UpdateStackCommand; +type CFNCommandInput = DescribeStackResourcesCommandInput | DescribeStacksCommandInput | UpdateStackCommandInput; + +export const toBeACloudFormationCommand = (actual: [CFNCommand], expectedInput: CFNCommandInput, expectedType: CFNCommandType) => { + const actualInstance = actual[0]; + expect(actualInstance.input).toEqual(expectedInput); + const constructorName = actualInstance.constructor.name; + const pass = constructorName === expectedType.prototype.constructor.name; + + return { + pass, + message: () => `expected ${actual} to be instance of ${constructorName}`, + }; +}; + +declare global { + // Needed for custom matchers. + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace jest { + interface Matchers { + toBeACloudFormationCommand(expectedInput: CFNCommandInput, expectedType: CFNCommandType): R; + } + } +} diff --git a/packages/amplify-migration-template-gen/src/migration-readme-generator.test.ts b/packages/amplify-migration-template-gen/src/migration-readme-generator.test.ts index 022741c435..f52f7bed46 100644 --- a/packages/amplify-migration-template-gen/src/migration-readme-generator.test.ts +++ b/packages/amplify-migration-template-gen/src/migration-readme-generator.test.ts @@ -56,103 +56,14 @@ describe('MigrationReadMeGenerator', () => { }); }); - // should render step1 it('should render step1', async () => { - await migrationReadMeGenerator.renderStep1(oldStackTemplate, newStackTemplate, [ - { - ParameterKey: 'authSelections', - ParameterValue: 'identityPoolAndUserPool', - }, - ]); + await migrationReadMeGenerator.renderStep1(oldStackTemplate, newStackTemplate, logicalIdMapping, oldStackTemplate, newStackTemplate); expect(fs.appendFile).toHaveBeenCalledWith( 'test/MIGRATION_README.md', - `### STEP 1: UPDATE GEN-1 AUTH STACK -It is a non-disruptive update since the template only replaces resource references with their resolved values. This is a required step to execute cloudformation stack refactor later. -\`\`\` -aws cloudformation update-stack \\ - --stack-name amplify-testauth-dev-12345-auth-ABCDE \\ - --template-body file://test/step1-gen1PreProcessUpdateStackTemplate.json \\ - --parameters '[{"ParameterKey":"authSelections","ParameterValue":"identityPoolAndUserPool"}]' \\ - --capabilities CAPABILITY_NAMED_IAM \\ - --tags '[]' - \`\`\` - -\`\`\` -aws cloudformation describe-stacks \\ - --stack-name amplify-testauth-dev-12345-auth-ABCDE - \`\`\` - - #### Rollback step: - \`\`\` - aws cloudformation update-stack \\ - --stack-name amplify-testauth-dev-12345-auth-ABCDE \\ - --template-body file://test/step1-gen1PreProcessUpdateStackTemplate-rollback.json \\ - --parameters '[{"ParameterKey\":\"authSelections\",\"ParameterValue\":\"identityPoolAndUserPool\"}]' \\ - --capabilities CAPABILITY_NAMED_IAM - \`\`\` - -\`\`\` -aws cloudformation describe-stacks \\ - --stack-name amplify-testauth-dev-12345-auth-ABCDE - \`\`\` - `, - { encoding: 'utf8' }, - ); - }); - - // should render step2 - it('should render step2', async () => { - await migrationReadMeGenerator.renderStep2(oldStackTemplate, newStackTemplate, [ - { - ParameterKey: 'authSelections', - ParameterValue: 'identityPoolAndUserPool', - }, - ]); - expect(fs.appendFile).toHaveBeenCalledWith( - 'test/MIGRATION_README.md', - `### STEP 2: REMOVE GEN-2 AUTH STACK RESOURCES -This step is required since we will eventually replace gen-2 resources with gen-1 resources as part of Step 3 (refactor). -\`\`\` -aws cloudformation update-stack \\ - --stack-name amplify-mygen2app-test-sandbox-12345-auth-ABCDE \\ - --template-body file://test/step2-gen2ResourcesRemovalStackTemplate.json \\ - --parameters '[{"ParameterKey":"authSelections","ParameterValue":"identityPoolAndUserPool"}]' \\ - --capabilities CAPABILITY_NAMED_IAM \\ - --tags '[]' - \`\`\` - -\`\`\` -aws cloudformation describe-stacks \\ - --stack-name amplify-mygen2app-test-sandbox-12345-auth-ABCDE - \`\`\` - - #### Rollback step: - \`\`\` - aws cloudformation update-stack \\ - --stack-name amplify-mygen2app-test-sandbox-12345-auth-ABCDE \\ - --template-body file://test/step2-gen2ResourcesRemovalStackTemplate-rollback.json \\ - --parameters '[{"ParameterKey\":\"authSelections\",\"ParameterValue\":\"identityPoolAndUserPool\"}]' \\ - --capabilities CAPABILITY_NAMED_IAM - \`\`\` - -\`\`\` -aws cloudformation describe-stacks \\ - --stack-name amplify-mygen2app-test-sandbox-12345-auth-ABCDE - \`\`\` - `, - { encoding: 'utf8' }, - ); - }); - - // should render step3 - it('should render step3', async () => { - await migrationReadMeGenerator.renderStep3(oldStackTemplate, newStackTemplate, logicalIdMapping, oldStackTemplate, newStackTemplate); - expect(fs.appendFile).toHaveBeenCalledWith( - 'test/MIGRATION_README.md', - `### STEP 3: CREATE AND EXECUTE CLOUDFORMATION STACK REFACTOR FOR auth CATEGORY + `### STEP 1: CREATE AND EXECUTE CLOUDFORMATION STACK REFACTOR FOR auth CATEGORY This step will move the Gen1 auth resources to Gen2 stack. -3.a) Upload the source and destination templates to S3 +1.a) Upload the source and destination templates to S3 \`\`\` export BUCKET_NAME=<> \`\`\` @@ -165,7 +76,7 @@ aws s3 cp test/step3-sourceTemplate.json s3://$BUCKET_NAME aws s3 cp test/step3-destinationTemplate.json s3://$BUCKET_NAME \`\`\` -3.b) Create stack refactor +1.b) Create stack refactor \`\`\` aws cloudformation create-stack-refactor --stack-definitions StackName=amplify-testauth-dev-12345-auth-ABCDE,TemplateURL=s3://$BUCKET_NAME/step3-sourceTemplate.json StackName=amplify-mygen2app-test-sandbox-12345-auth-ABCDE,TemplateURL=s3://$BUCKET_NAME/step3-destinationTemplate.json --resource-mappings '[{\"Source\":{\"StackName\":\"amplify-testauth-dev-12345-auth-ABCDE\",\"LogicalResourceId\":\"Gen1FooUserPool\"},\"Destination\":{\"StackName\":\"amplify-mygen2app-test-sandbox-12345-auth-ABCDE\",\"LogicalResourceId\":\"Gen2FooUserPool\"}}]' \`\`\` @@ -174,17 +85,17 @@ aws cloudformation create-stack-refactor --stack-definitions StackName=amplify- export STACK_REFACTOR_ID=<> \`\`\` -3.c) Describe stack refactor to check for creation status +1.c) Describe stack refactor to check for creation status \`\`\` aws cloudformation describe-stack-refactor --stack-refactor-id $STACK_REFACTOR_ID \`\`\` -3.d) Execute stack refactor +1.d) Execute stack refactor \`\`\` aws cloudformation execute-stack-refactor --stack-refactor-id $STACK_REFACTOR_ID \`\`\` -3.e) Describe stack refactor to check for execution status +1.e) Describe stack refactor to check for execution status \`\`\` aws cloudformation describe-stack-refactor --stack-refactor-id $STACK_REFACTOR_ID \`\`\` @@ -225,19 +136,19 @@ Describe stack refactor to check for execution status ); }); - it('should render step4', async () => { - await migrationReadMeGenerator.renderStep4(); + it('should render step2', async () => { + await migrationReadMeGenerator.renderStep2(); expect(fs.appendFile).toHaveBeenCalledWith( 'test/MIGRATION_README.md', - `### STEP 4: REDEPLOY GEN2 APPLICATION + `### STEP 2: REDEPLOY GEN2 APPLICATION This step will remove the hardcoded references from the template and replace them with resource references (where applicable). -4.a) Only applicable to Storage category: Uncomment the following line in \`amplify/backend.ts\` file to instruct CDK to use the gen1 S3 bucket +2.a) Only applicable to Storage category: Uncomment the following line in \`amplify/backend.ts\` file to instruct CDK to use the gen1 S3 bucket \`\`\` s3Bucket.bucketName = YOUR_GEN1_BUCKET_NAME; \`\`\` -4.b) Deploy sandbox using the below command or trigger a CI/CD build via hosting by committing this file to your Git repository +2.b) Deploy sandbox using the below command or trigger a CI/CD build via hosting by committing this file to your Git repository \`\`\` npx ampx sandbox \`\`\` diff --git a/packages/amplify-migration-template-gen/src/migration-readme-generator.ts b/packages/amplify-migration-template-gen/src/migration-readme-generator.ts index cd19feb51c..c8b76a39fc 100644 --- a/packages/amplify-migration-template-gen/src/migration-readme-generator.ts +++ b/packages/amplify-migration-template-gen/src/migration-readme-generator.ts @@ -29,101 +29,101 @@ class MigrationReadmeGenerator { await fs.writeFile(this.migrationReadMePath, `## Stack refactor steps for ${this.category} category\n`, { encoding: 'utf8' }); } - /** - * Resolves outputs and dependencies to prepare for refactor - * @param oldGen1StackTemplate - * @param newGen1StackTemplate - * @param parameters - */ - async renderStep1(oldGen1StackTemplate: CFNTemplate, newGen1StackTemplate: CFNTemplate, parameters: Parameter[]): Promise { - const step1FileNamePath = `${this.path}/step1-gen1PreProcessUpdateStackTemplate.json`; - const step1RollbackFileNamePath = `${this.path}/step1-gen1PreProcessUpdateStackTemplate-rollback.json`; - const paramsString = JSON.stringify(parameters); - await fs.appendFile( - this.migrationReadMePath, - `### STEP 1: UPDATE GEN-1 ${this.category.toUpperCase()} STACK -It is a non-disruptive update since the template only replaces resource references with their resolved values. This is a required step to execute cloudformation stack refactor later. -\`\`\` -aws cloudformation update-stack \\ - --stack-name ${this.gen1CategoryStackName} \\ - --template-body file://${step1FileNamePath} \\ - --parameters '${paramsString}' \\ - --capabilities CAPABILITY_NAMED_IAM \\ - --tags '[]' - \`\`\` - -\`\`\` -aws cloudformation describe-stacks \\ - --stack-name ${this.gen1CategoryStackName} - \`\`\` - - #### Rollback step: - \`\`\` - aws cloudformation update-stack \\ - --stack-name ${this.gen1CategoryStackName} \\ - --template-body file://${step1RollbackFileNamePath} \\ - --parameters '${paramsString}' \\ - --capabilities CAPABILITY_NAMED_IAM - \`\`\` - -\`\`\` -aws cloudformation describe-stacks \\ - --stack-name ${this.gen1CategoryStackName} - \`\`\` - `, - { encoding: 'utf8' }, - ); - await fs.writeFile(step1FileNamePath, JSON.stringify(newGen1StackTemplate, null, 2), { encoding: 'utf8' }); - await fs.writeFile(step1RollbackFileNamePath, JSON.stringify(oldGen1StackTemplate, null, 2), { encoding: 'utf8' }); - } - - /** - * Removes Gen2 resources from Gen2 stack to prepare for refactor - * @param oldGen2StackTemplate - * @param newGen2StackTemplate - * @param parameters - */ - async renderStep2(oldGen2StackTemplate: CFNTemplate, newGen2StackTemplate: CFNTemplate, parameters: Parameter[] = []): Promise { - const step2FileNamePath = `${this.path}/step2-gen2ResourcesRemovalStackTemplate.json`; - const step2RollbackFileNamePath = `${this.path}/step2-gen2ResourcesRemovalStackTemplate-rollback.json`; - const paramsString = JSON.stringify(parameters); - await fs.appendFile( - this.migrationReadMePath, - `### STEP 2: REMOVE GEN-2 ${this.category.toUpperCase()} STACK RESOURCES -This step is required since we will eventually replace gen-2 resources with gen-1 resources as part of Step 3 (refactor). -\`\`\` -aws cloudformation update-stack \\ - --stack-name ${this.gen2CategoryStackName} \\ - --template-body file://${step2FileNamePath} \\ - --parameters '${paramsString}' \\ - --capabilities CAPABILITY_NAMED_IAM \\ - --tags '[]' - \`\`\` - -\`\`\` -aws cloudformation describe-stacks \\ - --stack-name ${this.gen2CategoryStackName} - \`\`\` - - #### Rollback step: - \`\`\` - aws cloudformation update-stack \\ - --stack-name ${this.gen2CategoryStackName} \\ - --template-body file://${step2RollbackFileNamePath} \\ - --parameters '${paramsString}' \\ - --capabilities CAPABILITY_NAMED_IAM - \`\`\` - -\`\`\` -aws cloudformation describe-stacks \\ - --stack-name ${this.gen2CategoryStackName} - \`\`\` - `, - { encoding: 'utf8' }, - ); - await fs.writeFile(step2FileNamePath, JSON.stringify(newGen2StackTemplate, null, 2), { encoding: 'utf8' }); - await fs.writeFile(step2RollbackFileNamePath, JSON.stringify(oldGen2StackTemplate, null, 2), { encoding: 'utf8' }); - } + // /** + // * Resolves outputs and dependencies to prepare for refactor + // * @param oldGen1StackTemplate + // * @param newGen1StackTemplate + // * @param parameters + // */ + // async renderStep1(oldGen1StackTemplate: CFNTemplate, newGen1StackTemplate: CFNTemplate, parameters: Parameter[]): Promise { + // const step1FileNamePath = `${this.path}/step1-gen1PreProcessUpdateStackTemplate.json`; + // const step1RollbackFileNamePath = `${this.path}/step1-gen1PreProcessUpdateStackTemplate-rollback.json`; + // const paramsString = JSON.stringify(parameters); + // await fs.appendFile( + // this.migrationReadMePath, + // `### STEP 1: UPDATE GEN-1 ${this.category.toUpperCase()} STACK + // It is a non-disruptive update since the template only replaces resource references with their resolved values. This is a required step to execute cloudformation stack refactor later. + // \`\`\` + // aws cloudformation update-stack \\ + // --stack-name ${this.gen1CategoryStackName} \\ + // --template-body file://${step1FileNamePath} \\ + // --parameters '${paramsString}' \\ + // --capabilities CAPABILITY_NAMED_IAM \\ + // --tags '[]' + // \`\`\` + // + // \`\`\` + // aws cloudformation describe-stacks \\ + // --stack-name ${this.gen1CategoryStackName} + // \`\`\` + // + // #### Rollback step: + // \`\`\` + // aws cloudformation update-stack \\ + // --stack-name ${this.gen1CategoryStackName} \\ + // --template-body file://${step1RollbackFileNamePath} \\ + // --parameters '${paramsString}' \\ + // --capabilities CAPABILITY_NAMED_IAM + // \`\`\` + // + // \`\`\` + // aws cloudformation describe-stacks \\ + // --stack-name ${this.gen1CategoryStackName} + // \`\`\` + // `, + // { encoding: 'utf8' }, + // ); + // await fs.writeFile(step1FileNamePath, JSON.stringify(newGen1StackTemplate, null, 2), { encoding: 'utf8' }); + // await fs.writeFile(step1RollbackFileNamePath, JSON.stringify(oldGen1StackTemplate, null, 2), { encoding: 'utf8' }); + // } + // + // /** + // * Removes Gen2 resources from Gen2 stack to prepare for refactor + // * @param oldGen2StackTemplate + // * @param newGen2StackTemplate + // * @param parameters + // */ + // async renderStep2(oldGen2StackTemplate: CFNTemplate, newGen2StackTemplate: CFNTemplate, parameters: Parameter[] = []): Promise { + // const step2FileNamePath = `${this.path}/step2-gen2ResourcesRemovalStackTemplate.json`; + // const step2RollbackFileNamePath = `${this.path}/step2-gen2ResourcesRemovalStackTemplate-rollback.json`; + // const paramsString = JSON.stringify(parameters); + // await fs.appendFile( + // this.migrationReadMePath, + // `### STEP 2: REMOVE GEN-2 ${this.category.toUpperCase()} STACK RESOURCES + // This step is required since we will eventually replace gen-2 resources with gen-1 resources as part of Step 3 (refactor). + // \`\`\` + // aws cloudformation update-stack \\ + // --stack-name ${this.gen2CategoryStackName} \\ + // --template-body file://${step2FileNamePath} \\ + // --parameters '${paramsString}' \\ + // --capabilities CAPABILITY_NAMED_IAM \\ + // --tags '[]' + // \`\`\` + // + // \`\`\` + // aws cloudformation describe-stacks \\ + // --stack-name ${this.gen2CategoryStackName} + // \`\`\` + // + // #### Rollback step: + // \`\`\` + // aws cloudformation update-stack \\ + // --stack-name ${this.gen2CategoryStackName} \\ + // --template-body file://${step2RollbackFileNamePath} \\ + // --parameters '${paramsString}' \\ + // --capabilities CAPABILITY_NAMED_IAM + // \`\`\` + // + // \`\`\` + // aws cloudformation describe-stacks \\ + // --stack-name ${this.gen2CategoryStackName} + // \`\`\` + // `, + // { encoding: 'utf8' }, + // ); + // await fs.writeFile(step2FileNamePath, JSON.stringify(newGen2StackTemplate, null, 2), { encoding: 'utf8' }); + // await fs.writeFile(step2RollbackFileNamePath, JSON.stringify(oldGen2StackTemplate, null, 2), { encoding: 'utf8' }); + // } /** * Creates and executes Stack refactor operation for the given category @@ -131,7 +131,7 @@ aws cloudformation describe-stacks \\ * @param destinationTemplate * @param logicalIdMapping */ - async renderStep3( + async renderStep1( sourceTemplate: CFNTemplate, destinationTemplate: CFNTemplate, logicalIdMapping: Map, @@ -174,10 +174,10 @@ aws cloudformation describe-stacks \\ } await fs.appendFile( this.migrationReadMePath, - `### STEP 3: CREATE AND EXECUTE CLOUDFORMATION STACK REFACTOR FOR ${this.category} CATEGORY + `### STEP 1: CREATE AND EXECUTE CLOUDFORMATION STACK REFACTOR FOR ${this.category} CATEGORY This step will move the Gen1 ${this.category} resources to Gen2 stack. -3.a) Upload the source and destination templates to S3 +1.a) Upload the source and destination templates to S3 \`\`\` export BUCKET_NAME=<> \`\`\` @@ -190,7 +190,7 @@ aws s3 cp ${step3SourceTemplateFileNamePath} s3://$BUCKET_NAME aws s3 cp ${step3DestinationTemplateFileNamePath} s3://$BUCKET_NAME \`\`\` -3.b) Create stack refactor +1.b) Create stack refactor \`\`\` aws cloudformation create-stack-refactor \ --stack-definitions StackName=${this.gen1CategoryStackName},TemplateURL=s3://$BUCKET_NAME/${sourceTemplateFileName} \ @@ -203,17 +203,17 @@ aws cloudformation create-stack-refactor \ export STACK_REFACTOR_ID=<> \`\`\` -3.c) Describe stack refactor to check for creation status +1.c) Describe stack refactor to check for creation status \`\`\` aws cloudformation describe-stack-refactor --stack-refactor-id $STACK_REFACTOR_ID \`\`\` -3.d) Execute stack refactor +1.d) Execute stack refactor \`\`\` aws cloudformation execute-stack-refactor --stack-refactor-id $STACK_REFACTOR_ID \`\`\` -3.e) Describe stack refactor to check for execution status +1.e) Describe stack refactor to check for execution status \`\`\` aws cloudformation describe-stack-refactor --stack-refactor-id $STACK_REFACTOR_ID \`\`\` @@ -262,18 +262,18 @@ Describe stack refactor to check for execution status await fs.writeFile(step3RollbackDestinationTemplateFileNamePath, JSON.stringify(oldDestinationTemplate, null, 2), { encoding: 'utf8' }); } - async renderStep4() { + async renderStep2() { await fs.appendFile( this.migrationReadMePath, - `### STEP 4: REDEPLOY GEN2 APPLICATION + `### STEP 2: REDEPLOY GEN2 APPLICATION This step will remove the hardcoded references from the template and replace them with resource references (where applicable). -4.a) Only applicable to Storage category: Uncomment the following line in \`amplify/backend.ts\` file to instruct CDK to use the gen1 S3 bucket +2.a) Only applicable to Storage category: Uncomment the following line in \`amplify/backend.ts\` file to instruct CDK to use the gen1 S3 bucket \`\`\` s3Bucket.bucketName = YOUR_GEN1_BUCKET_NAME; \`\`\` -4.b) Deploy sandbox using the below command or trigger a CI/CD build via hosting by committing this file to your Git repository +2.b) Deploy sandbox using the below command or trigger a CI/CD build via hosting by committing this file to your Git repository \`\`\` npx ampx sandbox \`\`\` diff --git a/packages/amplify-migration-template-gen/src/setup-jest.ts b/packages/amplify-migration-template-gen/src/setup-jest.ts new file mode 100644 index 0000000000..01bf7df61f --- /dev/null +++ b/packages/amplify-migration-template-gen/src/setup-jest.ts @@ -0,0 +1,6 @@ +import { expect } from '@jest/globals'; +import { toBeACloudFormationCommand } from './custom-test-matchers'; + +expect.extend({ + toBeACloudFormationCommand, +}); diff --git a/packages/amplify-migration-template-gen/src/template-generator.test.ts b/packages/amplify-migration-template-gen/src/template-generator.test.ts index e4ed110c3f..d4ef20c197 100644 --- a/packages/amplify-migration-template-gen/src/template-generator.test.ts +++ b/packages/amplify-migration-template-gen/src/template-generator.test.ts @@ -1,7 +1,15 @@ import { TemplateGenerator } from './template-generator'; -import { CloudFormationClient, DescribeStackResourcesCommand, DescribeStackResourcesOutput } from '@aws-sdk/client-cloudformation'; +import { + CloudFormationClient, + DescribeStackResourcesCommand, + DescribeStackResourcesOutput, + DescribeStacksCommand, + UpdateStackCommand, +} from '@aws-sdk/client-cloudformation'; import fs from 'node:fs/promises'; +jest.useFakeTimers(); + const mockCfnClientSendMock = jest.fn(); const mockGenerateGen1PreProcessTemplate = jest.fn(); const mockGenerateGen2ResourceRemovalTemplate = jest.fn(); @@ -20,40 +28,6 @@ const GEN1_S3_BUCKET_LOGICAL_ID = 'S3Bucket'; const GEN2_S3_BUCKET_LOGICAL_ID = 'Gen2S3Bucket'; const MOCK_CFN_CLIENT = new CloudFormationClient(); -jest.mock('node:fs/promises'); -jest.mock('./migration-readme-generator', () => { - return function () { - return { - initialize: mockReadMeInitialize, - renderStep1: mockReadMeRenderStep1, - renderStep2: mockReadMeRenderStep2, - renderStep3: mockReadMeRenderStep3, - renderStep4: mockReadMeRenderStep4, - }; - }; -}); -jest.mock('./category-template-generator', () => { - return function () { - return { - generateGen1PreProcessTemplate: mockGenerateGen1PreProcessTemplate.mockReturnValue({ - oldTemplate: {}, - newTemplate: {}, - parameters: [], - }), - generateGen2ResourceRemovalTemplate: mockGenerateGen2ResourceRemovalTemplate.mockReturnValue({ - oldTemplate: {}, - newTemplate: {}, - parameters: [], - }), - generateStackRefactorTemplates: mockGenerateStackRefactorTemplates.mockReturnValue({ - sourceTemplate: {}, - destinationTemplate: {}, - logicalIdMapping: {}, - }), - }; - }; -}); - const mockDescribeGen1StackResources: DescribeStackResourcesOutput = { StackResources: [ { @@ -146,6 +120,40 @@ jest.mock('@aws-sdk/client-cloudformation', () => { }; }); +jest.mock('node:fs/promises'); +jest.mock('./migration-readme-generator', () => { + return function () { + return { + initialize: mockReadMeInitialize, + renderStep1: mockReadMeRenderStep1, + renderStep2: mockReadMeRenderStep2, + renderStep3: mockReadMeRenderStep3, + renderStep4: mockReadMeRenderStep4, + }; + }; +}); +jest.mock('./category-template-generator', () => { + return function () { + return { + generateGen1PreProcessTemplate: mockGenerateGen1PreProcessTemplate.mockReturnValue({ + oldTemplate: {}, + newTemplate: {}, + parameters: [], + }), + generateGen2ResourceRemovalTemplate: mockGenerateGen2ResourceRemovalTemplate.mockReturnValue({ + oldTemplate: {}, + newTemplate: {}, + parameters: [], + }), + generateStackRefactorTemplates: mockGenerateStackRefactorTemplates.mockReturnValue({ + sourceTemplate: {}, + destinationTemplate: {}, + logicalIdMapping: {}, + }), + }; + }; +}); + describe('TemplateGenerator', () => { beforeEach(() => { mockCfnClientSendMock.mockImplementation((command) => { @@ -154,21 +162,30 @@ describe('TemplateGenerator', () => { command.input.StackName === GEN1_ROOT_STACK_NAME ? mockDescribeGen1StackResources : mockDescribeGen2StackResources, ); } + if (command instanceof UpdateStackCommand) { + return Promise.resolve({}); + } + if (command instanceof DescribeStacksCommand) { + return Promise.resolve({ + Stacks: [{ StackStatus: 'UPDATE_COMPLETE' }], + }); + } return Promise.resolve({}); }); }); + afterEach(() => { + jest.clearAllMocks(); + }); + it('should generate a template', async () => { + // Act const generator = new TemplateGenerator(GEN1_ROOT_STACK_NAME, GEN2_ROOT_STACK_NAME, ACCOUNT_ID, MOCK_CFN_CLIENT); await generator.generate(); - expect(fs.mkdir).toBeCalledTimes(NUM_CATEGORIES_TO_REFACTOR + 1); - expect(mockGenerateGen1PreProcessTemplate).toBeCalledTimes(NUM_CATEGORIES_TO_REFACTOR); - expect(mockGenerateGen2ResourceRemovalTemplate).toBeCalledTimes(NUM_CATEGORIES_TO_REFACTOR); - expect(mockGenerateStackRefactorTemplates).toBeCalledTimes(NUM_CATEGORIES_TO_REFACTOR); - expect(mockReadMeInitialize).toBeCalledTimes(NUM_CATEGORIES_TO_REFACTOR); - expect(mockReadMeRenderStep1).toBeCalledTimes(NUM_CATEGORIES_TO_REFACTOR); - expect(mockReadMeRenderStep2).toBeCalledTimes(NUM_CATEGORIES_TO_REFACTOR); - expect(mockReadMeRenderStep3).toBeCalledTimes(NUM_CATEGORIES_TO_REFACTOR); + + // Assert + successfulTemplateGenerationAssertions(); + assertCFNCalls(); }); it('should fail to generate when no applicable categories are found', async () => { @@ -186,4 +203,196 @@ describe('TemplateGenerator', () => { mockCfnClientSendMock.mockImplementationOnce(failureSendMock).mockImplementationOnce(failureSendMock); await expect(() => generator.generate()).rejects.toEqual(new Error('No corresponding category found in Gen2 for storage category')); }); + + it('should throw exception when update stack fails', async () => { + // Arrange + const errorMessage = 'Malformed template'; + mockCfnClientSendMock.mockImplementation((command) => { + if (command instanceof DescribeStackResourcesCommand) { + return Promise.resolve( + command.input.StackName === GEN1_ROOT_STACK_NAME ? mockDescribeGen1StackResources : mockDescribeGen2StackResources, + ); + } + if (command instanceof UpdateStackCommand) { + throw new Error(errorMessage); + } + return Promise.resolve({}); + }); + + // Act + Assert + const generator = new TemplateGenerator(GEN1_ROOT_STACK_NAME, GEN2_ROOT_STACK_NAME, ACCOUNT_ID, MOCK_CFN_CLIENT); + await expect(generator.generate()).rejects.toThrow(errorMessage); + }); + + it('should skip update if already updated', async () => { + // Arrange + mockCfnClientSendMock.mockImplementation((command) => { + if (command instanceof DescribeStackResourcesCommand) { + return Promise.resolve( + command.input.StackName === GEN1_ROOT_STACK_NAME ? mockDescribeGen1StackResources : mockDescribeGen2StackResources, + ); + } + if (command instanceof UpdateStackCommand) { + throw new Error('No updates are to be performed'); + } + if (command instanceof DescribeStacksCommand) { + return Promise.resolve({ + Stacks: [{ StackStatus: 'UPDATE_COMPLETE' }], + }); + } + return Promise.resolve({}); + }); + + // Act + const generator = new TemplateGenerator(GEN1_ROOT_STACK_NAME, GEN2_ROOT_STACK_NAME, ACCOUNT_ID, MOCK_CFN_CLIENT); + await generator.generate(); + + // Assert + successfulTemplateGenerationAssertions(); + assertCFNCalls(true); + }); + + it('should fail after all poll attempts have exhausted', async () => { + // Arrange + mockCfnClientSendMock.mockImplementation((command) => { + if (command instanceof DescribeStackResourcesCommand) { + return Promise.resolve( + command.input.StackName === GEN1_ROOT_STACK_NAME ? mockDescribeGen1StackResources : mockDescribeGen2StackResources, + ); + } + if (command instanceof UpdateStackCommand) { + return Promise.resolve({}); + } + if (command instanceof DescribeStacksCommand) { + return Promise.resolve({ + Stacks: [{ StackStatus: 'UPDATE_IN_PROGRESS' }], + }); + } + return Promise.resolve({}); + }); + + // Act + Assert + const generator = new TemplateGenerator(GEN1_ROOT_STACK_NAME, GEN2_ROOT_STACK_NAME, ACCOUNT_ID, MOCK_CFN_CLIENT); + expect.assertions(1); + // Intentionally not awaiting the below call to be able to advance timers and micro task queue in waitForPromisesAndFakeTimers + // and catch the error below + generator.generate().catch((e) => { + expect(e.message).toBe( + `Stack ${getStackId(GEN1_ROOT_STACK_NAME, 'auth')} did not reach a completion state within the given time period.`, + ); + }); + await waitForPromisesAndFakeTimers(); + return; + }); }); + +function successfulTemplateGenerationAssertions() { + expect(fs.mkdir).toBeCalledTimes(NUM_CATEGORIES_TO_REFACTOR + 1); + expect(mockGenerateGen1PreProcessTemplate).toBeCalledTimes(NUM_CATEGORIES_TO_REFACTOR); + expect(mockGenerateGen2ResourceRemovalTemplate).toBeCalledTimes(NUM_CATEGORIES_TO_REFACTOR); + expect(mockGenerateStackRefactorTemplates).toBeCalledTimes(NUM_CATEGORIES_TO_REFACTOR); + expect(mockReadMeInitialize).toBeCalledTimes(NUM_CATEGORIES_TO_REFACTOR); + expect(mockReadMeRenderStep1).toBeCalledTimes(NUM_CATEGORIES_TO_REFACTOR); + expect(mockReadMeRenderStep2).toBeCalledTimes(NUM_CATEGORIES_TO_REFACTOR); +} + +function assertCFNCalls(skipUpdate = false) { + expect(mockCfnClientSendMock.mock.calls[0]).toBeACloudFormationCommand( + { + StackName: GEN1_ROOT_STACK_NAME, + }, + DescribeStackResourcesCommand, + ); + expect(mockCfnClientSendMock.mock.calls[1]).toBeACloudFormationCommand( + { + StackName: GEN2_ROOT_STACK_NAME, + }, + DescribeStackResourcesCommand, + ); + + // If updates are skipped, there are no describe stack calls + if (!skipUpdate) { + expect(mockCfnClientSendMock.mock.calls[3]).toBeACloudFormationCommand( + { + StackName: getStackId(GEN1_ROOT_STACK_NAME, 'auth'), + }, + DescribeStacksCommand, + ); + expect(mockCfnClientSendMock.mock.calls[5]).toBeACloudFormationCommand( + { + StackName: getStackId(GEN2_ROOT_STACK_NAME, 'auth'), + }, + DescribeStacksCommand, + ); + expect(mockCfnClientSendMock.mock.calls[7]).toBeACloudFormationCommand( + { + StackName: getStackId(GEN1_ROOT_STACK_NAME, 'storage'), + }, + DescribeStacksCommand, + ); + expect(mockCfnClientSendMock.mock.calls[9]).toBeACloudFormationCommand( + { + StackName: getStackId(GEN2_ROOT_STACK_NAME, 'storage'), + }, + DescribeStacksCommand, + ); + } + + let updateStackCallIndex = 2; + const updateStackCallIndexInterval = skipUpdate ? 1 : 2; + expect(mockCfnClientSendMock.mock.calls[updateStackCallIndex]).toBeACloudFormationCommand( + { + StackName: getStackId(GEN1_ROOT_STACK_NAME, 'auth'), + Capabilities: ['CAPABILITY_NAMED_IAM'], + Parameters: [], + TemplateBody: JSON.stringify({}), + Tags: [], + }, + UpdateStackCommand, + ); + updateStackCallIndex += updateStackCallIndexInterval; + expect(mockCfnClientSendMock.mock.calls[updateStackCallIndex]).toBeACloudFormationCommand( + { + StackName: getStackId(GEN2_ROOT_STACK_NAME, 'auth'), + Capabilities: ['CAPABILITY_NAMED_IAM'], + Parameters: [], + TemplateBody: JSON.stringify({}), + Tags: [], + }, + UpdateStackCommand, + ); + + updateStackCallIndex += updateStackCallIndexInterval; + expect(mockCfnClientSendMock.mock.calls[updateStackCallIndex]).toBeACloudFormationCommand( + { + StackName: getStackId(GEN1_ROOT_STACK_NAME, 'storage'), + Capabilities: ['CAPABILITY_NAMED_IAM'], + Parameters: [], + TemplateBody: JSON.stringify({}), + Tags: [], + }, + UpdateStackCommand, + ); + + updateStackCallIndex += updateStackCallIndexInterval; + expect(mockCfnClientSendMock.mock.calls[updateStackCallIndex]).toBeACloudFormationCommand( + { + StackName: getStackId(GEN2_ROOT_STACK_NAME, 'storage'), + Capabilities: ['CAPABILITY_NAMED_IAM'], + Parameters: [], + TemplateBody: JSON.stringify({}), + Tags: [], + }, + UpdateStackCommand, + ); +} + +const waitForPromisesAndFakeTimers = async () => { + do { + jest.runAllTimers(); + await new Promise(jest.requireActual('timers').setImmediate); + } while (jest.getTimerCount() > 0); +}; + +const getStackId = (stackName: string, category: 'auth' | 'storage') => + `arn:aws:cloudformation:us-east-1:${ACCOUNT_ID}:stack/${stackName}-${category}/12345`; diff --git a/packages/amplify-migration-template-gen/src/template-generator.ts b/packages/amplify-migration-template-gen/src/template-generator.ts index 7a4b24765f..bb69bf3c96 100644 --- a/packages/amplify-migration-template-gen/src/template-generator.ts +++ b/packages/amplify-migration-template-gen/src/template-generator.ts @@ -2,8 +2,9 @@ import { CloudFormationClient, DescribeStackResourcesCommand } from '@aws-sdk/cl import assert from 'node:assert'; import CategoryTemplateGenerator from './category-template-generator'; import fs from 'node:fs/promises'; -import { CATEGORY, CFN_AUTH_TYPE, CFN_CATEGORY_TYPE, CFN_S3_TYPE, CFNResource } from './types'; +import { CATEGORY, CFN_AUTH_TYPE, CFN_CATEGORY_TYPE, CFN_S3_TYPE, CFNResource, CFNStackStatus } from './types'; import MigrationReadmeGenerator from './migration-readme-generator'; +import { tryUpdateStack } from './cfn-stack-updater'; const CFN_RESOURCE_STACK_TYPE = 'AWS::CloudFormation::Stack'; @@ -130,21 +131,33 @@ class TemplateGenerator { parameters: gen1StackParameters, } = await categoryTemplateGenerator.generateGen1PreProcessTemplate(); assert(gen1StackParameters); - await migrationReadMeGenerator.renderStep1(oldGen1Template, newGen1Template, gen1StackParameters); + console.log(`Updating Gen1 ${category} stack...`); + const gen1StackUpdateStatus = await tryUpdateStack(this.cfnClient, gen1CategoryStackId, gen1StackParameters, newGen1Template); + assert(gen1StackUpdateStatus === CFNStackStatus.UPDATE_COMPLETE); + console.log(`Updated Gen1 ${category} stack successfully`); const { oldTemplate: oldGen2Template, newTemplate: newGen2Template, parameters: gen2StackParameters, } = await categoryTemplateGenerator.generateGen2ResourceRemovalTemplate(); - await migrationReadMeGenerator.renderStep2(oldGen2Template, newGen2Template, gen2StackParameters); + console.log(`Updating Gen2 ${category} stack...`); + const gen2StackUpdateStatus = await tryUpdateStack( + this.cfnClient, + gen2CategoryStackId, + gen2StackParameters ?? [], + newGen2Template, + 60, + ); + assert(gen2StackUpdateStatus === CFNStackStatus.UPDATE_COMPLETE); + console.log(`Updated Gen2 ${category} stack successfully`); const { sourceTemplate, destinationTemplate, logicalIdMapping } = categoryTemplateGenerator.generateStackRefactorTemplates( newGen1Template, newGen2Template, ); - await migrationReadMeGenerator.renderStep3(sourceTemplate, destinationTemplate, logicalIdMapping, newGen1Template, newGen2Template); - await migrationReadMeGenerator.renderStep4(); + await migrationReadMeGenerator.renderStep1(sourceTemplate, destinationTemplate, logicalIdMapping, newGen1Template, newGen2Template); + await migrationReadMeGenerator.renderStep2(); } } } diff --git a/packages/amplify-migration-template-gen/src/types.ts b/packages/amplify-migration-template-gen/src/types.ts index 1307a1d424..1d2d2ae00c 100644 --- a/packages/amplify-migration-template-gen/src/types.ts +++ b/packages/amplify-migration-template-gen/src/types.ts @@ -98,3 +98,7 @@ export type CFN_CATEGORY_TYPE = CFN_AUTH_TYPE | CFN_S3_TYPE; export enum CFN_PSEUDO_PARAMETERS_REF { StackName = 'AWS::StackName', } + +export enum CFNStackStatus { + UPDATE_COMPLETE = 'UPDATE_COMPLETE', +} diff --git a/yarn.lock b/yarn.lock index ad68ead63c..b63be69cca 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1812,6 +1812,7 @@ __metadata: resolution: "@aws-amplify/migrate-template-gen@workspace:packages/amplify-migration-template-gen" dependencies: "@aws-sdk/client-cloudformation": ^3.592.0 + "@jest/globals": ^29.7.0 jest: ^29.5.0 typescript: ^5.4.5 languageName: unknown From 634dcd3f7301d8db5e80db3f206b94dc03895068 Mon Sep 17 00:00:00 2001 From: rjabhi Date: Thu, 21 Nov 2024 10:23:05 -0800 Subject: [PATCH 2/5] fix: remove dead code from migrate readme --- .../src/migration-readme-generator.ts | 120 ++---------------- 1 file changed, 12 insertions(+), 108 deletions(-) diff --git a/packages/amplify-migration-template-gen/src/migration-readme-generator.ts b/packages/amplify-migration-template-gen/src/migration-readme-generator.ts index c8b76a39fc..d9cfd821a7 100644 --- a/packages/amplify-migration-template-gen/src/migration-readme-generator.ts +++ b/packages/amplify-migration-template-gen/src/migration-readme-generator.ts @@ -29,102 +29,6 @@ class MigrationReadmeGenerator { await fs.writeFile(this.migrationReadMePath, `## Stack refactor steps for ${this.category} category\n`, { encoding: 'utf8' }); } - // /** - // * Resolves outputs and dependencies to prepare for refactor - // * @param oldGen1StackTemplate - // * @param newGen1StackTemplate - // * @param parameters - // */ - // async renderStep1(oldGen1StackTemplate: CFNTemplate, newGen1StackTemplate: CFNTemplate, parameters: Parameter[]): Promise { - // const step1FileNamePath = `${this.path}/step1-gen1PreProcessUpdateStackTemplate.json`; - // const step1RollbackFileNamePath = `${this.path}/step1-gen1PreProcessUpdateStackTemplate-rollback.json`; - // const paramsString = JSON.stringify(parameters); - // await fs.appendFile( - // this.migrationReadMePath, - // `### STEP 1: UPDATE GEN-1 ${this.category.toUpperCase()} STACK - // It is a non-disruptive update since the template only replaces resource references with their resolved values. This is a required step to execute cloudformation stack refactor later. - // \`\`\` - // aws cloudformation update-stack \\ - // --stack-name ${this.gen1CategoryStackName} \\ - // --template-body file://${step1FileNamePath} \\ - // --parameters '${paramsString}' \\ - // --capabilities CAPABILITY_NAMED_IAM \\ - // --tags '[]' - // \`\`\` - // - // \`\`\` - // aws cloudformation describe-stacks \\ - // --stack-name ${this.gen1CategoryStackName} - // \`\`\` - // - // #### Rollback step: - // \`\`\` - // aws cloudformation update-stack \\ - // --stack-name ${this.gen1CategoryStackName} \\ - // --template-body file://${step1RollbackFileNamePath} \\ - // --parameters '${paramsString}' \\ - // --capabilities CAPABILITY_NAMED_IAM - // \`\`\` - // - // \`\`\` - // aws cloudformation describe-stacks \\ - // --stack-name ${this.gen1CategoryStackName} - // \`\`\` - // `, - // { encoding: 'utf8' }, - // ); - // await fs.writeFile(step1FileNamePath, JSON.stringify(newGen1StackTemplate, null, 2), { encoding: 'utf8' }); - // await fs.writeFile(step1RollbackFileNamePath, JSON.stringify(oldGen1StackTemplate, null, 2), { encoding: 'utf8' }); - // } - // - // /** - // * Removes Gen2 resources from Gen2 stack to prepare for refactor - // * @param oldGen2StackTemplate - // * @param newGen2StackTemplate - // * @param parameters - // */ - // async renderStep2(oldGen2StackTemplate: CFNTemplate, newGen2StackTemplate: CFNTemplate, parameters: Parameter[] = []): Promise { - // const step2FileNamePath = `${this.path}/step2-gen2ResourcesRemovalStackTemplate.json`; - // const step2RollbackFileNamePath = `${this.path}/step2-gen2ResourcesRemovalStackTemplate-rollback.json`; - // const paramsString = JSON.stringify(parameters); - // await fs.appendFile( - // this.migrationReadMePath, - // `### STEP 2: REMOVE GEN-2 ${this.category.toUpperCase()} STACK RESOURCES - // This step is required since we will eventually replace gen-2 resources with gen-1 resources as part of Step 3 (refactor). - // \`\`\` - // aws cloudformation update-stack \\ - // --stack-name ${this.gen2CategoryStackName} \\ - // --template-body file://${step2FileNamePath} \\ - // --parameters '${paramsString}' \\ - // --capabilities CAPABILITY_NAMED_IAM \\ - // --tags '[]' - // \`\`\` - // - // \`\`\` - // aws cloudformation describe-stacks \\ - // --stack-name ${this.gen2CategoryStackName} - // \`\`\` - // - // #### Rollback step: - // \`\`\` - // aws cloudformation update-stack \\ - // --stack-name ${this.gen2CategoryStackName} \\ - // --template-body file://${step2RollbackFileNamePath} \\ - // --parameters '${paramsString}' \\ - // --capabilities CAPABILITY_NAMED_IAM - // \`\`\` - // - // \`\`\` - // aws cloudformation describe-stacks \\ - // --stack-name ${this.gen2CategoryStackName} - // \`\`\` - // `, - // { encoding: 'utf8' }, - // ); - // await fs.writeFile(step2FileNamePath, JSON.stringify(newGen2StackTemplate, null, 2), { encoding: 'utf8' }); - // await fs.writeFile(step2RollbackFileNamePath, JSON.stringify(oldGen2StackTemplate, null, 2), { encoding: 'utf8' }); - // } - /** * Creates and executes Stack refactor operation for the given category * @param sourceTemplate @@ -143,10 +47,10 @@ class MigrationReadmeGenerator { const rollbackSourceTemplateFileName = 'step3-sourceTemplate-rollback.json'; const rollbackDestinationTemplateFileName = 'step3-destinationTemplate-rollback.json'; - const step3SourceTemplateFileNamePath = `${this.path}/${sourceTemplateFileName}`; - const step3DestinationTemplateFileNamePath = `${this.path}/${destinationTemplateFileName}`; - const step3RollbackSourceTemplateFileNamePath = `${this.path}/${rollbackSourceTemplateFileName}`; - const step3RollbackDestinationTemplateFileNamePath = `${this.path}/${rollbackDestinationTemplateFileName}`; + const step1SourceTemplateFileNamePath = `${this.path}/${sourceTemplateFileName}`; + const step1DestinationTemplateFileNamePath = `${this.path}/${destinationTemplateFileName}`; + const step1RollbackSourceTemplateFileNamePath = `${this.path}/${rollbackSourceTemplateFileName}`; + const step1RollbackDestinationTemplateFileNamePath = `${this.path}/${rollbackDestinationTemplateFileName}`; const resourceMappings: ResourceMapping[] = []; const rollbackResourceMappings: ResourceMapping[] = []; @@ -183,11 +87,11 @@ export BUCKET_NAME=<> \`\`\` \`\`\` -aws s3 cp ${step3SourceTemplateFileNamePath} s3://$BUCKET_NAME +aws s3 cp ${step1SourceTemplateFileNamePath} s3://$BUCKET_NAME \`\`\` \`\`\` -aws s3 cp ${step3DestinationTemplateFileNamePath} s3://$BUCKET_NAME +aws s3 cp ${step1DestinationTemplateFileNamePath} s3://$BUCKET_NAME \`\`\` 1.b) Create stack refactor @@ -220,11 +124,11 @@ export STACK_REFACTOR_ID=<> #### Rollback step for refactor: \`\`\` -aws s3 cp ${step3RollbackSourceTemplateFileNamePath} s3://$BUCKET_NAME +aws s3 cp ${step1RollbackSourceTemplateFileNamePath} s3://$BUCKET_NAME \`\`\` \`\`\` -aws s3 cp ${step3RollbackDestinationTemplateFileNamePath} s3://$BUCKET_NAME +aws s3 cp ${step1RollbackDestinationTemplateFileNamePath} s3://$BUCKET_NAME \`\`\` \`\`\` @@ -256,10 +160,10 @@ Describe stack refactor to check for execution status `, { encoding: 'utf8' }, ); - await fs.writeFile(step3SourceTemplateFileNamePath, JSON.stringify(sourceTemplate, null, 2), { encoding: 'utf8' }); - await fs.writeFile(step3DestinationTemplateFileNamePath, JSON.stringify(destinationTemplate, null, 2), { encoding: 'utf8' }); - await fs.writeFile(step3RollbackSourceTemplateFileNamePath, JSON.stringify(oldSourceTemplate, null, 2), { encoding: 'utf8' }); - await fs.writeFile(step3RollbackDestinationTemplateFileNamePath, JSON.stringify(oldDestinationTemplate, null, 2), { encoding: 'utf8' }); + await fs.writeFile(step1SourceTemplateFileNamePath, JSON.stringify(sourceTemplate, null, 2), { encoding: 'utf8' }); + await fs.writeFile(step1DestinationTemplateFileNamePath, JSON.stringify(destinationTemplate, null, 2), { encoding: 'utf8' }); + await fs.writeFile(step1RollbackSourceTemplateFileNamePath, JSON.stringify(oldSourceTemplate, null, 2), { encoding: 'utf8' }); + await fs.writeFile(step1RollbackDestinationTemplateFileNamePath, JSON.stringify(oldDestinationTemplate, null, 2), { encoding: 'utf8' }); } async renderStep2() { From b1b32cc798c025c936305b5fae56636916dde77f Mon Sep 17 00:00:00 2001 From: rjabhi Date: Fri, 22 Nov 2024 09:31:46 -0800 Subject: [PATCH 3/5] fix(migrate-template-gen): lint warnings --- .../src/migration-readme-generator.ts | 1 - .../src/resolvers/cfn-output-resolver.ts | 1 - .../src/template-generator.ts | 14 ++++---------- 3 files changed, 4 insertions(+), 12 deletions(-) diff --git a/packages/amplify-migration-template-gen/src/migration-readme-generator.ts b/packages/amplify-migration-template-gen/src/migration-readme-generator.ts index d9cfd821a7..3e0f341427 100644 --- a/packages/amplify-migration-template-gen/src/migration-readme-generator.ts +++ b/packages/amplify-migration-template-gen/src/migration-readme-generator.ts @@ -1,6 +1,5 @@ import fs from 'node:fs/promises'; import { CATEGORY, CFNTemplate, ResourceMapping } from './types'; -import { Parameter } from '@aws-sdk/client-cloudformation'; import extractStackNameFromId from './cfn-stack-name-extractor'; interface MigrationReadMeGeneratorOptions { diff --git a/packages/amplify-migration-template-gen/src/resolvers/cfn-output-resolver.ts b/packages/amplify-migration-template-gen/src/resolvers/cfn-output-resolver.ts index a66e70b81b..aae1aade83 100644 --- a/packages/amplify-migration-template-gen/src/resolvers/cfn-output-resolver.ts +++ b/packages/amplify-migration-template-gen/src/resolvers/cfn-output-resolver.ts @@ -94,7 +94,6 @@ class CfnOutputResolver { /** * Build a custom replacer function to replace Fn::GetAtt references with resource attribute values. - * @param resource * @param record * @private */ diff --git a/packages/amplify-migration-template-gen/src/template-generator.ts b/packages/amplify-migration-template-gen/src/template-generator.ts index bb69bf3c96..997d1d50be 100644 --- a/packages/amplify-migration-template-gen/src/template-generator.ts +++ b/packages/amplify-migration-template-gen/src/template-generator.ts @@ -125,22 +125,16 @@ class TemplateGenerator { }); await migrationReadMeGenerator.initialize(); - const { - oldTemplate: oldGen1Template, - newTemplate: newGen1Template, - parameters: gen1StackParameters, - } = await categoryTemplateGenerator.generateGen1PreProcessTemplate(); + const { newTemplate: newGen1Template, parameters: gen1StackParameters } = + await categoryTemplateGenerator.generateGen1PreProcessTemplate(); assert(gen1StackParameters); console.log(`Updating Gen1 ${category} stack...`); const gen1StackUpdateStatus = await tryUpdateStack(this.cfnClient, gen1CategoryStackId, gen1StackParameters, newGen1Template); assert(gen1StackUpdateStatus === CFNStackStatus.UPDATE_COMPLETE); console.log(`Updated Gen1 ${category} stack successfully`); - const { - oldTemplate: oldGen2Template, - newTemplate: newGen2Template, - parameters: gen2StackParameters, - } = await categoryTemplateGenerator.generateGen2ResourceRemovalTemplate(); + const { newTemplate: newGen2Template, parameters: gen2StackParameters } = + await categoryTemplateGenerator.generateGen2ResourceRemovalTemplate(); console.log(`Updating Gen2 ${category} stack...`); const gen2StackUpdateStatus = await tryUpdateStack( this.cfnClient, From c5c92f65940a17f275a489477afa6b8a497e0654 Mon Sep 17 00:00:00 2001 From: rjabhi Date: Fri, 22 Nov 2024 10:42:16 -0800 Subject: [PATCH 4/5] fix: increase default poll attemps and poll interval --- .../src/cfn-stack-updater.ts | 4 ++-- .../src/template-generator.ts | 8 +------- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/packages/amplify-migration-template-gen/src/cfn-stack-updater.ts b/packages/amplify-migration-template-gen/src/cfn-stack-updater.ts index 0763b9bf5c..c446e79d68 100644 --- a/packages/amplify-migration-template-gen/src/cfn-stack-updater.ts +++ b/packages/amplify-migration-template-gen/src/cfn-stack-updater.ts @@ -2,8 +2,8 @@ import { CloudFormationClient, DescribeStacksCommand, Parameter, UpdateStackComm import { CFNTemplate } from './types'; import assert from 'node:assert'; -const POLL_ATTEMPTS = 30; -const POLL_INTERVAL_MS = 1000; +const POLL_ATTEMPTS = 60; +const POLL_INTERVAL_MS = 1500; const NO_UPDATES_MESSAGE = 'No updates are to be performed'; const CFN_IAM_CAPABILIY = 'CAPABILITY_NAMED_IAM'; const COMPLETION_STATE = '_COMPLETE'; diff --git a/packages/amplify-migration-template-gen/src/template-generator.ts b/packages/amplify-migration-template-gen/src/template-generator.ts index 997d1d50be..87aa77e1c8 100644 --- a/packages/amplify-migration-template-gen/src/template-generator.ts +++ b/packages/amplify-migration-template-gen/src/template-generator.ts @@ -136,13 +136,7 @@ class TemplateGenerator { const { newTemplate: newGen2Template, parameters: gen2StackParameters } = await categoryTemplateGenerator.generateGen2ResourceRemovalTemplate(); console.log(`Updating Gen2 ${category} stack...`); - const gen2StackUpdateStatus = await tryUpdateStack( - this.cfnClient, - gen2CategoryStackId, - gen2StackParameters ?? [], - newGen2Template, - 60, - ); + const gen2StackUpdateStatus = await tryUpdateStack(this.cfnClient, gen2CategoryStackId, gen2StackParameters ?? [], newGen2Template); assert(gen2StackUpdateStatus === CFNStackStatus.UPDATE_COMPLETE); console.log(`Updated Gen2 ${category} stack successfully`); From b52381c99372efcd5c75c4c9bee295dae4390594 Mon Sep 17 00:00:00 2001 From: rjabhi Date: Fri, 22 Nov 2024 16:00:00 -0800 Subject: [PATCH 5/5] chore: update comment to reflect polling interval --- .../amplify-migration-template-gen/src/cfn-stack-updater.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/amplify-migration-template-gen/src/cfn-stack-updater.ts b/packages/amplify-migration-template-gen/src/cfn-stack-updater.ts index c446e79d68..8f5ceca502 100644 --- a/packages/amplify-migration-template-gen/src/cfn-stack-updater.ts +++ b/packages/amplify-migration-template-gen/src/cfn-stack-updater.ts @@ -14,7 +14,7 @@ export const UPDATE_COMPLETE = 'UPDATE_COMPLETE'; * @param stackName * @param parameters * @param templateBody - * @param attempts number of attempts to poll CFN stack for update completion state. The interval between the polls is 1 second. + * @param attempts number of attempts to poll CFN stack for update completion state. The interval between the polls is 1.5 seconds. */ export async function tryUpdateStack( cfnClient: CloudFormationClient,