UNPKG

serverless-offline-sns

Version:

Serverless plugin to run a local SNS server and call lambdas with events notifications.

446 lines (445 loc) 21.5 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __generator = (this && this.__generator) || function (thisArg, body) { var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; function verb(n) { return function (v) { return step([n, v]); }; } function step(op) { if (f) throw new TypeError("Generator is already executing."); while (_) try { if (f = 1, y && (t = y[op[0] & 2 ? "return" : op[0] ? "throw" : "next"]) && !(t = t.call(y, op[1])).done) return t; if (y = 0, t) op = [0, t.value]; switch (op[0]) { case 0: case 1: t = op; break; case 4: _.label++; return { value: op[1], done: false }; case 5: _.label++; y = op[1]; op = [0]; continue; case 7: op = _.ops.pop(); _.trys.pop(); continue; default: if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } if (t[2]) _.ops.pop(); _.trys.pop(); continue; } op = body.call(thisArg, _); } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; } }; Object.defineProperty(exports, "__esModule", { value: true }); var sns_adapter_1 = require("./sns-adapter"); var express = require("express"); var cors = require("cors"); var bodyParser = require("body-parser"); var sns_server_1 = require("./sns-server"); var _ = require("lodash"); var AWS = require("aws-sdk"); var path_1 = require("path"); var helpers_1 = require("./helpers"); var child_process_1 = require("child_process"); var ServerlessOfflineSns = /** @class */ (function () { function ServerlessOfflineSns(serverless, options) { var _this = this; this.app = express(); this.app.use(cors()); this.app.use(function (req, res, next) { // fix for https://github.com/s12v/sns/issues/45 not sending content-type req.headers["content-type"] = req.headers["content-type"] || "text/plain"; next(); }); this.app.use(bodyParser.json({ type: ["application/json", "text/plain"] })); this.options = options; this.serverless = serverless; this.commands = { "offline-sns": { usage: "Listens to offline SNS events and passes them to configured Lambda fns", lifecycleEvents: [ "start", "cleanup", ], commands: { start: { lifecycleEvents: [ "init", "end", ], }, cleanup: { lifecycleEvents: [ "init", ], }, }, }, }; this.hooks = { "before:offline:start": function () { return _this.start(); }, "before:offline:start:init": function () { return _this.start(); }, "after:offline:start:end": function () { return _this.stop(); }, "offline-sns:start:init": function () { _this.start(); return _this.waitForSigint(); }, "offline-sns:cleanup:init": function () { return __awaiter(_this, void 0, void 0, function () { return __generator(this, function (_a) { this.init(); this.setupSnsAdapter(); return [2 /*return*/, this.unsubscribeAll()]; }); }); }, "offline-sns:start:end": function () { return _this.stop(); }, }; } ServerlessOfflineSns.prototype.init = function () { process.env = _.extend({}, this.serverless.service.provider.environment, process.env); this.config = this.serverless.service.custom["serverless-offline-sns"] || {}; this.localPort = this.config.port || this.config.localPort || 4002; this.remotePort = this.config.port || this.config.remotePort || 4002; this.accountId = this.config.accountId || "123456789012"; var offlineConfig = this.serverless.service.custom["serverless-offline"] || {}; this.location = process.cwd(); var locationRelativeToCwd = this.options.location || offlineConfig.location; if (locationRelativeToCwd) { this.location = process.cwd() + "/" + locationRelativeToCwd; } else if (this.serverless.config.servicePath) { this.location = this.serverless.config.servicePath; } if (this.serverless.service.provider.region) { this.region = this.serverless.service.provider.region; } else { this.region = "us-east-1"; } // Congure SNS client to be able to find us. AWS.config.sns = { endpoint: "http://127.0.0.1:" + this.localPort, region: this.region, }; }; ServerlessOfflineSns.prototype.start = function () { return __awaiter(this, void 0, void 0, function () { return __generator(this, function (_a) { switch (_a.label) { case 0: this.init(); return [4 /*yield*/, this.listen()]; case 1: _a.sent(); return [4 /*yield*/, this.serve()]; case 2: _a.sent(); return [4 /*yield*/, this.subscribeAll()]; case 3: _a.sent(); return [2 /*return*/, this.snsAdapter]; } }); }); }; ServerlessOfflineSns.prototype.waitForSigint = function () { return __awaiter(this, void 0, void 0, function () { var _this = this; return __generator(this, function (_a) { return [2 /*return*/, new Promise(function (res) { process.on("SIGINT", function () { _this.log("Halting offline-sns server"); res(); }); })]; }); }); }; ServerlessOfflineSns.prototype.serve = function () { return __awaiter(this, void 0, void 0, function () { var _this = this; return __generator(this, function (_a) { this.snsServer = new sns_server_1.SNSServer(function (msg, ctx) { return _this.debug(msg, ctx); }, this.app, this.region, this.accountId); return [2 /*return*/]; }); }); }; ServerlessOfflineSns.prototype.subscribeAll = function () { return __awaiter(this, void 0, void 0, function () { var _this = this; return __generator(this, function (_a) { switch (_a.label) { case 0: this.setupSnsAdapter(); return [4 /*yield*/, this.unsubscribeAll()]; case 1: _a.sent(); this.debug("subscribing"); return [4 /*yield*/, Promise.all(Object.keys(this.serverless.service.functions).map(function (fnName) { var fn = _this.serverless.service.functions[fnName]; return Promise.all(fn.events.filter(function (event) { return event.sns != null; }).map(function (event) { return _this.subscribe(fnName, event.sns); })); }))]; case 2: _a.sent(); return [2 /*return*/]; } }); }); }; ServerlessOfflineSns.prototype.unsubscribeAll = function () { return __awaiter(this, void 0, void 0, function () { var _this = this; var subs; return __generator(this, function (_a) { switch (_a.label) { case 0: return [4 /*yield*/, this.snsAdapter.listSubscriptions()]; case 1: subs = _a.sent(); this.debug("subs!: " + JSON.stringify(subs)); return [4 /*yield*/, Promise.all(subs.Subscriptions .filter(function (sub) { return sub.Endpoint.indexOf(":" + _this.remotePort) > -1; }) .filter(function (sub) { return sub.SubscriptionArn !== "PendingConfirmation"; }) .map(function (sub) { return _this.snsAdapter.unsubscribe(sub.SubscriptionArn); }))]; case 2: _a.sent(); return [2 /*return*/]; } }); }); }; ServerlessOfflineSns.prototype.subscribe = function (fnName, snsConfig) { return __awaiter(this, void 0, void 0, function () { var fn, topicName, data; return __generator(this, function (_a) { switch (_a.label) { case 0: this.debug("subscribe: " + fnName); fn = this.serverless.service.functions[fnName]; topicName = ""; // https://serverless.com/framework/docs/providers/aws/events/sns#using-a-pre-existing-topic if (typeof snsConfig === "string") { if (snsConfig.indexOf("arn:aws:sns") === 0) { topicName = helpers_1.topicNameFromArn(snsConfig); } else { topicName = snsConfig; } } else if (snsConfig.topicName && typeof snsConfig.topicName === "string") { topicName = snsConfig.topicName; } else if (snsConfig.arn && typeof snsConfig.arn === "string") { topicName = helpers_1.topicNameFromArn(snsConfig.arn); } if (!topicName) { this.log("Unable to create topic for \"" + fnName + "\". Please ensure the sns configuration is correct."); return [2 /*return*/, Promise.resolve("Unable to create topic for \"" + fnName + "\". Please ensure the sns configuration is correct.")]; } this.log("Creating topic: \"" + topicName + "\" for fn \"" + fnName + "\""); return [4 /*yield*/, this.snsAdapter.createTopic(topicName)]; case 1: data = _a.sent(); this.debug("topic: " + JSON.stringify(data)); return [4 /*yield*/, this.snsAdapter.subscribe(fn, this.createHandler(fnName, fn), data.TopicArn, snsConfig)]; case 2: _a.sent(); return [2 /*return*/]; } }); }); }; ServerlessOfflineSns.prototype.createHandler = function (fnName, fn) { if (!fn.runtime || fn.runtime.startsWith("nodejs")) { return this.createJavascriptHandler(fn); } else { return this.createProxyHandler(fnName, fn); } }; ServerlessOfflineSns.prototype.createProxyHandler = function (funName, funOptions) { var options = this.options; return function (event, context) { var args = ["invoke", "local", "-f", funName]; var stage = options.s || options.stage; if (stage) { args.push("-s", stage); } // Use path to binary if provided, otherwise assume globally-installed var binPath = options.b || options.binPath; var cmd = binPath || "sls"; var process = child_process_1.spawn(cmd, args, { cwd: funOptions.servicePath, shell: true, stdio: ["pipe", "pipe", "pipe"], }); process.stdin.write(JSON.stringify(event) + "\n"); process.stdin.end(); var results = []; var error = false; process.stdout.on("data", function (data) { if (data) { var str = data.toString(); if (str) { // should we check the debug flag & only log if debug is true? console.log(str); results.push(data.toString()); } } }); process.stderr.on("data", function (data) { error = true; console.warn("error", data); context.fail(data); }); process.on("close", function (code) { if (!error) { // try to parse to json // valid result should be a json array | object // technically a string is valid json // but everything comes back as a string // so we can't reliably detect json primitives with this method var response = null; // we go end to start because the one we want should be last // or next to last for (var i = results.length - 1; i >= 0; i--) { // now we need to find the min | max [] or {} within the string // if both exist then we need the outer one. // { "something": [] } is valid, // [{"something": "valid"}] is also valid // *NOTE* Doesn't currently support 2 separate valid json bundles // within a single result. // this can happen if you use a python logger // and then do log.warn(json.dumps({'stuff': 'here'})) var item = results[i]; var firstCurly = item.indexOf("{"); var firstSquare = item.indexOf("["); var start = 0; var end = item.length; if (firstCurly === -1 && firstSquare === -1) { // no json found continue; } if (firstSquare === -1 || firstCurly < firstSquare) { // found an object start = firstCurly; end = item.lastIndexOf("}") + 1; } else if (firstCurly === -1 || firstSquare < firstCurly) { // found an array start = firstSquare; end = item.lastIndexOf("]") + 1; } try { response = JSON.parse(item.substring(start, end)); break; } catch (err) { // not json, check the next one continue; } } if (response !== null) { context.succeed(response); } else { context.succeed(results.join("\n")); } } }); }; }; ServerlessOfflineSns.prototype.createJavascriptHandler = function (fn) { var _this = this; return function () { // Options are passed from the command line in the options parameter // ### OLD: use the main serverless config since this behavior is already supported there if (!_this.options.skipCacheInvalidation || Array.isArray(_this.options.skipCacheInvalidation)) { var _loop_1 = function (key) { // don't invalidate cached modules from node_modules ... if (key.match(/node_modules/)) { return "continue"; } // if an array is provided to the serverless config, check the entries there too if (Array.isArray(_this.options.skipCacheInvalidation) && _this.options.skipCacheInvalidation.find(function (pattern) { return new RegExp(pattern).test(key); })) { return "continue"; } delete require.cache[key]; }; for (var key in require.cache) { _loop_1(key); } } _this.debug(process.cwd()); var handlerFnNameIndex = fn.handler.lastIndexOf("."); var handlerPath = fn.handler.substring(0, handlerFnNameIndex); var handlerFnName = fn.handler.substring(handlerFnNameIndex + 1); var fullHandlerPath = path_1.resolve(_this.location, handlerPath); _this.debug("require(" + fullHandlerPath + ")[" + handlerFnName + "]"); var handler = require(fullHandlerPath)[handlerFnName]; return handler; }; }; ServerlessOfflineSns.prototype.log = function (msg, prefix) { if (prefix === void 0) { prefix = "INFO[serverless-offline-sns]: "; } this.serverless.cli.log.call(this.serverless.cli, prefix + msg); }; ServerlessOfflineSns.prototype.debug = function (msg, context) { if (this.config.debug) { if (context) { this.log(msg, "DEBUG[serverless-offline-sns][" + context + "]: "); } else { this.log(msg, "DEBUG[serverless-offline-sns]: "); } } }; ServerlessOfflineSns.prototype.listen = function () { return __awaiter(this, void 0, void 0, function () { var _this = this; var host; return __generator(this, function (_a) { this.debug("starting plugin"); host = "127.0.0.1"; if (this.config.host) { this.debug("using specified host " + this.config.host); host = this.config.host; } else if (this.options.host) { this.debug("using offline specified host " + this.options.host); host = this.options.host; } return [2 /*return*/, new Promise(function (res) { _this.server = _this.app.listen(_this.localPort, host, function () { _this.debug("listening on " + host + ":" + _this.localPort); res(); }); _this.server.setTimeout(0); })]; }); }); }; ServerlessOfflineSns.prototype.stop = function () { return __awaiter(this, void 0, void 0, function () { return __generator(this, function (_a) { this.init(); this.debug("stopping plugin"); if (this.server) { this.server.close(); } return [2 /*return*/]; }); }); }; ServerlessOfflineSns.prototype.setupSnsAdapter = function () { var _this = this; this.snsAdapter = new sns_adapter_1.SNSAdapter(this.localPort, this.remotePort, this.serverless.service.provider.region, this.config["sns-endpoint"], function (msg, ctx) { return _this.debug(msg, ctx); }, this.app, this.serverless.service.service, this.serverless.service.provider.stage, this.accountId, this.config.host, this.config["sns-subscribe-endpoint"]); }; return ServerlessOfflineSns; }()); module.exports = ServerlessOfflineSns;