cf-custom-resources/lib/trigger-state-machine.js (95 lines of code) (raw):
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
"use strict";
const{ SFN, StartSyncExecutionCommand } = require('@aws-sdk/client-sfn');
/**
* Main handler, invoked by Lambda
*/
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 sf = new SFN();
const res = await sf.send(new StartSyncExecutionCommand({
stateMachineArn: event.ResourceProperties.StateMachineARN,
}));
// Even if the execution starts and does not throw an error it does not mean the execution was successful.
// See https://docs.aws.amazon.com/step-functions/latest/apireference/API_StartSyncExecution.html#StepFunctions-StartSyncExecution-response-status
if (res.status !== "SUCCEEDED") {
throw new Error(`State machine failed: ${res.cause}`);
}
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})`
);
}
};
let deadlineExpired = () => {
return new Promise((resolve, reject) => {
setTimeout(
reject,
14 * 60 * 1000 /* 14 minutes */,
new Error("Lambda took longer than 14 minutes")
);
});
};
/**
* 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");
});
};
/**
* @private
* withDeadlineExpired overrides the default deadlineExpired function.
* Used for testing.
*/
exports.withDeadlineExpired = fn => {
deadlineExpired = fn;
};