diff --git a/packages/amplify-migration-e2e/src/__tests__/migration_codegen_e2e.test.ts b/packages/amplify-migration-e2e/src/__tests__/migration_codegen_e2e.test.ts index fe5a87ad5cc..ede03c878ef 100644 --- a/packages/amplify-migration-e2e/src/__tests__/migration_codegen_e2e.test.ts +++ b/packages/amplify-migration-e2e/src/__tests__/migration_codegen_e2e.test.ts @@ -1,9 +1,8 @@ import path from 'node:path'; import assert from 'node:assert'; -import { createNewProjectDir, generateRandomShortId, npmInstall } from '@aws-amplify/amplify-e2e-core'; +import { createNewProjectDir, generateRandomShortId, getSocialProviders, npmInstall } from '@aws-amplify/amplify-e2e-core'; import { createGen2Renderer } from '@aws-amplify/amplify-gen2-codegen'; import { copyFunctionFile, removeErrorThrowsFromFunctionFile } from '../function_utils'; -import { copyGen1Schema } from '../api_utils'; import { cleanupProjects, setupAndPushDefaultGen1Project, @@ -54,17 +53,21 @@ void describe('Gen 2 Codegen E2E tests', () => { await cleanupProjects(projRoot, projName); }); - void it.only('should init a project & add auth, function, storage, api with defaults & perform full migration codegen flow', async () => { + void it('should init a project & add auth, function, storage, api with defaults & perform full migration codegen flow', async () => { + // Arrange await setupAndPushDefaultGen1Project(projRoot, projName); + + // Act const { gen1UserPoolId, gen1ClientIds, gen1IdentityPoolId, gen1FunctionName, gen1BucketName, gen1GraphqlApiId, gen1Region, envName } = await assertDefaultGen1Setup(projRoot); runCodegenCommand(projRoot); copyFunctionFile(projRoot, 'function', gen1FunctionName); - copyGen1Schema(projRoot, projName); removeErrorThrowsFromFunctionFile(projRoot, 'function', extractFunctionResourceName(gen1FunctionName, envName)); updateAmplifyBackendPackagesVersion(projRoot); npmInstall(projRoot); const gen2StackName = await runGen2SandboxCommand(projRoot, projName); + + // Assert await assertAuthResource(projRoot, gen1UserPoolId, gen1ClientIds, gen1IdentityPoolId, gen1Region); await assertStorageResource(projRoot, gen1BucketName, gen1Region); await assertFunctionResource(projRoot, gen2StackName, gen1FunctionName, gen1Region); @@ -72,7 +75,18 @@ void describe('Gen 2 Codegen E2E tests', () => { }); void it('should init a project where all possible auth options are selected and perform full migration codegen flow ', async () => { + // Arrange + const socialProviders = getSocialProviders(); + Object.entries(socialProviders).forEach(([socialProvider, value]) => { + // we expect APPLE_PRIVATE_KEY_2 in process.env but getSocialProviders returns as APPLE_PRIVATE_KEY + if (socialProvider === 'APPLE_PRIVATE_KEY') { + socialProvider = 'APPLE_PRIVATE_KEY_2'; + } + process.env[socialProvider] = process.env[socialProvider] ?? value; + }); await setupAndPushAuthWithMaxOptionsGen1Project(projRoot, projName); + + // Act const { gen1UserPoolId, gen1ClientIds, gen1IdentityPoolId, gen1FunctionName, gen1Region } = await assertAuthWithMaxOptionsGen1Setup( projRoot, ); @@ -84,19 +98,26 @@ void describe('Gen 2 Codegen E2E tests', () => { await toggleSandboxSecrets(projRoot, projName, 'set'); const gen2StackName = await runGen2SandboxCommand(projRoot, projName); await toggleSandboxSecrets(projRoot, projName, 'remove'); + + // Assert await assertAuthResource(projRoot, gen1UserPoolId, gen1ClientIds, gen1IdentityPoolId, gen1Region); await assertFunctionResource(projRoot, gen2StackName, gen1FunctionName, gen1Region); }); void it('should init a project where default auth, all possible s3 bucket resource options are selected and perform full migration codegen flow ', async () => { + // Arrange await setupAndPushStorageWithMaxOptionsGen1Project(projRoot, projName); - const { gen1UserPoolId, gen1ClientIds, gen1BucketName, gen1IdentityPoolId, gen1Region, gen1FunctionName } = + + // Act + const { gen1UserPoolId, gen1ClientIds, gen1BucketName, gen1IdentityPoolId, gen1Region, gen1FunctionName, envName } = await assertStorageWithMaxOptionsGen1Setup(projRoot); runCodegenCommand(projRoot); updateAmplifyBackendPackagesVersion(projRoot); npmInstall(projRoot); removeErrorThrowsFromFunctionFile(projRoot, 'storage', extractFunctionResourceName(gen1FunctionName, envName)); await runGen2SandboxCommand(projRoot, projName); + + // Assert await assertAuthResource(projRoot, gen1UserPoolId, gen1ClientIds, gen1IdentityPoolId, gen1Region); await assertStorageResource(projRoot, gen1BucketName, gen1Region); }); diff --git a/packages/amplify-migration-e2e/src/__tests__/migration_templategen_e2e.test.ts b/packages/amplify-migration-e2e/src/__tests__/migration_templategen_e2e.test.ts index ee28a080a3d..3a38d3d0f21 100644 --- a/packages/amplify-migration-e2e/src/__tests__/migration_templategen_e2e.test.ts +++ b/packages/amplify-migration-e2e/src/__tests__/migration_templategen_e2e.test.ts @@ -1,56 +1,59 @@ import path from 'node:path'; import assert from 'node:assert'; -import { createNewProjectDir, npmInstall, deleteS3Bucket, generateRandomShortId } from '@aws-amplify/amplify-e2e-core'; +import { createNewProjectDir, npmInstall, generateRandomShortId } from '@aws-amplify/amplify-e2e-core'; import { assertDefaultGen1Setup } from '../assertions'; -import { setupAndPushDefaultGen1Project, runCodegenCommand, runGen2SandboxCommand, cleanupProjects } from '..'; -import { copyFunctionFile } from '../function_utils'; -import { copyGen1Schema } from '../api_utils'; -import { createS3Bucket } from '../sdk_calls'; -import { runExecuteCommand, runRevertCommand } from '../templategen'; +import { + setupAndPushDefaultGen1Project, + runCodegenCommand, + runGen2SandboxCommand, + cleanupProjects, + extractFunctionResourceName, + updateAmplifyBackendPackagesVersion, +} from '..'; +import { copyFunctionFile, removeErrorThrowsFromFunctionFile } from '../function_utils'; +import { assertExecuteCommand, RefactorCategory, runExecuteCommand, runGen2DeployPostExecute, runRevertCommand } from '../templategen'; + +const CATEGORIES_TO_MOVE: RefactorCategory[] = ['auth', 'storage']; void describe('Templategen E2E tests', () => { void describe('Full Migration Templategen Flow', () => { let projRoot: string; let projName: string; - let bucketName: string; beforeEach(async () => { const baseDir = process.env.INIT_CWD ?? process.cwd(); projRoot = await createNewProjectDir('templategen_e2e_flow_test', path.join(baseDir, '..', '..')); projName = `test${generateRandomShortId()}`; - bucketName = `testbucket${generateRandomShortId()}`; }); afterEach(async () => { await cleanupProjects(projRoot, projName); - await deleteS3Bucket(bucketName); }); void it('should init a project & add auth, function, storage, api with defaults & perform refactor', async () => { + // Arrange await setupAndPushDefaultGen1Project(projRoot, projName); - const { gen1StackName, gen1FunctionName } = await assertDefaultGen1Setup(projRoot); - assert(gen1StackName); - runCodegenCommand(projRoot); - copyFunctionFile(projRoot, 'function', gen1FunctionName); - copyGen1Schema(projRoot, projName); - npmInstall(projRoot); - const gen2StackName = await runGen2SandboxCommand(projRoot, projName); - assert(gen2StackName); - runExecuteCommand(projRoot, gen1StackName, gen2StackName); - }); - void it('should init a project & add auth, function, storage, api with defaults, perform refactor and revert to the original state', async () => { - await setupAndPushDefaultGen1Project(projRoot, projName); - const { gen1StackName, gen1FunctionName, gen1Region } = await assertDefaultGen1Setup(projRoot); - await createS3Bucket(bucketName, gen1Region); + // Act + const { gen1StackName, gen1FunctionName, envName } = await assertDefaultGen1Setup(projRoot); assert(gen1StackName); runCodegenCommand(projRoot); copyFunctionFile(projRoot, 'function', gen1FunctionName); - copyGen1Schema(projRoot, projName); + removeErrorThrowsFromFunctionFile(projRoot, 'function', extractFunctionResourceName(gen1FunctionName, envName)); + updateAmplifyBackendPackagesVersion(projRoot); npmInstall(projRoot); + // Below env is only needed for CI/CD deployments and is expected to be set by customers for their app + // To emulate the migration in sandbox, we set it explicitly. + process.env.AMPLIFY_GEN_1_ENV_NAME = envName; const gen2StackName = await runGen2SandboxCommand(projRoot, projName); assert(gen2StackName); + runExecuteCommand(projRoot, gen1StackName, gen2StackName); + await runGen2DeployPostExecute(projRoot, projName, envName, CATEGORIES_TO_MOVE); + + // Assert + await assertExecuteCommand(projRoot, CATEGORIES_TO_MOVE); + runRevertCommand(projRoot, gen1StackName, gen2StackName); }); }); diff --git a/packages/amplify-migration-e2e/src/assertions.ts b/packages/amplify-migration-e2e/src/assertions.ts index 76a450f0130..25a8d2fae7e 100644 --- a/packages/amplify-migration-e2e/src/assertions.ts +++ b/packages/amplify-migration-e2e/src/assertions.ts @@ -13,6 +13,8 @@ import { removeProperties } from '.'; import { $TSAny } from '@aws-amplify/amplify-cli-core'; import assert from 'node:assert'; +const DATA_SOURCE_PROPS_TO_REMOVE = ['dataSourceArn', 'serviceRoleArn', 'dynamodbConfig']; + export async function assertUserPool(gen1Meta: $TSAny, gen1Region: string) { const { UserPoolId: gen1UserPoolId } = Object.keys(gen1Meta.auth).map((key) => gen1Meta.auth[key])[0].output; const cloudUserPool = await getUserPool(gen1UserPoolId, gen1Region); @@ -129,6 +131,7 @@ export async function assertAuthWithMaxOptionsGen1Setup(projRoot: string) { export async function assertStorageWithMaxOptionsGen1Setup(projRoot: string) { const gen1Meta = getProjectMeta(projRoot); + const gen1StackName = gen1Meta.providers.awscloudformation.StackName; const gen1Region = gen1Meta.providers.awscloudformation.Region; const { gen1BucketName } = await assertStorage(gen1Meta, gen1Region); const { gen1UserPoolId } = await assertUserPool(gen1Meta, gen1Region); @@ -136,10 +139,16 @@ export async function assertStorageWithMaxOptionsGen1Setup(projRoot: string) { assert.match(gen1FunctionName, /S3Trigger/); const { gen1ClientIds } = await assertUserPoolClients(gen1Meta, gen1Region); const { gen1IdentityPoolId } = await assertIdentityPool(gen1Meta, gen1Region); + const envName = gen1StackName.split('-')[2]; - return { gen1UserPoolId, gen1ClientIds, gen1BucketName, gen1IdentityPoolId, gen1Region, gen1FunctionName }; + return { gen1UserPoolId, gen1ClientIds, gen1BucketName, gen1IdentityPoolId, gen1Region, gen1FunctionName, envName }; } +const extractUserPoolNamePrefix = (userPoolName: string) => { + const [userPoolNamePrefix] = userPoolName.split('-'); + return userPoolNamePrefix; +}; + async function assertUserPoolResource(projRoot: string, gen1UserPoolId: string, gen1Region: string) { const gen1Resource = await getResourceDetails('AWS::Cognito::UserPool', gen1UserPoolId, gen1Region); removeProperties(gen1Resource, ['ProviderURL', 'ProviderName', 'UserPoolId', 'Arn', 'LambdaConfig.PostConfirmation']); @@ -156,6 +165,8 @@ async function assertUserPoolResource(projRoot: string, gen1UserPoolId: string, const gen2UserPoolId = gen2Meta.auth.user_pool_id; const gen2Region = gen2Meta.auth.aws_region; const gen2Resource = await getResourceDetails('AWS::Cognito::UserPool', gen2UserPoolId, gen2Region); + gen1Resource.UserPoolName = extractUserPoolNamePrefix(gen1Resource.UserPoolName); + gen2Resource.UserPoolName = extractUserPoolNamePrefix(gen2Resource.UserPoolName); if (gen1Resource.LambdaConfig.PostConfirmation) assert(gen2Resource.LambdaConfig.PostConfirmation); removeProperties(gen2Resource, ['ProviderURL', 'ProviderName', 'UserPoolId', 'Arn', 'LambdaConfig.PostConfirmation']); // TODO: remove below line after EmailMessage, EmailSubject, SmsMessage, SmsVerificationMessage, EmailVerificationMessage, EmailVerificationSubject, AccountRecoverySetting inconsistency is fixed @@ -275,7 +286,7 @@ export async function assertFunctionResource(projRoot: string, gen2StackName: st const gen1Resource = await getResourceDetails('AWS::Lambda::Function', gen1FunctionName, gen1Region); removeProperties(gen1Resource, ['Arn', 'FunctionName', 'LoggingConfig.LogGroup', 'Role']); // TODO: remove below line after Tags inconsistency is fixed - removeProperties(gen1Resource, ['Tags']); + removeProperties(gen1Resource, ['Tags', 'Environment']); const gen2Meta = getProjectOutputs(projRoot); const gen2Region = gen2Meta.auth.aws_region; @@ -285,7 +296,7 @@ export async function assertFunctionResource(projRoot: string, gen2StackName: st assert(gen2Resource.FunctionName); removeProperties(gen2Resource, ['Arn', 'FunctionName', 'LoggingConfig.LogGroup', 'Role']); // TODO: remove below line after Environment.Variables.AMPLIFY_SSM_ENV_CONFIG, Tags inconsistency is fixed - removeProperties(gen2Resource, ['Environment.Variables.AMPLIFY_SSM_ENV_CONFIG', 'Tags']); + removeProperties(gen2Resource, ['Environment.Variables.AMPLIFY_SSM_ENV_CONFIG', 'Tags', 'Environment']); expect(gen2Resource).toEqual(gen1Resource); } @@ -293,7 +304,7 @@ export async function assertFunctionResource(projRoot: string, gen2StackName: st export async function assertDataResource(projRoot: string, gen2StackName: string, gen1GraphqlApiId: string, gen1Region: string) { const gen1Resource = await getAppSyncApi(gen1GraphqlApiId, gen1Region); const gen1DataSource = (await getAppSyncDataSource(gen1GraphqlApiId, 'TodoTable', gen1Region)) as Record; - removeProperties(gen1DataSource, ['dataSourceArn', 'serviceRoleArn']); + removeProperties(gen1DataSource, DATA_SOURCE_PROPS_TO_REMOVE); removeProperties(gen1Resource.graphqlApi as Record, ['name', 'apiId', 'arn', 'uris', 'tags', 'dns']); // TODO: remove below line after authenticationType inconsistency is fixed removeProperties(gen1Resource.graphqlApi as Record, ['authenticationType']); @@ -304,7 +315,7 @@ export async function assertDataResource(projRoot: string, gen2StackName: string const gen2GraphqlApiId = outputs?.find((output) => output.OutputKey === 'awsAppsyncApiId')?.OutputValue ?? ''; const gen2Resource = await getAppSyncApi(gen2GraphqlApiId, gen2Region); const gen2DataSource = (await getAppSyncDataSource(gen2GraphqlApiId, 'TodoTable', gen1Region)) as Record; - removeProperties(gen2DataSource, ['dataSourceArn', 'serviceRoleArn']); + removeProperties(gen2DataSource, DATA_SOURCE_PROPS_TO_REMOVE); removeProperties(gen2Resource.graphqlApi as Record, [ 'name', 'apiId', diff --git a/packages/amplify-migration-e2e/src/gen1ResourceDetailsFetcher.ts b/packages/amplify-migration-e2e/src/gen1ResourceDetailsFetcher.ts index bd6e49cd402..8b69571af71 100644 --- a/packages/amplify-migration-e2e/src/gen1ResourceDetailsFetcher.ts +++ b/packages/amplify-migration-e2e/src/gen1ResourceDetailsFetcher.ts @@ -2,7 +2,6 @@ import path from 'path'; import { RefactorCategory } from './templategen'; import { getProjectMeta } from '@aws-amplify/amplify-e2e-core'; import { assertIdentityPool, assertStorage, assertUserPool, assertUserPoolClients } from './assertions'; -import { getResourceDetails } from './sdk_calls'; async function getGen1AuthResourceDetails(projRoot: string) { const gen1ProjRoot = path.join(projRoot, '.amplify', 'migration'); @@ -14,13 +13,7 @@ async function getGen1AuthResourceDetails(projRoot: string) { const gen1ClientIdWeb = gen1ClientIds[0]; const gen1ResourceIds = [gen1UserPoolId, gen1IdentityPoolId, gen1ClientIdWeb]; - const gen1ResourceDetails = await Promise.all([ - getResourceDetails('AWS::Cognito::UserPool', gen1UserPoolId, gen1Region), - getResourceDetails('AWS::Cognito::IdentityPool', gen1IdentityPoolId, gen1Region), - getResourceDetails('AWS::Cognito::UserPoolClient', `${gen1UserPoolId}|${gen1ClientIdWeb}`, gen1Region), - ]); - - return { gen1ResourceIds, gen1ResourceDetails }; + return { gen1ResourceIds }; } async function getGen1StorageResourceDetails(projRoot: string) { @@ -29,14 +22,14 @@ async function getGen1StorageResourceDetails(projRoot: string) { const gen1Region = gen1Meta.providers.awscloudformation.Region; const { gen1BucketName } = await assertStorage(gen1Meta, gen1Region); const gen1ResourceIds = [gen1BucketName]; - const gen1ResourceDetails = await getResourceDetails('AWS::S3::Bucket', gen1BucketName, gen1Region); - return { gen1ResourceIds, gen1ResourceDetails }; + return { gen1ResourceIds }; } export async function getGen1ResourceDetails(projRoot: string, category: RefactorCategory) { if (category === 'auth') { return await getGen1AuthResourceDetails(projRoot); - } else { + } else if (category === 'storage') { return await getGen1StorageResourceDetails(projRoot); } + throw new Error(`Invalid category for getting Gen 1 resource details ${category}`); } diff --git a/packages/amplify-migration-e2e/src/gen2ResourceDetailsFetcher.ts b/packages/amplify-migration-e2e/src/gen2ResourceDetailsFetcher.ts index 3bddf9998ba..f5bae5730d9 100644 --- a/packages/amplify-migration-e2e/src/gen2ResourceDetailsFetcher.ts +++ b/packages/amplify-migration-e2e/src/gen2ResourceDetailsFetcher.ts @@ -1,37 +1,28 @@ import { getProjectOutputs } from './projectOutputs'; -import { getResourceDetails } from './sdk_calls'; import { RefactorCategory } from './templategen'; async function getGen2AuthResourceDetails(projRoot: string) { const gen2Meta = getProjectOutputs(projRoot); - const gen2Region = gen2Meta.auth.aws_region; const gen2UserPoolId = gen2Meta.auth.user_pool_id; const gen2ClientIdWeb = gen2Meta.auth.user_pool_client_id; const gen2IdentityPoolId = gen2Meta.auth.identity_pool_id; const gen2ResourceIds = [gen2UserPoolId, gen2IdentityPoolId, gen2ClientIdWeb]; - const gen2ResourceDetails = await Promise.all([ - getResourceDetails('AWS::Cognito::UserPool', gen2UserPoolId, gen2Region), - getResourceDetails('AWS::Cognito::IdentityPool', gen2IdentityPoolId, gen2Region), - getResourceDetails('AWS::Cognito::UserPoolClient', `${gen2UserPoolId}|${gen2ClientIdWeb}`, gen2Region), - ]); - - return { gen2ResourceIds, gen2ResourceDetails }; + return { gen2ResourceIds }; } async function getGen2StorageResourceDetails(projRoot: string) { const gen2Meta = getProjectOutputs(projRoot); - const gen2Region = gen2Meta.auth.aws_region; const gen2BucketName = gen2Meta.storage.bucket_name; const gen2ResourceIds = [gen2BucketName]; - const gen2ResourceDetails = await getResourceDetails('AWS::S3::Bucket', gen2BucketName, gen2Region); - return { gen2ResourceIds, gen2ResourceDetails }; + return { gen2ResourceIds }; } export async function getGen2ResourceDetails(projRoot: string, category: RefactorCategory) { if (category === 'auth') { return await getGen2AuthResourceDetails(projRoot); - } else { + } else if (category === 'storage') { return await getGen2StorageResourceDetails(projRoot); } + throw new Error(`Invalid category for getting Gen 2 resource details ${category}`); } diff --git a/packages/amplify-migration-e2e/src/index.ts b/packages/amplify-migration-e2e/src/index.ts index 4608c21c6ee..f8387661db1 100644 --- a/packages/amplify-migration-e2e/src/index.ts +++ b/packages/amplify-migration-e2e/src/index.ts @@ -33,7 +33,7 @@ export * from './sandbox'; export const pushTimeoutMS = 1000 * 60 * 20; // 20 minutes; -export const MIGRATE_TOOL_VERSION = '0.1.0-next-6.0'; +export const MIGRATE_TOOL_VERSION = '0.1.0-next-9.0'; export const BACKEND_DATA_VERSION = '0.0.0-test-20250416182614'; export async function setupAndPushDefaultGen1Project(projRoot: string, projName: string) { @@ -44,9 +44,9 @@ export async function setupAndPushDefaultGen1Project(projRoot: string, projName: await addS3WithGuestAccess(projRoot); await addApiWithoutSchema(projRoot, { transformerVersion: 2 }); updateApiSchema(projRoot, projName, 'simple_model.graphql'); - await amplifyPush(projRoot); + await amplifyPush(projRoot, true); addFeatureFlag(projRoot, 'graphqltransformer', 'enablegen2migration', true); - await amplifyPushForce(projRoot); + await amplifyPushForce(projRoot, true); } export async function setupAndPushAuthWithMaxOptionsGen1Project(projRoot: string, projName: string) { @@ -60,7 +60,7 @@ export async function setupAndPushAuthWithMaxOptionsGen1Project(projRoot: string updateSigninUrl: 'https://updatesignin1.com/', updateSignoutUrl: 'https://updatesignout1.com/', }); - await amplifyPushAuth(projRoot); + await amplifyPushAuth(projRoot, true); } export async function setupAndPushStorageWithMaxOptionsGen1Project(projRoot: string, projName: string) { @@ -68,7 +68,7 @@ export async function setupAndPushStorageWithMaxOptionsGen1Project(projRoot: str await initJSProjectWithProfile(projRoot, { name: projName, disableAmplifyAppCreation: false, includeGen2RecommendationPrompt: false }); await addAuthWithDefault(projRoot); await addS3WithTrigger(projRoot); - await amplifyPushAuth(projRoot); + await amplifyPushAuth(projRoot, true); console.log(`pushed auth successfully`); } @@ -79,7 +79,6 @@ export function runCodegenCommand(cwd: string) { env: { ...process.env, npm_config_user_agent: 'npm' }, encoding: 'utf-8', }); - console.log(processResult); if (processResult.exitCode !== 0) { throw new Error(`Codegen command exit code: ${processResult.exitCode}, message: ${processResult.stderr}`); } @@ -114,3 +113,5 @@ export function updateAmplifyBackendPackagesVersion(projRoot: string) { updatePackageDependency(projRoot, '@aws-amplify/backend-data', BACKEND_DATA_VERSION); updatePackageDependency(projRoot, '@aws-amplify/backend', BACKEND_DATA_VERSION); } + +export const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); diff --git a/packages/amplify-migration-e2e/src/sdk_calls.ts b/packages/amplify-migration-e2e/src/sdk_calls.ts index d1d1712e29f..24dc101d23a 100644 --- a/packages/amplify-migration-e2e/src/sdk_calls.ts +++ b/packages/amplify-migration-e2e/src/sdk_calls.ts @@ -1,19 +1,12 @@ import { CloudControlClient, GetResourceCommand } from '@aws-sdk/client-cloudcontrol'; import { AppSyncClient, GetDataSourceCommand } from '@aws-sdk/client-appsync'; import { CognitoIdentityClient, DescribeIdentityPoolCommand } from '@aws-sdk/client-cognito-identity'; -import { S3Client, CreateBucketCommand, BucketLocationConstraint } from '@aws-sdk/client-s3'; +import assert from 'node:assert'; +import { delay } from './index'; + +const MAX_ATTEMPTS = 5; +const FIXED_DELAY = 1000; -export async function createS3Bucket(bucketName: string, region: string) { - const client = new S3Client({ region }); - const command = new CreateBucketCommand({ - Bucket: bucketName, - CreateBucketConfiguration: { - LocationConstraint: region as BucketLocationConstraint, - }, - }); - const response = await client.send(command); - return response; -} export async function getAppSyncDataSource(apiId: string, dataSourceName: string, region: string) { const client = new AppSyncClient({ region }); const command = new GetDataSourceCommand({ @@ -24,14 +17,38 @@ export async function getAppSyncDataSource(apiId: string, dataSourceName: string return response.dataSource; } -export async function getResourceDetails(typeName: string, identifier: string, region: string) { +export async function getResourceDetails( + typeName: string, + identifier: string, + region: string, + attempts = MAX_ATTEMPTS, +): Promise | undefined> { + if (attempts <= 0) { + return undefined; + } const client = new CloudControlClient({ region }); const command = new GetResourceCommand({ TypeName: typeName, Identifier: identifier, }); - const response = await client.send(command); - return JSON.parse(response.ResourceDescription.Properties); + try { + const response = await client.send(command); + const resourceProperties = response.ResourceDescription?.Properties; + assert(resourceProperties); + return JSON.parse(resourceProperties); + } catch (e) { + // account for eventual consistency with retries + if (typeof e === 'object' && e !== null && 'message' in e && typeof e.message === 'string' && e.message.includes('NotFound')) { + console.log( + `Attempting to get resource details using CloudControl API for ${typeName} type and ${identifier} identifier in ${region} region: ${ + attempts - 1 + } attempts remaining.`, + ); + await delay(2 ** (MAX_ATTEMPTS - attempts) * FIXED_DELAY); + return getResourceDetails(typeName, identifier, region, attempts - 1); + } + throw e; + } } export async function getIdentityPool(identityPoolId: string, region: string) { @@ -39,6 +56,5 @@ export async function getIdentityPool(identityPoolId: string, region: string) { const command = new DescribeIdentityPoolCommand({ IdentityPoolId: identityPoolId, }); - const response = await client.send(command); - return response; + return await client.send(command); } diff --git a/packages/amplify-migration-e2e/src/templategen.ts b/packages/amplify-migration-e2e/src/templategen.ts index 1e4ee1091d2..17da45d5d7f 100644 --- a/packages/amplify-migration-e2e/src/templategen.ts +++ b/packages/amplify-migration-e2e/src/templategen.ts @@ -2,29 +2,16 @@ import assert from 'node:assert'; import execa from 'execa'; import path from 'node:path'; import * as fs from 'fs-extra'; -import { getNpxPath, readJsonFile, retry, RetrySettings } from '@aws-amplify/amplify-e2e-core'; +import { getNpxPath } from '@aws-amplify/amplify-e2e-core'; import { runGen2SandboxCommand } from './sandbox'; -import { getRollbackCommandsFromReadme, getStackRefactorCommandsFromReadme, readMigrationReadmeFile } from './migrationReadmeParser'; -import { envVariable } from './envVariables'; import { getGen1ResourceDetails } from './gen1ResourceDetailsFetcher'; import { getGen2ResourceDetails } from './gen2ResourceDetailsFetcher'; -import { MIGRATE_TOOL_VERSION, removeProperties } from '.'; +import { MIGRATE_TOOL_VERSION } from '.'; export type RefactorCategory = 'auth' | 'storage'; -const RETRY_CONFIG: RetrySettings = { - times: 50, - delayMS: 1000, // 1 second - timeoutMS: 1000 * 60 * 5, // 5 minutes - stopOnError: true, -}; - -const STATUS_AVAILABLE = 'AVAILABLE'; -const STATUS_EXECUTE_COMPLETE = 'EXECUTE_COMPLETE'; -const STATUS_UPDATE_COMPLETE = 'UPDATE_COMPLETE'; -const STATUS_FAILED = 'FAILED'; - export function runExecuteCommand(cwd: string, gen1StackName: string, gen2StackName: string) { + console.log(`running execute command in ${cwd} for ${gen1StackName}->${gen2StackName}`); const parentDir = path.resolve(cwd, '..'); const processResult = execa.sync( getNpxPath(), @@ -45,13 +32,14 @@ export function runExecuteCommand(cwd: string, gen1StackName: string, gen2StackN encoding: 'utf-8', }, ); + console.log(processResult); if (processResult.exitCode !== 0) { throw new Error(`Execute command exit code: ${processResult.exitCode}, message: ${processResult.stderr}`); } } -export function runRevertCommand(cwd: string, gen1StackName: string, gen2StackName: string, version = 'latest') { +export function runRevertCommand(cwd: string, gen1StackName: string, gen2StackName: string) { const parentDir = path.resolve(cwd, '..'); const processResult = execa.sync( getNpxPath(), @@ -62,9 +50,9 @@ export function runRevertCommand(cwd: string, gen1StackName: string, gen2StackNa 'to-gen-2', 'revert', '--from', - gen1StackName, - '--to', gen2StackName, + '--to', + gen1StackName, ], { cwd, @@ -86,131 +74,30 @@ function uncommentS3BucketLineFromBackendFile(projRoot: string) { fs.writeFileSync(backendFilePath, updatedBackendFileContent); } -async function executeCommand(command: string, cwd?: string) { - cwd = cwd ?? process.cwd(); - const processResult = execa.sync(command, { - cwd, - env: { ...process.env, npm_config_user_agent: 'npm' }, - encoding: 'utf-8', - shell: true, - }); - if (processResult.exitCode === 0) { - return processResult.stdout; - } else { - throw new Error(`Command exit code: ${processResult.exitCode}, message: ${processResult.stderr}`); - } -} - -async function executeCreateStackRefactorCallCommand(command: string, cwd: string) { - const processResult = JSON.parse(await executeCommand(command, cwd)); - const stackRefactorId = processResult.StackRefactorId; - return stackRefactorId; -} - -async function executeStep1(cwd: string, commands: string[]) { - await executeCommand(commands[0], cwd); - await retry( - () => assertStepCompletion(commands[1]), - (status) => status === STATUS_UPDATE_COMPLETE, - RETRY_CONFIG, - (status) => status.includes(STATUS_FAILED), - ); -} - -async function executeStep2(cwd: string, commands: string[]) { - await executeCommand(commands[0], cwd); - await retry( - () => assertStepCompletion(commands[1]), - (status) => status === STATUS_UPDATE_COMPLETE, - RETRY_CONFIG, - (status) => status.includes(STATUS_FAILED), - ); -} - -async function executeStep3(cwd: string, commands: string[], bucketName: string) { - envVariable.set('BUCKET_NAME', bucketName); - await executeCommand(commands[0], cwd); - await executeCommand(commands[1], cwd); - const stackRefactorId = await executeCreateStackRefactorCallCommand(commands[2], cwd); - envVariable.set('STACK_REFACTOR_ID', stackRefactorId); - await retry( - () => assertRefactorStepCompletion(commands[4]), - (processResult) => processResult.ExecutionStatus === STATUS_AVAILABLE || processResult.ExecutionStatus === STATUS_EXECUTE_COMPLETE, - RETRY_CONFIG, - (processResult) => processResult.Status.includes(STATUS_FAILED), - ); - await executeCommand(commands[5], cwd); - await retry( - () => assertRefactorStepCompletion(commands[6]), - (processResult) => processResult.ExecutionStatus === STATUS_AVAILABLE || processResult.ExecutionStatus === STATUS_EXECUTE_COMPLETE, - RETRY_CONFIG, - (processResult) => processResult.Status.includes(STATUS_FAILED), - ); - envVariable.delete('BUCKET_NAME'); - envVariable.delete('STACK_REFACTOR_ID'); -} - -async function assertStepCompletion(command: string) { - const processResult = JSON.parse(await executeCommand(command)); - return processResult.Stacks[0].StackStatus; -} - -async function assertRefactorStepCompletion(command: string) { - const processResult = JSON.parse(await executeCommand(command)); - return processResult; -} - -async function takeTemplateSnapshot(projectRoot: string, category: RefactorCategory, templateName: string) { - const templateFilePath = path.join(projectRoot, '.amplify', 'migration', 'templates', category, templateName); - const templateFileContent = readJsonFile(templateFilePath); - return templateFileContent; -} - -export async function executeStackRefactorSteps(projRoot: string, category: RefactorCategory, bucketName: string) { - const readmeContent = readMigrationReadmeFile(projRoot, category); - const { step1Commands, step2commands, step3Commands } = getStackRefactorCommandsFromReadme(readmeContent); - await executeStep1(projRoot, step1Commands); - await executeStep2(projRoot, step2commands); - await executeStep3(projRoot, step3Commands, bucketName); +function uncommentTagsLineFromBackendFile(projRoot: string) { + const backendFilePath = path.join(projRoot, 'amplify', 'backend.ts'); + const backendFileContent = fs.readFileSync(backendFilePath, 'utf8'); + const regex = /^\s*\/\/\s*(Tags\.of)/m; + const updatedBackendFileContent = backendFileContent.replace(regex, '$1'); + fs.writeFileSync(backendFilePath, updatedBackendFileContent); } -export async function stackRefactor(projRoot: string, projName: string, category: RefactorCategory, bucketName: string) { - const { gen1ResourceIds, gen1ResourceDetails } = await getGen1ResourceDetails(projRoot, category); - - // Remove properties that can safely differ between Gen1 and Gen2 - // This ensures accurate comparison of resources - removeProperties(gen1ResourceDetails, ['CorsConfiguration.CorsRules[0].Id', 'Tags']); - - await executeStackRefactorSteps(projRoot, category, bucketName); - - if (category === 'storage') await uncommentS3BucketLineFromBackendFile(projRoot); - +export async function runGen2DeployPostExecute(projRoot: string, projName: string, envName: string, categories: RefactorCategory[]) { + if (categories.includes('storage')) { + uncommentS3BucketLineFromBackendFile(projRoot); + } + uncommentTagsLineFromBackendFile(projRoot); await runGen2SandboxCommand(projRoot, projName); - - const { gen2ResourceIds, gen2ResourceDetails } = await getGen2ResourceDetails(projRoot, category); - - // Remove tags from Gen2 resources to ensure accurate comparison - // Tags can differ due to sandbox environment but don't affect functionality - removeProperties(gen2ResourceDetails, ['Tags']); - - assert.deepEqual(gen1ResourceIds, gen2ResourceIds); - assert.deepEqual(gen1ResourceDetails, gen2ResourceDetails); } -export async function rollbackStackRefactor(projRoot: string, category: RefactorCategory, bucketName: string) { - const sourceTemplateBeforeStackRefactor = await takeTemplateSnapshot(projRoot, category, 'step3-sourceTemplate.json'); - const destinationTemplateBeforeStackRefactor = await takeTemplateSnapshot(projRoot, category, 'step3-destinationTemplate.json'); - - const readmeContent = readMigrationReadmeFile(projRoot, category); - const { step1RollbackCommands, step2RollbackCommands, step3RollbackCommands } = getRollbackCommandsFromReadme(readmeContent); +export async function assertExecuteCommand(projRoot: string, categories: RefactorCategory[]) { + for (const category of categories) { + console.log(`Asserting post execute for ${category}...`); - await executeStep3(projRoot, step3RollbackCommands, bucketName); - await executeStep2(projRoot, step2RollbackCommands); - await executeStep1(projRoot, step1RollbackCommands); + const { gen1ResourceIds } = await getGen1ResourceDetails(projRoot, category); + const { gen2ResourceIds } = await getGen2ResourceDetails(projRoot, category); - const sourceTemplateAfterStackRefactor = await takeTemplateSnapshot(projRoot, 'storage', 'step3-sourceTemplate.json'); - const destinationTemplateAfterStackRefactor = await takeTemplateSnapshot(projRoot, 'storage', 'step3-destinationTemplate.json'); - - assert.deepStrictEqual(sourceTemplateBeforeStackRefactor, sourceTemplateAfterStackRefactor); - assert.deepStrictEqual(destinationTemplateBeforeStackRefactor, destinationTemplateAfterStackRefactor); + assert.deepEqual(gen1ResourceIds, gen2ResourceIds); + console.log(`Asserted post execute for ${category}`); + } } diff --git a/packages/amplify-migration-template-gen/src/category-template-generator.ts b/packages/amplify-migration-template-gen/src/category-template-generator.ts index 884f8a246b1..359dca34d89 100644 --- a/packages/amplify-migration-template-gen/src/category-template-generator.ts +++ b/packages/amplify-migration-template-gen/src/category-template-generator.ts @@ -25,7 +25,7 @@ import extractStackNameFromId from './cfn-stack-name-extractor'; import retrieveOAuthValues from './oauth-values-retriever'; import { CognitoIdentityProviderClient } from '@aws-sdk/client-cognito-identity-provider'; -const HOSTED_PROVIDER_META_PARAMETER_NAME = 'hostedUIProviderMeta'; +export const HOSTED_PROVIDER_META_PARAMETER_NAME = 'hostedUIProviderMeta'; const HOSTED_PROVIDER_CREDENTIALS_PARAMETER_NAME = 'hostedUIProviderCreds'; const USER_POOL_ID_OUTPUT_KEY_NAME = 'UserPoolId'; const GEN1_WEB_APP_CLIENT = 'UserPoolClientWeb'; 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 f2db6401efb..5e6b535f797 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 @@ -8,6 +8,10 @@ describe('MigrationReadMeGenerator', () => { const migrationReadMeGenerator = new MigrationReadMeGenerator({ path: PATH, categories: ['auth', 'storage'], + hasOAuthEnabled: false, + }); + beforeEach(() => { + jest.clearAllMocks(); }); it('should initialize migration readme', async () => { @@ -17,29 +21,21 @@ describe('MigrationReadMeGenerator', () => { }); }); - it('should render step1', async () => { + it('should render step1 without oauth related information', async () => { await migrationReadMeGenerator.renderStep1(); expect(fs.appendFile).toHaveBeenCalledWith( 'test/MIGRATION_README.md', `## REDEPLOY GEN2 APPLICATION -1.a) Uncomment the following lines in \`amplify/backend.ts\` file +1.a) Uncomment the following lines in \`amplify/backend.ts\` file: \`\`\` s3Bucket.bucketName = YOUR_GEN1_BUCKET_NAME; \`\`\` -\`\`\` -backend.auth.resources.userPool.node.tryRemoveChild('UserPoolDomain'); -\`\`\` - \`\`\` Tags.of(backend.stack).add("gen1-migrated-app", "true"); \`\`\` -1.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 -\`\`\` -`, +1.b) Trigger a CI/CD build via hosting by committing \`amplify/backend.ts\` file to your Git repository`, ); }); @@ -48,26 +44,22 @@ npx ampx sandbox const migrationReadMeGenerator = new MigrationReadMeGenerator({ path: PATH, categories: ['auth'], + hasOAuthEnabled: true, }); await migrationReadMeGenerator.renderStep1(); expect(fs.appendFile).toHaveBeenCalledWith( 'test/MIGRATION_README.md', `## REDEPLOY GEN2 APPLICATION -1.a) Uncomment the following lines in \`amplify/backend.ts\` file +1.a) Uncomment the following lines in \`amplify/backend.ts\` file: \`\`\` backend.auth.resources.userPool.node.tryRemoveChild('UserPoolDomain'); \`\`\` - \`\`\` Tags.of(backend.stack).add("gen1-migrated-app", "true"); \`\`\` -1.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 -\`\`\` -`, +1.b) Trigger a CI/CD build via hosting by committing \`amplify/backend.ts\` file to your Git repository`, ); }); }); 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 65b440bf789..f2e0d89f971 100644 --- a/packages/amplify-migration-template-gen/src/migration-readme-generator.ts +++ b/packages/amplify-migration-template-gen/src/migration-readme-generator.ts @@ -4,17 +4,20 @@ import { CATEGORY } from './types'; interface MigrationReadMeGeneratorOptions { path: string; categories: CATEGORY[]; + hasOAuthEnabled: boolean; } class MigrationReadmeGenerator { private readonly path: string; private readonly migrationReadMePath: string; private readonly categories: CATEGORY[]; + private readonly hasOAuthEnabled: boolean; - constructor({ path, categories }: MigrationReadMeGeneratorOptions) { + constructor({ path, categories, hasOAuthEnabled }: MigrationReadMeGeneratorOptions) { this.path = path; this.migrationReadMePath = `${this.path}/MIGRATION_README.md`; this.categories = categories; + this.hasOAuthEnabled = hasOAuthEnabled; } async initialize(): Promise { @@ -24,30 +27,22 @@ class MigrationReadmeGenerator { async renderStep1() { const s3BucketChanges = `\`\`\` s3Bucket.bucketName = YOUR_GEN1_BUCKET_NAME; -\`\`\` -`; +\`\`\``; + const userPoolDomainRemoval = `\`\`\` +backend.auth.resources.userPool.node.tryRemoveChild('UserPoolDomain'); +\`\`\``; + const gen2Tag = `\`\`\` +Tags.of(backend.stack).add("gen1-migrated-app", "true"); +\`\`\``; await fs.appendFile( this.migrationReadMePath, `## REDEPLOY GEN2 APPLICATION -1.a) Uncomment the following lines in \`amplify/backend.ts\` file +1.a) Uncomment the following lines in \`amplify/backend.ts\` file: ${this.categories.includes('storage') ? s3BucketChanges : ''} -${ - this.categories.includes('auth') - ? `\`\`\` -backend.auth.resources.userPool.node.tryRemoveChild('UserPoolDomain'); -\`\`\`` - : '' -} - -\`\`\` -Tags.of(backend.stack).add("gen1-migrated-app", "true"); -\`\`\` +${this.hasOAuthEnabled ? userPoolDomainRemoval : ''} +${gen2Tag} -1.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 -\`\`\` -`, +1.b) Trigger a CI/CD build via hosting by committing \`amplify/backend.ts\` file to your Git repository`, ); } } 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 f13aac652f8..242f75724f1 100644 --- a/packages/amplify-migration-template-gen/src/template-generator.test.ts +++ b/packages/amplify-migration-template-gen/src/template-generator.test.ts @@ -163,6 +163,18 @@ const mockDescribeGen2AuthStackResources: DescribeStackResourcesOutput = { ], }; +const mockDescribeGen2StorageStackResources: DescribeStackResourcesOutput = { + StackResources: [ + { + ResourceType: CFN_S3_TYPE.Bucket, + ResourceStatus: 'CREATE_COMPLETE', + LogicalResourceId: GEN2_S3_BUCKET_LOGICAL_ID, + PhysicalResourceId: `myGen1BucketAfterRefactor`, + Timestamp: new Date(), + }, + ], +}; + const mockDescribeGen1AuthStackResources: DescribeStackResourcesOutput = { StackResources: [ { @@ -373,6 +385,8 @@ const describeStackResourcesResponse = (stackName: string | undefined) => { return Promise.resolve(mockDescribeGen2AuthStackResources); case GEN1_AUTH_USER_POOL_GROUP_STACK_ID: return Promise.resolve(mockDescribeGen1AuthUserPoolGroupStackResources); + case GEN2_STORAGE_STACK_ID: + return Promise.resolve(mockDescribeGen2StorageStackResources); default: throw new Error(`Unexpected stack: ${stackName}`); } @@ -778,8 +792,9 @@ describe('TemplateGenerator', () => { // 1 describe stack resources call for Gen2 auth stack to get physical ids for auth roles let callIndex = assertStackRefactorCommands('auth', 5, false, false, true); // 1 describe stack resources call for Gen2 auth stack to get physical ids for user group roles + // 1 describe stack resources call for Gen2 storage stack to get physical ids for user group roles callIndex = assertStackRefactorCommands('auth-user-pool-group', callIndex + 2, false, false, true); - assertStackRefactorCommands('storage', callIndex + 1, false, false, true); + assertStackRefactorCommands('storage', callIndex + 2, false, false, true); }); it('should revert resources from Gen2 to Gen1 successfully, skipping categories that have already been updated previously', async () => { diff --git a/packages/amplify-migration-template-gen/src/template-generator.ts b/packages/amplify-migration-template-gen/src/template-generator.ts index ca5141781d8..de5824927db 100644 --- a/packages/amplify-migration-template-gen/src/template-generator.ts +++ b/packages/amplify-migration-template-gen/src/template-generator.ts @@ -1,25 +1,17 @@ -import { - CloudFormationClient, - DescribeStackResourcesCommand, - DescribeStacksCommand, - Parameter, - StackResource, -} from '@aws-sdk/client-cloudformation'; +import { CloudFormationClient, DescribeStackResourcesCommand, DescribeStacksCommand, Parameter } from '@aws-sdk/client-cloudformation'; import assert from 'node:assert'; -import CategoryTemplateGenerator from './category-template-generator'; +import CategoryTemplateGenerator, { HOSTED_PROVIDER_META_PARAMETER_NAME } from './category-template-generator'; import fs from 'node:fs/promises'; import { CATEGORY, NON_CUSTOM_RESOURCE_CATEGORY, CFN_AUTH_TYPE, CFN_CATEGORY_TYPE, - CFN_IAM_TYPE, CFN_RESOURCE_TYPES, CFN_S3_TYPE, CFNResource, CFNStackStatus, CFNTemplate, - GEN2_AUTH_LOGICAL_RESOURCE_ID, ResourceMapping, } from './types'; import MigrationReadmeGenerator from './migration-readme-generator'; @@ -64,9 +56,6 @@ const LOGICAL_IDS_TO_REMOVE_FOR_REVERT_MAP = new Map, sourceCategoryStackId: string, - ): Promise { + ): Promise<[CFNTemplate, Parameter[]] | undefined> { let updatingGen1CategoryStack; try { const { newTemplate, parameters: gen1StackParameters } = await categoryTemplateGenerator.generateGen1PreProcessTemplate(); @@ -268,7 +257,7 @@ class TemplateGenerator { assert(gen1StackUpdateStatus === CFNStackStatus.UPDATE_COMPLETE, `Gen 1 stack is in an invalid state: ${gen1StackUpdateStatus}`); updatingGen1CategoryStack.succeed(`Updated Gen 1 ${this.getStackCategoryName(category)} stack successfully`); - return newTemplate; + return [newTemplate, gen1StackParameters]; } catch (e) { if (this.isNoResourcesError(e)) { updatingGen1CategoryStack?.succeed( @@ -377,21 +366,25 @@ class TemplateGenerator { private async generateCategoryTemplates(isRevert = false, customResourceMap?: ResourceMapping[]) { this.initializeCategoryGenerators(customResourceMap); + let hasOAuthEnabled = false; for (const [category, sourceCategoryStackId, destinationCategoryStackId, categoryTemplateGenerator] of this .categoryTemplateGenerators) { let newSourceTemplate: CFNTemplate | undefined; let newDestinationTemplate: CFNTemplate | undefined; let oldDestinationTemplate: CFNTemplate | undefined; + let sourceStackParameters: Parameter[] | undefined; let destinationStackParameters: Parameter[] | undefined; let sourceTemplateForRefactor: CFNTemplate | undefined; let destinationTemplateForRefactor: CFNTemplate | undefined; let logicalIdMappingForRefactor: Map | undefined; if (customResourceMap && this.isCustomResource(category)) { - newSourceTemplate = await this.processGen1Stack(category, categoryTemplateGenerator, sourceCategoryStackId); - if (!newSourceTemplate) continue; - const { newTemplate } = await this.processGen2Stack(category, categoryTemplateGenerator, destinationCategoryStackId); + const processGen1StackResponse = await this.processGen1Stack(category, categoryTemplateGenerator, sourceCategoryStackId); + if (!processGen1StackResponse) continue; + const [newGen1Template] = processGen1StackResponse; + newSourceTemplate = newGen1Template; + const { newTemplate } = await this.processGen2Stack(category, categoryTemplateGenerator, destinationCategoryStackId); newDestinationTemplate = newTemplate; const sourceToDestinationMap = new Map(); @@ -417,8 +410,14 @@ class TemplateGenerator { destinationTemplateForRefactor = destinationTemplate; logicalIdMappingForRefactor = logicalIdMapping; } else if (!isRevert) { - newSourceTemplate = await this.processGen1Stack(category, categoryTemplateGenerator, sourceCategoryStackId); - if (!newSourceTemplate) continue; + const processGen1StackResponse = await this.processGen1Stack(category, categoryTemplateGenerator, sourceCategoryStackId); + if (!processGen1StackResponse) continue; + const [newGen1Template, gen1StackParameters] = processGen1StackResponse; + sourceStackParameters = gen1StackParameters; + newSourceTemplate = newGen1Template; + if (category === 'auth' && sourceStackParameters?.find((param) => param.ParameterKey === HOSTED_PROVIDER_META_PARAMETER_NAME)) { + hasOAuthEnabled = true; + } const { newTemplate, oldTemplate, parameters } = await this.processGen2Stack( category, categoryTemplateGenerator, @@ -434,7 +433,9 @@ class TemplateGenerator { sourceTemplateForRefactor = sourceTemplate; destinationTemplateForRefactor = destinationTemplate; logicalIdMappingForRefactor = logicalIdMapping; - } else { + } + // revert scenario + else { const sourceCategoryTemplate = await categoryTemplateGenerator.readTemplate(sourceCategoryStackId); const destinationCategoryTemplate = await categoryTemplateGenerator.readTemplate(destinationCategoryStackId); newSourceTemplate = sourceCategoryTemplate; @@ -496,6 +497,7 @@ class TemplateGenerator { const migrationReadMeGenerator = new MigrationReadmeGenerator({ path: `${TEMPLATES_DIR}`, categories: [...this.categoryStackMap.keys()], + hasOAuthEnabled, }); await migrationReadMeGenerator.initialize(); await migrationReadMeGenerator.renderStep1(); @@ -575,46 +577,19 @@ class TemplateGenerator { const { Outputs, Parameters } = describeStackResponseForSourceTemplate; assert(Outputs); assert(this.region); + const { StackResources } = await this.cfnClient.send( + new DescribeStackResourcesCommand({ + StackName: sourceCategoryStackId, + }), + ); + assert(StackResources); const newSourceTemplateWithParametersResolved = new CfnParameterResolver(newSourceTemplate).resolve(Parameters ?? []); const newSourceTemplateWithOutputsResolved = new CfnOutputResolver( newSourceTemplateWithParametersResolved, this.region, this.accountId, - ).resolve(sourceLogicalIds, Outputs, []); + ).resolve(sourceLogicalIds, Outputs, StackResources); const newSourceTemplateWithDepsResolved = new CfnDependencyResolver(newSourceTemplateWithOutputsResolved).resolve(sourceLogicalIds); - if (category === 'auth' || category === 'auth-user-pool-group') { - const { StackResources: AuthStackResources } = await this.cfnClient.send( - new DescribeStackResourcesCommand({ - StackName: sourceCategoryStackId, - }), - ); - assert(AuthStackResources); - const roleResources = AuthStackResources.filter((resource) => resource.ResourceType === CFN_IAM_TYPE.Role); - assert(roleResources.length > 0); - if (category === 'auth') { - const identityPoolRoleMapLogicalId = sourceLogicalIds.find((sourceLogicalId) => - sourceLogicalId.includes(GEN2_AUTH_LOGICAL_RESOURCE_ID.IDENTITY_POOL_ROLE_ATTACHMENT), - ); - assert(identityPoolRoleMapLogicalId); - const roles = newSourceTemplateWithDepsResolved.Resources[identityPoolRoleMapLogicalId].Properties.Roles; - assert(typeof roles === 'object' && UNAUTH_ROLE_NAME in roles && AUTH_ROLE_NAME in roles); - const unAuthRoleArn = roles[UNAUTH_ROLE_NAME]; - const authRoleArn = roles[AUTH_ROLE_NAME]; - const physicalUnAuthRoleArn = this.resolveFnGetAttRoleArn(roleResources, unAuthRoleArn); - assert(physicalUnAuthRoleArn); - roles[UNAUTH_ROLE_NAME] = this.constructRoleArn(physicalUnAuthRoleArn); - const physicalAuthRoleArn = this.resolveFnGetAttRoleArn(roleResources, authRoleArn); - assert(physicalAuthRoleArn); - roles[AUTH_ROLE_NAME] = this.constructRoleArn(physicalAuthRoleArn); - } else if (category === 'auth-user-pool-group') { - for (const sourceLogicalId of sourceLogicalIds) { - const groupRoleArn = newSourceTemplateWithDepsResolved.Resources[sourceLogicalId].Properties.RoleArn; - const physicalGroupRoleArn = this.resolveFnGetAttRoleArn(roleResources, groupRoleArn); - assert(physicalGroupRoleArn); - newSourceTemplateWithDepsResolved.Resources[sourceLogicalId].Properties.RoleArn = this.constructRoleArn(physicalGroupRoleArn); - } - } - } return categoryTemplateGenerator.generateRefactorTemplates( sourceResourcesToRemove, new Map(), @@ -624,31 +599,11 @@ class TemplateGenerator { ); } - private resolveFnGetAttRoleArn(roleResources: StackResource[], roleArn: unknown) { - if ( - roleArn && - typeof roleArn === 'object' && - CFN_FN_GET_ATTTRIBUTE in roleArn && - Array.isArray(roleArn[CFN_FN_GET_ATTTRIBUTE]) && - roleArn[CFN_FN_GET_ATTTRIBUTE].length > 0 && - roleArn[CFN_FN_GET_ATTTRIBUTE][1] === 'Arn' - ) { - const roleLogicalId = roleArn[CFN_FN_GET_ATTTRIBUTE][0]; - const role = roleResources.find((resource) => resource.LogicalResourceId === roleLogicalId); - return role?.PhysicalResourceId; - } - return undefined; - } - private getSourceToDestinationMessage(revert: boolean) { const SOURCE_TO_DESTINATION_STACKS = [GEN1, GEN2]; return revert ? SOURCE_TO_DESTINATION_STACKS.reverse().join(SEPARATOR) : SOURCE_TO_DESTINATION_STACKS.join(SEPARATOR); } - private constructRoleArn(roleName: string) { - return `arn:aws:iam::${this.accountId}:role/${roleName}`; - } - private buildSourceToDestinationMapForRevert(sourceResourcesToRemove: Map): Map { const sourceToDestinationLogicalIdsMap = new Map(); for (const [sourceLogicalId, resource] of sourceResourcesToRemove) {