Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New Router: CloudFormation Custom Resource #1268

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions packages/cloudformation-router/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<div align="center">
<h1>Middy cloudformation-router lambda handler</h1>
<img alt="Middy logo" src="https://raw.githubusercontent.com/middyjs/middy/main/docs/img/middy-logo.svg"/>
<p><strong>CloudFormation Custom Response router for the middy framework, the stylish Node.js middleware engine for AWS Lambda</strong></p>
<p>
<a href="https://www.npmjs.com/package/@middy/cloudformation-router?activeTab=versions">
<img src="https://badge.fury.io/js/%40middy%cloudformation-router.svg" alt="npm version" style="max-width:100%;">
</a>
<a href="https://packagephobia.com/result?p=@middy/cloudformation-router">
<img src="https://packagephobia.com/badge?p=@middy/cloudformation-router" alt="npm install size" style="max-width:100%;">
</a>
<a href="https://github.com/middyjs/middy/actions/workflows/tests.yml">
<img src="https://github.com/middyjs/middy/actions/workflows/tests.yml/badge.svg?branch=main&event=push" alt="GitHub Actions CI status badge" style="max-width:100%;">
</a>
<br/>
<a href="https://standardjs.com/">
<img src="https://img.shields.io/badge/code_style-standard-brightgreen.svg" alt="Standard Code Style" style="max-width:100%;">
</a>
<a href="https://snyk.io/test/github/middyjs/middy">
<img src="https://snyk.io/test/github/middyjs/middy/badge.svg" alt="Known Vulnerabilities" data-canonical-src="https://snyk.io/test/github/middyjs/middy" style="max-width:100%;">
</a>
<a href="https://github.com/middyjs/middy/actions/workflows/sast.yml">
<img src="https://github.com/middyjs/middy/actions/workflows/sast.yml/badge.svg
?branch=main&event=push" alt="CodeQL" style="max-width:100%;">
</a>
<a href="https://bestpractices.coreinfrastructure.org/projects/5280">
<img src="https://bestpractices.coreinfrastructure.org/projects/5280/badge" alt="Core Infrastructure Initiative (CII) Best Practices" style="max-width:100%;">
</a>
<br/>
<a href="https://gitter.im/middyjs/Lobby">
<img src="https://badges.gitter.im/gitterHQ/gitter.svg" alt="Chat on Gitter" style="max-width:100%;">
</a>
<a href="https://stackoverflow.com/questions/tagged/middy?sort=Newest&uqlId=35052">
<img src="https://img.shields.io/badge/StackOverflow-[middy]-yellow" alt="Ask questions on StackOverflow" style="max-width:100%;">
</a>
</p>
<p>You can read the documentation at: <a href="https://middy.js.org/docs/routers/cloudformation-router">https://middy.js.org/docs/routers/cloudformation-router</a></p>
</div>

## License

