cf-custom-resources/lib/unique-json-values.js (105 lines of code) (raw):
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
"use strict";
/**
* Upload a CloudFormation response object to S3.
*
* @param {object} event the Lambda event payload received by the handler function
* @param {object} context the Lambda context received by the handler function
* @param {string} responseStatus the response status, either 'SUCCESS' or 'FAILED'
* @param {string} physicalResourceId CloudFormation physical resource ID
* @param {object} [responseData] arbitrary response data object
* @param {string} [reason] reason for failure, if any, to convey to the user
* @returns {Promise} Promise that is resolved on success, or rejected on connection error or HTTP error response
*/
const report = function (
event,
context,
responseStatus,
physicalResourceId,
responseData,
reason
) {
return new Promise((resolve, reject) => {
const https = require("https");
const { URL } = require("url");
let responseBody = JSON.stringify({
Status: responseStatus,
Reason: reason,
PhysicalResourceId: physicalResourceId,
StackId: event.StackId,
RequestId: event.RequestId,
LogicalResourceId: event.LogicalResourceId,
Data: responseData,
});
const parsedUrl = new URL(event.ResponseURL);
const options = {
hostname: parsedUrl.hostname,
port: 443,
path: parsedUrl.pathname + parsedUrl.search,
method: "PUT",
headers: {
"Content-Type": "",
"Content-Length": responseBody.length,
},
};
https
.request(options)
.on("error", reject)
.on("response", (res) => {
res.resume();
if (res.statusCode >= 400) {
reject(new Error(`Error ${res.statusCode}: ${res.statusMessage}`));
} else {
resolve();
}
})
.end(responseBody, "utf8");
});
};
const deadlineExpired = function () {
return new Promise((resolve, reject) => {
setTimeout(
reject,
9 * 60 * 1000 + 30 * 1000 /* 9.5 minutes */,
new Error("Lambda took longer than 9.5 minutes")
);
});
};
/**
* Main handler, invoked by Lambda
*
* The input event.ResourceProperties.Aliases is a map of service name to
* it's alises. For example, it might look like this:
* {
* "svc1": ["svc1.com", "example.com"],
* "svc2": ["example.com"]
* "svc3": ["svc3.com"]
* }
*
* The input event.ResourceProperties.FilterFor is a comma delimated list
* of keys in event.ResourceProperties.Aliases to filter for.
* Taking the above map, and event.ResourceProperties.FilterFor = "svc1,svc2"
* The services considered for uniqueness is:
* {
* "svc1": ["svc1.com", "example.com"],
* "svc2": ["example.com"]
* }
*
* The input event.ResourceProperties.AdditionalStrings is a string slice (e.g., ["foobar.com"]).
*
* This function returns a sorted list of unique values found in the map and the additional string.
* For this example, UniqueValues would be:
* ["example.com", "foobar.com", "svc1.com"]
*/
exports.handler = async function (event, context) {
const responseData = {};
const physicalResourceId =
event.PhysicalResourceId || event.LogicalResourceId;
const handler = async function () {
switch (event.RequestType) {
case "Create":
case "Update":
const aliasesForService = JSON.parse(
event.ResourceProperties.Aliases || "{}"
);
const filterFor = new Set(
event.ResourceProperties.FilterFor.split(",")
);
const filteredAliasesForService = Object.fromEntries(
Object.entries(aliasesForService).filter(([key]) =>
filterFor.has(key)
)
);
const unique = new Set(Object.values(filteredAliasesForService).flat());
if (event.ResourceProperties.AdditionalStrings) {
event.ResourceProperties.AdditionalStrings.forEach((element) => {
if (element) {
unique.add(element);
}
});
}
responseData.UniqueValues = Array.from(unique).sort();
break;
case "Delete":
// Do nothing on delete, since this isn't a "real" resource.
break;
default:
throw new Error(`Unsupported request type ${event.RequestType}`);
}
};
try {
await Promise.race([deadlineExpired(), handler()]);
await report(event, context, "SUCCESS", physicalResourceId, responseData);
} catch (err) {
console.error(`caught error: ${err}`);
await report(
event,
context,
"FAILED",
physicalResourceId,
null,
`${err.message} (Log: ${context.logGroupName}/${context.logStreamName})`
);
}
};