UNPKG

@middy/ssm

Version:

SSM (EC2 Systems Manager) parameters middleware for the middy framework

237 lines (212 loc) 6.53 kB
// Copyright 2017 - 2026 will Farrell, Luciano Mammino, and Middy contributors. // SPDX-License-Identifier: MIT import { GetParametersByPathCommand, GetParametersCommand, SSMClient, } from "@aws-sdk/client-ssm"; import { assignSetToContext, buildSetToContextSpec, canPrefetch, catchInvalidSignatureException, createClient, createPrefetchClient, getCache, jsonSafeParse, modifyCache, processCache, sanitizeKey, validateOptions, } from "@middy/util"; const name = "ssm"; const pkg = `@middy/${name}`; const defaults = { AwsClient: SSMClient, // Allow for XRay awsClientOptions: {}, awsClientAssumeRole: undefined, awsClientCapture: undefined, fetchData: {}, // { contextKey: fetchKey, contextPrefix: fetchPath/ } disablePrefetch: false, cacheKey: pkg, cacheKeyExpiry: {}, cacheExpiry: -1, setToContext: false, awsRequestLimit: 10, }; const optionSchema = { type: "object", properties: { AwsClient: { instanceof: "Function" }, awsClientOptions: { type: "object" }, awsClientAssumeRole: { type: "string" }, awsClientCapture: { instanceof: "Function" }, fetchData: { type: "object", additionalProperties: { type: "string" }, }, disablePrefetch: { type: "boolean" }, cacheKey: { type: "string" }, cacheKeyExpiry: { type: "object", additionalProperties: { type: "number", minimum: -1 }, }, cacheExpiry: { type: "number", minimum: -1 }, setToContext: { type: "boolean" }, awsRequestLimit: { type: "integer", minimum: 1, maximum: 10 }, }, additionalProperties: false, }; export const ssmValidateOptions = (options) => validateOptions(pkg, optionSchema, options); const ssmMiddleware = (opts = {}) => { const options = { ...defaults, ...opts }; const fetchDataKeys = Object.keys(options.fetchData); const fetchDataValues = Object.values(options.fetchData); const contextSpec = buildSetToContextSpec(options); const fetchRequest = (request, cachedValues) => { const single = fetchSingleRequest(request, cachedValues); const path = fetchByPathRequest(request, cachedValues); return Object.assign(single, path); }; const fetchSingleRequest = (request, cachedValues = {}) => { const values = {}; let batchReq = null; const batchKeys = new Map(); const namedKeys = []; const internalKeys = fetchDataKeys; const fetchKeys = fetchDataValues; for (const internalKey of internalKeys) { if (cachedValues[internalKey]) continue; if (options.fetchData[internalKey].endsWith("/")) continue; // Skip path passed in namedKeys.push(internalKey); } for (const [idx, internalKey] of namedKeys.entries()) { const fetchKey = options.fetchData[internalKey]; batchKeys.set(internalKey, fetchKey); // from the first to the batch size skip, unless it's the last entry if ( (!idx || (idx + 1) % options.awsRequestLimit !== 0) && !(idx + 1 === namedKeys.length) ) { continue; } const command = new GetParametersCommand({ Names: Array.from(batchKeys.values()), WithDecryption: true, }); const currentBatchInternalKeys = Array.from(batchKeys.keys()); batchReq = client .send(command) .catch((e) => catchInvalidSignatureException(e, client, command)) .then((resp) => { // Don't sanitize key, mapped to set value in options const result = {}; for (const fetchKey of resp.InvalidParameters ?? []) { const internalKey = internalKeys[fetchKeys.indexOf(fetchKey)]; const value = getCache(options.cacheKey).value ?? {}; value[internalKey] = undefined; modifyCache(options.cacheKey, value); result[fetchKey] = Promise.reject( new Error(`InvalidParameter ${fetchKey}`, { cause: { package: pkg }, }), ); } for (const param of resp.Parameters ?? []) { result[param.Name] = parseValue(param); } return result; }) .catch((e) => { const value = getCache(options.cacheKey).value ?? {}; for (const key of currentBatchInternalKeys) { value[key] = undefined; } modifyCache(options.cacheKey, value); throw e; }); for (const [internalKey, fetchKey] of batchKeys.entries()) { values[internalKey] = batchReq.then((params) => { if (fetchKey.startsWith("arn:aws:ssm:")) { const matchingParamName = Object.keys(params).find((key) => fetchKey.endsWith(`:parameter${key}`), ); return params[matchingParamName]; } return params[options.fetchData[internalKey]]; }); } batchKeys.clear(); batchReq = null; } return values; }; const fetchByPathRequest = (request, cachedValues = {}) => { const values = {}; for (const internalKey of fetchDataKeys) { if (cachedValues[internalKey]) continue; const fetchKey = options.fetchData[internalKey]; if (!fetchKey.endsWith("/")) 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) => { for (const param of resp.Parameters ?? []) { values[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; let clientInit; if (canPrefetch(options)) { client = createPrefetchClient(options); processCache(options, fetchRequest); } const ssmMiddlewareBefore = async (request) => { if (!client) { clientInit ??= createClient(options, request); client = await clientInit; } const { value } = processCache(options, fetchRequest, request); Object.assign(request.internal, value); if (contextSpec) { const pending = assignSetToContext(contextSpec, value, request); if (pending) await pending; } }; return { before: ssmMiddlewareBefore, }; }; export default ssmMiddleware; // used for TS type inference (see index.d.ts) export function ssmParam(name) { return name; }