TRITON-53 x-account image clone

Reviewed by: Trent Mick <trentm@gmail.com>
Approved by: Trent Mick <trentm@gmail.com>
This commit is contained in:
Todd Whiteman 2018-06-27 17:28:02 -07:00
parent 264f69dc54
commit dc5dc12052
9 changed files with 264 additions and 17 deletions

View File

@ -6,6 +6,8 @@ Known issues:
## not yet released ## not yet released
## 6.1.0
- [joyent/node-triton#250] Avoid an error from `triton profile list` if - [joyent/node-triton#250] Avoid an error from `triton profile list` if
only *some* of the minimal `TRITON_` or `SDC_` envvars are defined. only *some* of the minimal `TRITON_` or `SDC_` envvars are defined.
- [TRITON-401] Add `triton network` and `triton vlan` commands, for - [TRITON-401] Add `triton network` and `triton vlan` commands, for
@ -17,6 +19,13 @@ Known issues:
Docker setup and signs them with an account key, rather than copying (and Docker setup and signs them with an account key, rather than copying (and
decrypting) the account key itself. This makes using Docker simpler with keys decrypting) the account key itself. This makes using Docker simpler with keys
in an SSH Agent. in an SSH Agent.
- [TRITON-53] x-account image clone. A user can make a copy of a shared image
using the `triton image clone` command.
- [TRITON-53] A shared image (i.e. when the user is on the image.acl) is no
longer provisionable by default - you will need to explicitly add the
--allow-shared-images cli option when calling `triton create` command to
provision from a shared image (or clone the image then provision from the
clone).
## 6.0.0 ## 6.0.0

View File

@ -1039,7 +1039,7 @@ CloudApi.prototype.exportImage = function exportImage(opts, cb) {
* - {Object} fields Required. The fields to update in the image. * - {Object} fields Required. The fields to update in the image.
* @param {Function} cb of the form `function (err, body, res)` * @param {Function} cb of the form `function (err, body, res)`
*/ */
CloudApi.prototype.updateImage = function shareImage(opts, cb) { CloudApi.prototype.updateImage = function updateImage(opts, cb) {
assert.uuid(opts.id, 'id'); assert.uuid(opts.id, 'id');
assert.object(opts.fields, 'fields'); assert.object(opts.fields, 'fields');
assert.func(cb, 'cb'); assert.func(cb, 'cb');
@ -1057,6 +1057,31 @@ CloudApi.prototype.updateImage = function shareImage(opts, cb) {
}); });
}; };
/**
* Clone an image.
* <http://apidocs.joyent.com/cloudapi/#CloneImage>
*
* @param {Object} opts
* - {UUID} id Required. The id of the image to update.
* @param {Function} cb of the form `function (err, body, res)`
*/
CloudApi.prototype.cloneImage = function cloneImage(opts, cb) {
assert.uuid(opts.id, 'id');
assert.func(cb, 'cb');
this._request({
method: 'POST',
path: format('/%s/images/%s?action=clone', this.account, opts.id),
data: {}
}, function (err, req, res, body) {
if (err) {
cb(err, null, res);
return;
}
cb(null, body, res);
});
};
/** /**
* Wait for an image to go one of a set of specfic states. * Wait for an image to go one of a set of specfic states.
* *

View File

@ -1461,6 +1461,19 @@ function parseNicStr(nic) {
return obj; return obj;
} }
/*
* Return a short image string that represents the given image object.
*
* @param img {Object} The image object.
* @returns {String} A network object. E.g.
* 'a6cf222d-73f4-414c-a427-5c238ef8e1b7 (jillmin@1.0.0)'
*/
function imageRepr(img) {
assert.object(img);
return format('%s (%s@%s)', img.id, img.name, img.version);
}
//---- exports //---- exports
@ -1502,6 +1515,7 @@ module.exports = {
readStdin: readStdin, readStdin: readStdin,
validateObject: validateObject, validateObject: validateObject,
ipv4ToLong: ipv4ToLong, ipv4ToLong: ipv4ToLong,
parseNicStr: parseNicStr parseNicStr: parseNicStr,
imageRepr: imageRepr
}; };
// vim: set softtabstop=4 shiftwidth=4: // vim: set softtabstop=4 shiftwidth=4:

107
lib/do_image/do_clone.js Normal file
View File

