Skip to content

Commit

Permalink
Merge pull request #14024 from aws-amplify/gen2-migrations-execute
Browse files Browse the repository at this point in the history
feat: automate preprocessing steps for refactor
  • Loading branch information
abhi7cr authored Nov 25, 2024
2 parents e6f36b0 + b52381c commit 3e93c87
Show file tree
Hide file tree
Showing 11 changed files with 423 additions and 281 deletions.
6 changes: 5 additions & 1 deletion packages/amplify-migration-template-gen/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"type": "commonjs",
"main": "lib/index.js",
"devDependencies": {
"@jest/globals": "^29.7.0",
"jest": "^29.5.0",
"typescript": "^5.4.5"
},
Expand Down Expand Up @@ -31,7 +32,10 @@
"json",
"node"
],
"collectCoverage": true
"collectCoverage": true,
"setupFilesAfterEnv": [
"<rootDir>/src/setup-jest.ts"
]
},
"publishConfig": {
"access": "public"
Expand Down
70 changes: 70 additions & 0 deletions packages/amplify-migration-template-gen/src/cfn-stack-updater.ts
Original file line number Diff line number Diff line change
@@ -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 = 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';
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.5 seconds.
*/
export async function tryUpdateStack(
cfnClient: CloudFormationClient,
stackName: string,
parameters: Parameter[],
templateBody: CFNTemplate,
attempts = POLL_ATTEMPTS,
): Promise<string> {
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<string> {
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.`);
}
Original file line number Diff line number Diff line change
@@ -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<R> {
toBeACloudFormationCommand(expectedInput: CFNCommandInput, expectedType: CFNCommandType): R;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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=<<YOUR_BUCKET_NAME>>
\`\`\`
Expand All @@ -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\"}}]'
\`\`\`
Expand All @@ -174,17 +85,17 @@ aws cloudformation create-stack-refactor --stack-definitions StackName=amplify-
export STACK_REFACTOR_ID=<<REFACTOR-ID-FROM-CREATE-STACK-REFACTOR_CALL>>
\`\`\`
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
\`\`\`
Expand Down Expand Up @@ -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
\`\`\`
Expand Down
Loading

0 comments on commit 3e93c87

Please sign in to comment.