Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: automate preprocessing steps for refactor #14024

Merged
merged 5 commits into from
Nov 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
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;
abhi7cr marked this conversation as resolved.
Show resolved Hide resolved
}
}

/**
* 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)) {
abhi7cr marked this conversation as resolved.
Show resolved Hide resolved
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
Loading