UNPKG

@adpt/cloud

Version:
325 lines 12.4 kB
"use strict"; /* * Copyright 2018-2019 Unbounded Systems, LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ Object.defineProperty(exports, "__esModule", { value: true }); const tslib_1 = require("tslib"); const core_1 = tslib_1.__importStar(require("@adpt/core")); const utils_1 = require("@adpt/utils"); const lodash_1 = require("lodash"); const aws_sdk_1 = tslib_1.__importDefault(require("./aws-sdk")); const CFResource_1 = require("./CFResource"); const CFStack_1 = require("./CFStack"); const plugin_utils_1 = require("./plugin_utils"); var TemplateFormatVersion; (function (TemplateFormatVersion) { TemplateFormatVersion["current"] = "2010-09-09"; })(TemplateFormatVersion = exports.TemplateFormatVersion || (exports.TemplateFormatVersion = {})); function cfLogicalRef(handle) { return { Ref: plugin_utils_1.adaptResourceId(handle) }; } function addAdaptDeployId(input, deployID) { plugin_utils_1.addTag(input, plugin_utils_1.adaptDeployIdTag, deployID); } function getAdaptDeployId(stack) { return plugin_utils_1.getTag(stack, plugin_utils_1.adaptDeployIdTag); } exports.getAdaptDeployId = getAdaptDeployId; function addAdaptStackId(input, id) { plugin_utils_1.addTag(input, plugin_utils_1.adaptStackIdTag, id); } function getAdaptStackId(stack) { return plugin_utils_1.getTag(stack, plugin_utils_1.adaptStackIdTag); } exports.getAdaptStackId = getAdaptStackId; function addAdaptResourceId(input, id) { plugin_utils_1.addTag(input, plugin_utils_1.adaptResourceIdTag, id); } function getAdaptResourceId(item) { return plugin_utils_1.getTag(item, plugin_utils_1.adaptResourceIdTag); } exports.getAdaptResourceId = getAdaptResourceId; function isStatusActive(status) { switch (status) { case "CREATE_FAILED": case "DELETE_IN_PROGRESS": case "DELETE_COMPLETE": return false; default: return true; } } exports.isStatusActive = isStatusActive; function isStackActive(stack) { return isStatusActive(stack.StackStatus); } exports.isStackActive = isStackActive; // Exported for testing function createTemplate(stackEl) { const template = { AWSTemplateFormatVersion: TemplateFormatVersion.current, Resources: {}, }; const resources = findResourceElems(stackEl); for (const r of resources) { const resourceId = plugin_utils_1.adaptResourceId(r); // Don't modify the element's props. Clone. const properties = Object.assign({}, r.props.Properties); for (const k of Object.keys(properties)) { if (core_1.isHandle(properties[k])) { properties[k] = cfLogicalRef(properties[k]); } } if (!r.props.tagsUnsupported) { // Don't modify the tags on the element either properties.Tags = properties.Tags ? properties.Tags.slice() : []; addAdaptResourceId(properties, resourceId); } template.Resources[resourceId] = { Type: r.props.Type, Properties: properties, }; } return template; } exports.createTemplate = createTemplate; function toTemplateBody(template) { return JSON.stringify(template, null, 2); } function queryDomain(stackEl) { const creds = stackEl.props.awsCredentials; if (creds == null) throw new Error(`Required AWS credentials not set`); const id = { region: creds.awsRegion, accessKeyId: creds.awsAccessKeyId, }; const secret = { awsSecretAccessKey: creds.awsSecretAccessKey, }; return { id, secret }; } function adaptStackId(el) { return plugin_utils_1.adaptIdFromElem("CFStack", el); } function findResourceElems(dom) { const rules = core_1.default.createElement(core_1.Style, null, CFResource_1.CFResourcePrimitive, " ", core_1.default.rule()); const candidateElems = core_1.findElementsInDom(rules, dom); return lodash_1.compact(candidateElems.map((e) => CFResource_1.isCFResourcePrimitiveElement(e) ? e : null)); } function findStackElems(dom) { const rules = core_1.default.createElement(core_1.Style, null, CFStack_1.CFStackPrimitive, " ", core_1.default.rule()); const candidateElems = core_1.findElementsInDom(rules, dom); return lodash_1.compact(candidateElems.map((e) => CFStack_1.isCFStackPrimitiveFinalElement(e) ? e : null)); } exports.findStackElems = findStackElems; function stacksWithDeployID(stacks, deployID) { if (stacks == null) return []; return stacks.filter((s) => (getAdaptDeployId(s) === deployID)); } exports.stacksWithDeployID = stacksWithDeployID; const createDefaults = { Capabilities: [], NotificationARNs: [], Parameters: [], Tags: [], RollbackConfiguration: {}, }; /** * Given a CFStackPrimitiveElement, creates a representation of the stack * that can be given to the client to create the stack. */ function createStackParams(el, deployID) { const _a = el.props, { handle, key, awsCredentials, children } = _a, params = tslib_1.__rest(_a, ["handle", "key", "awsCredentials", "children"]); addAdaptDeployId(params, deployID); addAdaptStackId(params, adaptStackId(el)); params.TemplateBody = toTemplateBody(createTemplate(el)); return Object.assign({}, createDefaults, params); } exports.createStackParams = createStackParams; const modifyProps = [ "Capabilities", "Description", "EnableTerminationProtection", "NotificationARNs", "Parameters", "RoleARN", "RollbackConfiguration", "Tags", "TemplateBody", ]; const replaceProps = [ "StackName", ]; function areEqual(expected, actual, propsToCompare) { const exp = lodash_1.pick(expected, propsToCompare); const act = lodash_1.pick(actual, propsToCompare); return utils_1.isEqualUnorderedArrays(exp, act); } function computeStackChanges(change, actual, deployID) { const { to, from } = change; const getElems = () => { const els = []; const root = to || from || null; if (root) els.push(root); return els.concat(findResourceElems(root)); }; // TODO: Ask AWS for detail on resource changes via change set API const actionInfo = (type, detail, elDetailTempl = detail) => ({ type, detail, changes: getElems().map((element) => { const elDetail = element.componentType === CFStack_1.CFStackPrimitive ? detail : elDetailTempl.replace("{TYPE}", element.props.Type || "resource"); return { type, element, detail: elDetail, }; }) }); if (from == null && to == null) { return actionInfo(core_1.ChangeType.delete, "Destroying unrecognized CFStack"); } if (to == null) { return actual ? actionInfo(core_1.ChangeType.delete, "Destroying CFStack", "Destroying {TYPE} due to CFStack deletion") : actionInfo(core_1.ChangeType.none, "No changes required"); } if (actual == null) { return actionInfo(core_1.ChangeType.create, "Creating CFStack", "Creating {TYPE}"); } const expected = createStackParams(to, deployID); // Ugh. Special case. OnFailure doesn't show up in describeStacks output, // but instead transforms into DisableRollback. const onFailure = expected.OnFailure; switch (onFailure) { case "DO_NOTHING": expected.DisableRollback = true; break; case "DELETE": case "ROLLBACK": expected.DisableRollback = false; break; } if (!areEqual(expected, actual, replaceProps)) { return actionInfo(core_1.ChangeType.replace, "Replacing CFStack", "Replacing {TYPE} due to replacing CFStack"); } if (!areEqual(expected, actual, modifyProps)) { // TODO: Because we're modifying the stack, each resource within the // stack could be created, deleted, updated, or replaced...we must // ask the AWS API to know. return actionInfo(core_1.ChangeType.modify, "Modifying CFStack", "Resource {TYPE} may be affected by CFStack modification"); } return actionInfo(core_1.ChangeType.none, "No changes required"); } exports.computeStackChanges = computeStackChanges; // Exported for testing class AwsPluginImpl extends core_1.WidgetPlugin { constructor() { super(...arguments); this.findElems = (dom) => { return findStackElems(dom); }; this.getElemQueryDomain = (el) => { return queryDomain(el); }; this.getWidgetTypeFromObs = (_obs) => { return "CloudFormation Stack"; }; this.getWidgetIdFromObs = (obs) => { return getAdaptStackId(obs) || obs.StackId || obs.StackName; }; this.getWidgetTypeFromElem = (_el) => { return "CloudFormation Stack"; }; this.getWidgetIdFromElem = (el) => { return adaptStackId(el); }; this.computeChanges = (change, obs) => { return computeStackChanges(change, obs, this.deployID); }; this.getObservations = async (domain, deployID) => { const client = this.getClient(domain); const resp = await client.describeStacks().promise(); const stacks = stacksWithDeployID(resp.Stacks, deployID) .filter((stk) => isStackActive(stk)); let s; for (s of stacks) { const r = await client.getTemplate({ StackName: s.StackId || s.StackName }).promise(); s.TemplateBody = r.TemplateBody; } return stacks; }; this.createWidget = async (domain, deployID, resource) => { const el = resource.element; if (!el) throw new Error(`resource element null`); const params = createStackParams(el, deployID); const client = this.getClient(domain); await client.createStack(params).promise(); }; this.destroyWidget = async (domain, _deployID, resource) => { const stackName = resource.observed && (resource.observed.StackId || resource.observed.StackName); if (!stackName) throw new Error(`Unable to delete stack that doesn't exist`); const client = this.getClient(domain); await client.deleteStack({ StackName: stackName }).promise(); }; this.modifyWidget = async (domain, deployID, resource) => { const stackName = resource.observed && (resource.observed.StackId || resource.observed.StackName); if (!stackName) throw new Error(`Unable to update stack that doesn't exist`); const el = resource.element; if (!el) throw new Error(`resource element null`); const updateable = lodash_1.pick(createStackParams(el, deployID), modifyProps); // tslint:disable-next-line:no-object-literal-type-assertion const params = Object.assign({ StackName: stackName }, updateable); const client = this.getClient(domain); await client.updateStack(params).promise(); }; } getClient(domain) { // TODO(mark): Cache a client for each domain. return new aws_sdk_1.default.CloudFormation({ region: domain.id.region, accessKeyId: domain.id.accessKeyId, secretAccessKey: domain.secret.awsSecretAccessKey, }); } } exports.AwsPluginImpl = AwsPluginImpl; // Exported for testing function createAwsPlugin() { return new AwsPluginImpl(); } exports.createAwsPlugin = createAwsPlugin; core_1.registerPlugin({ name: "aws", module, create: createAwsPlugin, }); //# sourceMappingURL=aws_plugin.js.map