@axway/axway-central-cli
Version:
Manage APIs, services and publish to the Amplify Marketplace
298 lines (290 loc) • 15.7 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.DefinitionsManager = void 0;
var _snooplogg = _interopRequireDefault(require("snooplogg"));
var _easyTable = _interopRequireDefault(require("easy-table"));
var _findIndex = _interopRequireDefault(require("lodash/findIndex"));
var _findLastIndex = _interopRequireDefault(require("lodash/findLastIndex"));
var _omit = _interopRequireDefault(require("lodash/omit"));
var _max2 = _interopRequireDefault(require("lodash/max"));
var _min2 = _interopRequireDefault(require("lodash/min"));
var _chalk = require("chalk");
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; }
function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == typeof i ? i : i + ""; }
function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
const {
log
} = (0, _snooplogg.default)('central:class.DefinitionsManager');
/**
* Get / fetch / set specs.
*/
class DefinitionsManager {
constructor(apiServerClient) {
_defineProperty(this, "apiServerClient", void 0);
_defineProperty(this, "specs", void 0);
_defineProperty(this, "cli", new Map());
_defineProperty(this, "resources", new Map());
this.apiServerClient = apiServerClient;
}
/**
* Private
*/
/**
* A reducer for sorting ResourceDefinition by references in the sortByReferences's "fromTo" list.
* Function utilize current "fromTo" list which is expected to be passed in { sorted, unsorted } form
* and "from" list which is used to find references for entities in the "fromTo" list.
* IF resource's "to" references still in "unsorted" list - put it back to unsorted also
* (its too early for the resource to be sorted).
* ELSE resource's "to" links are in the "from" list or/and current "sorted" list - find max index
* and append to "right-most" position of the "sorted" list
* @param curr
* @param defsFrom
*/
reduceByReferenceLinks(curr, defsFrom) {
return curr.unsorted.reduce((a, c, _, arr) => {
// IF any unsorted reference found, push current definition to unsorted too and skip
const unsortedRefs = c.spec.references.toResources.find(ref => {
return (0, _findLastIndex.default)(arr, def => {
var _def$spec$scope;
return def.spec.kind === ref.kind && ((_def$spec$scope = def.spec.scope) === null || _def$spec$scope === void 0 ? void 0 : _def$spec$scope.kind) === ref.scopeKind;
}) !== -1;
});
if (!!unsortedRefs) {
a.unsorted.push(c);
} else {
var _max, _min;
// ELSE all refs are in pre populated or in sorted lists, calculate
const startIndex = (_max = (0, _max2.default)(c.spec.references.toResources.map(ref => {
// find index in current sorted array
const sortedListIndex = (0, _findLastIndex.default)(a.sorted, def => {
var _def$spec$scope2;
return def.spec.kind === ref.kind && ((_def$spec$scope2 = def.spec.scope) === null || _def$spec$scope2 === void 0 ? void 0 : _def$spec$scope2.kind) === ref.scopeKind;
});
// find index in "from-only" array
const fromListIndex = (0, _findLastIndex.default)(defsFrom, def => {
var _def$spec$scope3;
return def.spec.kind === ref.kind && ((_def$spec$scope3 = def.spec.scope) === null || _def$spec$scope3 === void 0 ? void 0 : _def$spec$scope3.kind) === ref.scopeKind;
});
// this should never happen only if the api-server is missing some corresponding
// references so nothing found
if (sortedListIndex === -1 && fromListIndex === -1) {
var _c$spec$scope;
log('reduceByReferenceLinks, startIndex error, def: ', JSON.stringify(c, null, 2), '\nref: ', ref);
throw Error(`References calculation error, startIndex for kind: ${c.spec.kind} in scope: ${(_c$spec$scope = c.spec.scope) === null || _c$spec$scope === void 0 ? void 0 : _c$spec$scope.kind}`);
}
// if nothing found in sorted and pre return null so it will put it back to unsorted
else return sortedListIndex === -1 ? 0 : sortedListIndex;
}))) !== null && _max !== void 0 ? _max : null;
const stopIndex = (_min = (0, _min2.default)(c.spec.references.fromResources.map(ref => {
const i = (0, _findIndex.default)(a.sorted, def => {
var _def$spec$scope4;
return def.spec.kind === ref.kind && ((_def$spec$scope4 = def.spec.scope) === null || _def$spec$scope4 === void 0 ? void 0 : _def$spec$scope4.kind) === ref.scopeKind;
});
return i === -1 ? null : i;
}))) !== null && _min !== void 0 ? _min : null;
if (startIndex && stopIndex && startIndex >= stopIndex || startIndex === null) {
var _c$spec$scope2;
log('reduceByReferenceLinks, indexes error, definition: ', JSON.stringify(c, null, 2));
throw Error(`References calculation error, indexes for kind: ${c.spec.kind} in scope: ${(_c$spec$scope2 = c.spec.scope) === null || _c$spec$scope2 === void 0 ? void 0 : _c$spec$scope2.kind}`);
} else {
a.sorted.splice(startIndex + 1, 0, c);
}
}
return a;
}, {
sorted: curr.sorted,
unsorted: []
});
}
/**
* Utility for sorting ResourceDefinition by refs. Its grouping resources into 4 arrays and
* iterates over the "fromTo" list with "reduceByReferenceLinks" reducer until its completely sorted.
* @param defs list of resources to sort
*/
sortByReferences(defs) {
// 1. Sort by references into 4 arrays
const groupedDefs = defs.reduce((a, c) => {
const fromRefsNum = c.spec.references.fromResources.length;
const toRefsNum = c.spec.references.toResources.length;
if (fromRefsNum && toRefsNum) a.fromTo.push(c);else if (!fromRefsNum && !toRefsNum) a.noRefs.push(c);else if (!toRefsNum) a.from.push(c);else if (!fromRefsNum) a.to.push(c);
return a;
}, {
noRefs: [],
from: [],
fromTo: [],
to: []
});
// 2. Iterate over "fromTo" defs until its completely sorted
let result = this.reduceByReferenceLinks({
sorted: [],
unsorted: groupedDefs.fromTo
}, groupedDefs.from);
let loopCount = 0; // just in case, circuit breaker;
while (result.unsorted.length > 0 && loopCount <= 1000) {
result = this.reduceByReferenceLinks(result, groupedDefs.from);
loopCount += 1;
}
// On average function should not take more than 5 loops currently.
// Lets signal that something is wrong here.
if (loopCount === 1000) throw Error('Definition references calculation error, max loop count reached');
return [...groupedDefs.noRefs, ...groupedDefs.from, ...result.sorted, ...groupedDefs.to];
}
/**
* Public
* Constructs two maps per resource group(e.g. management, catalog)
* First map is for the nested 'cli' object and the other is for the nested 'resource' object
* Created by stripping the 'definitions' group from the specs object, leaving only the 'management' and 'catalog' groups as of 6/7(this will dynamically update in case new groups are added on api-server)
* Then iterating over that specs object and pushing the cli and resource objects for each group into arrays, which are used to initialize the final maps
*/
async init() {
log('init');
this.specs = await this.apiServerClient.getSpecs();
const filteredSpecs = (0, _omit.default)(this.specs, 'definitions');
const cliArray = [];
const resourcesArray = [];
for (const [key] of Object.entries(filteredSpecs)) {
resourcesArray.push(...filteredSpecs[key].resources);
cliArray.push(...filteredSpecs[key].cli);
}
this.cli = new Map(cliArray);
this.resources = new Map(resourcesArray);
return this;
}
getAllWordsList() {
if (!this.specs) return [];
const result = [];
this.cli.forEach(v => {
result.push(v.spec.names.plural, v.spec.names.singular, ...v.spec.names.shortNames);
});
return result;
}
getAllKindsList() {
if (!this.specs) throw Error('DefinitionManager.specs is not initialized.');
const result = new Set([]);
this.resources.forEach(v => {
result.add(v.spec.kind);
});
return result;
}
/**
* Get the map with resources definitions sorted by references.
* Used to identify the correct order in which resources should be created / removed.
* @returns {Map<string,ResourceDefinition>} map where key is ResourceDefinition.name, value is ResourceDefinition
*/
getSortedKindsMap() {
if (!this.specs) throw Error('DefinitionManager.specs is not initialized.');
// For each spec modify the references:
// 1. since cli do not support creating or updating sub-resources we are ignoring references and removing them atm.
// 2. if it is a scoped resource, add a manual "to" reference to the scope resource and a corresponding "from"
// reference in the scope resource
this.resources.forEach(definition => {
// 1. remove the references from the sub-resources and circular references (to self)
// TODO: circular references support: https://jira.axway.com/browse/APIGOV-20808
definition.spec.references.toResources = definition.spec.references.toResources.filter(ref => {
var _definition$spec$scop;
return !ref.from && !(ref.kind === definition.spec.kind && ref.scopeKind === ((_definition$spec$scop = definition.spec.scope) === null || _definition$spec$scop === void 0 ? void 0 : _definition$spec$scop.kind));
});
definition.spec.references.fromResources = definition.spec.references.fromResources.filter(ref => {
var _definition$spec$scop2;
return !ref.from && !(ref.kind === definition.spec.kind && ref.scopeKind === ((_definition$spec$scop2 = definition.spec.scope) === null || _definition$spec$scop2 === void 0 ? void 0 : _definition$spec$scop2.kind));
});
// 2. add references between scope and scoped resources
if (!!definition.spec.scope) {
const scopeDef = [...this.resources.values()].find(res => res.spec.kind === definition.spec.scope.kind); // mind the non-null assertion here
// modify current definition by adding "toResources" link to scopeDef
if (!definition.spec.references.toResources.find(ref => ref.kind === scopeDef.spec.kind)) {
definition.spec.references.toResources.push({
kind: scopeDef.spec.kind,
// @ts-ignore
// NOTE: not used value, adding just to indicate it's manual nature.
types: ['CALCULATED']
});
}
// modify related "scope" definition by adding "fromResources" link to current definition
if (!scopeDef.spec.references.fromResources.find(ref => ref.kind === definition.spec.kind)) {
scopeDef.spec.references.fromResources.push({
kind: definition.spec.kind,
// @ts-ignore
// NOTE: not used value, adding just to indicate it's manual nature.
types: ['CALCULATED'],
scopeKind: definition.spec.scope.kind
});
}
}
});
// execute the sorting, note that the returning map is using the "name" field as keys.
const res = this.sortByReferences([...this.resources.values()]);
return new Map(res.map(v => [v.name, v]));
}
getDefsTableForHelpMsg() {
if (!this.specs) return 'No resources found.';
const t = new _easyTable.default();
// create the 'axway central get' table
this.cli.forEach(v => {
var _this$resources$get, _this$resources$get2, _this$resources, _this$resources$get3, _this$resources$get3$;
// grab the resource group
const group = v.metadata.scope.name;
t.cell('RESOURCE', `${v.spec.names.plural}`, () => (0, _chalk.cyan)(v.spec.names.plural));
t.cell('SHORT NAMES', [...(v.spec.names.shortNamesAlias || v.spec.names.shortNames)].join(','));
t.cell('RESOURCE KIND', (_this$resources$get = this.resources.get(v.spec.resourceDefinition)) === null || _this$resources$get === void 0 ? void 0 : _this$resources$get.spec.kind);
t.cell('SCOPED', (_this$resources$get2 = this.resources.get(v.spec.resourceDefinition)) !== null && _this$resources$get2 !== void 0 && _this$resources$get2.spec.scope ? 'true' : 'false');
t.cell('SCOPE KIND', (_this$resources = this.resources) === null || _this$resources === void 0 ? void 0 : (_this$resources$get3 = _this$resources.get(v.spec.resourceDefinition)) === null || _this$resources$get3 === void 0 ? void 0 : (_this$resources$get3$ = _this$resources$get3.spec.scope) === null || _this$resources$get3$ === void 0 ? void 0 : _this$resources$get3$.kind);
t.cell('RESOURCE GROUP', group);
t.newRow();
});
return t.sort(['RESOURCE']).toString();
}
findDefsByKind(kind) {
log('findDefsByKind: ', kind);
const res = [...this.resources].reduce((a, [_, def]) => {
if (def.spec.kind === kind) {
a.push({
resource: def,
cli: [...this.cli].find(([_, cliDef]) => cliDef.spec.resourceDefinition === def.name)[1],
scope: def.spec.scope ? [...this.resources].find(([_, resDef]) => resDef.spec.kind === def.spec.scope.kind)[1] : undefined
});
}
return a;
}, []);
return res.length ? res : null;
}
/**
* Returns set of related definitions if word is known.
* @param word word to search for
* @returns {object | null} {
* resource: definition of the resource
* cli: cli definition of the resource
* scope: scope resource definition, can support multiple scopes (only for scoped resources, otherwise it is undefined)
* } or null if no definitions found for this word.
*/
findDefsByWord(word) {
log('findDefsByWord: ', word);
if (!this.specs) return null;
const cliKv = [...this.cli].filter(([_, v]) => {
var _v$spec$names$shortNa;
return v.spec.names.plural === word || v.spec.names.singular === word || v.spec.names.shortNames.includes(word) || ((_v$spec$names$shortNa = v.spec.names.shortNamesAlias) === null || _v$spec$names$shortNa === void 0 ? void 0 : _v$spec$names$shortNa.includes(word));
});
// no match found returning null
if (!cliKv.length) return null;
const result = [...this.cli].reduce((a, [_, cliDef]) => {
var _cliDef$spec$names$sh;
if (cliDef.spec.names.plural === word || cliDef.spec.names.singular === word || cliDef.spec.names.shortNames.includes(word) || (_cliDef$spec$names$sh = cliDef.spec.names.shortNamesAlias) !== null && _cliDef$spec$names$sh !== void 0 && _cliDef$spec$names$sh.includes(word)) {
var _this$findDefsByKind;
// note: mind non-null assertion
const resource = this.resources.get(cliDef.spec.resourceDefinition);
const scope = resource.spec.scope ? (_this$findDefsByKind = this.findDefsByKind(resource.spec.scope.kind)) === null || _this$findDefsByKind === void 0 ? void 0 : _this$findDefsByKind[0].resource : null;
a.push({
resource,
cli: cliDef,
scope: !!scope ? scope : undefined
});
}
return a;
}, []);
return result;
}
}
exports.DefinitionsManager = DefinitionsManager;