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:
parent
264f69dc54
commit
dc5dc12052
@ -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
|
||||||
|
|
||||||
|
@ -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.
|
||||||
*
|
*
|
||||||
|
@ -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
107
lib/do_image/do_clone.js
Normal 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;
|
@ -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',
|
||||||
|
@ -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');
|
||||||
|
@ -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'
|
||||||
|
@ -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.
|
||||||
*
|
*
|
||||||
|
@ -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": {
|
||||||
|
Reference in New Issue
Block a user