From 52ea5d9270e87247ec557da2acf0075f241467b1 Mon Sep 17 00:00:00 2001 From: soflass1293 Date: Wed, 24 Jan 2024 17:38:40 +0100 Subject: [PATCH] feat: Add extractor (#3) * Added CloudFormation template extratctor * Added `yaml-cfn`package * Added `app.ts` * Added CloudFormation extractor tests * Added filter tests * fix: Added fixed * Linted files * update implementation * fix typo --- .projen/deps.json | 4 + .projen/tasks.json | 4 +- .projenrc.ts | 5 +- a.json | 114 +++++ app.ts | 82 ++++ b.json | 72 +++ mock/template1.json | 114 +++++ mock/template2.json | 108 +++++ mock/template3.json | 84 ++++ mock/template4.json | 68 +++ mock/template5.json | 74 +++ mock/template6.json | 86 ++++ mock/template7.json | 509 ++++++++++++++++++++ package.json | 3 +- src/helpers/cf-extractor.ts | 30 ++ src/helpers/filter.ts | 59 +++ test/helpers/cf-extractor.test.ts | 107 +++++ test/helpers/filter.test.ts | 746 ++++++++++++++++++++++++++++++ test/yaml-cfn.test.ts | 20 + yarn.lock | 9 +- 20 files changed, 2293 insertions(+), 5 deletions(-) create mode 100644 a.json create mode 100644 app.ts create mode 100644 b.json create mode 100644 mock/template1.json create mode 100644 mock/template2.json create mode 100644 mock/template3.json create mode 100644 mock/template4.json create mode 100644 mock/template5.json create mode 100644 mock/template6.json create mode 100644 mock/template7.json create mode 100644 src/helpers/cf-extractor.ts create mode 100644 src/helpers/filter.ts create mode 100644 test/helpers/cf-extractor.test.ts create mode 100644 test/helpers/filter.test.ts create mode 100644 test/yaml-cfn.test.ts diff --git a/.projen/deps.json b/.projen/deps.json index 3f6a045..633f562 100644 --- a/.projen/deps.json +++ b/.projen/deps.json @@ -82,6 +82,10 @@ { "name": "cloudform-types", "type": "runtime" + }, + { + "name": "yaml-cfn", + "type": "runtime" } ], "//": "~~ Generated by projen. To modify, edit .projenrc.ts and run \"npx projen\"." diff --git a/.projen/tasks.json b/.projen/tasks.json index 6ac55a2..8ae3976 100644 --- a/.projen/tasks.json +++ b/.projen/tasks.json @@ -225,13 +225,13 @@ }, "steps": [ { - "exec": "npx npm-check-updates@16 --upgrade --target=minor --peer --dep=dev,peer,prod,optional --filter=@types/jest,@types/node,@typescript-eslint/eslint-plugin,@typescript-eslint/parser,constructs,eslint-config-prettier,eslint-import-resolver-typescript,eslint-plugin-import,eslint-plugin-prettier,eslint,jest,jest-junit,prettier,projen,standard-version,ts-jest,ts-node,typescript,cloudform-types" + "exec": "npx npm-check-updates@16 --upgrade --target=minor --peer --dep=dev,peer,prod,optional --filter=@types/jest,@types/node,@typescript-eslint/eslint-plugin,@typescript-eslint/parser,constructs,eslint-config-prettier,eslint-import-resolver-typescript,eslint-plugin-import,eslint-plugin-prettier,eslint,jest,jest-junit,prettier,projen,standard-version,ts-jest,ts-node,typescript,cloudform-types,yaml-cfn" }, { "exec": "yarn install --check-files" }, { - "exec": "yarn upgrade @types/jest @types/node @typescript-eslint/eslint-plugin @typescript-eslint/parser constructs eslint-config-prettier eslint-import-resolver-typescript eslint-plugin-import eslint-plugin-prettier eslint jest jest-junit prettier projen standard-version ts-jest ts-node typescript cloudform-types" + "exec": "yarn upgrade @types/jest @types/node @typescript-eslint/eslint-plugin @typescript-eslint/parser constructs eslint-config-prettier eslint-import-resolver-typescript eslint-plugin-import eslint-plugin-prettier eslint jest jest-junit prettier projen standard-version ts-jest ts-node typescript cloudform-types yaml-cfn" }, { "exec": "npx projen" diff --git a/.projenrc.ts b/.projenrc.ts index fa85e89..98fb0f6 100644 --- a/.projenrc.ts +++ b/.projenrc.ts @@ -4,7 +4,10 @@ const project = new typescript.TypeScriptProject({ name: "sqs-lambda-assistant", projenrcTs: true, - deps: ["cloudform-types"] /* Runtime dependencies of this module. */, + deps: [ + "cloudform-types", + "yaml-cfn", + ] /* Runtime dependencies of this module. */, // description: undefined, /* The description is just a string that helps people understand the purpose of the package. */ // devDeps: [], /* Build dependencies for this module. */ // packageName: undefined, /* The "name" in package.json. */ diff --git a/a.json b/a.json new file mode 100644 index 0000000..bd7234d --- /dev/null +++ b/a.json @@ -0,0 +1,114 @@ + +############### - 7 - ################ + + --- SQSs of 7 --- +[ + { + "LogicalId": "jobsDlqD18CF374", + "Properties": { + "MessageRetentionPeriod": 1209600, + "QueueName": "aws-node-sqs-worker-project-dev-jobs-dlq" + } + }, + { + "LogicalId": "jobsQueueCEDBAE3E", + "Properties": { + "DelaySeconds": 60, + "QueueName": "aws-node-sqs-worker-project-dev-jobs", + "RedrivePolicy": { + "deadLetterTargetArn": { + "Fn::GetAtt": [ + "jobsDlqD18CF374", + "Arn" + ] + }, + "maxReceiveCount": 3 + }, + "VisibilityTimeout": 69 + } + } +] + --- Lambdas of 7 --- +[ + { + "LogicalId": "ProducerLambdaFunction", + "Properties": { + "Code": { + "S3Bucket": { + "Ref": "ServerlessDeploymentBucket" + }, + "S3Key": "serverless/aws-node-sqs-worker-project/dev/1705833879496-2024-01-21T10:44:39.496Z/aws-node-sqs-worker-project.zip" + }, + "Handler": "index.producer", + "Runtime": "nodejs18.x", + "FunctionName": "aws-node-sqs-worker-project-dev-producer", + "MemorySize": 1024, + "Timeout": 6, + "Environment": { + "Variables": { + "QUEUE_URL": { + "Ref": "jobsQueueCEDBAE3E" + } + } + }, + "Role": { + "Fn::GetAtt": [ + "IamRoleLambdaExecution", + "Arn" + ] + } + } + }, + { + "LogicalId": "JobsWorkerLambdaFunction", + "Properties": { + "Code": { + "S3Bucket": { + "Ref": "ServerlessDeploymentBucket" + }, + "S3Key": "serverless/aws-node-sqs-worker-project/dev/1705833879496-2024-01-21T10:44:39.496Z/aws-node-sqs-worker-project.zip" + }, + "Handler": "index.consumer", + "Runtime": "nodejs18.x", + "FunctionName": "aws-node-sqs-worker-project-dev-jobsWorker", + "MemorySize": 1024, + "Timeout": 6, + "Role": { + "Fn::GetAtt": [ + "IamRoleLambdaExecution", + "Arn" + ] + }, + "ReservedConcurrentExecutions": 200 + } + } +] + --- EventSourceMappings of 7 --- +[ + { + "LogicalId": "JobsWorkerEventSourceMappingSQSJobsQueueCEDBAE3E", + "Properties": { + "BatchSize": 5, + "MaximumBatchingWindowInSeconds": 33, + "EventSourceArn": { + "Fn::GetAtt": [ + "jobsQueueCEDBAE3E", + "Arn" + ] + }, + "FunctionName": { + "Fn::GetAtt": [ + "JobsWorkerLambdaFunction", + "Arn" + ] + }, + "Enabled": true, + "FunctionResponseTypes": [ + "ReportBatchItemFailures" + ], + "ScalingConfig": { + "MaximumConcurrency": 1000 + } + } + } +] \ No newline at end of file diff --git a/app.ts b/app.ts new file mode 100644 index 0000000..cf97f1b --- /dev/null +++ b/app.ts @@ -0,0 +1,82 @@ +import { Lambda, SQS } from "cloudform-types"; +import { extract } from "./src/helpers/cf-extractor"; +import { filter } from "./src/helpers/filter"; +import { Worker } from "./src/worker"; + +const fs = require("fs"); + +const run = () => { + const workers: Worker[] = []; + for (let index = 1; index <= 7; index++) { + const path = `./mock/template${index}.json`; + const template = JSON.parse(fs.readFileSync(path)); + const filtered = filter(template); + filtered.forEach( + ( + options: Record< + string, + | Partial + | Partial + | Partial + >, + ) => { + const worker = new Worker({ + id: `${index}_worker-${Date.now()}`, + // @ts-ignore + lambda: options.lambda, + // @ts-ignore + integration: options.integration, + // @ts-ignore + sqs: options.sqs, + }); + workers.push(worker); + }, + ); + } + workers.forEach((worker: Worker) => { + worker.analyze(); + }); +}; + +const run1 = () => { + fs.writeFileSync("b.json", ""); + for (let index = 7; index <= 7; index++) { + const path = `./mock/template${index}.json`; + const template = JSON.parse(fs.readFileSync(path)); + const options = filter(template); + if (options.length > 0) { + fs.appendFileSync("b.json", `\n --- Options of ${index} --- \n`); + fs.appendFileSync("b.json", JSON.stringify(options, null, 2)); + } + } +}; +const run2 = () => { + fs.writeFileSync("a.json", ""); + for (let index = 7; index <= 7; index++) { + const path = `./mock/template${index}.json`; + const template = JSON.parse(fs.readFileSync(path)); + const sqs = extract(template, "AWS::SQS::Queue"); + const lambdas = extract(template, "AWS::Lambda::Function"); + const mappings = extract(template, "AWS::Lambda::EventSourceMapping"); + if (sqs.length > 0 && lambdas.length > 0 && mappings.length > 0) { + fs.appendFileSync( + "a.json", + `\n############### - ${index} - ################\n`, + ); + fs.appendFileSync("a.json", `\n --- SQSs of ${index} --- \n`); + fs.appendFileSync("a.json", JSON.stringify(sqs, null, 2)); + fs.appendFileSync("a.json", `\n --- Lambdas of ${index} --- \n`); + fs.appendFileSync("a.json", JSON.stringify(lambdas, null, 2)); + fs.appendFileSync( + "a.json", + `\n --- EventSourceMappings of ${index} --- \n`, + ); + fs.appendFileSync("a.json", JSON.stringify(mappings, null, 2)); + } + } + console.log("Done"); +}; + +run(); +run1(); +run2(); diff --git a/b.json b/b.json new file mode 100644 index 0000000..e913448 --- /dev/null +++ b/b.json @@ -0,0 +1,72 @@ + + --- Options of 7 --- +[ + { + "integration": { + "LogicalId": "JobsWorkerEventSourceMappingSQSJobsQueueCEDBAE3E", + "Properties": { + "BatchSize": 5, + "MaximumBatchingWindowInSeconds": 33, + "EventSourceArn": { + "Fn::GetAtt": [ + "jobsQueueCEDBAE3E", + "Arn" + ] + }, + "FunctionName": { + "Fn::GetAtt": [ + "JobsWorkerLambdaFunction", + "Arn" + ] + }, + "Enabled": true, + "FunctionResponseTypes": [ + "ReportBatchItemFailures" + ], + "ScalingConfig": { + "MaximumConcurrency": 1000 + } + } + }, + "lambda": { + "LogicalId": "JobsWorkerLambdaFunction", + "Properties": { + "Code": { + "S3Bucket": { + "Ref": "ServerlessDeploymentBucket" + }, + "S3Key": "serverless/aws-node-sqs-worker-project/dev/1705833879496-2024-01-21T10:44:39.496Z/aws-node-sqs-worker-project.zip" + }, + "Handler": "index.consumer", + "Runtime": "nodejs18.x", + "FunctionName": "aws-node-sqs-worker-project-dev-jobsWorker", + "MemorySize": 1024, + "Timeout": 6, + "Role": { + "Fn::GetAtt": [ + "IamRoleLambdaExecution", + "Arn" + ] + }, + "ReservedConcurrentExecutions": 200 + } + }, + "sqs": { + "LogicalId": "jobsQueueCEDBAE3E", + "Properties": { + "DelaySeconds": 60, + "QueueName": "aws-node-sqs-worker-project-dev-jobs", + "RedrivePolicy": { + "deadLetterTargetArn": { + "Fn::GetAtt": [ + "jobsDlqD18CF374", + "Arn" + ] + }, + "maxReceiveCount": 3 + }, + "VisibilityTimeout": 69 + } + } + } +] \ No newline at end of file diff --git a/mock/template1.json b/mock/template1.json new file mode 100644 index 0000000..cb9b08c --- /dev/null +++ b/mock/template1.json @@ -0,0 +1,114 @@ +{ + "Resources": { + "SQSQueue1": { + "Type": "AWS::SQS::Queue" + }, + "SQSQueue2": { + "Type": "AWS::SQS::Queue" + }, + "LambdaFunction1": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Handler": "index.handler", + "Role": "arn:aws:iam::123456789012:role/lambda-role", + "FunctionName": "LambdaFunction1", + "Runtime": "nodejs14.x", + "Code": { + "S3Bucket": "lambda-code-bucket", + "S3Key": "lambda-code1.zip" + }, + "Environment": { + "Variables": { + "QUEUE_URL": { "Ref": "SQSQueue1" } + } + } + } + }, + "LambdaFunction2": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Handler": "index.handler", + "Role": "arn:aws:iam::123456789012:role/lambda-role", + "FunctionName": "LambdaFunction2", + "Runtime": "nodejs14.x", + "Code": { + "S3Bucket": "lambda-code-bucket", + "S3Key": "lambda-code2.zip" + }, + "Environment": { + "Variables": { + "QUEUE_URL": { "Ref": "SQSQueue2" } + } + } + } + }, + "EventSourceMapping1": { + "Type": "AWS::Lambda::EventSourceMapping", + "Properties": { + "BatchSize": 5, + "EventSourceArn": { "Fn::GetAtt": ["SQSQueue1", "Arn"] }, + "FunctionName": { "Ref": "LambdaFunction1" } + } + }, + "EventSourceMapping2": { + "Type": "AWS::Lambda::EventSourceMapping", + "Properties": { + "BatchSize": 5, + "EventSourceArn": { "Fn::GetAtt": ["SQSQueue2", "Arn"] }, + "FunctionName": { "Ref": "LambdaFunction2" } + } + }, + "S3Bucket": { + "Type": "AWS::S3::Bucket" + }, + "LambdaFunctionS3": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Handler": "index.handler", + "Role": "arn:aws:iam::123456789012:role/lambda-role", + "FunctionName": "LambdaFunctionS3", + "Runtime": "nodejs14.x", + "Code": { + "S3Bucket": "lambda-code-bucket", + "S3Key": "lambda-code-s3.zip" + }, + "Environment": { + "Variables": { + "BUCKET_NAME": { "Ref": "S3Bucket" } + } + } + } + }, + "DynamoDBTable": { + "Type": "AWS::DynamoDB::Table", + "Properties": { + "AttributeDefinitions": [ + { "AttributeName": "UserId", "AttributeType": "N" } + ], + "KeySchema": [{ "AttributeName": "UserId", "KeyType": "HASH" }], + "ProvisionedThroughput": { + "ReadCapacityUnits": 5, + "WriteCapacityUnits": 5 + } + } + }, + "LambdaFunctionDynamoDB": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Handler": "index.handler", + "Role": "arn:aws:iam::123456789012:role/lambda-role", + "FunctionName": "LambdaFunctionDynamoDB", + "Runtime": "nodejs14.x", + "Code": { + "S3Bucket": "lambda-code-bucket", + "S3Key": "lambda-code-dynamodb.zip" + }, + "Environment": { + "Variables": { + "TABLE_NAME": { "Ref": "DynamoDBTable" } + } + } + } + } + } +} diff --git a/mock/template2.json b/mock/template2.json new file mode 100644 index 0000000..3207b7a --- /dev/null +++ b/mock/template2.json @@ -0,0 +1,108 @@ +{ + "Resources": { + "SQSQueue1": { + "Type": "AWS::SQS::Queue" + }, + "SQSQueue2": { + "Type": "AWS::SQS::Queue" + }, + "LambdaFunction1": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Handler": "index.handler", + "Role": "arn:aws:iam::123456789012:role/lambda-role", + "FunctionName": "LambdaFunction1", + "Runtime": "nodejs14.x", + "Code": { + "S3Bucket": "lambda-code-bucket", + "S3Key": "lambda-code1.zip" + }, + "Environment": { + "Variables": { + "QUEUE_URL": { "Ref": "SQSQueue1" } + } + } + } + }, + "LambdaFunction2": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Handler": "index.handler", + "Role": "arn:aws:iam::123456789012:role/lambda-role", + "FunctionName": "LambdaFunction2", + "Runtime": "nodejs14.x", + "Code": { + "S3Bucket": "lambda-code-bucket", + "S3Key": "lambda-code2.zip" + }, + "Environment": { + "Variables": { + "QUEUE_URL": { "Ref": "SQSQueue2" } + } + } + } + }, + "EventSourceMapping1": { + "Type": "AWS::Lambda::EventSourceMapping", + "Properties": { + "BatchSize": 5, + "EventSourceArn": { "Fn::GetAtt": ["SQSQueue1", "Arn"] }, + "FunctionName": { "Ref": "LambdaFunction1" } + } + }, + "EventSourceMapping2": { + "Type": "AWS::Lambda::EventSourceMapping", + "Properties": { + "BatchSize": 5, + "EventSourceArn": { "Fn::GetAtt": ["SQSQueue2", "Arn"] }, + "FunctionName": { "Ref": "LambdaFunction2" } + } + }, + "ApiGateway": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "Name": "MyApi" + } + }, + "ApiResource": { + "Type": "AWS::ApiGateway::Resource", + "Properties": { + "RestApiId": { "Ref": "ApiGateway" }, + "ParentId": { "Fn::GetAtt": ["ApiGateway", "RootResourceId"] }, + "PathPart": "myresource" + } + }, + "ApiMethod": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "RestApiId": { "Ref": "ApiGateway" }, + "ResourceId": { "Ref": "ApiResource" }, + "HttpMethod": "POST", + "AuthorizationType": "NONE", + "Integration": { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": { + "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${LambdaFunction1.Arn}/invocations" + } + } + } + }, + "ApiMethod2": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "RestApiId": { "Ref": "ApiGateway" }, + "ResourceId": { "Ref": "ApiResource" }, + "HttpMethod": "GET", + "AuthorizationType": "NONE", + "Integration": { + "IntegrationHttpMethod": "GET", + "Type": "AWS_PROXY", + "Uri": { + "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${LambdaFunction2.Arn}/invocations" + } + } + } + } + } +} diff --git a/mock/template3.json b/mock/template3.json new file mode 100644 index 0000000..951fbe5 --- /dev/null +++ b/mock/template3.json @@ -0,0 +1,84 @@ +{ + "Resources": { + "SQSQueue1": { + "Type": "AWS::SQS::Queue" + }, + "SQSQueue2": { + "Type": "AWS::SQS::Queue" + }, + "LambdaFunction1": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Handler": "index.handler", + "Role": "arn:aws:iam::123456789012:role/lambda-role", + "FunctionName": "LambdaFunction1", + "Runtime": "nodejs14.x", + "Code": { + "S3Bucket": "lambda-code-bucket", + "S3Key": "lambda-code1.zip" + }, + "Environment": { + "Variables": { + "QUEUE_URL": { "Ref": "SQSQueue1" } + } + } + } + }, + "LambdaFunction2": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Handler": "index.handler", + "Role": "arn:aws:iam::123456789012:role/lambda-role", + "FunctionName": "LambdaFunction2", + "Runtime": "nodejs14.x", + "Code": { + "S3Bucket": "lambda-code-bucket", + "S3Key": "lambda-code2.zip" + }, + "Environment": { + "Variables": { + "QUEUE_URL": { "Ref": "SQSQueue2" } + } + } + } + }, + "EventSourceMapping1": { + "Type": "AWS::Lambda::EventSourceMapping", + "Properties": { + "BatchSize": 5, + "EventSourceArn": { "Fn::GetAtt": ["SQSQueue1", "Arn"] }, + "FunctionName": { "Ref": "LambdaFunction1" } + } + }, + "EventSourceMapping2": { + "Type": "AWS::Lambda::EventSourceMapping", + "Properties": { + "BatchSize": 5, + "EventSourceArn": { "Fn::GetAtt": ["SQSQueue2", "Arn"] }, + "FunctionName": { "Ref": "LambdaFunction2" } + } + }, + "StepFunction": { + "Type": "AWS::StepFunctions::StateMachine", + "Properties": { + "Definition": { + "Comment": "A simple AWS Step Functions state machine", + "StartAt": "Lambda1", + "States": { + "Lambda1": { + "Type": "Task", + "Resource": { "Fn::GetAtt": ["LambdaFunction1", "Arn"] }, + "End": true + }, + "Lambda2": { + "Type": "Task", + "Resource": { "Fn::GetAtt": ["LambdaFunction2", "Arn"] }, + "End": true + } + } + }, + "RoleArn": "arn:aws:iam::123456789012:role/step-functions-role" + } + } + } +} diff --git a/mock/template4.json b/mock/template4.json new file mode 100644 index 0000000..8ed70f5 --- /dev/null +++ b/mock/template4.json @@ -0,0 +1,68 @@ +{ + "Resources": { + "SQSQueue1": { + "Type": "AWS::SQS::Queue" + }, + "SQSQueue2": { + "Type": "AWS::SQS::Queue" + }, + "LambdaFunction1": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Handler": "index.handler", + "Role": "arn:aws:iam::123456789012:role/lambda-role", + "FunctionName": "LambdaFunction1", + "Runtime": "nodejs14.x", + "Code": { + "S3Bucket": "lambda-code-bucket", + "S3Key": "lambda-code1.zip" + }, + "Environment": { + "Variables": { + "QUEUE_URL": { "Ref": "SQSQueue1" } + } + } + } + }, + "LambdaFunction2": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Handler": "index.handler", + "Role": "arn:aws:iam::123456789012:role/lambda-role", + "FunctionName": "LambdaFunction2", + "Runtime": "nodejs14.x", + "Code": { + "S3Bucket": "lambda-code-bucket", + "S3Key": "lambda-code2.zip" + }, + "Environment": { + "Variables": { + "QUEUE_URL": { "Ref": "SQSQueue2" } + } + } + } + }, + "SNSTopic1": { + "Type": "AWS::SNS::Topic" + }, + "SNSTopic2": { + "Type": "AWS::SNS::Topic" + }, + "LambdaSubscription1": { + "Type": "AWS::SNS::Subscription", + "Properties": { + "Protocol": "lambda", + "TopicArn": { "Ref": "SNSTopic1" }, + "Endpoint": { "Fn::GetAtt": ["LambdaFunction1", "Arn"] } + } + }, + "LambdaSubscription2": { + "Type": "AWS::SNS::Subscription", + "Properties": { + "Protocol": "lambda", + "TopicArn": { "Ref": "SNSTopic2" }, + "Endpoint": { "Fn::GetAtt": ["LambdaFunction2", "Arn"] } + } + } + } +} diff --git a/mock/template5.json b/mock/template5.json new file mode 100644 index 0000000..26fa49d --- /dev/null +++ b/mock/template5.json @@ -0,0 +1,74 @@ +{ + "Resources": { + "SQSQueue1": { + "Type": "AWS::SQS::Queue" + }, + "SQSQueue2": { + "Type": "AWS::SQS::Queue" + }, + "LambdaFunction1": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Handler": "index.handler", + "Role": "arn:aws:iam::123456789012:role/lambda-role", + "FunctionName": "LambdaFunction1", + "Runtime": "nodejs14.x", + "Code": { + "S3Bucket": "lambda-code-bucket", + "S3Key": "lambda-code1.zip" + }, + "Environment": { + "Variables": { + "QUEUE_URL": { "Ref": "SQSQueue1" } + } + } + } + }, + "LambdaFunction2": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Handler": "index.handler", + "Role": "arn:aws:iam::123456789012:role/lambda-role", + "FunctionName": "LambdaFunction2", + "Runtime": "nodejs14.x", + "Code": { + "S3Bucket": "lambda-code-bucket", + "S3Key": "lambda-code2.zip" + }, + "Environment": { + "Variables": { + "QUEUE_URL": { "Ref": "SQSQueue2" } + } + } + } + }, + "KinesisStream1": { + "Type": "AWS::Kinesis::Stream", + "Properties": { + "ShardCount": 1 + } + }, + "KinesisStream2": { + "Type": "AWS::Kinesis::Stream", + "Properties": { + "ShardCount": 1 + } + }, + "LambdaKinesisEventSourceMapping1": { + "Type": "AWS::Lambda::EventSourceMapping", + "Properties": { + "BatchSize": 5, + "EventSourceArn": { "Ref": "KinesisStream1" }, + "FunctionName": { "Ref": "LambdaFunction1" } + } + }, + "LambdaKinesisEventSourceMapping2": { + "Type": "AWS::Lambda::EventSourceMapping", + "Properties": { + "BatchSize": 5, + "EventSourceArn": { "Ref": "KinesisStream2" }, + "FunctionName": { "Ref": "LambdaFunction2" } + } + } + } +} diff --git a/mock/template6.json b/mock/template6.json new file mode 100644 index 0000000..12444e4 --- /dev/null +++ b/mock/template6.json @@ -0,0 +1,86 @@ +{ + "Resources": { + "SQSQueue1": { + "Type": "AWS::SQS::Queue" + }, + "SQSQueue2": { + "Type": "AWS::SQS::Queue" + }, + "LambdaFunction1": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Handler": "index.handler", + "Role": "arn:aws:iam::123456789012:role/lambda-role", + "FunctionName": "LambdaFunction1", + "Runtime": "nodejs14.x", + "Code": { + "S3Bucket": "lambda-code-bucket", + "S3Key": "lambda-code1.zip" + }, + "Environment": { + "Variables": { + "QUEUE_URL": { "Ref": "SQSQueue1" } + } + } + } + }, + "LambdaFunction2": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Handler": "index.handler", + "Role": "arn:aws:iam::123456789012:role/lambda-role", + "FunctionName": "LambdaFunction2", + "Runtime": "nodejs14.x", + "Code": { + "S3Bucket": "lambda-code-bucket", + "S3Key": "lambda-code2.zip" + }, + "Environment": { + "Variables": { + "QUEUE_URL": { "Ref": "SQSQueue2" } + } + } + } + }, + "CloudWatchAlarm1": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ActionsEnabled": true, + "AlarmDescription": "Alarm for LambdaFunction1 errors", + "MetricName": "Errors", + "Namespace": "AWS/Lambda", + "Statistic": "Sum", + "Threshold": 1, + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 1, + "Period": 300, + "Dimensions": [ + { + "Name": "FunctionName", + "Value": { "Ref": "LambdaFunction1" } + } + ] + } + }, + "CloudWatchAlarm2": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ActionsEnabled": true, + "AlarmDescription": "Alarm for LambdaFunction2 errors", + "MetricName": "Errors", + "Namespace": "AWS/Lambda", + "Statistic": "Sum", + "Threshold": 1, + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 1, + "Period": 300, + "Dimensions": [ + { + "Name": "FunctionName", + "Value": { "Ref": "LambdaFunction2" } + } + ] + } + } + } +} diff --git a/mock/template7.json b/mock/template7.json new file mode 100644 index 0000000..fb75476 --- /dev/null +++ b/mock/template7.json @@ -0,0 +1,509 @@ +{ + "AWSTemplateFormatVersion": "2010-09-09", + "Description": "The AWS CloudFormation template for this Serverless application", + "Resources": { + "ServerlessDeploymentBucket": { + "Type": "AWS::S3::Bucket", + "Properties": { + "BucketEncryption": { + "ServerSideEncryptionConfiguration": [ + { + "ServerSideEncryptionByDefault": { + "SSEAlgorithm": "AES256" + } + } + ] + } + } + }, + "ServerlessDeploymentBucketPolicy": { + "Type": "AWS::S3::BucketPolicy", + "Properties": { + "Bucket": { + "Ref": "ServerlessDeploymentBucket" + }, + "PolicyDocument": { + "Statement": [ + { + "Action": "s3:*", + "Effect": "Deny", + "Principal": "*", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":s3:::", + { + "Ref": "ServerlessDeploymentBucket" + }, + "/*" + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":s3:::", + { + "Ref": "ServerlessDeploymentBucket" + } + ] + ] + } + ], + "Condition": { + "Bool": { + "aws:SecureTransport": false + } + } + } + ] + } + } + }, + "ProducerLogGroup": { + "Type": "AWS::Logs::LogGroup", + "Properties": { + "LogGroupName": "/aws/lambda/aws-node-sqs-worker-project-dev-producer" + } + }, + "JobsWorkerLogGroup": { + "Type": "AWS::Logs::LogGroup", + "Properties": { + "LogGroupName": "/aws/lambda/aws-node-sqs-worker-project-dev-jobsWorker" + } + }, + "IamRoleLambdaExecution": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + }, + "Action": [ + "sts:AssumeRole" + ] + } + ] + }, + "Policies": [ + { + "PolicyName": { + "Fn::Join": [ + "-", + [ + "aws-node-sqs-worker-project", + "dev", + "lambda" + ] + ] + }, + "PolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "logs:CreateLogStream", + "logs:CreateLogGroup", + "logs:TagResource" + ], + "Resource": [ + { + "Fn::Sub": "arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/aws-node-sqs-worker-project-dev*:*" + } + ] + }, + { + "Effect": "Allow", + "Action": [ + "logs:PutLogEvents" + ], + "Resource": [ + { + "Fn::Sub": "arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/aws-node-sqs-worker-project-dev*:*:*" + } + ] + }, + { + "Effect": "Allow", + "Action": [ + "sqs:SendMessage", + "sqs:ChangeMessageVisibility" + ], + "Resource": [ + { + "Fn::GetAtt": [ + "jobsQueueCEDBAE3E", + "Arn" + ] + } + ] + }, + { + "Effect": "Allow", + "Action": [ + "sqs:ReceiveMessage", + "sqs:DeleteMessage", + "sqs:GetQueueAttributes" + ], + "Resource": [ + { + "Fn::GetAtt": [ + "jobsQueueCEDBAE3E", + "Arn" + ] + } + ] + } + ] + } + } + ], + "Path": "/", + "RoleName": { + "Fn::Join": [ + "-", + [ + "aws-node-sqs-worker-project", + "dev", + { + "Ref": "AWS::Region" + }, + "lambdaRole" + ] + ] + } + } + }, + "ProducerLambdaFunction": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Ref": "ServerlessDeploymentBucket" + }, + "S3Key": "serverless/aws-node-sqs-worker-project/dev/1705833879496-2024-01-21T10:44:39.496Z/aws-node-sqs-worker-project.zip" + }, + "Handler": "index.producer", + "Runtime": "nodejs18.x", + "FunctionName": "aws-node-sqs-worker-project-dev-producer", + "MemorySize": 1024, + "Timeout": 6, + "Environment": { + "Variables": { + "QUEUE_URL": { + "Ref": "jobsQueueCEDBAE3E" + } + } + }, + "Role": { + "Fn::GetAtt": [ + "IamRoleLambdaExecution", + "Arn" + ] + } + }, + "DependsOn": [ + "ProducerLogGroup" + ] + }, + "JobsWorkerLambdaFunction": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Ref": "ServerlessDeploymentBucket" + }, + "S3Key": "serverless/aws-node-sqs-worker-project/dev/1705833879496-2024-01-21T10:44:39.496Z/aws-node-sqs-worker-project.zip" + }, + "Handler": "index.consumer", + "Runtime": "nodejs18.x", + "FunctionName": "aws-node-sqs-worker-project-dev-jobsWorker", + "MemorySize": 1024, + "Timeout": 6, + "Role": { + "Fn::GetAtt": [ + "IamRoleLambdaExecution", + "Arn" + ] + }, + "ReservedConcurrentExecutions": 200 + }, + "DependsOn": [ + "JobsWorkerLogGroup" + ] + }, + "ProducerLambdaVersionuTQ7t4amwQGQCm6r1nMXU48wnVVaGIpkfkz7ZdThFp0": { + "Type": "AWS::Lambda::Version", + "DeletionPolicy": "Retain", + "Properties": { + "FunctionName": { + "Ref": "ProducerLambdaFunction" + }, + "CodeSha256": "AnrSvCAIoAKh7dIyDh8TyJ5Jbrqhyd8KoY1zzxb7XCM=" + } + }, + "JobsWorkerLambdaVersionx1AYxWc2gFMOTQcCDa9Z0cA1wcVonrxuYZMubQpE": { + "Type": "AWS::Lambda::Version", + "DeletionPolicy": "Retain", + "Properties": { + "FunctionName": { + "Ref": "JobsWorkerLambdaFunction" + }, + "CodeSha256": "AnrSvCAIoAKh7dIyDh8TyJ5Jbrqhyd8KoY1zzxb7XCM=" + } + }, + "JobsWorkerEventSourceMappingSQSJobsQueueCEDBAE3E": { + "Type": "AWS::Lambda::EventSourceMapping", + "DependsOn": [ + "IamRoleLambdaExecution" + ], + "Properties": { + "BatchSize": 5, + "MaximumBatchingWindowInSeconds": 33, + "EventSourceArn": { + "Fn::GetAtt": [ + "jobsQueueCEDBAE3E", + "Arn" + ] + }, + "FunctionName": { + "Fn::GetAtt": [ + "JobsWorkerLambdaFunction", + "Arn" + ] + }, + "Enabled": true, + "FunctionResponseTypes": [ + "ReportBatchItemFailures" + ], + "ScalingConfig": { + "MaximumConcurrency": 1000 + } + } + }, + "HttpApi": { + "Type": "AWS::ApiGatewayV2::Api", + "Properties": { + "Name": "dev-aws-node-sqs-worker-project", + "ProtocolType": "HTTP" + } + }, + "HttpApiStage": { + "Type": "AWS::ApiGatewayV2::Stage", + "Properties": { + "ApiId": { + "Ref": "HttpApi" + }, + "StageName": "$default", + "AutoDeploy": true, + "DefaultRouteSettings": { + "DetailedMetricsEnabled": false + } + } + }, + "ProducerLambdaPermissionHttpApi": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "FunctionName": { + "Fn::GetAtt": [ + "ProducerLambdaFunction", + "Arn" + ] + }, + "Action": "lambda:InvokeFunction", + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "HttpApi" + }, + "/*" + ] + ] + } + } + }, + "HttpApiIntegrationProducer": { + "Type": "AWS::ApiGatewayV2::Integration", + "Properties": { + "ApiId": { + "Ref": "HttpApi" + }, + "IntegrationType": "AWS_PROXY", + "IntegrationUri": { + "Fn::GetAtt": [ + "ProducerLambdaFunction", + "Arn" + ] + }, + "PayloadFormatVersion": "2.0", + "TimeoutInMillis": 30000 + } + }, + "HttpApiRoutePostProduce": { + "Type": "AWS::ApiGatewayV2::Route", + "Properties": { + "ApiId": { + "Ref": "HttpApi" + }, + "RouteKey": "POST /produce", + "Target": { + "Fn::Join": [ + "/", + [ + "integrations", + { + "Ref": "HttpApiIntegrationProducer" + } + ] + ] + } + }, + "DependsOn": "HttpApiIntegrationProducer" + }, + "jobsDlqD18CF374": { + "Type": "AWS::SQS::Queue", + "Properties": { + "MessageRetentionPeriod": 1209600, + "QueueName": "aws-node-sqs-worker-project-dev-jobs-dlq" + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "jobsQueueCEDBAE3E": { + "Type": "AWS::SQS::Queue", + "Properties": { + "DelaySeconds": 60, + "QueueName": "aws-node-sqs-worker-project-dev-jobs", + "RedrivePolicy": { + "deadLetterTargetArn": { + "Fn::GetAtt": [ + "jobsDlqD18CF374", + "Arn" + ] + }, + "maxReceiveCount": 3 + }, + "VisibilityTimeout": 69 + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + } + }, + "Outputs": { + "ServerlessDeploymentBucketName": { + "Value": { + "Ref": "ServerlessDeploymentBucket" + }, + "Export": { + "Name": "sls-aws-node-sqs-worker-project-dev-ServerlessDeploymentBucketName" + } + }, + "ProducerLambdaFunctionQualifiedArn": { + "Description": "Current Lambda function version", + "Value": { + "Ref": "ProducerLambdaVersionuTQ7t4amwQGQCm6r1nMXU48wnVVaGIpkfkz7ZdThFp0" + }, + "Export": { + "Name": "sls-aws-node-sqs-worker-project-dev-ProducerLambdaFunctionQualifiedArn" + } + }, + "JobsWorkerLambdaFunctionQualifiedArn": { + "Description": "Current Lambda function version", + "Value": { + "Ref": "JobsWorkerLambdaVersionx1AYxWc2gFMOTQcCDa9Z0cA1wcVonrxuYZMubQpE" + }, + "Export": { + "Name": "sls-aws-node-sqs-worker-project-dev-JobsWorkerLambdaFunctionQualifiedArn" + } + }, + "HttpApiId": { + "Description": "Id of the HTTP API", + "Value": { + "Ref": "HttpApi" + }, + "Export": { + "Name": "sls-aws-node-sqs-worker-project-dev-HttpApiId" + } + }, + "HttpApiUrl": { + "Description": "URL of the HTTP API", + "Value": { + "Fn::Join": [ + "", + [ + "https://", + { + "Ref": "HttpApi" + }, + ".execute-api.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + } + ] + ] + }, + "Export": { + "Name": "sls-aws-node-sqs-worker-project-dev-HttpApiUrl" + } + }, + "jobsQueueArnA5A2FF7E": { + "Description": "ARN of the \"jobs\" SQS queue.", + "Value": { + "Fn::GetAtt": [ + "jobsQueueCEDBAE3E", + "Arn" + ] + } + }, + "jobsQueueUrl573F5B7A": { + "Description": "URL of the \"jobs\" SQS queue.", + "Value": { + "Ref": "jobsQueueCEDBAE3E" + } + }, + "jobsDlqUrl2C7FA9D4": { + "Description": "URL of the \"jobs\" SQS dead letter queue.", + "Value": { + "Ref": "jobsDlqD18CF374" + } + } + } +} diff --git a/package.json b/package.json index 873b5bb..b5ad368 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,8 @@ "typescript": "^5.3.3" }, "dependencies": { - "cloudform-types": "^7.5.0" + "cloudform-types": "^7.5.0", + "yaml-cfn": "^0.3.2" }, "main": "lib/index.js", "license": "Apache-2.0", diff --git a/src/helpers/cf-extractor.ts b/src/helpers/cf-extractor.ts new file mode 100644 index 0000000..0322578 --- /dev/null +++ b/src/helpers/cf-extractor.ts @@ -0,0 +1,30 @@ +import { Template } from "cloudform-types"; + +export type CFTemplateType = + | "AWS::SQS::Queue" + | "AWS::Lambda::Function" + | "AWS::Lambda::EventSourceMapping"; + +export const extract = ( + template: Template, + type: CFTemplateType, +): Partial<{ + LogicalId: string; + Properties: T; +}>[] => { + if (template.Resources) { + const resources = template.Resources; + if (resources && Object.keys(resources).length > 0) { + return Object.keys(resources) + .filter((key) => resources[key].Type === type) + .map((key) => ({ + LogicalId: key, + Properties: resources[key].Properties, + })) as Partial<{ + LogicalId: string; + Properties: T; + }>[]; + } + } + return []; +}; diff --git a/src/helpers/filter.ts b/src/helpers/filter.ts new file mode 100644 index 0000000..579263f --- /dev/null +++ b/src/helpers/filter.ts @@ -0,0 +1,59 @@ +import { Template, Lambda, SQS } from "cloudform-types"; +import { extract } from "./cf-extractor"; + +export const filter = (template: Template) => { + const queues = extract(template, "AWS::SQS::Queue"); + const functions = extract( + template, + "AWS::Lambda::Function", + ); + const integrations = extract( + template, + "AWS::Lambda::EventSourceMapping", + ); + const filtered: Record< + string, + | Partial + | Partial + | Partial + >[] = []; + if (queues.length > 0 && functions.length > 0 && integrations.length > 0) { + integrations.forEach((integration) => { + if ( + integration && + integration.Properties && + integration.Properties.EventSourceArn && + integration.Properties.FunctionName + ) { + const eventSourceArn = integration.Properties.EventSourceArn; + if ( + //@ts-ignore + (eventSourceArn && eventSourceArn["Fn::GetAtt"]) || + //@ts-ignore + eventSourceArn.Ref + ) { + //@ts-ignore + let logicalId = eventSourceArn.Ref; + //@ts-ignore + if (eventSourceArn["Fn::GetAtt"]) { + //@ts-ignore + logicalId = eventSourceArn["Fn::GetAtt"][0]; + } + const sqs = queues.find(({ LogicalId }) => LogicalId === logicalId); + const lambda = functions.find( + ({ LogicalId }) => + //@ts-ignore + LogicalId === integration.Properties.FunctionName?.Ref || + LogicalId === + //@ts-ignore + integration.Properties.FunctionName?.["Fn::GetAtt"]?.[0], + ); + if (sqs && lambda) { + filtered.push({ integration, lambda, sqs }); + } + } + } + }); + } + return filtered; +}; diff --git a/test/helpers/cf-extractor.test.ts b/test/helpers/cf-extractor.test.ts new file mode 100644 index 0000000..4144873 --- /dev/null +++ b/test/helpers/cf-extractor.test.ts @@ -0,0 +1,107 @@ +import { extract } from "../../src/helpers/cf-extractor"; + +describe(`when template does not contain any resource`, () => { + const template = { + Resources: {}, + }; + const type = "AWS::SQS::Queue"; + it(`should return an empty array`, () => { + expect(extract(template, type)).toHaveLength(0); + }); +}); +describe(`when template contains resources other that specified`, () => { + const template = { + Resources: { + my_function: { + Type: "AWS::Lambda::Function", + }, + my_mapping: { + Type: "AWS::Lambda::EventSourceMapping", + }, + }, + }; + const type = "AWS::SQS::Queue"; + it(`should return an empty array`, () => { + expect(extract(template, type)).toHaveLength(0); + }); +}); +describe(`when template contains an SQS Queue resource and its corresponding type specified`, () => { + describe("when SQS does not contain any properties", () => { + const template = { + Resources: { + my_queue: { + Type: "AWS::SQS::Queue", + }, + my_mapping: { + Type: "AWS::Lambda::EventSourceMapping", + }, + }, + }; + const type = "AWS::SQS::Queue"; + it(`should return an array containing an SQS Queue resource with undefined "Properties"`, () => { + expect(extract(template, type)).toHaveLength(1); + expect(extract(template, type)).toEqual( + expect.arrayContaining([ + { LogicalId: "my_queue", Properties: undefined }, + ]), + ); + }); + }); + describe("when SQS contains properties as an empty object", () => { + const template = { + Resources: { + my_queue: { + Type: "AWS::SQS::Queue", + Properties: {}, + }, + my_mapping: { + Type: "AWS::Lambda::EventSourceMapping", + }, + }, + }; + const type = "AWS::SQS::Queue"; + it(`should return an array containing an SQS Queue resource with "Properties" as an empty object`, () => { + expect(extract(template, type)).toHaveLength(1); + expect(extract(template, type)).toEqual( + expect.arrayContaining([{ LogicalId: "my_queue", Properties: {} }]), + ); + }); + }); + describe("when SQS contains properties", () => { + const template = { + Resources: { + my_queue: { + Type: "AWS::SQS::Queue", + Properties: { + foo: "bar", + baz: { + boo: "kaz", + koo: 123, + }, + }, + }, + my_mapping: { + Type: "AWS::Lambda::EventSourceMapping", + }, + }, + }; + const type = "AWS::SQS::Queue"; + it(`should return an array containing an SQS Queue resource having some "Properties"`, () => { + expect(extract(template, type)).toHaveLength(1); + expect(extract(template, type)).toEqual( + expect.arrayContaining([ + { + LogicalId: "my_queue", + Properties: { + foo: "bar", + baz: { + boo: "kaz", + koo: 123, + }, + }, + }, + ]), + ); + }); + }); +}); diff --git a/test/helpers/filter.test.ts b/test/helpers/filter.test.ts new file mode 100644 index 0000000..48fa2dc --- /dev/null +++ b/test/helpers/filter.test.ts @@ -0,0 +1,746 @@ +import { filter } from "../../src/helpers/filter"; + +describe(`when template does not contain any resource`, () => { + const template = { + Resources: {}, + }; + it(`should return an empty array`, () => { + expect(filter(template)).toHaveLength(0); + }); +}); +describe(`when template contains resources other that specified`, () => { + const template = { + Resources: { + my_table: { + Type: "AWS::DynamoDB::Table", + }, + }, + }; + it(`should return an empty array`, () => { + expect(filter(template)).toHaveLength(0); + }); +}); +describe(`when template contains only an SQS Queue resource`, () => { + const template = { + Resources: { + my_queue: { + Type: "AWS::SQS::Queue", + }, + }, + }; + it(`should return an empty array`, () => { + expect(filter(template)).toHaveLength(0); + }); +}); +describe(`when template contains only an SQS Queue and DynamoDB table resources`, () => { + const template = { + Resources: { + my_queue: { + Type: "AWS::SQS::Queue", + }, + my_table: { + Type: "AWS::DynamoDB::Table", + }, + }, + }; + it(`should return an empty array`, () => { + expect(filter(template)).toHaveLength(0); + }); +}); +describe(`when template contains only an the required resources`, () => { + describe("when there is no mapping", () => { + const template = { + Resources: { + my_queue: { + Type: "AWS::SQS::Queue", + }, + my_function: { + Type: "AWS::Lambda::Function", + }, + }, + }; + it(`should return an empty array`, () => { + expect(filter(template)).toHaveLength(0); + }); + }); + describe("when the mapping does not have properties", () => { + const template = { + Resources: { + my_queue: { + Type: "AWS::SQS::Queue", + }, + my_function: { + Type: "AWS::Lambda::Function", + }, + my_mapping: { + Type: "AWS::Lambda::EventSourceMapping", + }, + }, + }; + it(`should return an empty array`, () => { + expect(filter(template)).toHaveLength(0); + }); + }); + describe(`when the mapping has "Properties" as an empty object`, () => { + const template = { + Resources: { + my_queue: { + Type: "AWS::SQS::Queue", + }, + my_function: { + Type: "AWS::Lambda::Function", + }, + my_mapping: { + Type: "AWS::Lambda::EventSourceMapping", + Properties: {}, + }, + }, + }; + it(`should return an empty array`, () => { + expect(filter(template)).toHaveLength(0); + }); + }); + describe(`when the mapping "Properties.EventSourceArn" does not have "EventSourceArn"`, () => { + const template = { + Resources: { + my_queue: { + Type: "AWS::SQS::Queue", + }, + my_function: { + Type: "AWS::Lambda::Function", + }, + my_mapping: { + Type: "AWS::Lambda::EventSourceMapping", + Properties: { + foo: "bar", + baz: { + kaz: 123, + }, + }, + }, + }, + }; + it(`should return an empty array`, () => { + expect(filter(template)).toHaveLength(0); + }); + }); + describe(`when the mapping "Properties.EventSourceArn" is a number`, () => { + const template = { + Resources: { + my_queue: { + Type: "AWS::SQS::Queue", + }, + my_function: { + Type: "AWS::Lambda::Function", + }, + my_mapping: { + Type: "AWS::Lambda::EventSourceMapping", + Properties: { + EventSourceArn: 123, + foo: "bar", + baz: { + kaz: 123, + }, + }, + }, + }, + }; + it(`should return an empty array`, () => { + expect(filter(template)).toHaveLength(0); + }); + }); + describe(`when the mapping "Properties.EventSourceArn" is valid`, () => { + describe(`when the mapping "Properties.FunctionName" a number`, () => { + const template = { + Resources: { + my_queue: { + Type: "AWS::SQS::Queue", + }, + my_function: { + Type: "AWS::Lambda::Function", + }, + my_mapping: { + Type: "AWS::Lambda::EventSourceMapping", + Properties: { + EventSourceArn: { "Fn::GetAtt": ["x", "Arn"] }, + FunctionName: 123, + foo: "bar", + baz: { + kaz: 123, + }, + }, + }, + }, + }; + it(`should return an empty array`, () => { + expect(filter(template)).toHaveLength(0); + }); + }); + describe(`when the mapping "Properties.FunctionName" is valid`, () => { + const template = { + Resources: { + my_queue: { + Type: "AWS::SQS::Queue", + }, + my_function: { + Type: "AWS::Lambda::Function", + }, + my_mapping: { + Type: "AWS::Lambda::EventSourceMapping", + Properties: { + EventSourceArn: { "Fn::GetAtt": ["x", "Arn"] }, + FunctionName: { "Fn::GetAtt": ["y", "Arn"] }, + foo: "bar", + baz: { + kaz: 123, + }, + }, + }, + }, + }; + it(`should return an empty array`, () => { + expect(filter(template)).toHaveLength(0); + }); + }); + describe(`when the mapping "Properties.EventSourceArn" and "Properties.FunctionName" is correct`, () => { + describe(`when the mapping "Properties.EventSourceArn" and "Properties.FunctionName" are Fn GetAtt intrensic functions`, () => { + const template = { + Resources: { + my_queue: { + Type: "AWS::SQS::Queue", + }, + my_function: { + Type: "AWS::Lambda::Function", + }, + my_mapping: { + Type: "AWS::Lambda::EventSourceMapping", + Properties: { + EventSourceArn: { "Fn::GetAtt": ["my_queue", "Arn"] }, + FunctionName: { "Fn::GetAtt": ["my_function", "Arn"] }, + foo: "bar", + baz: { + kaz: 123, + }, + }, + }, + }, + }; + it(`should return an array having length 1`, () => { + expect(filter(template)).toHaveLength(1); + }); + it(`should return a correct array`, () => { + expect(filter(template)).toEqual( + expect.arrayContaining([ + { + integration: { + LogicalId: "my_mapping", + Properties: { + EventSourceArn: { "Fn::GetAtt": ["my_queue", "Arn"] }, + FunctionName: { "Fn::GetAtt": ["my_function", "Arn"] }, + baz: { kaz: 123 }, + foo: "bar", + }, + }, + lambda: { LogicalId: "my_function", Properties: undefined }, + sqs: { LogicalId: "my_queue", Properties: undefined }, + }, + ]), + ); + }); + }); + describe(`when the mapping "Properties.EventSourceArn" is a Ref and "Properties.FunctionName" is an Fn GetAtt intrensic functions`, () => { + const template = { + Resources: { + my_queue: { + Type: "AWS::SQS::Queue", + }, + my_function: { + Type: "AWS::Lambda::Function", + }, + my_mapping: { + Type: "AWS::Lambda::EventSourceMapping", + Properties: { + EventSourceArn: { Ref: "my_queue" }, + FunctionName: { "Fn::GetAtt": ["my_function", "Arn"] }, + foo: "bar", + baz: { + kaz: 123, + }, + }, + }, + }, + }; + it(`should return an array having length 1`, () => { + expect(filter(template)).toHaveLength(1); + }); + it(`should return a correct array`, () => { + expect(filter(template)).toEqual( + expect.arrayContaining([ + { + integration: { + LogicalId: "my_mapping", + Properties: { + EventSourceArn: { Ref: "my_queue" }, + FunctionName: { "Fn::GetAtt": ["my_function", "Arn"] }, + baz: { kaz: 123 }, + foo: "bar", + }, + }, + lambda: { LogicalId: "my_function", Properties: undefined }, + sqs: { LogicalId: "my_queue", Properties: undefined }, + }, + ]), + ); + }); + }); + describe(`when the mapping "Properties.EventSourceArn" is an Fn GetAtt and "Properties.FunctionName" is a Ref intrensic functions`, () => { + const template = { + Resources: { + my_queue: { + Type: "AWS::SQS::Queue", + }, + my_function: { + Type: "AWS::Lambda::Function", + }, + my_mapping: { + Type: "AWS::Lambda::EventSourceMapping", + Properties: { + EventSourceArn: { "Fn::GetAtt": ["my_queue", "Arn"] }, + FunctionName: { Ref: "my_function" }, + foo: "bar", + baz: { + kaz: 123, + }, + }, + }, + }, + }; + it(`should return an array having length 1`, () => { + expect(filter(template)).toHaveLength(1); + }); + it(`should return a correct array`, () => { + expect(filter(template)).toEqual( + expect.arrayContaining([ + { + integration: { + LogicalId: "my_mapping", + Properties: { + EventSourceArn: { "Fn::GetAtt": ["my_queue", "Arn"] }, + FunctionName: { Ref: "my_function" }, + baz: { kaz: 123 }, + foo: "bar", + }, + }, + lambda: { LogicalId: "my_function", Properties: undefined }, + sqs: { LogicalId: "my_queue", Properties: undefined }, + }, + ]), + ); + }); + }); + describe(`when the mapping "Properties.EventSourceArn" and "Properties.FunctionName" are Ref intrensic functions`, () => { + const template = { + Resources: { + my_queue: { + Type: "AWS::SQS::Queue", + }, + my_function: { + Type: "AWS::Lambda::Function", + }, + my_mapping: { + Type: "AWS::Lambda::EventSourceMapping", + Properties: { + EventSourceArn: { Ref: "my_queue" }, + FunctionName: { Ref: "my_function" }, + foo: "bar", + baz: { + kaz: 123, + }, + }, + }, + }, + }; + it(`should return an array having length 1`, () => { + expect(filter(template)).toHaveLength(1); + }); + it(`should return a correct array`, () => { + expect(filter(template)).toEqual( + expect.arrayContaining([ + { + integration: { + LogicalId: "my_mapping", + Properties: { + EventSourceArn: { Ref: "my_queue" }, + FunctionName: { Ref: "my_function" }, + baz: { kaz: 123 }, + foo: "bar", + }, + }, + lambda: { LogicalId: "my_function", Properties: undefined }, + sqs: { LogicalId: "my_queue", Properties: undefined }, + }, + ]), + ); + }); + }); + describe(`when SQS Queue and Lambda Function have properties`, () => { + const template = { + Resources: { + my_queue: { + Type: "AWS::SQS::Queue", + Properties: { + p1: { + p2: "abc", + }, + }, + }, + my_function: { + Type: "AWS::Lambda::Function", + Properties: { + p3: { + p4: "def", + }, + }, + }, + my_mapping: { + Type: "AWS::Lambda::EventSourceMapping", + Properties: { + EventSourceArn: { Ref: "my_queue" }, + FunctionName: { Ref: "my_function" }, + foo: "bar", + baz: { + kaz: 123, + }, + }, + }, + }, + }; + it(`should return an array having length 1`, () => { + expect(filter(template)).toHaveLength(1); + }); + it(`should return a correct array`, () => { + expect(filter(template)).toEqual( + expect.arrayContaining([ + { + integration: { + LogicalId: "my_mapping", + Properties: { + EventSourceArn: { Ref: "my_queue" }, + FunctionName: { Ref: "my_function" }, + baz: { kaz: 123 }, + foo: "bar", + }, + }, + lambda: { + LogicalId: "my_function", + Properties: { + p3: { + p4: "def", + }, + }, + }, + sqs: { + LogicalId: "my_queue", + Properties: { + p1: { + p2: "abc", + }, + }, + }, + }, + ]), + ); + }); + }); + describe(`when having multiple mappings`, () => { + describe("when one only one mapping is correct", () => { + const template = { + Resources: { + my_queue_1: { + Type: "AWS::SQS::Queue", + }, + my_function_1: { + Type: "AWS::Lambda::Function", + }, + my_mapping_1: { + Type: "AWS::Lambda::EventSourceMapping", + Properties: { + EventSourceArn: { Ref: "my_queue_1" }, + FunctionName: { Ref: "my_function_1" }, + foo: "bar", + baz: { + kaz: 123, + }, + }, + }, + my_topic_2: { + Type: "AWS::SNS::Topic", + }, + my_function_2: { + Type: "AWS::Lambda::Function", + }, + my_mapping_2: { + Type: "AWS::Lambda::EventSourceMapping", + Properties: { + EventSourceArn: { Ref: "my_topic_2" }, + FunctionName: { Ref: "my_function_2" }, + foo: "bar", + baz: { + kaz: 123, + }, + }, + }, + }, + }; + it(`should return an array having length 1`, () => { + expect(filter(template)).toHaveLength(1); + }); + it(`should return a correct array`, () => { + expect(filter(template)).toEqual( + expect.arrayContaining([ + { + integration: { + LogicalId: "my_mapping_1", + Properties: { + EventSourceArn: { Ref: "my_queue_1" }, + FunctionName: { Ref: "my_function_1" }, + baz: { kaz: 123 }, + foo: "bar", + }, + }, + lambda: { LogicalId: "my_function_1", Properties: undefined }, + sqs: { LogicalId: "my_queue_1", Properties: undefined }, + }, + ]), + ); + }); + }); + describe("when two mappings are correct", () => { + const template = { + Resources: { + my_queue_1: { + Type: "AWS::SQS::Queue", + }, + my_function_1: { + Type: "AWS::Lambda::Function", + }, + my_mapping_1: { + Type: "AWS::Lambda::EventSourceMapping", + Properties: { + EventSourceArn: { Ref: "my_queue_1" }, + FunctionName: { Ref: "my_function_1" }, + foo: "bar", + baz: { + kaz: 123, + }, + }, + }, + my_queue_2: { + Type: "AWS::SQS::Queue", + }, + my_function_2: { + Type: "AWS::Lambda::Function", + }, + my_mapping_2: { + Type: "AWS::Lambda::EventSourceMapping", + Properties: { + EventSourceArn: { Ref: "my_queue_2" }, + FunctionName: { Ref: "my_function_2" }, + foo: "bar", + baz: { + kaz: 123, + }, + }, + }, + }, + }; + it(`should return an array having length 2`, () => { + expect(filter(template)).toHaveLength(2); + }); + it(`should return an array having length 2`, () => { + expect(filter(template)).toEqual( + expect.arrayContaining([ + { + integration: { + LogicalId: "my_mapping_1", + Properties: { + EventSourceArn: { Ref: "my_queue_1" }, + FunctionName: { Ref: "my_function_1" }, + baz: { kaz: 123 }, + foo: "bar", + }, + }, + lambda: { LogicalId: "my_function_1", Properties: undefined }, + sqs: { LogicalId: "my_queue_1", Properties: undefined }, + }, + { + integration: { + LogicalId: "my_mapping_2", + Properties: { + EventSourceArn: { Ref: "my_queue_2" }, + FunctionName: { Ref: "my_function_2" }, + baz: { kaz: 123 }, + foo: "bar", + }, + }, + lambda: { LogicalId: "my_function_2", Properties: undefined }, + sqs: { LogicalId: "my_queue_2", Properties: undefined }, + }, + ]), + ); + }); + }); + describe("when two mappings are pointing to the same SQS Queue", () => { + const template = { + Resources: { + my_queue_1: { + Type: "AWS::SQS::Queue", + }, + my_function_1: { + Type: "AWS::Lambda::Function", + }, + my_mapping_1: { + Type: "AWS::Lambda::EventSourceMapping", + Properties: { + EventSourceArn: { Ref: "my_queue_1" }, + FunctionName: { Ref: "my_function_1" }, + foo: "bar", + baz: { + kaz: 123, + }, + }, + }, + my_queue_2: { + Type: "AWS::SQS::Queue", + }, + my_function_2: { + Type: "AWS::Lambda::Function", + }, + my_mapping_2: { + Type: "AWS::Lambda::EventSourceMapping", + Properties: { + EventSourceArn: { Ref: "my_queue_1" }, + FunctionName: { Ref: "my_function_2" }, + foo: "bar", + baz: { + kaz: 123, + }, + }, + }, + }, + }; + it(`should return an array having length 2`, () => { + expect(filter(template)).toHaveLength(2); + }); + it(`should return a correct array`, () => { + expect(filter(template)).toEqual( + expect.arrayContaining([ + { + integration: { + LogicalId: "my_mapping_1", + Properties: { + EventSourceArn: { Ref: "my_queue_1" }, + FunctionName: { Ref: "my_function_1" }, + baz: { kaz: 123 }, + foo: "bar", + }, + }, + lambda: { LogicalId: "my_function_1", Properties: undefined }, + sqs: { LogicalId: "my_queue_1", Properties: undefined }, + }, + { + integration: { + LogicalId: "my_mapping_2", + Properties: { + EventSourceArn: { Ref: "my_queue_1" }, + FunctionName: { Ref: "my_function_2" }, + baz: { kaz: 123 }, + foo: "bar", + }, + }, + lambda: { LogicalId: "my_function_2", Properties: undefined }, + sqs: { LogicalId: "my_queue_1", Properties: undefined }, + }, + ]), + ); + }); + }); + describe("when two mappings are pointing to the same Lambda Function", () => { + const template = { + Resources: { + my_queue_1: { + Type: "AWS::SQS::Queue", + }, + my_function_1: { + Type: "AWS::Lambda::Function", + }, + my_mapping_1: { + Type: "AWS::Lambda::EventSourceMapping", + Properties: { + EventSourceArn: { Ref: "my_queue_1" }, + FunctionName: { Ref: "my_function_1" }, + foo: "bar", + baz: { + kaz: 123, + }, + }, + }, + my_queue_2: { + Type: "AWS::SQS::Queue", + }, + my_function_2: { + Type: "AWS::Lambda::Function", + }, + my_mapping_2: { + Type: "AWS::Lambda::EventSourceMapping", + Properties: { + EventSourceArn: { Ref: "my_queue_2" }, + FunctionName: { Ref: "my_function_1" }, + foo: "bar", + baz: { + kaz: 123, + }, + }, + }, + }, + }; + it(`should return an array having length 2`, () => { + expect(filter(template)).toHaveLength(2); + }); + it(`should return a correct array`, () => { + expect(filter(template)).toEqual( + expect.arrayContaining([ + { + integration: { + LogicalId: "my_mapping_1", + Properties: { + EventSourceArn: { Ref: "my_queue_1" }, + FunctionName: { Ref: "my_function_1" }, + baz: { kaz: 123 }, + foo: "bar", + }, + }, + lambda: { LogicalId: "my_function_1", Properties: undefined }, + sqs: { LogicalId: "my_queue_1", Properties: undefined }, + }, + { + integration: { + LogicalId: "my_mapping_2", + Properties: { + EventSourceArn: { Ref: "my_queue_2" }, + FunctionName: { Ref: "my_function_1" }, + baz: { kaz: 123 }, + foo: "bar", + }, + }, + lambda: { LogicalId: "my_function_1", Properties: undefined }, + sqs: { LogicalId: "my_queue_2", Properties: undefined }, + }, + ]), + ); + }); + }); + }); + }); + }); +}); diff --git a/test/yaml-cfn.test.ts b/test/yaml-cfn.test.ts new file mode 100644 index 0000000..dbbc235 --- /dev/null +++ b/test/yaml-cfn.test.ts @@ -0,0 +1,20 @@ +import { yamlParse, yamlDump } from "yaml-cfn"; + +describe("Convert CloudFormation template from YAML to JSON", () => { + const input = ` + Key: + - !GetAtt Foo.Bar + - !Equals [!Ref Baz, "hello"] + `; + + const parsed = { + Key: [ + { "Fn::GetAtt": ["Foo", "Bar"] }, + { "Fn::Equals": [{ Ref: "Baz" }, "hello"] }, + ], + }; + it("should convert a CloudFormation template from YAML to JSON", () => { + expect(yamlParse(input)).toEqual(parsed); + expect(yamlParse(yamlDump(parsed))).toEqual(parsed); + }); +}); diff --git a/yarn.lock b/yarn.lock index b995b21..02fffd8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3122,7 +3122,7 @@ js-yaml@3.14.1, js-yaml@^3.13.1: argparse "^1.0.7" esprima "^4.0.0" -js-yaml@^4.1.0: +js-yaml@^4.0.0, js-yaml@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== @@ -4639,6 +4639,13 @@ yallist@^4.0.0: resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== +yaml-cfn@^0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/yaml-cfn/-/yaml-cfn-0.3.2.tgz#f20f5d7a6eec2a9e3775b36360c21a573d679597" + integrity sha512-MvrWhv40GKWHFGCliTGGAMwAeqIXf/bzf6WW48+xND9iMp8cTj0R8xkwM0lX/GzNN/EZKr5gP4Hx63Fn+sICoA== + dependencies: + js-yaml "^4.0.0" + yaml@^2.2.2: version "2.3.4" resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.3.4.tgz#53fc1d514be80aabf386dc6001eb29bf3b7523b2"