diff --git a/CHANGES.md b/CHANGES.md index 0506adf..1c1075b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,8 +1,9 @@ # node-triton changelog -## 4.2.1 (not yet released) +## 4.3.0 (not yet released) -(nothing yet) +- #76 `triton image create ...` and `triton image wait ...` +- #72 want `triton image` to still return image details even when it is not in 'active' state ## 4.2.0 diff --git a/lib/cloudapi2.js b/lib/cloudapi2.js index ed1c921..bae42d2 100644 --- a/lib/cloudapi2.js +++ b/lib/cloudapi2.js @@ -540,6 +540,85 @@ CloudApi.prototype.getImage = function getImage(opts, cb) { }; +/** + * + * + * @param {Object} opts + * - {UUID} machine Required. The ID of the machine from which to create + * the image. + * - {String} name Required. The image name. + * - {String} version Required. The image version. + * - {String} description Optional. A short description. + * - {String} homepage Optional. Homepage URL. + * - {String} eula Optional. EULA URL. + * - {Array} acl Optional. An array of account UUIDs to which to give + * access. "Access Control List." + * - {Object} tags Optional. + * @param {Function} cb of the form `function (err, image, res)` + */ +CloudApi.prototype.createImageFromMachine = +function createImageFromMachine(opts, cb) { + assert.object(opts, 'opts'); + assert.uuid(opts.machine, 'opts.machine'); + assert.string(opts.name, 'opts.name'); + assert.string(opts.version, 'opts.version'); + assert.optionalString(opts.description, 'opts.description'); + assert.optionalString(opts.homepage, 'opts.homepage'); + assert.optionalString(opts.eula, 'opts.eula'); + assert.optionalArrayOfUuid(opts.acl, 'opts.acl'); + assert.optionalObject(opts.tags, 'opts.tags'); + assert.func(cb, 'cb'); + + this._request({ + method: 'POST', + path: format('/%s/images', this.account), + data: opts + }, function (err, req, res, body) { + cb(err, body, res); + }); +}; + + + +/** + * Wait for an image to go one of a set of specfic states. + * + * @param {Object} options + * - {String} id - machine UUID + * - {Array of String} states - desired state + * - {Number} interval (optional) - Time in ms to poll. Default is 1000ms. + * @param {Function} cb - `function (err, image, res)` + * Called when state is reached or on error + */ +CloudApi.prototype.waitForImageStates = +function waitForImageStates(opts, cb) { + var self = this; + assert.object(opts, 'opts'); + assert.uuid(opts.id, 'opts.id'); + assert.arrayOfString(opts.states, 'opts.states'); + assert.optionalNumber(opts.interval, 'opts.interval'); + assert.func(cb, 'cb'); + var interval = (opts.interval === undefined ? 1000 : opts.interval); + assert.ok(interval > 0, 'interval must be a positive number'); + + poll(); + + function poll() { + self.getImage({id: opts.id}, function (err, img, res) { + if (err) { + cb(err, null, res); + return; + } + if (opts.states.indexOf(img.state) !== -1) { + cb(null, img, res); + return; + } + setTimeout(poll, interval); + }); + } +}; + + // ---- packages /** @@ -583,14 +662,20 @@ CloudApi.prototype.getPackage = function getPackage(opts, cb) { * XXX add getCredentials equivalent * XXX cloudapi docs don't doc the credentials=true option * - * @param {String} uuid (required) The machine id. - * @param {Function} callback of the form `function (err, machine, res)` + * For backwards compat, calling with `getMachine(id, cb)` is allowed. + * + * @param {Object} opts + * - id {UUID} Required. The machine id. + * @param {Function} cb of the form `function (err, machine, res)` */ -CloudApi.prototype.getMachine = function getMachine(id, cb) { - assert.uuid(id, 'id'); - assert.func(cb, 'cb'); +CloudApi.prototype.getMachine = function getMachine(opts, cb) { + if (typeof (opts) === 'string') { + opts = {id: opts}; + } + assert.object(opts, 'opts'); + assert.uuid(opts.id, 'opts.id'); - var endpoint = format('/%s/machines/%s', this.account, id); + var endpoint = format('/%s/machines/%s', this.account, opts.id); this._request(endpoint, function (err, req, res, body) { cb(err, body, res); }); @@ -677,14 +762,15 @@ CloudApi.prototype._doMachine = function _doMachine(action, uuid, callback) { * @param {Function} callback - called when state is reached or on error */ CloudApi.prototype.waitForMachineStates = - function waitForMachineStates(opts, callback) { +function waitForMachineStates(opts, callback) { var self = this; assert.object(opts, 'opts'); - assert.string(opts.id, 'opts.id'); + assert.uuid(opts.id, 'opts.id'); assert.arrayOfString(opts.states, 'opts.states'); assert.optionalNumber(opts.interval, 'opts.interval'); assert.func(callback, 'callback'); var interval = (opts.interval === undefined ? 1000 : opts.interval); + assert.ok(interval > 0, 'interval must be a positive number'); poll(); diff --git a/lib/do_image/do_create.js b/lib/do_image/do_create.js new file mode 100644 index 0000000..14910a1 --- /dev/null +++ b/lib/do_image/do_create.js @@ -0,0 +1,270 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +/* + * Copyright 2015 Joyent, Inc. + * + * `triton image create ...` + */ + +var assert = require('assert-plus'); +var format = require('util').format; +var fs = require('fs'); +var strsplit = require('strsplit'); +var tabula = require('tabula'); +var tilde = require('tilde-expansion'); +var vasync = require('vasync'); + +var common = require('../common'); +var distractions = require('../distractions'); +var errors = require('../errors'); +var mat = require('../metadataandtags'); + + +// ---- the command + +function do_create(subcmd, opts, args, cb) { + var self = this; + if (opts.help) { + this.do_help('help', {}, [subcmd], cb); + return; + } else if (args.length !== 3) { + cb(new errors.UsageError( + 'incorrect number of args: expect 3, got ' + args.length)); + return; + } + + var log = this.top.log; + var cloudapi = this.top.tritonapi.cloudapi; + + vasync.pipeline({arg: {}, funcs: [ + function loadTags(ctx, next) { + mat.tagsFromOpts(opts, log, function (err, tags) { + if (err) { + next(err); + return; + } + if (tags) { + log.trace({tags: tags}, 'tags loaded from opts'); + ctx.tags = tags; + } + next(); + }); + }, + function loadAcl(ctx, next) { + if (!opts.acl) { + next(); + return; + } + for (var i = 0; i < opts.acl.length; i++) { + if (!common.isUUID(opts.acl[i])) { + next(new errors.UsageError(format( + 'invalid --acl: "%s" is not a UUID', opts.acl[i]))); + return; + } + } + ctx.acl = opts.acl; + next(); + }, + function getInst(ctx, next) { + var id = args[0]; + if (common.isUUID(id)) { + ctx.inst = {id: id}; + next(); + return; + } + + self.top.tritonapi.getInstance(id, function (err, inst) { + if (err) { + next(err); + return; + } + log.trace({inst: inst}, 'image create: inst'); + ctx.inst = inst; + next(); + }); + }, + function createImg(ctx, next) { + var createOpts = { + machine: ctx.inst.id, + name: args[1], + version: args[2], + description: opts.description, + homepage: opts.homepage, + eula: opts.eula, + acl: ctx.acl, + tags: ctx.tags + }; + + log.trace({dryRun: opts.dry_run, createOpts: createOpts}, + 'image create createOpts'); + ctx.start = Date.now(); + if (opts.dry_run) { + ctx.inst = { + id: 'cafecafe-4c0e-11e5-86cd-a7fd38d2a50b', + name: 'this-is-a-dry-run' + }; + console.log('Creating image %s@%s from instance %s%s', + createOpts.name, createOpts.version, ctx.inst.id, + (ctx.inst.name ? ' ('+ctx.inst.name+')' : '')); + next(); + return; + } + + cloudapi.createImageFromMachine(createOpts, function (err, img) { + if (err) { + next(new errors.TritonError(err, 'error creating image')); + return; + } + ctx.img = img; + if (opts.json) { + console.log(JSON.stringify(img)); + } else { + console.log('Creating image %s@%s (%s)', + img.name, img.version, img.id); + } + next(); + }); + }, + function maybeWait(ctx, next) { + if (!opts.wait) { + return next(); + } + + // 1 'wait': no distraction. + // >1 'wait': distraction, pass in the N. + var distraction; + if (process.stderr.isTTY && opts.wait.length > 1) { + distraction = distractions.createDistraction(opts.wait.length); + } + + // Dry-run: fake wait for a few seconds. + var waiter = (opts.dry_run ? + function dryWait(waitOpts, waitCb) { + setTimeout(function () { + ctx.img.state = 'running'; + waitCb(null, ctx.img); + }, 5000); + } + : cloudapi.waitForImageStates.bind(cloudapi)); + + waiter({ + id: ctx.img.id, + states: ['active', 'failed'] + }, function (err, img) { + if (distraction) { + distraction.destroy(); + } + if (err) { + return next(err); + } + if (opts.json) { + console.log(JSON.stringify(img)); + } else if (img.state === 'active') { + var dur = Date.now() - ctx.start; + console.log('Created image %s (%s@%s) in %s', + img.id, img.name, img.version, + common.humanDurationFromMs(dur)); + } + if (img.state !== 'active') { + next(new Error(format('failed to create image %s (%s@%s)%s', + img.id, img.name, img.version, + (img.error ? format(': (%s) %s', + img.error.code, img.error.message): '')))); + } else { + next(); + } + }); + } + ]}, function (err) { + cb(err); + }); +} + +do_create.options = [ + { + names: ['help', 'h'], + type: 'bool', + help: 'Show this help.' + }, + { + group: 'Create options' + }, + { + names: ['description', 'd'], + type: 'string', + helpArg: 'DESC', + help: 'A short description of the image.' + }, + { + names: ['homepage'], + type: 'string', + helpArg: 'URL', + help: 'A homepage URL for the image.' + }, + { + names: ['eula'], + type: 'string', + helpArg: 'DESC', + help: 'A URL for an End User License Agreement (EULA) for the image.' + }, + { + names: ['acl'], + type: 'arrayOfString', + helpArg: 'ID', + help: 'Access Control List. The ID of an account to which to give ' + + 'access to this private image. This option can be used multiple ' + + 'times to give access to multiple accounts.' + }, + { + names: ['tag', 't'], + type: 'arrayOfString', + helpArg: 'TAG', + help: 'Add a tag when creating the image. Tags are ' + + 'key/value pairs available on the image API object as the ' + + '"tags" field. TAG is one of: a "key=value" string (bool and ' + + 'numeric "value" are converted to that type), a JSON object ' + + '(if first char is "{"), or a "@FILE" to have tags be ' + + 'loaded from FILE. This option can be used multiple times.' + }, + { + group: 'Other options' + }, + { + names: ['dry-run'], + type: 'bool', + help: 'Go through the motions without actually creating.' + }, + { + names: ['wait', 'w'], + type: 'arrayOfBool', + help: 'Wait for the creation to complete. Use multiple times for a ' + + 'spinner.' + }, + { + names: ['json', 'j'], + type: 'bool', + help: 'JSON stream output.' + } +]; + +do_create.help = ( + /* BEGIN JSSTYLED */ + 'Create a new instance.\n' + + '\n' + + 'Usage:\n' + + ' {{name}} create [] INSTANCE IMAGE-NAME IMAGE-VERSION\n' + + '\n' + + '{{options}}' + /* END JSSTYLED */ +); + +do_create.helpOpts = { + maxHelpCol: 20 +}; + + +module.exports = do_create; diff --git a/lib/do_image/do_wait.js b/lib/do_image/do_wait.js new file mode 100644 index 0000000..4674c7a --- /dev/null +++ b/lib/do_image/do_wait.js @@ -0,0 +1,142 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +/* + * Copyright 2016 Joyent, Inc. + * + * `triton image wait ...` + */ + +var vasync = require('vasync'); + +var distractions = require('../distractions'); +var errors = require('../errors'); + + +function do_wait(subcmd, opts, args, cb) { + var self = this; + if (opts.help) { + return this.do_help('help', {}, [subcmd], cb); + } else if (args.length < 1) { + return cb(new errors.UsageError('missing IMAGE arg(s)')); + } + var ids = args; + var states = []; + opts.states.forEach(function (s) { + /* JSSTYLED */ + states = states.concat(s.trim().split(/\s*,\s*/g)); + }); + + var distraction; + var done = 0; + var imgFromId = {}; + + vasync.pipeline({funcs: [ + function getImgs(_, next) { + vasync.forEachParallel({ + inputs: ids, + func: function getImg(id, nextImg) { + self.top.tritonapi.getImage(id, function (err, img) { + if (err) { + return nextImg(err); + } + if (states.indexOf(img.state) !== -1) { + console.log('%d/%d: Image %s (%s@%s) already %s', + ++done, ids.length, img.id, img.name, + img.version, img.state); + } else { + imgFromId[img.id] = img; + } + nextImg(); + }); + } + }, next); + }, + + function waitForImgs(_, next) { + var idsToWaitFor = Object.keys(imgFromId); + if (idsToWaitFor.length === 0) { + return next(); + } + + if (idsToWaitFor.length === 1) { + var img2 = imgFromId[idsToWaitFor[0]]; + console.log( + 'Waiting for image %s (%s@%s) to enter state (states: %s)', + img2.id, img2.name, img2.version, states.join(', ')); + } else { + console.log( + 'Waiting for %d images to enter state (states: %s)', + idsToWaitFor.length, states.join(', ')); + } + + /* + * TODO: need BigSpinner.log first. + * TODO: Also when adding a spinner, we need an equiv option to + * `triton create -wwww` to trigger the spinner (and size). By + * default: no spinner. + */ + if (false && + process.stderr.isTTY) + { + distraction = distractions.createDistraction(); + } + + vasync.forEachParallel({ + inputs: idsToWaitFor, + func: function waitForImg(id, nextImg) { + self.top.tritonapi.cloudapi.waitForImageStates({ + id: id, + states: states + }, function (err, img, res) { + if (err) { + return nextImg(err); + } + console.log('%d/%d: Image %s (%s@%s) moved to state %s', + ++done, ids.length, img.id, img.name, + img.version, img.state); + nextImg(); + }); + } + }, next); + } + + ]}, function (err) { + if (distraction) { + distraction.destroy(); + } + cb(err); + }); +} + +do_wait.help = [ + 'Wait for images to change to a particular state.', + '', + 'Usage:', + ' {{name}} wait [-s STATES] IMAGE [IMAGE ...]', + '', + '{{options}}', + 'Where "states" is a comma-separated list of target instance states,', + 'by default "active,failed". In other words, "triton img wait foo0" will', + 'wait for image "foo0" to complete creation.' +].join('\n'); +do_wait.options = [ + { + names: ['help', 'h'], + type: 'bool', + help: 'Show this help.' + }, + { + names: ['states', 's'], + type: 'arrayOfString', + default: ['active', 'failed'], + helpArg: 'STATES', + help: 'Instance states on which to wait. Default is "active,failed". ' + + 'Values can be comma-separated or multiple uses of the option.' + } +]; + +module.exports = do_wait; diff --git a/lib/do_image/index.js b/lib/do_image/index.js index cc65e3b..7e1b2bf 100644 --- a/lib/do_image/index.js +++ b/lib/do_image/index.js @@ -32,7 +32,9 @@ function ImageCLI(top) { helpSubcmds: [ 'help', 'list', - 'get' + 'get', + 'create', + 'wait' ] }); } @@ -45,6 +47,8 @@ ImageCLI.prototype.init = function init(opts, args, cb) { ImageCLI.prototype.do_list = require('./do_list'); ImageCLI.prototype.do_get = require('./do_get'); +ImageCLI.prototype.do_create = require('./do_create'); +ImageCLI.prototype.do_wait = require('./do_wait'); ImageCLI.aliases = ['img']; diff --git a/lib/do_instance/do_create.js b/lib/do_instance/do_create.js index f10c519..f326407 100644 --- a/lib/do_instance/do_create.js +++ b/lib/do_instance/do_create.js @@ -12,287 +12,15 @@ var assert = require('assert-plus'); var format = require('util').format; -var fs = require('fs'); -var strsplit = require('strsplit'); var tabula = require('tabula'); -var tilde = require('tilde-expansion'); var vasync = require('vasync'); var common = require('../common'); var distractions = require('../distractions'); var errors = require('../errors'); +var mat = require('../metadataandtags'); -// ---- loading/parsing metadata (and tags) from relevant options - -/* - * Load and validate metadata from these options: - * -m,--metadata DATA - * -M,--metadata-file KEY=FILE - * --script FILE - * - * - * says values may be string, num or bool. - */ -function metadataFromOpts(opts, log, cb) { - assert.arrayOfObject(opts._order, 'opts._order'); - assert.object(log, 'log'); - assert.func(cb, 'cb'); - - var metadata = {}; - - vasync.forEachPipeline({ - inputs: opts._order, - func: function metadataFromOpt(o, next) { - log.trace({opt: o}, 'metadataFromOpt'); - if (o.key === 'metadata') { - if (!o.value) { - next(new errors.UsageError( - 'empty metadata option value')); - return; - } else if (o.value[0] === '{') { - _addMetadataFromJsonStr( - 'metadata', metadata, o.value, null, next); - } else if (o.value[0] === '@') { - _addMetadataFromFile( - 'metadata', metadata, o.value.slice(1), next); - } else { - _addMetadataFromKvStr( - 'metadata', metadata, o.value, null, next); - } - } else if (o.key === 'metadata_file') { - _addMetadataFromKfStr( - 'metadata', metadata, o.value, null, next); - } else if (o.key === 'script') { - _addMetadatumFromFile('metadata', metadata, - 'user-script', o.value, o.value, next); - } else { - next(); - } - } - }, function (err) { - if (err) { - cb(err); - } else if (Object.keys(metadata).length) { - cb(null, metadata); - } else { - cb(); - } - }); -} - - -/* - * Load and validate tags from these options: - * -t,--tag DATA - * - * - * says values may be string, num or bool. - */ -function tagsFromOpts(opts, log, cb) { - assert.arrayOfObject(opts._order, 'opts._order'); - assert.object(log, 'log'); - assert.func(cb, 'cb'); - - var tags = {}; - - vasync.forEachPipeline({ - inputs: opts._order, - func: function tagsFromOpt(o, next) { - log.trace({opt: o}, 'tagsFromOpt'); - if (o.key === 'tag') { - if (!o.value) { - next(new errors.UsageError( - 'empty tag option value')); - return; - } else if (o.value[0] === '{') { - _addMetadataFromJsonStr('tag', tags, o.value, null, next); - } else if (o.value[0] === '@') { - _addMetadataFromFile('tag', tags, o.value.slice(1), next); - } else { - _addMetadataFromKvStr('tag', tags, o.value, null, next); - } - } else { - next(); - } - } - }, function (err) { - if (err) { - cb(err); - } else if (Object.keys(tags).length) { - cb(null, tags); - } else { - cb(); - } - }); -} - - -var allowedTypes = ['string', 'number', 'boolean']; -function _addMetadatum(ilk, metadata, key, value, from, cb) { - assert.string(ilk, 'ilk'); - assert.object(metadata, 'metadata'); - assert.string(key, 'key'); - assert.optionalString(from, 'from'); - assert.func(cb, 'cb'); - - if (allowedTypes.indexOf(typeof (value)) === -1) { - cb(new errors.UsageError(format( - 'invalid %s value type%s: must be one of %s: %s=%j', - ilk, (from ? ' (from ' + from + ')' : ''), - allowedTypes.join(', '), key, value))); - return; - } - - if (metadata.hasOwnProperty(key)) { - var valueStr = value.toString(); - console.error( - 'warning: %s "%s=%s"%s replaces earlier value for "%s"', - ilk, - key, - (valueStr.length > 10 - ? valueStr.slice(0, 7) + '...' : valueStr), - (from ? ' (from ' + from + ')' : ''), - key); - } - metadata[key] = value; - cb(); -} - -function _addMetadataFromObj(ilk, metadata, obj, from, cb) { - assert.string(ilk, 'ilk'); - assert.object(metadata, 'metadata'); - assert.object(obj, 'obj'); - assert.optionalString(from, 'from'); - assert.func(cb, 'cb'); - - vasync.forEachPipeline({ - inputs: Object.keys(obj), - func: function _oneField(key, next) { - _addMetadatum(ilk, metadata, key, obj[key], from, next); - } - }, cb); -} - -function _addMetadataFromJsonStr(ilk, metadata, s, from, cb) { - assert.string(ilk, 'ilk'); - try { - var obj = JSON.parse(s); - } catch (parseErr) { - cb(new errors.TritonError(parseErr, - format('%s%s is not valid JSON', ilk, - (from ? ' (from ' + from + ')' : '')))); - return; - } - _addMetadataFromObj(ilk, metadata, obj, from, cb); -} - -function _addMetadataFromFile(ilk, metadata, file, cb) { - assert.string(ilk, 'ilk'); - tilde(file, function (metaPath) { - fs.stat(metaPath, function (statErr, stats) { - if (statErr || !stats.isFile()) { - cb(new errors.TritonError(format( - '"%s" is not an existing file', file))); - return; - } - fs.readFile(metaPath, 'utf8', function (readErr, data) { - if (readErr) { - cb(readErr); - return; - } - /* - * The file is either a JSON object (first non-space - * char is '{'), or newline-separated key=value - * pairs. - */ - var dataTrim = data.trim(); - if (dataTrim.length && dataTrim[0] === '{') { - _addMetadataFromJsonStr(ilk, metadata, dataTrim, file, cb); - } else { - var lines = dataTrim.split(/\r?\n/g).filter( - function (line) { return line.trim(); }); - vasync.forEachPipeline({ - inputs: lines, - func: function oneLine(line, next) { - _addMetadataFromKvStr( - ilk, metadata, line, file, next); - } - }, cb); - } - }); - }); - }); -} - -function _addMetadataFromKvStr(ilk, metadata, s, from, cb) { - assert.string(ilk, 'ilk'); - - var parts = strsplit(s, '=', 2); - if (parts.length !== 2) { - cb(new errors.UsageError(format( - 'invalid KEY=VALUE %s argument: %s', ilk, s))); - return; - } - var value = parts[1]; - var valueTrim = value.trim(); - if (valueTrim === 'true') { - value = true; - } else if (valueTrim === 'false') { - value = false; - } else { - var num = Number(value); - if (!isNaN(num)) { - value = num; - } - } - _addMetadatum(ilk, metadata, parts[0].trim(), value, from, cb); -} - -/* - * Add metadata from `KEY=FILE` argument. - * Here "Kf" stands for "key/file". - */ -function _addMetadataFromKfStr(ilk, metadata, s, from, cb) { - assert.string(ilk, 'ilk'); - - var parts = strsplit(s, '=', 2); - if (parts.length !== 2) { - cb(new errors.UsageError(format( - 'invalid KEY=FILE %s argument: %s', ilk, s))); - return; - } - var key = parts[0].trim(); - var file = parts[1]; - - _addMetadatumFromFile(ilk, metadata, key, file, file, cb); -} - -function _addMetadatumFromFile(ilk, metadata, key, file, from, cb) { - assert.string(ilk, 'ilk'); - - tilde(file, function (filePath) { - fs.stat(filePath, function (statErr, stats) { - if (statErr || !stats.isFile()) { - cb(new errors.TritonError(format( - '%s path "%s" is not an existing file', ilk, file))); - return; - } - fs.readFile(filePath, 'utf8', function (readErr, content) { - if (readErr) { - cb(readErr); - return; - } - _addMetadatum(ilk, metadata, key, content, from, cb); - }); - }); - }); -} - - - -// ---- the command - function do_create(subcmd, opts, args, cb) { var self = this; if (opts.help) { @@ -307,7 +35,7 @@ function do_create(subcmd, opts, args, cb) { vasync.pipeline({arg: {}, funcs: [ function loadMetadata(ctx, next) { - metadataFromOpts(opts, log, function (err, metadata) { + mat.metadataFromOpts(opts, log, function (err, metadata) { if (err) { next(err); return; @@ -321,7 +49,7 @@ function do_create(subcmd, opts, args, cb) { }); }, function loadTags(ctx, next) { - tagsFromOpts(opts, log, function (err, tags) { + mat.tagsFromOpts(opts, log, function (err, tags) { if (err) { next(err); return; @@ -604,5 +332,3 @@ do_create.helpOpts = { module.exports = do_create; -do_create.metadataFromOpts = metadataFromOpts; // export for testing -do_create.tagsFromOpts = tagsFromOpts; // export for testing diff --git a/lib/tritonapi.js b/lib/tritonapi.js index a0a3c2c..0213f32 100644 --- a/lib/tritonapi.js +++ b/lib/tritonapi.js @@ -354,9 +354,6 @@ TritonApi.prototype.getImage = function getImage(opts, cb) { ]}, function done(err) { if (err) { cb(err); - } else if (img.state !== 'active') { - cb(new errors.TritonError( - format('image %s is not active', opts.name))); } else { cb(null, img); } @@ -366,7 +363,10 @@ TritonApi.prototype.getImage = function getImage(opts, cb) { var name = s[0]; var version = s[1]; - var listOpts = {}; + var listOpts = { + // Explicitly include inactive images. + state: 'all' + }; if (version) { listOpts.name = name; listOpts.version = version; diff --git a/package.json b/package.json index 83c49c5..980092f 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "triton", "description": "Joyent Triton CLI and client (https://www.joyent.com/triton)", - "version": "4.2.1", + "version": "4.3.0", "author": "Joyent (joyent.com)", "dependencies": { "assert-plus": "0.2.0", diff --git a/test/integration/cli-manage-workflow.test.js b/test/integration/cli-manage-workflow.test.js index 81f49dd..a4f06be 100644 --- a/test/integration/cli-manage-workflow.test.js +++ b/test/integration/cli-manage-workflow.test.js @@ -225,8 +225,10 @@ test('triton manage workflow', opts, function (tt) { }); }); - // remove instance - tt.test(' triton delete', function (t) { + // Remove instance. Add a test timeout, because '-w' on delete doesn't + // have a way to know if the attempt failed or if it is just taking a + // really long time. + tt.test(' triton delete', {timeout: 10 * 60 * 1000}, function (t) { h.safeTriton(t, ['delete', '-w', instance.id], function (stdout) { t.end(); }); diff --git a/test/unit/metadataFromOpts.test.js b/test/unit/metadataFromOpts.test.js index 0227b75..b26c2b2 100644 --- a/test/unit/metadataFromOpts.test.js +++ b/test/unit/metadataFromOpts.test.js @@ -17,8 +17,7 @@ var cmdln = require('cmdln'); var format = require('util').format; var test = require('tape'); -var metadataFromOpts = - require('../../lib/do_instance/do_create').metadataFromOpts; +var metadataFromOpts = require('../../lib/metadataandtags').metadataFromOpts; // ---- globals diff --git a/test/unit/tagsFromOpts.test.js b/test/unit/tagsFromOpts.test.js index e71a064..12a82e4 100644 --- a/test/unit/tagsFromOpts.test.js +++ b/test/unit/tagsFromOpts.test.js @@ -17,7 +17,7 @@ var cmdln = require('cmdln'); var format = require('util').format; var test = require('tape'); -var tagsFromOpts = require('../../lib/do_instance/do_create').tagsFromOpts; +var tagsFromOpts = require('../../lib/metadataandtags').tagsFromOpts; // ---- globals