From 1dca6b346b9afbd333368d758a53745fc9f8686f Mon Sep 17 00:00:00 2001 From: "Marko (ServerlessLife)" Date: Wed, 23 Jul 2025 19:14:19 +0200 Subject: [PATCH 1/8] feat: Optimize and clean adding and removing infrastructure --- .github/workflows/common-test.yml | 1 - package-lock.json | 1 + src/configuration.ts | 33 +- src/infraDeploy.ts | 1004 +++++++++++++++++------------ src/lldebugger.ts | 174 +++-- src/nodeEsBuild.ts | 4 +- src/resourceDiscovery.ts | 10 +- src/types/lambdaProps.ts | 5 - src/types/resourcesDiscovery.ts | 2 + 9 files changed, 736 insertions(+), 498 deletions(-) delete mode 100644 src/types/lambdaProps.ts diff --git a/.github/workflows/common-test.yml b/.github/workflows/common-test.yml index 59fc90fe..72be87dd 100644 --- a/.github/workflows/common-test.yml +++ b/.github/workflows/common-test.yml @@ -23,7 +23,6 @@ permissions: contents: write env: - DISABLE_PARALLEL_DEPLOY: false REAL_NPM: ${{ inputs.mode == 'global' || inputs.mode == 'local' }} TEST_MONOREPO: ${{ inputs.testMonorepo }} node_version: 22 diff --git a/package-lock.json b/package-lock.json index 7d5f506a..f28a5395 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27184,6 +27184,7 @@ "version": "7.6.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, "bin": { "semver": "bin/semver.js" }, diff --git a/src/configuration.ts b/src/configuration.ts index 62f114f2..5e9d24ab 100644 --- a/src/configuration.ts +++ b/src/configuration.ts @@ -1,4 +1,3 @@ -import { LambdaProps } from './types/lambdaProps.js'; import { LldConfig } from './types/lldConfig.js'; import { LambdaResource } from './types/resourcesDiscovery.js'; import * as crypto from 'crypto'; @@ -12,7 +11,7 @@ import { ResourceDiscovery } from './resourceDiscovery.js'; import { Logger } from './logger.js'; let config: LldConfig; -const lambdas: Record = {}; +const lambdas: Record = {}; let lambdasList: LambdaResource[] | undefined = undefined; /** @@ -103,34 +102,43 @@ async function generateDebuggerId(observableMode: boolean) { * Add a Lambda to the configuration * @param props */ -function addLambda(props: Omit) { +function addLambda(props: LambdaResource) { lambdas[props.functionName] = { - functionId: props.functionName, ...props, }; } /** - * Get a Lambda by functionId - * @param functionId + * Get a Lambda by name + * @param functionName * @returns */ -async function getLambda(functionId: string): Promise { - const lambda = lambdas[functionId]; +async function getLambda(functionName: string): Promise { + const lambda = lambdas[functionName]; if (lambda) return lambda; - throw new Error(`Lambda not found: ${functionId}`); + throw new Error(`Lambda not found: ${functionName}`); } /** * Get all Lambdas * @returns */ -function getLambdas() { +function getLambdasAll(): LambdaResource[] { return Object.values(lambdas); } +/** + * Get filtered Lambdas + * @returns + */ +function getLambdasFiltered() { + const list = Object.values(lambdas); + + return list.filter((l) => !l.filteredOut); +} + /** * Discover Lambdas */ @@ -180,7 +188,7 @@ function saveDiscoveredLambdas(lambdasListNew: LambdaResource[]) { Logger.log('Found the following Lambdas to debug:'); Logger.log( - ` - ${getLambdas() + ` - ${getLambdasFiltered() .map((f) => `${f.functionName} code: ${f.codePath}`) .join('\n - ')}`, ); @@ -205,6 +213,7 @@ export const Configuration = { }, discoverLambdas, getLambda, - getLambdas, + getLambdasAll, + getLambdasFiltered, setConfig, }; diff --git a/src/infraDeploy.ts b/src/infraDeploy.ts index 76ff07b6..cbb2820b 100755 --- a/src/infraDeploy.ts +++ b/src/infraDeploy.ts @@ -50,6 +50,36 @@ const policyDocument = { ], }; +/** + * Type for Lambda update data + */ +export type InfraLambdaUpdate = { + functionName: string; + layers: string[]; + environmentVariables: Record; + timeout: number; +}; + +/** + * Type for infrastructure changes when adding Lambda Live Debugger + */ +export type InfraAddingChanges = { + deployLayer: boolean; + existingLayerVersionArn: string | undefined; + lambdasToAdd: InfraLambdaUpdate[]; + rolesToAdd: string[]; + lambdasToRemove: InfraLambdaUpdate[]; + rolesToRemove: string[]; +}; + +/** + * Type for infrastructure changes when removing Lambda Live Debugger + */ +export type InfraRemovalChanges = { + lambdasToRemove: InfraLambdaUpdate[]; + rolesToRemove: string[]; +}; + /** * Get the Lambda client * @returns @@ -145,13 +175,6 @@ async function getLayerDescription() { async function deployLayer() { const layerDescription = await getLayerDescription(); - // Check if the layer already exists - const existingLayer = await findExistingLayerVersion(); - if (existingLayer && existingLayer.LayerVersionArn) { - Logger.verbose(`${layerDescription} already deployed.`); - return existingLayer.LayerVersionArn; - } - // check the ZIP let layerZipPathFullPath = path.resolve( path.join(getModuleDirname(), './extension/extension.zip'), @@ -171,7 +194,7 @@ async function deployLayer() { await fs.access(layerZipPathFullPath2); layerZipPathFullPath = layerZipPathFullPath2; } catch { - throw new Error(`File for the layer not found: ${layerZipPathFullPath}`); + throw new Error(`File for the layer not found: ${layerZipPathFullPath}.`); } } @@ -196,7 +219,7 @@ async function deployLayer() { const response = await getLambdaClient().send(publishLayerVersionCommand); if (!response.LayerVersionArn) { - throw new Error('Failed to retrieve the layer version ARN'); + throw new Error('Failed to retrieve the layer version ARN.'); } Logger.verbose( @@ -214,7 +237,6 @@ async function deleteLayer() { const layers = await getLambdaClient().send( new ListLayersCommand({ Marker: nextMarker, - MaxItems: 10, }), ); @@ -241,7 +263,6 @@ async function deleteAllVersionsOfLayer(layerArn: string): Promise { new ListLayerVersionsCommand({ LayerName: layerArn, Marker: nextMarker, - //MaxItems: 5, }), ); @@ -280,114 +301,419 @@ async function deleteLayerVersion( } /** - * Remove the layer from the Lambda function + * Check if policy needs to be removed from the Lambda role * @param functionName + * @returns */ -async function removeLayerFromLambda(functionName: string) { +async function analyzeRemovePolicyFromLambdaRole(functionName: string) { try { - let needToUpdate: boolean = false; + Logger.verbose( + `[Function ${functionName}] Analyzing policy removal from Lambda role`, + ); + + const getFunctionResponse = await getLambdaClient().send( + new GetFunctionCommand({ + FunctionName: functionName, + }), + ); + const roleArn = getFunctionResponse.Configuration?.Role; + if (!roleArn) { + throw new Error( + `Failed to retrieve the role ARN for lambda ${functionName}.`, + ); + } + + const roleName = roleArn.split('/').pop(); + + if (!roleName) { + throw new Error( + `Failed to extract role name from role ARN: ${roleArn} for lambda ${functionName}.`, + ); + } + + Logger.verbose(`[Function ${functionName}] Found role: ${roleName}`); + + const existingPolicy = await createPolicyDocument(roleName); + + const needToRemovePolicy = !!existingPolicy; + Logger.verbose( + `[Function ${functionName}] Policy ${needToRemovePolicy ? 'needs to be removed' : 'not found to remove'} from role ${roleName}`, + ); + + return { + needToRemovePolicy, + roleName, + }; + } catch (error: any) { + throw new Error( + `Failed to analyze removal policy from Lambda ${functionName}.`, + { cause: error }, + ); + } +} + +/** + * Remove the policy from the Lambda role + * @param roleData + * @returns + */ +async function removePolicyFromLambdaRole(roleName: string) { + Logger.verbose(`[Role ${roleName}] Removing policy from the role`); + try { + await getIAMClient().send( + new DeleteRolePolicyCommand({ + RoleName: roleName, + PolicyName: inlinePolicyName, + }), + ); + Logger.verbose(`[Role ${roleName}] Policy removed successfully`); + } catch (error: any) { + throw new Error(`Failed to remove policy from the role ${roleName}.`, { + cause: error, + }); + } +} + +/** + * Create policy document needed to attach to the Lambda role needed for the Lambda Live Debugger + * @param roleName + * @returns + */ +async function createPolicyDocument(roleName: string) { + try { + Logger.verbose(`[Role ${roleName}] Checking for existing policy document`); + + const policy = await getIAMClient().send( + new GetRolePolicyCommand({ + RoleName: roleName, + PolicyName: inlinePolicyName, + }), + ); + + if (policy.PolicyDocument) { + Logger.verbose(`[Role ${roleName}] Found existing policy document`); + const policyDocument = JSON.parse( + decodeURIComponent(policy.PolicyDocument), + ); + return policyDocument; + } else { + Logger.verbose(`[Role ${roleName}] No policy document found`); + return undefined; + } + } catch (error: any) { + if (error.name === 'NoSuchEntityException') { + Logger.verbose( + `[Role ${roleName}] Policy does not exist (NoSuchEntityException)`, + ); + return undefined; + } else { + throw new Error( + `Failed to create policy document for role ${roleName}.`, + { cause: error }, + ); + } + } +} + +/** + * Deploy the infrastructure + */ +async function applyAddingInfra(changes: InfraAddingChanges) { + Logger.verbose( + 'Starting infrastructure deployment for adding Lambda Live Debugger', + ); + + let layerVersionArn: string; + + if (changes.deployLayer) { + Logger.verbose('Deploying new layer version'); + layerVersionArn = await deployLayer(); + } else { + if (!changes.existingLayerVersionArn) { + throw new Error('Expected existing layer ARN but none provided.'); + } + Logger.verbose( + `Using existing layer version: ${changes.existingLayerVersionArn}`, + ); + layerVersionArn = changes.existingLayerVersionArn; + } + + const promises: Promise[] = []; + + // Add LLD to functions + for (const lambdaData of changes.lambdasToAdd) { + promises.push( + addLayerToLambda({ + ...lambdaData, + layers: [ + layerVersionArn, + // remove LLD layer if exist + ...lambdaData.layers.filter( + (arn) => !arn.includes(`:layer:${layerName}:`), + ), + ], + }), + ); + } + + // Remove LLD from filtered functions + for (const lambdaData of changes.lambdasToRemove) { + promises.push(removeLayerFromLambda(lambdaData)); + } + + // Add policies to roles + for (const roleName of changes.rolesToAdd) { + promises.push(addPolicyToRole(roleName)); + } + + // Remove policies from roles + for (const roleName of changes.rolesToRemove) { + promises.push(removePolicyFromLambdaRole(roleName)); + } + + await Promise.all(promises); +} + +/** + * Get the planned infrastructure changes including removal from filtered functions + */ +async function getInfraChangesForAdding(): Promise { + Logger.verbose( + 'Analyzing infrastructure changes for adding Lambda Live Debugger', + ); + + const existingLayer = await findExistingLayerVersion(); + + const configLambdasAll = Configuration.getLambdasAll(); + + const configLambdasUpdate = configLambdasAll.filter( + (l) => !(l.filteredOut === true), + ); + const configLambdasRemove = configLambdasAll.filter( + (l) => l.filteredOut === true, + ); + + const lambdasToUpdatePromise = Promise.all( + configLambdasUpdate.map(async (func) => { + const lambdaUpdate = await analyzeLambdaAdd( + func.functionName, + existingLayer?.LayerVersionArn, + ); + return lambdaUpdate; + }), + ); + + const rolesToAddPromise = Promise.all( + configLambdasUpdate.map(async (func) => { + const roleUpdate = await analyzeLambdaRoleAdd(func.functionName); + return roleUpdate.addPolicy ? roleUpdate.roleName : undefined; + }), + ); + const lambdasToRemovePromise = Promise.all( + configLambdasRemove.map(async (func) => { + return analyzeRemoveLambda(func.functionName); + }), + ); + + const rolesToRemovePromise = Promise.all( + configLambdasRemove.map(async (func) => { + const roleRemoval = await analyzeRemovePolicyFromLambdaRole( + func.functionName, + ); + return roleRemoval.needToRemovePolicy ? roleRemoval.roleName : undefined; + }), + ); + + const lambdasToUpdate = await lambdasToUpdatePromise; + const lambdasToAddFiltered = lambdasToUpdate.filter( + (l) => l, + ) as InfraLambdaUpdate[]; + + const rolesToAdd = await rolesToAddPromise; + const rolesToAddFiltered = [ + ...new Set(rolesToAdd.filter((r) => r)), + ] as string[]; + + const lambdasToRemove = await lambdasToRemovePromise; + const lambdasToRemoveFiltered = lambdasToRemove.filter( + (l) => l, + ) as InfraLambdaUpdate[]; + + const rolesToRemove = await rolesToRemovePromise; + let rolesToRemoveFiltered = rolesToRemove.filter((r) => r) as string[]; + + // Filter out roles that are being added to avoid conflicts + rolesToRemoveFiltered = rolesToRemoveFiltered.filter( + (r) => !rolesToAddFiltered.includes(r), + ); + + return { + deployLayer: !existingLayer, + existingLayerVersionArn: existingLayer?.LayerVersionArn, + lambdasToAdd: lambdasToAddFiltered, + rolesToAdd: rolesToAddFiltered, + lambdasToRemove: lambdasToRemoveFiltered, + rolesToRemove: rolesToRemoveFiltered, + }; +} + +/** + * Check what needs to be removed from a Lambda function + * @param func - Lambda function properties + * @returns Lambda update configuration or undefined if no update needed + */ +async function analyzeRemoveLambda(functionName: string) { + try { const { environmentVariables, ddlLayerArns, otherLayerArns, initialTimeout, - } = await getLambdaCongfiguration(functionName); + } = await getLambdaConfiguration(functionName); - if (ddlLayerArns.length > 0) { - needToUpdate = true; - Logger.verbose(`Detaching layer from the function ${functionName}`); + const needToRemoveLayer = ddlLayerArns.length > 0; + let needToRemoveEnvironmentVariables = false; + + if (needToRemoveLayer) { + Logger.verbose( + `[Function ${functionName}] Lambda Live Debugger layer(s) detected: ${ddlLayerArns.join(', ')}. Marked for removal.`, + ); } else { Logger.verbose( - `Skipping detaching layer from the function ${functionName}, no layer attached`, + `[Function ${functionName}] No Lambda Live Debugger layer(s) to remove.`, ); } - const initalExecWraper = - environmentVariables.LLD_INITIAL_AWS_LAMBDA_EXEC_WRAPPER; - - const ddlEnvironmentVariables = getEnvironmentVarablesForDebugger({ + const ddlEnvironmentVariables = getEnvironmentVariablesForDebugger({ // set dummy data, so we just get the list of environment variables - functionId: 'xxx', + functionName: 'xxx', timeout: 0, verbose: true, - initalExecWraper: 'test', + initialExecWrapper: 'test', }); // check if environment variables are set for each property for (const [key] of Object.entries(ddlEnvironmentVariables)) { if (environmentVariables && environmentVariables[key]) { - needToUpdate = true; + needToRemoveEnvironmentVariables = true; break; } } - if (needToUpdate) { - Logger.verbose( - `Updating function configuration for ${functionName} to remove layer and reset environment variables`, - ); + Logger.verbose( + `[Function ${functionName}] ${needToRemoveEnvironmentVariables ? 'Removing environment variables' : 'No environment variables to remove'}. Existing environment variables: `, + JSON.stringify(ddlEnvironmentVariables, null, 2), + ); - Logger.verbose( - 'Existing environment variables', - JSON.stringify(environmentVariables, null, 2), - ); + const needToRemove = needToRemoveLayer || needToRemoveEnvironmentVariables; - //remove environment variables + if (needToRemove) { + const initialExecWrapper = + environmentVariables.LLD_INITIAL_AWS_LAMBDA_EXEC_WRAPPER; + const ddlEnvironmentVariables = getEnvironmentVariablesForDebugger({ + functionName: 'xxx', + timeout: 0, + verbose: true, + initialExecWrapper: 'test', + }); + + // Remove LLD environment variables + const cleanedEnvironmentVariables = { ...environmentVariables }; for (const [key] of Object.entries(ddlEnvironmentVariables)) { - if (environmentVariables && environmentVariables[key]) { - if (key === 'AWS_LAMBDA_EXEC_WRAPPER') { - if (environmentVariables[key] === lldWrapperPath) { - delete environmentVariables[key]; - } else { - // do not remove the original AWS_LAMBDA_EXEC_WRAPPER that was set before LLD - } - } else { - delete environmentVariables[key]; + if (key === 'AWS_LAMBDA_EXEC_WRAPPER') { + if (cleanedEnvironmentVariables[key] === lldWrapperPath) { + delete cleanedEnvironmentVariables[key]; } + } else { + delete cleanedEnvironmentVariables[key]; } } - if (initalExecWraper) { - environmentVariables.AWS_LAMBDA_EXEC_WRAPPER = initalExecWraper; + if (initialExecWrapper) { + cleanedEnvironmentVariables.AWS_LAMBDA_EXEC_WRAPPER = + initialExecWrapper; } - Logger.verbose( - 'New environment variables', - JSON.stringify(environmentVariables, null, 2), + return { + functionName, + layers: otherLayerArns, + environmentVariables: cleanedEnvironmentVariables, + timeout: initialTimeout, + }; + } + return undefined; + } catch (error: any) { + throw new Error(`Failed to analyze removal from lambda ${functionName}.`, { + cause: error, + }); + } +} + +/** + * Get the planned removal changes + */ +async function getInfraChangesForRemoving(): Promise { + Logger.verbose( + 'Analyzing infrastructure changes for removing Lambda Live Debugger', + ); + + const allLambdas = Configuration.getLambdasAll(); + + const lambdasToRemovePromise = Promise.all( + allLambdas.map(async (func) => { + return analyzeRemoveLambda(func.functionName); + }), + ); + + const rolesToRemovePromise = Promise.all( + allLambdas.map(async (func) => { + const roleRemoval = await analyzeRemovePolicyFromLambdaRole( + func.functionName, ); + return roleRemoval.needToRemovePolicy ? roleRemoval.roleName : undefined; + }), + ); - const updateFunctionConfigurationCommand = - new UpdateFunctionConfigurationCommand({ - FunctionName: functionName, - Layers: otherLayerArns, - Environment: { - Variables: { - ...environmentVariables, - }, - }, - Timeout: initialTimeout, - }); + const lambdasToRemove = await lambdasToRemovePromise; + const lambdasToRemoveFiltered = lambdasToRemove.filter( + (l) => l, + ) as InfraLambdaUpdate[]; - await getLambdaClient().send(updateFunctionConfigurationCommand); + const rolesToRemove = await rolesToRemovePromise; + const rolesToRemoveFiltered = rolesToRemove.filter((r) => r) as string[]; - Logger.verbose(`Function configuration cleared ${functionName}`); - } else { - Logger.verbose(`Function ${functionName} configuration already cleared.`); - } - } catch (error: any) { - throw new Error( - `Failed to remove layer from lambda ${functionName}: ${error.message}`, - { cause: error }, - ); + return { + lambdasToRemove: lambdasToRemoveFiltered, + rolesToRemove: rolesToRemoveFiltered, + }; +} + +/** + * Remove the infrastructure + */ +async function applyRemoveInfra(changes: InfraRemovalChanges) { + Logger.verbose('Starting infrastructure removal'); + + const promises: Promise[] = []; + + for (const lambdaData of changes.lambdasToRemove) { + promises.push(removeLayerFromLambda(lambdaData)); } + + for (const roleName of changes.rolesToRemove) { + promises.push(removePolicyFromLambdaRole(roleName)); + } + + await Promise.all(promises); } /** - * Get the Lambda configuration - * @param functionName - * @returns + * Get the Lambda function configuration including layers, environment variables, and timeout + * @param functionName - The name of the Lambda function + * @returns Lambda configuration details */ -async function getLambdaCongfiguration(functionName: string) { +async function getLambdaConfiguration(functionName: string) { try { const getFunctionResponse = await getLambdaClient().send( new GetFunctionCommand({ @@ -397,7 +723,7 @@ async function getLambdaCongfiguration(functionName: string) { const timeout = getFunctionResponse.Configuration?.Timeout; - // get all layers this fuction has by name + // get all layers this function has by name const layers = getFunctionResponse.Configuration?.Layers || []; const layerArns = layers.map((l) => l.Arn).filter((arn) => arn) as string[]; const ddlLayerArns = layerArns.filter((arn) => @@ -427,154 +753,190 @@ async function getLambdaCongfiguration(functionName: string) { otherLayerArns, initialTimeout, }; + } catch (error: any) { + throw new Error(`Failed to get lambda configuration ${functionName}.`, { + cause: error, + }); + } +} + +/** + * Attach the layer to the Lambda function and update the environment variables + * @param lambdaData + */ +async function addLayerToLambda(lambdaData: InfraLambdaUpdate) { + Logger.verbose( + `[Function ${lambdaData.functionName}] Adding layer and environment variables`, + ); + try { + await updateLambda(lambdaData); } catch (error: any) { throw new Error( - `Failed to get lambda configuration ${functionName}: ${error.message}`, + `Failed to update add layer to lambda ${lambdaData.functionName}.`, { cause: error }, ); } } - /** - * Attach the layer to the Lambda function and update the environment variables + * Remove the layer from the Lambda function and update the environment variables + * @param lambdaData */ -async function updateLambda({ - functionName, - functionId, - layerVersionArn, -}: { - functionName: string; - functionId: string; - layerVersionArn: string; -}) { - const { needToUpdate, layers, environmentVariables, initialTimeout } = - await prepareLambdaUpdate({ - functionName, - functionId, - layerVersionArn, - }); - - if (needToUpdate) { - try { - const updateFunctionConfigurationCommand = - new UpdateFunctionConfigurationCommand({ - FunctionName: functionName, - Layers: layers, - Environment: { - Variables: environmentVariables, - }, - //Timeout: LlDebugger.argOptions.observable ? undefined : 300, // Increase the timeout to 5 minutes - Timeout: Math.max(initialTimeout, 300), // Increase the timeout to min. 5 minutes - }); - - await getLambdaClient().send(updateFunctionConfigurationCommand); - - Logger.verbose( - `[Function ${functionName}] Lambda layer and environment variables updated`, - ); - } catch (error: any) { - throw new Error( - `Failed to update Lambda ${functionName}: ${error.message}`, - { cause: error }, - ); - } - } else { - Logger.verbose( - `[Function ${functionName}] Lambda layer and environment already up to date`, +async function removeLayerFromLambda(lambdaData: InfraLambdaUpdate) { + Logger.verbose( + `[Function ${lambdaData.functionName}] Removing layer and environment variables`, + ); + try { + await updateLambda(lambdaData); + } catch (error: any) { + throw new Error( + `Failed to remove layer from lambda ${lambdaData.functionName}.`, + { cause: error }, ); } } /** - * Prepare the Lambda function for the update + * General function to update the Lambda function configuration */ -async function prepareLambdaUpdate({ - functionName, - functionId, - layerVersionArn, -}: { - functionName: string; - functionId: string; - layerVersionArn: string; -}) { - let needToUpdate: boolean = false; +async function updateLambda(lambdaData: InfraLambdaUpdate) { + const updateFunctionConfigurationCommand = + new UpdateFunctionConfigurationCommand({ + FunctionName: lambdaData.functionName, + Layers: lambdaData.layers, + Environment: { + Variables: lambdaData.environmentVariables, + }, + Timeout: lambdaData.timeout, + }); + await getLambdaClient().send(updateFunctionConfigurationCommand); +} + +/** + * Analyze the Lambda function to determine if it needs to be updated + * @param func - Lambda function properties + * @param existingLayerVersionArn - ARN of existing layer version if available + * @returns Lambda update configuration or undefined if no update needed + */ +async function analyzeLambdaAdd( + functionName: string, + existingLayerVersionArn: string | undefined, +) { const { environmentVariables, ddlLayerArns, otherLayerArns, initialTimeout } = - await getLambdaCongfiguration(functionName); + await getLambdaConfiguration(functionName); + + if (!existingLayerVersionArn) { + const ddlEnvironmentVariables = getEnvironmentVariablesForDebugger({ + functionName, + timeout: initialTimeout, + verbose: Configuration.config.verbose, + initialExecWrapper: + environmentVariables.AWS_LAMBDA_EXEC_WRAPPER !== lldWrapperPath + ? environmentVariables.AWS_LAMBDA_EXEC_WRAPPER + : undefined, + }); - // check if layer is already attached - if (!ddlLayerArns?.find((arn) => arn === layerVersionArn)) { - needToUpdate = true; - Logger.verbose( - `[Function ${functionName}] Layer not attached to the function`, - ); - } else { Logger.verbose( - `[Function ${functionName}] Layer already attached to the function`, + `[Function ${functionName}] Layer does not exist at all, need to add it and attach to the function`, ); - } - - // check if layers with the wrong version are attached - if (!needToUpdate && ddlLayerArns.find((arn) => arn !== layerVersionArn)) { - needToUpdate = true; - Logger.verbose('Layer with the wrong version attached to the function'); - } - - // support for multiple internal Lambda extensions - const initalExecWraper = - environmentVariables.AWS_LAMBDA_EXEC_WRAPPER !== lldWrapperPath - ? environmentVariables.AWS_LAMBDA_EXEC_WRAPPER - : undefined; - if (initalExecWraper) { - Logger.warn( - `[Function ${functionName}] Another internal Lambda extension is already attached to the function, which might cause unpredictable behavior.`, - ); - } + return { + functionName, + layers: otherLayerArns, + environmentVariables: { + ...environmentVariables, + ...ddlEnvironmentVariables, + }, + timeout: Math.max(initialTimeout, 300), + }; + } else { + let needToUpdate: boolean = false; - const ddlEnvironmentVariables = getEnvironmentVarablesForDebugger({ - functionId, - timeout: initialTimeout, - verbose: Configuration.config.verbose, - initalExecWraper, - }); + // check if layer is already attached + if (!ddlLayerArns?.find((arn) => arn === existingLayerVersionArn)) { + needToUpdate = true; + Logger.verbose( + `[Function ${functionName}] Layer not attached to the function`, + ); + } else { + Logger.verbose( + `[Function ${functionName}] Layer already attached to the function`, + ); + } - // check if environment variables are already set for each property - for (const [key, value] of Object.entries(ddlEnvironmentVariables)) { - if (!environmentVariables || environmentVariables[key] !== value) { + // check if layers with the wrong version are attached + if ( + !needToUpdate && + ddlLayerArns.find((arn) => arn !== existingLayerVersionArn) + ) { needToUpdate = true; Logger.verbose( - `[Function ${functionName}] need to update environment variables`, + `[Function ${functionName}] Layer with the wrong version attached to the function`, ); - break; } - } - return { - needToUpdate, - layers: [layerVersionArn, ...otherLayerArns], - environmentVariables: { - ...environmentVariables, - ...ddlEnvironmentVariables, - }, - initialTimeout, - }; + // support for multiple internal Lambda extensions + const initialExecWrapper = + environmentVariables.AWS_LAMBDA_EXEC_WRAPPER !== lldWrapperPath + ? environmentVariables.AWS_LAMBDA_EXEC_WRAPPER + : undefined; + + if (initialExecWrapper) { + Logger.warn( + `[Function ${functionName}] Another internal Lambda extension is already attached to the function, which might cause unpredictable behavior.`, + ); + } + + const ddlEnvironmentVariables = getEnvironmentVariablesForDebugger({ + functionName, + timeout: initialTimeout, + verbose: Configuration.config.verbose, + initialExecWrapper, + }); + + // check if environment variables are already set for each property + for (const [key, value] of Object.entries(ddlEnvironmentVariables)) { + if (!environmentVariables || environmentVariables[key] !== value) { + needToUpdate = true; + Logger.verbose( + `[Function ${functionName}] need to update environment variables`, + ); + break; + } + } + + return needToUpdate + ? { + functionName, + layers: [existingLayerVersionArn, ...otherLayerArns], + environmentVariables: { + ...environmentVariables, + ...ddlEnvironmentVariables, + }, + timeout: Math.max(initialTimeout, 300), + } + : undefined; + } } /** * Add the policy to the Lambda role */ -async function lambdaRoleUpdate(roleName: string) { - // add inline policy to the role using PutRolePolicyCommand - Logger.verbose(`[Role ${roleName}] Attaching policy to the role ${roleName}`); - - await getIAMClient().send( - new PutRolePolicyCommand({ - RoleName: roleName, - PolicyName: inlinePolicyName, - PolicyDocument: JSON.stringify(policyDocument), - }), - ); +async function addPolicyToRole(roleName: string) { + Logger.verbose(`[Role ${roleName}] Attaching policy to the role`); + try { + await getIAMClient().send( + new PutRolePolicyCommand({ + RoleName: roleName, + PolicyName: inlinePolicyName, + PolicyDocument: JSON.stringify(policyDocument), + }), + ); + } catch (error: any) { + throw new Error(`Failed to attach policy to role ${roleName}.`, { + cause: error, + }); + } } /** @@ -582,7 +944,11 @@ async function lambdaRoleUpdate(roleName: string) { * @param functionName * @returns */ -async function prepareLambdaRoleUpdate(functionName: string) { +async function analyzeLambdaRoleAdd(functionName: string) { + Logger.verbose( + `[Function ${functionName}] Analyzing role for policy attachment`, + ); + const getFunctionResponse = await getLambdaClient().send( new GetFunctionCommand({ FunctionName: functionName, @@ -591,7 +957,7 @@ async function prepareLambdaRoleUpdate(functionName: string) { const roleArn = getFunctionResponse.Configuration?.Role; if (!roleArn) { throw new Error( - `Failed to retrieve the role ARN for Lambda ${functionName}`, + `[Function ${functionName}] Failed to retrieve the role ARN.`, ); } @@ -600,11 +966,13 @@ async function prepareLambdaRoleUpdate(functionName: string) { if (!roleName) { throw new Error( - `Failed to extract role name from role ARN: ${roleArn} for lambda ${functionName}`, + `[Function ${functionName}] Failed to extract role name from role ARN: ${roleArn}.`, ); } - const existingPolicy = await getPolicyDocument(roleName); + Logger.verbose(`[Function ${functionName}] Found role: ${roleName}`); + + const existingPolicy = await createPolicyDocument(roleName); let addPolicy: boolean = true; @@ -615,7 +983,15 @@ async function prepareLambdaRoleUpdate(functionName: string) { `[Function ${functionName}] Policy already attached to the role ${roleName}`, ); addPolicy = false; + } else { + Logger.verbose( + `[Function ${functionName}] Different policy found on role ${roleName}, will update`, + ); } + } else { + Logger.verbose( + `[Function ${functionName}] No policy found on role ${roleName}, will attach`, + ); } return { addPolicy, roleName }; } @@ -623,19 +999,19 @@ async function prepareLambdaRoleUpdate(functionName: string) { /** * Get the environment variables for the Lambda function */ -function getEnvironmentVarablesForDebugger({ - functionId, +function getEnvironmentVariablesForDebugger({ + functionName, timeout, verbose, - initalExecWraper, + initialExecWrapper, }: { - functionId: string; + functionName: string; timeout: number | undefined; verbose: boolean | undefined; - initalExecWraper: string | undefined; + initialExecWrapper: string | undefined; }): Record { const env: Record = { - LLD_FUNCTION_ID: functionId, + LLD_FUNCTION_ID: functionName, AWS_LAMBDA_EXEC_WRAPPER: lldWrapperPath, LLD_DEBUGGER_ID: Configuration.config.debuggerId, LLD_INITIAL_TIMEOUT: timeout ? timeout.toString() : '-1', // should never be negative @@ -643,8 +1019,8 @@ function getEnvironmentVarablesForDebugger({ LLD_OBSERVABLE_INTERVAL: Configuration.config.interval.toString(), }; - if (initalExecWraper) { - env.LLD_INITIAL_AWS_LAMBDA_EXEC_WRAPPER = initalExecWraper; + if (initialExecWrapper) { + env.LLD_INITIAL_AWS_LAMBDA_EXEC_WRAPPER = initialExecWrapper; } if (verbose) { @@ -654,224 +1030,10 @@ function getEnvironmentVarablesForDebugger({ return env; } -/** - * Remove the policy from the Lambda role - * @param functionName - * @returns - */ -async function removePolicyFromLambdaRole(functionName: string) { - try { - // Retrieve the Lambda function's execution role ARN - const getFunctionResponse = await getLambdaClient().send( - new GetFunctionCommand({ - FunctionName: functionName, - }), - ); - const roleArn = getFunctionResponse.Configuration?.Role; - if (!roleArn) { - throw new Error( - `Failed to retrieve the role ARN for lambda ${functionName}`, - ); - } - - // Extract the role name from the role ARN - const roleName = roleArn.split('/').pop(); - - if (!roleName) { - Logger.error( - `Failed to extract role name from role ARN: ${roleArn} for Lambda ${functionName}`, - ); - return; - } - - const existingPolicy = await getPolicyDocument(roleName); - - if (existingPolicy) { - try { - Logger.verbose( - `[Function ${functionName}] Removing policy from the role ${roleName}`, - ); - await getIAMClient().send( - new DeleteRolePolicyCommand({ - RoleName: roleName, - PolicyName: inlinePolicyName, - }), - ); - } catch (error: any) { - Logger.error( - `Failed to delete inline policy ${inlinePolicyName} from role ${roleName} for Lambda ${functionName}:`, - error, - ); - } - } else { - Logger.verbose( - `[Function ${functionName}] No need to remove policy from the role ${roleName}, policy not found`, - ); - } - } catch (error: any) { - throw new Error( - `Failed to remove policy from the role for Lambda ${functionName}: ${error.message}`, - { cause: error }, - ); - } -} - -/** - * Get the policy document needed to attach to the Lambda role needed for the Lambda Live Debugger - * @param roleName - * @returns - */ -async function getPolicyDocument(roleName: string) { - try { - const policy = await getIAMClient().send( - new GetRolePolicyCommand({ - RoleName: roleName, - PolicyName: inlinePolicyName, - }), - ); - - if (policy.PolicyDocument) { - const policyDocument = JSON.parse( - decodeURIComponent(policy.PolicyDocument), - ); - return policyDocument; - } else { - return undefined; - } - } catch (error: any) { - if (error.name === 'NoSuchEntityException') { - return undefined; - } else { - throw error; - } - } -} - -/** - * Deploy the infrastructure - */ -async function deployInfrastructure() { - const layerVersionArn = await deployLayer(); - - const promises: Promise[] = []; - - for (const func of Configuration.getLambdas()) { - const p = updateLambda({ - functionName: func.functionName, - functionId: func.functionId, - layerVersionArn: layerVersionArn, - }); - if (process.env.DISABLE_PARALLEL_DEPLOY === 'true') { - await p; - } else { - promises.push(p); - } - } - - const rolesToUpdatePromise = Promise.all( - Configuration.getLambdas().map(async (func) => { - const roleUpdate = await prepareLambdaRoleUpdate(func.functionName); - - return roleUpdate.addPolicy ? roleUpdate.roleName : undefined; - }), - ); - const rolesToUpdate = await rolesToUpdatePromise; - const rolesToUpdateFiltered = [ - // unique roles - ...new Set(rolesToUpdate.filter((r) => r)), - ] as string[]; - - for (const roleName of rolesToUpdateFiltered) { - const p = lambdaRoleUpdate(roleName); - if (process.env.DISABLE_PARALLEL_DEPLOY === 'true') { - await p; - } else { - promises.push(p); - } - } - - await Promise.all(promises); -} - -/** - * Get the planed infrastructure changes - */ -async function getPlanedInfrastructureChanges() { - const existingLayer = await findExistingLayerVersion(); - - const lambdasToUpdatePromise = Promise.all( - Configuration.getLambdas().map(async (func) => { - if (!existingLayer?.LayerVersionArn) { - return func.functionName; - } else { - const lambdaUpdate = await prepareLambdaUpdate({ - functionName: func.functionName, - functionId: func.functionId, - layerVersionArn: existingLayer.LayerVersionArn, - }); - - return lambdaUpdate.needToUpdate ? func.functionName : undefined; - } - }), - ); - - const rolesToUpdatePromise = Promise.all( - Configuration.getLambdas().map(async (func) => { - const roleUpdate = await prepareLambdaRoleUpdate(func.functionName); - - return roleUpdate.addPolicy ? roleUpdate.roleName : undefined; - }), - ); - - const lambdasToUpdate = await lambdasToUpdatePromise; - const lambdasToUpdateFiltered = lambdasToUpdate.filter((l) => l) as string[]; - - const rolesToUpdate = await rolesToUpdatePromise; - const rolesToUpdateFiltered = [ - ...new Set(rolesToUpdate.filter((r) => r)), - ] as string[]; - - return { - deployLayer: !existingLayer, - lambdasToUpdate: lambdasToUpdateFiltered, - rolesToUpdate: rolesToUpdateFiltered, - }; -} - -/** - * Remove the infrastructure - */ -async function removeInfrastructure() { - Logger.verbose('Removing Lambda Live Debugger infrastructure.'); - const promises: Promise[] = []; - - for (const func of Configuration.getLambdas()) { - const p = removeLayerFromLambda(func.functionName); - if (process.env.DISABLE_PARALLEL_DEPLOY === 'true') { - await p; - } else { - promises.push(p); - } - } - - const p = (async () => { - // do not do it in parallel, because Lambdas could share the same role - for (const func of Configuration.getLambdas()) { - await removePolicyFromLambdaRole(func.functionName); - } - })(); // creates one promise - if (process.env.DISABLE_PARALLEL_DEPLOY === 'true') { - await p; - } else { - promises.push(p); - } - - await Promise.all(promises); -} - export const InfraDeploy = { - getPlanedInfrastructureChanges, - deployInfrastructure, - removeInfrastructure, + getInfraChangesForAdding, + getInfraChangesForRemoving, + applyAddingInfra, + applyRemoveInfra, deleteLayer, }; diff --git a/src/lldebugger.ts b/src/lldebugger.ts index 30467222..3f508660 100755 --- a/src/lldebugger.ts +++ b/src/lldebugger.ts @@ -77,7 +77,61 @@ async function run() { await Configuration.discoverLambdas(); if (Configuration.config.remove) { - await InfraDeploy.removeInfrastructure(); + const removalChanges = await InfraDeploy.getInfraChangesForRemoving(); + + const hasChanges = + removalChanges.lambdasToRemove.length || + removalChanges.rolesToRemove.length || + Configuration.config.remove === 'all'; + + const changesMessage = `\nThe following changes will be applied to your AWS account:${ + (removalChanges.lambdasToRemove.length + ? `\n - Remove LLD layer and environment variables from Lambdas:\n${removalChanges.lambdasToRemove + .map((l) => ` - ${l.functionName}`) + .join('\n')}` + : '') + + (removalChanges.rolesToRemove.length + ? `\n - Remove IoT permissions from IAM Roles:\n${removalChanges.rolesToRemove + .map((r) => ` - ${r}`) + .join('\n')}` + : '') + + (Configuration.config.remove === 'all' + ? `\n - Delete Lambda Live Debugger layer` + : '') + }`; + + if (!hasChanges) { + Logger.log('No infrastructure to remove.'); + } else if (Configuration.config.approval === true) { + // ask for approval with changes shown in the prompt + try { + const confirm = await inquirer.prompt([ + { + type: 'confirm', + name: 'approval', + message: `${changesMessage}\n\nDo you want to continue?`, + }, + ]); + + if (!confirm.approval) { + Logger.log('Exiting...'); + return; + } + } catch (error: any) { + if (error.name === 'ExitPromptError') { + // user canceled the prompt + Logger.log('Exiting...'); + return; + } else { + throw error; + } + } + } else { + // show changes without approval + Logger.log(changesMessage); + } + + await InfraDeploy.applyRemoveInfra(removalChanges); // await GitIgnore.removeFromGitIgnore(); // delete folder .lldebugger const folder = path.join(getProjectDirname(), '.lldebugger'); @@ -93,71 +147,87 @@ async function run() { return; } - if (!Configuration.getLambdas().length) { + if (!Configuration.getLambdasFiltered().length) { Logger.error('No Lambdas found. Exiting...'); return; } - if (Configuration.config.approval === true) { - const changes = await InfraDeploy.getPlanedInfrastructureChanges(); - - if ( - !changes.deployLayer && - !changes.lambdasToUpdate.length && - !changes.rolesToUpdate.length - ) { - Logger.verbose('No infrastructure changes required.'); - } else { - // list all changes and ask for approval - try { - const confirn = await inquirer.prompt([ - { - type: 'confirm', - name: 'approval', - message: `\nThe following changes will be applied to your AWS account:${ - (changes.deployLayer - ? `\n - Deploy Lambda Live Debugger layer version ${version}` - : '') + - (changes.lambdasToUpdate.length - ? `\n - Attach the layer and add environment variables to the Lambdas:\n${changes.lambdasToUpdate - .map((l) => ` - ${l}`) - .join('\n')}` - : '') + - (changes.rolesToUpdate.length - ? `\n - Add IoT permissions to IAM Roles:\n${changes.rolesToUpdate - .map((r) => ` - ${r}`) - .join('\n')}` - : '') - }\n\nDo you want to continue?`, - }, - ]); - - if (!confirn.approval) { - Logger.log('Exiting...'); - return; - } - } catch (error: any) { - if (error.name === 'ExitPromptError') { - // user canceled the prompt - Logger.log('Exiting...'); - return; - } else { - throw error; - } + const changes = await InfraDeploy.getInfraChangesForAdding(); + + const hasChanges = + changes.deployLayer || + changes.lambdasToAdd.length || + changes.rolesToAdd.length || + changes.lambdasToRemove.length || + changes.rolesToRemove.length; + + const changesMessage = `\nThe following changes will be applied to your AWS account:${ + (changes.deployLayer + ? `\n - Deploy Lambda Live Debugger layer version ${version}` + : '') + + (changes.lambdasToAdd.length + ? `\n - Attach the layer and add environment variables to the Lambdas:\n${changes.lambdasToAdd + .map((l) => ` - ${l.functionName}`) + .join('\n')}` + : '') + + (changes.rolesToAdd.length + ? `\n - Add IoT permissions to IAM Roles:\n${changes.rolesToAdd + .map((r) => ` - ${r}`) + .join('\n')}` + : '') + + (changes.lambdasToRemove.length + ? `\n - Remove the layer and environment variables from Lambdas no longer in scope:\n${changes.lambdasToRemove + .map((f) => ` - ${f.functionName}`) + .join('\n')}` + : '') + + (changes.rolesToRemove.length + ? `\n - Remove IoT permissions from IAM Roles no longer in scope:\n${changes.rolesToRemove + .map((r) => ` - ${r}`) + .join('\n')}` + : '') + }`; + + if (!hasChanges) { + Logger.log('No infrastructure changes required.'); + } else if (Configuration.config.approval === true) { + // ask for approval with changes shown in the prompt + try { + const confirm = await inquirer.prompt([ + { + type: 'confirm', + name: 'approval', + message: `${changesMessage}\n\nDo you want to continue?`, + }, + ]); + + if (!confirm.approval) { + Logger.log('Exiting...'); + return; + } + } catch (error: any) { + if (error.name === 'ExitPromptError') { + // user canceled the prompt + Logger.log('Exiting...'); + return; + } else { + throw error; } } + } else { + // show changes without approval + Logger.log(changesMessage); } - await InfraDeploy.deployInfrastructure(); + await InfraDeploy.applyAddingInfra(changes); const folders = [ path.resolve('.'), - ...Configuration.getLambdas().map((l) => l.codePath), + ...Configuration.getLambdasFiltered().map((l) => l.codePath), ]; // get the uppermost folder of all lambdas or the project root to watch for changes - const rootFolderForWarchingChanges = getRootFolder(folders); - FileWatcher.watchForFileChanges(rootFolderForWarchingChanges); + const rootFolderForWatchingChanges = getRootFolder(folders); + FileWatcher.watchForFileChanges(rootFolderForWatchingChanges); await LambdaConnection.connect(); Logger.log('Debugger started!'); diff --git a/src/nodeEsBuild.ts b/src/nodeEsBuild.ts index de1cdd30..652f18d7 100755 --- a/src/nodeEsBuild.ts +++ b/src/nodeEsBuild.ts @@ -1,4 +1,3 @@ -import { LambdaProps } from './types/lambdaProps.js'; import * as path from 'path'; import * as fs from 'fs/promises'; import * as esbuild from 'esbuild'; @@ -11,6 +10,7 @@ import { combineArray } from './utils/combineArray.js'; import { combineObject } from './utils/combineObject.js'; import { combineObjectStrings } from './utils/combineObjectStrings.js'; import { removeUndefinedProperties } from './utils/removeUndefinedProperties.js'; +import { LambdaResource } from './types/resourcesDiscovery.js'; type BuiltOutput = { result: Promise; @@ -95,7 +95,7 @@ async function getBuild(functionId: string) { */ async function build(input: { functionId: string; - function: LambdaProps; + function: LambdaResource; oldCtx?: esbuild.BuildContext; }): Promise<{ result: esbuild.BuildResult; diff --git a/src/resourceDiscovery.ts b/src/resourceDiscovery.ts index fe5e674d..104e6b05 100644 --- a/src/resourceDiscovery.ts +++ b/src/resourceDiscovery.ts @@ -62,14 +62,14 @@ async function getLambdas( if (config.function && !config.remove) { // if we are removing the debugger, we don't want to filter by function name const functionNameFilter = config.function.trim(); - resources = resources.filter( - // filter by function name, can use * as wildcard - (l) => + resources = resources.map((l) => { + const matches = l.functionName === functionNameFilter || new RegExp('^' + functionNameFilter.split('*').join('.*') + '$').test( l.functionName, - ), - ); + ); + return matches ? l : { ...l, filteredOut: true }; + }); } } else { return undefined; diff --git a/src/types/lambdaProps.ts b/src/types/lambdaProps.ts deleted file mode 100644 index 0ddb53eb..00000000 --- a/src/types/lambdaProps.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { LambdaResource } from './resourcesDiscovery.js'; - -export type LambdaProps = { - functionId: string; -} & LambdaResource; diff --git a/src/types/resourcesDiscovery.ts b/src/types/resourcesDiscovery.ts index 3d1dca63..f330d199 100755 --- a/src/types/resourcesDiscovery.ts +++ b/src/types/resourcesDiscovery.ts @@ -28,6 +28,8 @@ export type LambdaResource = { */ cdkPath?: string; }; + + filteredOut?: boolean; }; export enum BundlingType { From 86e2178ade37fc955bb0487d79ed8e6fb32ebd51 Mon Sep 17 00:00:00 2001 From: "Marko (ServerlessLife)" Date: Thu, 24 Jul 2025 09:25:08 +0200 Subject: [PATCH 2/8] chore: Improve messages --- README.md | 14 +++++++------- src/configuration/getConfigFromCliArgs.ts | 2 +- src/infraDeploy.ts | 21 ++++++++++----------- src/lldebugger.ts | 4 ++-- 4 files changed, 20 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index f5ec336c..64593611 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ Serverless is amazing and solves many issues with traditional systems. However, Lambda Live Debugger connects to your deployed Lambda, routes requests to your computer, and sends responses back to the deployed Lambda. This allows you to debug locally, but the system behaves as if the code is running in the cloud with the same permissions. In case of code changes, you do not have to redeploy. The code is reloaded automatically without deploying or even restarting the debugger. -The tool attaches Lambda Extensions (via a Layer), intercepts, and relays calls to AWS IoT. AWS IoT transfers messages between your Lambda and the local machine. If the Lambda is written in TypeScript, it's transpiled to JavaScript. The code is executed via the Node Worker Thread. +The tool attaches Lambda Extensions (via a layer), intercepts, and relays calls to AWS IoT. AWS IoT transfers messages between your Lambda and the local machine. If the Lambda is written in TypeScript, it's transpiled to JavaScript. The code is executed via the Node Worker Thread. ![Architecture](./public/architecture.drawio.png) @@ -34,11 +34,11 @@ AWS keys generated on the cloud for Lambda are transferred to the local environm Lambda Live Debugger makes the following changes to your AWS infrastructure: -- Deploys the Lambda Layer -- Attaches the Layer to each Lambda you're debugging +- Deploys the Lambda layer +- Attaches the layer to each Lambda you're debugging - Adds a policy to the Lambda Role for AWS IoT access -In case you do not want to debug all functions and add the Layer to them, you can limit to the ones you need via the `function` parameter. +In case you do not want to debug all functions and add the layer to them, you can limit to the ones you need via the `function` parameter. The tool generates temporary files in the `.lldebugger` folder, which can be deleted after debugging. The wizard can add `.lldebugger` to `.gitignore` for you. @@ -109,7 +109,7 @@ The configuration is saved to `lldebugger.config.ts`. ``` -V, --version output the version number - -r, --remove [option] Remove Lambda Live Debugger infrastructure. Options: 'keep-layer' (default), 'all'. The latest also removes the Lambda Layer + -r, --remove [option] Remove Lambda Live Debugger infrastructure. Options: 'keep-layer' (default), 'all'. The latest also removes the Lambda layer -w, --wizard Program interactively asks for each parameter and saves it to lldebugger.config.ts -v, --verbose Verbose logging -c, --context AWS CDK context (default: []) @@ -187,9 +187,9 @@ When you no longer want to debug and want Lambda to execute the code deployed to lld -r ``` -This detaches the Layer from your Lambdas and removes the IoT permission policy. It will not remove the Layer because others might use it. +This detaches the layer from your Lambdas and removes the IoT permission policy. It will not remove the layer because others might use it. -To also remove the Layer: +To also remove the layer: ``` lld -r=all diff --git a/src/configuration/getConfigFromCliArgs.ts b/src/configuration/getConfigFromCliArgs.ts index 3dbd34e7..a0f55341 100644 --- a/src/configuration/getConfigFromCliArgs.ts +++ b/src/configuration/getConfigFromCliArgs.ts @@ -20,7 +20,7 @@ export async function getConfigFromCliArgs( program.name('lld').description('Lambda Live Debugger').version(version); program.option( '-r, --remove [option]', - "Remove Lambda Live Debugger infrastructure. Options: 'keep-layer' (default), 'all'. The latest also removes the Lambda Layer", + "Remove Lambda Live Debugger infrastructure. Options: 'keep-layer' (default), 'all'. The latest also removes the Lambda layer", //validateRemoveOption, //"keep-layer" ); diff --git a/src/infraDeploy.ts b/src/infraDeploy.ts index cbb2820b..f466e834 100755 --- a/src/infraDeploy.ts +++ b/src/infraDeploy.ts @@ -146,23 +146,25 @@ async function findExistingLayerVersion() { nextMarker = response.NextMarker; } while (nextMarker); - Logger.verbose('No existing layer found.'); + Logger.verbose( + `No matching layer version found with description ${layerDescription}`, + ); return undefined; } /** - * Get the description of the Lambda Layer that is set to the layer + * Get the description of the Lambda layer that is set to the layer * @returns */ async function getLayerDescription() { if (!layerDescription) { - layerDescription = `Lambda Live Debugger Layer version ${await getVersion()}`; + layerDescription = `Lambda Live Debugger layer version ${await getVersion()}`; } if ((await getVersion()) === '0.0.1') { // add a random string to the description to make it unique - layerDescription = `Lambda Live Debugger Layer - development ${crypto.randomUUID()}`; + layerDescription = `Lambda Live Debugger layer - development ${crypto.randomUUID()}`; } return layerDescription; @@ -229,7 +231,7 @@ async function deployLayer() { } /** - * Delete the Lambda Layer + * Delete the Lambda layer */ async function deleteLayer() { let nextMarker: string | undefined; @@ -366,7 +368,6 @@ async function removePolicyFromLambdaRole(roleName: string) { PolicyName: inlinePolicyName, }), ); - Logger.verbose(`[Role ${roleName}] Policy removed successfully`); } catch (error: any) { throw new Error(`Failed to remove policy from the role ${roleName}.`, { cause: error, @@ -402,9 +403,7 @@ async function createPolicyDocument(roleName: string) { } } catch (error: any) { if (error.name === 'NoSuchEntityException') { - Logger.verbose( - `[Role ${roleName}] Policy does not exist (NoSuchEntityException)`, - ); + Logger.verbose(`[Role ${roleName}] Policy does not exist`); return undefined; } else { throw new Error( @@ -837,7 +836,7 @@ async function analyzeLambdaAdd( }); Logger.verbose( - `[Function ${functionName}] Layer does not exist at all, need to add it and attach to the function`, + `[Function ${functionName}] The layer for this version does not exist in the account. We need to add it and attach it to the function`, ); return { @@ -899,7 +898,7 @@ async function analyzeLambdaAdd( if (!environmentVariables || environmentVariables[key] !== value) { needToUpdate = true; Logger.verbose( - `[Function ${functionName}] need to update environment variables`, + `[Function ${functionName}] Need to update environment variables`, ); break; } diff --git a/src/lldebugger.ts b/src/lldebugger.ts index 3f508660..39e5af0e 100755 --- a/src/lldebugger.ts +++ b/src/lldebugger.ts @@ -84,7 +84,7 @@ async function run() { removalChanges.rolesToRemove.length || Configuration.config.remove === 'all'; - const changesMessage = `\nThe following changes will be applied to your AWS account:${ + const changesMessage = `The following changes will be applied to your AWS account:${ (removalChanges.lambdasToRemove.length ? `\n - Remove LLD layer and environment variables from Lambdas:\n${removalChanges.lambdasToRemove .map((l) => ` - ${l.functionName}`) @@ -161,7 +161,7 @@ async function run() { changes.lambdasToRemove.length || changes.rolesToRemove.length; - const changesMessage = `\nThe following changes will be applied to your AWS account:${ + const changesMessage = `The following changes will be applied to your AWS account:${ (changes.deployLayer ? `\n - Deploy Lambda Live Debugger layer version ${version}` : '') + From 8cc5ad929890d17e4498a849449df66f36bde429 Mon Sep 17 00:00:00 2001 From: "Marko (ServerlessLife)" Date: Thu, 24 Jul 2025 10:04:49 +0200 Subject: [PATCH 3/8] chore: increase retry --- .github/workflows/common-test.yml | 50 +++++++++++++++---------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/.github/workflows/common-test.yml b/.github/workflows/common-test.yml index 72be87dd..098bd3ce 100644 --- a/.github/workflows/common-test.yml +++ b/.github/workflows/common-test.yml @@ -73,14 +73,14 @@ jobs: run: npm run deploy working-directory: test/cdk-basic - name: Test - run: npx vitest --retry 1 test/cdk-basic.test.ts + run: npx vitest --retry 2 test/cdk-basic.test.ts - name: Test - observability mode - run: OBSERVABLE_MODE=true npx vitest --retry 1 test/cdk-basic.test.ts + run: OBSERVABLE_MODE=true npx vitest --retry 2 test/cdk-basic.test.ts - name: Deploy YAML version run: npm run deploy-yaml working-directory: test/cdk-basic - name: Test YAML - run: npx vitest --retry 1 test/cdk-basic.test.ts + run: npx vitest --retry 2 test/cdk-basic.test.ts test-cdk-esm: runs-on: ubuntu-latest @@ -127,9 +127,9 @@ jobs: run: npm run deploy working-directory: test/cdk-esm - name: Test - run: npx vitest --retry 1 test/cdk-esm.test.ts + run: npx vitest --retry 2 test/cdk-esm.test.ts - name: Test - observability mode - run: OBSERVABLE_MODE=true npx vitest --retry 1 test/cdk-esm.test.ts + run: OBSERVABLE_MODE=true npx vitest --retry 2 test/cdk-esm.test.ts test-sls-basic: runs-on: ubuntu-latest @@ -177,9 +177,9 @@ jobs: run: npm run deploy working-directory: test/sls-basic - name: Test - run: npx vitest --retry 1 test/sls-basic.test.ts + run: npx vitest --retry 2 test/sls-basic.test.ts - name: Test - observability mode - run: OBSERVABLE_MODE=true npx vitest --retry 1 test/sls-basic.test.ts + run: OBSERVABLE_MODE=true npx vitest --retry 2 test/sls-basic.test.ts test-sls-esbuild-cjs: runs-on: ubuntu-latest @@ -227,9 +227,9 @@ jobs: run: npm run deploy working-directory: test/sls-esbuild-cjs - name: Test - run: npx vitest --retry 1 test/sls-esbuild-cjs.test.ts + run: npx vitest --retry 2 test/sls-esbuild-cjs.test.ts - name: Test - observability mode - run: OBSERVABLE_MODE=true npx vitest --retry 1 test/sls-esbuild-cjs.test.ts + run: OBSERVABLE_MODE=true npx vitest --retry 2 test/sls-esbuild-cjs.test.ts test-sls-esbuild-esm: runs-on: ubuntu-latest @@ -277,9 +277,9 @@ jobs: run: npm run deploy working-directory: test/sls-esbuild-esm - name: Test - run: npx vitest --retry 1 test/sls-esbuild-esm.test.ts + run: npx vitest --retry 2 test/sls-esbuild-esm.test.ts - name: Test - observability mode - run: OBSERVABLE_MODE=true npx vitest --retry 1 test/sls-esbuild-esm.test.ts + run: OBSERVABLE_MODE=true npx vitest --retry 2 test/sls-esbuild-esm.test.ts test-osls-basic: runs-on: ubuntu-latest @@ -327,9 +327,9 @@ jobs: run: npm run deploy working-directory: test/osls-basic - name: Test - run: npx vitest --retry 1 test/osls-basic.test.ts + run: npx vitest --retry 2 test/osls-basic.test.ts - name: Test - observability mode - run: OBSERVABLE_MODE=true npx vitest --retry 1 test/osls-basic.test.ts + run: OBSERVABLE_MODE=true npx vitest --retry 2 test/osls-basic.test.ts test-osls-esbuild-cjs: runs-on: ubuntu-latest @@ -377,9 +377,9 @@ jobs: run: npm run deploy working-directory: test/osls-esbuild-cjs - name: Test - run: npx vitest --retry 1 test/osls-esbuild-cjs.test.ts + run: npx vitest --retry 2 test/osls-esbuild-cjs.test.ts - name: Test - observability mode - run: OBSERVABLE_MODE=true npx vitest --retry 1 test/osls-esbuild-cjs.test.ts + run: OBSERVABLE_MODE=true npx vitest --retry 2 test/osls-esbuild-cjs.test.ts test-osls-esbuild-esm: runs-on: ubuntu-latest @@ -427,9 +427,9 @@ jobs: run: npm run deploy working-directory: test/osls-esbuild-esm - name: Test - run: npx vitest --retry 1 test/osls-esbuild-esm.test.ts + run: npx vitest --retry 2 test/osls-esbuild-esm.test.ts - name: Test - observability mode - run: OBSERVABLE_MODE=true npx vitest --retry 1 test/osls-esbuild-esm.test.ts + run: OBSERVABLE_MODE=true npx vitest --retry 2 test/osls-esbuild-esm.test.ts test-sam-basic: runs-on: ubuntu-latest @@ -480,9 +480,9 @@ jobs: run: npm run deploy working-directory: test/sam-basic - name: Test - run: npx vitest --retry 1 test/sam-basic.test.ts + run: npx vitest --retry 2 test/sam-basic.test.ts - name: Test - observability mode - run: OBSERVABLE_MODE=true npx vitest --retry 1 test/sam-basic.test.ts + run: OBSERVABLE_MODE=true npx vitest --retry 2 test/sam-basic.test.ts test-sam-alt: runs-on: ubuntu-latest @@ -533,9 +533,9 @@ jobs: run: npm run deploy working-directory: test/sam-alt - name: Test - run: npx vitest --retry 1 test/sam-alt.test.ts + run: npx vitest --retry 2 test/sam-alt.test.ts - name: Test - observability mode - run: OBSERVABLE_MODE=true npx vitest --retry 1 test/sam-alt.test.ts + run: OBSERVABLE_MODE=true npx vitest --retry 2 test/sam-alt.test.ts test-terraform-basic: runs-on: ubuntu-latest @@ -589,9 +589,9 @@ jobs: run: npm run deploy working-directory: test/terraform-basic - name: Test - run: npx vitest --retry 1 test/terraform-basic.test.ts + run: npx vitest --retry 2 test/terraform-basic.test.ts - name: Test - observability mode - run: OBSERVABLE_MODE=true npx vitest --retry 1 test/terraform-basic.test.ts + run: OBSERVABLE_MODE=true npx vitest --retry 2 test/terraform-basic.test.ts test-opentofu-basic: runs-on: ubuntu-latest @@ -645,6 +645,6 @@ jobs: run: npm run deploy working-directory: test/opentofu-basic - name: Test - run: npx vitest --retry 1 test/opentofu-basic.test.ts + run: npx vitest --retry 2 test/opentofu-basic.test.ts - name: Test - observability mode - run: OBSERVABLE_MODE=true npx vitest --retry 1 test/opentofu-basic.test.ts + run: OBSERVABLE_MODE=true npx vitest --retry 2 test/opentofu-basic.test.ts From 9e593ce1de65f0354e68866036704a5f957ee4f6 Mon Sep 17 00:00:00 2001 From: "Marko (ServerlessLife)" Date: Thu, 24 Jul 2025 10:04:53 +0200 Subject: [PATCH 4/8] fix: Role fix --- src/infraDeploy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/infraDeploy.ts b/src/infraDeploy.ts index f466e834..1f7fb9bf 100755 --- a/src/infraDeploy.ts +++ b/src/infraDeploy.ts @@ -544,7 +544,7 @@ async function getInfraChangesForAdding(): Promise { // Filter out roles that are being added to avoid conflicts rolesToRemoveFiltered = rolesToRemoveFiltered.filter( - (r) => !rolesToAddFiltered.includes(r), + (r) => !rolesToAdd.includes(r), ); return { From d56c8b920a01d1a078319ad7314df8dfc8551d75 Mon Sep 17 00:00:00 2001 From: "Marko (ServerlessLife)" Date: Thu, 24 Jul 2025 11:46:31 +0200 Subject: [PATCH 5/8] fix: Optimization --- src/infraDeploy.ts | 209 ++++++++++++++++++++++++++------------------- 1 file changed, 122 insertions(+), 87 deletions(-) diff --git a/src/infraDeploy.ts b/src/infraDeploy.ts index 1f7fb9bf..0250421d 100755 --- a/src/infraDeploy.ts +++ b/src/infraDeploy.ts @@ -303,15 +303,13 @@ async function deleteLayerVersion( } /** - * Check if policy needs to be removed from the Lambda role + * Get the role name from a Lambda function * @param functionName - * @returns + * @returns role name */ -async function analyzeRemovePolicyFromLambdaRole(functionName: string) { +async function getRoleNameFromFunction(functionName: string): Promise { try { - Logger.verbose( - `[Function ${functionName}] Analyzing policy removal from Lambda role`, - ); + Logger.verbose(`[Function ${functionName}] Getting role from function`); const getFunctionResponse = await getLambdaClient().send( new GetFunctionCommand({ @@ -325,6 +323,7 @@ async function analyzeRemovePolicyFromLambdaRole(functionName: string) { ); } + // Extract the role name from the role ARN const roleName = roleArn.split('/').pop(); if (!roleName) { @@ -334,12 +333,30 @@ async function analyzeRemovePolicyFromLambdaRole(functionName: string) { } Logger.verbose(`[Function ${functionName}] Found role: ${roleName}`); + return roleName; + } catch (error: any) { + throw new Error(`Failed to get role name from function ${functionName}.`, { + cause: error, + }); + } +} + +/** + * Check if policy needs to be removed from the Lambda role + * @param roleName + * @returns + */ +async function analyzeRoleRemove(roleName: string) { + try { + Logger.verbose( + `[Role ${roleName}] Analyzing policy removal from Lambda role`, + ); const existingPolicy = await createPolicyDocument(roleName); const needToRemovePolicy = !!existingPolicy; Logger.verbose( - `[Function ${functionName}] Policy ${needToRemovePolicy ? 'needs to be removed' : 'not found to remove'} from role ${roleName}`, + `[Role ${roleName}] Policy ${needToRemovePolicy ? 'needs to be removed' : 'not found to remove'} from role ${roleName}`, ); return { @@ -347,10 +364,9 @@ async function analyzeRemovePolicyFromLambdaRole(functionName: string) { roleName, }; } catch (error: any) { - throw new Error( - `Failed to analyze removal policy from Lambda ${functionName}.`, - { cause: error }, - ); + throw new Error(`Failed to analyze removal policy from role ${roleName}.`, { + cause: error, + }); } } @@ -502,24 +518,54 @@ async function getInfraChangesForAdding(): Promise { }), ); - const rolesToAddPromise = Promise.all( + const lambdasToRemovePromise = Promise.all( + configLambdasRemove.map(async (func) => { + return analyzeLambdaRemove(func.functionName); + }), + ); + + // Get all role names for lambdas to update, ensure uniqueness, then analyze + const roleNamesToAddSet = new Set(); + const roleNamesToAddPromise = Promise.all( configLambdasUpdate.map(async (func) => { - const roleUpdate = await analyzeLambdaRoleAdd(func.functionName); - return roleUpdate.addPolicy ? roleUpdate.roleName : undefined; + const roleName = await getRoleNameFromFunction(func.functionName); + roleNamesToAddSet.add(roleName); }), ); - const lambdasToRemovePromise = Promise.all( + // Get all role names for lambdas to remove, ensure uniqueness, then analyze + const roleNamesToRemoveSet = new Set(); + const roleNamesToRemovePromise = Promise.all( configLambdasRemove.map(async (func) => { - return analyzeRemoveLambda(func.functionName); + const roleName = await getRoleNameFromFunction(func.functionName); + roleNamesToRemoveSet.add(roleName); }), ); + // Analyze roles to add + await roleNamesToAddPromise; + + const roleNamesToAdd = Array.from(roleNamesToAddSet); + const rolesToAddPromise = Promise.all( + roleNamesToAdd.map(async (roleName) => { + const roleUpdate = await analyzeRoleAdd(roleName); + return roleUpdate.addPolicy ? roleUpdate.roleName : undefined; + }), + ); + + // Analyze roles to remove + await roleNamesToRemovePromise; + + let roleNamesToRemove = Array.from(roleNamesToRemoveSet); + + // make sure that roles removed are not in the list to add + roleNamesToRemove = roleNamesToRemove.filter( + (role) => !roleNamesToAdd.includes(role), + ); + const rolesToRemovePromise = Promise.all( - configLambdasRemove.map(async (func) => { - const roleRemoval = await analyzeRemovePolicyFromLambdaRole( - func.functionName, - ); + roleNamesToRemove.map(async (roleName) => { + const roleRemoval = await analyzeRoleRemove(roleName); return roleRemoval.needToRemovePolicy ? roleRemoval.roleName : undefined; }), ); @@ -540,12 +586,7 @@ async function getInfraChangesForAdding(): Promise { ) as InfraLambdaUpdate[]; const rolesToRemove = await rolesToRemovePromise; - let rolesToRemoveFiltered = rolesToRemove.filter((r) => r) as string[]; - - // Filter out roles that are being added to avoid conflicts - rolesToRemoveFiltered = rolesToRemoveFiltered.filter( - (r) => !rolesToAdd.includes(r), - ); + const rolesToRemoveFiltered = rolesToRemove.filter((r) => r) as string[]; return { deployLayer: !existingLayer, @@ -562,7 +603,7 @@ async function getInfraChangesForAdding(): Promise { * @param func - Lambda function properties * @returns Lambda update configuration or undefined if no update needed */ -async function analyzeRemoveLambda(functionName: string) { +async function analyzeLambdaRemove(functionName: string) { try { const { environmentVariables, @@ -601,8 +642,8 @@ async function analyzeRemoveLambda(functionName: string) { } Logger.verbose( - `[Function ${functionName}] ${needToRemoveEnvironmentVariables ? 'Removing environment variables' : 'No environment variables to remove'}. Existing environment variables: `, - JSON.stringify(ddlEnvironmentVariables, null, 2), + `[Function ${functionName}] ${needToRemoveEnvironmentVariables ? 'Environment variables needed to be removed' : 'No environment variables to remove'}. Existing environment variables: ` + + JSON.stringify(environmentVariables, null, 2), ); const needToRemove = needToRemoveLayer || needToRemoveEnvironmentVariables; @@ -661,15 +702,24 @@ async function getInfraChangesForRemoving(): Promise { const lambdasToRemovePromise = Promise.all( allLambdas.map(async (func) => { - return analyzeRemoveLambda(func.functionName); + return analyzeLambdaRemove(func.functionName); }), ); - const rolesToRemovePromise = Promise.all( + // Get all role names for lambdas to remove, ensure uniqueness, then analyze + const roleNamesToRemoveSet = new Set(); + await Promise.all( allLambdas.map(async (func) => { - const roleRemoval = await analyzeRemovePolicyFromLambdaRole( - func.functionName, - ); + const roleName = await getRoleNameFromFunction(func.functionName); + roleNamesToRemoveSet.add(roleName); + }), + ); + + const roleNamesToRemove = Array.from(roleNamesToRemoveSet); + + const rolesToRemovePromise = Promise.all( + roleNamesToRemove.map(async (roleName) => { + const roleRemoval = await analyzeRoleRemove(roleName); return roleRemoval.needToRemovePolicy ? roleRemoval.roleName : undefined; }), ); @@ -849,11 +899,11 @@ async function analyzeLambdaAdd( timeout: Math.max(initialTimeout, 300), }; } else { - let needToUpdate: boolean = false; + let needToUpdateLayer: boolean = false; // check if layer is already attached if (!ddlLayerArns?.find((arn) => arn === existingLayerVersionArn)) { - needToUpdate = true; + needToUpdateLayer = true; Logger.verbose( `[Function ${functionName}] Layer not attached to the function`, ); @@ -865,10 +915,10 @@ async function analyzeLambdaAdd( // check if layers with the wrong version are attached if ( - !needToUpdate && + !needToUpdateLayer && ddlLayerArns.find((arn) => arn !== existingLayerVersionArn) ) { - needToUpdate = true; + needToUpdateLayer = true; Logger.verbose( `[Function ${functionName}] Layer with the wrong version attached to the function`, ); @@ -893,18 +943,21 @@ async function analyzeLambdaAdd( initialExecWrapper, }); + let needToUpdateEnvironmentVariables = false; + // check if environment variables are already set for each property for (const [key, value] of Object.entries(ddlEnvironmentVariables)) { if (!environmentVariables || environmentVariables[key] !== value) { - needToUpdate = true; - Logger.verbose( - `[Function ${functionName}] Need to update environment variables`, - ); + needToUpdateEnvironmentVariables = true; break; } } + Logger.verbose( + `[Function ${functionName}] ${needToUpdateEnvironmentVariables ? 'Need to update environment variables' : 'No need to update environment variables'}. Existing environment variables: ` + + JSON.stringify(environmentVariables, null, 2), + ); - return needToUpdate + return needToUpdateLayer || needToUpdateEnvironmentVariables ? { functionName, layers: [existingLayerVersionArn, ...otherLayerArns], @@ -940,59 +993,41 @@ async function addPolicyToRole(roleName: string) { /** * Prepare the Lambda role for the update - * @param functionName + * @param roleName * @returns */ -async function analyzeLambdaRoleAdd(functionName: string) { - Logger.verbose( - `[Function ${functionName}] Analyzing role for policy attachment`, - ); - - const getFunctionResponse = await getLambdaClient().send( - new GetFunctionCommand({ - FunctionName: functionName, - }), - ); - const roleArn = getFunctionResponse.Configuration?.Role; - if (!roleArn) { - throw new Error( - `[Function ${functionName}] Failed to retrieve the role ARN.`, - ); - } - - // Extract the role name from the role ARN - const roleName = roleArn.split('/').pop(); - - if (!roleName) { - throw new Error( - `[Function ${functionName}] Failed to extract role name from role ARN: ${roleArn}.`, - ); - } - - Logger.verbose(`[Function ${functionName}] Found role: ${roleName}`); +async function analyzeRoleAdd(roleName: string) { + try { + Logger.verbose(`[Role ${roleName}] Analyzing role for policy attachment`); - const existingPolicy = await createPolicyDocument(roleName); + const existingPolicy = await createPolicyDocument(roleName); - let addPolicy: boolean = true; + let addPolicy: boolean = true; - // compare existing policy with the new one - if (existingPolicy) { - if (JSON.stringify(existingPolicy) === JSON.stringify(policyDocument)) { - Logger.verbose( - `[Function ${functionName}] Policy already attached to the role ${roleName}`, - ); - addPolicy = false; + // compare existing policy with the new one + if (existingPolicy) { + if (JSON.stringify(existingPolicy) === JSON.stringify(policyDocument)) { + Logger.verbose( + `[Role ${roleName}] Policy already attached to the role`, + ); + addPolicy = false; + } else { + Logger.verbose( + `[Role ${roleName}] Different policy found on role, will update`, + ); + } } else { - Logger.verbose( - `[Function ${functionName}] Different policy found on role ${roleName}, will update`, - ); + Logger.verbose(`[Role ${roleName}] No policy found on role, will attach`); } - } else { - Logger.verbose( - `[Function ${functionName}] No policy found on role ${roleName}, will attach`, + return { addPolicy, roleName }; + } catch (error: any) { + throw new Error( + `Failed to analyze role ${roleName} for policy attachment.`, + { + cause: error, + }, ); } - return { addPolicy, roleName }; } /** From b8c433cb04157a28bf9cc3d3f0a129d8a396a2bb Mon Sep 17 00:00:00 2001 From: "Marko (ServerlessLife)" Date: Thu, 24 Jul 2025 13:06:28 +0200 Subject: [PATCH 6/8] chore: improve CICD --- .github/workflows/publish.yml | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index de41eb41..1fe9a38a 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -6,16 +6,6 @@ run-name: > on: workflow_dispatch: - inputs: - release-type: - description: 'Release type' - required: true - default: 'stable' - type: choice - options: - - stable - - alpha - - beta permissions: id-token: write From 107f9c9e055ee64a2cc6478079ba844b22ef2dbb Mon Sep 17 00:00:00 2001 From: "Marko (ServerlessLife)" Date: Thu, 24 Jul 2025 13:53:06 +0200 Subject: [PATCH 7/8] chore: CICD remove old layers --- .github/workflows/pull-request.yml | 33 +++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index cfd0f584..9f6da130 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -15,10 +15,41 @@ jobs: uses: ./.github/workflows/common-build.yml secrets: inherit + remove-old-layers: + runs-on: ubuntu-latest + needs: build + concurrency: + group: remove-all-layers + steps: + - uses: actions/checkout@v4 + - name: Use Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.node_version }} + registry-url: 'https://registry.npmjs.org' + - name: Install dependencies + run: npm ci + - name: Download build artifact + uses: actions/download-artifact@v4 + with: + name: dist + path: dist + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-region: eu-west-1 + role-to-assume: ${{ secrets.AWS_ROLE }} + role-session-name: GitHubActions + - name: Remove old layers + run: | + node ../../dist/lldebugger.mjs -r all --config-env=test -v + # Picking random test so I can have environment + working-directory: test/sam-basic + test: uses: ./.github/workflows/common-test.yml secrets: inherit - needs: build + needs: remove-old-layers with: mode: build testMonorepo: false From 0099b63b530f19a0ffac2b7ddd1669b8e2c6fced Mon Sep 17 00:00:00 2001 From: "Marko (ServerlessLife)" Date: Thu, 24 Jul 2025 13:58:53 +0200 Subject: [PATCH 8/8] fix: Removing folder .lldebugger --- src/lldebugger.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/lldebugger.ts b/src/lldebugger.ts index 39e5af0e..49d93e6a 100755 --- a/src/lldebugger.ts +++ b/src/lldebugger.ts @@ -135,8 +135,13 @@ async function run() { // await GitIgnore.removeFromGitIgnore(); // delete folder .lldebugger const folder = path.join(getProjectDirname(), '.lldebugger'); - Logger.verbose(`Removing ${folder} folder...`); - await fs.rm(folder, { recursive: true }); + try { + Logger.verbose(`Removing ${folder} folder...`); + await fs.access(folder); + await fs.rm(folder, { recursive: true }); + } catch { + Logger.verbose(`${folder} does not exist, skipping removal.`); + } if (Configuration.config.remove === 'all') { await InfraDeploy.deleteLayer();