@adpt/cloud
Version:
AdaptJS cloud component library
325 lines • 12.4 kB
JavaScript
;
/*
* 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