diff --git a/.github/workflows/common-test.yml b/.github/workflows/common-test.yml index 59fc90fe..098bd3ce 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 @@ -74,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 @@ -128,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 @@ -178,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 @@ -228,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 @@ -278,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 @@ -328,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 @@ -378,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 @@ -428,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 @@ -481,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 @@ -534,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 @@ -590,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 @@ -646,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 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 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 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/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/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 76ff07b6..0250421d 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 @@ -116,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; @@ -145,13 +177,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 +196,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 +221,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( @@ -206,7 +231,7 @@ async function deployLayer() { } /** - * Delete the Lambda Layer + * Delete the Lambda layer */ async function deleteLayer() { let nextMarker: string | undefined; @@ -214,7 +239,6 @@ async function deleteLayer() { const layers = await getLambdaClient().send( new ListLayersCommand({ Marker: nextMarker, - MaxItems: 10, }), ); @@ -241,7 +265,6 @@ async function deleteAllVersionsOfLayer(layerArn: string): Promise { new ListLayerVersionsCommand({ LayerName: layerArn, Marker: nextMarker, - //MaxItems: 5, }), ); @@ -280,114 +303,466 @@ async function deleteLayerVersion( } /** - * Remove the layer from the Lambda function + * Get the role name from a Lambda function * @param functionName + * @returns role name */ -async function removeLayerFromLambda(functionName: string) { +async function getRoleNameFromFunction(functionName: string): Promise { try { - let needToUpdate: boolean = false; + Logger.verbose(`[Function ${functionName}] Getting role from function`); + + 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) { + throw new Error( + `Failed to extract role name from role ARN: ${roleArn} for lambda ${functionName}.`, + ); + } + + 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( + `[Role ${roleName}] 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 role ${roleName}.`, { + 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, + }), + ); + } 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`); + 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 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 roleName = await getRoleNameFromFunction(func.functionName); + roleNamesToAddSet.add(roleName); + }), + ); + // Get all role names for lambdas to remove, ensure uniqueness, then analyze + const roleNamesToRemoveSet = new Set(); + const roleNamesToRemovePromise = Promise.all( + configLambdasRemove.map(async (func) => { + 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( + roleNamesToRemove.map(async (roleName) => { + const roleRemoval = await analyzeRoleRemove(roleName); + 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; + const rolesToRemoveFiltered = rolesToRemove.filter((r) => r) as string[]; + + 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 analyzeLambdaRemove(functionName: string) { + try { const { environmentVariables, ddlLayerArns, otherLayerArns, initialTimeout, - } = await getLambdaCongfiguration(functionName); + } = await getLambdaConfiguration(functionName); + + const needToRemoveLayer = ddlLayerArns.length > 0; + let needToRemoveEnvironmentVariables = false; - if (ddlLayerArns.length > 0) { - needToUpdate = true; - Logger.verbose(`Detaching layer from the function ${functionName}`); + 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( - 'Existing environment variables', + Logger.verbose( + `[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; - //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, + }); + } +} - const updateFunctionConfigurationCommand = - new UpdateFunctionConfigurationCommand({ - FunctionName: functionName, - Layers: otherLayerArns, - Environment: { - Variables: { - ...environmentVariables, - }, - }, - Timeout: initialTimeout, - }); +/** + * Get the planned removal changes + */ +async function getInfraChangesForRemoving(): Promise { + Logger.verbose( + 'Analyzing infrastructure changes for removing Lambda Live Debugger', + ); - await getLambdaClient().send(updateFunctionConfigurationCommand); + const allLambdas = Configuration.getLambdasAll(); - 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 }, - ); + const lambdasToRemovePromise = Promise.all( + allLambdas.map(async (func) => { + return analyzeLambdaRemove(func.functionName); + }), + ); + + // Get all role names for lambdas to remove, ensure uniqueness, then analyze + const roleNamesToRemoveSet = new Set(); + await Promise.all( + allLambdas.map(async (func) => { + 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; + }), + ); + + const lambdasToRemove = await lambdasToRemovePromise; + const lambdasToRemoveFiltered = lambdasToRemove.filter( + (l) => l, + ) as InfraLambdaUpdate[]; + + const rolesToRemove = await rolesToRemovePromise; + const rolesToRemoveFiltered = rolesToRemove.filter((r) => r) as string[]; + + 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 +772,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) => @@ -428,450 +803,271 @@ async function getLambdaCongfiguration(functionName: string) { initialTimeout, }; } catch (error: any) { - throw new Error( - `Failed to get lambda configuration ${functionName}: ${error.message}`, - { cause: error }, - ); + 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 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 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 update add layer to lambda ${lambdaData.functionName}.`, + { cause: error }, ); } } - /** - * Prepare the Lambda function for the update + * Remove the layer from the Lambda function and update the environment variables + * @param lambdaData */ -async function prepareLambdaUpdate({ - functionName, - functionId, - layerVersionArn, -}: { - functionName: string; - functionId: string; - layerVersionArn: string; -}) { - let needToUpdate: boolean = false; - - const { environmentVariables, ddlLayerArns, otherLayerArns, initialTimeout } = - await getLambdaCongfiguration(functionName); - - // 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`, - ); - } - - // 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.`, +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 }, ); } - - const ddlEnvironmentVariables = getEnvironmentVarablesForDebugger({ - functionId, - timeout: initialTimeout, - verbose: Configuration.config.verbose, - initalExecWraper, - }); - - // 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, - layers: [layerVersionArn, ...otherLayerArns], - environmentVariables: { - ...environmentVariables, - ...ddlEnvironmentVariables, - }, - initialTimeout, - }; } /** - * Add the policy to the Lambda role + * General function to update the Lambda function configuration */ -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 updateLambda(lambdaData: InfraLambdaUpdate) { + const updateFunctionConfigurationCommand = + new UpdateFunctionConfigurationCommand({ + FunctionName: lambdaData.functionName, + Layers: lambdaData.layers, + Environment: { + Variables: lambdaData.environmentVariables, + }, + Timeout: lambdaData.timeout, + }); + + await getLambdaClient().send(updateFunctionConfigurationCommand); } /** - * Prepare the Lambda role for the update - * @param functionName - * @returns + * 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 prepareLambdaRoleUpdate(functionName: string) { - 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}`, - ); - } +async function analyzeLambdaAdd( + functionName: string, + existingLayerVersionArn: string | undefined, +) { + const { environmentVariables, ddlLayerArns, otherLayerArns, initialTimeout } = + await getLambdaConfiguration(functionName); - // Extract the role name from the role ARN - const roleName = roleArn.split('/').pop(); + 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, + }); - if (!roleName) { - throw new Error( - `Failed to extract role name from role ARN: ${roleArn} for lambda ${functionName}`, + Logger.verbose( + `[Function ${functionName}] The layer for this version does not exist in the account. We need to add it and attach it to the function`, ); - } - - const existingPolicy = await getPolicyDocument(roleName); - let addPolicy: boolean = true; + return { + functionName, + layers: otherLayerArns, + environmentVariables: { + ...environmentVariables, + ...ddlEnvironmentVariables, + }, + timeout: Math.max(initialTimeout, 300), + }; + } else { + let needToUpdateLayer: boolean = false; - // compare existing policy with the new one - if (existingPolicy) { - if (JSON.stringify(existingPolicy) === JSON.stringify(policyDocument)) { + // check if layer is already attached + if (!ddlLayerArns?.find((arn) => arn === existingLayerVersionArn)) { + needToUpdateLayer = true; Logger.verbose( - `[Function ${functionName}] Policy already attached to the role ${roleName}`, + `[Function ${functionName}] Layer not attached to the function`, + ); + } else { + Logger.verbose( + `[Function ${functionName}] Layer already attached to the function`, ); - addPolicy = false; } - } - return { addPolicy, roleName }; -} - -/** - * Get the environment variables for the Lambda function - */ -function getEnvironmentVarablesForDebugger({ - functionId, - timeout, - verbose, - initalExecWraper, -}: { - functionId: string; - timeout: number | undefined; - verbose: boolean | undefined; - initalExecWraper: string | undefined; -}): Record { - const env: Record = { - LLD_FUNCTION_ID: functionId, - AWS_LAMBDA_EXEC_WRAPPER: lldWrapperPath, - LLD_DEBUGGER_ID: Configuration.config.debuggerId, - LLD_INITIAL_TIMEOUT: timeout ? timeout.toString() : '-1', // should never be negative - LLD_OBSERVABLE_MODE: Configuration.config.observable ? 'true' : 'false', - LLD_OBSERVABLE_INTERVAL: Configuration.config.interval.toString(), - }; - - if (initalExecWraper) { - env.LLD_INITIAL_AWS_LAMBDA_EXEC_WRAPPER = initalExecWraper; - } - if (verbose) { - env.LLD_VERBOSE = 'true'; - } - - 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}`, + // check if layers with the wrong version are attached + if ( + !needToUpdateLayer && + ddlLayerArns.find((arn) => arn !== existingLayerVersionArn) + ) { + needToUpdateLayer = true; + Logger.verbose( + `[Function ${functionName}] Layer with the wrong version attached to the function`, ); } - // Extract the role name from the role ARN - const roleName = roleArn.split('/').pop(); + // support for multiple internal Lambda extensions + const initialExecWrapper = + environmentVariables.AWS_LAMBDA_EXEC_WRAPPER !== lldWrapperPath + ? environmentVariables.AWS_LAMBDA_EXEC_WRAPPER + : undefined; - if (!roleName) { - Logger.error( - `Failed to extract role name from role ARN: ${roleArn} for Lambda ${functionName}`, + if (initialExecWrapper) { + Logger.warn( + `[Function ${functionName}] Another internal Lambda extension is already attached to the function, which might cause unpredictable behavior.`, ); - return; } - const existingPolicy = await getPolicyDocument(roleName); + const ddlEnvironmentVariables = getEnvironmentVariablesForDebugger({ + functionName, + timeout: initialTimeout, + verbose: Configuration.config.verbose, + initialExecWrapper, + }); + + let needToUpdateEnvironmentVariables = false; - 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, - ); + // check if environment variables are already set for each property + for (const [key, value] of Object.entries(ddlEnvironmentVariables)) { + if (!environmentVariables || environmentVariables[key] !== value) { + needToUpdateEnvironmentVariables = true; + break; } - } 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 }, + 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 needToUpdateLayer || needToUpdateEnvironmentVariables + ? { + functionName, + layers: [existingLayerVersionArn, ...otherLayerArns], + environmentVariables: { + ...environmentVariables, + ...ddlEnvironmentVariables, + }, + timeout: Math.max(initialTimeout, 300), + } + : undefined; } } /** - * Get the policy document needed to attach to the Lambda role needed for the Lambda Live Debugger - * @param roleName - * @returns + * Add the policy to the Lambda role */ -async function getPolicyDocument(roleName: string) { +async function addPolicyToRole(roleName: string) { + Logger.verbose(`[Role ${roleName}] Attaching policy to the role`); try { - const policy = await getIAMClient().send( - new GetRolePolicyCommand({ + await getIAMClient().send( + new PutRolePolicyCommand({ RoleName: roleName, PolicyName: inlinePolicyName, + PolicyDocument: JSON.stringify(policyDocument), }), ); - - 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; - } + throw new Error(`Failed to attach policy to role ${roleName}.`, { + cause: error, + }); } } /** - * Deploy the infrastructure + * Prepare the Lambda role for the update + * @param roleName + * @returns */ -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); - } - } +async function analyzeRoleAdd(roleName: string) { + try { + Logger.verbose(`[Role ${roleName}] Analyzing role for policy attachment`); - const rolesToUpdatePromise = Promise.all( - Configuration.getLambdas().map(async (func) => { - const roleUpdate = await prepareLambdaRoleUpdate(func.functionName); + const existingPolicy = await createPolicyDocument(roleName); - return roleUpdate.addPolicy ? roleUpdate.roleName : undefined; - }), - ); - const rolesToUpdate = await rolesToUpdatePromise; - const rolesToUpdateFiltered = [ - // unique roles - ...new Set(rolesToUpdate.filter((r) => r)), - ] as string[]; + let addPolicy: boolean = true; - for (const roleName of rolesToUpdateFiltered) { - const p = lambdaRoleUpdate(roleName); - if (process.env.DISABLE_PARALLEL_DEPLOY === 'true') { - await p; + // 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 { - promises.push(p); + Logger.verbose(`[Role ${roleName}] No policy found on role, will attach`); } + return { addPolicy, roleName }; + } catch (error: any) { + throw new Error( + `Failed to analyze role ${roleName} for policy attachment.`, + { + cause: error, + }, + ); } - - await Promise.all(promises); } /** - * Get the planed infrastructure changes + * Get the environment variables for the Lambda function */ -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, +function getEnvironmentVariablesForDebugger({ + functionName, + timeout, + verbose, + initialExecWrapper, +}: { + functionName: string; + timeout: number | undefined; + verbose: boolean | undefined; + initialExecWrapper: string | undefined; +}): Record { + const env: Record = { + 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 + LLD_OBSERVABLE_MODE: Configuration.config.observable ? 'true' : 'false', + LLD_OBSERVABLE_INTERVAL: Configuration.config.interval.toString(), }; -} - -/** - * 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); - } + if (initialExecWrapper) { + env.LLD_INITIAL_AWS_LAMBDA_EXEC_WRAPPER = initialExecWrapper; } - 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); + if (verbose) { + env.LLD_VERBOSE = 'true'; } - await Promise.all(promises); + return env; } export const InfraDeploy = { - getPlanedInfrastructureChanges, - deployInfrastructure, - removeInfrastructure, + getInfraChangesForAdding, + getInfraChangesForRemoving, + applyAddingInfra, + applyRemoveInfra, deleteLayer, }; diff --git a/src/lldebugger.ts b/src/lldebugger.ts index 30467222..49d93e6a 100755 --- a/src/lldebugger.ts +++ b/src/lldebugger.ts @@ -77,62 +77,43 @@ async function run() { await Configuration.discoverLambdas(); if (Configuration.config.remove) { - await InfraDeploy.removeInfrastructure(); - // await GitIgnore.removeFromGitIgnore(); - // delete folder .lldebugger - const folder = path.join(getProjectDirname(), '.lldebugger'); - Logger.verbose(`Removing ${folder} folder...`); - await fs.rm(folder, { recursive: true }); - - if (Configuration.config.remove === 'all') { - await InfraDeploy.deleteLayer(); - } - - Logger.log('Lambda Live Debugger removed!'); - - return; - } - - if (!Configuration.getLambdas().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 + const removalChanges = await InfraDeploy.getInfraChangesForRemoving(); + + const hasChanges = + removalChanges.lambdasToRemove.length || + removalChanges.rolesToRemove.length || + Configuration.config.remove === 'all'; + + 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}`) + .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 confirn = await inquirer.prompt([ + const confirm = 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?`, + message: `${changesMessage}\n\nDo you want to continue?`, }, ]); - if (!confirn.approval) { + if (!confirm.approval) { Logger.log('Exiting...'); return; } @@ -145,19 +126,113 @@ async function run() { 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'); + 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(); + } + + Logger.log('Lambda Live Debugger removed!'); + + return; + } + + if (!Configuration.getLambdasFiltered().length) { + Logger.error('No Lambdas found. Exiting...'); + return; + } + + const changes = await InfraDeploy.getInfraChangesForAdding(); + + const hasChanges = + changes.deployLayer || + changes.lambdasToAdd.length || + changes.rolesToAdd.length || + changes.lambdasToRemove.length || + changes.rolesToRemove.length; + + const changesMessage = `The 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 {