From 810f0add5677b2372591a0522fe9f63eab23ac91 Mon Sep 17 00:00:00 2001 From: Trent Mick Date: Mon, 25 Jan 2016 23:23:36 -0800 Subject: [PATCH] node-triton#78 'triton image delete IMAGE' --- CHANGES.md | 3 +- lib/cloudapi2.js | 28 ++++++- lib/do_image/do_delete.js | 155 ++++++++++++++++++++++++++++++++++++++ lib/do_image/index.js | 4 +- lib/tritonapi.js | 12 ++- package.json | 2 +- 6 files changed, 194 insertions(+), 10 deletions(-) create mode 100644 lib/do_image/do_delete.js diff --git a/CHANGES.md b/CHANGES.md index 776ea36..1846abe 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,7 +1,8 @@ # node-triton changelog -## 4.3.2 (not yet released) +## 4.4.0 (not yet released) +- #78 `triton image delete IMAGE` - #79 Fix `triton instance get NAME` to make sure it gets the `dns_names` CNS field. - PUBAPI-1227: Note that `triton image list` doesn't include Docker images, at diff --git a/lib/cloudapi2.js b/lib/cloudapi2.js index bae42d2..9b5a6a7 100644 --- a/lib/cloudapi2.js +++ b/lib/cloudapi2.js @@ -539,6 +539,26 @@ CloudApi.prototype.getImage = function getImage(opts, cb) { }); }; +/** + * Delete an image by id. + * + * + * @param {String} id (required) The image id. + * @param {Function} callback of the form `function (err, res)` + */ +CloudApi.prototype.deleteImage = function deleteImage(id, callback) { + var self = this; + assert.uuid(id, 'id'); + assert.func(callback, 'callback'); + + var opts = { + path: format('/%s/images/%s', self.account, id), + method: 'DELETE' + }; + this._request(opts, function (err, req, res) { + callback(err, res); + }); +}; /** * @@ -684,16 +704,16 @@ CloudApi.prototype.getMachine = function getMachine(opts, cb) { /** * delete a machine by id. * - * @param {String} uuid (required) The machine id. + * @param {String} id (required) The machine id. * @param {Function} callback of the form `function (err, res)` */ -CloudApi.prototype.deleteMachine = function deleteMachine(uuid, callback) { +CloudApi.prototype.deleteMachine = function deleteMachine(id, callback) { var self = this; - assert.string(uuid, 'uuid'); + assert.uuid(id, 'id'); assert.func(callback, 'callback'); var opts = { - path: format('/%s/machines/%s', self.account, uuid), + path: format('/%s/machines/%s', self.account, id), method: 'DELETE' }; this._request(opts, function (err, req, res) { diff --git a/lib/do_image/do_delete.js b/lib/do_image/do_delete.js new file mode 100644 index 0000000..a1e1a86 --- /dev/null +++ b/lib/do_image/do_delete.js @@ -0,0 +1,155 @@ +/* + * 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 delete ...` + */ + +var format = require('util').format; +var vasync = require('vasync'); + +var common = require('../common'); +var errors = require('../errors'); + + +function do_delete(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; + + vasync.pipeline({arg: {}, funcs: [ + /* + * Lookup images, if not given UUIDs: we'll need to do it anyway + * for the DeleteImage call(s), and doing so explicitly here allows + * us to emit better output. + */ + function getImgs(ctx, next) { + ctx.imgFromId = {}; + ctx.missingIds = []; + // TODO: this should have a concurrency + vasync.forEachParallel({ + inputs: ids, + func: function getImg(id, nextImg) { + if (common.isUUID(id)) { + // TODO: get info from cache if we have it + ctx.imgFromId[id] = { + id: id, + _repr: id + }; + nextImg(); + return; + } + // TODO: allow use of cache here + self.top.tritonapi.getImage(id, function (err, img) { + if (err) { + if (err.statusCode === 404) { + ctx.missingIds.push(id); + nextImg(); + } else { + nextImg(err); + } + } else { + ctx.imgFromId[img.id] = img; + img._repr = format('%s (%s@%s)', img.id, + img.name, img.version); + nextImg(); + } + }); + } + }, next); + }, + + function errOnMissingIds(ctx, next) { + if (ctx.missingIds.length === 1) { + next(new errors.TritonError('no such image: ' + + ctx.missingIds[0])); + } else if (ctx.missingIds.length > 1) { + next(new errors.TritonError('no such images: ' + + ctx.missingIds.join(', '))); + } else { + next(); + } + }, + + function confirm(ctx, next) { + if (opts.force) { + next(); + return; + } + + var keys = Object.keys(ctx.imgFromId); + var msg; + if (keys.length === 1) { + msg = format('Delete image %s? [y/n] ', + ctx.imgFromId[keys[0]]._repr); + } else { + msg = format('Delete %d images? [y/n] ', keys.length); + } + + common.promptYesNo({msg: msg}, function (answer) { + if (answer !== 'y') { + console.error('Aborting'); + next(true); // early abort signal + } else { + next(); + } + }); + }, + + function deleteThem(ctx, next) { + // TODO: forEachParallel with concurrency + vasync.forEachPipeline({ + inputs: Object.keys(ctx.imgFromId), + func: function deleteOne(id, nextOne) { + self.top.tritonapi.cloudapi.deleteImage(id, function (err) { + if (!err) { + console.log('Deleted image %s', + ctx.imgFromId[id]._repr); + } + nextOne(err); + }); + } + }, next); + } + ]}, function (err) { + cb(err); + }); +} + +do_delete.help = [ + /* BEGIN JSSTYLED */ + 'Delete one or more images.', + '', + 'Usage:', + ' {{name}} delete IMAGE [IMAGE...]', + '', + '{{options}}', + 'Where "IMAGE" is an image ID (a full UUID), an image name (selects the', + 'latest, by "published_at", image with that name), an image "name@version"', + '(selects latest match by "published_at"), or an image short ID (ID prefix).' + /* END JSSTYLED */ +].join('\n'); +do_delete.options = [ + { + names: ['help', 'h'], + type: 'bool', + help: 'Show this help.' + }, + { + names: ['force', 'f'], + type: 'bool', + help: 'Skip confirmation of delete.' + } +]; + +do_delete.aliases = ['rm']; +module.exports = do_delete; diff --git a/lib/do_image/index.js b/lib/do_image/index.js index 7e1b2bf..40d0807 100644 --- a/lib/do_image/index.js +++ b/lib/do_image/index.js @@ -23,7 +23,7 @@ function ImageCLI(top) { name: top.name + ' image', /* BEGIN JSSTYLED */ desc: [ - 'List, get, create and update Triton images.' + 'List, get, create and manage Triton images.' ].join('\n'), /* END JSSTYLED */ helpOpts: { @@ -34,6 +34,7 @@ function ImageCLI(top) { 'list', 'get', 'create', + 'delete', 'wait' ] }); @@ -48,6 +49,7 @@ 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_delete = require('./do_delete'); ImageCLI.prototype.do_wait = require('./do_wait'); diff --git a/lib/tritonapi.js b/lib/tritonapi.js index 1926867..6dbb4c3 100644 --- a/lib/tritonapi.js +++ b/lib/tritonapi.js @@ -362,16 +362,20 @@ TritonApi.prototype.getImage = function getImage(opts, cb) { var s = opts.name.split('@'); var name = s[0]; var version = s[1]; + var nameSelector; var listOpts = { // Explicitly include inactive images. state: 'all' }; if (version) { + nameSelector = name + '@' + version; listOpts.name = name; listOpts.version = version; // XXX This is bogus now? listOpts.useCache = opts.useCache; + } else { + nameSelector = name; } this.cloudapi.listImages(listOpts, function (err, imgs) { if (err) { @@ -399,11 +403,13 @@ TritonApi.prototype.getImage = function getImage(opts, cb) { cb(null, shortIdMatches[0]); } else if (shortIdMatches.length === 0) { cb(new errors.ResourceNotFoundError(format( - 'no image with name or short id "%s" was found', name))); + 'no image with %s or short id "%s" was found', + nameSelector, name))); } else { cb(new errors.ResourceNotFoundError( - format('no image with name "%s" was found ' - + 'and "%s" is an ambiguous short id', name))); + format('no image with %s "%s" was found ' + + 'and "%s" is an ambiguous short id', + nameSelector, name, name))); } }); } diff --git a/package.json b/package.json index 98aa665..96d288d 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.3.2", + "version": "4.4.0", "author": "Joyent (joyent.com)", "dependencies": { "assert-plus": "0.2.0",