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
## 6.1.0
- [joyent/node-triton#250] Avoid an error from `triton profile list` if
only *some* of the minimal `TRITON_` or `SDC_` envvars are defined.
- [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
decrypting) the account key itself. This makes using Docker simpler with keys
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

View File

@ -1039,7 +1039,7 @@ CloudApi.prototype.exportImage = function exportImage(opts, cb) {
* - {Object} fields Required. The fields to update in the image.
* @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.object(opts.fields, 'fields');
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.
*

View File

@ -1461,6 +1461,19 @@ function parseNicStr(nic) {
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
@ -1502,6 +1515,7 @@ module.exports = {
readStdin: readStdin,
validateObject: validateObject,
ipv4ToLong: ipv4ToLong,
parseNicStr: parseNicStr
parseNicStr: parseNicStr,
imageRepr: imageRepr
};
// 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 ...`
*/
var assert = require('assert-plus');
var format = require('util').format;
var tabula = require('tabula');
var vasync = require('vasync');
var common = require('../common');
var errors = require('../errors');
@ -67,17 +69,45 @@ function do_list(subcmd, opts, args, callback) {
listOpts.state = 'all';
}
var self = this;
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) {
common.jsonStream(imgs);
} else {
@ -99,6 +129,20 @@ function do_list(subcmd, opts, args, callback) {
if (img.origin) flags.push('I');
if (img['public']) flags.push('P');
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;
}
@ -108,9 +152,9 @@ function do_list(subcmd, opts, args, callback) {
sort: sort
});
}
callback();
});
});
next();
}
]}, callback);
}
do_list.options = [
@ -157,6 +201,8 @@ do_list.help = [
' shortid* A short ID prefix.',
' flags* Single letter flags summarizing some fields:',
' "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)',
' "X" has a state *other* than "active"',
' pubdate* Short form of "published_at" with just the date',

View File

@ -33,6 +33,7 @@ function ImageCLI(top) {
'help',
'list',
'get',
'clone',
'create',
'delete',
'export',
@ -51,6 +52,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_clone = require('./do_clone');
ImageCLI.prototype.do_create = require('./do_create');
ImageCLI.prototype.do_delete = require('./do_delete');
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];
});
}
if (opts.allow_shared_images) {
createOpts.allow_shared_images = true;
}
for (var i = 0; i < opts._order.length; i++) {
var opt = opts._order[i];
@ -498,6 +501,11 @@ do_create.options = [
'Joyent-provided images, the user-script is run at every boot ' +
'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'

View File

@ -133,7 +133,7 @@ var errors = require('./errors');
// ---- 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.
*

View File

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