UNPKG

lambda-live-debugger

Version:

Debug Lambda functions locally like it is running in the cloud

365 lines (362 loc) 15.5 kB
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();