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", 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) diff --git a/package-lock.json b/package-lock.json index 66943200..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", @@ -31878,6 +31883,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 +32217,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 +33204,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 +40880,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" } @@ -42101,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", 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..04242f37 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; } export const CloudFormation = { diff --git a/src/frameworks/cdkFramework.ts b/src/frameworks/cdkFramework.ts index fffa98b2..abf3079a 100755 --- a/src/frameworks/cdkFramework.ts +++ b/src/frameworks/cdkFramework.ts @@ -70,14 +70,18 @@ export class CdkFramework implements IFramework { JSON.stringify(lambdasInCdk, null, 2), ); - //get all stack names - const stackNames = [ - ...new Set( // unique - lambdasInCdk.map((lambda) => { - return lambda.stackName; - }), - ), - ]; + const cdkTokenRegex = /^\${Token\[TOKEN\.\d+\]}$/; + + const stackNamesDuplicated = lambdasInCdk.map((lambda) => { + if (cdkTokenRegex.test(lambda.stackName)) { + return lambda.rootStackName; + } else { + return lambda.stackName; + } + }); + + const stackNames = [...new Set(stackNamesDuplicated)]; + Logger.verbose( `[CDK] Found the following stacks in CDK: ${stackNames.join(', ')}`, ); @@ -85,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(); @@ -183,6 +200,7 @@ export class CdkFramework implements IFramework { Array<{ logicalId: string; cdkPath: string; + stackName: string; }> > { const cfTemplate = await CloudFormation.getCloudFormationStackTemplate( @@ -200,8 +218,10 @@ export class CdkFramework implements IFramework { return { logicalId: key, cdkPath: resource.Metadata['aws:cdk:path'], + stackName: stackName, }; }); + return lambdas; } @@ -308,9 +328,14 @@ 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, stackName: this.stack.stackName, + rootStackName: rootStack.stackName, codePath: props.entry, code: props.code, node: this.node, @@ -320,6 +345,7 @@ export class CdkFramework implements IFramework { // console.log("CDK INFRA: ", { // stackName: lambdaInfo.stackName, + // rootStackName: lambdaInfo.rootStackName, // codePath: lambdaInfo.codePath, // code: lambdaInfo.code, // handler: lambdaInfo.handler, @@ -490,6 +516,7 @@ export class CdkFramework implements IFramework { return { cdkPath: lambda.cdkPath, stackName: lambda.stackName, + rootStackName: lambda.rootStackName, packageJsonPath, codePath, handler, @@ -572,6 +599,7 @@ export class CdkFramework implements IFramework { return lambdas as { cdkPath: string; stackName: string; + rootStackName: string; codePath?: string; code: { path?: string; diff --git a/src/frameworks/cdkFrameworkWorker.mjs b/src/frameworks/cdkFrameworkWorker.mjs index 7f779045..1722adb6 100755 --- a/src/frameworks/cdkFrameworkWorker.mjs +++ b/src/frameworks/cdkFrameworkWorker.mjs @@ -25,6 +25,7 @@ parentPort.on('message', async (data) => { handler: lambda.handler, stackName: lambda.stackName, codePath: lambda.codePath, + 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"] +}