diff --git a/README.md b/README.md index 759109e..e0b7e82 100644 --- a/README.md +++ b/README.md @@ -170,6 +170,9 @@ for organization name, all of which are required. `--virtualhosts -v` (optional) A comma-separated list of virtual hosts that the deployed app will use. The two most common options are `default` and `secure`. The `default` option is always HTTP and `secure` is always HTTPS. By default, `apigeetool deploynodeapp` uses `default,secure`. +`--bundled-dependencies` +(optional) If specified, the source code will be uploaded with its `bundledDependencies` as defined in the `package.json`. + ## deployhostedfunction Deploys a Hosted Function to Apigee Edge as an API proxy. With your Hosted Function deployed to Edge, you can take advantage of Edge features like security, quotas, caching, analytics, trace tool, and more. @@ -220,6 +223,9 @@ The name of the API proxy. The name of the API proxy must be unique within an or `--virtualhosts -v` (optional) A comma-separated list of virtual hosts that the deployed app will use. The two most common options are `default` and `secure`. The `default` option is always HTTP and `secure` is always HTTPS. By default, `apigeetool deployhostedfunction` uses `default,secure`. +`--bundled-dependencies` +(optional) If specified, the source code will be uploaded with its `bundledDependencies` as defined in the `package.json`. + ## deployproxy Deploys an API proxy to Apigee Edge. If the proxy is currently deployed, it will be undeployed first, and the newly deployed proxy's revision number is incremented. @@ -263,6 +269,9 @@ for organization name, all of which are required. `--upload-modules -U` (optional) If specified, uploads Node.js modules from your system to Apigee Edge. +`--bundled-dependencies` +(optional) If specified, the `node` & `hosted` resources will be uploaded with their `bundledDependencies` as defined in their respective `package.json` files. + ## undeploy Undeploys a named API proxy or Node.js app deployed on Apigee Edge. diff --git a/lib/commands/deployhostedfunction.js b/lib/commands/deployhostedfunction.js index 25da8aa..96e2a88 100644 --- a/lib/commands/deployhostedfunction.js +++ b/lib/commands/deployhostedfunction.js @@ -4,7 +4,7 @@ var util = require('util'); var path = require('path'); var async = require('async'); -var fs = require('fs'); +var fs = require('fs-extra'); var mustache = require('mustache'); var _ = require('underscore'); var tmp = require('tmp'); @@ -22,6 +22,8 @@ var deployProxy = require('../deploycommon').deployProxy; var unzipProxy = require('../deploycommon').unzipProxy; var copyFile = require('../deploycommon').copyFile; var handleUploadResult = require('../deploycommon').handleUploadResult; +var usePackedSource = require('../deploycommon').usePackedSource; +var uploadSource = require('../deploycommon').uploadSource; const APP_YAML = 'app.yaml'; @@ -58,6 +60,15 @@ var descriptor = defaults.defaultDescriptor({ name: 'Preserve policies from previous revision', shortOption: 'P', toggle: true + }, + 'bundled-dependencies' : { + name: 'Upload dependencies from bundledDependencies', + toggle: true + }, + 'upload-modules': { + name: 'Upload Modules', + shortOption: 'U', + toggle: true } }); module.exports.descriptor = descriptor; @@ -97,24 +108,46 @@ module.exports.run = function(opts, cb) { return preservePoliciesRun(opts, cb); } + opts.remoteNpm = true; + if (opts['upload-modules'] && (opts['upload-modules'] === true)) { + opts.remoteNpm = false; + } + + var steps = [ + function(done) { + createApiProxy(opts, request, done); + } + ] + + if (opts['bundled-dependencies']) { + opts.remoteNpm = false; + + steps.push(function(done) { + usePackedSource(opts.directory, opts, function(err, packedDirectory) { + // set the target directory to upload to the packed directory + opts.directory = packedDirectory + return done(err); + }); + }) + } + + steps = steps.concat([ + function(done) { + uploadSource(opts.directory, 'hosted', opts, request, done); + }, + function(done) { + createTarget(opts, request, done); + }, + function(done) { + createProxy(opts, request, done); + }, + function(done) { + deployProxy(opts, request, done); + } + ]) + // Run each function in series, and collect an array of results. - async.series([ - function(done) { - createApiProxy(opts, request, done); - }, - function(done) { - uploadHostedSource(opts, request, done); - }, - function(done) { - createTarget(opts, request, done); - }, - function(done) { - createProxy(opts, request, done); - }, - function(done) { - deployProxy(opts, request, done); - } - ], + async.series(steps, function(err, results) { if (err) { return cb(err); } if (opts.debug) { console.log('results: %j', results); } @@ -182,7 +215,7 @@ function copySources(opts, targetDir, cb) { if (opts.verbose) { console.log('Copying sources into proxy'); } // Get a list of entries, broken down by which are directories - ziputils.enumerateSources(opts.directory, function(err, entries) { + ziputils.enumerateDirectory(opts.directory, 'hosted', opts.remoteNpm, function(err, entries) { if (err) { return cb(err); } if (opts.debug) { console.log('Directories to copy: %j', entries); } @@ -257,62 +290,6 @@ function getDeploymentInfo(opts, request, done) { }); } -function uploadHostedSource(opts, request, done) { - - // Get a list of entries, broken down by which are directories - ziputils.enumerateSources(opts.directory, function(err, entries) { - if (err) { return done(err); } - - if (opts.debug) { console.log('Directories to upload: %j', entries); } - - async.eachLimit(entries, opts.asynclimit, function(entry, entryDone) { - var uri = - util.format('%s/v1/o/%s/apis/%s/revisions/%d/resources?type=hosted&name=%s', - opts.baseuri, opts.organization, opts.api, - opts.deploymentVersion, entry.resourceName); - if (entry.directory) { - // ZIP up all directories, possibly with additional file prefixes - ziputils.zipDirectory(entry.fileName, entry.zipEntryName, function(err, zipBuf) { - if (err) { - entryDone(err); - } else { - if (opts.verbose) { - console.log('Uploading resource %s of size %d', entry.resourceName, zipBuf.length); - } - request({ - uri: uri, - method: 'POST', - json: false, - headers: { 'Content-Type': 'application/octet-stream' }, - body: zipBuf - }, function(err, req, body) { - handleUploadResult(err, req, entry.fileName, entryDone); - }); - } - }); - - } else { - if (opts.verbose) { - console.log('Uploading resource %s', entry.resourceName); - } - var httpReq = request({ - uri: uri, - method: 'POST', - json: false, - headers: { 'Content-Type': 'application/octet-stream' } - }, function(err, req, body) { - handleUploadResult(err, req, entry.fileName, entryDone); - }); - - var fileStream = fs.createReadStream(entry.fileName); - fileStream.pipe(httpReq); - } - }, function(err) { - done(err); - }); - }); -} - // Create a target endpoint that references the Hosted Function target function createTarget(opts, request, done) { var targetDoc = mustache.render( diff --git a/lib/commands/deploynodeapp.js b/lib/commands/deploynodeapp.js index 84b0dc9..f9d97f8 100644 --- a/lib/commands/deploynodeapp.js +++ b/lib/commands/deploynodeapp.js @@ -22,6 +22,8 @@ var deployProxy = require('../deploycommon').deployProxy; var unzipProxy = require('../deploycommon').unzipProxy; var copyFile = require('../deploycommon').copyFile; var handleUploadResult = require('../deploycommon').handleUploadResult; +var usePackedSource = require('../deploycommon').usePackedSource; +var uploadSource = require('../deploycommon').uploadSource; // By default, do not run NPM remotely var DefaultResolveModules = false; @@ -65,6 +67,10 @@ var descriptor = defaults.defaultDescriptor({ shortOption: 'R', toggle: true }, + 'bundled-dependencies' : { + name: 'Upload dependencies from bundledDependencies', + toggle: true + }, 'upload-modules': { name: 'Upload Modules', shortOption: 'U', @@ -123,27 +129,44 @@ module.exports.run = function(opts, cb) { return preservePoliciesRun(opts, cb); } + var steps = [ + function(done) { + createApiProxy(opts, request, done); + } + ] + + if (opts['bundled-dependencies']) { + opts.remoteNpm = false; + + steps.push(function(done) { + usePackedSource(opts.directory, opts, function(err, packedDirectory) { + // set the target directory to upload to the packed directory + opts.directory = packedDirectory + return done(err); + }); + }) + } + + steps = steps.concat([ + function(done) { + uploadSource(opts.directory, 'node', opts, request, done); + }, + function(done) { + createTarget(opts, request, done); + }, + function(done) { + createProxy(opts, request, done); + }, + function(done) { + runNpm(opts, request, done); + }, + function(done) { + deployProxy(opts, request, done); + } + ]) + // Run each function in series, and collect an array of results. - async.series([ - function(done) { - createApiProxy(opts, request, done); - }, - function(done) { - uploadNodeSource(opts, request, done); - }, - function(done) { - createTarget(opts, request, done); - }, - function(done) { - createProxy(opts, request, done); - }, - function(done) { - runNpm(opts, request, done); - }, - function(done) { - deployProxy(opts, request, done); - } - ], + async.series(steps, function(err, results) { if (err) { return cb(err); } if (opts.debug) { console.log('results: %j', results); } @@ -212,7 +235,7 @@ function copyNodeSource(opts, targetDir, cb) { // Get a list of entries, broken down by which are directories, // and with special handling for the node_modules directory. - ziputils.enumerateNodeDirectory(opts.directory, opts.remoteNpm, function(err, entries) { + ziputils.enumerateDirectory(opts.directory, 'node', opts.remoteNpm, function(err, entries) { if (err) { return cb(err); } if (opts.debug) { console.log('Directories to copy: %j', entries); } @@ -313,63 +336,6 @@ function getDeploymentInfo(opts, request, done) { }); } -function uploadNodeSource(opts, request, done) { - - // Get a list of entries, broken down by which are directories, - // and with special handling for the node_modules directory. - ziputils.enumerateNodeDirectory(opts.directory, opts.remoteNpm, function(err, entries) { - if (err) { return done(err); } - - if (opts.debug) { console.log('Directories to upload: %j', entries); } - - async.eachLimit(entries, opts.asynclimit, function(entry, entryDone) { - var uri = - util.format('%s/v1/o/%s/apis/%s/revisions/%d/resources?type=node&name=%s', - opts.baseuri, opts.organization, opts.api, - opts.deploymentVersion, entry.resourceName); - if (entry.directory) { - // ZIP up all directories, possibly with additional file prefixes - ziputils.zipDirectory(entry.fileName, entry.zipEntryName, function(err, zipBuf) { - if (err) { - entryDone(err); - } else { - if (opts.verbose) { - console.log('Uploading resource %s of size %d', entry.resourceName, zipBuf.length); - } - request({ - uri: uri, - method: 'POST', - json: false, - headers: { 'Content-Type': 'application/octet-stream' }, - body: zipBuf - }, function(err, req, body) { - handleUploadResult(err, req, entry.fileName, entryDone); - }); - } - }); - - } else { - if (opts.verbose) { - console.log('Uploading resource %s', entry.resourceName); - } - var httpReq = request({ - uri: uri, - method: 'POST', - json: false, - headers: { 'Content-Type': 'application/octet-stream' } - }, function(err, req, body) { - handleUploadResult(err, req, entry.fileName, entryDone); - }); - - var fileStream = fs.createReadStream(entry.fileName); - fileStream.pipe(httpReq); - } - }, function(err) { - done(err); - }); - }); -} - // Create a target endpoint that references the Node.js script function createTarget(opts, request, done) { var targetDoc = mustache.render( @@ -400,7 +366,7 @@ function createTarget(opts, request, done) { } function runNpm(opts, request, done) { - if (!opts.remoteNpm) { + if (!opts.remoteNpm && !opts['bundled-dependencies']) { done(); } else { if (opts.verbose) { diff --git a/lib/commands/deployproxy.js b/lib/commands/deployproxy.js index 846c969..7c89a46 100644 --- a/lib/commands/deployproxy.js +++ b/lib/commands/deployproxy.js @@ -14,6 +14,8 @@ var fsutils = require('../fsutils'); var options = require('../options'); var ziputils = require('../ziputils'); var parseDeployments = require('./parsedeployments'); +var usePackedSource = require('../deploycommon').usePackedSource; +var uploadSource = require('../deploycommon').uploadSource; var ProxyBase = 'apiproxy'; var XmlExp = /(.+)\.xml$/i; @@ -22,6 +24,8 @@ var BASE_PATH_REGEXP = /]*>(.*?)<\/BasePath>/; // By default, do not run NPM remotely var DefaultResolveModules = false; +// used when bundled-dependencies is active to potentially force remoteNPM +var noNodeSource = false; var descriptor = defaults.defaultDescriptor({ api: { @@ -57,6 +61,10 @@ var descriptor = defaults.defaultDescriptor({ name: 'Upload Modules', shortOption: 'U', toggle: true + }, + 'bundled-dependencies' : { + name: 'Upload dependencies from bundledDependencies', + toggle: true } }); module.exports.descriptor = descriptor; @@ -290,7 +298,10 @@ function uploadResources(opts, request, done) { util.format('%s/v1/o/%s/apis/%s/revisions/%d/resources?type=%s&name=%s', opts.baseuri, opts.organization, opts.api, opts.deploymentVersion, entry.resourceType, entry.resourceName); - if (entry.directory) { + if(opts['bundled-dependencies'] && (entry.resourceType === 'node' || entry.resourceType === 'hosted')) { + // skip node/hosted files we will handle these special + return entryDone(); + } else if (entry.directory) { // ZIP up all directories, possibly with additional file prefixes ziputils.zipDirectory(entry.fileName, entry.zipEntryName, function(err, zipBuf) { if (err) { @@ -311,7 +322,6 @@ function uploadResources(opts, request, done) { }); } }); - } else { if (opts.verbose) { console.log('Uploading %s resource %s', entry.resourceType, entry.resourceName); @@ -329,7 +339,46 @@ function uploadResources(opts, request, done) { fileStream.pipe(httpReq); } }, function(err) { - done(err); + if (opts['bundled-dependencies']) { + var steps = []; + + // check for hosted resources + var hostedDir = path.join(resBaseDir, 'hosted'); + if (fs.existsSync(hostedDir)) { + steps.push(function(stepDone) { + // package hosted resources and upload the packed directory + usePackedSource(hostedDir, opts, function(err, packedDirectory) { + if (err) { + return stepDone(err); + } + + uploadSource(packedDirectory, 'hosted', opts, request, stepDone) + }); + }); + } + + // check for node resources + var nodeDir = path.join(resBaseDir, 'node'); + if (fs.existsSync(nodeDir)) { + steps.push(function(stepDone) { + // package node resources and upload the packed directory + usePackedSource(nodeDir, opts, function(err, packedDirectory) { + if (err) { + return stepDone(err); + } + + uploadSource(packedDirectory, 'node', opts, request, stepDone) + }); + }); + } else { + // no node resources, so do not force remoteNPM + noNodeSource = true; + } + + return async.series(steps, done); + } + + return done(err); }); } @@ -553,7 +602,7 @@ function uploadProxies(opts, request, done) { } function runNpm(opts, request, done) { - if (!opts.remoteNpm) { + if (!opts.remoteNpm && (!opts['bundled-dependencies'] || noNodeSource)) { done(); } else { if (opts.verbose) { diff --git a/lib/deploycommon.js b/lib/deploycommon.js index fb7b524..66b05c5 100644 --- a/lib/deploycommon.js +++ b/lib/deploycommon.js @@ -4,10 +4,15 @@ var async = require('async'); var util = require('util'); var path = require('path'); -var fs = require('fs'); +var fs = require('fs-extra'); +var tar = require('tar-fs'); +var zlib = require('zlib'); +var spawn = require('child_process').spawn; var mustache = require('mustache'); var unzip = require('node-unzip-2'); var _ = require('underscore'); +var tmp = require('tmp'); +tmp.setGracefulCleanup(); var ziputils = require('./ziputils'); @@ -242,6 +247,109 @@ module.exports.copyFile = function(source, target, cb) { }); } +module.exports.uploadSource = function(sourceDir, type, opts, request, done) { +// Get a list of entries, broken down by which are directories + ziputils.enumerateDirectory(sourceDir, type, opts.remoteNpm, function(err, entries) { + if (err) { return done(err); } + + if (opts.debug) { console.log('Directories to upload: %j', entries); } + + async.eachLimit(entries, opts.asynclimit, function(entry, entryDone) { + var uri = + util.format('%s/v1/o/%s/apis/%s/revisions/%d/resources?type=%s&name=%s', + opts.baseuri, opts.organization, opts.api, + opts.deploymentVersion, type, entry.resourceName); + if (entry.directory) { + // ZIP up all directories, possibly with additional file prefixes + ziputils.zipDirectory(entry.fileName, entry.zipEntryName, function(err, zipBuf) { + if (err) { + entryDone(err); + } else { + if (opts.verbose) { + console.log('Uploading resource %s of size %d', entry.resourceName, zipBuf.length); + } + request({ + uri: uri, + method: 'POST', + json: false, + headers: { 'Content-Type': 'application/octet-stream' }, + body: zipBuf + }, function(err, req, body) { + handleUploadResult(err, req, entry.fileName, entryDone); + }); + } + }); + + } else { + if (opts.verbose) { + console.log('Uploading resource %s', entry.resourceName); + } + var httpReq = request({ + uri: uri, + method: 'POST', + json: false, + headers: { 'Content-Type': 'application/octet-stream' } + }, function(err, req, body) { + handleUploadResult(err, req, entry.fileName, entryDone); + }); + + var fileStream = fs.createReadStream(entry.fileName); + fileStream.pipe(httpReq); + } + }, function(err) { + done(err); + }); + }); +} + +module.exports.usePackedSource = function(sourceDir, opts, cb) { + if (opts.debug) { + console.log('packaging bundled dependencies for upload') + } + + tmp.dir(function(err, tempDir) { + if (err) { + return cb(err); + } + + fs.copy(sourceDir, tempDir, function(err) { + if (err) { + return cb(err) + } + + var packageName; + var pack = spawn('npm', ['pack'], {cwd: tempDir}); + pack.on('error', function(err) { + return cb(err) + }); + + pack.stdout.on('data', function(data) { + packageName = data.toString().trim() + }); + + pack.on('close', function() { + try { + var packageArchive = path.join(tempDir, packageName); + fs.createReadStream(packageArchive).pipe(zlib.createGunzip()).pipe(tar.extract(tempDir)).on('finish', function() { + fs.removeSync(packageArchive) // remove the pack archive so it doesn't show up in the proxy + + if (opts.debug) { + console.log('bundled dependencies ready for upload') + } + + // return path to packed directory + return cb(undefined, path.join(tempDir, 'package')) + }).on('error', function(err) { + return cb(err); + }); + } catch(err) { + return cb(err) + } + }); + }); + }); +} + function proxyCreationDone (err, req, body, opts, done) { if (err) { done(err); diff --git a/lib/options.js b/lib/options.js index d5a6b09..1fd9486 100644 --- a/lib/options.js +++ b/lib/options.js @@ -171,10 +171,14 @@ module.exports.validateSync = function(opts, descriptor) { function checkPropertySync(opts, descriptor, propName) { var desc = descriptor[propName]; if (desc === null || desc === undefined) { - console.error(new Error(util.format('Invalid property %s', propName))); + var err = new Error(util.format('Invalid property %s', propName)); + console.error(err); + throw err; } if (desc.required && !opts[propName] && (!opts.prompt && desc.prompt)) { - console.error(new Error(util.format('Missing required option "%s"', propName))); + var err = new Error(util.format('Missing required option "%s"', propName)); + console.error(err); + throw err; } else { if (opts[propName] && (desc.secure === true)) { makeSecure(opts, propName); diff --git a/lib/ziputils.js b/lib/ziputils.js index 34244a3..35d5a0d 100644 --- a/lib/ziputils.js +++ b/lib/ziputils.js @@ -89,7 +89,7 @@ function zipDirectory(baseArch, baseName, entryName, done) { * Given a base directory, produce a list of entries. Each entry has a name, * a fully-qualified path, and whether it is a directory. The base is * assumed to be a "resources" directory, and the "node" resources folder, - * if present, will be treated specially using "enumerateNodeDirectory" below. + * if present, will be treated specially using "enumerateDirectory" below. */ module.exports.enumerateResourceDirectory = function(baseDir, remoteNpm) { var fileList = []; @@ -99,22 +99,12 @@ module.exports.enumerateResourceDirectory = function(baseDir, remoteNpm) { var stat = fs.statSync(fullName); if (stat.isDirectory()) { visitDirectory(fullName, fileList, '', '', - type, (type === 'node'), remoteNpm); + type, (type === 'node' || type === 'hosted'), remoteNpm); } }); return fileList; }; -module.exports.enumerateSources = function(baseDir, cb) { - try { - var fileList = []; - visitDirectory(baseDir, fileList, '', '', '', false, false); - cb(undefined, fileList); - } catch (err) { - cb(err); - } -} - /* * Given a base directory, produce a list of entries. Each entry has a name, * a fully-qualified path, and whether it is a directory. This method has @@ -122,10 +112,12 @@ module.exports.enumerateSources = function(baseDir, cb) { * one level deeper. The result can be used to ZIP up a node.js "resources/node" * directory. */ -module.exports.enumerateNodeDirectory = function(baseDir, remoteNpm, cb) { +module.exports.enumerateDirectory = function(baseDir, resourceType, remoteNpm, cb) { try { var fileList = []; - visitDirectory(baseDir, fileList, '', '', 'node', true, remoteNpm); + var isNode = (resourceType === 'hosted' || resourceType === 'node'); + + visitDirectory(baseDir, fileList, '', '', resourceType, isNode, remoteNpm); cb(undefined, fileList); } catch (err) { cb(err); diff --git a/package.json b/package.json index cc7816b..7ad8538 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,8 @@ "mustache": "^2.1.3", "node-unzip-2": "^0.2.1", "node-zip": "^1.1.1", + "tar-fs": "^1.16.0", + "fs-extra": "^4.0.2", "read": "^1.0.7", "request": "^2.63.0", "tmp": "^0.0.27", diff --git a/test/testzip.js b/test/testzip.js index ba63085..4d6b3cb 100644 --- a/test/testzip.js +++ b/test/testzip.js @@ -23,7 +23,7 @@ describe('ZIP Utilities Test', function() { }); it('Enumerate node file list', function(done) { - ziputils.enumerateNodeDirectory('./test/fixtures/employeesnode', false, function(err, files) { + ziputils.enumerateDirectory('./test/fixtures/employeesnode', 'node', false, function(err, files) { if (err) { return done(err); } //console.log('%j', files);