From 20a59a87e37b98485716a02f7b7d9236342e33ac Mon Sep 17 00:00:00 2001 From: Hugo Lewenhaupt Date: Sat, 4 Oct 2025 03:05:22 +0200 Subject: [PATCH 1/7] feat: Support nested stacks in aws-cdk --- package-lock.json | 6 +- package.json | 3 + src/cloudFormation.ts | 47 +++++ src/frameworks/cdkFramework.ts | 109 +++++++++++- src/frameworks/cdkFrameworkWorker.mjs | 2 + test/cdk-nested.test.ts | 165 ++++++++++++++++++ test/cdk-nested/.gitignore | 12 ++ test/cdk-nested/.npmignore | 6 + test/cdk-nested/CdkNestedStack.yaml | 0 test/cdk-nested/README.md | 14 ++ test/cdk-nested/bin/cdk-nested.ts | 21 +++ test/cdk-nested/cdk.context.json | 3 + test/cdk-nested/cdk.json | 64 +++++++ test/cdk-nested/deploy-yaml.sh | 26 +++ test/cdk-nested/jest.config.js | 8 + test/cdk-nested/lib/cdk-nested-stack.ts | 10 ++ .../cdk-nested/lib/lld-nested-nested-stack.ts | 73 ++++++++ test/cdk-nested/lib/lld-nested-stack.ts | 64 +++++++ test/cdk-nested/package.json | 27 +++ .../services/testJsCommonJs/lambda.js | 40 +++++ .../services/testJsCommonJs/package.json | 8 + .../services/testJsEsModule/lambda.js | 35 ++++ .../services/testJsEsModule/package.json | 8 + .../services/testTsCommonJs/lambda.ts | 36 ++++ .../services/testTsCommonJs/package.json | 11 ++ .../services/testTsCommonJs/tsconfig.json | 7 + .../services/testTsEsModule/lambda.ts | 36 ++++ .../services/testTsEsModule/package.json | 11 ++ .../services/testTsEsModule/tsconfig.json | 7 + test/cdk-nested/tsconfig.json | 23 +++ 30 files changed, 877 insertions(+), 5 deletions(-) create mode 100644 test/cdk-nested.test.ts create mode 100644 test/cdk-nested/.gitignore create mode 100644 test/cdk-nested/.npmignore create mode 100644 test/cdk-nested/CdkNestedStack.yaml create mode 100644 test/cdk-nested/README.md create mode 100644 test/cdk-nested/bin/cdk-nested.ts create mode 100644 test/cdk-nested/cdk.context.json create mode 100644 test/cdk-nested/cdk.json create mode 100644 test/cdk-nested/deploy-yaml.sh create mode 100644 test/cdk-nested/jest.config.js create mode 100644 test/cdk-nested/lib/cdk-nested-stack.ts create mode 100644 test/cdk-nested/lib/lld-nested-nested-stack.ts create mode 100644 test/cdk-nested/lib/lld-nested-stack.ts create mode 100644 test/cdk-nested/package.json create mode 100755 test/cdk-nested/services/testJsCommonJs/lambda.js create mode 100644 test/cdk-nested/services/testJsCommonJs/package.json create mode 100755 test/cdk-nested/services/testJsEsModule/lambda.js create mode 100644 test/cdk-nested/services/testJsEsModule/package.json create mode 100755 test/cdk-nested/services/testTsCommonJs/lambda.ts create mode 100644 test/cdk-nested/services/testTsCommonJs/package.json create mode 100755 test/cdk-nested/services/testTsCommonJs/tsconfig.json create mode 100755 test/cdk-nested/services/testTsEsModule/lambda.ts create mode 100644 test/cdk-nested/services/testTsEsModule/package.json create mode 100755 test/cdk-nested/services/testTsEsModule/tsconfig.json create mode 100644 test/cdk-nested/tsconfig.json diff --git a/package-lock.json b/package-lock.json index 66943200..ea364eb5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31878,6 +31878,7 @@ "version": "11.2.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", + "dev": true, "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", @@ -32211,7 +32212,8 @@ "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true }, "node_modules/graphemer": { "version": "1.4.0", @@ -33197,6 +33199,7 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, "dependencies": { "universalify": "^2.0.0" }, @@ -40872,6 +40875,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, "engines": { "node": ">= 10.0.0" } diff --git a/package.json b/package.json index ff276d96..d54865e5 100755 --- a/package.json +++ b/package.json @@ -52,6 +52,8 @@ "test": "npm run build && RUN_TEST_FROM_CLI=true vitest run && RUN_TEST_FROM_CLI=true OBSERVABLE_MODE=true vitest run", "test-cdk-basic": "npm run build && RUN_TEST_FROM_CLI=true vitest run test/cdk-basic.test.ts", "test-cdk-basic-observable": "npm run build && RUN_TEST_FROM_CLI=true OBSERVABLE_MODE=true vitest run test/cdk-basic.test.ts", + "test-cdk-nested": "npm run build && RUN_TEST_FROM_CLI=true vitest run test/cdk-nested.test.ts", + "test-cdk-nested-observable": "npm run build && RUN_TEST_FROM_CLI=true OBSERVABLE_MODE=true vitest run test/cdk-nested.test.ts", "test-cdk-esm": "npm run build && RUN_TEST_FROM_CLI=true vitest run test/cdk-esm.test.ts", "test-cdk-esm-observable": "npm run build && RUN_TEST_FROM_CLI=true OBSERVABLE_MODE=true vitest run test/cdk-esm.test.ts", "test-sls-basic": "npm run build && RUN_TEST_FROM_CLI=true vitest run test/sls-basic.test.ts", @@ -148,6 +150,7 @@ "src/extension/*", "test", "test/cdk-basic", + "test/cdk-nested", "test/cdk-esm", "test/cdk-config", "test/sls-basic", diff --git a/src/cloudFormation.ts b/src/cloudFormation.ts index a6e02294..112c700f 100644 --- a/src/cloudFormation.ts +++ b/src/cloudFormation.ts @@ -159,7 +159,54 @@ async function getLambdasInStack( ); } +/** + * Get CloudFormation stack template + * @param stackName + * @param awsConfiguration + * @returns + */ +async function getStackResourcePhysicalResourceId( + stackName: string, + logicalResourceId: string, + awsConfiguration: AwsConfiguration, +) { + const { DescribeStackResourceCommand } = await import( + '@aws-sdk/client-cloudformation' + ); + const command = new DescribeStackResourceCommand({ + StackName: stackName, + LogicalResourceId: logicalResourceId, + }); + const cloudFormationClient = await getCloudFormationClient(awsConfiguration); + + try { + const response = await cloudFormationClient.send(command); + + if (!response.StackResourceDetail) { + throw new Error( + `No resource details found in stack ${stackName} for ${logicalResourceId}`, + ); + } + + const physicalResourceId = response.StackResourceDetail.PhysicalResourceId; + if (!physicalResourceId) + throw new Error( + `No physicalResourceId found in stack ${stackName} for ${logicalResourceId}`, + ); + return physicalResourceId; + } catch (error: any) { + if (error.name === 'ValidationError') { + Logger.error( + `Stack ${stackName} and/or ${logicalResourceId} not found. Try specifying a region. Error: ${error.message}`, + error, + ); + } + throw error; + } +} + export const CloudFormation = { getCloudFormationStackTemplate, getLambdasInStack, + getStackResourcePhysicalResourceId: getStackResourcePhysicalResourceId, }; diff --git a/src/frameworks/cdkFramework.ts b/src/frameworks/cdkFramework.ts index fffa98b2..7ead4ab0 100755 --- a/src/frameworks/cdkFramework.ts +++ b/src/frameworks/cdkFramework.ts @@ -70,14 +70,48 @@ export class CdkFramework implements IFramework { JSON.stringify(lambdasInCdk, null, 2), ); - //get all stack names - const stackNames = [ - ...new Set( // unique + const cdkTokenRegex = /^\${Token\[TOKEN\.\d+\]}$/; + const stackTokensCdkPathMappings = Object.fromEntries( + lambdasInCdk + .filter((lambda) => cdkTokenRegex.test(lambda.stackName)) + .map((lambda) => [lambda.stackName, lambda.stackCdkPath]), + ); + const realStackNames = [ + ...new Set( lambdasInCdk.map((lambda) => { - return lambda.stackName; + return lambda.rootStackName; }), ), ]; + + const unresolvedTokens = new Set(Object.keys(stackTokensCdkPathMappings)); + const resolvedTokenizedStackNames = new Set(); + + if (Object.keys(stackTokensCdkPathMappings).length) { + Logger.verbose( + `[CDK] Found tokenized stackNames: ${[...unresolvedTokens].join(', ')}`, + ); + Logger.verbose( + `[CDK] Will look for tokenized stackNames in ${realStackNames.join( + ', ', + )}`, + ); + for (const realStackName of realStackNames) { + if (!unresolvedTokens.size) break; + await this.resolveTokenizedStackNames( + unresolvedTokens, + resolvedTokenizedStackNames, + stackTokensCdkPathMappings, + awsConfiguration, + realStackName, + ); + } + } + + //get all stack names + const stackNames = [ + ...new Set(realStackNames.concat([...resolvedTokenizedStackNames])), // unique + ]; Logger.verbose( `[CDK] Found the following stacks in CDK: ${stackNames.join(', ')}`, ); @@ -170,6 +204,61 @@ export class CdkFramework implements IFramework { return lambdasDiscovered; } + protected async resolveTokenizedStackNames( + unresolvedTokens: Set, + resolvedTokenizedStackNames: Set, + stackTokensCdkPathMappings: Record, + awsConfiguration: AwsConfiguration, + stackName: string, + ): Promise { + if (!unresolvedTokens.size) { + return; + } + + const cfTemplate = await CloudFormation.getCloudFormationStackTemplate( + stackName, + awsConfiguration, + ); + + if (cfTemplate) { + const nestedStacks = Object.entries(cfTemplate.Resources) + .filter( + ([, resource]: [string, any]) => + resource.Type === 'AWS::CloudFormation::Stack', + ) + .map(([key, resource]: [string, any]) => { + return { + logicalId: key, + cdkPath: resource.Metadata['aws:cdk:path'], + }; + }); + + for (const nestedStack of nestedStacks) { + const mapping = Object.entries(stackTokensCdkPathMappings).find( + (f) => f[1] === nestedStack.cdkPath, + ); + + if (mapping) { + unresolvedTokens.delete(mapping[0]); + const physicalResourceId = + await CloudFormation.getStackResourcePhysicalResourceId( + stackName, + nestedStack.logicalId, + awsConfiguration, + ); + resolvedTokenizedStackNames.add(physicalResourceId); + await this.resolveTokenizedStackNames( + unresolvedTokens, + resolvedTokenizedStackNames, + stackTokensCdkPathMappings, + awsConfiguration, + physicalResourceId, + ); + } + } + } + } + /** * Getz Lambda functions from the CloudFormation template metadata * @param stackName @@ -308,9 +397,15 @@ export class CdkFramework implements IFramework { `; global.lambdas = global.lambdas ?? []; + let rootStack = this.stack; + while (rootStack.nestedStackParent) { + rootStack = rootStack.nestedStackParent; + } const lambdaInfo = { //cdkPath: this.node.defaultChild?.node.path ?? this.node.path, + stackCdkPath: this.stack.node.defaultChild?.node.path ?? this.stack.node.path, stackName: this.stack.stackName, + rootStackName: rootStack.stackName, codePath: props.entry, code: props.code, node: this.node, @@ -320,6 +415,8 @@ export class CdkFramework implements IFramework { // console.log("CDK INFRA: ", { // stackName: lambdaInfo.stackName, + // stackCdkPath: lambdaInfo.stackCdkPath, + // rootStackName: lambdaInfo.rootStackName, // codePath: lambdaInfo.codePath, // code: lambdaInfo.code, // handler: lambdaInfo.handler, @@ -490,6 +587,8 @@ export class CdkFramework implements IFramework { return { cdkPath: lambda.cdkPath, stackName: lambda.stackName, + stackCdkPath: lambda.stackCdkPath, + rootStackName: lambda.rootStackName, packageJsonPath, codePath, handler, @@ -572,6 +671,8 @@ export class CdkFramework implements IFramework { return lambdas as { cdkPath: string; stackName: string; + stackCdkPath: string; + rootStackName: string; codePath?: string; code: { path?: string; diff --git a/src/frameworks/cdkFrameworkWorker.mjs b/src/frameworks/cdkFrameworkWorker.mjs index 7f779045..939e9a33 100755 --- a/src/frameworks/cdkFrameworkWorker.mjs +++ b/src/frameworks/cdkFrameworkWorker.mjs @@ -25,6 +25,8 @@ parentPort.on('message', async (data) => { handler: lambda.handler, stackName: lambda.stackName, codePath: lambda.codePath, + stackCdkPath: lambda.stackCdkPath, + rootStackName: lambda.rootStackName, code: { path: lambda.code?.path, }, diff --git a/test/cdk-nested.test.ts b/test/cdk-nested.test.ts new file mode 100644 index 00000000..7ae5c5b5 --- /dev/null +++ b/test/cdk-nested.test.ts @@ -0,0 +1,165 @@ +import { expect, test, describe, beforeAll, afterAll } from 'vitest'; +import { ChildProcess } from 'child_process'; +import fs from 'fs/promises'; +import { startDebugger } from './utils/startDebugger.js'; +import { expectInfraRemoved } from './utils/expectInfraRemoved.js'; +import { expectInfraDeployed } from './utils/expectInfraDeployed.js'; +import { removeInfra } from './utils/removeInfra.js'; +import { exec } from 'child_process'; +import { promisify } from 'util'; +import { callLambda } from './utils/callLambda.js'; +import { getSamplePayload } from './utils/getSamplePayload.js'; +import { validateLocalResponse } from './utils/validateLocalResponse.js'; +import { getTestProjectFolder } from './utils/getTestProjectFolder.js'; +import path from 'path'; + +export const execAsync = promisify(exec); + +export const observableMode = process.env.OBSERVABLE_MODE === 'true'; + +describe('cdk-nested', async () => { + const folder = await getTestProjectFolder('cdk-nested'); + + let lldProcess: ChildProcess | undefined; + + beforeAll(async () => { + if (process.env.CI === 'true' || process.env.RUN_TEST_FROM_CLI === 'true') { + lldProcess = await startDebugger(folder, ['-c environment=test']); + } + }); + + afterAll(async () => { + // stop the debugger + lldProcess?.kill(); + }); + + test('check infra', async () => { + const lambdaName = await getFunctionName( + folder, + 'FunctionNameTestTsCommonJs', + ); + await expectInfraDeployed(lambdaName); + }); + + test('call Lambda - testTsCommonJs', async () => { + const lambdaName = await getFunctionName( + folder, + 'FunctionNameTestTsCommonJs', + ); + + const payload = getSamplePayload(lambdaName); + const response = await callLambda(lambdaName, payload); + + expect(response.inputEvent).toEqual(payload); + expect(response.runningLocally).toEqual(!observableMode); + if (observableMode) { + await validateLocalResponse(lambdaName, payload); + } + }); + + test('call Lambda - testTsEsModule', async () => { + const lambdaName = await getFunctionName( + folder, + 'FunctionNameTestTsEsModule', + ); + + const payload = getSamplePayload(lambdaName); + const response = await callLambda(lambdaName, payload); + + expect(response.inputEvent).toEqual(payload); + expect(response.runningLocally).toEqual(!observableMode); + if (observableMode) { + await validateLocalResponse(lambdaName, payload); + } + }); + + test('call Lambda - testJsCommonJs', async () => { + const lambdaName = await getFunctionName( + folder, + 'FunctionNameTestJsCommonJs', + ); + + const payload = getSamplePayload(lambdaName); + const response = await callLambda(lambdaName, payload); + + expect(response.runningLocally).toEqual(!observableMode); + expect(response.inputEvent).toEqual(payload); + if (observableMode) { + await validateLocalResponse(lambdaName, payload); + } + }); + + test('call Lambda - testTsCommonJsNested', async () => { + const lambdaName = await getFunctionName( + folder, + 'FunctionNameTestTsCommonJsNested', + ); + + const payload = getSamplePayload(lambdaName); + const response = await callLambda(lambdaName, payload); + + expect(response.inputEvent).toEqual(payload); + expect(response.runningLocally).toEqual(!observableMode); + if (observableMode) { + await validateLocalResponse(lambdaName, payload); + } + }); + + test('call Lambda - testTsEsModuleNested', async () => { + const lambdaName = await getFunctionName( + folder, + 'FunctionNameTestTsEsModuleNested', + ); + + const payload = getSamplePayload(lambdaName); + const response = await callLambda(lambdaName, payload); + + expect(response.inputEvent).toEqual(payload); + expect(response.runningLocally).toEqual(!observableMode); + if (observableMode) { + await validateLocalResponse(lambdaName, payload); + } + }); + + test('call Lambda - testJsCommonJsNested', async () => { + const lambdaName = await getFunctionName( + folder, + 'FunctionNameTestJsCommonJsNested', + ); + + const payload = getSamplePayload(lambdaName); + const response = await callLambda(lambdaName, payload); + + expect(response.runningLocally).toEqual(!observableMode); + expect(response.inputEvent).toEqual(payload); + if (observableMode) { + await validateLocalResponse(lambdaName, payload); + } + }); + + test('remove infra', async () => { + if (process.env.CI === 'true' || process.env.RUN_TEST_FROM_CLI === 'true') { + await removeInfra(lldProcess, folder, ['-c environment=test']); + const lambdaName = await getFunctionName( + folder, + 'FunctionNameTestTsCommonJs', + ); + await expectInfraRemoved(lambdaName); + } + }); +}); + +async function getFunctionName(folder: string, functionName: string) { + const cdkOutputs = JSON.parse( + await fs.readFile(path.join(folder, 'cdk-outputs.json'), 'utf-8'), + ); + const lambdaName = cdkOutputs['test-lld-cdk-nested'][functionName]; + + if (!lambdaName) { + throw new Error( + `Lambda name not found for ${functionName} in cdk-outputs.json`, + ); + } + + return lambdaName; +} diff --git a/test/cdk-nested/.gitignore b/test/cdk-nested/.gitignore new file mode 100644 index 00000000..7b9f1c6a --- /dev/null +++ b/test/cdk-nested/.gitignore @@ -0,0 +1,12 @@ +!jest.config.js +*.d.ts +node_modules + +# CDK asset staging directory +.cdk.staging +cdk.out + +cdk-outputs.json + +CdkbasicStack.yaml +cdk-synth.log \ No newline at end of file diff --git a/test/cdk-nested/.npmignore b/test/cdk-nested/.npmignore new file mode 100644 index 00000000..c1d6d45d --- /dev/null +++ b/test/cdk-nested/.npmignore @@ -0,0 +1,6 @@ +*.ts +!*.d.ts + +# CDK asset staging directory +.cdk.staging +cdk.out diff --git a/test/cdk-nested/CdkNestedStack.yaml b/test/cdk-nested/CdkNestedStack.yaml new file mode 100644 index 00000000..e69de29b diff --git a/test/cdk-nested/README.md b/test/cdk-nested/README.md new file mode 100644 index 00000000..80c3df35 --- /dev/null +++ b/test/cdk-nested/README.md @@ -0,0 +1,14 @@ +# Welcome to your CDK TypeScript project + +This is a blank project for CDK development with TypeScript. + +The `cdk.json` file tells the CDK Toolkit how to execute your app. + +## Useful commands + +- `npm run build` compile typescript to js +- `npm run watch` watch for changes and compile +- `npm run test` perform the jest unit tests +- `npx cdk deploy` deploy this stack to your default AWS account/region +- `npx cdk diff` compare deployed stack with current state +- `npx cdk synth` emits the synthesized CloudFormation template diff --git a/test/cdk-nested/bin/cdk-nested.ts b/test/cdk-nested/bin/cdk-nested.ts new file mode 100644 index 00000000..a08c6084 --- /dev/null +++ b/test/cdk-nested/bin/cdk-nested.ts @@ -0,0 +1,21 @@ +#!/usr/bin/env node +import 'source-map-support/register'; +import * as cdk from 'aws-cdk-lib'; +import { CdkNestedStack } from '../lib/cdk-nested-stack'; + +if (process.env.CDK_DEFAULT_REGION !== 'eu-west-1') { + // checking if the region is set with Lambda Live Debugger + throw new Error('CDK_DEFAULT_REGION must be set to eu-west-1'); +} + +const app = new cdk.App(); + +const environment = app.node.tryGetContext('environment'); + +if (!environment) { + throw new Error('Environment is not set in the context'); +} + +new CdkNestedStack(app, 'CdkNesetedStack', { + stackName: `${environment}-lld-cdk-nested`, +}); diff --git a/test/cdk-nested/cdk.context.json b/test/cdk-nested/cdk.context.json new file mode 100644 index 00000000..b6cc4212 --- /dev/null +++ b/test/cdk-nested/cdk.context.json @@ -0,0 +1,3 @@ +{ + "contextb": "b" +} diff --git a/test/cdk-nested/cdk.json b/test/cdk-nested/cdk.json new file mode 100644 index 00000000..7ee247b3 --- /dev/null +++ b/test/cdk-nested/cdk.json @@ -0,0 +1,64 @@ +{ + "app": "npx ts-node --prefer-ts-exts bin/cdk-nested.ts", + "watch": { + "include": ["**"], + "exclude": [ + "README.md", + "cdk*.json", + "**/*.d.ts", + "**/*.js", + "tsconfig.json", + "package*.json", + "yarn.lock", + "node_modules", + "test" + ] + }, + "context": { + "contexta": "a", + "@aws-cdk/aws-lambda:recognizeLayerVersion": true, + "@aws-cdk/core:checkSecretUsage": true, + "@aws-cdk/core:target-partitions": ["aws", "aws-cn"], + "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, + "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, + "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, + "@aws-cdk/aws-iam:minimizePolicies": true, + "@aws-cdk/core:validateSnapshotRemovalPolicy": true, + "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, + "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, + "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, + "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, + "@aws-cdk/core:enablePartitionLiterals": true, + "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, + "@aws-cdk/aws-iam:standardizedServicePrincipals": true, + "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, + "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true, + "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true, + "@aws-cdk/aws-route53-patters:useCertificate": true, + "@aws-cdk/customresources:installLatestAwsSdkDefault": false, + "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true, + "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true, + "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true, + "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true, + "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true, + "@aws-cdk/aws-redshift:columnId": true, + "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true, + "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true, + "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true, + "@aws-cdk/aws-kms:aliasNameRef": true, + "@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true, + "@aws-cdk/core:includePrefixInUniqueNameGeneration": true, + "@aws-cdk/aws-efs:denyAnonymousAccess": true, + "@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true, + "@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true, + "@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": true, + "@aws-cdk/aws-rds:auroraClusterChangeScopeOfInstanceParameterGroupWithEachParameters": true, + "@aws-cdk/aws-appsync:useArnForSourceApiAssociationIdentifier": true, + "@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials": true, + "@aws-cdk/aws-codepipeline-actions:useNewDefaultBranchForCodeCommitSource": true, + "@aws-cdk/aws-cloudwatch-actions:changeLambdaPermissionLogicalIdForLambdaAction": true, + "@aws-cdk/aws-codepipeline:crossAccountKeysDefaultValueToFalse": true, + "@aws-cdk/aws-codepipeline:defaultPipelineTypeToV2": true, + "@aws-cdk/aws-kms:reduceCrossAccountRegionPolicyScope": true + } +} diff --git a/test/cdk-nested/deploy-yaml.sh b/test/cdk-nested/deploy-yaml.sh new file mode 100644 index 00000000..abbd1411 --- /dev/null +++ b/test/cdk-nested/deploy-yaml.sh @@ -0,0 +1,26 @@ +# This script is used to deploy the CDK stack using a YAML template. +npx cdk synth -c environment=test CdkNestedStack > CdkbasicStack.yaml + +# Add a dummy resource to the template to force a change in the stack +awk ' + /^Resources:/ && !injected { + print + print " DummyForceResource:" + print " Type: AWS::CloudFormation::WaitConditionHandle" + print " Properties: {}" + injected=1 + next + } + { print } +' CdkNestedStack.yaml > template.patched.yaml + +# Cdk synth sometimes generates a dummy notification line at the top of the file. +# Remove the first part of the template up to and including the Resources section +awk 'f{print} /^Resources:/ {f=1; print}' template.patched.yaml | awk '/\[cdk:skip\]/{exit} {print}' > CdkNestedStack.yaml + +echo "Deploying stack with the following template:" +echo "-------------------------------------------" +cat CdkNestedStack.yaml +echo "-------------------------------------------" + +aws cloudformation deploy --template-file CdkNestedStack.yaml --stack-name test-lld-cdk-nested --capabilities CAPABILITY_NAMED_IAM diff --git a/test/cdk-nested/jest.config.js b/test/cdk-nested/jest.config.js new file mode 100644 index 00000000..44ead854 --- /dev/null +++ b/test/cdk-nested/jest.config.js @@ -0,0 +1,8 @@ +module.exports = { + testEnvironment: 'node', + roots: ['/test'], + testMatch: ['**/*.test.ts'], + transform: { + '^.+\\.tsx?$': 'ts-jest', + }, +}; diff --git a/test/cdk-nested/lib/cdk-nested-stack.ts b/test/cdk-nested/lib/cdk-nested-stack.ts new file mode 100644 index 00000000..40e84281 --- /dev/null +++ b/test/cdk-nested/lib/cdk-nested-stack.ts @@ -0,0 +1,10 @@ +import * as cdk from 'aws-cdk-lib'; +import { Construct } from 'constructs'; +import { LLDNestedStack } from './lld-nested-stack'; + +export class CdkNestedStack extends cdk.Stack { + constructor(scope: Construct, id: string, props?: cdk.StackProps) { + super(scope, id, props); + new LLDNestedStack(this, 'nested-stack'); + } +} diff --git a/test/cdk-nested/lib/lld-nested-nested-stack.ts b/test/cdk-nested/lib/lld-nested-nested-stack.ts new file mode 100644 index 00000000..3adf3a7d --- /dev/null +++ b/test/cdk-nested/lib/lld-nested-nested-stack.ts @@ -0,0 +1,73 @@ +import { CfnOutput, NestedStack, NestedStackProps } from 'aws-cdk-lib'; +import type { Construct } from 'constructs'; +import * as lambda_nodejs from 'aws-cdk-lib/aws-lambda-nodejs'; +import * as lambda from 'aws-cdk-lib/aws-lambda'; +import * as log from 'aws-cdk-lib/aws-logs'; +import * as path from 'path'; + +export class LLDNestedNestedStack extends NestedStack { + constructor(scope: Construct, id: string, props?: NestedStackProps) { + super(scope, id, props); + + const functionTestTsCommonJs = new lambda_nodejs.NodejsFunction( + this, + 'TestTsCommonJs', + { + // a different way to get the path + entry: path.join(__dirname, '../services/testTsCommonJs/lambda.ts'), + handler: 'lambdaHandler', + runtime: lambda.Runtime.NODEJS_22_X, + logRetention: log.RetentionDays.ONE_DAY, + }, + ); + + const functionTestTsEsModule = new lambda_nodejs.NodejsFunction( + this, + 'TestTsEsModule', + { + entry: 'services/testTsEsModule/lambda.ts', + handler: 'lambdaHandler', + runtime: lambda.Runtime.NODEJS_22_X, + bundling: { + format: lambda_nodejs.OutputFormat.ESM, + }, + logRetention: log.RetentionDays.ONE_DAY, + }, + ); + + const functionTestJsCommonJs = new lambda_nodejs.NodejsFunction( + this, + 'TestJsCommonJs', + { + entry: 'services/testJsCommonJs/lambda.js', + handler: 'lambdaHandler', + runtime: lambda.Runtime.NODEJS_22_X, + logRetention: log.RetentionDays.ONE_DAY, + }, + ); + + new CfnOutput( + this.nestedStackParent!.nestedStackParent!, + 'FunctionNameTestTsCommonJsNested', + { + value: functionTestTsCommonJs.functionName, + }, + ); + + new CfnOutput( + this.nestedStackParent!.nestedStackParent!, + 'FunctionNameTestTsEsModuleNested', + { + value: functionTestTsEsModule.functionName, + }, + ); + + new CfnOutput( + this.nestedStackParent!.nestedStackParent!, + 'FunctionNameTestJsCommonJsNested', + { + value: functionTestJsCommonJs.functionName, + }, + ); + } +} diff --git a/test/cdk-nested/lib/lld-nested-stack.ts b/test/cdk-nested/lib/lld-nested-stack.ts new file mode 100644 index 00000000..ed9f0e5b --- /dev/null +++ b/test/cdk-nested/lib/lld-nested-stack.ts @@ -0,0 +1,64 @@ +import { CfnOutput, NestedStack, NestedStackProps } from 'aws-cdk-lib'; +import type { Construct } from 'constructs'; +import * as lambda_nodejs from 'aws-cdk-lib/aws-lambda-nodejs'; +import * as lambda from 'aws-cdk-lib/aws-lambda'; +import * as log from 'aws-cdk-lib/aws-logs'; +import * as path from 'path'; +import { LLDNestedNestedStack } from './lld-nested-nested-stack'; + +export class LLDNestedStack extends NestedStack { + constructor(scope: Construct, id: string, props?: NestedStackProps) { + super(scope, id, props); + + new LLDNestedNestedStack(this, 'nested-nested-stack'); + + const functionTestTsCommonJs = new lambda_nodejs.NodejsFunction( + this, + 'TestTsCommonJs', + { + // a different way to get the path + entry: path.join(__dirname, '../services/testTsCommonJs/lambda.ts'), + handler: 'lambdaHandler', + runtime: lambda.Runtime.NODEJS_22_X, + logRetention: log.RetentionDays.ONE_DAY, + }, + ); + + const functionTestTsEsModule = new lambda_nodejs.NodejsFunction( + this, + 'TestTsEsModule', + { + entry: 'services/testTsEsModule/lambda.ts', + handler: 'lambdaHandler', + runtime: lambda.Runtime.NODEJS_22_X, + bundling: { + format: lambda_nodejs.OutputFormat.ESM, + }, + logRetention: log.RetentionDays.ONE_DAY, + }, + ); + + const functionTestJsCommonJs = new lambda_nodejs.NodejsFunction( + this, + 'TestJsCommonJs', + { + entry: 'services/testJsCommonJs/lambda.js', + handler: 'lambdaHandler', + runtime: lambda.Runtime.NODEJS_22_X, + logRetention: log.RetentionDays.ONE_DAY, + }, + ); + + new CfnOutput(this.nestedStackParent!, 'FunctionNameTestTsCommonJs', { + value: functionTestTsCommonJs.functionName, + }); + + new CfnOutput(this.nestedStackParent!, 'FunctionNameTestTsEsModule', { + value: functionTestTsEsModule.functionName, + }); + + new CfnOutput(this.nestedStackParent!, 'FunctionNameTestJsCommonJs', { + value: functionTestJsCommonJs.functionName, + }); + } +} diff --git a/test/cdk-nested/package.json b/test/cdk-nested/package.json new file mode 100644 index 00000000..36070de8 --- /dev/null +++ b/test/cdk-nested/package.json @@ -0,0 +1,27 @@ +{ + "name": "cdk-nested", + "version": "0.0.1", + "bin": { + "cdk-nested": "bin/cdk-nested.js" + }, + "scripts": { + "deploy": "cdk deploy --all -c environment=test --require-approval never --outputs-file cdk-outputs.json", + "build": "cdk synth -c environment=test", + "deploy-yaml": "bash deploy-yaml.sh", + "destroy": "cdk destroy --all -c environment=test --force" + }, + "devDependencies": { + "@types/node": "24.3.0", + "@tsconfig/node22": "^22.0.2", + "aws-cdk": "2.1027.0", + "ts-node": "^10.9.2", + "typescript": "~5.9.2", + "@types/aws-lambda": "^8.10.152" + }, + "dependencies": { + "aws-cdk-lib": "2.213.0", + "constructs": "^10.4.2", + "source-map-support": "^0.5.21", + "@aws-sdk/client-sts": "^3.879.0" + } +} diff --git a/test/cdk-nested/services/testJsCommonJs/lambda.js b/test/cdk-nested/services/testJsCommonJs/lambda.js new file mode 100755 index 00000000..4e37462d --- /dev/null +++ b/test/cdk-nested/services/testJsCommonJs/lambda.js @@ -0,0 +1,40 @@ +const { STSClient, GetCallerIdentityCommand } = require('@aws-sdk/client-sts'); + +const stsClient = new STSClient({}); + +const lambdaHandler = async (event, context) => { + // Check context + const remainingTime = context.getRemainingTimeInMillis(); + if (remainingTime === undefined) { + throw new Error('Remaining time is undefined'); + } + + // check if SDK works + const command = new GetCallerIdentityCommand({}); + const identity = await stsClient.send(command); + + const response = { + inputEvent: event, + accountId: identity.Account, + runningLocally: process.env.IS_LOCAL === 'true', + }; + + if (process.env.IS_LOCAL === 'true') { + const fs = require('fs'); + const path = require('path'); + const filePath = path.join( + '..', + 'local_lambda_responses', + `${context.functionName}.json`, + ); + + fs.writeFileSync(filePath, JSON.stringify(response, null, 2)); + } + + return response; +}; + +// Export the lambda handler if needed, e.g., for unit testing +module.exports = { + lambdaHandler, +}; diff --git a/test/cdk-nested/services/testJsCommonJs/package.json b/test/cdk-nested/services/testJsCommonJs/package.json new file mode 100644 index 00000000..b601d7c3 --- /dev/null +++ b/test/cdk-nested/services/testJsCommonJs/package.json @@ -0,0 +1,8 @@ +{ + "name": "cdk-basic-test-js-commonjs", + "version": "1.0.0", + "type": "commonjs", + "dependencies": { + "@aws-sdk/client-sts": "^3.879.0" + } +} diff --git a/test/cdk-nested/services/testJsEsModule/lambda.js b/test/cdk-nested/services/testJsEsModule/lambda.js new file mode 100755 index 00000000..e3508208 --- /dev/null +++ b/test/cdk-nested/services/testJsEsModule/lambda.js @@ -0,0 +1,35 @@ +import { STSClient, GetCallerIdentityCommand } from '@aws-sdk/client-sts'; + +const stsClient = new STSClient({}); + +export const lambdaHandler = async (event, context) => { + // check context + const remainingTime = context.getRemainingTimeInMillis(); + if (remainingTime === undefined) { + throw new Error('Remaining time is undefined'); + } + + // check SDK works + const command = new GetCallerIdentityCommand({}); + const identity = await stsClient.send(command); + + const response = { + inputEvent: event, + accountId: identity.Account, + runningLocally: process.env.IS_LOCAL === 'true', + }; + + if (process.env.IS_LOCAL === 'true') { + const fs = await import('fs'); + const path = await import('path'); + const filePath = path.join( + '..', + 'local_lambda_responses', + `${context.functionName}.json`, + ); + + fs.writeFileSync(filePath, JSON.stringify(response, null, 2)); + } + + return response; +}; diff --git a/test/cdk-nested/services/testJsEsModule/package.json b/test/cdk-nested/services/testJsEsModule/package.json new file mode 100644 index 00000000..0332a37a --- /dev/null +++ b/test/cdk-nested/services/testJsEsModule/package.json @@ -0,0 +1,8 @@ +{ + "name": "cdk-basic-test-js-esmodule", + "version": "1.0.0", + "type": "module", + "dependencies": { + "@aws-sdk/client-sts": "^3.879.0" + } +} diff --git a/test/cdk-nested/services/testTsCommonJs/lambda.ts b/test/cdk-nested/services/testTsCommonJs/lambda.ts new file mode 100755 index 00000000..70bd2e6e --- /dev/null +++ b/test/cdk-nested/services/testTsCommonJs/lambda.ts @@ -0,0 +1,36 @@ +import { Handler } from 'aws-lambda'; +import { STSClient, GetCallerIdentityCommand } from '@aws-sdk/client-sts'; + +const stsClient = new STSClient({}); + +export const lambdaHandler: Handler = async (event, context) => { + // check context + const remainingTime = context.getRemainingTimeInMillis(); + if (remainingTime === undefined) { + throw new Error('Remaining time is undefined'); + } + + // check SDK works + const command = new GetCallerIdentityCommand({}); + const identity = await stsClient.send(command); + + const response = { + inputEvent: event, + accountId: identity.Account, + runningLocally: process.env.IS_LOCAL === 'true', + }; + + if (process.env.IS_LOCAL === 'true') { + const fs = await import('fs'); + const path = await import('path'); + const filePath = path.join( + '..', + 'local_lambda_responses', + `${context.functionName}.json`, + ); + + fs.writeFileSync(filePath, JSON.stringify(response, null, 2)); + } + + return response; +}; diff --git a/test/cdk-nested/services/testTsCommonJs/package.json b/test/cdk-nested/services/testTsCommonJs/package.json new file mode 100644 index 00000000..9ccbc18a --- /dev/null +++ b/test/cdk-nested/services/testTsCommonJs/package.json @@ -0,0 +1,11 @@ +{ + "name": "cdk-basic-test-ts-commonjs", + "version": "1.0.0", + "type": "commonjs", + "devDependencies": { + "@tsconfig/node22": "^22.0.2" + }, + "dependencies": { + "@aws-sdk/client-sts": "^3.879.0" + } +} diff --git a/test/cdk-nested/services/testTsCommonJs/tsconfig.json b/test/cdk-nested/services/testTsCommonJs/tsconfig.json new file mode 100755 index 00000000..851b3d3b --- /dev/null +++ b/test/cdk-nested/services/testTsCommonJs/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "@tsconfig/node22/tsconfig.json", + "compilerOptions": { + "moduleResolution": "node", + "module": "CommonJS" + } +} diff --git a/test/cdk-nested/services/testTsEsModule/lambda.ts b/test/cdk-nested/services/testTsEsModule/lambda.ts new file mode 100755 index 00000000..70bd2e6e --- /dev/null +++ b/test/cdk-nested/services/testTsEsModule/lambda.ts @@ -0,0 +1,36 @@ +import { Handler } from 'aws-lambda'; +import { STSClient, GetCallerIdentityCommand } from '@aws-sdk/client-sts'; + +const stsClient = new STSClient({}); + +export const lambdaHandler: Handler = async (event, context) => { + // check context + const remainingTime = context.getRemainingTimeInMillis(); + if (remainingTime === undefined) { + throw new Error('Remaining time is undefined'); + } + + // check SDK works + const command = new GetCallerIdentityCommand({}); + const identity = await stsClient.send(command); + + const response = { + inputEvent: event, + accountId: identity.Account, + runningLocally: process.env.IS_LOCAL === 'true', + }; + + if (process.env.IS_LOCAL === 'true') { + const fs = await import('fs'); + const path = await import('path'); + const filePath = path.join( + '..', + 'local_lambda_responses', + `${context.functionName}.json`, + ); + + fs.writeFileSync(filePath, JSON.stringify(response, null, 2)); + } + + return response; +}; diff --git a/test/cdk-nested/services/testTsEsModule/package.json b/test/cdk-nested/services/testTsEsModule/package.json new file mode 100644 index 00000000..c2bef92a --- /dev/null +++ b/test/cdk-nested/services/testTsEsModule/package.json @@ -0,0 +1,11 @@ +{ + "name": "cdk-basic-test-ts-esmodule", + "version": "1.0.0", + "type": "module", + "devDependencies": { + "@tsconfig/node22": "^22.0.2" + }, + "dependencies": { + "@aws-sdk/client-sts": "^3.879.0" + } +} diff --git a/test/cdk-nested/services/testTsEsModule/tsconfig.json b/test/cdk-nested/services/testTsEsModule/tsconfig.json new file mode 100755 index 00000000..a1902a03 --- /dev/null +++ b/test/cdk-nested/services/testTsEsModule/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "@tsconfig/node22/tsconfig.json", + "compilerOptions": { + "module": "esnext", + "moduleResolution": "node" + } +} diff --git a/test/cdk-nested/tsconfig.json b/test/cdk-nested/tsconfig.json new file mode 100644 index 00000000..f8ec89d3 --- /dev/null +++ b/test/cdk-nested/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": ["es2020", "dom"], + "declaration": true, + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "noImplicitThis": true, + "alwaysStrict": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": false, + "inlineSourceMap": true, + "inlineSources": true, + "experimentalDecorators": true, + "strictPropertyInitialization": false, + "typeRoots": ["../../node_modules/@types"] + }, + "exclude": ["node_modules", "cdk.out"] +} From a5b7b29025c04036e0d2542541031ff6fbd4ff9c Mon Sep 17 00:00:00 2001 From: "Marko (ServerlessLife)" Date: Sun, 5 Oct 2025 15:51:20 +0200 Subject: [PATCH 2/7] chore: npm install --- package-lock.json | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/package-lock.json b/package-lock.json index ea364eb5..e0976105 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,6 +32,7 @@ "src/extension/*", "test", "test/cdk-basic", + "test/cdk-nested", "test/cdk-esm", "test/cdk-config", "test/sls-basic", @@ -29262,6 +29263,10 @@ "resolved": "test/cdk-esm", "link": true }, + "node_modules/cdk-nested": { + "resolved": "test/cdk-nested", + "link": true + }, "node_modules/chai": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/chai/-/chai-5.2.1.tgz", @@ -42105,6 +42110,26 @@ "typescript": "~5.9.2" } }, + "test/cdk-nested": { + "version": "0.0.1", + "dependencies": { + "@aws-sdk/client-sts": "^3.879.0", + "aws-cdk-lib": "2.213.0", + "constructs": "^10.4.2", + "source-map-support": "^0.5.21" + }, + "bin": { + "cdk-nested": "bin/cdk-nested.js" + }, + "devDependencies": { + "@tsconfig/node22": "^22.0.2", + "@types/aws-lambda": "^8.10.152", + "@types/node": "24.3.0", + "aws-cdk": "2.1027.0", + "ts-node": "^10.9.2", + "typescript": "~5.9.2" + } + }, "test/node_modules/@aws-sdk/client-sso": { "version": "3.879.0", "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.879.0.tgz", From 79c592c565795c04ec003927c54152140669d83a Mon Sep 17 00:00:00 2001 From: "Marko (ServerlessLife)" Date: Sun, 5 Oct 2025 17:01:30 +0200 Subject: [PATCH 3/7] chore: Add contributor --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 64593611..ae03fc9e 100644 --- a/README.md +++ b/README.md @@ -353,6 +353,7 @@ If you have a new feature idea, please create and issue. - [Ben Moses](https://github.com/benjymoses) - [Kristian Dreher](https://www.linkedin.com/in/kristiandreher) +- [Hugo Lewenhaupt](https://www.linkedin.com/in/hugo-lewenhaupt-84751289) - [Roger Chi](https://rogerchi.com/) - [Sebastian / avocadomaster](https://github.com/avocadomaster) - [Sebastian Bille](https://blog.sebastianbille.com) From 999bb8b95d50bba426a4e9c6a290a391853aa64e Mon Sep 17 00:00:00 2001 From: "Marko (ServerlessLife)" Date: Sun, 5 Oct 2025 18:52:41 +0200 Subject: [PATCH 4/7] chore: Add cdk-nested to launch.json --- .vscode/launch.json | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/.vscode/launch.json b/.vscode/launch.json index b7f742aa..a4acbe13 100755 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -69,6 +69,26 @@ "type": "node", "cwd": "${workspaceRoot}/test/cdk-esm" }, + { + "name": "LLDebugger - CDK nested", + "program": "${workspaceRoot}/node_modules/tsx/dist/cli.mjs", + "args": ["../../src/lldebugger.ts", "-c environment=test"], + "request": "launch", + "skipFiles": ["/**"], + "console": "integratedTerminal", + "type": "node", + "cwd": "${workspaceRoot}/test/cdk-nested" + }, + { + "name": "LLDebugger - CDK nested - observability", + "program": "${workspaceRoot}/node_modules/tsx/dist/cli.mjs", + "args": ["../../src/lldebugger.ts", "-c environment=test", "-o"], + "request": "launch", + "skipFiles": ["/**"], + "console": "integratedTerminal", + "type": "node", + "cwd": "${workspaceRoot}/test/cdk-nested" + }, { "name": "LLDebugger - SLS basic", "program": "${workspaceRoot}/node_modules/tsx/dist/cli.mjs", From 4715a1d652fbe90e93904f76692d91e58af23325 Mon Sep 17 00:00:00 2001 From: "Marko (ServerlessLife)" Date: Mon, 6 Oct 2025 18:25:57 +0200 Subject: [PATCH 5/7] refactor: getLambdasInStack returns lambdas in nested stack --- src/cloudFormation.ts | 28 ++++- src/frameworks/cdkFramework.ts | 143 +++++++------------------- src/frameworks/cdkFrameworkWorker.mjs | 1 - 3 files changed, 63 insertions(+), 109 deletions(-) diff --git a/src/cloudFormation.ts b/src/cloudFormation.ts index 112c700f..f53c936c 100644 --- a/src/cloudFormation.ts +++ b/src/cloudFormation.ts @@ -139,24 +139,48 @@ async function getLambdasInStack( Array<{ lambdaName: string; logicalId: string; + stackName: string; }> > { const response = await getCloudFormationResources( stackName, awsConfiguration, ); + const lambdaResources = response?.filter( (resource) => resource.ResourceType === 'AWS::Lambda::Function', ); - return ( + const nestedStacks = response?.filter( + (resource) => resource.ResourceType === 'AWS::CloudFormation::Stack', + ); + + const lambdas = lambdaResources?.map((resource) => { return { lambdaName: resource.PhysicalResourceId!, logicalId: resource.LogicalResourceId!, + stackName: stackName, }; - }) ?? [] + }) ?? []; + + const lambdasInNestedStacks = await Promise.all( + (nestedStacks ?? []).map(async (nestedStack) => { + if (!nestedStack.PhysicalResourceId) return []; + + const lambdasInNestedStack = await getLambdasInStack( + nestedStack.PhysicalResourceId, + awsConfiguration, + ); + + return lambdasInNestedStack; + }), ); + + const flattenedLambdasInNestedStacks = lambdasInNestedStacks.flat(); + + const allLambdas = [...lambdas, ...flattenedLambdasInNestedStacks]; + return allLambdas; } /** diff --git a/src/frameworks/cdkFramework.ts b/src/frameworks/cdkFramework.ts index 7ead4ab0..8525b721 100755 --- a/src/frameworks/cdkFramework.ts +++ b/src/frameworks/cdkFramework.ts @@ -71,47 +71,17 @@ export class CdkFramework implements IFramework { ); const cdkTokenRegex = /^\${Token\[TOKEN\.\d+\]}$/; - const stackTokensCdkPathMappings = Object.fromEntries( - lambdasInCdk - .filter((lambda) => cdkTokenRegex.test(lambda.stackName)) - .map((lambda) => [lambda.stackName, lambda.stackCdkPath]), - ); - const realStackNames = [ - ...new Set( - lambdasInCdk.map((lambda) => { - return lambda.rootStackName; - }), - ), - ]; - const unresolvedTokens = new Set(Object.keys(stackTokensCdkPathMappings)); - const resolvedTokenizedStackNames = new Set(); - - if (Object.keys(stackTokensCdkPathMappings).length) { - Logger.verbose( - `[CDK] Found tokenized stackNames: ${[...unresolvedTokens].join(', ')}`, - ); - Logger.verbose( - `[CDK] Will look for tokenized stackNames in ${realStackNames.join( - ', ', - )}`, - ); - for (const realStackName of realStackNames) { - if (!unresolvedTokens.size) break; - await this.resolveTokenizedStackNames( - unresolvedTokens, - resolvedTokenizedStackNames, - stackTokensCdkPathMappings, - awsConfiguration, - realStackName, - ); + const stackNamesDuplicated = lambdasInCdk.map((lambda) => { + if (cdkTokenRegex.test(lambda.stackName)) { + return lambda.rootStackName; + } else { + return lambda.stackName; } - } + }); + + const stackNames = [...new Set(stackNamesDuplicated)]; - //get all stack names - const stackNames = [ - ...new Set(realStackNames.concat([...resolvedTokenizedStackNames])), // unique - ]; Logger.verbose( `[CDK] Found the following stacks in CDK: ${stackNames.join(', ')}`, ); @@ -119,35 +89,48 @@ export class CdkFramework implements IFramework { const lambdasDeployed = ( await Promise.all( stackNames.map(async (stackName) => { - const lambdasInStackPromise = CloudFormation.getLambdasInStack( + const lambdasInStack = await CloudFormation.getLambdasInStack( stackName, awsConfiguration, ); - const lambdasMetadataPromise = - this.getLambdaCdkPathFromTemplateMetadata( - stackName, - awsConfiguration, - ); - const lambdasInStack = await lambdasInStackPromise; + const stackAndNestedStackNames = [ + ...new Set(lambdasInStack.map((l) => l.stackName)), + ]; + + const lambdasMetadata = ( + await Promise.all( + stackAndNestedStackNames.map((stackOrNestedStackName) => + this.getLambdaCdkPathFromTemplateMetadata( + stackOrNestedStackName, + awsConfiguration, + ), + ), + ) + ).flat(); + Logger.verbose( `[CDK] Found Lambda functions in the stack ${stackName}:`, JSON.stringify(lambdasInStack, null, 2), ); - const lambdasMetadata = await lambdasMetadataPromise; + Logger.verbose( `[CDK] Found Lambda functions in the stack ${stackName} in the template metadata:`, JSON.stringify(lambdasMetadata, null, 2), ); - return lambdasInStack.map((lambda) => { + const lambdasPhysicalResourceIds = lambdasInStack.map((lambda) => { return { lambdaName: lambda.lambdaName, cdkPath: lambdasMetadata.find( - (lm) => lm.logicalId === lambda.logicalId, + (lm) => + lm.logicalId === lambda.logicalId && + lm.stackName === lambda.stackName, )?.cdkPath, }; }); + + return lambdasPhysicalResourceIds; }), ) ).flat(); @@ -204,61 +187,6 @@ export class CdkFramework implements IFramework { return lambdasDiscovered; } - protected async resolveTokenizedStackNames( - unresolvedTokens: Set, - resolvedTokenizedStackNames: Set, - stackTokensCdkPathMappings: Record, - awsConfiguration: AwsConfiguration, - stackName: string, - ): Promise { - if (!unresolvedTokens.size) { - return; - } - - const cfTemplate = await CloudFormation.getCloudFormationStackTemplate( - stackName, - awsConfiguration, - ); - - if (cfTemplate) { - const nestedStacks = Object.entries(cfTemplate.Resources) - .filter( - ([, resource]: [string, any]) => - resource.Type === 'AWS::CloudFormation::Stack', - ) - .map(([key, resource]: [string, any]) => { - return { - logicalId: key, - cdkPath: resource.Metadata['aws:cdk:path'], - }; - }); - - for (const nestedStack of nestedStacks) { - const mapping = Object.entries(stackTokensCdkPathMappings).find( - (f) => f[1] === nestedStack.cdkPath, - ); - - if (mapping) { - unresolvedTokens.delete(mapping[0]); - const physicalResourceId = - await CloudFormation.getStackResourcePhysicalResourceId( - stackName, - nestedStack.logicalId, - awsConfiguration, - ); - resolvedTokenizedStackNames.add(physicalResourceId); - await this.resolveTokenizedStackNames( - unresolvedTokens, - resolvedTokenizedStackNames, - stackTokensCdkPathMappings, - awsConfiguration, - physicalResourceId, - ); - } - } - } - } - /** * Getz Lambda functions from the CloudFormation template metadata * @param stackName @@ -272,6 +200,7 @@ export class CdkFramework implements IFramework { Array<{ logicalId: string; cdkPath: string; + stackName: string; }> > { const cfTemplate = await CloudFormation.getCloudFormationStackTemplate( @@ -289,8 +218,10 @@ export class CdkFramework implements IFramework { return { logicalId: key, cdkPath: resource.Metadata['aws:cdk:path'], + stackName: stackName, }; }); + return lambdas; } @@ -399,12 +330,13 @@ export class CdkFramework implements IFramework { let rootStack = this.stack; while (rootStack.nestedStackParent) { - rootStack = rootStack.nestedStackParent; + rootStack = rootStack.nestedStackParent; } const lambdaInfo = { //cdkPath: this.node.defaultChild?.node.path ?? this.node.path, - stackCdkPath: this.stack.node.defaultChild?.node.path ?? this.stack.node.path, stackName: this.stack.stackName, + stack: this.stack, + rootStack: rootStack, rootStackName: rootStack.stackName, codePath: props.entry, code: props.code, @@ -415,7 +347,6 @@ export class CdkFramework implements IFramework { // console.log("CDK INFRA: ", { // stackName: lambdaInfo.stackName, - // stackCdkPath: lambdaInfo.stackCdkPath, // rootStackName: lambdaInfo.rootStackName, // codePath: lambdaInfo.codePath, // code: lambdaInfo.code, diff --git a/src/frameworks/cdkFrameworkWorker.mjs b/src/frameworks/cdkFrameworkWorker.mjs index 939e9a33..1722adb6 100755 --- a/src/frameworks/cdkFrameworkWorker.mjs +++ b/src/frameworks/cdkFrameworkWorker.mjs @@ -25,7 +25,6 @@ parentPort.on('message', async (data) => { handler: lambda.handler, stackName: lambda.stackName, codePath: lambda.codePath, - stackCdkPath: lambda.stackCdkPath, rootStackName: lambda.rootStackName, code: { path: lambda.code?.path, From 835b12bf29c0cba5c5c3aa76b972346408b7e179 Mon Sep 17 00:00:00 2001 From: "Marko (ServerlessLife)" Date: Mon, 6 Oct 2025 18:28:49 +0200 Subject: [PATCH 6/7] chore: cleaning --- src/cloudFormation.ts | 47 ------------------------------------------- 1 file changed, 47 deletions(-) diff --git a/src/cloudFormation.ts b/src/cloudFormation.ts index f53c936c..04242f37 100644 --- a/src/cloudFormation.ts +++ b/src/cloudFormation.ts @@ -183,54 +183,7 @@ async function getLambdasInStack( return allLambdas; } -/** - * Get CloudFormation stack template - * @param stackName - * @param awsConfiguration - * @returns - */ -async function getStackResourcePhysicalResourceId( - stackName: string, - logicalResourceId: string, - awsConfiguration: AwsConfiguration, -) { - const { DescribeStackResourceCommand } = await import( - '@aws-sdk/client-cloudformation' - ); - const command = new DescribeStackResourceCommand({ - StackName: stackName, - LogicalResourceId: logicalResourceId, - }); - const cloudFormationClient = await getCloudFormationClient(awsConfiguration); - - try { - const response = await cloudFormationClient.send(command); - - if (!response.StackResourceDetail) { - throw new Error( - `No resource details found in stack ${stackName} for ${logicalResourceId}`, - ); - } - - const physicalResourceId = response.StackResourceDetail.PhysicalResourceId; - if (!physicalResourceId) - throw new Error( - `No physicalResourceId found in stack ${stackName} for ${logicalResourceId}`, - ); - return physicalResourceId; - } catch (error: any) { - if (error.name === 'ValidationError') { - Logger.error( - `Stack ${stackName} and/or ${logicalResourceId} not found. Try specifying a region. Error: ${error.message}`, - error, - ); - } - throw error; - } -} - export const CloudFormation = { getCloudFormationStackTemplate, getLambdasInStack, - getStackResourcePhysicalResourceId: getStackResourcePhysicalResourceId, }; From ef679daf5e83c27ffd1837fdd1ec01f1aadc8c49 Mon Sep 17 00:00:00 2001 From: "Marko (ServerlessLife)" Date: Mon, 6 Oct 2025 18:30:49 +0200 Subject: [PATCH 7/7] chore: cleaning --- src/frameworks/cdkFramework.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/frameworks/cdkFramework.ts b/src/frameworks/cdkFramework.ts index 8525b721..abf3079a 100755 --- a/src/frameworks/cdkFramework.ts +++ b/src/frameworks/cdkFramework.ts @@ -335,8 +335,6 @@ export class CdkFramework implements IFramework { const lambdaInfo = { //cdkPath: this.node.defaultChild?.node.path ?? this.node.path, stackName: this.stack.stackName, - stack: this.stack, - rootStack: rootStack, rootStackName: rootStack.stackName, codePath: props.entry, code: props.code, @@ -518,7 +516,6 @@ export class CdkFramework implements IFramework { return { cdkPath: lambda.cdkPath, stackName: lambda.stackName, - stackCdkPath: lambda.stackCdkPath, rootStackName: lambda.rootStackName, packageJsonPath, codePath, @@ -602,7 +599,6 @@ export class CdkFramework implements IFramework { return lambdas as { cdkPath: string; stackName: string; - stackCdkPath: string; rootStackName: string; codePath?: string; code: {