@ -0,0 +1,107 @@
/*
* 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 (c) 2018, Joyent, Inc.
*
* `triton image clone ...`
*/
var vasync = require('vasync');
var common = require('../common');
var errors = require('../errors');
// ---- the command
function do_clone(subcmd, opts, args, cb) {
if (opts.help) {
this.do_help('help', {}, [subcmd], cb);
return;
} else if (args.length !== 1) {
cb(new errors.UsageError(
'incorrect number of args: expected 1, got ' + args.length));
return;
}
var log = this.top.log;
var tritonapi = this.top.tritonapi;
vasync.pipeline({arg: {cli: this.top}, funcs: [
common.cliSetupTritonApi,
function cloneImage(ctx, next) {
log.trace({dryRun: opts.dry_run, account: ctx.account},
'image clone account');
if (opts.dry_run) {
next();
return;
}
tritonapi.cloneImage({image: args[0]}, function _cloneCb(err, img) {
if (err) {
next(new errors.TritonError(err, 'error cloning image'));
return;
}
log.trace({img: img}, 'image clone result');
if (opts.json) {
console.log(JSON.stringify(img));
} else {
console.log('Cloned image %s to %s',
args[0], common.imageRepr(img));
}
next();
});
}
]}, cb);
}
do_clone.options = [
{
names: ['help', 'h'],
type: 'bool',
help: 'Show this help.'
},
{
group: 'Other options'
},
{
names: ['dry-run'],
type: 'bool',
help: 'Go through the motions without actually cloning.'
},
{
names: ['json', 'j'],
type: 'bool',
help: 'JSON stream output.'
}
];
do_clone.synopses = [
'{{name}} {{cmd}} [OPTIONS] IMAGE'
];
do_clone.help = [
/* BEGIN JSSTYLED */
'Clone a shared image.',
'',
'{{usage}}',
'',
'{{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).',
'',
'Note: Only shared images can be cloned.'
/* END JSSTYLED */
].join('\n');
do_clone.completionArgtypes = ['tritonimage', 'none'];
module.exports = do_clone;

View File

