@middy/ssm
Version:
SSM (EC2 Systems Manager) parameters middleware for the middy framework
199 lines (178 loc) • 5.22 kB
JavaScript
import {
GetParametersByPathCommand,
GetParametersCommand,
SSMClient,
} from "@aws-sdk/client-ssm";
import {
canPrefetch,
catchInvalidSignatureException,
createClient,
createPrefetchClient,
getCache,
getInternal,
jsonSafeParse,
modifyCache,
processCache,
sanitizeKey,
} from "../util/index.js";
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;
}