Skip to content
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
2 changes: 1 addition & 1 deletion packages/amplify-gen2-codegen/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ export interface Gen2RenderingOptions {
// (undocumented)
backendEnvironmentName?: string | undefined;
// (undocumented)
customResources?: string[];
customResources?: Map<string, string>;
// (undocumented)
data?: DataDefinition;
// (undocumented)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -521,7 +521,10 @@ describe('BackendRenderer', () => {
it('renders custom resources', () => {
const renderer = new BackendSynthesizer();
const rendered = renderer.render({
customResources: ['resource1', 'resource2'],
customResources: new Map([
['resource1', 'resource1Value'],
['resource2', 'resource2Value'],
]),
});

const output = printNodeArray(rendered);
Expand Down
18 changes: 9 additions & 9 deletions packages/amplify-gen2-codegen/src/backend/synthesizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export interface BackendRenderParameters {
importFrom: string;
functionNamesAndCategories: Map<string, string>;
};
customResources?: string[];
customResources?: Map<string, string>;
unsupportedCategories?: Map<string, string>;
}

Expand Down Expand Up @@ -697,7 +697,7 @@ export class BackendSynthesizer {
TemporaryPasswordValidityDays: 'temporaryPasswordValidityDays',
};

if (renderArgs.auth || renderArgs.storage?.hasS3Bucket) {
if (renderArgs.auth || renderArgs.storage?.hasS3Bucket || renderArgs.customResources) {
imports.push(
this.createImportStatement([factory.createIdentifier('RemovalPolicy'), factory.createIdentifier('Tags')], 'aws-cdk-lib'),
);
Expand Down Expand Up @@ -757,30 +757,30 @@ export class BackendSynthesizer {
}

if (renderArgs.customResources) {
for (const resource of renderArgs.customResources) {
for (const [resourceName, className] of renderArgs.customResources) {
const importStatement = factory.createImportDeclaration(
undefined,
factory.createImportClause(
false,
undefined,
factory.createNamedImports([
factory.createImportSpecifier(false, factory.createIdentifier('cdkStack'), factory.createIdentifier(`${resource}`)),
factory.createImportSpecifier(false, factory.createIdentifier(`${className}`), factory.createIdentifier(`${resourceName}`)),
]),
),
factory.createStringLiteral(`./custom/${resource}/cdk-stack`),
factory.createStringLiteral(`./custom/${resourceName}/cdk-stack`),
undefined,
);

imports.push(importStatement);

const customResourceExpression = factory.createNewExpression(factory.createIdentifier(`${resource}`), undefined, [
const customResourceExpression = factory.createNewExpression(factory.createIdentifier(`${resourceName}`), undefined, [
factory.createPropertyAccessExpression(factory.createIdentifier('backend'), factory.createIdentifier('stack')),
factory.createStringLiteral(`${resource}`),
factory.createStringLiteral(`${resourceName}`),
factory.createIdentifier('undefined'),
factory.createObjectLiteralExpression(
[
factory.createPropertyAssignment(factory.createIdentifier('category'), factory.createStringLiteral('custom')),
factory.createPropertyAssignment(factory.createIdentifier('resourceName'), factory.createStringLiteral(`${resource}`)),
factory.createPropertyAssignment(factory.createIdentifier('resourceName'), factory.createStringLiteral(`${resourceName}`)),
],
true,
),
Expand Down Expand Up @@ -1043,7 +1043,7 @@ export class BackendSynthesizer {

// Add a tag commented out to force a deployment post refactor
// Tags.of(backend.stack).add('gen1-migrated-app', 'true')
if (renderArgs.auth || renderArgs.storage?.hasS3Bucket) {
if (renderArgs.auth || renderArgs.storage?.hasS3Bucket || renderArgs.customResources) {
const tagAssignment = factory.createExpressionStatement(
factory.createCallExpression(
factory.createPropertyAccessExpression(
Expand Down
4 changes: 2 additions & 2 deletions packages/amplify-gen2-codegen/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export interface Gen2RenderingOptions {
storage?: StorageRenderParameters;
data?: DataDefinition;
functions?: FunctionDefinition[];
customResources?: string[];
customResources?: Map<string, string>;
unsupportedCategories?: Map<string, string>;
fileWriter?: (content: string, path: string) => Promise<void>;
}
Expand Down Expand Up @@ -215,7 +215,7 @@ export const createGen2Renderer = ({
};
}

if (customResources && customResources.length > 0) {
if (customResources && customResources.size > 0) {
backendRenderOptions.customResources = customResources;
}

Expand Down
12 changes: 11 additions & 1 deletion packages/amplify-migration-template-gen/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,21 @@ import { CloudFormationClient } from '@aws-sdk/client-cloudformation';
import { CognitoIdentityProviderClient } from '@aws-sdk/client-cognito-identity-provider';
import { SSMClient } from '@aws-sdk/client-ssm';

// @public (undocumented)
export interface ResourceMapping {
// (undocumented)
Destination: ResourceMappingLocation;
// Warning: (ae-forgotten-export) The symbol "ResourceMappingLocation" needs to be exported by the entry point index.d.ts
//
// (undocumented)
Source: ResourceMappingLocation;
}

// @public (undocumented)
export class TemplateGenerator {
constructor(fromStack: string, toStack: string, accountId: string, cfnClient: CloudFormationClient, ssmClient: SSMClient, cognitoIdpClient: CognitoIdentityProviderClient, appId: string, environmentName: string);
// (undocumented)
generate(): Promise<boolean>;
generate(customResourceMap?: ResourceMapping[]): Promise<boolean>;
// (undocumented)
revert(): Promise<boolean>;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { CFN_AUTH_TYPE, CFN_PSEUDO_PARAMETERS_REF, CFN_S3_TYPE, CFNTemplate } fr
import {
CloudFormationClient,
DescribeStacksCommand,
DescribeStackResourcesCommand,
DescribeStacksOutput,
GetTemplateCommand,
GetTemplateOutput,
Expand Down Expand Up @@ -659,6 +660,20 @@ jest.mock('@aws-sdk/client-cloudformation', () => {
return Promise.resolve(generateDescribeStacksResponse(command));
} else if (command instanceof GetTemplateCommand) {
return Promise.resolve(generateGetTemplateResponse(command));
} else if (command instanceof DescribeStackResourcesCommand) {
return Promise.resolve({
StackResources: [
{
StackId: command.input.StackName,
StackName: command.input.StackName,
LogicalResourceId: GEN1_S3_BUCKET_LOGICAL_ID,
PhysicalResourceId: GEN1_S3_BUCKET_LOGICAL_ID,
ResourceType: CFN_S3_TYPE.Bucket,
ResourceStatus: 'CREATE_COMPLETE',
Timestamp: new Date(),
},
],
});
}
return Promise.resolve({});
}),
Expand Down Expand Up @@ -845,13 +860,28 @@ describe('CategoryTemplateGenerator', () => {
? JSON.stringify(oldGen1Template)
: JSON.stringify(oldGen2TemplateWithoutS3Bucket),
});
} else if (command instanceof DescribeStackResourcesCommand) {
return Promise.resolve({
StackResources: [
{
StackId: command.input.StackName,
StackName: command.input.StackName,
LogicalResourceId: GEN1_S3_BUCKET_LOGICAL_ID,
PhysicalResourceId: GEN1_S3_BUCKET_LOGICAL_ID,
ResourceType: CFN_S3_TYPE.Bucket,
ResourceStatus: 'CREATE_COMPLETE',
Timestamp: new Date(),
},
],
});
}
return Promise.resolve({});
};
stubCfnClientSend
.mockImplementationOnce(sendFailureMock)
.mockImplementationOnce(sendFailureMock)
.mockImplementationOnce(sendFailureMock)
.mockImplementationOnce(sendFailureMock)
.mockImplementationOnce(sendFailureMock);
await noGen1ResourcesToMoveS3TemplateGenerator.generateGen1PreProcessTemplate();
await expect(noGen1ResourcesToMoveS3TemplateGenerator.generateGen2ResourceRemovalTemplate()).rejects.toThrowError(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import { CloudFormationClient, DescribeStacksCommand, GetTemplateCommand, Stack } from '@aws-sdk/client-cloudformation';
import {
CloudFormationClient,
DescribeStacksCommand,
DescribeStackResourcesCommand,
GetTemplateCommand,
Stack,
Parameter,
} from '@aws-sdk/client-cloudformation';
import { SSMClient } from '@aws-sdk/client-ssm';
import assert from 'node:assert';
import {
Expand Down Expand Up @@ -32,8 +39,10 @@ const RESOURCE_TYPES_WITH_MULTIPLE_RESOURCES = [
class CategoryTemplateGenerator<CFNCategoryType extends CFN_CATEGORY_TYPE> {
private gen1DescribeStacksResponse: Stack | undefined;
private gen2DescribeStacksResponse: Stack | undefined;
private gen1ResourcesToMove: Map<string, CFNResource>;
private gen2ResourcesToRemove: Map<string, CFNResource>;
public gen1ResourcesToMove: Map<string, CFNResource>;
public gen2ResourcesToRemove: Map<string, CFNResource>;
public gen2Template: CFNTemplate | undefined;
public gen2StackParameters: Parameter[] | undefined;
constructor(
private readonly gen1StackId: string,
private readonly gen2StackId: string,
Expand Down Expand Up @@ -72,9 +81,12 @@ class CategoryTemplateGenerator<CFNCategoryType extends CFN_CATEGORY_TYPE> {
const gen1ParametersResolvedTemplate = new CfnParameterResolver(oldGen1Template, extractStackNameFromId(this.gen1StackId)).resolve(
Parameters,
);

const stackResources = await this.describeStackResources(this.gen1StackId);
const gen1TemplateWithOutputsResolved = new CfnOutputResolver(gen1ParametersResolvedTemplate, this.region, this.accountId).resolve(
logicalResourceIds,
Outputs,
stackResources,
);
const gen1TemplateWithDepsResolved = new CfnDependencyResolver(gen1TemplateWithOutputsResolved).resolve(logicalResourceIds);
const gen1TemplateWithConditionsResolved = new CFNConditionResolver(gen1TemplateWithDepsResolved).resolve(Parameters);
Expand Down Expand Up @@ -106,16 +118,21 @@ class CategoryTemplateGenerator<CFNCategoryType extends CFN_CATEGORY_TYPE> {
assert(this.gen2DescribeStacksResponse);
const { Parameters, Outputs } = this.gen2DescribeStacksResponse;
assert(Outputs);
this.gen2StackParameters = Parameters;
const oldGen2Template = await this.readTemplate(this.gen2StackId);
this.gen2Template = oldGen2Template;
this.gen2ResourcesToRemove = new Map(
Object.entries(oldGen2Template.Resources).filter(([, value]) =>
this.resourcesToMove.some((resourceToMove) => resourceToMove.valueOf() === value.Type),
),
Object.entries(oldGen2Template.Resources).filter(([logicalId, value]) => {
return (
this.resourcesToMovePredicate?.(this.resourcesToMove, [logicalId, value]) ??
this.resourcesToMove.some((resourceToMove) => resourceToMove.valueOf() === value.Type)
);
}),
);
// validate empty resources
if (this.gen2ResourcesToRemove.size === 0) throw new Error('No resources to remove in Gen2 stack.');
const logicalResourceIds = [...this.gen2ResourcesToRemove.keys()];
const updatedGen2Template = this.removeGen2ResourcesFromGen2Stack(oldGen2Template, logicalResourceIds);
const updatedGen2Template = await this.removeGen2ResourcesFromGen2Stack(oldGen2Template, logicalResourceIds);
return {
oldTemplate: oldGen2Template,
newTemplate: updatedGen2Template,
Expand Down Expand Up @@ -148,6 +165,18 @@ class CategoryTemplateGenerator<CFNCategoryType extends CFN_CATEGORY_TYPE> {
).Stacks?.[0];
}

private async describeStackResources(stackId: string) {
const { StackResources } = await this.cfnClient.send(
new DescribeStackResourcesCommand({
StackName: stackId,
}),
);

assert(StackResources && StackResources.length > 0);

return StackResources;
}

private removeGen1ResourcesFromGen1Stack(gen1Template: CFNTemplate, resourcesToRefactor: string[]) {
const resources = gen1Template.Resources;
assert(resources);
Expand Down Expand Up @@ -217,13 +246,16 @@ class CategoryTemplateGenerator<CFNCategoryType extends CFN_CATEGORY_TYPE> {
return gen1ToGen2ResourceLogicalIdMapping;
}

private removeGen2ResourcesFromGen2Stack(gen2Template: CFNTemplate, resourcesToRemove: string[]) {
private async removeGen2ResourcesFromGen2Stack(gen2Template: CFNTemplate, resourcesToRemove: string[]) {
const clonedGen2Template = JSON.parse(JSON.stringify(gen2Template));
const stackOutputs = this.gen2DescribeStacksResponse?.Outputs;
assert(stackOutputs);
const resolvedRefsGen2Template = new CfnOutputResolver(clonedGen2Template, this.region, this.accountId).resolve(
const stackResources = await this.describeStackResources(this.gen2StackId);
const gen2TemplateWithDepsResolved = new CfnDependencyResolver(clonedGen2Template).resolve(resourcesToRemove);
const resolvedRefsGen2Template = new CfnOutputResolver(gen2TemplateWithDepsResolved, this.region, this.accountId).resolve(
resourcesToRemove,
stackOutputs,
stackResources,
);
resourcesToRemove.forEach((logicalResourceId) => {
delete resolvedRefsGen2Template.Resources[logicalResourceId];
Expand Down
1 change: 1 addition & 0 deletions packages/amplify-migration-template-gen/src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './template-generator';
export { ResourceMapping } from './types';
Loading
Loading