@ -5,13 +5,15 @@
*/ */
/* /*
* Copyright 2016 Joyent, Inc. * Copyright 2018 Joyent, Inc.
* *
* `triton image list ...` * `triton image list ...`
*/ */
var assert = require('assert-plus');
var format = require('util').format; var format = require('util').format;
var tabula = require('tabula'); var tabula = require('tabula');
var vasync = require('vasync');
var common = require('../common'); var common = require('../common');
var errors = require('../errors'); var errors = require('../errors');
@ -67,17 +69,45 @@ function do_list(subcmd, opts, args, callback) {
listOpts.state = 'all'; listOpts.state = 'all';
} }
var self = this;
var tritonapi = this.top.tritonapi; var tritonapi = this.top.tritonapi;
common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
if (setupErr) {
callback(setupErr);
return;
}
tritonapi.listImages(listOpts, function onRes(err, imgs, res) {
if (err) {
return callback(err);
}
vasync.pipeline({ arg: {}, funcs: [
function setupTritonApi(_, next) {
common.cliSetupTritonApi({cli: self.top}, next);
},
function getImages(ctx, next) {
tritonapi.listImages(listOpts, function onRes(err, imgs, res) {
if (err) {
next(err);
return;
}
ctx.imgs = imgs;
next();
});
},
function getUserAccount(ctx, next) {
// If using json output, or when there are no images that use an ACL
// - we don't need to fetch the account, as the account is only used
// to check if the image is shared (i.e. the account is in the image
// ACL) so it can output image flags in non-json mode.
if (opts.json || ctx.imgs.every(function _checkAcl(img) {
return !Array.isArray(img.acl) || img.acl.length === 0;
})) {
next();
return;
}
tritonapi.cloudapi.getAccount(function _accountCb(err, account) {
if (err) {
next(err);
return;
}
ctx.account = account;
next();
});
},
function formatImages(ctx, next) {
var imgs = ctx.imgs;
if (opts.json) { if (opts.json) {
common.jsonStream(imgs); common.jsonStream(imgs);
} else { } else {
@ -99,6 +129,20 @@ function do_list(subcmd, opts, args, callback) {
if (img.origin) flags.push('I'); if (img.origin) flags.push('I');
if (img['public']) flags.push('P'); if (img['public']) flags.push('P');
if (img.state !== 'active') flags.push('X'); if (img.state !== 'active') flags.push('X');
// Add image sharing flags.
if (Array.isArray(img.acl) && img.acl.length > 0) {
assert.string(ctx.account, 'ctx.account');
if (img.owner === ctx.account.id) {
// This image has been shared with other accounts.
flags.push('+');
}
if (img.acl.indexOf(ctx.account.id) !== -1) {
// This image has been shared with this account.
flags.push('S');
}
}
img.flags = flags.length ? flags.join('') : undefined; img.flags = flags.length ? flags.join('') : undefined;
} }
@ -108,9 +152,9 @@ function do_list(subcmd, opts, args, callback) {
sort: sort sort: sort
}); });
} }
callback(); next();
}); }
}); ]}, callback);
} }
do_list.options = [ do_list.options = [
@ -157,6 +201,8 @@ do_list.help = [
' shortid* A short ID prefix.', ' shortid* A short ID prefix.',
' flags* Single letter flags summarizing some fields:', ' flags* Single letter flags summarizing some fields:',
' "P" image is public', ' "P" image is public',
' "+" you are sharing this image with others',
' "S" this image has been shared with you',
' "I" an incremental image (i.e. has an origin)', ' "I" an incremental image (i.e. has an origin)',
' "X" has a state *other* than "active"', ' "X" has a state *other* than "active"',
' pubdate* Short form of "published_at" with just the date', ' pubdate* Short form of "published_at" with just the date',

View File

@ -33,6 +33,7 @@ function ImageCLI(top) {
'help', 'help',
'list', 'list',
'get', 'get',
'clone',
'create', 'create',
'delete', 'delete',
'export', 'export',
@ -51,6 +52,7 @@ ImageCLI.prototype.init = function init(opts, args, cb) {
ImageCLI.prototype.do_list = require('./do_list'); ImageCLI.prototype.do_list = require('./do_list');
ImageCLI.prototype.do_get = require('./do_get'); ImageCLI.prototype.do_get = require('./do_get');
ImageCLI.prototype.do_clone = require('./do_clone');
ImageCLI.prototype.do_create = require('./do_create'); ImageCLI.prototype.do_create = require('./do_create');
ImageCLI.prototype.do_delete = require('./do_delete'); ImageCLI.prototype.do_delete = require('./do_delete');
ImageCLI.prototype.do_export = require('./do_export'); ImageCLI.prototype.do_export = require('./do_export');

View File

@ -289,6 +289,9 @@ function do_create(subcmd, opts, args, cb) {
createOpts['tag.'+key] = ctx.tags[key]; createOpts['tag.'+key] = ctx.tags[key];
}); });
} }
if (opts.allow_shared_images) {
createOpts.allow_shared_images = true;
}
for (var i = 0; i < opts._order.length; i++) { for (var i = 0; i < opts._order.length; i++) {
var opt = opts._order[i]; var opt = opts._order[i];
@ -498,6 +501,11 @@ do_create.options = [
'Joyent-provided images, the user-script is run at every boot ' + 'Joyent-provided images, the user-script is run at every boot ' +
'of the instance. This is a shortcut for `-M user-script=FILE`.' 'of the instance. This is a shortcut for `-M user-script=FILE`.'
}, },
{
names: ['allow-shared-images'],
type: 'bool',
help: 'Allow instance creation to use a shared image.'
},
{ {
group: 'Other options' group: 'Other options'

View File

@ -133,7 +133,7 @@ var errors = require('./errors');
// ---- globals // ---- globals
var CLOUDAPI_ACCEPT_VERSION = '~8'; var CLOUDAPI_ACCEPT_VERSION = '~9||~8';
@ -958,6 +958,42 @@ TritonApi.prototype.unshareImage = function unshareImage(opts, cb)
}); });
}; };
/**
* Clone a shared image.
*
* @param {Object} opts
* - {String} image The image UUID, name, or short ID. Required.
* @param {Function} cb `function (err, img)`
* On failure `err` is an error instance, else it is null.
* On success: `img` is the cloned image object.
*/
TritonApi.prototype.cloneImage = function cloneImage(opts, cb)
{
var self = this;
assert.object(opts, 'opts');
assert.string(opts.image, 'opts.image');
assert.func(cb, 'cb');
var arg = {
image: opts.image,
client: self
};
var img;
vasync.pipeline({arg: arg, funcs: [
_stepImg,
function cloudApiCloneImage(ctx, next) {
self.cloudapi.cloneImage({id: ctx.img.id},
function _cloneImageCb(err, img_) {
img = img_;
next(err);
});
}
]}, function (err) {
cb(err, img);
});
};
/** /**
* Get an active package by ID, exact name, or short ID, in that order. * Get an active package by ID, exact name, or short ID, in that order.
* *

View File

@ -1,7 +1,7 @@
{ {
"name": "triton", "name": "triton",
"description": "Joyent Triton CLI and client (https://www.joyent.com/triton)", "description": "Joyent Triton CLI and client (https://www.joyent.com/triton)",
"version": "6.0.0", "version": "6.1.0",
"author": "Joyent (joyent.com)", "author": "Joyent (joyent.com)",
"homepage": "https://github.com/joyent/node-triton", "homepage": "https://github.com/joyent/node-triton",
"dependencies": { "dependencies": {