@middy/ssm
Version:
SSM (EC2 Systems Manager) parameters middleware for the middy framework
197 lines (176 loc) • 5.62 kB
JavaScript
import {
canPrefetch,
createPrefetchClient,
createClient,
processCache,
getCache,
modifyCache,
jsonSafeParse,
getInternal,
sanitizeKey,
catchInvalidSignatureException
} from '../util/index.js'
import {
SSMClient,
GetParametersCommand,
GetParametersByPathCommand
} from '@aws-sdk/client-ssm'
const awsRequestLimit = 10
const defaults = {
AwsClient: SSMClient, // Allow for XRay
awsClientOptions: {},
awsClientAssumeRole: undefined,
awsClientCapture: undefined,
fetchData: {}, // { contextKey: fetchKey, contextPrefix: fetchPath/ }
disablePrefetch: false,
cacheKey: 'ssm',
cacheKeyExpiry: {},
cacheExpiry: -1,
setToContext: false
}
const ssmMiddleware = (opts = {}) => {
const options = { ...defaults, ...opts }
const fetchRequest = (request, cachedValues) => {
return {
...fetchSingleRequest(request, cachedValues),
...fetchByPathRequest(request, cachedValues)
}
}
const fetchSingleRequest = (request, cachedValues = {}) => {
const values = {}
let batchReq = null
let batchInternalKeys = []
let batchFetchKeys = []
const namedKeys = []
const internalKeys = Object.keys(options.fetchData)
const fetchKeys = Object.values(options.fetchData)
for (const internalKey of internalKeys) {
if (cachedValues[internalKey]) continue
if (options.fetchData[internalKey].substr(-1) === '/') continue // Skip path passed in
namedKeys.push(internalKey)
}
for (const [idx, internalKey] of namedKeys.entries()) {
const fetchKey = options.fetchData[internalKey]
batchInternalKeys.push(internalKey)
batchFetchKeys.push(fetchKey)
// from the first to the batch size skip, unless it's the last entry
if (
(!idx || (idx + 1) % awsRequestLimit !== 0) &&
!(idx + 1 === namedKeys.length)
) {
continue
}
const command = new GetParametersCommand({
Names: batchFetchKeys,
WithDecryption: true
})
batchReq = client
.send(command)
.catch((e) => catchInvalidSignatureException(e, client, command))
.then((resp) => {
// Don't sanitize key, mapped to set value in options
return Object.assign(
...(resp.InvalidParameters ?? []).map((fetchKey) => {
return {
[fetchKey]: new Promise(() => {
const internalKey = internalKeys[fetchKeys.indexOf(fetchKey)]
const value = getCache(options.cacheKey).value ?? {}
value[internalKey] = undefined
modifyCache(options.cacheKey, value)
throw new Error('InvalidParameter ' + fetchKey, {
cause: { package: '@middy/ssm' }
})
})
}
}),
...(resp.Parameters ?? []).map((param) => {
return { [param.Name]: parseValue(param) }
})
)
})
.catch((e) => {
const value = getCache(options.cacheKey).value ?? {}
value[internalKey] = undefined
modifyCache(options.cacheKey, value)
throw e
})
for (const internalKey of batchInternalKeys) {
values[internalKey] = batchReq.then((params) => {
return params[options.fetchData[internalKey]]
})
}
batchInternalKeys = []
batchFetchKeys = []
batchReq = null
}
return values
}
const fetchByPathRequest = (request, cachedValues = {}) => {
const values = {}
for (const internalKey in options.fetchData) {
if (cachedValues[internalKey]) continue
const fetchKey = options.fetchData[internalKey]
if (fetchKey.substr(-1) !== '/') continue // Skip not path passed in
values[internalKey] = fetchPathRequest(fetchKey).catch((e) => {
const value = getCache(options.cacheKey).value ?? {}
value[internalKey] = undefined
modifyCache(options.cacheKey, value)
throw e
})
}
return values
}
const fetchPathRequest = (path, nextToken, values = {}) => {
const command = new GetParametersByPathCommand({
Path: path,
NextToken: nextToken,
Recursive: true,
WithDecryption: true
})
return client
.send(command)
.catch((e) => catchInvalidSignatureException(e, client, command))
.then((resp) => {
Object.assign(
values,
...resp.Parameters.map((param) => {
return {
[sanitizeKey(param.Name.replace(path, ''))]: parseValue(param)
}
})
)
if (resp.NextToken) { return fetchPathRequest(path, resp.NextToken, values) }
return values
})
}
const parseValue = (param) => {
if (param.Type === 'StringList') {
return param.Value.split(',')
}
return jsonSafeParse(param.Value)
}
let client
if (canPrefetch(options)) {
client = createPrefetchClient(options)
processCache(options, fetchRequest)
}
const ssmMiddlewareBefore = async (request) => {
if (!client) {
client = await createClient(options, request)
}
const { value } = processCache(options, fetchRequest, request)
Object.assign(request.internal, value)
if (options.setToContext) {
const data = await getInternal(Object.keys(options.fetchData), request)
Object.assign(request.context, data)
}
}
return {
before: ssmMiddlewareBefore
}
}
export default ssmMiddleware
// used for TS type inference (see index.d.ts)
export function ssmParam (name) {
return name
}