cf-custom-resources/lib/alb-rule-priority-generator.js (164 lines of code) (raw):

// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 "use strict"; const { ElasticLoadBalancingV2, DescribeRulesCommand} = require("@aws-sdk/client-elastic-load-balancing-v2"); // minPriorityForRootRule is the min priority number for the root path "/". const minPriorityForRootRule = 48000; // maxPriorityForRootRule is the max priority number for the root path "/". const maxPriorityForRootRule = 50000; // These are used for test purposes only let defaultResponseURL; let defaultLogGroup; let defaultLogStream; /** * 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 */ let 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 || context.logStreamName, StackId: event.StackId, RequestId: event.RequestId, LogicalResourceId: event.LogicalResourceId, Data: responseData, }); const parsedUrl = new URL(event.ResponseURL || defaultResponseURL); 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"); }); }; /** * Lists all the existing rules for an ALB Listener, finds the max of their * priorities, and then returns max + 1. * * @param {string} listenerArn the ARN of the ALB listener. * @returns {number} The next available ALB listener rule priority. */ const calculateNextRulePriority = async function (listenerArn) { let rules = await getListenerRules(listenerArn); let nextRulePriority = 1; if (rules.length > 0) { // Take the max rule priority, and add 1 to it. const rulePriorities = rules.map((rule) => { if ( rule.Priority === "default" || rule.Priority >= minPriorityForRootRule ) { // Ignore the root rule's priority. // Ignore the default rule's priority since it's the same as 0. return 0; } return parseInt(rule.Priority); }); nextRulePriority = Math.max(...rulePriorities) + 1; } return nextRulePriority; }; /** * Lists all the existing rules for an ALB Listener, finds the min of their root rule * priorities, and then returns min - 1. * * @param {string} listenerArn the ARN of the ALB listener. * @returns {number} The next available ALB listener rule priority. */ const calculateNextRootRulePriority = async function (listenerArn) { let rules = await getListenerRules(listenerArn); let nextRulePriority = maxPriorityForRootRule; if (rules.length > 0) { // We'll start from the max rule priority number for root path so that // it won't override any other rule with the same host header. // Then, take the min rule priority among all the root rule, and decrement it by 1. const rulePriorities = rules.map((rule) => { if ( rule.Priority === "default" || rule.Priority < minPriorityForRootRule ) { // Ignore the root rule's priority. // Ignore the non root rule's priority. return maxPriorityForRootRule + 1; } return parseInt(rule.Priority); }); nextRulePriority = Math.min(...rulePriorities) - 1; } return nextRulePriority; }; const getListenerRules = async function (listenerArn) { let elb = new ElasticLoadBalancingV2(); // Grab all the rules for this listener let marker; let rules = []; do { const rulesResponse = await elb .send(new DescribeRulesCommand({ ListenerArn: listenerArn, Marker: marker, })); rules = rules.concat(rulesResponse.Rules); marker = rulesResponse.NextMarker; } while (marker); return rules; }; /** * Next Available ALB Rule Priority handler, invoked by Lambda */ exports.nextAvailableRulePriorityHandler = async function (event, context) { let responseData = {}; let nextRootRuleNumber, nextNonRootRuleNumber; const physicalResourceId = event.PhysicalResourceId || `alb-rule-priority-${event.LogicalResourceId}`; try { switch (event.RequestType) { case "Create": case "Update": for (let i=0; i < event.ResourceProperties.RulePath.length; i++){ if (event.ResourceProperties.RulePath[i] === "/") { if (nextRootRuleNumber == null){ nextRootRuleNumber = await calculateNextRootRulePriority( event.ResourceProperties.ListenerArn ); } if (i == 0) { responseData["Priority"] = nextRootRuleNumber--; } else { responseData["Priority"+i] = nextRootRuleNumber--; } } else { if (nextNonRootRuleNumber == null) { nextNonRootRuleNumber = await calculateNextRulePriority( event.ResourceProperties.ListenerArn ); } if (i == 0) { responseData["Priority"] = nextNonRootRuleNumber++; } else { responseData["Priority"+i] = nextNonRootRuleNumber++; } } } break; // Do nothing on delete, since this isn't a "real" resource. case "Delete": break; default: throw new Error(`Unsupported request type ${event.RequestType}`); } await report(event, context, "SUCCESS", physicalResourceId, responseData); } catch (err) { console.log(`Caught error ${err}.`); await report( event, context, "FAILED", physicalResourceId, null, `${err.message} (Log: ${defaultLogGroup || context.logGroupName}/${ defaultLogStream || context.logStreamName })` ); } }; /** * @private */ exports.withDefaultResponseURL = function (url) { defaultResponseURL = url; }; /** * @private */ exports.withDefaultLogStream = function (logStream) { defaultLogStream = logStream; }; /** * @private */ exports.withDefaultLogGroup = function (logGroup) { defaultLogGroup = logGroup; };