TRITON-116 node-triton image sharing

Reviewed by: Trent Mick <trentm@gmail.com>
Approved by: Trent Mick <trentm@gmail.com>
This commit is contained in:
Todd Whiteman 2018-02-14 11:52:29 -08:00
parent e7c02436df
commit 3f243f8c8f
8 changed files with 431 additions and 12 deletions

View File

@ -6,6 +6,11 @@ Known issues:
## not yet released
## 5.7.0
- [TRITON-116] node-triton image sharing. Adds `triton image share` and
`triton image unshare` commands.
## 5.6.1
- [PUBAPI-1470] volume objects should expose their creation timestamp in a

View File

@ -5,7 +5,7 @@
*/
/*
* Copyright 2017 Joyent, Inc.
* Copyright (c) 2018, Joyent, Inc.
*
* Client library for the SmartDataCenter Cloud API (cloudapi).
* http://apidocs.joyent.com/cloudapi/
@ -767,6 +767,33 @@ CloudApi.prototype.exportImage = function exportImage(opts, cb) {
});
};
/**
* Update an image.
* <http://apidocs.joyent.com/cloudapi/#UpdateImage>
*
* @param {Object} opts
* - {UUID} id Required. The id of the image to update.
* - {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) {
assert.uuid(opts.id, 'id');
assert.object(opts.fields, 'fields');
assert.func(cb, 'cb');
this._request({
method: 'POST',
path: format('/%s/images/%s?action=update', this.account, opts.id),
data: opts.fields
}, 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.
*

115
lib/do_image/do_share.js Normal file
View File

@ -0,0 +1,115 @@
/*
* 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 share ...`
*/
var format = require('util').format;
var vasync = require('vasync');
var common = require('../common');
var errors = require('../errors');
// ---- the command
function do_share(subcmd, opts, args, cb) {
if (opts.help) {
this.do_help('help', {}, [subcmd], cb);
return;
} else if (args.length !== 2) {
cb(new errors.UsageError(
'incorrect number of args: expect 2, got ' + args.length));
return;
}
var log = this.top.log;
var tritonapi = this.top.tritonapi;
vasync.pipeline({arg: {cli: this.top}, funcs: [
common.cliSetupTritonApi,
function shareImage(ctx, next) {
log.trace({dryRun: opts.dry_run, account: ctx.account},
'image share account');
if (opts.dry_run) {
next();
return;
}
tritonapi.shareImage({
image: args[0],
account: args[1]
}, function (err, img) {
if (err) {
next(new errors.TritonError(err, 'error sharing image'));
return;
}
log.trace({img: img}, 'image share result');
if (opts.json) {
console.log(JSON.stringify(img));
} else {
console.log('Shared image %s with account %s',
args[0], args[1]);
}
next();
});
}
]}, function (err) {
cb(err);
});
}
do_share.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 sharing.'
},
{
names: ['json', 'j'],
type: 'bool',
help: 'JSON stream output.'
}
];
do_share.synopses = [
'{{name}} {{cmd}} [OPTIONS] IMAGE ACCOUNT'
];
do_share.help = [
/* BEGIN JSSTYLED */
'Share an image with another account.',
'',
'{{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).',
'',
'Where "ACCOUNT" is the full account UUID.',
'',
'Note: Only images that are owned by the account can be shared.'
/* END JSSTYLED */
].join('\n');
do_share.completionArgtypes = ['tritonimage', 'none'];
module.exports = do_share;

115
lib/do_image/do_unshare.js Normal file
View File

