diff --git a/lib/cloudapi2.js b/lib/cloudapi2.js index df190cc..2b379eb 100644 --- a/lib/cloudapi2.js +++ b/lib/cloudapi2.js @@ -43,6 +43,7 @@ var vasync = require('vasync'); var auth = require('smartdc-auth'); var bunyannoop = require('./bunyannoop'); +var common = require('./common'); var errors = require('./errors'); var SaferJsonClient = require('./SaferJsonClient'); @@ -199,21 +200,24 @@ CloudApi.prototype._path = function _path(subpath /* , qparams, ... */) { /** * Cloud API request wrapper - modeled after http.request * - * @param {Object|String} options - object or string for endpoint + * @param {Object|String} opts - object or string for endpoint * - {String} path - URL endpoint to hit * - {String} method - HTTP(s) request method * - {Object} data - data to be passed - * @param {Function} callback passed via the restify client + * - {Object} headers - optional additional request headers + * @param {Function} cb passed via the restify client */ -CloudApi.prototype._request = function _request(options, callback) { +CloudApi.prototype._request = function _request(opts, cb) { var self = this; - if (typeof (options) === 'string') - options = {path: options}; - assert.object(options, 'options'); - assert.func(callback, 'callback'); - assert.optionalObject(options.data, 'options.data'); + if (typeof (opts) === 'string') + opts = {path: opts}; + assert.object(opts, 'opts'); + assert.optionalObject(opts.data, 'opts.data'); + assert.optionalString(opts.method, 'opts.method'); + assert.optionalObject(opts.headers, 'opts.headers'); + assert.func(cb, 'cb'); - var method = (options.method || 'GET').toLowerCase(); + var method = (opts.method || 'GET').toLowerCase(); assert.ok(['get', 'post', 'put', 'delete', 'head'].indexOf(method) >= 0, 'invalid method given'); switch (method) { @@ -226,17 +230,20 @@ CloudApi.prototype._request = function _request(options, callback) { self._getAuthHeaders(function (err, headers) { if (err) { - callback(err); + cb(err); return; } - var opts = { - path: options.path, + if (opts.headers) { + common.objMerge(headers, opts.headers); + } + var reqOpts = { + path: opts.path, headers: headers }; - if (options.data) - self.client[method](opts, options.data, callback); + if (opts.data) + self.client[method](reqOpts, opts.data, cb); else - self.client[method](opts, callback); + self.client[method](reqOpts, cb); }); }; diff --git a/lib/common.js b/lib/common.js index 2459cb3..ce8caa5 100644 --- a/lib/common.js +++ b/lib/common.js @@ -46,6 +46,32 @@ function deepObjCopy(obj) { } +/* + * Merge given objects into the given `target` object. Last one wins. + * The `target` is modified in place. + * + * var foo = {bar: 32}; + * objMerge(foo, {bar: 42}, {bling: 'blam'}); + * + * Adapted from tunnel-agent `mergeOptions`. + */ +function objMerge(target) { + for (var i = 1, len = arguments.length; i < len; ++i) { + var overrides = arguments[i]; + if (typeof (overrides) === 'object') { + var keys = Object.keys(overrides); + for (var j = 0, keyLen = keys.length; j < keyLen; ++j) { + var k = keys[j]; + if (overrides[k] !== undefined) { + target[k] = overrides[k]; + } + } + } + } + return target; +} + + function zeroPad(n, width) { var s = String(n); assert.number(width, 'width'); @@ -852,6 +878,7 @@ function tildeSync(s) { module.exports = { objCopy: objCopy, deepObjCopy: deepObjCopy, + objMerge: objMerge, zeroPad: zeroPad, boolFromString: boolFromString, jsonStream: jsonStream, diff --git a/lib/do_cloudapi.js b/lib/do_cloudapi.js index 077dd22..7f21c4c 100644 --- a/lib/do_cloudapi.js +++ b/lib/do_cloudapi.js @@ -12,6 +12,9 @@ var http = require('http'); +var errors = require('./errors'); + + function do_cloudapi(subcmd, opts, args, callback) { if (opts.help) { this.do_help('help', {}, [subcmd], callback); @@ -21,34 +24,50 @@ function do_cloudapi(subcmd, opts, args, callback) { return; } - var path = args[0]; - - var reqopts = { - method: opts.method.toLowerCase(), + // Get `reqOpts` from given options. + var method = opts.method; + if (!method) { + if (opts.data) { + method = 'PUT'; + } else { + method = 'GET'; + } + } + var reqOpts = { + method: method.toLowerCase(), headers: {}, - path: path + path: args[0] }; - - // parse -H headers - for (var i = 0; i < opts.header.length; i++) { - var raw = opts.header[i]; - var j = raw.indexOf(':'); - if (j < 0) { - callback(new Error('failed to parse header: ' + raw)); + if (opts.header) { + for (var i = 0; i < opts.header.length; i++) { + var raw = opts.header[i]; + var j = raw.indexOf(':'); + if (j < 0) { + callback(new errors.TritonError( + 'failed to parse header: ' + raw)); + return; + } + var header = raw.substr(0, j); + var value = raw.substr(j + 1).trimLeft(); + reqOpts.headers[header] = value; + } + } + if (opts.data) { + try { + reqOpts.data = JSON.parse(opts.data); + } catch (parseErr) { + callback(new errors.TritonError(parseErr, + 'given is not valid JSON: ' + parseErr.message)); return; } - var header = raw.substr(0, j); - var value = raw.substr(j + 1).leftTrim(); - - reqopts.headers[header] = value; } - this.tritonapi.cloudapi._request(reqopts, function (err, req, res, body) { + this.tritonapi.cloudapi._request(reqOpts, function (err, req, res, body) { if (err) { callback(err); return; } - if (opts.headers || reqopts.method === 'head') { + if (opts.headers || reqOpts.method === 'head') { console.error('%s/%s %d %s', req.connection.encrypted ? 'HTTPS' : 'HTTP', res.httpVersion, @@ -60,7 +79,7 @@ function do_cloudapi(subcmd, opts, args, callback) { console.error(); } - if (reqopts.method !== 'head') + if (reqOpts.method !== 'head') console.log(JSON.stringify(body, null, 4)); callback(); }); @@ -75,26 +94,33 @@ do_cloudapi.options = [ { names: ['method', 'X'], type: 'string', - default: 'GET', + helpArg: '', help: 'Request method to use. Default is "GET".' }, { names: ['header', 'H'], type: 'arrayOfString', - default: [], + helpArg: '
', help: 'Headers to send with request.' }, { names: ['headers', 'i'], type: 'bool', help: 'Print response headers to stderr.' + }, + { + names: ['data', 'd'], + type: 'string', + helpArg: '', + help: 'Add POST data. This must be valid JSON.' } ]; do_cloudapi.help = ( 'Raw cloudapi request.\n' + '\n' + 'Usage:\n' - + ' {{name}} cloudapi [-X method] [-H header=value] \n' + + ' {{name}} cloudapi [-X ] [-H ] \\\n' + + ' [-d ] \n' + '\n' + '{{options}}' );