UNPKG

apigeelint

Version:

Node module and tool to lint a bundle for an Apigee API Proxy or sharedflow.

612 lines (555 loc) 17.5 kB
/* Copyright © 2019-2026 Google 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 https://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. */ const fs = require("node:fs"), path = require("node:path"), Resource = require("./Resource.js"), Policy = require("./Policy.js"), Endpoint = require("./Endpoint.js"), xpath = require("xpath"), Dom = require("@xmldom/xmldom").DOMParser, bundleType = require("./BundleTypes.js"), DEBUG = require("debug"), debug = DEBUG("apigeelint:Bundle"), lintUtil = require("./lintUtil.js"), getcb = lintUtil.curry(lintUtil.diagcb, debug), domOptions = lintUtil.domOptions(debug); function _buildEndpoints(folder, tag, bundle, processFunction) { try { if (fs.existsSync(folder)) { fs.readdirSync(folder).forEach(function (endptFile) { //can't be a directory //must end in .xml const fqFname = path.join(folder, endptFile); if (endptFile.endsWith(".xml") && fs.lstatSync(fqFname).isFile()) { const doc = xpath.select( tag, new Dom(domOptions).parseFromString( fs.readFileSync(fqFname).toString(), ), ); doc.forEach((element) => processFunction(element, fqFname, bundle)); } }); } } catch (e) { debug(e); } } function buildProxyEndpoints(bundle) { const folder = path.join(bundle.proxyRoot, "proxies"), tag = "ProxyEndpoint", processFunction = function (element, fqFname, bundle) { bundle.proxyEndpoints = bundle.proxyEndpoints || []; bundle.proxyEndpoints.push(new Endpoint(element, bundle, fqFname)); }; _buildEndpoints(folder, tag, bundle, processFunction); } function buildsharedflows(bundle) { const folder = path.join(bundle.proxyRoot, "sharedflows"), tag = "SharedFlow", processFunction = function (element, fqFname, bundle) { bundle.proxyEndpoints = bundle.proxyEndpoints || []; bundle.proxyEndpoints.push( new Endpoint( element, bundle, fqFname, bundleType.BundleType.SHAREDFLOW, ), ); }; _buildEndpoints(folder, tag, bundle, processFunction); } function buildTargetEndpoints(bundle) { const folder = path.join(bundle.proxyRoot, "targets"), tag = "TargetEndpoint", processFunction = function (element, fname, bundle) { bundle.targetEndpoints = bundle.targetEndpoints || []; bundle.targetEndpoints.push(new Endpoint(element, bundle, fname)); }; _buildEndpoints(folder, tag, bundle, processFunction); } function buildPolicies(bundle) { //get the list of policies and create the policy objects bundle.policies = []; if (fs.existsSync(bundle.proxyRoot + "/policies")) { fs.readdirSync(bundle.proxyRoot + "/policies").forEach( function (policyFile) { const ext = policyFile.split(".").pop(); if (ext === "xml") { bundle.policies.push( new Policy(bundle.proxyRoot + "/policies", policyFile, bundle), ); } }, ); } } function _buildResources(parent, path, resources) { // Append the resources found in the passed path. // If the path is a directory, then recurse. fs.readdirSync(path) .filter((file) => !["node_modules", ".DS_Store"].includes(file)) .forEach(function (policyFile) { if (fs.statSync(path + "/" + policyFile).isDirectory()) { _buildResources(parent, path + "/" + policyFile, resources); } else if (!policyFile.endsWith("~")) { resources.push( new Resource(parent, path + "/" + policyFile, policyFile), ); } }); } function buildResources(bundle) { //get the list of policies and create the resources objects - must recurse bundle.resources = []; if (fs.existsSync(path.join(bundle.proxyRoot, "resources"))) { _buildResources( bundle, path.join(bundle.proxyRoot, "resources"), bundle.resources, ); } } // function buildAll(bundle, bundleName) { // buildResources(bundle); // buildPolicies(bundle); // buildProxyEndpoints(bundle); // // //no targets for shared flow // if (bundleName !== bundleType.BundleType.SHAREDFLOW) { // buildTargetEndpoints(bundle); // } // } function processFileSystem(config, bundle, cb) { // build the bundle representation from file system. // Normalize the root path. Convert all path separator to forward slash. // This is used in error and warning messages. bundle.root = path.resolve(config.source.path).split(path.sep).join("/"); // Populate the filepath by looking at the only .xml file // present at the bundle.root. const files = fs .readdirSync(bundle.root) .filter( (file) => fs.lstatSync(path.join(bundle.root, file)).isFile() && file.endsWith(".xml"), ); if (files.length > 1) { debug("More than one .xml file found at bundle root. Aborting."); throw new Error( "There is more than one .xml file found at the bundle root", ); } bundle.filePath = bundle.root + "/" + files[0]; bundle.proxyRoot = bundle.root; const bundleTypeName = config.source.bundleType; //bundle.policies = []; //process.chdir(config.source.path); bundle.report = { filePath: lintUtil.effectivePath(bundle, bundle.root), errorCount: 0, warningCount: 0, fixableErrorCount: 0, fixableWarningCount: 0, messages: [], }; if (!bundle.proxyRoot.endsWith(bundleTypeName)) { bundle.addMessage({ message: `No ${bundleTypeName} folder found in: ${bundle.root}`, }); } if (cb) { cb(bundle); } } class Bundle { constructor(config, cb) { const bundle = this; this.xPathName = bundleType.getXPathName(config.source.bundleType); this.bundleTypeName = config.source.bundleType; this.excluded = config.excluded; this.profile = config.profile; this.ignoreDirectives = config.ignoreDirectives; this.sourcePath = config.source.sourcePath; this.resolvedPath = config.source.path; this.sourceType = config.source.type; this.config = config; processFileSystem(config, bundle, cb); } getElement() { const filePath = this.filePath; if (!this.element && fs.existsSync(filePath)) { debug(`getElement: filePath:${filePath}`); this.element = new Dom(domOptions).parseFromString( fs.readFileSync(filePath).toString(), ); } return this.element; } getName() { if (!this.name) { //look at the root of the proxy for the .xml file and get name from there try { const attr = xpath.select( `/${this.xPathName}/@name`, this.getElement(), ); this.name = (attr[0] && attr[0].value) || "undefined"; } catch (e) { debug(`While getting name: ${e}`); this.name = "undefined"; } } return this.name; } getRevision() { if (!this.revision) { //look at the root of the proxy for the .xml file and get name from there try { const attr = xpath.select( `/${this.xPathName}/@revision`, this.getElement(), ); this.revision = (attr[0] && attr[0].value) || "undefined"; } catch (e) { debug(`While getting revision: ${e}`); this.revision = "undefined"; } } return this.revision; } #isFile = (spec) => { const exists = fs.existsSync(spec), stats = exists && fs.lstatSync(spec); return stats && stats.isFile(); }; getReport(cb) { const result = [this.report]; const appendCollection = (collection) => collection.forEach((item) => result.push(item.getReport())); appendCollection(this.getEndpoints()); // for either apiproxy or sharedflow appendCollection(this.getPolicies()); appendCollection(this.getResources()); const d = DEBUG("apigeelint:directives"); if (this.ignoreDirectives) { d(`not considering apigeelint disable directives in files.`); } else { // apply directives to optionally disable some errors/warnings result.forEach((item) => { if (item.messages && item.messages.length) { d(`item ${item.filePath} has ${item.messages.length} messages`); if ( item.filePath && item.filePath.endsWith(".xml") && this.#isFile(item.filePath) ) { try { const docElement = new Dom(domOptions).parseFromString( fs.readFileSync(item.filePath).toString(), ).documentElement; const directives = lintUtil.findDirectives(docElement); d(JSON.stringify(directives, null, 2)); if (directives.length > 0) { const keep = (item) => { d("checking " + JSON.stringify(item)); const directivesForThisRule = directives.filter((directive) => directive.disable.includes(item.ruleId), ); /* * If there is a directive specifying the ruleId for this message * appears, and if it is on the line preceding where the error/warning * was flagged, or if there is no line number for the error/warning, * discard the it. **/ return ( directivesForThisRule.length == 0 || (item.line && !directivesForThisRule.find( (d) => d.line + 1 == item.line, )) || (!item.line && !directivesForThisRule.find( (d) => d.line == 2 || d.line == 3, )) ); }; item.messages = item.messages.filter(keep); // update totals item.warningCount = item.messages.filter( (m) => m.severity == 1, ).length; item.errorCount = item.messages.filter( (m) => m.severity == 2, ).length; } } /* c8 ignore start */ catch (e) { /* nothing to do */ d("while scanning XML for directives: " + e.stack); } /* c8 ignore stop */ } } }); } if (cb) { cb(result); } return result; } addMessage(msg) { if (msg.plugin) { msg.ruleId = msg.plugin.ruleId; if (!msg.severity) msg.severity = msg.plugin.severity; msg.nodeType = msg.plugin.nodeType; delete msg.plugin; } if (!msg.entity) { msg.entity = this; msg.source = "Bundle"; } if ( !msg.source && msg.entity && typeof msg.entity.getSource == "function" ) { msg.source = msg.entity.getSource(); } if ( typeof msg.line == "undefined" && msg.entity && typeof msg.entity.getElement == "function" ) { const element = msg.entity.getElement(); if (element) { msg.line = element.lineNumber; if (!msg.column) { msg.column = element.columnNumber; } } } delete msg.entity; this.report.messages.push(msg); //Severity should be one of the following: 0 = off, 1 = warning, 2 = error switch (msg.severity) { case 1: this.report.warningCount++; break; case 2: this.report.errorCount++; break; } } getPolicyByName(pname) { return this.getPolicies().find((policy) => policy.getName() === pname); } onBundle(pluginFunction, cb) { pluginFunction(this, cb); } onPolicies(pluginFunction, cb) { this.getPolicies().forEach((policy) => pluginFunction(policy, getcb(`policy '${policy.getName()}'`)), ); cb(null, {}); } onSteps(pluginFunction, callback) { const bundle = this, proxies = bundle.getProxyEndpoints(), targets = bundle.getTargetEndpoints(); debug(`onSteps: bundle name: '${bundle.getName()}'`); try { if (proxies && proxies.length > 0) { proxies.forEach((ep) => ep.onSteps( pluginFunction, getcb(`STEP proxyendpoint '${ep.getName()}'`), ), ); } else { debug("no proxyEndpoints"); } if (targets && targets.length > 0) { targets.forEach((ep) => ep.onSteps( pluginFunction, getcb(`STEP targetendpoint '${ep.getName()}'`), ), ); } else { debug("no targetEndpoints"); } } /* c8 ignore start */ catch (exc1) { debug("exception: " + exc1); debug(exc1.stack); } /* c8 ignore stop */ callback(null, {}); } onConditions(pluginFunction, callback) { const bundle = this, proxies = bundle.getProxyEndpoints(), targets = bundle.getTargetEndpoints(); debug(`onConditions: bundle name: '${bundle.getName()}'`); try { if (proxies && proxies.length > 0) { proxies.forEach((ep) => ep.onConditions( pluginFunction, getcb(`COND proxyendpoint '${ep.getName()}'`), ), ); } if (targets && targets.length > 0) { targets.forEach((ep) => ep.onConditions( pluginFunction, getcb(`COND targetendpoint '${ep.getName()}'`), ), ); } } /* c8 ignore start */ catch (exc1) { debug("exception: " + exc1); debug(exc1.stack); } /* c8 ignore stop */ callback(null, {}); } onResources(pluginFunction, cb) { const bundle = this; if (bundle.getResources()) { bundle .getResources() .forEach((re) => pluginFunction(re, getcb(`resource '${re.getFileName()}'`)), ); } cb(null, {}); } onFaultRules(pluginFunction, cb) { if (this.getFaultRules()) { this.getFaultRules().forEach( (fr) => fr && pluginFunction(fr, getcb(`faultrule '${fr.getName()}'`)), ); } cb(null, {}); } onDefaultFaultRules(pluginFunction, cb) { if (this.getDefaultFaultRules()) { this.getDefaultFaultRules().forEach( (dfr) => dfr && pluginFunction(dfr, getcb("defaultfaultrule")), ); } cb(null, {}); } onProxyEndpoints(pluginFunction, cb) { const eps = this.getProxyEndpoints(); if (eps && eps.length > 0) { eps.forEach((ep) => pluginFunction(ep, getcb(`PEP proxyendpoint '${ep.getName()}'`)), ); } cb(null, {}); } onTargetEndpoints(pluginFunction, cb) { const eps = this.getTargetEndpoints(); if (eps && eps.length > 0) { eps.forEach((ep) => pluginFunction(ep, getcb(`TEP targetendpoint '${ep.getName()}'`)), ); } cb(null, {}); } getProxyEndpoints() { if (!this.proxyEndpoints) { if (this.bundleTypeName === bundleType.BundleType.SHAREDFLOW) { buildsharedflows(this); } else { buildProxyEndpoints(this); } if (!this.proxyEndpoints) { this.proxyEndpoints = []; } } return this.proxyEndpoints; } getTargetEndpoints() { if (!this.targetEndpoints) { buildTargetEndpoints(this); if (!this.targetEndpoints) { this.targetEndpoints = []; } } return this.targetEndpoints; } getEndpoints() { if (!this.endpoints) { this.endpoints = this.getProxyEndpoints() || []; if (this.bundleTypeName !== bundleType.BundleType.SHAREDFLOW) { this.endpoints = this.endpoints.concat(this.getTargetEndpoints()); } } return this.endpoints; } getResources() { if (!this.resources) { buildResources(this); } return this.resources; } getPolicies() { if (!this.policies) { buildPolicies(this); } return this.policies; } getFaultRules() { if (!this.faultRules) { this.faultRules = this.getEndpoints().reduce( (a, ep) => [...a, ep.getFaultRules()], [], ); } return this.faultRules; } getDefaultFaultRules() { if (!this.defaultFaultRules) { this.defaultFaultRules = this.getEndpoints().reduce( (a, ep) => [...a, ep.getDefaultFaultRule()], [], ); } return this.defaultFaultRules; } summarize() { const summary = { report: this.getReport(), root: this.root, name: this.getName(), revision: this.getRevision(), policies: this.getPolicies(), proxyEndpoints: this.getProxyEndpoints() ? this.getProxyEndpoints().map((ep) => ep.summarize()) : [], targetEndpoints: this.getTargetEndpoints() ? this.getTargetEndpoints().map((ep) => ep.summarize()) : [], resources: this.getResources() ? this.getResources().map((re) => re.summarize()) : [], policies: this.getPolicies() ? this.getPolicies().map((po) => po.summarize()) : [], }; return summary; } } //Public module.exports = Bundle;