@ -0,0 +1,115 @@
/*
* 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 unshare ...`
*/
var format = require('util').format;
var vasync = require('vasync');
var common = require('../common');
var errors = require('../errors');
// ---- the command
function do_unshare(subcmd, opts, args, cb) {
if (opts.help) {
this.do_help('help', {}, [subcmd], cb);
return;
} else if (args.length !== 2) {
cb(new errors.UsageError(
'incorrect number of args: expect 2, got ' + args.length));
return;
}
var log = this.top.log;
var tritonapi = this.top.tritonapi;
vasync.pipeline({arg: {cli: this.top}, funcs: [
common.cliSetupTritonApi,
function unshareImage(ctx, next) {
log.trace({dryRun: opts.dry_run, account: ctx.account},
'image unshare account');
if (opts.dry_run) {
next();
return;
}
tritonapi.unshareImage({
image: args[0],
account: args[1]
}, function (err, img) {
if (err) {
next(new errors.TritonError(err, 'error unsharing image'));
return;
}
log.trace({img: img}, 'image unshare result');
if (opts.json) {
console.log(JSON.stringify(img));
} else {
console.log('Unshared image %s with account %s',
args[0], args[1]);
}
next();
});
}
]}, function (err) {
cb(err);
});
}
do_unshare.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 sharing.'
},
{
names: ['json', 'j'],
type: 'bool',
help: 'JSON stream output.'
}
];
do_unshare.synopses = [
'{{name}} {{cmd}} [OPTIONS] IMAGE ACCOUNT'
];
do_unshare.help = [
/* BEGIN JSSTYLED */
'Unshare an image with another account.',
'',
'{{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).',
'',
'Where "ACCOUNT" is the full account UUID.',
'',
'Note: Only images that are owned by the account can be unshared.'
/* END JSSTYLED */
].join('\n');
do_unshare.completionArgtypes = ['tritonimage', 'none'];
module.exports = do_unshare;

View File

@ -5,7 +5,7 @@
*/
/*
* Copyright 2017 Joyent, Inc.
* Copyright (c) 2018, Joyent, Inc.
*
* `triton image ...`
*/
@ -36,6 +36,8 @@ function ImageCLI(top) {
'create',
'delete',
'export',
'share',
'unshare',
'wait'
]
});
@ -52,6 +54,8 @@ ImageCLI.prototype.do_get = require('./do_get');
ImageCLI.prototype.do_create = require('./do_create');
ImageCLI.prototype.do_delete = require('./do_delete');
ImageCLI.prototype.do_export = require('./do_export');
ImageCLI.prototype.do_share = require('./do_share');
ImageCLI.prototype.do_unshare = require('./do_unshare');
ImageCLI.prototype.do_wait = require('./do_wait');

View File

