UNPKG

serverless-ssm-publish

Version:

Serverless Framework plugin to publish data to AWS SSM Parameter Store

394 lines 22.9 kB
"use strict"; var __assign = (this && this.__assign) || function () { __assign = Object.assign || function(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; return __assign.apply(this, arguments); }; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 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) : adopt(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 = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); return g.next = verb(0), g["throw"] = verb(1), g["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 (g && (g = 0, op[0] && (_ = 0)), _) try { if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; if (y = 0, t) op = [op[0] & 2, 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 }; } }; var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { if (ar || !(i in from)) { if (!ar) ar = Array.prototype.slice.call(from, 0, i); ar[i] = from[i]; } } return to.concat(ar || Array.prototype.slice.call(from)); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); var chalk_1 = __importDefault(require("chalk")); var js_yaml_1 = __importDefault(require("js-yaml")); var markdown_table_1 = __importDefault(require("markdown-table")); var util = __importStar(require("util")); var util_1 = require("./util"); var unsupportedRegionPrefixes = [ 'ap-east-1', // Hong Kong - region disabled by default 'me-south-1', // Bahrain - region disabled by default ]; var ServerlessSSMPublish = /** @class */ (function () { /** * Constructor * @param serverless */ function ServerlessSSMPublish(serverless) { var _this = this; // SSM Publish internal properties this.initialized = false; this.serverless = serverless; // Bind plugin to aws provider. It will not run on a different provider. this.provider = this.serverless.getProvider('aws'); // Optional this.commands = { ssmPublish: { usage: 'Helps you publish params to SSM', lifecycleEvents: [ 'checkIfParamsExist', 'upsertParams', ], }, }; this.hooks = { // Pre & post hooks 'after:ssmPublish:upsertParams': this.hookWrapper.bind(this, this.summary.bind(this)), // tslint:disable-line:no-unsafe-any // Actual lifecycle event handling 'ssmPublish:checkIfParamsExist': this.hookWrapper.bind(this, this.getAndCheckParams.bind(this)), // tslint:disable-line:no-unsafe-any 'ssmPublish:upsertParams': this.hookWrapper.bind(this, this.updateParams.bind(this)), // tslint:disable-line:no-unsafe-any // Serverless lifecycle events 'after:deploy:deploy': function () { return __awaiter(_this, void 0, void 0, function () { return __generator(this, function (_a) { switch (_a.label) { case 0: // check if this is the best place to call our plugin return [4 /*yield*/, this.hookWrapper.bind(this, this.getAndCheckParams.bind(this))()]; case 1: _a.sent(); // tslint:disable-line:no-unsafe-any return [4 /*yield*/, this.hookWrapper.bind(this, this.updateParams.bind(this))()]; case 2: _a.sent(); // tslint:disable-line:no-unsafe-any this.hookWrapper.bind(this, this.summary.bind(this))(); // tslint:disable-line:no-unsafe-any return [2 /*return*/]; } }); }); }, }; } ServerlessSSMPublish.prettifyCFOutput = function (stackResult) { if (!stackResult.Stacks) return []; var stackOutput = stackResult.Stacks[0].Outputs; if (!stackOutput) return []; var formattedParams = stackOutput // ensure output matches our formatting .map(function (output) { return ({ path: output.OutputKey || '', value: output.OutputValue || '', description: output.Description, }); }); return formattedParams || []; }; /** * Wrapper for lifecycle function, initializes variables and checks if enabled. * @param lifecycleFunc lifecycle function that actually does desired action */ ServerlessSSMPublish.prototype.hookWrapper = function (lifecycleFunc) { return __awaiter(this, void 0, void 0, function () { return __generator(this, function (_a) { switch (_a.label) { case 0: // tslint:disable-line:no-any return [4 /*yield*/, this.initializeVariables()]; case 1: _a.sent(); if (!this.enabled) { this.log('serverless-ssm-publish: SSM Publish is disabled.'); } else { return [2 /*return*/, lifecycleFunc.call(this)]; // tslint:disable-line:no-unsafe-any } return [2 /*return*/]; } }); }); }; /** * Goes through custom ssm publish properties and initializes local variables. */ ServerlessSSMPublish.prototype.initializeVariables = function () { return __awaiter(this, void 0, void 0, function () { var credentials, credentialsWithRegion, yamlDefinedParams, _a, checkIfParamHasValue_1, mergedParams; var _this = this; var _b; return __generator(this, function (_c) { switch (_c.label) { case 0: if (!!this.initialized) return [3 /*break*/, 4]; this.enabled = (0, util_1.evaluateEnabled)(this.serverless.service.custom, this.throwError.bind(this)); // tslint:disable-line:no-unsafe-any if (!this.enabled) return [3 /*break*/, 3]; credentials = this.provider.getCredentials(); // Extract plugin variables this.region = this.serverless.service.provider.region; this.stackName = (_b = this.serverless.service.provider.stackName) !== null && _b !== void 0 ? _b : util.format('%s-%s', this.serverless.service.getServiceName(), this.serverless.getProvider('aws').getStage()); credentialsWithRegion = __assign(__assign({}, credentials), { region: this.region }); this.ssm = new this.provider.sdk.SSM(credentialsWithRegion); // tslint:disable-line:no-unsafe-any yamlDefinedParams = (0, util_1.validateParams)(this.serverless.service.custom.ssmPublish.params, this.throwError.bind(this), this.log.bind(this)) || []; if (!yamlDefinedParams.some(function (param) { return param.source; })) return [3 /*break*/, 2]; this.logIfDebug('Retrieving Cloud Formation Outputs'); _a = this; return [4 /*yield*/, this.retrieveAndFormatCloudFormationOutput()]; case 1: _a.cloudFormationOutput = _c.sent(); _c.label = 2; case 2: checkIfParamHasValue_1 = function (param) { return param.hasOwnProperty('value'); }; mergedParams = yamlDefinedParams.map(function (slsParam) { var _a; if (checkIfParamHasValue_1(slsParam)) return slsParam; var foundCloudFormationParam = (_a = _this.cloudFormationOutput) === null || _a === void 0 ? void 0 : _a.find(function (cloudFormationParam) { return cloudFormationParam.path === slsParam.source; }); if (!foundCloudFormationParam) { _this.throwError("No Cloud Formation Output found for source ".concat(slsParam.source)); throw new Error("No Cloud Formation Output found for source ".concat(slsParam.source)); // Throwing again as typescript won't recognise throwError as throwing } return __assign(__assign({}, slsParam), { value: foundCloudFormationParam.value, description: foundCloudFormationParam.description || slsParam.description }); }); // Put params on this for following logic this.params = __spreadArray([], mergedParams, true); // tslint:disable-line:no-unsafe-any unsupportedRegionPrefixes.forEach(function (unsupportedRegionPrefix) { if (_this.region.startsWith(unsupportedRegionPrefix)) { _this.log(chalk_1.default.bold.yellow("The configured region ".concat(_this.region, " does not support SSM. Plugin disabled"))); _this.enabled = false; } }); _c.label = 3; case 3: this.initialized = true; _c.label = 4; case 4: return [2 /*return*/]; } }); }); }; /** * Checks whether parameters exist in SSM and if they've been changed * Stores arrays of changed/unchanged/new Parameters on class * We need to account for SSM.GetParameters taking a max of 10 names */ ServerlessSSMPublish.prototype.getAndCheckParams = function () { return __awaiter(this, void 0, void 0, function () { var chunkArray, getParameters, paramsToCheck, retrievedParameterArray, foundParams, invalidOrNewParams, _a, nonExistingParams, existingChangedParams, existingUnchangedParams; var _this = this; return __generator(this, function (_b) { switch (_b.label) { case 0: if (!this.params) return [2 /*return*/]; chunkArray = (function (array, arraySize) { var arrayChunks = []; for (var i = 0; i < array.length; i += arraySize) { arrayChunks.push(array.slice(i, arraySize + i)); } return arrayChunks; }); getParameters = function (params) { return __awaiter(_this, void 0, void 0, function () { return __generator(this, function (_a) { return [2 /*return*/, this.ssm.getParameters({ Names: params.map(function (param) { return param.path; }), WithDecryption: true }).promise()]; }); }); }; paramsToCheck = chunkArray(this.params, 10); return [4 /*yield*/, Promise.all(paramsToCheck.map(function (paramGroup) { return __awaiter(_this, void 0, void 0, function () { return __generator(this, function (_a) { return [2 /*return*/, getParameters(paramGroup)]; }); }); }))]; case 1: retrievedParameterArray = _b.sent(); foundParams = retrievedParameterArray.map(function (result) { return result.Parameters; }).reduce(function (acc, curr) { return __spreadArray(__spreadArray([], acc ? acc : [], true), curr ? curr : [], true); }, []); invalidOrNewParams = retrievedParameterArray.map(function (result) { return result.InvalidParameters; }).reduce(function (acc, curr) { return __spreadArray(__spreadArray([], acc ? acc : [], true), curr ? curr : [], true); }, []); if (invalidOrNewParams === null || invalidOrNewParams === void 0 ? void 0 : invalidOrNewParams.length) this.log(chalk_1.default.yellow("New or invalid parameters present:\n\t".concat(invalidOrNewParams.join('\n\t')))); _a = (0, util_1.compareParams)(this.params, foundParams), nonExistingParams = _a.nonExistingParams, existingChangedParams = _a.existingChangedParams, existingUnchangedParams = _a.existingUnchangedParams; this.logIfDebug("New param paths:\n\t".concat(nonExistingParams.map(function (param) { return param.path; }).join('\n\t'))); this.logIfDebug("Changed param paths:\n\t".concat(existingChangedParams.map(function (param) { return param.path; }).join('\n\t'))); this.logIfDebug("Unchanged param paths:\n\t".concat(existingUnchangedParams.map(function (param) { return param.path; }).join('\n\t'))); this.nonExistingParams = nonExistingParams; this.existingChangedParams = existingChangedParams; this.existingUnchangedParams = existingUnchangedParams; return [2 /*return*/]; } }); }); }; /** * Makes putParameter request for all changed/new parameters */ ServerlessSSMPublish.prototype.updateParams = function () { return __awaiter(this, void 0, void 0, function () { var toUpdate, putResults; var _this = this; return __generator(this, function (_a) { switch (_a.label) { case 0: toUpdate = __spreadArray(__spreadArray([], this.nonExistingParams, true), this.existingChangedParams, true); return [4 /*yield*/, Promise.all(toUpdate.map(function (param) { return __awaiter(_this, void 0, void 0, function () { return __generator(this, function (_a) { return [2 /*return*/, this.ssm.putParameter({ Name: param.path, Description: param.description || "Placed by ".concat(this.serverless.service.getServiceName(), " - serverless-ssm-plugin"), Value: typeof param.value === 'string' ? param.value : param.type === 'StringList' && Array.isArray(param.value) && param.value.every(function (item) { return typeof item === 'string'; }) ? param.value.join(',') : js_yaml_1.default.safeDump(param.value), Overwrite: true, Type: param.type ? param.type : param.secure ? 'SecureString' : 'String', }).promise()]; }); }); }))]; case 1: putResults = _a.sent(); this.logIfDebug("SSM Put Results:\n".concat(chalk_1.default.green(putResults.length > 0 ? ((0, markdown_table_1.default)(__spreadArray([ ['Path', 'Secure', 'Version', 'Tier', 'Type'] ], putResults.map(function (_a, i) { var Version = _a.Version, Tier = _a.Tier; return ([ toUpdate[i].path, toUpdate[i].secure, Version ? Version : '', Tier ? Tier : '', toUpdate[i].type, ]); }), true))) : 'No updates performed.'))); return [2 /*return*/]; } }); }); }; ServerlessSSMPublish.prototype.retrieveAndFormatCloudFormationOutput = function () { return __awaiter(this, void 0, void 0, function () { var stackResult; return __generator(this, function (_a) { switch (_a.label) { case 0: return [4 /*yield*/, this.fetchCFOutput()]; case 1: stackResult = _a.sent(); return [2 /*return*/, ServerlessSSMPublish.prettifyCFOutput(stackResult)]; } }); }); }; ServerlessSSMPublish.prototype.fetchCFOutput = function () { return __awaiter(this, void 0, void 0, function () { return __generator(this, function (_a) { return [2 /*return*/, this.serverless.getProvider('aws').request('CloudFormation', 'describeStacks', { StackName: this.stackName }, this.serverless.getProvider('aws').getStage(), this.serverless.getProvider('aws').getRegion())]; }); }); }; /** * Logs message with prefix * @param message message to be printed */ ServerlessSSMPublish.prototype.log = function (message) { this.serverless.cli.log("[serverless-ssm-publish]: ".concat(message)); }; /** * Logs message with prefix if SLS_DEBUG is set * @param message message to be printed */ ServerlessSSMPublish.prototype.logIfDebug = function (message) { if (process.env.SLS_DEBUG) { this.serverless.cli.log("[serverless-ssm-publish]: ".concat(message)); } }; /** * Throws error using Serverless formatting * @param message message to be printed */ ServerlessSSMPublish.prototype.throwError = function (message) { throw new this.serverless.classes.Error("[serverless-ssm-publish]: ".concat(message)); // tslint:disable-line:no-unsafe-any }; ServerlessSSMPublish.prototype.summary = function () { this.log("SSM Publish Summary:\n".concat(chalk_1.default.bold.green((0, markdown_table_1.default)(__spreadArray(__spreadArray(__spreadArray([ [ "Created (".concat(this.nonExistingParams.length, ")"), "Updated (".concat(this.existingChangedParams.length, ")"), "Unchanged (".concat(this.existingUnchangedParams.length, ")"), ] ], this.nonExistingParams.map(function (p) { return ([p.path, '', '']); }), true), this.existingChangedParams.map(function (p) { return (['', p.path, '']); }), true), this.existingUnchangedParams.map(function (p) { return (['', '', p.path]); }), true))))); }; return ServerlessSSMPublish; }()); module.exports = ServerlessSSMPublish; //# sourceMappingURL=index.js.map