UNPKG

@axway/axway-central-cli

Version:

Manage APIs, services and publish to the Amplify Marketplace

476 lines (450 loc) 21.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.FormatString = FormatString; exports.KeyValueMapToNameValueArray = KeyValueMapToNameValueArray; exports.ValueFromKey = ValueFromKey; exports.parseScopeParam = exports.loadAndVerifySpecs = exports.loadAndVerifyApigeeXCredentialFile = exports.isWindows = exports.isValidJson = exports.isApiServerErrorType = exports.isApiServerErrorResponseType = exports.hbsCompare = exports.getResourceDefinition = exports.getLatestServedAPIVersion = exports.getConfig = exports.createLanguageSubresourceNames = exports.configFile = exports.compareResourcesByKindDesc = exports.compareResourcesByKindAsc = exports.buildTemplate = exports.buildGenericResource = void 0; exports.sanitizeMetadata = sanitizeMetadata; exports.writeToFile = exports.writeTemplates = exports.wait = exports.verifyScopeParam = exports.verifyFile = exports.transformSimpleFilters = void 0; var _fsExtra = require("fs-extra"); var _handlebars = _interopRequireDefault(require("handlebars")); var _jsYaml = require("js-yaml"); var _os = require("os"); var _path = require("path"); var _ApiServerClient = require("./ApiServerClient"); var _CompositeError = require("./CompositeError"); var _types = require("./types"); var _chalk = _interopRequireDefault(require("chalk")); function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; } const isWindows = exports.isWindows = /^win/.test(process.platform); const configFile = exports.configFile = (0, _path.join)((0, _os.homedir)(), '.axway', 'central.json'); const getConfig = async () => !(0, _fsExtra.existsSync)(configFile) ? Promise.resolve({}) : await (0, _fsExtra.readJson)(configFile); exports.getConfig = getConfig; const verifyFile = specFilePath => { let stats; let fileExtension; try { stats = (0, _fsExtra.lstatSync)(specFilePath); fileExtension = (0, _path.extname)(specFilePath); } catch (e) { throw new Error(`Couldn't find the definition file: ${specFilePath}`); } if (!stats.isFile()) { throw new Error(`Couldn't load the definition file: ${specFilePath}`); } else if (stats.size >= _types.MAX_FILE_SIZE) { throw new Error(`File size too large`); } else if (fileExtension !== '.yaml' && fileExtension !== '.yml' && fileExtension !== '.json') { throw new Error(`File extension is invalid, please provide '.yaml' or '.yml' or '.json' file`); } }; exports.verifyFile = verifyFile; const writeToFile = (path, data) => { try { (0, _fsExtra.writeFileSync)(path, data); } catch (e) { // if parser is failing, rethrow with our own error throw new Error(`Error while writing the yaml file to: ${path}`); } }; /** * Checks if the passed item can be converted to a JSON or is a valid JSON object. * @param item item to check * @returns true if the item can be converted, false otherwise. */ exports.writeToFile = writeToFile; const isValidJson = item => { let parsedItem = typeof item !== 'string' ? JSON.stringify(item) : item; try { parsedItem = JSON.parse(parsedItem); } catch (e) { return false; } return typeof parsedItem === 'object' && item !== null; }; /** * Loads and parse file from path, accepts JSON and YAML files. Also completing validation on "kind" values. * @param specFilePath file path * @param allowedKinds array of allowed "kind" values */ exports.isValidJson = isValidJson; const loadAndVerifySpecs = async (specFilePath, allowedKinds, skipKindCheck) => { // Load the given JSON or YAML file. let docs = []; let isMissingName = false; try { docs = (0, _jsYaml.loadAll)(await (0, _fsExtra.readFile)(specFilePath, 'utf8')); } catch (e) { throw new Error(e.reason && e.reason.includes('null byte') ? `File encoding is invalid, please make sure it is using UTF-8` : `File content is invalid.`); } // if user pass an array of json objects, docs const will have nested array, workaround for this: if ((0, _path.extname)(specFilePath) === '.json' && docs.length === 1 && Array.isArray(docs[0])) { docs = docs[0]; } // Do not continue if given an empty file. if (!docs.length) throw new Error(`File is empty.`); // Validate all entries in the file. const errors = []; const createErrorPrefix = (index, kind, name) => { return `Entry ${index + 1}, "${kind}/${name || 'Unknown name'}"`; }; for (let index = 0; index < docs.length; index++) { // Verify document is defined/valid. const doc = docs[index]; if (typeof doc !== 'object' || !doc) { errors.push(new Error(`${createErrorPrefix(index)}: Entry format is invalid.`)); continue; } // Set a flag if at least 1 name is messing in file. if (!doc.name) { isMissingName = true; } if (!skipKindCheck) { // Validate resource kind. if (!doc.kind) { errors.push(Error(`${createErrorPrefix(index, doc.kind, doc.name)}: The "kind" field is missing.` + `\nCurrently supported values are (case sensitive): ${[...allowedKinds.values()].join(', ')}`)); } else if (!allowedKinds.has(doc.kind)) { errors.push(new Error(`${createErrorPrefix(index, doc.kind, doc.name)}: Kind "${doc.kind}" is unsupported.` + `\nCurrently supported values are (case sensitive): ${[...allowedKinds.values()].join(', ')}`)); } } // TODO: Validate "metadata.scope.kind" if available. Requires DefinitionManager.getSortedKindsMap() result. } if (errors.length > 0) { throw new _CompositeError.CompositeError(errors); } // File's contents appears to be valid. Return loaded info. return { docs, isMissingName }; }; /** * Generate a GenericResource instance from resource definition, resource name, and scope name. Used * in some rendering logic for the "delete" command. * Note that generated metadata includes only scope info. * @param {ResourceDefinition} resourceDef resource definition * @param {string} resourceName resource name * @param {string} scopeName optional scope name * @returns {GenericResource} generic resource representation */ exports.loadAndVerifySpecs = loadAndVerifySpecs; const buildGenericResource = ({ resourceDef, resourceName, scopeName }) => { if (resourceName) { var _resourceDef$spec, _resourceDef$spec2, _resourceDef$spec2$sc; return { apiVersion: resourceDef === null || resourceDef === void 0 ? void 0 : resourceDef.apiVersion, group: resourceDef === null || resourceDef === void 0 ? void 0 : resourceDef.group, title: resourceName, name: resourceName, kind: resourceDef === null || resourceDef === void 0 ? void 0 : resourceDef.spec.kind, attributes: {}, tags: [], metadata: resourceDef !== null && resourceDef !== void 0 && (_resourceDef$spec = resourceDef.spec) !== null && _resourceDef$spec !== void 0 && _resourceDef$spec.scope && scopeName ? { scope: { kind: resourceDef === null || resourceDef === void 0 ? void 0 : (_resourceDef$spec2 = resourceDef.spec) === null || _resourceDef$spec2 === void 0 ? void 0 : (_resourceDef$spec2$sc = _resourceDef$spec2.scope) === null || _resourceDef$spec2$sc === void 0 ? void 0 : _resourceDef$spec2$sc.kind, name: scopeName } // note: forced conversion here only because using generated resources for rendering simple text } : undefined, spec: {} }; } else { var _resourceDef$spec3, _resourceDef$spec4, _resourceDef$spec4$sc; return { apiVersion: resourceDef === null || resourceDef === void 0 ? void 0 : resourceDef.apiVersion, group: resourceDef === null || resourceDef === void 0 ? void 0 : resourceDef.group, kind: resourceDef === null || resourceDef === void 0 ? void 0 : resourceDef.spec.kind, attributes: {}, tags: [], metadata: resourceDef !== null && resourceDef !== void 0 && (_resourceDef$spec3 = resourceDef.spec) !== null && _resourceDef$spec3 !== void 0 && _resourceDef$spec3.scope && scopeName ? { scope: { kind: resourceDef === null || resourceDef === void 0 ? void 0 : (_resourceDef$spec4 = resourceDef.spec) === null || _resourceDef$spec4 === void 0 ? void 0 : (_resourceDef$spec4$sc = _resourceDef$spec4.scope) === null || _resourceDef$spec4$sc === void 0 ? void 0 : _resourceDef$spec4$sc.kind, name: scopeName } // note: forced conversion here only because using generated resources for rendering simple text } : undefined, spec: {} }; } }; /** * Returns true if error object is of type ApiServerError * @param err error object to check */ exports.buildGenericResource = buildGenericResource; const isApiServerErrorType = err => { const cast = err; return !!cast.status && !!cast.title && !!cast.detail; }; /** * Returns true if error object is of type ApiServerErrorResponse * @param err error object to check */ exports.isApiServerErrorType = isApiServerErrorType; const isApiServerErrorResponseType = err => { const cast = err; return !!cast.errors && Array.isArray(cast.errors); }; /** * Wrapper to return a compare function for sorting by "kind" value in array of resources in ascending * order. Order based on "sortedKindValues" array. * @param {Array<ResourceDefinition>} sortedResourceDefs array of ResourceDefinitions defining a required order of the resources. * @returns {Function} Array sorting function */ exports.isApiServerErrorResponseType = isApiServerErrorResponseType; const compareResourcesByKindAsc = sortedResourceDefs => (a, b) => sortedResourceDefs.findIndex(def => { var _def$spec$scope, _a$metadata, _a$metadata$scope; return def.spec.kind === a.kind && ((_def$spec$scope = def.spec.scope) === null || _def$spec$scope === void 0 ? void 0 : _def$spec$scope.kind) === ((_a$metadata = a.metadata) === null || _a$metadata === void 0 ? void 0 : (_a$metadata$scope = _a$metadata.scope) === null || _a$metadata$scope === void 0 ? void 0 : _a$metadata$scope.kind); }) - sortedResourceDefs.findIndex(def => { var _def$spec$scope2, _b$metadata, _b$metadata$scope; return def.spec.kind === b.kind && ((_def$spec$scope2 = def.spec.scope) === null || _def$spec$scope2 === void 0 ? void 0 : _def$spec$scope2.kind) === ((_b$metadata = b.metadata) === null || _b$metadata === void 0 ? void 0 : (_b$metadata$scope = _b$metadata.scope) === null || _b$metadata$scope === void 0 ? void 0 : _b$metadata$scope.kind); }); /** * Wrapper to return a compare function for sorting by "kind" value in array of resources in descending * order. Order based on "sortedKindValues" array. * @param {Array<ResourceDefinition>} sortedResourceDefs array of ResourceDefinitions defining a required order of the resources. * @returns {Function} Array sorting function */ exports.compareResourcesByKindAsc = compareResourcesByKindAsc; const compareResourcesByKindDesc = sortedResourceDefs => (a, b) => sortedResourceDefs.findIndex(def => { var _def$spec$scope3, _b$metadata2, _b$metadata2$scope; return def.spec.kind === b.kind && ((_def$spec$scope3 = def.spec.scope) === null || _def$spec$scope3 === void 0 ? void 0 : _def$spec$scope3.kind) === ((_b$metadata2 = b.metadata) === null || _b$metadata2 === void 0 ? void 0 : (_b$metadata2$scope = _b$metadata2.scope) === null || _b$metadata2$scope === void 0 ? void 0 : _b$metadata2$scope.kind); }) - sortedResourceDefs.findIndex(def => { var _def$spec$scope4, _a$metadata2, _a$metadata2$scope; return def.spec.kind === a.kind && ((_def$spec$scope4 = def.spec.scope) === null || _def$spec$scope4 === void 0 ? void 0 : _def$spec$scope4.kind) === ((_a$metadata2 = a.metadata) === null || _a$metadata2 === void 0 ? void 0 : (_a$metadata2$scope = _a$metadata2.scope) === null || _a$metadata2$scope === void 0 ? void 0 : _a$metadata2$scope.kind); }); /** * Api-server returns the "resourceVersion" in metadata object as a counter for resource updates. * If a user will send this key in the payload it will throw an error so using this helper to sanitizing metadata on * the updates. * @param doc resource data * @returns {GenericResource} resource data without metadata.resourceVersion key */ exports.compareResourcesByKindDesc = compareResourcesByKindDesc; function sanitizeMetadata(doc) { var _doc$metadata; if (doc !== null && doc !== void 0 && (_doc$metadata = doc.metadata) !== null && _doc$metadata !== void 0 && _doc$metadata.resourceVersion) { delete doc.metadata.resourceVersion; } return doc; } const buildTemplate = (templateFunc, input) => { const template = _handlebars.default.compile(templateFunc(), { noEscape: true }); return template(input); }; /** * Takes in a set of parameters (values), compiles a string utilizing the provided handlebars templating function (templateFunc) * and the buildTemplate function with those parameters, and finally writes the output string to a file of the name that was provided (fileName). * @param {string} fileName * @param {object} values * @param {()=>string} templateFunc */ exports.buildTemplate = buildTemplate; const writeTemplates = (fileName, values, templateFunc) => { const data = buildTemplate(templateFunc, values); writeToFile(fileName, data); }; /** * helper function to extend the handlebars built-in helpers functionality to add a way to compare equality of two strings or numbers * @param {string|number} lvalue * @param {string|number} rvalue * @param {{fn:(} options */ exports.writeTemplates = writeTemplates; const hbsCompare = () => { _handlebars.default.registerHelper('compare', (context, lvalue, rvalue, options) => { let operator = options.hash.operator || '==='; let operators = { '==': (l, r) => { return l == r; }, '===': (l, r) => { return l === r; }, '!=': (l, r) => { return l != r; }, '<': (l, r) => { return l < r; }, '>': (l, r) => { return l > r; }, '<=': (l, r) => { return l <= r; }, '>=': (l, r) => { return l >= r; }, typeof: (l, r) => { return typeof l == r; } }; var result = operators[operator](lvalue, rvalue); if (result) { return options.fn(context); } else { return options.inverse(context); } }); }; /** * Parse and verify scope param, returns undefined if param is undefined. Throws an error if "Kind" is unknown. * @param scopeParam raw scope param value * @returns {ParsedScopeParam | undefined} */ exports.hbsCompare = hbsCompare; const parseScopeParam = scopeParam => { if (!scopeParam) return undefined; let sp = scopeParam.toString(); if (sp.indexOf('/') === -1) return { name: scopeParam };else { const name = sp.substring(scopeParam.indexOf('/') + 1); const kind = sp.substring(0, scopeParam.indexOf('/')); if (!name.length || !kind.length) throw Error(`invalid scope (-s/--scope) parameter value.` + `\nPlease use "--scope <scope kind>/<scope name>" or "--scope <scope name>" formats.`); return { name, kind }; } }; /** * Verify parsed scope param: * 1. scope kind should be known. * 2. scope kind should match at least one in a resource "scoped" definitions ("non-scoped" definition will be ignored). * @param allKinds all available kinds. * @param defs resource definitions where at least one should match the scope kind if some "scoped" resources are there. * @param scopeParam parsed scope param. */ exports.parseScopeParam = parseScopeParam; const verifyScopeParam = (allKinds, defs, scopeParam) => { const allowedScopeKinds = new Set(); defs.forEach(defs => !!defs.scope && allowedScopeKinds.add(defs.scope.spec.kind)); if (scopeParam !== null && scopeParam !== void 0 && scopeParam.kind) { if (!allKinds.has(scopeParam.kind)) throw new Error(`unsupported kind value "${scopeParam.kind}" in the "--scope" param.` + `\nCurrently supported values are (case sensitive): ${[...allKinds.values()].join(', ')}`); if (allowedScopeKinds.size > 0 && !allowedScopeKinds.has(scopeParam.kind)) throw Error(`scope kind "${scopeParam.kind}" is invalid.` + `\n"${defs[0].resource.spec.kind}" resource might exist in the following scopes: ${[...allowedScopeKinds.values()].join(', ')}`); } }; /** * Transforms simple filters(title, attribute, tag) into an RSQL-formatted query string the GET API supports. * @param {string} title The title the user wants to filter the resource list by. * @param {string} attribute The attribute(key=value) the user wants to filter the resource list by. * @param {string} tag The tag the user wants to filter the resource list by. * @returns {string} transformedFilter, the RSQL formatted query string */ exports.verifyScopeParam = verifyScopeParam; const transformSimpleFilters = (title, attribute, tag) => { const titleFilter = title ? `title=='*${title}*'` : ''; const attributeKey = attribute && attribute.split('=')[0]; const attributeValue = attribute && attribute.split('=')[1]; const attributeFilter = attributeKey && attributeValue ? `attributes.${attributeKey}==${attributeValue}` : ''; const tagFilter = tag ? `tags==${tag}` : ''; const formattedFilter = `${titleFilter && `${titleFilter};`}${attributeFilter && `${attributeFilter};`}${tagFilter}`; const transformedFilter = formattedFilter.charAt(formattedFilter.length - 1) === ';' ? formattedFilter.slice(0, -1) : formattedFilter; return transformedFilter; }; exports.transformSimpleFilters = transformSimpleFilters; function FormatString(str, ...val) { for (let index = 0; index < val.length; index++) { str = str.replace(`{${index}}`, val[index]); } return str; } /** * Loads and parse file from path, accepts JSON file. Also validates additional details. * @param credentialFilePath file path */ const loadAndVerifyApigeeXCredentialFile = async credentialFilePath => { // Load the given JSON file. let fileInfo = ''; try { fileInfo = await (0, _fsExtra.readJson)(credentialFilePath); } catch (e) { throw new Error(e.reason); } // Do not continue if given an empty file. if (fileInfo === '') throw new Error(`File is empty.`); // TODO: Validate additional details if needed // Return loaded info. return fileInfo; }; exports.loadAndVerifyApigeeXCredentialFile = loadAndVerifyApigeeXCredentialFile; function KeyValueMapToNameValueArray(m) { let array = []; m.forEach((value, key) => { array.push({ name: key, value: value }); }); if (array.length == 0) { return undefined; } return array; } const createLanguageSubresourceNames = langCode => { const langCodeArr = langCode.split(','); let langSubresourceNamesArr = ['languages']; let languageTypesArr = []; Object.keys(_types.LanguageTypes).forEach(key => languageTypesArr.push(ValueFromKey(_types.LanguageTypes, key))); langCodeArr.forEach(langCode => { if (langCode.trim() != '') { if (!languageTypesArr.includes(langCode)) { console.log(_chalk.default.yellow(`\n\'${langCode}\' language code is not supported, hence create/update cannot be performed on \'languages-${langCode}\. Allowed language codes: ${_types.LanguageTypes.French} | ${_types.LanguageTypes.German} | ${_types.LanguageTypes.US} | ${_types.LanguageTypes.Portugese}.'`)); } else { langSubresourceNamesArr.push(`languages-${langCode.trim()}`); } } }); return langSubresourceNamesArr; }; exports.createLanguageSubresourceNames = createLanguageSubresourceNames; function ValueFromKey(stringEnum, key) { for (const k of Object.values(stringEnum)) { if (k === stringEnum[key]) return k; } return undefined; } const getLatestServedAPIVersion = resourceDef => { let apiVersions = resourceDef.spec.apiVersions; if (apiVersions && apiVersions.length > 0) { for (const version of apiVersions) { if (version.served && !version.deprecated) { return version.name; } } return _ApiServerClient.ApiServerVersions.v1alpha1; } // if the apiVersions are not set on the resource definition, fallback to v1alpha1 version return _ApiServerClient.ApiServerVersions.v1alpha1; }; /** * Wait for the given milliseconds * @param {number} ms The given time to wait * @returns {Promise} A fulfilled promise after the given time has passed */ exports.getLatestServedAPIVersion = getLatestServedAPIVersion; const wait = ms => new Promise(resolve => setTimeout(resolve, ms)); /** * Fetch resource definition of given kind and scope kind if exists * @param {ResourceDefinition[]} sortedDefsArray The given time to wait * @param {Kind} kind The kind of the resource * @param {Kind} scopeKind The scope kind of the resource */ exports.wait = wait; const getResourceDefinition = async (sortedDefsArray, kind, scopeKind) => { const resourceDefinition = sortedDefsArray.find(def => { var _def$spec$scope5; return scopeKind ? def.spec.kind === kind && ((_def$spec$scope5 = def.spec.scope) === null || _def$spec$scope5 === void 0 ? void 0 : _def$spec$scope5.kind) === scopeKind : def.spec.kind === kind; }); return resourceDefinition; }; exports.getResourceDefinition = getResourceDefinition;