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-cli-npm/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,4 @@ export const install = async (): Promise<void> => {
return binary.install();
};

// force version bump to 14.5.0
// force version bump to 14.6.0
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ describe('AmplifyMigrationAssessor', () => {
expect(assessment.resources[0].refactor.level).toBe('unsupported');
});

it('marks function with non-JS runtime as unsupported for generate', () => {
it('marks function with non-JS runtime as supported for generate', () => {
const gen1App = mockGen1App(
[{ category: 'function', resourceName: 'myPythonFunc', service: 'Lambda', key: 'function:Lambda' }],
['function/myPythonFunc/myPythonFunc-cloudformation-template.json'],
Expand All @@ -99,9 +99,9 @@ describe('AmplifyMigrationAssessor', () => {
const assessment = assessor.assess();

expect(assessment.resources).toHaveLength(1);
expect(assessment.resources[0].generate.level).toBe('unsupported');
expect(assessment.resources[0].generate.level).toBe('supported');
expect(assessment.resources[0].refactor.level).toBe('not-applicable');
expect(assessment.validFor('generate')).toBe(false);
expect(assessment.validFor('generate')).toBe(true);
expect(assessment.validFor('refactor')).toBe(true);
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,38 +78,17 @@ describe('FunctionAssessor', () => {
expect(assessment.features).toHaveLength(0);
});

describe('non-JS runtime detection', () => {
it('marks resource as unsupported for generate when runtime is Python', () => {
it('marks generate as supported for every runtime', () => {
for (const runtime of ['nodejs18.x', 'python3.11', 'go1.x', 'java21', 'dotnet8', 'ruby3.3']) {
const gen1App = mockGen1App([], {
[NODEJS_TEMPLATE_PATH]: { Resources: { LambdaFunction: { Properties: { Runtime: 'python3.11' } } } },
[NODEJS_TEMPLATE_PATH]: { Resources: { LambdaFunction: { Properties: { Runtime: runtime } } } },
});
const assessment = new Assessment('app', 'dev');
new FunctionAssessor(gen1App, RESOURCE).record(assessment);

const entry = assessment.resources[0];
expect(entry!.generate.level).toBe('unsupported');
expect(entry!.generate.note).toContain('requires adding code after generate');
expect(entry!.refactor.level).toBe('not-applicable');
});

it('fails assessment validFor generate when runtime is non-JS', () => {
const gen1App = mockGen1App([], {
[NODEJS_TEMPLATE_PATH]: { Resources: { LambdaFunction: { Properties: { Runtime: 'dotnet8' } } } },
});
const assessment = new Assessment('app', 'dev');
new FunctionAssessor(gen1App, RESOURCE).record(assessment);

expect(assessment.validFor('generate')).toBe(false);
expect(assessment.validFor('refactor')).toBe(true);
});

it('treats nodejs runtimes as supported', () => {
const gen1App = mockGen1App([], { [NODEJS_TEMPLATE_PATH]: NODEJS_TEMPLATE });
const assessment = new Assessment('app', 'dev');
new FunctionAssessor(gen1App, RESOURCE).record(assessment);

expect(assessment.resources[0]!.generate.level).toBe('supported');
expect(assessment.features).toHaveLength(0);
});
expect(assessment.validFor('generate')).toBe(true);
expect(assessment.validFor('refactor')).toBe(true);
}
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -157,13 +157,8 @@ describe('AmplifyMigrationGenerateStep', () => {

it('skips unsupported resources without instantiating generators', async () => {
const gen1 = mockGen1App({
discover: () => [{ category: 'function', resourceName: 'myPythonFunc', service: 'Lambda', key: 'function:Lambda' as const }],
json: (p: string) => {
if (p.endsWith('-cloudformation-template.json')) {
return { Resources: { LambdaFunction: { Properties: { Runtime: 'python3.11' } } } };
}
return undefined;
},
discover: () => [{ category: 'storage', resourceName: 'myImportedTable', service: 'DynamoDB', key: 'storage:DynamoDB' as const }],
categoryMeta: () => ({ myImportedTable: { serviceType: 'imported' } }),
});
const logger = new SpinningLogger('generate', { debug: true });

Expand All @@ -172,7 +167,7 @@ describe('AmplifyMigrationGenerateStep', () => {
// 3 validation ops (lock, working dir, assessment)
// 6 infrastructure generators (backend, root package.json, backend package.json, tsconfig, amplify.yml, gitignore)
// 1 post-generation op (replace folder)
// = 10 total — the unsupported function contributes zero operations.
// = 10 total — the unsupported (imported) resource contributes zero operations.
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- accessing private field to assert operation count
expect((plan as any).operations).toHaveLength(10);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1237,7 +1237,7 @@ describe('FunctionGenerator', () => {
expect(output).toContain('KinesisEventSource');
});

it('throws for non-nodejs runtime', async () => {
it('generates a custom function resource.ts for non-nodejs runtime', async () => {
const gen1App = await createGen1App({
providers: { awscloudformation: { StackName: 'amplify-test-main-123456', Region: 'us-east-1' } },
function: {
Expand All @@ -1254,15 +1254,31 @@ describe('FunctionGenerator', () => {
jest.spyOn(gen1App.aws, 'fetchFunctionConfig').mockResolvedValue({
FunctionName: 'myFunc-main-abc',
Handler: 'handler.handler',
Timeout: 3,
MemorySize: 128,
Timeout: 15,
MemorySize: 256,
Runtime: 'python3.9',
Environment: { Variables: {} },
});
jest.spyOn(gen1App.aws, 'fetchFunctionSchedule').mockResolvedValue(undefined);

const generator = createFunctionGenerator({ gen1App, backendGenerator, packageJsonGenerator, outputDir });
await expect(generator.plan()).rejects.toThrow("unsupported runtime 'python3.9'");
const ops = await generator.plan();
await ops[0].execute();

const content = writtenFile('resource.ts');
expect(content).toContain('export const myFunc = defineFunction(');
expect(content).toContain("handler: 'handler.handler'");
expect(content).toContain('runtime: Runtime.PYTHON_3_9');
expect(content).toContain('timeout: Duration.seconds(15)');
expect(content).toContain('memorySize: 256');
expect(content).toContain('python3 -m pip install');

// Non-JS functions are wired into backend.ts and retain their source (no throw).
expect(mockCp).toHaveBeenCalledWith(
expect.stringContaining('myFunc'),
expect.any(String),
expect.objectContaining({ recursive: true }),
);
});

it('uses original model name casing from schema for table grants and env vars', async () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { extractTableName } from '../../../../../../commands/gen2-migration/generate/amplify/function/function.renderer';
import {
extractTableName,
FunctionRenderer,
mapToCdkRuntime,
} from '../../../../../../commands/gen2-migration/generate/amplify/function/function.renderer';

describe('extractTableName', () => {
it('recovers original casing via case-insensitive match against known model names', () => {
Expand Down Expand Up @@ -33,3 +37,194 @@ describe('extractTableName', () => {
expect(extractTableName('API_MYAPI_GRAPHQLAPIIDOUTPUT')).toBeUndefined();
});
});

describe('FunctionRenderer.renderCustomFunction', () => {
const renderer = new FunctionRenderer('d1abc2def3', 'main');

it('renders a Python function with CDK Function construct', () => {
const output = renderer.renderCustomFunction({
resourceName: 'myPythonFunc',
handler: 'index.handler',
runtime: 'python3.11',
timeoutSeconds: 20,
memoryMB: 256,
});

expect(output).toContain("import { defineFunction } from '@aws-amplify/backend'");
expect(output).toContain("import { Architecture, Code, Function, Runtime } from 'aws-cdk-lib/aws-lambda'");
expect(output).toContain('export const myPythonFunc = defineFunction(');
expect(output).toContain("handler: 'index.handler'");
expect(output).toContain('runtime: Runtime.PYTHON_3_11');
expect(output).toContain('architecture: Architecture.X86_64');
expect(output).toContain('timeout: Duration.seconds(20)');
expect(output).toContain('memorySize: 256');
expect(output).toContain('python3 -m pip install');
expect(output).toContain('--platform manylinux2014_x86_64');
expect(output).toContain('image: Runtime.PYTHON_3_11.bundlingImage');
});

it('renders a Go function with CDK Function construct', () => {
const output = renderer.renderCustomFunction({
resourceName: 'myGoFunc',
handler: 'bootstrap',
runtime: 'provided.al2023',
timeoutSeconds: 10,
});

expect(output).toContain('export const myGoFunc = defineFunction(');
expect(output).toContain("handler: 'bootstrap'");
expect(output).toContain('runtime: Runtime.PROVIDED_AL2023');
expect(output).toContain('GOARCH=amd64 GOOS=linux go build');
});

it('renders a Java function with CDK Function construct', () => {
const output = renderer.renderCustomFunction({
resourceName: 'myJavaFunc',
handler: 'com.example.Handler::handleRequest',
runtime: 'java21',
});

expect(output).toContain('export const myJavaFunc = defineFunction(');
expect(output).toContain("handler: 'com.example.Handler::handleRequest'");
expect(output).toContain('runtime: Runtime.JAVA_21');
expect(output).toContain('mvn package');
});

it('renders a .NET function with CDK Function construct', () => {
const output = renderer.renderCustomFunction({
resourceName: 'myDotnetFunc',
handler: 'MyAssembly::MyNamespace.Handler::FunctionHandler',
runtime: 'dotnet8',
});

expect(output).toContain('export const myDotnetFunc = defineFunction(');
expect(output).toContain('runtime: Runtime.DOTNET_8');
expect(output).toContain('dotnet publish');
});

it('uses default timeout and memory when not specified', () => {
const output = renderer.renderCustomFunction({
resourceName: 'myFunc',
handler: 'index.handler',
runtime: 'python3.9',
});

expect(output).toContain('timeout: Duration.seconds(3)');
expect(output).toContain('memorySize: 128');
});

it('emits arm64 architecture and arm bundling flags when the function is arm64', () => {
const pythonOutput = renderer.renderCustomFunction({
resourceName: 'myArmPythonFunc',
handler: 'index.handler',
runtime: 'python3.12',
architecture: 'arm64',
});

expect(pythonOutput).toContain('architecture: Architecture.ARM_64');
expect(pythonOutput).toContain('--platform manylinux2014_aarch64');

const goOutput = renderer.renderCustomFunction({
resourceName: 'myArmGoFunc',
handler: 'bootstrap',
runtime: 'provided.al2023',
architecture: 'arm64',
});

expect(goOutput).toContain('architecture: Architecture.ARM_64');
expect(goOutput).toContain('GOARCH=arm64 GOOS=linux go build');
});

it('falls back to PROVIDED_AL2023 with a note for a runtime CDK no longer exports', () => {
// python3.4 maps to PYTHON_3_4, which is not a member of CDK's Runtime enum.
const output = renderer.renderCustomFunction({
resourceName: 'myEolFunc',
handler: 'index.handler',
runtime: 'python3.4',
});

expect(output).toContain('runtime: Runtime.PROVIDED_AL2023');
expect(output).toContain("// NOTE: runtime 'python3.4' has no matching CDK Runtime member");
});

it('emits literal environment variables on the function', () => {
const output = renderer.renderCustomFunction({
resourceName: 'myEnvFunc',
handler: 'index.handler',
runtime: 'python3.12',
environment: { API_URL: 'https://example.com', STAGE: 'prod' },
});

expect(output).toContain('environment: {');
expect(output).toContain('"API_URL": "https://example.com"');
expect(output).toContain('"STAGE": "prod"');
});

it('emits a TODO block for configuration that was not migrated', () => {
const output = renderer.renderCustomFunction({
resourceName: 'myTodoFunc',
handler: 'index.handler',
runtime: 'python3.12',
manualMigrationNotes: ['A schedule was not migrated.', 'DynamoDB access was not migrated.'],
});

expect(output).toContain('// TODO: The following Gen1 configuration was not migrated automatically');
expect(output).toContain('// - A schedule was not migrated.');
expect(output).toContain('// - DynamoDB access was not migrated.');
});

it('forces the handler to bootstrap for go/provided runtimes regardless of the Gen1 handler', () => {
const output = renderer.renderCustomFunction({
resourceName: 'myGoFunc',
handler: 'main', // non-bootstrap Gen1 handler
runtime: 'go1.x',
});

expect(output).toContain("handler: 'bootstrap'");
expect(output).not.toContain("handler: 'main'");
});

it('warns loudly for an unrecognized runtime family instead of silently generating broken code', () => {
const output = renderer.renderCustomFunction({
resourceName: 'myUnknownFunc',
handler: 'index.handler',
runtime: 'cobol1.0',
});

expect(output).toContain('runtime: Runtime.PROVIDED_AL2023');
expect(output).toContain("// WARNING: runtime 'cobol1.0' is not recognized");
});
});

describe('mapToCdkRuntime', () => {
it('maps Python runtimes', () => {
expect(mapToCdkRuntime('python3.9')).toBe('PYTHON_3_9');
expect(mapToCdkRuntime('python3.11')).toBe('PYTHON_3_11');
expect(mapToCdkRuntime('python3.12')).toBe('PYTHON_3_12');
});

it('maps Java runtimes', () => {
expect(mapToCdkRuntime('java11')).toBe('JAVA_11');
expect(mapToCdkRuntime('java17')).toBe('JAVA_17');
expect(mapToCdkRuntime('java21')).toBe('JAVA_21');
});

it('maps .NET runtimes', () => {
expect(mapToCdkRuntime('dotnet6')).toBe('DOTNET_6');
expect(mapToCdkRuntime('dotnet8')).toBe('DOTNET_8');
});

it('maps Go/custom runtimes to PROVIDED_AL2023', () => {
expect(mapToCdkRuntime('go1.x')).toBe('PROVIDED_AL2023');
expect(mapToCdkRuntime('provided.al2023')).toBe('PROVIDED_AL2023');
expect(mapToCdkRuntime('provided.al2')).toBe('PROVIDED_AL2023');
});

it('maps Ruby runtimes', () => {
expect(mapToCdkRuntime('ruby3.3')).toBe('RUBY_3_3');
});

it('falls back to PROVIDED_AL2023 for unknown runtimes', () => {
expect(mapToCdkRuntime('unknown')).toBe('PROVIDED_AL2023');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import { Gen1App, DiscoveredResource, KNOWN_FEATURES } from '../../_common/gen1-

/**
* Assesses migration readiness for a single Lambda function resource.
* Detects non-JS runtimes and custom-policies.json usage.
* All runtimes (Node.js, Python, Go, Java, .NET) are supported for generation.
* Detects custom-policies.json usage as an unsupported feature.
*/
export class FunctionAssessor implements Assessor {
public constructor(private readonly gen1App: Gen1App, private readonly resource: DiscoveredResource) {}
Expand All @@ -13,13 +14,9 @@ export class FunctionAssessor implements Assessor {
* Records resource-level and feature-level support for this function.
*/
public record(assessment: Assessment): void {
const templatePath = `function/${this.resource.resourceName}/${this.resource.resourceName}-cloudformation-template.json`;
const template = this.gen1App.json(templatePath);
const runtime = template.Resources.LambdaFunction.Properties.Runtime;

assessment.recordResource({
resource: this.resource,
generate: this.isNonJsRuntime(runtime) ? unsupported('requires adding code after generate') : supported(),
generate: supported(),
refactor: notApplicable(),
});

Expand All @@ -34,13 +31,6 @@ export class FunctionAssessor implements Assessor {
}
}

/**
* Returns true if the runtime is present and is not a Node.js variant.
*/
private isNonJsRuntime(runtime: string): boolean {
return !runtime.startsWith('nodejs');
}

/**
* Returns true if the function has non-empty custom policies.
* The file always exists but defaults to `[{"Action":[],"Resource":[]}]`.
Expand Down
Loading
Loading