Skip to content

Commit ac24dd8

Browse files
authored
Merge pull request #14120 from aws-amplify/gen2-migrations-execute
Gen2 migrations revert command
2 parents e4800f6 + 13e68f1 commit ac24dd8

14 files changed

Lines changed: 1341 additions & 704 deletions

packages/amplify-migration-template-gen/API.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ export class TemplateGenerator {
1313
constructor(fromStack: string, toStack: string, accountId: string, cfnClient: CloudFormationClient, ssmClient: SSMClient, cognitoIdpClient: CognitoIdentityProviderClient, appId: string, environmentName: string);
1414
// (undocumented)
1515
generate(): Promise<boolean>;
16+
// (undocumented)
17+
revert(): Promise<boolean>;
1618
}
1719

1820
// (No @packageDocumentation comment for this package)

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

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import assert from 'node:assert';
44
import {
55
CFN_AUTH_TYPE,
66
CFN_CATEGORY_TYPE,
7+
CFN_IAM_TYPE,
78
CFNChangeTemplateWithParams,
89
CFNResource,
910
CFNStackRefactorTemplates,
@@ -22,6 +23,11 @@ const HOSTED_PROVIDER_CREDENTIALS_PARAMETER_NAME = 'hostedUIProviderCreds';
2223
const USER_POOL_ID_OUTPUT_KEY_NAME = 'UserPoolId';
2324
const GEN1_WEB_APP_CLIENT = 'UserPoolClientWeb';
2425
const GEN2_NATIVE_APP_CLIENT = 'UserPoolNativeAppClient';
26+
const RESOURCE_TYPES_WITH_MULTIPLE_RESOURCES = [
27+
CFN_AUTH_TYPE.UserPoolClient.valueOf(),
28+
CFN_AUTH_TYPE.UserPoolGroup.valueOf(),
29+
CFN_IAM_TYPE.Role.valueOf(),
30+
];
2531

2632
class CategoryTemplateGenerator<CFNCategoryType extends CFN_CATEGORY_TYPE> {
2733
private gen1DescribeStacksResponse: Stack | undefined;
@@ -121,7 +127,7 @@ class CategoryTemplateGenerator<CFNCategoryType extends CFN_CATEGORY_TYPE> {
121127
return this.generateRefactorTemplates(this.gen1ResourcesToMove, this.gen2ResourcesToRemove, gen1Template, gen2Template);
122128
}
123129

124-
private async readTemplate(stackId: string) {
130+
public async readTemplate(stackId: string) {
125131
const getTemplateResponse = await this.cfnClient.send(
126132
new GetTemplateCommand({
127133
StackName: stackId,
@@ -132,7 +138,7 @@ class CategoryTemplateGenerator<CFNCategoryType extends CFN_CATEGORY_TYPE> {
132138
return JSON.parse(templateBody) as CFNTemplate;
133139
}
134140

135-
private async describeStack(stackId: string) {
141+
public async describeStack(stackId: string) {
136142
return (
137143
await this.cfnClient.send(
138144
new DescribeStacksCommand({
@@ -191,9 +197,15 @@ class CategoryTemplateGenerator<CFNCategoryType extends CFN_CATEGORY_TYPE> {
191197
// In gen1, we differentiate clients with Web. In gen2, we differentiate with Native.
192198
const isWebClient = gen1ResourceLogicalId === GEN1_WEB_APP_CLIENT && !gen2ResourceLogicalId.includes(GEN2_NATIVE_APP_CLIENT);
193199
const isNativeClient = gen1ResourceLogicalId !== GEN1_WEB_APP_CLIENT && gen2ResourceLogicalId.includes(GEN2_NATIVE_APP_CLIENT);
200+
const foundUserPoolClientPair = gen1Resource.Type === CFN_AUTH_TYPE.UserPoolClient && (isWebClient || isNativeClient);
201+
const foundUserPoolGroupPair =
202+
gen1Resource.Type === CFN_AUTH_TYPE.UserPoolGroup && gen2ResourceLogicalId.includes(gen1ResourceLogicalId);
203+
const foundIamRolePair = gen1Resource.Type === CFN_IAM_TYPE.Role && gen2ResourceLogicalId.includes(gen1ResourceLogicalId);
194204
if (
195-
gen1Resource.Type !== CFN_AUTH_TYPE.UserPoolClient ||
196-
(gen1Resource.Type === CFN_AUTH_TYPE.UserPoolClient && (isWebClient || isNativeClient))
205+
!RESOURCE_TYPES_WITH_MULTIPLE_RESOURCES.includes(gen1Resource.Type) ||
206+
foundUserPoolClientPair ||
207+
foundUserPoolGroupPair ||
208+
foundIamRolePair
197209
) {
198210
gen1ToGen2ResourceLogicalIdMapping.set(gen1ResourceLogicalId, gen2ResourceLogicalId);
199211
clonedGen1ResourceMap.delete(gen1ResourceLogicalId);
@@ -219,16 +231,17 @@ class CategoryTemplateGenerator<CFNCategoryType extends CFN_CATEGORY_TYPE> {
219231
return resolvedRefsGen2Template;
220232
}
221233

222-
private generateRefactorTemplates(
234+
public generateRefactorTemplates(
223235
gen1ResourcesToMove: Map<string, CFNResource>,
224236
gen2ResourcesToRemove: Map<string, CFNResource>,
225237
gen1Template: CFNTemplate,
226238
gen2Template: CFNTemplate,
239+
sourceToDestinationResourceLogicalIdMapping?: Map<string, string>,
227240
): CFNStackRefactorTemplates {
228241
const gen1LogicalResourceIds = [...gen1ResourcesToMove.keys()];
229-
const gen1StackOutputs = this.gen1DescribeStacksResponse?.Outputs;
230-
assert(gen1StackOutputs);
231-
const gen1ToGen2ResourceLogicalIdMapping = this.buildGen1ToGen2ResourceLogicalIdMapping(gen1ResourcesToMove, gen2ResourcesToRemove);
242+
const gen1ToGen2ResourceLogicalIdMapping =
243+
sourceToDestinationResourceLogicalIdMapping ??
244+
this.buildGen1ToGen2ResourceLogicalIdMapping(gen1ResourcesToMove, gen2ResourcesToRemove);
232245
const clonedGen1Template = JSON.parse(JSON.stringify(gen1Template));
233246
const clonedGen2Template = JSON.parse(JSON.stringify(gen2Template));
234247
const gen2TemplateForRefactor = this.addGen1ResourcesToGen2Stack(
@@ -237,6 +250,7 @@ class CategoryTemplateGenerator<CFNCategoryType extends CFN_CATEGORY_TYPE> {
237250
gen1ToGen2ResourceLogicalIdMapping,
238251
clonedGen2Template,
239252
);
253+
240254
const gen1TemplateForRefactor = this.removeGen1ResourcesFromGen1Stack(clonedGen1Template, gen1LogicalResourceIds);
241255
return {
242256
sourceTemplate: gen1TemplateForRefactor,

packages/amplify-migration-template-gen/src/cfn-stack-refactor-updater.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ import {
99
StackRefactorStatus,
1010
} from '@aws-sdk/client-cloudformation';
1111
import assert from 'node:assert';
12-
import { FailedRefactorResponse } from './types';
12+
import { CFNStackStatus, FailedRefactorResponse } from './types';
13+
import { pollStackForCompletionState } from './cfn-stack-updater';
1314

1415
const POLL_ATTEMPTS = 30;
1516
const POLL_INTERVAL_MS = 1500;
@@ -78,6 +79,16 @@ export async function tryRefactorStack(
7879
},
7980
];
8081
}
82+
83+
const sourceStackName = createStackRefactorCommandInput.StackDefinitions?.[0].StackName;
84+
const destinationStackName = createStackRefactorCommandInput.StackDefinitions?.[1].StackName;
85+
assert(sourceStackName);
86+
assert(destinationStackName);
87+
const sourceStackStatus = await pollStackForCompletionState(cfnClient, sourceStackName);
88+
assert(sourceStackStatus === CFNStackStatus.UPDATE_COMPLETE, `${sourceStackName} was not updated successfully.`);
89+
const destinationStackStatus = await pollStackForCompletionState(cfnClient, destinationStackName);
90+
assert(destinationStackStatus === CFNStackStatus.UPDATE_COMPLETE, `${destinationStackName} was not updated successfully.`);
91+
8192
return [true, undefined];
8293
}
8394

packages/amplify-migration-template-gen/src/cfn-stack-updater.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,11 @@ export async function tryUpdateStack(
4949
* @param attempts number of attempts to poll for completion.
5050
* @returns the stack status
5151
*/
52-
export async function pollStackForCompletionState(cfnClient: CloudFormationClient, stackName: string, attempts: number): Promise<string> {
52+
export async function pollStackForCompletionState(
53+
cfnClient: CloudFormationClient,
54+
stackName: string,
55+
attempts: number = POLL_ATTEMPTS,
56+
): Promise<string> {
5357
do {
5458
const { Stacks } = await cfnClient.send(
5559
new DescribeStacksCommand({

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

Lines changed: 23 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -1,135 +1,67 @@
11
import MigrationReadMeGenerator from './migration-readme-generator';
22
import fs from 'node:fs/promises';
3-
import { CFNTemplate } from './types';
43

54
jest.mock('node:fs/promises');
65

76
describe('MigrationReadMeGenerator', () => {
87
const PATH = 'test';
9-
const CATEGORY = 'auth';
10-
const GEN1_CATEGORY_STACK_ID = 'arn:aws:cloudformation:us-east-1:1234567890:stack/amplify-testauth-dev-12345-auth-ABCDE/12345';
11-
const GEN2_CATEGORY_STACK_ID = 'arn:aws:cloudformation:us-east-1:1234567890:stack/amplify-mygen2app-test-sandbox-12345-auth-ABCDE/12345';
128
const migrationReadMeGenerator = new MigrationReadMeGenerator({
139
path: PATH,
14-
category: CATEGORY,
15-
gen1CategoryStackId: GEN1_CATEGORY_STACK_ID,
16-
gen2CategoryStackId: GEN2_CATEGORY_STACK_ID,
10+
categories: ['auth', 'storage'],
1711
});
18-
const oldStackTemplate: CFNTemplate = {
19-
Description: 'Gen1FooTemplate',
20-
AWSTemplateFormatVersion: 'AWSTemplateFormatVersion',
21-
Resources: {
22-
Gen1Foo: {
23-
Type: 'AWS::S3::Bucket',
24-
Properties: {
25-
Name: 'FooBucket',
26-
},
27-
},
28-
},
29-
Parameters: {},
30-
Outputs: {},
31-
};
32-
const newStackTemplate: CFNTemplate = {
33-
Description: 'Gen1FooTemplate',
34-
AWSTemplateFormatVersion: 'AWSTemplateFormatVersion',
35-
Resources: {
36-
Gen2Foo: {
37-
Type: 'AWS::S3::Bucket',
38-
Properties: {
39-
Name: 'FooBucket',
40-
},
41-
},
42-
},
43-
Parameters: {
44-
authSelections: {
45-
Type: 'String',
46-
},
47-
},
48-
Outputs: {},
49-
};
50-
const logicalIdMapping = new Map([['Gen1FooUserPool', 'Gen2FooUserPool']]);
5112

5213
it('should initialize migration readme', async () => {
5314
await migrationReadMeGenerator.initialize();
54-
expect(fs.writeFile).toHaveBeenCalledWith('test/MIGRATION_README.md', '## Stack refactor steps for auth category\n', {
15+
expect(fs.writeFile).toHaveBeenCalledWith('test/MIGRATION_README.md', '', {
5516
encoding: 'utf8',
5617
});
5718
});
5819

5920
it('should render step1', async () => {
60-
await migrationReadMeGenerator.renderStep1(oldStackTemplate, newStackTemplate, logicalIdMapping, oldStackTemplate, newStackTemplate);
21+
await migrationReadMeGenerator.renderStep1();
6122
expect(fs.appendFile).toHaveBeenCalledWith(
6223
'test/MIGRATION_README.md',
63-
`### STEP 1: CREATE AND EXECUTE CLOUDFORMATION STACK REFACTOR FOR auth CATEGORY
64-
This step will move the Gen1 auth resources to Gen2 stack.
65-
66-
1.a) Create stack refactor
67-
\`\`\`
68-
aws cloudformation create-stack-refactor --stack-definitions StackName=amplify-testauth-dev-12345-auth-ABCDE,TemplateBody@=file://test/step3-sourceTemplate.json StackName=amplify-mygen2app-test-sandbox-12345-auth-ABCDE,TemplateBody@=file://test/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\"}}]'
69-
\`\`\`
70-
71-
\`\`\`
72-
export STACK_REFACTOR_ID=<<REFACTOR-ID-FROM-CREATE-STACK-REFACTOR_CALL>>
73-
\`\`\`
74-
75-
1.b) Describe stack refactor to check for creation status
76-
\`\`\`
77-
aws cloudformation describe-stack-refactor --stack-refactor-id $STACK_REFACTOR_ID
78-
\`\`\`
79-
80-
1.c) Execute stack refactor
24+
`## REDEPLOY GEN2 APPLICATION
25+
1.a) Uncomment the following lines in \`amplify/backend.ts\` file
8126
\`\`\`
82-
aws cloudformation execute-stack-refactor --stack-refactor-id $STACK_REFACTOR_ID
83-
\`\`\`
84-
85-
1.d) Describe stack refactor to check for execution status
86-
\`\`\`
87-
aws cloudformation describe-stack-refactor --stack-refactor-id $STACK_REFACTOR_ID
27+
s3Bucket.bucketName = YOUR_GEN1_BUCKET_NAME;
8828
\`\`\`
8929
90-
#### Rollback step for refactor:
9130
\`\`\`
92-
aws cloudformation create-stack-refactor --stack-definitions StackName=amplify-testauth-dev-12345-auth-ABCDE,TemplateBody@=file://test/step3-sourceTemplate-rollback.json StackName=amplify-mygen2app-test-sandbox-12345-auth-ABCDE,TemplateBody@=file://test/step3-destinationTemplate-rollback.json --resource-mappings '[{\"Source\":{\"StackName\":\"amplify-mygen2app-test-sandbox-12345-auth-ABCDE\",\"LogicalResourceId\":\"Gen2FooUserPool\"},\"Destination\":{\"StackName\":\"amplify-testauth-dev-12345-auth-ABCDE\",\"LogicalResourceId\":\"Gen1FooUserPool\"}}]'
31+
s3Bucket.applyRemovalPolicy(RemovalPolicy.RETAIN, { applyToUpdateReplacePolicy: true });
9332
\`\`\`
9433
9534
\`\`\`
96-
export STACK_REFACTOR_ID=<<REFACTOR-ID-FROM-CREATE-STACK-REFACTOR_CALL>>
35+
cfnUserPool.applyRemovalPolicy(RemovalPolicy.RETAIN, { applyToUpdateReplacePolicy: true });
9736
\`\`\`
9837
99-
Describe stack refactor to check for creation status
10038
\`\`\`
101-
aws cloudformation describe-stack-refactor --stack-refactor-id $STACK_REFACTOR_ID
39+
cfnIdentityPool.applyRemovalPolicy(RemovalPolicy.RETAIN, { applyToUpdateReplacePolicy: true });
10240
\`\`\`
10341
104-
Execute stack refactor
10542
\`\`\`
106-
aws cloudformation execute-stack-refactor --stack-refactor-id $STACK_REFACTOR_ID
43+
Tags.of(backend.stack).add("gen1-migrated-app", "true");
10744
\`\`\`
10845
109-
Describe stack refactor to check for execution status
46+
1.b) Deploy sandbox using the below command or trigger a CI/CD build via hosting by committing this file to your Git repository
11047
\`\`\`
111-
aws cloudformation describe-stack-refactor --stack-refactor-id $STACK_REFACTOR_ID
48+
npx ampx sandbox
11249
\`\`\`
113-
`,
114-
{ encoding: 'utf8' },
50+
`,
11551
);
11652
});
11753

118-
it('should render step2', async () => {
119-
await migrationReadMeGenerator.renderStep2();
54+
it('should render step1 without storage', async () => {
55+
const PATH = 'test';
56+
const migrationReadMeGenerator = new MigrationReadMeGenerator({
57+
path: PATH,
58+
categories: ['auth'],
59+
});
60+
await migrationReadMeGenerator.renderStep1();
12061
expect(fs.appendFile).toHaveBeenCalledWith(
12162
'test/MIGRATION_README.md',
122-
`### STEP 2: REDEPLOY GEN2 APPLICATION
123-
This step will remove the hardcoded references from the template and replace them with resource references (where applicable).
124-
125-
2.a) Uncomment the following lines in \`amplify/backend.ts\` file to instruct CDK to use the gen1 S3 bucket (if storage is enabled) and apply retain removal policies for auth and/or storage resources
126-
\`\`\`
127-
s3Bucket.bucketName = YOUR_GEN1_BUCKET_NAME;
128-
\`\`\`
129-
130-
\`\`\`
131-
s3Bucket.applyRemovalPolicy(RemovalPolicy.RETAIN, { applyToUpdateReplacePolicy: true });
132-
\`\`\`
63+
`## REDEPLOY GEN2 APPLICATION
64+
1.a) Uncomment the following lines in \`amplify/backend.ts\` file
13365
13466
\`\`\`
13567
cfnUserPool.applyRemovalPolicy(RemovalPolicy.RETAIN, { applyToUpdateReplacePolicy: true });
@@ -143,7 +75,7 @@ cfnIdentityPool.applyRemovalPolicy(RemovalPolicy.RETAIN, { applyToUpdateReplaceP
14375
Tags.of(backend.stack).add("gen1-migrated-app", "true");
14476
\`\`\`
14577
146-
2.b) Deploy sandbox using the below command or trigger a CI/CD build via hosting by committing this file to your Git repository
78+
1.b) Deploy sandbox using the below command or trigger a CI/CD build via hosting by committing this file to your Git repository
14779
\`\`\`
14880
npx ampx sandbox
14981
\`\`\`

0 commit comments

Comments
 (0)