@ -5,7 +5,7 @@
*/
/*
* Copyright 2017 Joyent, Inc.
* Copyright (c) 2018, Joyent, Inc.
*/
/* BEGIN JSSTYLED */
@ -233,10 +233,10 @@ function _stepPkgId(arg, next) {
/**
* A function appropriate for `vasync.pipeline` funcs that takes a `arg.image`
* image name, shortid, or uuid, and determines the image id (setting it
* as arg.imgId).
* image name, shortid, or uuid, and determines the image object (setting it
* as arg.img).
*/
function _stepImgId(arg, next) {
function _stepImg(arg, next) {
assert.object(arg.client, 'arg.client');
assert.string(arg.image, 'arg.image');
@ -244,7 +244,7 @@ function _stepImgId(arg, next) {
if (err) {
next(err);
} else {
arg.imgId = img.id;
arg.img = img;
next();
}
});
@ -745,10 +745,10 @@ TritonApi.prototype.exportImage = function exportImage(opts, cb)
};
vasync.pipeline({arg: arg, funcs: [
_stepImgId,
_stepImg,
function cloudApiExportImage(ctx, next) {
self.cloudapi.exportImage({
id: ctx.imgId, manta_path: opts.manta_path },
id: ctx.img.id, manta_path: opts.manta_path },
function (err, exportInfo_, res_) {
if (err) {
next(err);
@ -769,6 +769,101 @@ TritonApi.prototype.exportImage = function exportImage(opts, cb)
});
};
/**
* Share an image with another account.
*
* @param {Object} opts
* - {String} image The image UUID, name, or short ID. Required.
* - {String} account The account UUID. Required.
* @param {Function} cb `function (err, img)`
* On failure `err` is an error instance, else it is null.
* On success: `img` is an image object.
*/
TritonApi.prototype.shareImage = function shareImage(opts, cb)
{
var self = this;
assert.object(opts, 'opts');
assert.string(opts.image, 'opts.image');
assert.string(opts.account, 'opts.account');
assert.func(cb, 'cb');
var arg = {
image: opts.image,
client: self
};
var res;
vasync.pipeline({arg: arg, funcs: [
_stepImg,
function validateAcl(ctx, next) {
ctx.acl = ctx.img.acl && ctx.img.acl.slice() || [];
if (ctx.acl.indexOf(opts.account) === -1) {
ctx.acl.push(opts.account);
}
next();
},
function cloudApiShareImage(ctx, next) {
self.cloudapi.updateImage({id: ctx.img.id, fields: {acl: ctx.acl}},
function _updateImageCb(err, img) {
res = img;
next(err);
});
}
]}, function (err) {
cb(err, res);
});
};
/**
* Unshare an image with another account.
*
* @param {Object} opts
* - {String} image The image UUID, name, or short ID. Required.
* - {String} account The account UUID. Required.
* @param {Function} cb `function (err, img)`
* On failure `err` is an error instance, else it is null.
* On success: `img` is an image object.
*/
TritonApi.prototype.unshareImage = function unshareImage(opts, cb)
{
var self = this;
assert.object(opts, 'opts');
assert.string(opts.image, 'opts.image');
assert.string(opts.account, 'opts.account');
assert.func(cb, 'cb');
var arg = {
image: opts.image,
client: self
};
var res;
vasync.pipeline({arg: arg, funcs: [
_stepImg,
function validateAcl(ctx, next) {
assert.object(ctx.img, 'img');
ctx.acl = ctx.img.acl && ctx.img.acl.slice() || [];
var aclIdx = ctx.acl.indexOf(opts.account);
if (aclIdx === -1) {
cb(new errors.TritonError(format('image is not shared with %s',
opts.account)));
return;
}
ctx.acl.splice(aclIdx, 1);
next();
},
function cloudApiUnshareImage(ctx, next) {
self.cloudapi.updateImage({id: ctx.img.id, fields: {acl: ctx.acl}},
function _updateImageCb(err, img) {
res = img;
next(err);
});
}
]}, function (err) {
cb(err, res);
});
};
/**
* 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": "5.6.1",
"version": "5.7.0",
"author": "Joyent (joyent.com)",
"homepage": "https://github.com/joyent/node-triton",
"dependencies": {
@ -33,7 +33,8 @@
},
"devDependencies": {
"tape": "4.2.0",
"tap-summary": "3.0.2"
"tap-summary": "3.0.2",
"uuid": "3.2.1"
},
"main": "./lib",
"scripts": {

View File

@ -5,7 +5,7 @@
*/
/*
* Copyright 2016, Joyent, Inc.
* Copyright (c) 2018, Joyent, Inc.
*/
/*
@ -15,6 +15,7 @@
var format = require('util').format;
var os = require('os');
var test = require('tape');
var uuid = require('uuid');
var vasync = require('vasync');
var common = require('../../lib/common');
@ -199,6 +200,62 @@ test('triton image ...', testOpts, function (tt) {
}
});
tt.test(' triton image share ...', function (t) {
var dummyUuid = uuid.v4();
var argv = ['image', 'share', img.id, dummyUuid];
h.safeTriton(t, argv, function (err) {
if (err) {
t.end();
return;
}
argv = ['image', 'get', '-j', img.id];
h.safeTriton(t, argv, function (err2, stdout2) {
t.ifErr(err2, 'image get response');
if (err2) {
t.end();
return;
}
var result = JSON.parse(stdout2);
t.ok(result, 'image share result');
t.ok(result.acl, 'image share result.acl');
if (result.acl && Array.isArray(result.acl)) {
t.notEqual(result.acl.indexOf(dummyUuid), -1,
'image share result.acl contains uuid');
} else {
t.fail('image share result does not contain acl array');
}
unshareImage();
});
});
function unshareImage() {
argv = ['image', 'unshare', img.id, dummyUuid];
h.safeTriton(t, argv, function (err) {
if (err) {
t.end();
return;
}
argv = ['image', 'get', '-j', img.id];
h.safeTriton(t, argv, function (err2, stdout2) {
t.ifErr(err2, 'image get response');
if (err2) {
t.end();
return;
}
var result = JSON.parse(stdout2);
t.ok(result, 'image unshare result');
if (result.acl && Array.isArray(result.acl)) {
t.equal(result.acl.indexOf(dummyUuid), -1,
'image unshare result.acl should not contain uuid');
} else {
t.equal(result.acl, undefined, 'image has no acl');
}
t.end();
});
});
}
});
// TODO: Once have `triton ssh ...` working in test suite without hangs,
// then want to check that the created VM has the markerFile.