UNPKG

apigee-edge-js

Version:

nodejs library for the administration API for Apigee (Edge and X and hybrid).

960 lines (913 loc) 29.4 kB
// deployableAsset.js // ------------------------------------------------------------------ // Copyright 2018-2023 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. // /* global process */ /* jshint node:true, strict:implied, esversion:9 */ const utility = require("./utility.js"), common = require("./common.js"), fs = require("fs"), path = require("path"), AdmZip = require("adm-zip"), archiver = require("archiver"), xml2js = require("xml2js"), qs = require("qs"), request = require("postman-request"), urljoin = require("url-join"), sprintf = require("sprintf-js").sprintf, DEFAULT_DELAY_OVERRIDE = 8; // for debugging //require('request-debug')(request); // ======================================================================================== // functions used by ApiProxy and SharedFlow function get(collectionName, conn, options, cb) { return common.insureFreshToken(conn, function (requestOptions) { if (options.revision) { if (!options.name) { return cb({ error: "The name is required when specifying a revision" }); } requestOptions.url = options.policy ? urljoin( conn.urlBase, collectionName, options.name, "revisions", options.revision, "policies", options.policy ) : options.proxyendpoint ? urljoin( conn.urlBase, collectionName, options.name, "revisions", options.revision, "proxies", options.proxyendpoint ) : urljoin( conn.urlBase, collectionName, options.name, "revisions", options.revision ); } else { requestOptions.url = options.name ? urljoin(conn.urlBase, collectionName, options.name) : urljoin(conn.urlBase, collectionName); } if (conn.verbosity > 0) { utility.logWrite(sprintf("GET %s", requestOptions.url)); } request.get(requestOptions, common.callback(conn, [200], cb)); }); } function update(collectionName, conn, options, value, cb) { return common.insureFreshToken(conn, function (requestOptions) { if (options.revision) { if (!options.name) { return cb({ error: "The name is required when specifying a revision" }); } requestOptions.url = options.policy ? urljoin( conn.urlBase, collectionName, options.name, "revisions", options.revision, "policies", options.policy ) : options.proxyendpoint ? urljoin( conn.urlBase, collectionName, options.name, "revisions", options.revision, "proxies", options.proxyendpoint ) : urljoin( conn.urlBase, collectionName, options.name, "revisions", options.revision ); } else { requestOptions.url = options.name ? urljoin(conn.urlBase, collectionName, options.name) : urljoin(conn.urlBase, collectionName); } requestOptions.body = JSON.stringify(value); requestOptions.headers["content-type"] = "application/json"; if (conn.verbosity > 0) { utility.logWrite(sprintf("POST %s", requestOptions.url)); } request.post(requestOptions, common.callback(conn, [200], cb)); }); } function del(collectionName, conn, options, cb) { if (!options.name) { return cb({ error: "The name is required" }); } common.insureFreshToken(conn, function (requestOptions) { if (options.revision) { if (conn.verbosity > 0) { utility.logWrite( sprintf( "Delete from %s: %s r%s ", collectionName, options.name, options.revision, options.policy ? "(" + options.policy + ")" : "" ) ); } requestOptions.url = options.policy ? urljoin( conn.urlBase, collectionName, options.name, "revisions", options.revision, "policies", options.policy ) : urljoin( conn.urlBase, collectionName, options.name, "revisions", options.revision ); } else { if (conn.verbosity > 0) { utility.logWrite( sprintf("Delete from %s: %s", collectionName, options.name) ); } requestOptions.url = urljoin(conn.urlBase, collectionName, options.name); } if (conn.verbosity > 0) { utility.logWrite(sprintf("DELETE %s", requestOptions.url)); } request.del(requestOptions, common.callback(conn, [200], cb)); }); } function getPoliciesForRevision(conn, assetType, collectionName, options, cb) { // GET :mgmtserver/v1/o/:orgname/COLLECTIONNAME/:api/revisions/:REV/policies if (!options.name) { return cb({ error: "missing name for " + assetType }); } if (!options.revision) { return cb({ error: "missing revision for " + assetType }); } common.insureFreshToken(conn, function (requestOptions) { requestOptions.url = urljoin( conn.urlBase, collectionName, options.name, "revisions", options.revision, "policies" ); if (options.policy) { requestOptions.url = urljoin(requestOptions.url, options.policy); } if (conn.verbosity > 0) { utility.logWrite(sprintf("GET %s", requestOptions.url)); } request.get(requestOptions, common.callback(conn, [200], cb)); }); } function getResourcesForRevision(conn, assetType, collectionName, options, cb) { if (!options.name) { return cb({ error: "missing name for " + assetType }); } if (!options.revision) { return cb({ error: "missing revision for " + assetType }); } common.insureFreshToken(conn, function (requestOptions) { requestOptions.url = urljoin( conn.urlBase, collectionName, options.name, "revisions", options.revision, "resources" ); if (options.resource) { requestOptions.url = urljoin(requestOptions.url, options.resource); } if (conn.verbosity > 0) { utility.logWrite(sprintf("GET %s", requestOptions.url)); } request.get(requestOptions, common.callback(conn, [200], cb)); }); } function getDeployments(conn, assetType, collectionName, options, cb) { // GET :mgmtserver/v1/o/:orgname/COLLECTIONNAME/:asset/revisions/:rev/deployments // or // GET :mgmtserver/v1/o/:orgname/e/:env/COLLECTIONNAME/:asset/revisions/:rev/deployments // or // GET :mgmtserver/v1/o/:orgname/COLLECTIONNAME/:asset/deployments // or // GET :mgmtserver/v1/o/:orgname/e/:env/COLLECTIONNAME/:asset/deployments // or // GET :mgmtserver/v1/o/:orgname/deployments // or // GET :mgmtserver/v1/o/:orgname/e/:env/deployments common.insureFreshToken(conn, function (requestOptions) { let env = options.env || options.environment; let workingUrl = env ? urljoin(conn.urlBase, "environments", env) : conn.urlBase; if (!options.name) { // This is a short path for "api deployments", // does not work for sharedflows requestOptions.url = urljoin(workingUrl, "deployments"); } else { requestOptions.url = options.revision ? urljoin( workingUrl, collectionName, options.name, "revisions", options.revision, "deployments" ) : urljoin(workingUrl, collectionName, options.name, "deployments"); } if (conn.verbosity > 0) { utility.logWrite(sprintf("GET %s", requestOptions.url)); } request.get(requestOptions, common.callback(conn, [200, 400], cb)); }); } function getRevisions(conn, assetType, collectionName, options, cb) { // GET :mgmtserver/v1/o/:orgname/COLLECTIONNAME/:api/revisions if (!options.name) { return cb({ error: "missing name for " + assetType }); } common.insureFreshToken(conn, function (requestOptions) { requestOptions.url = urljoin( conn.urlBase, collectionName, options.name, "revisions" ); if (conn.verbosity > 0) { utility.logWrite(sprintf("GET %s", requestOptions.url)); } request.get(requestOptions, common.callback(conn, [200, 404], cb)); }); } function getCollectionNameForAssetType(assetType) { var supportedTypes = { apiproxy: "apis", proxy: "apis", sharedflowbundle: "sharedflows" }; return supportedTypes[assetType]; } function undeploy(conn, options, assetType, cb) { // DELETE :mgmtserver/v1/o/:orgname/e/:envname/apis/:proxyname/revisions/:revnum/deployments // Authorization: :apigee-auth const env = options.environment && options.environment.name ? options.environment.name : options.environment || options.env; if (!env) { return cb(new Error("The required param environment is missing")); } const collection = getCollectionNameForAssetType(assetType); if (!collection) { return cb(new Error("The assetType is not supported")); } const rev = options.revision ? options.revision.name || options.revision : null; const undeployRevision = function (name, rev, cb) { if (conn.verbosity > 0) { utility.logWrite( sprintf("Undeploy %s %s r%s from env:%s", assetType, name, rev, env) ); } common.insureFreshToken(conn, function (requestOptions) { requestOptions.url = urljoin( conn.urlBase, "environments", env, collection, name, "revisions", rev, "deployments" ); if (conn.verbosity > 0) { utility.logWrite(sprintf("DELETE %s", requestOptions.url)); } request.del(requestOptions, common.callback(conn, [200], cb)); }); }; if (rev) { undeployRevision(options.name, rev, cb); } else { // inquire deployments and undeploy all that are deployed in this env if (conn.verbosity > 0) { utility.logWrite( sprintf( "Get deployments for %s %s in env:%s", assetType, options.name, env ) ); } getDeployments(conn, assetType, collection, options, function (e, result) { if (conn.verbosity > 0) { utility.logWrite("Deployments: " + JSON.stringify(result)); } if ( result.deployments && result.deployments[0] && result.deployments[0].environment ) { // GAAMBO: // {"deployments":[{"environment":"e1","apiProxy":"jwt20190820","revision":"3","deployStartTime":"1612391239249"}]} const reducer = (p, item) => p.then( (a) => new Promise((resolve, reject) => undeployRevision(item.apiProxy, item.revision, (e, result) => { return resolve([...a, { rev: item.revision, error: e }]); }) ) ); let selectedDeployments = result.deployments.filter( (x) => x.environment == env ); if (selectedDeployments) { return selectedDeployments .reduce(reducer, Promise.resolve([])) .then((a) => cb(null, a)); } else { // no deployments in the specified environment return cb(null, {}); } } else if (result.environment && result.environment[0]) { // classic API. const reducer = (promise, rev) => promise.then( (accumulator) => new Promise((resolve, reject) => undeployRevision(options.name, rev.name, (e, result) => { return resolve([...accumulator, { rev: rev.name, error: e }]); }) ) ); //console.log(JSON.stringify(result)); let selectedEnv = result.revision && result.revision[0] && result.environment == env ? // single environment deployed. result : // multiple environments deployed. result.environment.find((x) => x.name == env); if (selectedEnv) { return selectedEnv.revision .reduce(reducer, Promise.resolve([])) .then((a) => cb(null, a)); } else { // no deployments in the specified environment return cb(null, {}); } } else { // no deployments to undeploy return cb(e, result); } }); } } function deploy(conn, options, assetType, cb) { // POST \ // -H content-type:application/x-www-form-urlencoded \ // "${mgmtserver}/v1/o/${org}/e/${environment}/apis/${proxyname}/revisions/${rev}/deployments" \ // -d 'override=true&delay=60' let qparams = { override: options.hasOwnProperty("override") ? options.override : true, // The service account represents the identity of the deployed proxy, and // determines what permissions it has. The format must be // {ACCOUNT_ID}@{PROJECT}.iam.gserviceaccount.com. serviceAccount: options.serviceAccount }; if (conn.urlBase.indexOf("apigee.googleapis.com") < 0) { qparams.delay = options.hasOwnProperty("delay") ? options.delay : DEFAULT_DELAY_OVERRIDE; } const env = options.environment && options.environment.name ? options.environment.name : options.environment || options.env; if (!env) { return cb(new Error("The required param environment is missing")); } let collection = getCollectionNameForAssetType(assetType); if (!collection) { return cb(new Error("The assetType is not supported")); } if (options.basepath) { if (assetType == "apiproxy") { qparams.basepath = options.basepath; } else { return cb({ error: "incorrect arguments - basepath is not supported" }); } } const rev = options.revision ? options.revision.name || options.revision : null; const deployRevision = function (name, rev) { if (conn.verbosity > 0) { utility.logWrite( sprintf("deploy %s %s r%d to env:%s", assetType, name, rev, env) ); } common.insureFreshToken(conn, function (requestOptions) { requestOptions.headers["content-type"] = "application/x-www-form-urlencoded"; requestOptions.body = qs.stringify(qparams); requestOptions.url = urljoin( conn.urlBase, "environments", env, collection, name, "revisions", rev, "deployments" ); if (conn.verbosity > 0) { utility.logWrite( sprintf( "POST %s", requestOptions.url + "\n " + requestOptions.body ) ); } request.post(requestOptions, common.callback(conn, [200], cb)); }); }; if (rev) { deployRevision(options.name, rev); } else { // inquire revisions and deploy the latest if (conn.verbosity > 0) { utility.logWrite( sprintf("Get revisions for %s %s", assetType, options.name) ); } getRevisions(conn, assetType, collection, options, function (e, result) { if (conn.verbosity > 0) { utility.logWrite("Revisions: " + JSON.stringify(result)); } if (e || !Array.isArray(result)) { return cb(e || new Error("failed to get revisions"), result); } result = result.map(Number).sort((a, b) => b - a); deployRevision(options.name, result[0]); }); } } function getDatestring() { let datestring = new Date() .toISOString() .replace(/-/g, "") .replace(/:/g, "") .replace("T", "-") .replace(/\.[0-9]+Z/, ""); return datestring; } function export0(conn, assetType, collectionName, options, cb) { if (!options.name) { return cb({ error: sprintf("missing name for %s", assetType) }); } const exportOneAssetRevision = function (requestOptions, revision) { if (!revision) { return cb({ error: sprintf("missing revision for %s", assetType) }); } if (conn.verbosity > 0) { utility.logWrite( sprintf("Export %s %s %s", assetType, options.name, revision) ); } requestOptions.url = urljoin( conn.urlBase, collectionName, options.name, "revisions", revision ) + "?format=bundle"; requestOptions.headers.accept = "*/*"; // not application/octet-stream ! requestOptions.encoding = null; // necessary to get if (conn.verbosity > 0) { utility.logWrite(sprintf("GET %s", requestOptions.url)); } request.get( requestOptions, common.callback(conn, [200], function (e, result) { // The filename in the response is meaningless, like this: // content-disposition: 'attachment; filename="apiproxy3668830505762375956.zip" // Here, we create a meaningful filename, but it's just a suggestion. The caller // is responsible for saving the buffer to the filename. if (e) return cb(e, result); let suggestedFilename = sprintf( "%s-%s-%s-r%s-%s.zip", assetType, conn.orgname, options.name, revision, getDatestring() ); // fs.writeFileSync(filename, result); return cb(e, { filename: suggestedFilename, buffer: result }); }) ); }; return common.insureFreshToken(conn, function (requestOptions) { if (!options.revision) { let collection = assetType == "sharedflow" ? conn.org.sharedflows : conn.org.proxies; collection.getRevisions({ name: options.name }, function (e, result) { if (e) { return cb(e, result); } //console.log('got revisions: ' + JSON.stringify(result)); let latestRevision = result[result.length - 1]; exportOneAssetRevision(requestOptions, latestRevision); }); } else { exportOneAssetRevision(requestOptions, options.revision); } }); } function needNpmInstall(collection, zipArchive) { if (collection != "apis") { return false; } let foundPackage = false, foundNodeModules = false, zip = new AdmZip(zipArchive), zipEntries = zip.getEntries(); zipEntries.forEach(function (entry) { if (entry.entryName == "apiproxy/resources/node/package.json") { foundPackage = true; } if (entry.entryName == "apiproxy/resources/node/node_modules.zip") { foundNodeModules = true; } }); return foundPackage && !foundNodeModules; } function runNpmInstall(conn, options, cb) { // POST :mgmtserver/v1/o/:orgname/apis/:apiname/revisions/:revnum/npm // -H content-type:application/x-www-form-urlencoded \ // -d 'command=install' if (conn.verbosity > 0) { utility.logWrite( sprintf("npm install %s r%d", options.name, options.revision) ); } common.insureFreshToken(conn, function (requestOptions) { requestOptions.url = urljoin( conn.urlBase, "apis", options.name, "revisions", options.revision, "npm" ); requestOptions.body = "command=install"; requestOptions.headers["content-type"] = "application/x-www-form-urlencoded"; if (conn.verbosity > 0) { utility.logWrite(sprintf("POST %s", requestOptions.url)); } request.post(requestOptions, common.callback(conn, [200], cb)); }); } function importFromZip(conn, assetName, assetType, zipArchive, cb) { // eg, // curl -X POST -H Content-Type:application/octet-stream "${mgmtserver}/v1/o/$org/apis?action=import&name=$proxyname" -T $zipname // or // curl -X POST -H content-type:application/octet-stream "${mgmtserver}/v1/o/$org/sharedflows?action=import&name=$sfname" -T $zipname // or // curl -X POST -H content-type:multipart/form-data "${mgmtserver}/v1/o/$org/sharedflows?action=import&name=$sfname" -F file=$zipname if (!fs.existsSync(zipArchive)) { return cb({ error: "The archive does not exist" }); } let collection = getCollectionNameForAssetType(assetType); if (!collection) { return cb({ error: "The assetType is not supported" }); } common.insureFreshToken(conn, function (requestOptions) { requestOptions.headers["content-type"] = conn.urlBase.indexOf("apigee.googleapis.com") > 0 ? "multipart/form-data" : "application/octet-stream"; requestOptions.url = urljoin( conn.urlBase, collection + "?action=import&name=" + assetName ); if (conn.verbosity > 0) { utility.logWrite(sprintf("POST %s", requestOptions.url)); } var afterImport = function (e, result) { if (conn.verbosity > 0) { if (e) { utility.logWrite("Import error: " + JSON.stringify(e)); } else { utility.logWrite("Import result: " + JSON.stringify(result)); } } if (e) { return cb(e, result); } if (!needNpmInstall(collection, zipArchive)) { return cb(null, result); } runNpmInstall( conn, { name: result.name, revision: result.revision }, // Return the result from the import, not the // result from the install. function (e2, result2) { if (e2) { return cb(e2, result2); } cb(e, result); } ); }; if (conn.urlBase.indexOf("apigee.googleapis.com") > 0) { requestOptions.formData = { file: fs.createReadStream(zipArchive) }; request.post( requestOptions, common.callback(conn, [200, 201], afterImport) ); } else { fs.createReadStream(zipArchive).pipe( request.post(requestOptions, common.callback(conn, [201], afterImport)) ); } }); } function verifyPathIsDir(dir, cb) { let resolvedPath = path.resolve(dir); fs.lstat(resolvedPath, function (e, stats) { if (e) return cb(e); if (!stats.isDirectory()) { return cb({ message: "The path " + resolvedPath + " is not a directory" }); } return cb(null); }); } function walkDirectory(dir, done) { let results = []; fs.readdir(dir, function (err, list) { if (err) return done(err); var i = 0; (function next() { var file = list[i++]; if (!file) return done(null, results); file = dir + "/" + file; fs.stat(file, function (err, stat) { if (stat && stat.isDirectory()) { walkDirectory(file, function (err, res) { results = results.concat(res); next(); }); } else { results.push(file); next(); } }); })(); }); } function includeInBundle(name) { // exclude backup files if (name.endsWith("~")) return false; // exclude configuration files for JavaScript if (name.endsWith(".jshintrc")) return false; if (name.endsWith(".jslintrc")) return false; // exclude status files for JavaScript tern utility if (name.endsWith(".tern-port")) return false; //if (name.endsWith('node_modules.zip')) return false; if (name.indexOf("/node_modules/") > 0) return false; let b = path.basename(name); // exclude emacs temporary files if (b.endsWith("#") && b.startsWith("#")) return false; if (b.startsWith(".#")) return false; return true; } function produceBundleZip(srcDir, assetType, verbosity, cb) { let pathToZip = path.resolve(path.join(srcDir, assetType)); verifyPathIsDir(pathToZip, function (e) { if (e) { return cb(e); } let tmpdir = process.env.tmpdir || "/tmp", rando = Math.random().toString(36).slice(2), archiveName = path.join( tmpdir, assetType + "-" + new Date().getTime() + "-" + rando + ".zip" ), outs = fs.createWriteStream(archiveName), archive = archiver("zip"); outs.on("close", function () { if (verbosity > 0) { utility.logWrite("zipped " + archive.pointer() + " total bytes"); } cb(null, archiveName); }); archive.on("error", function (e) { cb(e, archiveName); }); archive.pipe(outs); walkDirectory(pathToZip, function (e, results) { results.filter(includeInBundle).forEach(function (filename) { let shortName = filename.replace(pathToZip, assetType); archive.append(fs.createReadStream(filename), { name: shortName }); }); archive.finalize(); }); }); } function importFromDir(conn, name, assetType, srcDir, cb) { if (["apiproxy", "sharedflowbundle"].indexOf(assetType) < 0) { return cb({ error: "unknown assetType" }); } var resolvedPath = path.resolve(srcDir); if (conn.verbosity > 0) { utility.logWrite( sprintf("import %s %s from dir %s", assetType, name, resolvedPath) ); } verifyPathIsDir(srcDir, function (e) { if (e) { return cb(e); } produceBundleZip( srcDir, assetType, conn.verbosity, function (e, archiveName) { if (e) return cb(e); //console.log('archivename: %s', archiveName); importFromZip(conn, name, assetType, archiveName, function (e, result) { if (e) { return cb(e, result); } fs.unlinkSync(archiveName); cb(null, result); }); } ); }); } function findXmlFiles(dir, cb) { // from within a directory, find the XML files let xmlfiles = []; fs.readdir(dir, function (e, items) { if (e) return cb(e); let i = 0; (function next() { let file = items[i++]; if (!file) return cb(null, xmlfiles); file = dir + "/" + file; fs.stat(file, function (e, stat) { if (e) return cb(e); if (stat && stat.isFile() && file.endsWith(".xml")) { xmlfiles.push(file); } next(); }); })(); }); } function doParseForName(conn, data, cb) { let parser = new xml2js.Parser(); parser.parseString(data, function (e, result) { if (e) return cb(e); if (result.SharedFlowBundle) { if (conn.verbosity > 0) { utility.logWrite( sprintf("found name: %s", result.SharedFlowBundle.$.name) ); } return cb(null, result.SharedFlowBundle.$.name); } if (result.APIProxy) { if (conn.verbosity > 0) { utility.logWrite(sprintf("found name: %s", result.APIProxy.$.name)); } return cb(null, result.APIProxy.$.name); } cb({ error: "cannot determine asset name" }); }); } function inferAssetNameFromDir(conn, dir, cb) { findXmlFiles(dir, function (e, files) { if (e) return cb(e); if (files.length != 1) return cb({ error: sprintf("found %d files, expected 1", files.length) }); fs.readFile(files[0], "utf8", function (e, data) { if (e) return cb(e); doParseForName(conn, data, cb); }); }); } function inferAssetNameFromZip(conn, source, cb) { // temporarily unzip the file and then scan the dir let toplevelXmlRe = new RegExp("^apiproxy/[^/]+\\.xml$"), zip = new AdmZip(source), zipEntries = zip.getEntries(), foundit = false; zipEntries.forEach(function (entry) { if (!foundit) { if (toplevelXmlRe.test(entry.entryName)) { let data = entry.getData(); doParseForName(conn, data.toString("utf8"), cb); foundit = true; } } }); } function import0(conn, options, assetType, cb) { let source = path.resolve(options.source); fs.stat(source, function (e, stat) { if (e) return cb(e); if (!stat) return cb({ error: "stat null" }); if (stat.isFile() && source.endsWith(".zip")) { if (options.name) { return importFromZip(conn, options.name, assetType, source, cb); } return inferAssetNameFromZip(conn, source, function (e, name) { if (e) return cb(e); importFromZip(conn, name, assetType, source, cb); }); } else if (stat.isDirectory()) { if (options.name) { return importFromDir(conn, options.name, assetType, source, cb); } return inferAssetNameFromDir( conn, path.join(source, assetType), function (e, name) { if (e) return cb(e); importFromDir(conn, name, assetType, source, cb); } ); } else { return cb({ error: "source represents neither a zip nor a directory." }); } }); } module.exports = { importFromZip: importFromZip, importFromDir: importFromDir, import0: import0, export0: export0, get: get, update: update, deploy: deploy, undeploy: undeploy, del: del, getRevisions: getRevisions, getDeployments: getDeployments, getPoliciesForRevision: getPoliciesForRevision, getResourcesForRevision: getResourcesForRevision };