/**
* @overview provides methods for accessing and updating perspectives via CloudHealth's API
* @see {@link https://github.com/cloudhealth/cht_api_guide/blob/master/perspectives_api.md|cht_api_guide}
* @author Ben Watson <ben.watson@coxautoinc.com>
*/
var https = require('https');
var utils = require('../utils/chapi.js');
/**
* @class Perspective
* @memberof module:cox-chapi
* @param {string} [api_key]
*/
var Perspective = function(api_key) {
this.set_api_key(api_key);
}
/**
* sets the api key to use when making calls to CloudHealth's API
* @function module:cox-chapi.Perspective#set_api_key
* @param {string} api_key
*/
Perspective.prototype.set_api_key = function(api_key) {
this._api_key = api_key;
}
/**
* creates and returns an options object that can be given to utils.send_request
* @private
* @param {string} method - the method to use (ie. GET, POST, etc.)
* @param {string} [path] - a string to append to the path field of options
* @param {string[]} [params] - an array of parameters to add to the url in the
* form "key=value"
* @return {object} an options object
*/
Perspective.prototype._options = function(method, path, params) {
return utils._options('/v1/perspective_schemas', method, path, params, this._api_key);
};
/**
* gets a JSON object containing data for a perspective
* @function module:cox-chapi.Perspective#get
* @param {object} [flags] - an optional flags object
* @param {boolean} [flags.cache] - if true, this method will re-use a stored list
* of perspectives from the last time the --cache
* flag wasn't used when looking up ids from names
* @param {number} id - the id of the perspective
* @param {objectCallback} cb - called with an object representing the perspective
*/
Perspective.prototype.get = function(flags, id, cb) {
if (typeof id === 'function') {
cb = id;
id = flags;
flags = null;
}
// ensure id is a string
id = '' + id;
// if "id" is a name, lookup the name
if (!id.match(/^[0-9]+$/)) {
this._lookup_id(flags, id, (err, id) => {
if (err) return cb(err, id);
this.get(id, cb);
});
}
else {
return this._get(flags, id, cb);
}
}
/**
* helper for #get
* @private
*/
Perspective.prototype._get = function(flags, id, cb) {
var options = this._options('GET', '/' + id, ['include_version=true']);
utils.send_request(options, null, this._get_cb.bind(this, flags, id, cb));
};
/**
* Helper callback for #get
* @private
*/
Perspective.prototype._get_cb = function(flags, id, cb, err, result) {
if (err) return cb(err, result);
result = result.schema;
result.id = id;
return cb(null, result);
};
/**
* gets the id of the perspective with the given name
* @function module:cox-chapi.Perspective#_lookup_id
* @private
* @param {object} flags - a flags object
* @param {boolean} [flags.cache] - if true, this method will re-use a stored list
* of perspectives from the last time the --cache
* flag wasn't used
* @param {string} name - the name of the perspective
* @param {stringCallback} cb - called with the perspective id
*/
Perspective.prototype._lookup_id = function(flags, name, cb) {
this.list(flags, (err, data) => {
if (err) return cb(err, data);
var key_index = Object.keys(data).findIndex(
(id) => data[id].name.toLowerCase() === name.toLowerCase()
);
cb(null, Object.keys(data)[key_index]);
});
}
/**
* gets an id for a group
* @function module:cox-chapi.Perspective#_lookup_group_id
* @private
* @param {object} pers - an object representing a perspective
* @param {string} group_name - the name of the group to search for
* @param {stringCallback} cb - called with the group id associated with
* group_name, or group_name if not found
*/
Perspective.prototype._lookup_group_id = function(pers, group_name, cb) {
var groups = this.list_groups(pers, (err, groups) => {
if (err) return cb(err);
// get the group from the list of groups
var group = groups.find(
(group) => group.name.toLowerCase() === group_name.toLowerCase()
);
// return the id, or name if name didn't match any groups
var id = group_name;
if (group) {
id = group.ref_id;
}
cb(null, id);
});
}
/**
* gets an array of groups for a perspective
* @function module:cox-chapi.Perspective#list_groups
* @param {object|string} pers - an object representing a perspective, or
* the perspective's id
* @param {arrayCallback} cb - an array of groups for the perspective
*/
Perspective.prototype.list_groups = function(pers, cb) {
// check if we have an id or the perspective object
if (typeof pers === 'object') {
// get the groups object from the list of constants
var groups = pers.constants.find(
(constant) => constant.type.toLowerCase() === 'group'
);
cb(null, groups.list);
}
else {
// get a perspective object from the id and try again
this.get(pers, (err, perspective) => {
if (err) return cb(err);
this.list_groups(perspective, cb);
});
}
}
/**
* adds an account to a group in a perspective
* @function module:cox-chapi.Perspective#add_to_group
* @param {object|string} pers - an object representing the perspective, or
* the perspective's id
* @param {mixed} accts - the account to add to a group, the account's id, or
* an array of a mixture of those
* @param {string} group_name - the name of the group to add an account to
* @param {objectCallback} cb - called with the updated perspective
*/
Perspective.prototype.add_to_group = function(pers, accts, group_name, cb) {
// if pers is an object, rather than an id
if (typeof pers === 'object') {
// convert acct to an array of account ids to add
if (!Array.isArray(accts)) {
accts = [accts];
}
accts = accts.map(
(acct) => (typeof acct === 'object')? acct.id : acct
);
this._lookup_group_id(pers, group_name, (err, id) => {
if (err) return cb(err);
// get the rule specifying accounts that belong to this group
var rule = this._get_rule(pers, id);
for (let acct of accts) {
// add a condition for this account to the rule
rule.condition.clauses.push({
asset_ref: acct,
op: '=',
val: acct,
});
}
// if more than one clause, specify 'OR' for combining conditions
if (rule.condition.clauses.length > 1) {
rule.condition.combine_with = 'OR';
}
// update the perspective with the new info
this.update(pers, cb);
});
}
else {
// if pers is an id, get the perspective and try again
this.get(pers, (err, pers) => {
if (err) return cb(err);
this.add_to_group(pers, accts, group_name, cb);
});
}
}
/**
* Gets the rule in pers that specifies membership to the group with group_id
* @function module:cox-chapi.Perspective#_get_rule
* @private
* @param {object} pers - a perspective containing rules
* @param {string} group_id - the id of the group that the rule points to
* @return {object} the matching rule, or a new rule that has already been
* added to the perspective
*/
Perspective.prototype._get_rule = function(pers, group_id) {
// get the rule specifying accounts that belong to this group
var rule = pers.rules.find(
(rule) => rule.asset === 'AwsAccount' && rule.to === group_id
);
// if the rule doesn't exist, make a rule
if (!rule) {
rule = {
asset: 'AwsAccount',
to: group_id,
type: 'filter',
condition: {
clauses: [],
},
};
pers.rules.push(rule);
}
return rule;
};
/**
* gets a JSON object containing all the perspectives
* @function module:cox-chapi.Perspective#list
* @param {object} [flags] - an optional flags object
* @param {boolean} [flags.cache] - if true, this method will re-use a stored list
* of perspectives from the last time the --cache
* flag wasn't used
* @param {objectCallback} cb - called with an object with an array of perspective names/ids
*/
Perspective.prototype.list = function(flags, cb) {
if (typeof flags === 'function') {
cb = flags;
flags = {};
}
// if the cache flag is set, try to fetch cache
if (flags.cache) {
utils.find_cache('perspective_list', (err, cache_list) => {
if (err) return cb(err, cache_list);
if (!cache_list) return this.list(cb);
cb(null, cache_list);
});
}
else {
return this._list(flags, cb);
}
};
/**
* Helper for #list
* @private
*/
Perspective.prototype._list = function(flags, cb) {
var options = this._options('GET');
utils.send_request(options, null, this._list_cb.bind(this, flags, cb));
};
/**
* Helper callback for #list
* @private
*/
Perspective.prototype._list_cb = function(flags, cb, err, result) {
if (err) return cb(err, result);
utils.set_cache('perspective_list', result, (err, cache_list) => {
if (err) return cb(err, cache_list);
cb(null, cache_list);
});
};
/**
* Creates an perspective from the json object
* @function module:cox-chapi.Perspective#create
* @param {object} perspective - an object specifying fields for the new perspective
* @param {objectCallback} cb - called with the new perspective
*/
Perspective.prototype.create = function(perspective, cb) {
if (perspective.hasOwnProperty('schema')) {
perspective = perspective.schema;
}
var options = this._options('POST');
utils.send_request(options, JSON.stringify({schema: perspective}), cb);
};
/**
* Updates fields for the perspective with the specified id to match the given object
* @function module:cox-chapi.Perspective#update
* @param {object} perspective - an object holding new data to update the perspective with
* @param {number} perspective.id - the id of the perspective
* @param {objectCallback} cb - called with the updated perspective
*/
Perspective.prototype.update = function(perspective, cb) {
if (perspective.hasOwnProperty('schema')) {
perspective = perspective.schema;
}
var options = this._options('PUT', '/' + perspective.id);
utils.send_request(options, JSON.stringify({schema: perspective}), cb);
};
/**
* Deletes the perspective with the specified id
* @function module:cox-chapi.Perspective#destroy
* @param {object} [flags] - leave null/undefined if not specifying options
* @param {boolean} [flags.force] - if true, delete regardless of dependencies
* @param {boolean} [flags.hard_delete] - if true, skips archiving the perspective before deletion
* @param {number} id - the id of the perspective
* @param {stringCallback} cb - called with a success message
*/
Perspective.prototype.destroy = function(flags, id, cb) {
if (typeof flags !== 'object') {
cb = id;
id = flags;
flags = {};
}
return this._destroy(flags, id, cb);
};
/**
* Helper for #destroy
* @private
*/
Perspective.prototype._destroy = function(flags, id, cb) {
var path = '/' + id;
if(flags.hard_delete) {
path += '&force=true&hard_delete=true';
}
else if(flags.force) {
path += '&force=true';
}
var options = this._options('DELETE', path);
utils.send_request(options, null, this._destroy_cb.bind(this, flags, id, cb));
};
/**
* Helper callback for #destroy
* @private
*/
Perspective.prototype._destroy_cb = function(flags, id, cb, err, result) {
if(err) {
return cb(err, result);
}
else {
return cb(null, 'perspective destroyed');
}
};
module.exports = Perspective;