UNPKG

lambda-live-debugger

Version:

Debug Lambda functions locally like it is running in the cloud

261 lines (260 loc) 10.4 kB
import * as fs from 'fs/promises'; import * as path from 'path'; import { constants } from 'fs'; import { findPackageJson } from '../utils/findPackageJson.mjs'; import { exec } from 'child_process'; import { promisify } from 'util'; import ts from 'typescript'; import { Logger } from '../logger.mjs'; export const execAsync = promisify(exec); /** * Support for Terraform framework */ export class TerraformFramework { /** * Framework name */ get name() { return 'terraform'; } /** * Name of the framework in logs */ get logName() { return 'Terrform'; } /** * Get Terraform state CI command */ get stateCommand() { return 'terraform show --json'; } /** * * @returns Get command to check if Terraform is installed */ get checkInstalledCommand() { return 'terraform --version'; } /** * Can this class handle the current project * @returns */ async canHandle() { // is there any filey with *.tf extension const files = await fs.readdir(process.cwd()); const r = files.some((f) => f.endsWith('.tf')); if (!r) { Logger.verbose(`[${this.logName}] This is not a ${this.logName} project. There are no *.tf files in ${path.resolve('.')} folder.`); return false; } else { // check if Terraform or OpenTofu is installed try { await execAsync(this.checkInstalledCommand); return true; } catch { Logger.verbose(`[${this.logName}] This is not a ${this.logName} project. ${this.logName} is not installed.`); return false; } } } /** * Get Lambda functions * @param _config Configuration * @returns Lambda functions */ // eslint-disable-next-line @typescript-eslint/no-unused-vars async getLambdas(config) { const state = await this.readTerraformState(); const lambdas = this.extractLambdaInfo(state); Logger.verbose(`[${this.logName}] Found Lambdas:`, JSON.stringify(lambdas, null, 2)); const lambdasDiscovered = []; const tsOutDir = await this.getTsConfigOutDir(); if (tsOutDir) { Logger.verbose(`[${this.logName}] tsOutDir:`, tsOutDir); } for (const func of lambdas) { const functionName = func.functionName; const handlerParts = func.handler.split('.'); // get last part of the handler const handler = handlerParts[handlerParts.length - 1]; const filename = func.sourceFilename; let pathWithourExtension; if (filename) { // remove extension pathWithourExtension = filename.replace(/\.[^/.]+$/, ''); } else { pathWithourExtension = path.join(func.sourceDir, handlerParts[0]); } let possibleCodePaths = [ `${pathWithourExtension}.ts`, `${pathWithourExtension}.js`, `${pathWithourExtension}.cjs`, `${pathWithourExtension}.mjs`, ]; if (tsOutDir) { // remove outDir from path const pathWithourExtensionTypeScript = pathWithourExtension .replace(tsOutDir, '') .replace(/\/\//g, '/'); possibleCodePaths = [ `${pathWithourExtensionTypeScript}.ts`, `${pathWithourExtensionTypeScript}.js`, `${pathWithourExtensionTypeScript}.cjs`, `${pathWithourExtensionTypeScript}.mjs`, ...possibleCodePaths, ]; } let codePath; for (const cp of possibleCodePaths) { try { await fs.access(cp, constants.F_OK); codePath = cp; break; } catch { // ignore, file not found } } if (!codePath) { throw new Error(`Code path not found for handler: ${functionName}`); } const packageJsonPath = await findPackageJson(codePath); const lambdaResource = { functionName, codePath, handler, packageJsonPath, esBuildOptions: undefined, metadata: { framework: this.name, }, }; lambdasDiscovered.push(lambdaResource); } return lambdasDiscovered; } extractLambdaInfo(resources) { const lambdas = []; for (const resource of resources) { if (resource.type === 'aws_lambda_function') { Logger.verbose(`[${this.logName}] Found Lambda:`, JSON.stringify(resource, null, 2)); let sourceDir; let sourceFilename; const functionName = resource.values.function_name; const handler = resource.values.handler; if (!functionName) { Logger.error('Failed to find function name for Lambda'); continue; } // get dependency "data.archive_file" const dependencies = resource.depends_on; const archiveFileResourceName = dependencies.find((dep) => dep.startsWith('data.archive_file.')); if (archiveFileResourceName) { // get the resource const name = archiveFileResourceName.split('.')[2]; const archiveFileResource = resources.find((r) => r.name === name); // get source_dir or source_filename if (archiveFileResource) { sourceDir = archiveFileResource.values.source_dir; sourceFilename = archiveFileResource.values.source_file; } } // get dependency "archive_prepare" = serverless.tf support const archivePrepareResourceName = dependencies.find((dep) => dep.includes('.archive_prepare')); if (archivePrepareResourceName) { // get the resource const name = archivePrepareResourceName; const archivePrepareResource = resources.find((r) => r.address?.startsWith(name)); // get source_dir or source_filename if (archivePrepareResource) { sourceDir = archivePrepareResource.values.query?.source_path?.replaceAll('"', ''); } } if (!sourceDir && !sourceFilename) { Logger.error(`Failed to find source code for Lambda ${functionName}`); } else { lambdas.push({ functionName, sourceDir, sourceFilename, handler: handler ?? 'handler', }); } } } return lambdas; } async readTerraformState() { // Is there a better way to get the Terraform state??? let output; // get state by running "terraform show --json" command try { Logger.verbose(`[${this.logName}] Getting state with '${this.stateCommand}' command`); output = await execAsync(this.stateCommand); } catch (error) { throw new Error(`[${this.logName}] Failed to getstate from '${this.stateCommand}' command: ${error.message}`, { cause: error }); } if (output.stderr) { throw new Error(`[${this.logName}] Failed to get state from '${this.stateCommand}' command: ${output.stderr}`); } if (!output.stdout) { throw new Error(`[${this.logName}] Failed to get state from '${this.stateCommand}' command`); } let jsonString = output.stdout; Logger.verbose(`[${this.logName}] State:`, jsonString); jsonString = jsonString?.split('\n').find((line) => line.startsWith('{')); if (!jsonString) { throw new Error(`[${this.logName}] Failed to get state. JSON string not found in the output.`); } try { const state = JSON.parse(jsonString); const rootResources = state.values?.root_module?.resources ?? []; const childResources = state.values?.root_module?.child_modules ?.map((m) => m.resources) .flat() ?? []; return [...rootResources, ...childResources]; } catch (error) { //save state to file await fs.writeFile(`${this.name}-state.json`, jsonString); Logger.error(`[${this.logName}] Failed to parse state JSON:`, error); throw new Error(`Failed to parse ${this.logName} state JSON: ${error.message}`, { cause: error }); } } /** * Get the outDir from tsconfig.json */ async getTsConfigOutDir() { let currentDir = process.cwd(); let tsConfigPath; while (currentDir !== '/') { tsConfigPath = path.resolve(path.join(currentDir, 'tsconfig.json')); try { await fs.access(tsConfigPath, constants.F_OK); break; } catch { // tsconfig.json not found, move up one directory currentDir = path.dirname(currentDir); } } if (!tsConfigPath) { Logger.verbose(`[${this.logName}] tsconfig.json not found`); return undefined; } Logger.verbose(`[${this.logName}] tsconfig.json found:`, tsConfigPath); const configFile = ts.readConfigFile(tsConfigPath, ts.sys.readFile); const compilerOptions = ts.parseJsonConfigFileContent(configFile.config, ts.sys, './'); return compilerOptions.options.outDir ? path.resolve(compilerOptions.options.outDir) : undefined; } } export const terraformFramework = new TerraformFramework();