Licensed under [MIT License](LICENSE). Copyright (c) 2017-2024 [Luciano Mammino](https://github.com/lmammino), [will Farrell](https://github.com/willfarrell), and the [Middy team](https://github.com/middyjs/middy/graphs/contributors).

<a href="https://app.fossa.io/projects/git%2Bgit.luolix.top%2Fmiddyjs%2Fmiddy?ref=badge_large">
<img src="https://app.fossa.io/api/projects/git%2Bgit.luolix.top%2Fmiddyjs%2Fmiddy.svg?type=large" alt="FOSSA Status" style="max-width:100%;">
</a>
46 changes: 46 additions & 0 deletions packages/cloudformation-router/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
const defaults = {
routes: [],
notFoundResponse: ({ requestType }) => {
return {
Status: 'FAILED',
Reason: `Route ${requestType} does not exist. @middy/cloudformation-router`
}
}
}
const cloudformationCustomResourceRouteHandler = (opts = {}) => {
if (Array.isArray(opts)) {
opts = { routes: opts }
}
const { routes, notFoundResponse } = { ...defaults, ...opts }

const routesStatic = {}
for (const route of routes) {
const { requestType, handler } = route

// Static
routesStatic[requestType] = handler
}

return (event, context, abort) => {
const { RequestType: requestType } = event
if (!requestType) {
return notFoundResponse({ requestType })

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would for example expect that here we report to cloudformation that we have a failure, because if we don't do that cloudformation will simply wait x time until it timeouts whilst we already know that we won't be handling the request

}

// Static
const handler = routesStatic[requestType]
if (typeof handler !== 'undefined') {
const response = handler(event, context, abort)
response.Status ??= 'SUCCESS'
response.RequestId ??= event.RequestId
response.LogicalResourceId ??= event.LogicalResourceId
response.StackId ??= event.StackId
return response
}
Comment on lines +32 to +39

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be nice if it also reported to the responseUrl, I feel like even though the abstraction is nice, it doesn't really add that much value currently as it's still the responsibility of the developer to correctly handle all errors and respond.

The auto reporting implementation is more what I was looking for honestly


// Not Found
return notFoundResponse({ requestType })
}
}

export default cloudformationCustomResourceRouteHandler
70 changes: 70 additions & 0 deletions packages/cloudformation-router/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
{
"name": "@middy/cloudformation-router",
"version": "6.0.0",
"description": "CloudFormation Custom Response event router for the middy framework",
"type": "module",
"engines": {
"node": ">=20"
},
"engineStrict": true,
"publishConfig": {
"access": "public"
},
"module": "./index.js",
"exports": {
".": {
"import": {
"types": "./index.d.ts",
"default": "./index.js"
},
"require": {
"default": "./index.js"
}
}
},
"types": "index.d.ts",
"files": [
"index.js",
"index.d.ts"
],
"scripts": {
"test": "npm run test:unit",
"test:unit": "node --test __tests__/index.js",
"test:benchmark": "node __benchmarks__/index.js"
},
"license": "MIT",
"keywords": [
"Lambda",
"Middleware",
"Serverless",
"Framework",
"AWS",
"AWS Lambda",
"Middy",
"CloudFormation",
"Custom Response",
"router"
],
"author": {
"name": "Middy contributors",
"url": "https://github.com/middyjs/middy/graphs/contributors"
},
"repository": {
"type": "git",
"url": "github:middyjs/middy",
"directory": "packages/cloudformation-router"
},
"bugs": {
"url": "https://github.com/middyjs/middy/issues"
},
"homepage": "https://middy.js.org",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/willfarrell"
},
"devDependencies": {
"@middy/core": "6.0.0",
"@types/aws-lambda": "^8.10.100"
},
"gitHead": "7a6c0fbb8ab71d6a2171e678697de9f237568431"
}
29 changes: 15 additions & 14 deletions packages/secrets-manager/__tests__/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { setTimeout } from 'node:timers/promises'
import { test } from 'node:test'
import { test, mock } from 'node:test'
import { equal, deepEqual } from 'node:assert/strict'
import { mockClient } from 'aws-sdk-client-mock'
import middy from '../../core/index.js'
Expand All @@ -11,6 +10,9 @@ import {
} from '@aws-sdk/client-secrets-manager'
import secretsManager from '../index.js'

mock.timers.enable({ apis: ['setTimeout'] })
const now = Date.now() / 1000

