first pass at 'triton create'
This commit is contained in:
parent
15ca8ecc32
commit
0d4e93208c
25
TODO.txt
25
TODO.txt
@ -1,9 +1,6 @@
|
||||
|
||||
# today
|
||||
|
||||
triton instances|insts # list machines
|
||||
triton instance|inst ID|NAME|UNIQUE-NAME-SUBSTRING # get machine
|
||||
# -1 for unique match, a la 'vmadm lookup -1'
|
||||
|
||||
triton create # triton create-instance
|
||||
triton create -p PKG [...] IMG
|
||||
@ -23,38 +20,22 @@ triton image IMAGE # get image
|
||||
triton packages # list packages
|
||||
triton package PACKAGE
|
||||
|
||||
triton instances|insts # list machines
|
||||
triton instance|inst ID|NAME # get machine
|
||||
|
||||
triton cloudapi ...
|
||||
|
||||
|
||||
|
||||
|
||||
# maybe today
|
||||
|
||||
triton config defaultPkg t4-standard-1g
|
||||
|
||||
triton login|ssh VM # kexec?
|
||||
|
||||
triton delete VM|IMAGE # substring matching? too dangerous
|
||||
triton delete --vm VM
|
||||
triton delete --image IMAGE
|
||||
|
||||
triton raw|cloudapi ... # raw cloudapi call
|
||||
Equivalent of:
|
||||
function cloudapi() {
|
||||
local now=`date -u "+%a, %d %h %Y %H:%M:%S GMT"` ;
|
||||
local signature=`echo ${now} | tr -d '\n' | openssl dgst -sha256 -sign ~/.ssh/automation.id_rsa | openssl enc -e -a | tr -d '\n'` ;
|
||||
local curl_opts=
|
||||
[[ -n $SDC_TESTING ]] && curl_opts="-k $curl_opts";
|
||||
[[ -n $TRACE ]] && set -x;
|
||||
curl -is $curl_opts \
|
||||
-H "Accept: application/json" -H "api-version: ~7.2" \
|
||||
-H "Date: ${now}" \
|
||||
-H "Authorization: Signature keyId=\"/$SDC_ACCOUNT/keys/$SDC_KEY_ID\",algorithm=\"rsa-sha256\" ${signature}" \
|
||||
--url $SDC_URL$@ ;
|
||||
[[ -n $TRACE ]] && set +x;
|
||||
echo "";
|
||||
}
|
||||
|
||||
"shortid" instead of full UUID "id" in default output, and then allow lookup
|
||||
by that shortid. Really nice for 80 columns.
|
||||
|
||||
|
@ -61,7 +61,7 @@ function CLI() {
|
||||
{ group: 'Operator Commands' },
|
||||
'account',
|
||||
{ group: 'Instances (aka VMs/Machines/Containers)' },
|
||||
'create',
|
||||
'create-instance',
|
||||
'instances',
|
||||
'instance',
|
||||
'instance-audit',
|
||||
@ -117,9 +117,9 @@ CLI.prototype.do_images = require('./do_images');
|
||||
CLI.prototype.do_image = require('./do_image');
|
||||
|
||||
// Instances (aka VMs/containers/machines)
|
||||
CLI.prototype.do_create = require('./do_create');
|
||||
CLI.prototype.do_instance = require('./do_instance');
|
||||
CLI.prototype.do_instances = require('./do_instances');
|
||||
CLI.prototype.do_create_instance = require('./do_create_instance');
|
||||
CLI.prototype.do_instance_audit = require('./do_instance_audit');
|
||||
CLI.prototype.do_stop_instance = require('./do_startstop_instance')('stop');
|
||||
CLI.prototype.do_start_instance = require('./do_startstop_instance')('start');
|
||||
|
@ -404,19 +404,19 @@ CloudAPI.prototype._doMachine = function _doMachine(action, uuid, callback) {
|
||||
};
|
||||
|
||||
/**
|
||||
* wait for a specfic state for a machine
|
||||
* Wait for a machine to go one of a set of specfic states.
|
||||
*
|
||||
* @param {Object} options
|
||||
* - {String} id - machine UUID
|
||||
* - {String} state - desired state
|
||||
* - {Array of String} states - desired state
|
||||
* - {Number} interval (optional) - time in ms to poll
|
||||
* @param {Function} callback - called when state is reached or on error
|
||||
*/
|
||||
CloudAPI.prototype.waitForMachineState = function waitForMachineState(opts, callback) {
|
||||
CloudAPI.prototype.waitForMachineStates = function waitForMachineStates(opts, callback) {
|
||||
var self = this;
|
||||
assert.object(opts, 'opts');
|
||||
assert.string(opts.id, 'opts.id');
|
||||
assert.string(opts.state, 'opts.state');
|
||||
assert.arrayOfString(opts.states, 'opts.states');
|
||||
assert.optionalNumber(opts.interval, 'opts.interval');
|
||||
assert.func(callback, 'callback');
|
||||
var interval = (opts.interval === undefined ? 1000 : opts.interval);
|
||||
@ -429,7 +429,7 @@ CloudAPI.prototype.waitForMachineState = function waitForMachineState(opts, call
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
if (machine.state === opts.state) {
|
||||
if (opts.states.indexOf(machine.state) !== -1) {
|
||||
callback(null, machine);
|
||||
return;
|
||||
}
|
||||
@ -497,6 +497,26 @@ CloudAPI.prototype.listMachines = function listMachines(options, callback) {
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
CloudAPI.prototype.createMachine = function createMachine(options, callback) {
|
||||
assert.object(options, 'options');
|
||||
assert.optionalString(options.name, 'options.name');
|
||||
assert.uuid(options.image, 'options.image');
|
||||
assert.uuid(options.package, 'options.package');
|
||||
assert.optionalArrayOfUuid(options.networks, 'options.networks');
|
||||
// TODO: assert the other fields
|
||||
assert.func(callback, 'callback');
|
||||
|
||||
// XXX how does options.networks array work here?
|
||||
this._request({
|
||||
method: 'POST',
|
||||
path: this._path(format('/%s/machines', this.user), options)
|
||||
}, function (err, req, res, body) {
|
||||
callback(err, body, res);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* List machine audit (successful actions on the machine).
|
||||
*
|
||||
|
@ -1,11 +1,8 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Copyright (c) 2015 Joyent Inc. All rights reserved.
|
||||
*/
|
||||
|
||||
|
||||
var assert = require('assert-plus');
|
||||
var sprintf = require('extsprintf').sprintf;
|
||||
var util = require('util'),
|
||||
format = util.format;
|
||||
|
||||
@ -17,6 +14,7 @@ var errors = require('./errors'),
|
||||
|
||||
var p = console.log;
|
||||
|
||||
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
||||
|
||||
|
||||
// ---- support stuff
|
||||
@ -142,9 +140,45 @@ function isUUID(s) {
|
||||
return /^([a-f\d]{8}(-[a-f\d]{4}){3}-[a-f\d]{12}?)$/i.test(s);
|
||||
}
|
||||
|
||||
|
||||
function humanDurationFromMs(ms) {
|
||||
assert.number(ms, 'ms');
|
||||
var sizes = [
|
||||
['ms', 1000, 's'],
|
||||
['s', 60, 'm'],
|
||||
['m', 60, 'h'],
|
||||
['h', 24, 'd']
|
||||
];
|
||||
if (ms === 0) {
|
||||
return '0ms';
|
||||
}
|
||||
var bits = [];
|
||||
var n = ms;
|
||||
for (var i = 0; i < sizes.length; i++) {
|
||||
var size = sizes[i];
|
||||
var remainder = n % size[1];
|
||||
if (remainder === 0) {
|
||||
bits.unshift('');
|
||||
} else {
|
||||
bits.unshift(format('%d%s', remainder, size[0]));
|
||||
}
|
||||
n = Math.floor(n / size[1]);
|
||||
if (n === 0) {
|
||||
break;
|
||||
} else if (size[2] === 'd') {
|
||||
bits.unshift(format('%d%s', n, size[2]));
|
||||
break;
|
||||
}
|
||||
}
|
||||
return bits.slice(0, 2).join('');
|
||||
}
|
||||
|
||||
|
||||
|
||||
//---- exports
|
||||
|
||||
module.exports = {
|
||||
UUID_RE: UUID_RE,
|
||||
objCopy: objCopy,
|
||||
deepObjCopy: deepObjCopy,
|
||||
zeroPad: zeroPad,
|
||||
@ -153,5 +187,6 @@ module.exports = {
|
||||
kvToObj: kvToObj,
|
||||
longAgo: longAgo,
|
||||
isUUID: isUUID,
|
||||
humanDurationFromMs: humanDurationFromMs
|
||||
};
|
||||
// vim: set softtabstop=4 shiftwidth=4:
|
||||
|
197
lib/do_create_instance.js
Normal file
197
lib/do_create_instance.js
Normal file
@ -0,0 +1,197 @@
|
||||
/*
|
||||
* Copyright (c) 2015 Joyent Inc. All rights reserved.
|
||||
*
|
||||
* `triton create ...`
|
||||
*/
|
||||
|
||||
var bigspinner = require('bigspinner');
|
||||
var format = require('util').format;
|
||||
var tabula = require('tabula');
|
||||
var vasync = require('vasync');
|
||||
|
||||
var common = require('./common');
|
||||
var errors = require('./errors');
|
||||
|
||||
|
||||
function do_create_instance(subcmd, opts, args, callback) {
|
||||
var self = this;
|
||||
if (opts.help) {
|
||||
this.do_help('help', {}, [subcmd], callback);
|
||||
return;
|
||||
} else if (args.length < 1 || args.length > 2) {
|
||||
return callback(new errors.UsageError(format(
|
||||
'incorrect number of args (%d): %s', args.length, args.join(' '))));
|
||||
}
|
||||
|
||||
var log = this.triton.log;
|
||||
var cloudapi = this.triton.cloudapi;
|
||||
var cOpts = {};
|
||||
|
||||
vasync.pipeline({arg: {}, funcs: [
|
||||
function getImg(ctx, next) {
|
||||
// XXX don't get the image object if it is a UUID, waste of time
|
||||
self.triton.getImage(args[0], function (err, img) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
ctx.img = img;
|
||||
log.trace({img: img}, 'create-instance img');
|
||||
next();
|
||||
});
|
||||
},
|
||||
function getPkg(ctx, next) {
|
||||
if (args.length < 2) {
|
||||
return next();
|
||||
}
|
||||
// XXX don't get the package object if it is a UUID, waste of time
|
||||
self.triton.getPackage(args[1], function (err, pkg) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
log.trace({pkg: pkg}, 'create-instance pkg');
|
||||
ctx.pkg = pkg;
|
||||
next();
|
||||
});
|
||||
},
|
||||
function getNets(ctx, next) {
|
||||
if (!opts.networks) {
|
||||
return next();
|
||||
}
|
||||
self.triton.getNetworks(opts.networks, function (err, nets) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
ctx.nets = nets;
|
||||
next();
|
||||
});
|
||||
},
|
||||
function createInst(ctx, next) {
|
||||
var createOpts = {
|
||||
name: opts.name,
|
||||
image: ctx.img.id,
|
||||
'package': ctx.pkg && ctx.pkg.id,
|
||||
networks: ctx.nets && ctx.nets.map(
|
||||
function (net) { return net.id; })
|
||||
};
|
||||
log.trace({createOpts: createOpts}, 'create-instance createOpts');
|
||||
ctx.start = Date.now();
|
||||
cloudapi.createMachine(createOpts, function (err, inst) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
ctx.inst = inst;
|
||||
if (opts.json) {
|
||||
console.log(JSON.stringify(inst));
|
||||
} else {
|
||||
console.log('Creating instance %s (%s, %s@%s, %s)',
|
||||
inst.name, inst.id, ctx.img.name, ctx.img.version,
|
||||
inst.package);
|
||||
}
|
||||
next();
|
||||
});
|
||||
},
|
||||
function maybeWait(ctx, next) {
|
||||
if (!opts.wait) {
|
||||
return next();
|
||||
}
|
||||
|
||||
var spinner;
|
||||
if (!opts.quiet && process.stderr.isTTY) {
|
||||
spinner = bigspinner.createSpinner({
|
||||
delay: 250,
|
||||
stream: process.stderr,
|
||||
height: process.stdout.rows - 2,
|
||||
width: process.stdout.columns - 1,
|
||||
hideCursor: true,
|
||||
fontChar: '#'
|
||||
});
|
||||
}
|
||||
|
||||
cloudapi.waitForMachineStates({
|
||||
id: ctx.inst.id,
|
||||
states: ['running', 'failed']
|
||||
}, function (err, inst) {
|
||||
if (spinner) {
|
||||
spinner.destroy();
|
||||
}
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
if (opts.json) {
|
||||
console.log(JSON.stringify(inst));
|
||||
} else if (inst.state === 'running') {
|
||||
var dur = Date.now() - ctx.start;
|
||||
console.log('Created instance %s (%s) in %s',
|
||||
inst.name, inst.id, common.humanDurationFromMs(dur));
|
||||
}
|
||||
if (inst.state !== 'running') {
|
||||
next(new Error(format('failed to create instance %s (%s)',
|
||||
inst.name, inst.id)));
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
});
|
||||
}
|
||||
]}, function (err) {
|
||||
callback(err);
|
||||
});
|
||||
};
|
||||
|
||||
do_create_instance.options = [
|
||||
{
|
||||
names: ['help', 'h'],
|
||||
type: 'bool',
|
||||
help: 'Show this help.'
|
||||
},
|
||||
{
|
||||
group: 'Create options'
|
||||
},
|
||||
{
|
||||
names: ['name', 'n'],
|
||||
type: 'string',
|
||||
help: 'One or more (comma-separated) networks IDs.'
|
||||
},
|
||||
// XXX arrayOfCommaSepString dashdash type
|
||||
//{
|
||||
// names: ['networks', 'nets'],
|
||||
// type: 'arrayOfCommaSepString',
|
||||
// help: 'One or more (comma-separated) networks IDs.'
|
||||
//},
|
||||
// XXX enable-firewall
|
||||
// XXX locality: near, far
|
||||
// XXX metadata, metadata-file
|
||||
// XXX script (user-script)
|
||||
// XXX tag
|
||||
{
|
||||
group: 'Other options'
|
||||
},
|
||||
{
|
||||
names: ['wait', 'w'],
|
||||
type: 'bool',
|
||||
help: 'Wait for the creation to complete.'
|
||||
},
|
||||
{
|
||||
names: ['quiet', 'q'],
|
||||
type: 'bool',
|
||||
help: 'No progress spinner while waiting.'
|
||||
},
|
||||
{
|
||||
names: ['json', 'j'],
|
||||
type: 'bool',
|
||||
help: 'JSON stream output.'
|
||||
}
|
||||
];
|
||||
do_create_instance.help = (
|
||||
/* BEGIN JSSTYLED */
|
||||
'Create a new instance.\n' +
|
||||
'\n' +
|
||||
'Usage:\n' +
|
||||
' {{name}} create-instance [<options>] IMAGE [PACKAGE]\n' +
|
||||
'\n' +
|
||||
'{{options}}'
|
||||
/* END JSSTYLED */
|
||||
);
|
||||
|
||||
do_create_instance.aliases = ['create'];
|
||||
|
||||
module.exports = do_create_instance;
|
@ -42,8 +42,7 @@ function do_packages (subcmd, opts, args, callback) {
|
||||
tabula(packages, {
|
||||
skipHeader: opts.H,
|
||||
columns: columns,
|
||||
sort: sort,
|
||||
validFields: 'name,memory,disk,swap,vcpus,lwps,default,id,version'.split(',')
|
||||
sort: sort
|
||||
});
|
||||
}
|
||||
callback();
|
||||
@ -64,7 +63,7 @@ do_packages.options = [
|
||||
{
|
||||
names: ['o'],
|
||||
type: 'string',
|
||||
default: 'id,name,version,memory,disk',
|
||||
default: 'id,name,default,memory,disk',
|
||||
help: 'Specify fields (columns) to output.',
|
||||
helpArg: 'field1,...'
|
||||
},
|
||||
|
@ -98,11 +98,10 @@ function _do_instance(action, subcmd, opts, args, callback) {
|
||||
return;
|
||||
}
|
||||
|
||||
var waitOpts = {
|
||||
state: state,
|
||||
id: uuid
|
||||
};
|
||||
self.triton.cloudapi.waitForMachineState(waitOpts, function (err, machine) {
|
||||
self.triton.cloudapi.waitForMachineStates({
|
||||
id: uuid,
|
||||
states: [state]
|
||||
}, function (err, machine) {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
|
198
lib/triton.js
198
lib/triton.js
@ -13,6 +13,7 @@ var once = require('once');
|
||||
var path = require('path');
|
||||
var restifyClients = require('restify-clients');
|
||||
var sprintf = require('util').format;
|
||||
var tabula = require('tabula');
|
||||
|
||||
var cloudapi = require('./cloudapi2');
|
||||
var common = require('./common');
|
||||
@ -105,130 +106,89 @@ Triton.prototype._cloudapiFromProfile = function _cloudapiFromProfile(profile) {
|
||||
|
||||
|
||||
/**
|
||||
* Find a machine in the set of DCs for the current profile.
|
||||
*
|
||||
*
|
||||
* @param {Object} options
|
||||
* - {String} machine (required) The machine id.
|
||||
* XXX support name matching, prefix, etc.
|
||||
* @param {Function} callback `function (err, machine, dc)`
|
||||
* Returns the machine object (as from cloudapi GetMachine) and the `dc`,
|
||||
* e.g. "us-west-1".
|
||||
* Get an image by ID or name. If there is more than one image with that name,
|
||||
* then the latest (by published_at) is returned.
|
||||
*/
|
||||
Triton.prototype.findMachine = function findMachine(options, callback) {
|
||||
//XXX Eventually this can be cached for a *full* uuid. Arguably for a
|
||||
// uuid prefix or machine alias match, it cannot be cached, because an
|
||||
// ambiguous machine could have been added.
|
||||
var self = this;
|
||||
assert.object(options, 'options');
|
||||
assert.string(options.machine, 'options.machine');
|
||||
assert.func(callback, 'callback');
|
||||
var callback = once(callback);
|
||||
Triton.prototype.getImage = function getImage(name, cb) {
|
||||
assert.string(name, 'name');
|
||||
assert.func(cb, 'cb');
|
||||
|
||||
var errs = [];
|
||||
var foundMachine;
|
||||
var foundDc;
|
||||
async.each(
|
||||
self.dcs(),
|
||||
function oneDc(dc, next) {
|
||||
var client = self._clientFromDc(dc.name);
|
||||
client.getMachine({id: options.machine}, function (err, machine) {
|
||||
if (err) {
|
||||
errs.push(err);
|
||||
} else if (machine) {
|
||||
foundMachine = machine;
|
||||
foundDc = dc.name;
|
||||
// Return early on an unambiguous match.
|
||||
// XXX When other than full 'id' is supported, this isn't unambiguous.
|
||||
callback(null, foundMachine, foundDc);
|
||||
}
|
||||
next();
|
||||
});
|
||||
},
|
||||
function done(surpriseErr) {
|
||||
if (surpriseErr) {
|
||||
callback(surpriseErr);
|
||||
} else if (foundMachine) {
|
||||
callback(null, foundMachine, foundDc)
|
||||
} else if (errs.length) {
|
||||
callback(errs.length === 1 ?
|
||||
errs[0] : new errors.MultiError(errs));
|
||||
if (common.UUID_RE.test(name)) {
|
||||
this.cloudapi.getImage({id: name}, function (err, img) {
|
||||
if (err) {
|
||||
cb(err);
|
||||
} else if (img.state !== 'active') {
|
||||
cb(new Error(format('image %s is not active', name)));
|
||||
} else {
|
||||
callback(new errors.InternalError(
|
||||
'unexpected error finding machine ' + options.id));
|
||||
cb(null, img);
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* List machines for the current profile.
|
||||
*
|
||||
* var res = this.jc.listMachines();
|
||||
* res.on('data', function (dc, dcMachines) {
|
||||
* //...
|
||||
* });
|
||||
* res.on('dcError', function (dc, dcErr) {
|
||||
* //...
|
||||
* });
|
||||
* res.on('end', function () {
|
||||
* //...
|
||||
* });
|
||||
*
|
||||
* @param {Object} options Optional
|
||||
*/
|
||||
Triton.prototype.listMachines = function listMachines(options) {
|
||||
var self = this;
|
||||
if (options === undefined) {
|
||||
options = {};
|
||||
}
|
||||
assert.object(options, 'options');
|
||||
|
||||
var emitter = new EventEmitter();
|
||||
async.each(
|
||||
self.dcs(),
|
||||
function oneDc(dc, next) {
|
||||
var client = self._clientFromDc(dc.name);
|
||||
client.listMachines(function (err, machines) {
|
||||
if (err) {
|
||||
emitter.emit('dcError', dc.name, err);
|
||||
} else {
|
||||
emitter.emit('data', dc.name, machines);
|
||||
}
|
||||
next();
|
||||
});
|
||||
},
|
||||
function done(err) {
|
||||
emitter.emit('end');
|
||||
}
|
||||
);
|
||||
return emitter;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Return the audit for the given machine.
|
||||
*
|
||||
* @param {Object} options
|
||||
* - {String} machine (required) The machine id.
|
||||
* XXX support `machine` being more than just the UUID.
|
||||
* @param {Function} callback of the form `function (err, audit, dc)`
|
||||
*/
|
||||
Triton.prototype.machineAudit = function machineAudit(options, callback) {
|
||||
var self = this;
|
||||
assert.object(options, 'options');
|
||||
assert.string(options.machine, 'options.machine');
|
||||
|
||||
self.findMachine({machine: options.machine}, function (err, machine, dc) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
var client = self._clientFromDc(dc);
|
||||
client.machineAudit({id: machine.id}, function (err, audit) {
|
||||
callback(err, audit, dc);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
this.cloudapi.listImages(function (err, imgs) {
|
||||
if (err) {
|
||||
return cb(err);
|
||||
}
|
||||
var nameMatches = [];
|
||||
for (var i = 0; i < imgs.length; i++) {
|
||||
if (imgs[i].name === name) {
|
||||
nameMatches.push(imgs[i]);
|
||||
}
|
||||
}
|
||||
if (nameMatches.length === 0) {
|
||||
cb(new Error(format('no image with name=%s was found',
|
||||
name)));
|
||||
} else if (nameMatches.length === 1) {
|
||||
cb(null, nameMatches[0]);
|
||||
} else {
|
||||
tabula.sortArrayOfObjects(nameMatches, 'published_at');
|
||||
cb(null, nameMatches[nameMatches.length - 1]);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Get an active package by ID or name. If there is more than one package
|
||||
* with that name, then this errors out.
|
||||
*/
|
||||
Triton.prototype.getPackage = function getPackage(name, cb) {
|
||||
assert.string(name, 'name');
|
||||
assert.func(cb, 'cb');
|
||||
|
||||
if (common.UUID_RE.test(name)) {
|
||||
this.cloudapi.getPackage({id: name}, function (err, pkg) {
|
||||
if (err) {
|
||||
cb(err);
|
||||
} else if (!pkg.active) {
|
||||
cb(new Error(format('image %s is not active', name)));
|
||||
} else {
|
||||
cb(null, pkg);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.cloudapi.listPackages(function (err, pkgs) {
|
||||
if (err) {
|
||||
return cb(err);
|
||||
}
|
||||
var nameMatches = [];
|
||||
for (var i = 0; i < pkgs.length; i++) {
|
||||
if (pkgs[i].name === name) {
|
||||
nameMatches.push(pkgs[i]);
|
||||
}
|
||||
}
|
||||
if (nameMatches.length === 0) {
|
||||
cb(new Error(format('no package with name=%s was found',
|
||||
name)));
|
||||
} else if (nameMatches.length === 1) {
|
||||
cb(null, nameMatches[0]);
|
||||
} else {
|
||||
cb(new Error(format(
|
||||
'package name "%s" is ambiguous: matches %d packages',
|
||||
name, nameMatches.length)));
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
@ -7,6 +7,7 @@
|
||||
"dependencies": {
|
||||
"assert-plus": "0.1.5",
|
||||
"backoff": "2.4.1",
|
||||
"bigspinner": "^3.0.0",
|
||||
"bunyan": "1.4.0",
|
||||
"cmdln": "3.2.3",
|
||||
"dashdash": "1.10.0",
|
||||
|
Reference in New Issue
Block a user