lambda-live-debugger
Version:
Debug Lambda functions locally like it is running in the cloud
365 lines (362 loc) • 15.5 kB
JavaScript
import * as esbuild from 'esbuild';
import * as fs from 'fs/promises';
import * as path from 'path';
import { BundlingType } from '../types/resourcesDiscovery.mjs';
import { outputFolder } from '../constants.mjs';
import { findPackageJson } from '../utils/findPackageJson.mjs';
import { CloudFormation } from '../cloudFormation.mjs';
import { Logger } from '../logger.mjs';
import { Worker } from 'node:worker_threads';
import { getModuleDirname, getProjectDirname } from '../getDirname.mjs';
import { findNpmPath } from '../utils/findNpmPath.mjs';
/**
* Support for AWS CDK framework
*/
export class CdkFramework {
/**
* Framework name
*/
get name() {
return 'cdk';
}
/**
* Can this class handle the current project
* @returns
*/
async canHandle() {
// check if there is cdk.json
const cdkJsonPath = path.resolve('cdk.json');
try {
await fs.access(cdkJsonPath, fs.constants.F_OK);
return true;
}
catch {
Logger.verbose(`[CDK] This is not a CDK project. ${cdkJsonPath} not found`);
return false;
}
}
/**
* Get Lambda functions
* @param config Configuration
* @returns Lambda functions
*/
async getLambdas(config) {
const awsConfiguration = {
region: config.region,
profile: config.profile,
role: config.role,
};
const cdkConfigPath = 'cdk.json';
// read cdk.json and extract the entry file
const lambdasInCdk = await this.getLambdasDataFromCdkByCompilingAndRunning(cdkConfigPath, config);
Logger.verbose(`[CDK] Found Lambda functions:`, JSON.stringify(lambdasInCdk, null, 2));
//get all stack names
const stackNames = [
...new Set(// unique
lambdasInCdk.map((lambda) => {
return lambda.stackName;
})),
];
Logger.verbose(`[CDK] Found the following stacks in CDK: ${stackNames.join(', ')}`);
const lambdasDeployed = (await Promise.all(stackNames.map(async (stackName) => {
const lambdasInStackPromise = CloudFormation.getLambdasInStack(stackName, awsConfiguration);
const lambdasMetadataPromise = this.getLambdaCdkPathFromTemplateMetadata(stackName, awsConfiguration);
const lambdasInStack = await lambdasInStackPromise;
Logger.verbose(`[CDK] Found Lambda functions in the stack ${stackName}:`, JSON.stringify(lambdasInStack, null, 2));
const lambdasMetadata = await lambdasMetadataPromise;
Logger.verbose(`[CDK] Found Lambda functions in the stack ${stackName} in the template metadata:`, JSON.stringify(lambdasMetadata, null, 2));
return lambdasInStack.map((lambda) => {
return {
lambdaName: lambda.lambdaName,
cdkPath: lambdasMetadata.find((lm) => lm.logicalId === lambda.logicalId)?.cdkPath,
};
});
}))).flat();
const lambdasDiscovered = [];
// compare lambdas in CDK and Stack and get the code path of the Lambdas
for (const lambdaInCdk of lambdasInCdk) {
const functionName = lambdasDeployed.find((lambda) => lambda.cdkPath === lambdaInCdk.cdkPath)?.lambdaName;
if (functionName) {
const external = [
...(lambdaInCdk.bundling?.externalModules ?? []),
...(lambdaInCdk.bundling?.nodeModules ?? []),
];
lambdasDiscovered.push({
functionName: functionName,
codePath: lambdaInCdk.codePath,
handler: lambdaInCdk.handler,
packageJsonPath: lambdaInCdk.packageJsonPath,
bundlingType: BundlingType.ESBUILD,
esBuildOptions: {
minify: lambdaInCdk.bundling?.minify,
format: lambdaInCdk.bundling?.format,
sourcesContent: lambdaInCdk.bundling?.sourcesContent,
target: lambdaInCdk.bundling?.target,
loader: lambdaInCdk.bundling?.loader,
logLevel: lambdaInCdk.bundling?.logLevel,
keepNames: lambdaInCdk.bundling?.keepNames,
tsconfig: lambdaInCdk.bundling?.tsconfig,
metafile: lambdaInCdk.bundling?.metafile,
banner: lambdaInCdk.bundling?.banner
? { js: lambdaInCdk.bundling?.banner }
: undefined,
footer: lambdaInCdk.bundling?.footer
? { js: lambdaInCdk.bundling?.footer }
: undefined,
charset: lambdaInCdk.bundling?.charset,
define: lambdaInCdk.bundling?.define,
external: external.length > 0 ? external : undefined,
},
});
}
}
return lambdasDiscovered;
}
/**
* Getz Lambda functions from the CloudFormation template metadata
* @param stackName
* @param awsConfiguration
* @returns
*/
async getLambdaCdkPathFromTemplateMetadata(stackName, awsConfiguration) {
const cfTemplate = await CloudFormation.getCloudFormationStackTemplate(stackName, awsConfiguration);
// get all Lambda functions in the template
const lambdas = Object.entries(cfTemplate.Resources)
.filter(([, resource]) => resource.Type === 'AWS::Lambda::Function')
.map(([key, resource]) => {
return {
logicalId: key,
cdkPath: resource.Metadata['aws:cdk:path'],
};
});
return lambdas;
}
/**
* Get Lambdas data from CDK by compiling and running the CDK code
* @param cdkConfigPath
* @param config
* @returns
*/
async getLambdasDataFromCdkByCompilingAndRunning(cdkConfigPath, config) {
const entryFile = await this.getCdkEntryFile(cdkConfigPath);
// Define a plugin to prepend custom code to .ts or .tsx files
const injectCodePlugin = {
name: 'injectCode',
setup(build) {
build.onLoad({ filter: /.*/ }, async (args) => {
const absolutePath = path.resolve(args.path);
let source = await fs.readFile(absolutePath, 'utf8');
if (args.path.includes('aws-cdk-lib/aws-lambda/lib/function.')) {
const codeToFind = 'try{jsiiDeprecationWarnings().aws_cdk_lib_aws_lambda_FunctionProps(props)}';
if (!source.includes(codeToFind)) {
throw new Error(`Can not find code to inject in ${args.path}`);
}
// Inject code to get the file path of the Lambda function and CDK hierarchy
// path to match it with the Lambda function. Store data in the global variable.
source = source.replace(codeToFind, `;
global.lambdas = global.lambdas ?? [];
const lambdaInfo = {
//cdkPath: this.node.defaultChild?.node.path ?? this.node.path,
stackName: this.stack.stackName,
codePath: props.entry,
code: props.code,
node: this.node,
handler: props.handler,
bundling: props.bundling
};
// console.log("CDK INFRA: ", {
// stackName: lambdaInfo.stackName,
// codePath: lambdaInfo.codePath,
// code: lambdaInfo.code,
// handler: lambdaInfo.handler,
// bundling: lambdaInfo.bundling
// });
global.lambdas.push(lambdaInfo);` + codeToFind);
}
if (args.path.includes('aws-cdk-lib/aws-s3-deployment/lib/bucket-deployment.')) {
const codeToFind = 'super(scope,id),this.requestDestinationArn=!1;';
if (!source.includes(codeToFind)) {
throw new Error(`Can not find code to inject in ${args.path}`);
}
// Inject code to prevent deploying the assets
source = source.replace(codeToFind, codeToFind + `return;`);
}
return {
contents: source,
loader: 'default',
};
});
},
};
const compileOutput = path.join(getProjectDirname(), outputFolder, `compiledCdk.js`);
try {
// Build CDK code
await esbuild.build({
entryPoints: [entryFile],
bundle: true,
platform: 'node',
target: 'node18',
outfile: compileOutput,
sourcemap: false,
plugins: [injectCodePlugin],
});
}
catch (error) {
throw new Error(`Error building CDK code: ${error.message}`, {
cause: error,
});
}
const context = await this.getCdkContext(cdkConfigPath, config);
const CDK_CONTEXT_JSON = {
...context,
// prevent compiling assets
'aws:cdk:bundling-stacks': [],
};
process.env.CDK_CONTEXT_JSON = JSON.stringify(CDK_CONTEXT_JSON);
Logger.verbose(`[CDK] Context:`, JSON.stringify(CDK_CONTEXT_JSON, null, 2));
const awsCdkLibPath = await findNpmPath(path.join(getProjectDirname(), config.subfolder ?? '/'), 'aws-cdk-lib');
Logger.verbose(`[CDK] aws-cdk-lib path: ${awsCdkLibPath}`);
const lambdas = await this.runCdkCodeAndReturnLambdas({
config,
awsCdkLibPath,
compileCodeFile: compileOutput,
});
const list = await Promise.all(lambdas.map(async (lambda) => {
// handler slit into file and file name
const handlerSplit = lambda.handler.split('.');
const handler = handlerSplit.pop();
const filename = handlerSplit[0];
let codePath = lambda.codePath;
if (!codePath) {
const codePathJs = lambda.code?.path
? path.join(lambda.code.path, `${filename}.js`)
: undefined;
const codePathCjs = lambda.code?.path
? path.join(lambda.code.path, `${filename}.cjs`)
: undefined;
const codePathMjs = lambda.code?.path
? path.join(lambda.code.path, `${filename}.mjs`)
: undefined;
// get the first file that exists
codePath = [codePathJs, codePathCjs, codePathMjs]
.filter((c) => c)
.find((file) => fs
.access(file)
.then(() => true)
.catch(() => false));
if (!codePath) {
throw new Error(`Code file not found for Lambda function ${lambda.code.path}`);
}
}
const packageJsonPath = await findPackageJson(codePath);
Logger.verbose(`[CDK] package.json path: ${packageJsonPath}`);
return {
cdkPath: lambda.cdkPath,
stackName: lambda.stackName,
packageJsonPath,
codePath,
handler,
bundling: lambda.bundling,
};
}));
return list;
}
/**
* Run CDK code in a node thread worker and return the Lambda functions
* @param param0
* @returns
*/
async runCdkCodeAndReturnLambdas({ config, awsCdkLibPath, compileCodeFile, }) {
const lambdas = await new Promise((resolve, reject) => {
const worker = new Worker(path.resolve(path.join(getModuleDirname(), 'frameworks/cdkFrameworkWorker.mjs')), {
workerData: {
verbose: config.verbose,
awsCdkLibPath,
projectDirname: getProjectDirname(),
moduleDirname: getModuleDirname(),
subfolder: config.subfolder,
},
});
worker.on('message', async (message) => {
resolve(message);
await worker.terminate();
});
worker.on('error', (error) => {
reject(new Error(`Error running CDK code in worker: ${error.message}`, {
cause: error,
}));
});
worker.on('exit', (code) => {
if (code !== 0) {
reject(new Error(`CDK worker stopped with exit code ${code}`));
}
});
worker.postMessage({
compileOutput: compileCodeFile,
});
});
Logger.verbose(`[CDK] Found the following Lambda functions in the CDK code:`, JSON.stringify(lambdas, null, 2));
return lambdas;
}
/**
* Get CDK context
* @param cdkConfigPath
* @param config
* @returns
*/
async getCdkContext(cdkConfigPath, config) {
// get CDK context from the command line
// get all "-c" and "--context" arguments from the command line
const contextFromLldConfig = config.context?.reduce((acc, arg) => {
const [key, value] = arg.split('=');
if (key && value) {
acc[key] = value;
}
return acc;
}, {});
// get all context from 'cdk.context.json' if it exists
let contextFromJson = {};
try {
const cdkContextJson = await fs.readFile('cdk.context.json', 'utf8');
contextFromJson = JSON.parse(cdkContextJson);
}
catch (err) {
if (err.code !== 'ENOENT') {
throw new Error(`Error reading cdk.context.json: ${err.message}`);
}
}
// get context from cdk.json
let cdkJson = {};
try {
cdkJson = JSON.parse(await fs.readFile(cdkConfigPath, 'utf8'));
}
catch (err) {
if (err.code !== 'ENOENT') {
throw new Error(`Error reading cdk.json: ${err.message}`);
}
}
return { ...contextFromJson, ...cdkJson.context, ...contextFromLldConfig };
}
/**
* Get CDK entry file
* @param cdkConfigPath
* @returns
*/
async getCdkEntryFile(cdkConfigPath) {
const cdkJson = await fs.readFile(cdkConfigPath, 'utf8');
const cdkConfig = JSON.parse(cdkJson);
const entry = cdkConfig.app;
// just file that ends with .ts
let entryFile = entry
?.split(' ')
.find((file) => file.endsWith('.ts'))
?.trim();
if (!entryFile) {
throw new Error(`Entry file not found in ${cdkConfigPath}`);
}
entryFile = path.resolve(entryFile);
Logger.verbose(`[CDK] Entry file: ${entryFile}`);
return entryFile;
}
}
export const cdkFramework = new CdkFramework();