test.afterEach((t) => {
t.mock.reset()
clearCache()
Expand Down Expand Up @@ -234,8 +236,8 @@ test('It should call aws-sdk if cache enabled but cached param has expired using
const mockService = mockClient(SecretsManagerClient)
.on(DescribeSecretCommand, { SecretId: 'api_key' })
.resolves({
LastRotationDate: Date.now() / 1000 - 50,
LastChangedDate: Date.now() / 1000 - 50
LastRotationDate: now - 50,
LastChangedDate: now - 50
})
.on(GetSecretValueCommand, { SecretId: 'api_key' })
.resolves({ SecretString: 'token' })
Expand All @@ -251,7 +253,7 @@ test('It should call aws-sdk if cache enabled but cached param has expired using
.use(
secretsManager({
AwsClient: SecretsManagerClient,
cacheExpiry: 100,
cacheExpiry: 15 * 60 * 1000,
fetchData: {
token: 'api_key'
},
Expand All @@ -261,22 +263,21 @@ test('It should call aws-sdk if cache enabled but cached param has expired using
)
.before(middleware)

await handler(event, context) // fetch x 2
await handler(event, context)
await setTimeout(100)
await handler(event, context) // fetch x 2
await handler(event, context)
mock.timers.tick(15 * 60 * 1000)
await handler(event, context)

equal(sendStub.callCount, 2 * 2)
equal(sendStub.callCount, 2)
})

test('It should call aws-sdk if cache enabled but cached param has expired using LastRotationDate, fallback to NextRotationDate', async (t) => {
const now = Date.now() / 1000
const mockService = mockClient(SecretsManagerClient)
.on(DescribeSecretCommand, { SecretId: 'api_key' })
.resolves({
LastRotationDate: now - 25,
LastChangedDate: now - 25,
NextRotationDate: now + 50
NextRotationDate: now + 5 * 60
})
.on(GetSecretValueCommand, { SecretId: 'api_key' })
.resolves({ SecretString: 'token' })
Expand All @@ -292,7 +293,7 @@ test('It should call aws-sdk if cache enabled but cached param has expired using
.use(
secretsManager({
AwsClient: SecretsManagerClient,
cacheExpiry: 100,
cacheExpiry: 15 * 60 * 1000,
fetchData: {
token: 'api_key'
},
Expand All @@ -304,7 +305,7 @@ test('It should call aws-sdk if cache enabled but cached param has expired using

await handler(event, context)
await handler(event, context)
await setTimeout(100)
mock.timers.tick(15 * 60 * 1000)
await handler(event, context)

equal(sendStub.callCount, 2)
Expand Down Expand Up @@ -340,7 +341,7 @@ test('It should call aws-sdk if cache enabled but cached param has expired using

await handler(event, context)
await handler(event, context)
await setTimeout(100)
mock.timers.tick(15 * 60 * 1000)
await handler(event, context)

equal(sendStub.callCount, 2)
Expand Down
79 changes: 79 additions & 0 deletions website/docs/routers/cloudformation-router.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
---
title: cloudformation-router
---

This handler can route to requests to one of a nested handler based on `requestType` of a CloudFormation Custom Response event.

## Install

To install this middleware you can use NPM:

```bash
npm install --save @middy/cloudformation-router
```

## Options

- `routes` (array[\{routeKey, handler\}]) (required): Array of route objects.
- `routeKey` (string) (required): AWS formatted request type. ie `Create`, `Update`, `Delete`
- `handler` (function) (required): Any `handler(event, context, {signal})` function
- `notFoundHandler` (function): Override default error thrown with your own custom response. Passes in `{requestType}`

NOTES:

- Reponse parameters are automatically applied for `Status`, `RequestId`, `LogicalResourceId`, and/or `StackId` when not present.
- Errors should be handled as part of the router middleware stack **or** the lambdaHandler middleware stack. Handled errors in the later will trigger the `after` middleware stack of the former.
- Shared middlewares, connected to the router middleware stack, can only be run before the lambdaHandler middleware stack.

## Sample usage

```javascript
import middy from '@middy/core'
import cloudformationRouterHandler from '@middy/cloudformation-router'
import validatorMiddleware from '@middy/validator'

const createHandler = middy()
.use(validatorMiddleware({eventSchema: {...} }))
.handler((event, context) => {
return {
PhysicalResourceId: '...',
Data:{}
}
})

const updateHandler = middy()
.use(validatorMiddleware({eventSchema: {...} }))
.handler((event, context) => {
return {
PhysicalResourceId: '...',
Data: {}
}
})

const deleteHandler = middy()
.use(validatorMiddleware({eventSchema: {...} }))
.handler((event, context) => {
return {
PhysicalResourceId: '...'
}
})

const routes = [
{
requesType: 'Create',
handler: createHandler
},
{
requesType: 'Update',
handler: updateHandler
},
{
routeKey: 'delete',
willfarrell marked this conversation as resolved.
Show resolved Hide resolved
handler: deleteHandler
}
]

export const handler = middy()
.use(wsResponseMiddleware())
.handler(cloudformationRouterHandler(routes))
```
Loading