joyent/node-triton#88: `triton inst ...` support for updating tags

This commit is contained in:
Trent Mick 2016-02-09 12:23:55 -08:00
parent 5b6980e490
commit 4760defd05
22 changed files with 2002 additions and 147 deletions

View File

@ -264,7 +264,7 @@ CloudApi.prototype._passThrough = function _passThrough(endpoint, opts, cb) {
assert.func(cb, 'cb');
var p = this._path(endpoint, opts);
this._request(p, function (err, req, res, body) {
this._request({path: p}, function (err, req, res, body) {
/*
* Improve this kind of error message:
*
@ -917,6 +917,144 @@ CloudApi.prototype.machineAudit = function machineAudit(id, cb) {
};
// --- machine tags
/**
* <http://apidocs.joyent.com/cloudapi/#ListMachineTags>
*
* @param {Object} opts:
* - @param {UUID} id: The machine UUID.
* @param {Function} cb - `function (err, tags, res)`
*/
CloudApi.prototype.listMachineTags = function listMachineTags(opts, cb) {
assert.object(opts, 'opts');
assert.uuid(opts.id, 'opts.id');
assert.func(cb, 'cb');
var endpoint = format('/%s/machines/%s/tags', this.account, opts.id);
this._passThrough(endpoint, {}, cb);
};
/**
* <http://apidocs.joyent.com/cloudapi/#GetMachineTag>
*
* @param {Object} opts:
* - @param {UUID} id: The machine UUID. Required.
* - @param {UUID} tag: The tag name. Required.
* @param {Function} cb - `function (err, value, res)`
* On success, `value` is the tag value *as a string*. See note above.
*/
CloudApi.prototype.getMachineTag = function getMachineTag(opts, cb) {
assert.object(opts, 'opts');
assert.uuid(opts.id, 'opts.id');
assert.string(opts.tag, 'opts.tag');
assert.func(cb, 'cb');
this._request({
path: format('/%s/machines/%s/tags/%s', this.account, opts.id,
encodeURIComponent(opts.tag))
}, function (err, req, res, body) {
cb(err, body, res);
});
};
/**
* <http://apidocs.joyent.com/cloudapi/#AddMachineTags>
*
* @param {Object} opts:
* - @param {UUID} id: The machine UUID. Required.
* - @param {Object} tags: The tag name/value pairs.
* @param {Function} cb - `function (err, tags, res)`
* On success, `tags` is the updated set of instance tags.
*/
CloudApi.prototype.addMachineTags = function addMachineTags(opts, cb) {
assert.object(opts, 'opts');
assert.uuid(opts.id, 'opts.id');
assert.object(opts.tags, 'opts.tags');
assert.func(cb, 'cb');
// TODO: should this strictly guard on opts.tags types?
this._request({
method: 'POST',
path: format('/%s/machines/%s/tags', this.account, opts.id),
data: opts.tags
}, function (err, req, res, body) {
cb(err, body, res);
});
};
/**
* <http://apidocs.joyent.com/cloudapi/#ReplaceMachineTags>
*
* @param {Object} opts:
* - @param {UUID} id: The machine UUID. Required.
* - @param {Object} tags: The tag name/value pairs.
* @param {Function} cb - `function (err, tags, res)`
* On success, `tags` is the updated set of instance tags.
*/
CloudApi.prototype.replaceMachineTags = function replaceMachineTags(opts, cb) {
assert.object(opts, 'opts');
assert.uuid(opts.id, 'opts.id');
assert.object(opts.tags, 'opts.tags');
assert.func(cb, 'cb');
// TODO: should this strictly guard on opts.tags types?
this._request({
method: 'PUT',
path: format('/%s/machines/%s/tags', this.account, opts.id),
data: opts.tags
}, function (err, req, res, body) {
cb(err, body, res);
});
};
/**
* <http://apidocs.joyent.com/cloudapi/#DeleteMachineTag>
*
* @param {Object} opts:
* - @param {UUID} id: The machine UUID. Required.
* - @param {String} tag: The tag name. Required.
* @param {Function} cb - `function (err, res)`
*/
CloudApi.prototype.deleteMachineTag = function deleteMachineTag(opts, cb) {
assert.object(opts, 'opts');
assert.uuid(opts.id, 'opts.id');
assert.string(opts.tag, 'opts.tag');
assert.ok(opts.tag, 'opts.tag cannot be empty');
assert.func(cb, 'cb');
this._request({
method: 'DELETE',
path: format('/%s/machines/%s/tags/%s', this.account, opts.id,
encodeURIComponent(opts.tag))
}, function (err, req, res) {
cb(err, res);
});
};
/**
* <http://apidocs.joyent.com/cloudapi/#DeleteMachineTags>
*
* @param {Object} opts:
* - @param {UUID} id: The machine UUID. Required.
* @param {Function} cb - `function (err, res)`
*/
CloudApi.prototype.deleteMachineTags = function deleteMachineTags(opts, cb) {
assert.object(opts, 'opts');
assert.uuid(opts.id, 'opts.id');
assert.func(cb, 'cb');
this._request({
method: 'DELETE',
path: format('/%s/machines/%s/tags', this.account, opts.id)
}, function (err, req, res) {
cb(err, res);
});
};
// --- rbac
/**

View File

@ -49,7 +49,7 @@ function do_create(subcmd, opts, args, cb) {
});
},
function loadTags(ctx, next) {
mat.tagsFromOpts(opts, log, function (err, tags) {
mat.tagsFromCreateOpts(opts, log, function (err, tags) {
if (err) {
next(err);
return;

View File

@ -0,0 +1,112 @@
/*
* 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 instance tag delete ...`
*/
var vasync = require('vasync');
var errors = require('../../errors');
function do_delete(subcmd, opts, args, cb) {
var self = this;
if (opts.help) {
this.do_help('help', {}, [subcmd], cb);
return;
} else if (args.length < 1) {
cb(new errors.UsageError('incorrect number of args'));
return;
} else if (args.length > 1 && opts.all) {
cb(new errors.UsageError('cannot specify both tag names and --all'));
return;
}
var waitTimeoutMs = opts.wait_timeout * 1000; /* seconds to ms */
if (opts.all) {
self.top.tritonapi.deleteAllInstanceTags({
id: args[0],
wait: opts.wait,
waitTimeout: waitTimeoutMs
}, function (err) {
console.log('Deleted all tags on instance %s', args[0]);
cb(err);
});
} else {
// Uniq'ify the given names.
var names = {};
args.slice(1).forEach(function (arg) { names[arg] = true; });
names = Object.keys(names);
// TODO: Instead of waiting for each delete, let's delete them all then
// wait for the set.
vasync.forEachPipeline({
inputs: names,
func: function deleteOne(name, next) {
self.top.tritonapi.deleteInstanceTag({
id: args[0],
tag: name,
wait: opts.wait,
waitTimeout: waitTimeoutMs
}, function (err) {
if (!err) {
console.log('Deleted tag %s on instance %s',
name, args[0]);
}
next(err);
});
}
}, cb);
}
}
do_delete.options = [
{
names: ['help', 'h'],
type: 'bool',
help: 'Show this help.'
},
{
names: ['all', 'a'],
type: 'bool',
help: 'Remove all tags on this instance.'
},
{
names: ['wait', 'w'],
type: 'bool',
help: 'Wait for the tag changes to be applied.'
},
{
names: ['wait-timeout'],
type: 'positiveInteger',
default: 120,
help: 'The number of seconds to wait before timing out with an error. '
+ 'The default is 120 seconds.'
}
];
do_delete.help = [
/* BEGIN JSSTYLED */
'Delete one or more instance tags.',
'',
'Usage:',
' {{name}} delete <inst> [<name> ...]',
' {{name}} delete --all <inst> # delete all tags',
'',
'{{options}}',
'Where <inst> is an instance id, name, or shortid and <name> is a tag name.',
'',
'Changing instance tags is asynchronous. Use "--wait" to not return until',
'the changes are completed.'
/* END JSSTYLED */
].join('\n');
do_delete.aliases = ['rm'];
module.exports = do_delete;

View File

@ -0,0 +1,68 @@
/*
* 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 instance tag get ...`
*/
var errors = require('../../errors');
function do_get(subcmd, opts, args, cb) {
var self = this;
if (opts.help) {
this.do_help('help', {}, [subcmd], cb);
return;
} else if (args.length !== 2) {
cb(new errors.UsageError('incorrect number of args'));
return;
}
self.top.tritonapi.getInstanceTag({
id: args[0],
tag: args[1]
}, function (err, value) {
if (err) {
cb(err);
return;
}
if (opts.json) {
console.log(JSON.stringify(value));
} else {
console.log(value);
}
cb();
});
}
do_get.options = [
{
names: ['help', 'h'],
type: 'bool',
help: 'Show this help.'
},
{
names: ['json', 'j'],
type: 'bool',
help: 'JSON output.'
}
];
do_get.help = [
/* BEGIN JSSTYLED */
'Get an instance tag.',
'',
'Usage:',
' {{name}} get <inst> <name>',
'',
'{{options}}',
'Where <inst> is an instance id, name, or shortid and <name> is a tag name.'
/* END JSSTYLED */
].join('\n');
module.exports = do_get;

View File

@ -0,0 +1,69 @@
/*
* 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 instance tag list ...`
*/
var errors = require('../../errors');
function do_list(subcmd, opts, args, cb) {
var self = this;
if (opts.help) {
this.do_help('help', {}, [subcmd], cb);
return;
} else if (args.length !== 1) {
cb(new errors.UsageError('incorrect number of args'));
return;
}
self.top.tritonapi.listInstanceTags({id: args[0]}, function (err, tags) {
if (err) {
cb(err);
return;
}
if (opts.json) {
console.log(JSON.stringify(tags));
} else {
console.log(JSON.stringify(tags, null, 4));
}
cb();
});
}
do_list.options = [
{
names: ['help', 'h'],
type: 'bool',
help: 'Show this help.'
},
{
names: ['json', 'j'],
type: 'bool',
help: 'JSON output.'
}
];
do_list.help = [
/* BEGIN JSSTYLED */
'List instance tags.',
'',
'Usage:',
' {{name}} list <inst>',
'',
'{{options}}',
'Where <inst> is an instance id, name, or shortid.',
'',
'Note: Currently this dumps prettified JSON by default. That might change',
'in the future. Use "-j" to explicitly get JSON output.'
/* END JSSTYLED */
].join('\n');
do_list.aliases = ['ls'];
module.exports = do_list;

View File

@ -0,0 +1,132 @@
/*
* 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 instance tag replace-all ...`
*/
var vasync = require('vasync');
var errors = require('../../errors');
var mat = require('../../metadataandtags');
function do_replace_all(subcmd, opts, args, cb) {
var self = this;
if (opts.help) {
this.do_help('help', {}, [subcmd], cb);
return;
} else if (args.length < 1) {
cb(new errors.UsageError('incorrect number of args'));
return;
}
var log = self.log;
vasync.pipeline({arg: {}, funcs: [
function gatherTags(ctx, next) {
mat.tagsFromSetArgs(opts, args.slice(1), log, function (err, tags) {
if (err) {
next(err);
return;
}
log.trace({tags: tags || '<none>'},
'tags loaded from opts and args');
ctx.tags = tags;
next();
});
},
function replaceAway(ctx, next) {
if (!ctx.tags) {
next(new errors.UsageError('no tags were provided'));
return;
}
self.top.tritonapi.replaceAllInstanceTags({
id: args[0],
tags: ctx.tags,
wait: opts.wait,
waitTimeout: opts.wait_timeout * 1000 /* seconds to ms */
}, function (err, updatedTags) {
if (err) {
cb(err);
return;
}
if (!opts.quiet) {
if (opts.json) {
console.log(JSON.stringify(updatedTags));
} else {
console.log(JSON.stringify(updatedTags, null, 4));
}
}
cb();
});
}
]}, cb);
}
do_replace_all.options = [
{
names: ['help', 'h'],
type: 'bool',
help: 'Show this help.'
},
{
names: ['file', 'f'],
type: 'arrayOfString',
helpArg: 'FILE',
help: 'Load tag name/value pairs from the given file path. '
+ 'The file may contain a JSON object or a file with "NAME=VALUE" '
+ 'pairs, one per line. This option can be used multiple times.'
},
{
names: ['wait', 'w'],
type: 'bool',
help: 'Wait for the tag changes to be applied.'
},
{
names: ['wait-timeout'],
type: 'positiveInteger',
default: 120,
help: 'The number of seconds to wait before timing out with an error. '
+ 'The default is 120 seconds.'
},
{
names: ['json', 'j'],
type: 'bool',
help: 'JSON output.'
},
{
names: ['quiet', 'q'],
type: 'bool',
help: 'Quieter output. Specifically do not dump the updated set of '
+ 'tags on successful completion.'
}
];
do_replace_all.help = [
/* BEGIN JSSTYLED */
'Replace all tags on the given instance.',
'',
'Usage:',
' {{name}} replace-all <inst> [<name>=<value> ...]',
' {{name}} replace-all <inst> -f <file> # tags from file',
'',
'{{options}}',
'Where <inst> is an instance id, name, or shortid; <name> is a tag name;',
'and <value> is a tag value (bool and numeric "value" are converted to ',
'that type).',
'',
'Currently this dumps prettified JSON by default. That might change in the',
'future. Use "-j" to explicitly get JSON output.',
'',
'Changing instance tags is asynchronous. Use "--wait" to not return until',
'the changes are completed.'
/* END JSSTYLED */
].join('\n');
module.exports = do_replace_all;

View File

@ -0,0 +1,133 @@
/*
* 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 instance tag set ...`
*/
var vasync = require('vasync');
var errors = require('../../errors');
var mat = require('../../metadataandtags');
function do_set(subcmd, opts, args, cb) {
var self = this;
if (opts.help) {
this.do_help('help', {}, [subcmd], cb);
return;
} else if (args.length < 1) {
cb(new errors.UsageError('incorrect number of args'));
return;
}
var log = self.log;
vasync.pipeline({arg: {}, funcs: [
function gatherTags(ctx, next) {
mat.tagsFromSetArgs(opts, args.slice(1), log, function (err, tags) {
if (err) {
next(err);
return;
}
log.trace({tags: tags || '<none>'},
'tags loaded from opts and args');
ctx.tags = tags;
next();
});
},
function setMachineTags(ctx, next) {
if (!ctx.tags) {
log.trace('no tags to set');
next();
return;
}
self.top.tritonapi.setInstanceTags({
id: args[0],
tags: ctx.tags,
wait: opts.wait,
waitTimeout: opts.wait_timeout * 1000 /* seconds to ms */
}, function (err, updatedTags) {
if (err) {
cb(err);
return;
}
if (!opts.quiet) {
if (opts.json) {
console.log(JSON.stringify(updatedTags));
} else {
console.log(JSON.stringify(updatedTags, null, 4));
}
}
cb();
});
}
]}, cb);
}
do_set.options = [
{
names: ['help', 'h'],
type: 'bool',
help: 'Show this help.'
},
{
names: ['file', 'f'],
type: 'arrayOfString',
helpArg: 'FILE',
help: 'Load tag name/value pairs from the given file path. '
+ 'The file may contain a JSON object or a file with "NAME=VALUE" '
+ 'pairs, one per line. This option can be used multiple times.'
},
{
names: ['wait', 'w'],
type: 'bool',
help: 'Wait for the tag changes to be applied.'
},
{
names: ['wait-timeout'],
type: 'positiveInteger',
default: 120,
help: 'The number of seconds to wait before timing out with an error. '
+ 'The default is 120 seconds.'
},
{
names: ['json', 'j'],
type: 'bool',
help: 'JSON output.'
},
{
names: ['quiet', 'q'],
type: 'bool',
help: 'Quieter output. Specifically do not dump the updated set of '
+ 'tags on successful completion.'
}
];
do_set.help = [
/* BEGIN JSSTYLED */
'Set one or more instance tags.',
'',
'Usage:',
' {{name}} set <inst> [<name>=<value> ...]',
' {{name}} set <inst> -f <file> # tags from file',
'',
'{{options}}',
'Where <inst> is an instance id, name, or shortid; <name> is a tag name;',
'and <value> is a tag value (bool and numeric "value" are converted to ',
'that type).',
'',
'Currently this dumps prettified JSON by default. That might change in the',
'future. Use "-j" to explicitly get JSON output.',
'',
'Changing instance tags is asynchronous. Use "--wait" to not return until',
'the changes are completed.'
/* END JSSTYLED */
].join('\n');
module.exports = do_set;

View File

@ -0,0 +1,54 @@
/*
* 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 instance tag ...`
*/
var Cmdln = require('cmdln').Cmdln;
var util = require('util');
// ---- CLI class
function InstanceTagCLI(parent) {
this.top = parent.top;
Cmdln.call(this, {
name: parent.name + ' tag',
/* BEGIN JSSTYLED */
desc: [
'List, get, set and delete tags on Triton instances.'
].join('\n'),
/* END JSSTYLED */
helpOpts: {
minHelpCol: 24 /* line up with option help */
},
helpSubcmds: [
'help',
'list',
'get',
'set',
'replace-all',
'delete'
]
});
}
util.inherits(InstanceTagCLI, Cmdln);
InstanceTagCLI.prototype.init = function init(opts, args, cb) {
this.log = this.top.log;
Cmdln.prototype.init.apply(this, arguments);
};
InstanceTagCLI.prototype.do_list = require('./do_list');
InstanceTagCLI.prototype.do_get = require('./do_get');
InstanceTagCLI.prototype.do_set = require('./do_set');
InstanceTagCLI.prototype.do_replace_all = require('./do_replace_all');
InstanceTagCLI.prototype.do_delete = require('./do_delete');
module.exports = InstanceTagCLI;

View File

@ -0,0 +1,25 @@
/*
* 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 instance tags ...` shortcut for `triton instance tag list ...`.
*/
function do_tags(subcmd, opts, args, callback) {
this.handlerFromSubcmd('tag').dispatch({
subcmd: 'list',
opts: opts,
args: args
}, callback);
}
do_tags.help = 'A shortcut for "triton instance tag list".';
do_tags.options = require('./do_tag/do_list').options;
do_tags.hidden = true;
module.exports = do_tags;

View File

@ -41,7 +41,8 @@ function InstanceCLI(top) {
{ group: '' },
'ssh',
'wait',
'audit'
'audit',
'tag'
]
});
}
@ -64,6 +65,8 @@ InstanceCLI.prototype.do_reboot = require('./do_reboot');
InstanceCLI.prototype.do_ssh = require('./do_ssh');
InstanceCLI.prototype.do_wait = require('./do_wait');
InstanceCLI.prototype.do_audit = require('./do_audit');
InstanceCLI.prototype.do_tag = require('./do_tag');
InstanceCLI.prototype.do_tags = require('./do_tags');
InstanceCLI.aliases = ['inst'];

View File

@ -182,6 +182,7 @@ util.inherits(SigningError, _TritonBaseVError);
* A 'DEPTH_ZERO_SELF_SIGNED_CERT' An error signing a request.
*/
function SelfSignedCertError(cause, url) {
assert.string(url, 'url');
var msg = format('could not access CloudAPI %s because it uses a ' +
'self-signed TLS certificate and your current profile is not ' +
'configured for insecure access', url);
@ -195,6 +196,25 @@ function SelfSignedCertError(cause, url) {
util.inherits(SelfSignedCertError, _TritonBaseVError);
/**
* A timeout was reached waiting/polling for something.
*/
function TimeoutError(cause, msg) {
if (msg === undefined) {
msg = cause;
cause = undefined;
}
assert.string(msg, 'msg');
_TritonBaseVError.call(this, {
cause: cause,
message: msg,
code: 'Timeout',
exitStatus: 1
});
}
util.inherits(TimeoutError, _TritonBaseVError);
/**
* A resource (instance, image, ...) was not found.
*/
@ -244,6 +264,7 @@ module.exports = {
UsageError: UsageError,
SigningError: SigningError,
SelfSignedCertError: SelfSignedCertError,
TimeoutError: TimeoutError,
ResourceNotFoundError: ResourceNotFoundError,
MultiError: MultiError
};

View File

@ -84,7 +84,7 @@ function metadataFromOpts(opts, log, cb) {
* <https://github.com/joyent/sdc-vmapi/blob/master/docs/index.md#vm-metadata>
* says values may be string, num or bool.
*/
function tagsFromOpts(opts, log, cb) {
function tagsFromCreateOpts(opts, log, cb) {
assert.arrayOfObject(opts._order, 'opts._order');
assert.object(log, 'log');
assert.func(cb, 'cb');
@ -123,6 +123,60 @@ function tagsFromOpts(opts, log, cb) {
}
/*
* Load and validate tags from (a) these options:
* -f,--file FILE
* and (b) these args:
* name=value ...
*
* Later ones win, so *args* will win over file-loaded tags.
*
* <https://github.com/joyent/sdc-vmapi/blob/master/docs/index.md#vm-metadata>
* says values may be string, num or bool.
*/
function tagsFromSetArgs(opts, args, log, cb) {
assert.arrayOfObject(opts._order, 'opts._order');
assert.arrayOfString(args, 'args');
assert.object(log, 'log');
assert.func(cb, 'cb');
var tags = {};
vasync.pipeline({funcs: [
function tagsFromOpts(_, next) {
vasync.forEachPipeline({
inputs: opts._order,
func: function tagsFromOpt(o, nextOpt) {
log.trace({opt: o}, 'tagsFromOpt');
if (o.key === 'file') {
_addMetadataFromFile('tag', tags, o.value, nextOpt);
} else {
nextOpt();
}
}
}, next);
},
function tagsFromArgs(_, next) {
vasync.forEachPipeline({
inputs: args,
func: function tagFromArg(a, nextArg) {
log.trace({arg: a}, 'tagFromArg');
_addMetadataFromKvStr('tag', tags, a, null, nextArg);
}
}, next);
}
]}, function (err) {
if (err) {
cb(err);
} else if (Object.keys(tags).length) {
cb(null, tags);
} else {
cb();
}
});
}
var allowedTypes = ['string', 'number', 'boolean'];
function _addMetadatum(ilk, metadata, key, value, from, cb) {
assert.string(ilk, 'ilk');
@ -221,6 +275,10 @@ function _addMetadataFromFile(ilk, metadata, file, cb) {
function _addMetadataFromKvStr(ilk, metadata, s, from, cb) {
assert.string(ilk, 'ilk');
assert.object(metadata, 'metadata');
assert.string(s, 's');
assert.optionalString(from, 'from');
assert.func(cb, 'cb');
var parts = strsplit(s, '=', 2);
if (parts.length !== 2) {
@ -285,5 +343,6 @@ function _addMetadatumFromFile(ilk, metadata, key, file, from, cb) {
module.exports = {
metadataFromOpts: metadataFromOpts,
tagsFromOpts: tagsFromOpts
tagsFromCreateOpts: tagsFromCreateOpts,
tagsFromSetArgs: tagsFromSetArgs
};

View File

@ -59,6 +59,32 @@ function _roleTagResourceUrl(account, type, id) {
return format('/%s/%s/%s', account, ns, id);
}
/**
* A function appropriate for `vasync.pipeline` funcs that takes a `arg.id`
* instance name, shortid or uuid, and determines the instance id (setting it
* as `arg.instId`).
*/
function _stepInstId(arg, next) {
assert.object(arg.client, 'arg.client');
assert.string(arg.id, 'arg.id');
if (common.isUUID(arg.id)) {
arg.instId = arg.id;
next();
} else {
arg.client.getInstance({
id: arg.id,
fields: ['id']
}, function (err, inst) {
if (err) {
next(err);
} else {
arg.instId = inst.id;
next();
}
});
}
}
//---- TritonApi class
@ -538,16 +564,28 @@ TritonApi.prototype.getNetwork = function getNetwork(name, cb) {
/**
* Get an instance by ID, exact name, or short ID, in that order.
* Get an instance.
*
* @param {String} name
* Alternative call signature: `getInstance(id, callback)`.
*
* @param {Object} opts
* - {UUID} id: The instance ID, name, or short ID. Required.
* - {Array} fields: Optional. An array of instance field names that are
* wanted by the caller. This *can* allow the implementation to avoid
* extra API calls. E.g. `['id', 'name']`.
* @param {Function} callback `function (err, inst, res)`
* Where, on success, `res` is the response object from a `GetMachine` call
* if one was made.
* On success, `res` is the response object from a `GetMachine`, if one
* was made (possibly not if the instance was retrieved from `ListMachines`
* calls).
*/
TritonApi.prototype.getInstance = function getInstance(name, cb) {
TritonApi.prototype.getInstance = function getInstance(opts, cb) {
var self = this;
assert.string(name, 'name');
if (typeof (opts) === 'string') {
opts = {id: opts};
}
assert.object(opts, 'opts');
assert.string(opts.id, 'opts.id');
assert.optionalArrayOfString(opts.fields, 'opts.fields');
assert.func(cb, 'cb');
var res;
@ -558,10 +596,10 @@ TritonApi.prototype.getInstance = function getInstance(name, cb) {
vasync.pipeline({funcs: [
function tryUuid(_, next) {
var uuid;
if (common.isUUID(name)) {
uuid = name;
if (common.isUUID(opts.id)) {
uuid = opts.id;
} else {
shortId = common.normShortId(name);
shortId = common.normShortId(opts.id);
if (shortId && common.isUUID(shortId)) {
// E.g. a >32-char docker container ID normalized to a UUID.
uuid = shortId;
@ -575,7 +613,7 @@ TritonApi.prototype.getInstance = function getInstance(name, cb) {
if (err && err.restCode === 'ResourceNotFound') {
// The CloudApi 404 error message sucks: "VM not found".
err = new errors.ResourceNotFoundError(err,
format('instance with id %s was not found', name));
format('instance with id %s was not found', opts.id));
}
next(err);
});
@ -586,12 +624,12 @@ TritonApi.prototype.getInstance = function getInstance(name, cb) {
return next();
}
self.cloudapi.listMachines({name: name}, function (err, insts) {
self.cloudapi.listMachines({name: opts.id}, function (err, insts) {
if (err) {
return next(err);
}
for (var i = 0; i < insts.length; i++) {
if (insts[i].name === name) {
if (insts[i].name === opts.id) {
instFromList = insts[i];
// Relying on rule that instance name is unique
// for a user and DC.
@ -645,7 +683,22 @@ TritonApi.prototype.getInstance = function getInstance(name, cb) {
if (inst || !instFromList) {
next();
return;
} else if (opts.fields) {
// If already have all the requested fields, no need to re-get.
var missingAField = false;
for (var i = 0; i < opts.fields.length; i++) {
if (! instFromList.hasOwnProperty(opts.fields[i])) {
missingAField = true;
break;
}
}
if (!missingAField) {
inst = instFromList;
next();
return;
}
}
var uuid = instFromList.id;
self.cloudapi.getMachine(uuid, function (err, inst_, res_) {
res = res_;
@ -653,7 +706,7 @@ TritonApi.prototype.getInstance = function getInstance(name, cb) {
if (err && err.restCode === 'ResourceNotFound') {
// The CloudApi 404 error message sucks: "VM not found".
err = new errors.ResourceNotFoundError(err,
format('instance with id %s was not found', name));
format('instance with id %s was not found', opts.id));
}
next(err);
});
@ -665,12 +718,528 @@ TritonApi.prototype.getInstance = function getInstance(name, cb) {
cb(null, inst, res);
} else {
cb(new errors.ResourceNotFoundError(format(
'no instance with name or short id "%s" was found', name)));
'no instance with name or short id "%s" was found', opts.id)));
}
});
};
// ---- instance tags
/**
* List an instance's tags.
* <http://apidocs.joyent.com/cloudapi/#ListMachineTags>
*
* Alternative call signature: `listInstanceTags(id, callback)`.
*
* @param {Object} opts
* - {UUID} id: The instance ID, name, or short ID. Required.
* @param {Function} cb: `function (err, tags, res)`
* On success, `res` is *possibly* the response object from either a
* `ListMachineTags` or a `GetMachine` call.
*/
TritonApi.prototype.listInstanceTags = function listInstanceTags(opts, cb) {
var self = this;
if (typeof (opts) === 'string') {
opts = {id: opts};
}
assert.object(opts, 'opts');
assert.string(opts.id, 'opts.id');
assert.func(cb, 'cb');
if (common.isUUID(opts.id)) {
self.cloudapi.listMachineTags(opts, cb);
return;
}
self.getInstance({
id: opts.id,
fields: ['id', 'tags']
}, function (err, inst, res) {
if (err) {
cb(err);
return;
}
// No need to call `ListMachineTags` now.
cb(null, inst.tags, res);
});
};
/**
* Get an instance tag value.
* <http://apidocs.joyent.com/cloudapi/#GetMachineTag>
*
* @param {Object} opts
* - {UUID} id: The instance ID, name, or short ID. Required.
* - {String} tag: The tag name. Required.
* @param {Function} cb: `function (err, value, res)`
* On success, `value` is the tag value *as a string*. See note above.
* On success, `res` is *possibly* the response object from either a
* `GetMachineTag` or a `GetMachine` call.
*/
TritonApi.prototype.getInstanceTag = function getInstanceTag(opts, cb) {
var self = this;
assert.object(opts, 'opts');
assert.string(opts.id, 'opts.id');
assert.string(opts.tag, 'opts.tag');
assert.func(cb, 'cb');
if (common.isUUID(opts.id)) {
self.cloudapi.getMachineTag(opts, cb);
return;
}
self.getInstance({
id: opts.id,
fields: ['id', 'tags']
}, function (err, inst, res) {
if (err) {
cb(err);
return;
}
// No need to call `GetMachineTag` now.
if (inst.tags.hasOwnProperty(opts.tag)) {
var value = inst.tags[opts.tag];
cb(null, value, res);
} else {
cb(new errors.ResourceNotFoundError(format(
'tag with name "%s" was not found', opts.tag)));
}
});
};
/**
* Shared implementation for any methods to change instance tags.
*
* @param {Object} opts
* - {String} id: The instance ID, name, or short ID. Required.
* - {Object} change: Required. Describes the tag change to make. It
* has an "action" field and, depending on the particular action, a
* "tags" field.
* - {Boolean} wait: Wait (via polling) until the tag update is complete.
* Warning: A concurrent tag update to the same tags can result in this
* polling being unable to notice the change. Use `waitTimeout` to
* put an upper bound.
* - {Number} waitTimeout: The number of milliseconds after which to
* timeout (call `cb` with a timeout error) waiting. Only relevant if
* `opts.wait === true`. Default is Infinity (i.e. it doesn't timeout).
* @param {Function} cb: `function (err, tags, res)`
* On success, `tags` is the updated set of instance tags and `res` is
* the response object from the underlying CloudAPI call. Note that `tags`
* is not set (undefined) for the "delete" and "deleteAll" actions.
*/
TritonApi.prototype._changeInstanceTags =
function _changeInstanceTags(opts, cb) {
var self = this;
assert.object(opts, 'opts');
assert.string(opts.id, 'opts.id');
assert.object(opts.change, 'opts.change');
var KNOWN_CHANGE_ACTIONS = ['set', 'replace', 'delete', 'deleteAll'];
assert.ok(KNOWN_CHANGE_ACTIONS.indexOf(opts.change.action) != -1,
'invalid change action: ' + opts.change.action);
switch (opts.change.action) {
case 'set':
case 'replace':
assert.object(opts.change.tags,
'opts.change.tags for action=' + opts.change.action);
break;
case 'delete':
assert.string(opts.change.tagName,
'opts.change.tagName for action=delete');
break;
case 'deleteAll':
break;
default:
throw new Error('unexpected action: ' + opts.change.action);
}
assert.optionalBool(opts.wait, 'opts.wait');
assert.optionalNumber(opts.waitTimeout, 'opts.waitTimeout');
assert.func(cb, 'cb');
var theRes;
var updatedTags;
vasync.pipeline({arg: {client: self, id: opts.id}, funcs: [
_stepInstId,
function changeTheTags(arg, next) {
switch (opts.change.action) {
case 'set':
self.cloudapi.addMachineTags({
id: arg.instId,
tags: opts.change.tags
}, function (err, tags, res) {
updatedTags = tags;
theRes = res;
next(err);
});
break;
case 'replace':
self.cloudapi.replaceMachineTags({
id: arg.instId,
tags: opts.change.tags
}, function (err, tags, res) {
updatedTags = tags;
theRes = res;
next(err);
});
break;
case 'delete':
self.cloudapi.deleteMachineTag({
id: arg.instId,
tag: opts.change.tagName
}, function (err, res) {
theRes = res;
next(err);
});
break;
case 'deleteAll':
self.cloudapi.deleteMachineTags({
id: arg.instId
}, function (err, res) {
theRes = res;
next(err);
});
break;
default:
throw new Error('unexpected action: ' + opts.change.action);
}
},
function waitForChanges(arg, next) {
if (!opts.wait) {
next();
return;
}
self.waitForInstanceTagChanges({
id: arg.instId,
timeout: opts.waitTimeout,
change: opts.change
}, next);
}
]}, function (err) {
if (err) {
cb(err);
} else {
cb(null, updatedTags, theRes);
}
});
};
/**
* Wait (via polling) for the given tag changes to have taken on the instance.
*
* Dev Note: This polls `ListMachineTags` until it looks like the given changes
* have been applied. This is unreliable with concurrent tag updates. A
* workaround for that is `opts.timeout`. A better long term solution would
* be for cloudapi to expose some handle on the underlying Triton workflow
* jobs performing these, and poll/wait on those.
*
* @param {Object} opts: Required.
* - {UUID} id: Required. The instance id.
* Limitation: Currently requiring this to be the full instance UUID.
* - {Number} timeout: Optional. A number of milliseconds after which to
* timeout (callback with `TimeoutError`) the wait. By default this is
* Infinity.
* - {Object} changes: Required. It always has an 'action' field (one of
* 'set', 'replace', 'delete', 'deleteAll') and, depending on the
* action, a 'tags' (set, replace), 'tagName' (delete) or 'tagNames'
* (delete).
* @param {Function} cb: `function (err, updatedTags)`
* On failure, `err` can be an error from `ListMachineTags` or
* `TimeoutError`.
*/
TritonApi.prototype.waitForInstanceTagChanges =
function waitForInstanceTagChanges(opts, cb) {
var self = this;
assert.object(opts, 'opts');
assert.uuid(opts.id, 'opts.id');
assert.optionalNumber(opts.timeout, 'opts.timeout');
var timeout = opts.hasOwnProperty('timeout') ? opts.timeout : Infinity;
assert.ok(timeout > 0, 'opts.timeout must be greater than zero');
assert.object(opts.change, 'opts.change');
var KNOWN_CHANGE_ACTIONS = ['set', 'replace', 'delete', 'deleteAll'];
assert.ok(KNOWN_CHANGE_ACTIONS.indexOf(opts.change.action) != -1,
'invalid change action: ' + opts.change.action);
assert.func(cb, 'cb');
var tagNames;
switch (opts.change.action) {
case 'set':
case 'replace':
assert.object(opts.change.tags, 'opts.change.tags');
break;
case 'delete':
if (opts.change.tagNames) {
assert.arrayOfString(opts.change.tagNames, 'opts.change.tagNames');
tagNames = opts.change.tagNames;
} else {
assert.string(opts.change.tagName, 'opts.change.tagName');
tagNames = [opts.change.tagName];
}
break;
case 'deleteAll':
break;
default:
throw new Error('unexpected action: ' + opts.change.action);
}
/*
* Hardcoded 2s poll interval for now. Not yet configurable, being mindful
* of avoiding lots of clients naively swamping a CloudAPI and hitting
* throttling.
* TODO: General client support for dealing with polling and throttling.
*/
var POLL_INTERVAL = 2 * 1000;
var startTime = Date.now();
var poll = function () {
self.log.trace({id: opts.id}, 'waitForInstanceTagChanges: poll inst');
self.cloudapi.listMachineTags({id: opts.id}, function (err, tags) {
if (err) {
cb(err);
return;
}
// Determine in changes are not yet applied (incomplete).
var incomplete = false;
var i, k, keys;
switch (opts.change.action) {
case 'set':
keys = Object.keys(opts.change.tags);
for (i = 0; i < keys.length; i++) {
k = keys[i];
if (tags[k] !== opts.change.tags[k]) {
self.log.trace({tag: k},
'waitForInstanceTagChanges incomplete set: '
+ 'unexpected value for tag');
incomplete = true;
break;
}
}
break;
case 'replace':
keys = Object.keys(opts.change.tags);
var tagsCopy = common.objCopy(tags);
for (i = 0; i < keys.length; i++) {
k = keys[i];
if (tagsCopy[k] !== opts.change.tags[k]) {
self.log.trace({tag: k},
'waitForInstanceTagChanges incomplete replace: '
+ 'unexpected value for tag');
incomplete = true;
break;
}
delete tagsCopy[k];
}
var extraneousTags = Object.keys(tagsCopy);
if (extraneousTags.length > 0) {
self.log.trace({extraneousTags: extraneousTags},
'waitForInstanceTagChanges incomplete replace: '
+ 'extraneous tags');
incomplete = true;
}
break;
case 'delete':
for (i = 0; i < tagNames.length; i++) {
k = tagNames[i];
if (tags.hasOwnProperty(k)) {
self.log.trace({tag: k},
'waitForInstanceTagChanges incomplete delete: '
+ 'extraneous tag');
incomplete = true;
break;
}
}
break;
case 'deleteAll':
if (Object.keys(tags).length > 0) {
self.log.trace({tag: k},
'waitForInstanceTagChanges incomplete deleteAll: '
+ 'still have tags');
incomplete = true;
}
break;
default:
throw new Error('unexpected action: ' + opts.change.action);
}
if (!incomplete) {
self.log.trace('waitForInstanceTagChanges: complete');
cb(null, tags);
} else {
var elapsedTime = Date.now() - startTime;
if (elapsedTime > timeout) {
cb(new errors.TimeoutError(format('timeout waiting for '
+ 'tag changes on instance %s (elapsed %ds)',
opts.id, Math.round(elapsedTime * 1000))));
} else {
setTimeout(poll, POLL_INTERVAL);
}
}
});
};
setImmediate(poll);
};
/**
* Set instance tags.
* <http://apidocs.joyent.com/cloudapi/#AddMachineTags>
*
* @param {Object} opts
* - {String} id: The instance ID, name, or short ID. Required.
* - {Object} tags: The tag name/value pairs. Required.
* - {Boolean} wait: Wait (via polling) until the tag update is complete.
* Warning: A concurrent tag update to the same tags can result in this
* polling being unable to notice the change. Use `waitTimeout` to
* put an upper bound.
* - {Number} waitTimeout: The number of milliseconds after which to
* timeout (call `cb` with a timeout error) waiting. Only relevant if
* `opts.wait === true`. Default is Infinity (i.e. it doesn't timeout).
* @param {Function} cb: `function (err, updatedTags, res)`
* On success, `updatedTags` is the updated set of instance tags and `res`
* is the response object from the `AddMachineTags` CloudAPI call.
*/
TritonApi.prototype.setInstanceTags = function setInstanceTags(opts, cb) {
assert.object(opts, 'opts');
assert.string(opts.id, 'opts.id');
assert.object(opts.tags, 'opts.tags');
assert.optionalBool(opts.wait, 'opts.wait');
assert.optionalNumber(opts.waitTimeout, 'opts.waitTimeout');
assert.func(cb, 'cb');
this._changeInstanceTags({
id: opts.id,
change: {
action: 'set',
tags: opts.tags
},
wait: opts.wait,
waitTimeout: opts.waitTimeout
}, cb);
};
/**
* Replace all instance tags.
* <http://apidocs.joyent.com/cloudapi/#ReplaceMachineTags>
*
* @param {Object} opts
* - {String} id: The instance ID, name, or short ID. Required.
* - {Object} tags: The tag name/value pairs. Required.
* - {Boolean} wait: Wait (via polling) until the tag update is complete.
* Warning: A concurrent tag update to the same tags can result in this
* polling being unable to notice the change. Use `waitTimeout` to
* put an upper bound.
* - {Number} waitTimeout: The number of milliseconds after which to
* timeout (call `cb` with a timeout error) waiting. Only relevant if
* `opts.wait === true`. Default is Infinity (i.e. it doesn't timeout).
* @param {Function} cb: `function (err, tags, res)`
* On success, `tags` is the updated set of instance tags and `res` is
* the response object from the `ReplaceMachineTags` CloudAPI call.
*/
TritonApi.prototype.replaceAllInstanceTags =
function replaceAllInstanceTags(opts, cb) {
assert.object(opts, 'opts');
assert.string(opts.id, 'opts.id');
assert.object(opts.tags, 'opts.tags');
assert.optionalBool(opts.wait, 'opts.wait');
assert.optionalNumber(opts.waitTimeout, 'opts.waitTimeout');
assert.func(cb, 'cb');
this._changeInstanceTags({
id: opts.id,
change: {
action: 'replace',
tags: opts.tags
},
wait: opts.wait,
waitTimeout: opts.waitTimeout
}, cb);
};
/**
* Delete the named instance tag.
* <http://apidocs.joyent.com/cloudapi/#DeleteMachineTag>
*
* @param {Object} opts
* - {String} id: The instance ID, name, or short ID. Required.
* - {String} tag: The tag name. Required.
* - {Boolean} wait: Wait (via polling) until the tag update is complete.
* Warning: A concurrent tag update to the same tags can result in this
* polling being unable to notice the change. Use `waitTimeout` to
* put an upper bound.
* - {Number} waitTimeout: The number of milliseconds after which to
* timeout (call `cb` with a timeout error) waiting. Only relevant if
* `opts.wait === true`. Default is Infinity (i.e. it doesn't timeout).
* @param {Function} cb: `function (err, res)`
*/
TritonApi.prototype.deleteInstanceTag = function deleteInstanceTag(opts, cb) {
assert.object(opts, 'opts');
assert.string(opts.id, 'opts.id');
assert.string(opts.tag, 'opts.tag');
assert.optionalBool(opts.wait, 'opts.wait');
assert.optionalNumber(opts.waitTimeout, 'opts.waitTimeout');
assert.func(cb, 'cb');
this._changeInstanceTags({
id: opts.id,
change: {
action: 'delete',
tagName: opts.tag
},
wait: opts.wait,
waitTimeout: opts.waitTimeout
}, function (err, updatedTags, res) {
cb(err, res);
});
};
/**
* Delete all tags for the given instance.
* <http://apidocs.joyent.com/cloudapi/#DeleteMachineTags>
*
* @param {Object} opts
* - {String} id: The instance ID, name, or short ID. Required.
* - {Boolean} wait: Wait (via polling) until the tag update is complete.
* Warning: A concurrent tag update to the same tags can result in this
* polling being unable to notice the change. Use `waitTimeout` to
* put an upper bound.
* - {Number} waitTimeout: The number of milliseconds after which to
* timeout (call `cb` with a timeout error) waiting. Only relevant if
* `opts.wait === true`. Default is Infinity (i.e. it doesn't timeout).
* @param {Function} cb: `function (err, res)`
*/
TritonApi.prototype.deleteAllInstanceTags =
function deleteAllInstanceTags(opts, cb) {
assert.object(opts, 'opts');
assert.string(opts.id, 'opts.id');
assert.optionalBool(opts.wait, 'opts.wait');
assert.optionalNumber(opts.waitTimeout, 'opts.waitTimeout');
assert.func(cb, 'cb');
this._changeInstanceTags({
id: opts.id,
change: {
action: 'deleteAll'
},
wait: opts.wait,
waitTimeout: opts.waitTimeout
}, function (err, updatedTags, res) {
cb(err, res);
});
};
// ---- RBAC
/**
* Get role tags for a resource.
*

View File

@ -0,0 +1,264 @@
/*
* 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.
*/
/*
* Test 'triton inst tag ...'.
*/
var f = require('util').format;
var os = require('os');
var path = require('path');
var tabula = require('tabula');
var test = require('tape');
var vasync = require('vasync');
var common = require('../../lib/common');
var h = require('./helpers');
// --- globals
var INST_ALIAS = f('nodetritontest-insttag-%s', os.hostname());
var opts = {
skip: !h.CONFIG.allowWriteActions
};
// --- Tests
if (opts.skip) {
console.error('** skipping %s tests', __filename);
console.error('** set "allowWriteActions" in test config to enable');
}
test('triton inst tag ...', opts, function (tt) {
tt.comment('Test config:');
Object.keys(h.CONFIG).forEach(function (key) {
var value = h.CONFIG[key];
tt.comment(f('- %s: %j', key, value));
});
var inst;
tt.test(' cleanup: rm inst ' + INST_ALIAS + ' if exists', function (t) {
h.triton(['inst', 'get', '-j', INST_ALIAS],
function (err, stdout, stderr) {
if (err) {
if (err.code === 3) { // `triton` code for ResourceNotFound
t.ok(true, 'no pre-existing alias in the way');
t.end();
} else {
t.ifErr(err);
t.end();
}
} else {
var oldInst = JSON.parse(stdout);
h.safeTriton(t, ['delete', '-w', oldInst.id], function (dErr) {
t.ifError(dErr, 'deleted old inst ' + oldInst.id);
t.end();
});
}
});
});
var imgId;
tt.test(' setup: find test image', function (t) {
h.getTestImg(t, function (err, imgId_) {
t.ifError(err, 'getTestImg' + (err ? ': ' + err : ''));
imgId = imgId_;
t.end();
});
});
var pkgId;
tt.test(' setup: find test package', function (t) {
h.getTestPkg(t, function (err, pkgId_) {
t.ifError(err, 'getTestPkg' + (err ? ': ' + err : ''));
pkgId = pkgId_;
t.end();
});
});
// create a test machine (blocking) and output JSON
tt.test(' setup: triton create ' + INST_ALIAS, function (t) {
var argv = [
'create',
'-wj',
'--tag', 'blah=bling',
'-n', INST_ALIAS,
imgId, pkgId
];
var start = Date.now();
h.safeTriton(t, argv, function (err, stdout) {
var elapsedSec = Math.round((Date.now() - start) / 1000);
t.ok(true, 'created test inst ('+ elapsedSec + 's)');
var lines = h.jsonStreamParse(stdout);
inst = lines[1];
t.equal(lines[0].tags.blah, 'bling', '"blah" tag set');
t.equal(lines[1].state, 'running', 'inst is running');
t.end();
});
});
tt.test(' triton inst tag ls INST', function (t) {
h.safeTriton(t, ['inst', 'tag', 'ls', inst.name],
function (err, stdout) {
var tags = JSON.parse(stdout);
t.deepEqual(tags, {blah: 'bling'});
t.end();
});
});
tt.test(' triton inst tags INST', function (t) {
h.safeTriton(t, ['inst', 'tags', inst.name], function (err, stdout) {
var tags = JSON.parse(stdout);
t.deepEqual(tags, {blah: 'bling'});
t.end();
});
});
tt.test(' triton inst tag set -w INST name=value', function (t) {
var argv = ['inst', 'tag', 'set', '-w', inst.id,
'foo=bar', 'pi=3.14', 'really=true'];
h.safeTriton(t, argv, function (err, stdout) {
var tags = JSON.parse(stdout);
t.deepEqual(tags, {
blah: 'bling',
foo: 'bar',
pi: 3.14,
really: true
});
t.end();
});
});
tt.test(' triton inst get INST foo', function (t) {
h.safeTriton(t, ['inst', 'tag', 'get', inst.id.split('-')[0], 'foo'],
function (err, stdout) {
t.equal(stdout.trim(), 'bar');
t.end();
});
});
tt.test(' triton inst get INST foo -j', function (t) {
h.safeTriton(t, ['inst', 'tag', 'get', inst.id, 'foo', '-j'],
function (err, stdout) {
t.equal(stdout.trim(), '"bar"');
t.end();
});
});
tt.test(' triton inst get INST really -j', function (t) {
h.safeTriton(t, ['inst', 'tag', 'get', inst.name, 'really', '-j'],
function (err, stdout) {
t.equal(stdout.trim(), 'true');
t.end();
});
});
tt.test(' triton inst tag set -w INST -f tags.json', function (t) {
var argv = ['inst', 'tag', 'set', '-w', inst.name, '-f',
path.resolve(__dirname, 'data', 'tags.json')];
h.safeTriton(t, argv, function (err, stdout) {
var tags = JSON.parse(stdout);
t.deepEqual(tags, {
blah: 'bling',
foo: 'bling',
pi: 3.14,
really: true
});
t.end();
});
});
tt.test(' triton inst tag set -w INST -f tags.kv', function (t) {
var argv = ['inst', 'tag', 'set', '-w', inst.name, '-f',
path.resolve(__dirname, 'data', 'tags.kv')];
h.safeTriton(t, argv, function (err, stdout) {
var tags = JSON.parse(stdout);
t.deepEqual(tags, {
blah: 'bling',
foo: 'bling',
pi: 3.14,
really: true,
key: 'value',
beep: 'boop'
});
t.end();
});
});
tt.test(' triton inst tag rm -w INST key really', function (t) {
var argv = ['inst', 'tag', 'rm', '-w', inst.name, 'key', 'really'];
h.safeTriton(t, argv, function (err, stdout) {
var lines = stdout.trim().split(/\n/);
t.ok(/^Deleted tag key/.test(lines[0]),
'Deleted tag key ...:' + lines[0]);
t.ok(/^Deleted tag really/.test(lines[1]),
'Deleted tag really ...:' + lines[1]);
t.end();
});
});
tt.test(' triton inst tag list INST', function (t) {
var argv = ['inst', 'tag', 'list', inst.name];
h.safeTriton(t, argv, function (err, stdout) {
var tags = JSON.parse(stdout);
t.deepEqual(tags, {
blah: 'bling',
foo: 'bling',
pi: 3.14,
beep: 'boop'
});
t.end();
});
});
tt.test(' triton inst tag replace-all -w INST ...', function (t) {
var argv = ['inst', 'tag', 'replace-all', '-w',
inst.name, 'whoa=there'];
h.safeTriton(t, argv, function (err, stdout) {
var tags = JSON.parse(stdout);
t.deepEqual(tags, {
whoa: 'there'
});
t.end();
});
});
tt.test(' triton inst tag delete -w -a INST', function (t) {
var argv = ['inst', 'tag', 'delete', '-w', '-a', inst.name];
h.safeTriton(t, argv, function (err, stdout) {
t.equal(stdout.trim(), 'Deleted all tags on instance ' + inst.name);
t.end();
});
});
tt.test(' triton inst tags INST', function (t) {
var argv = ['inst', 'tags', inst.name];
h.safeTriton(t, argv, function (err, stdout) {
t.equal(stdout.trim(), '{}');
t.end();
});
});
/*
* Use a timeout, because '-w' on delete doesn't have a way to know if the
* attempt failed or if it is just taking a really long time.
*/
tt.test(' cleanup: triton rm INST', {timeout: 10 * 60 * 1000},
function (t) {
h.safeTriton(t, ['rm', '-w', inst.id], function (err, stdout) {
t.end();
});
});
});

View File

@ -14,7 +14,6 @@
var f = require('util').format;
var os = require('os');
var tabula = require('tabula');
var test = require('tape');
var vasync = require('vasync');
@ -24,7 +23,7 @@ var h = require('./helpers');
// --- globals
var INST_ALIAS = f('node-triton-test-%s-vm1', os.hostname());
var INST_ALIAS = f('nodetritontest-managewf-%s', os.hostname());
var opts = {
skip: !h.CONFIG.allowWriteActions
@ -34,21 +33,6 @@ var opts = {
var instance;
// --- internal support stuff
function _jsonStreamParse(s) {
var results = [];
var lines = s.split('\n');
for (var i = 0; i < lines.length; i++) {
var line = lines[i].trim();
if (line) {
results.push(JSON.parse(line));
}
}
return results;
}
// --- Tests
if (opts.skip) {
@ -75,7 +59,7 @@ test('triton manage workflow', opts, function (tt) {
}
} else {
var inst = JSON.parse(stdout);
h.safeTriton(t, ['delete', '-w', inst.id], function () {
h.safeTriton(t, ['inst', 'rm', '-w', inst.id], function () {
t.ok(true, 'deleted inst ' + inst.id);
t.end();
});
@ -84,65 +68,25 @@ test('triton manage workflow', opts, function (tt) {
});
var imgId;
tt.test(' find image to use', function (t) {
if (h.CONFIG.image) {
imgId = h.CONFIG.image;
t.ok(imgId, 'image from config: ' + imgId);
t.end();
return;
}
var candidateImageNames = {
'base-64-lts': true,
'base-64': true,
'minimal-64': true,
'base-32-lts': true,
'base-32': true,
'minimal-32': true,
'base': true
};
h.safeTriton(t, ['img', 'ls', '-j'], function (stdout) {
var imgs = _jsonStreamParse(stdout);
// Newest images first.
tabula.sortArrayOfObjects(imgs, ['-published_at']);
var imgRepr;
for (var i = 0; i < imgs.length; i++) {
var img = imgs[i];
if (candidateImageNames[img.name]) {
imgId = img.id;
imgRepr = f('%s@%s', img.name, img.version);
break;
}
}
t.ok(imgId, f('latest available base/minimal image: %s (%s)',
imgId, imgRepr));
tt.test(' setup: find test image', function (t) {
h.getTestImg(t, function (err, imgId_) {
t.ifError(err, 'getTestImg' + (err ? ': ' + err : ''));
imgId = imgId_;
t.end();
});
});
var pkgId;
tt.test(' find package to use', function (t) {
if (h.CONFIG.package) {
pkgId = h.CONFIG.package;
t.ok(pkgId, 'package from config: ' + pkgId);
t.end();
return;
}
h.safeTriton(t, ['pkg', 'list', '-j'], function (stdout) {
var pkgs = _jsonStreamParse(stdout);
// Smallest RAM first.
tabula.sortArrayOfObjects(pkgs, ['memory']);
pkgId = pkgs[0].id;
t.ok(pkgId, f('smallest (RAM) available package: %s (%s)',
pkgId, pkgs[0].name));
tt.test(' setup: find test package', function (t) {
h.getTestPkg(t, function (err, pkgId_) {
t.ifError(err, 'getTestPkg' + (err ? ': ' + err : ''));
pkgId = pkgId_;
t.end();
});
});
// create a test machine (blocking) and output JSON
tt.test(' triton create', function (t) {
tt.test(' setup: triton create', function (t) {
var argv = [
'create',
'-wj',
@ -153,19 +97,8 @@ test('triton manage workflow', opts, function (tt) {
imgId, pkgId
];
h.safeTriton(t, argv, function (stdout) {
// parse JSON response
var lines = stdout.trim().split('\n');
t.equal(lines.length, 2, 'correct number of JSON lines');
try {
lines = lines.map(function (line) {
return JSON.parse(line);
});
} catch (e) {
t.fail('failed to parse JSON');
t.end();
}
h.safeTriton(t, argv, function (err, stdout) {
var lines = h.jsonStreamParse(stdout);
instance = lines[1];
t.equal(lines[0].id, lines[1].id, 'correct UUID given');
t.equal(lines[0].metadata.foo, 'bar', 'foo metadata set');
@ -184,22 +117,13 @@ test('triton manage workflow', opts, function (tt) {
vasync.parallel({
funcs: [
function (cb) {
h.safeTriton(t, ['instance', 'get', '-j', INST_ALIAS],
function (stdout) {
cb(null, stdout);
});
h.safeTriton(t, ['instance', 'get', '-j', INST_ALIAS], cb);
},
function (cb) {
h.safeTriton(t, ['instance', 'get', '-j', uuid],
function (stdout) {
cb(null, stdout);
});
h.safeTriton(t, ['instance', 'get', '-j', uuid], cb);
},
function (cb) {
h.safeTriton(t, ['instance', 'get', '-j', shortId],
function (stdout) {
cb(null, stdout);
});
h.safeTriton(t, ['instance', 'get', '-j', shortId], cb);
}
]
}, function (err, results) {
@ -229,7 +153,7 @@ test('triton manage workflow', opts, function (tt) {
// have a way to know if the attempt failed or if it is just taking a
// really long time.
tt.test(' triton delete', {timeout: 10 * 60 * 1000}, function (t) {
h.safeTriton(t, ['delete', '-w', instance.id], function (stdout) {
h.safeTriton(t, ['delete', '-w', instance.id], function () {
t.end();
});
});
@ -240,7 +164,7 @@ test('triton manage workflow', opts, function (tt) {
// create a test machine (non-blocking)
tt.test(' triton create', function (t) {
h.safeTriton(t, ['create', '-jn', INST_ALIAS, imgId, pkgId],
function (stdout) {
function (err, stdout) {
// parse JSON response
var lines = stdout.trim().split('\n');
@ -263,7 +187,7 @@ test('triton manage workflow', opts, function (tt) {
// wait for the machine to start
tt.test(' triton inst wait', function (t) {
h.safeTriton(t, ['inst', 'wait', instance.id],
function (stdout) {
function (err, stdout) {
// parse JSON response
var lines = stdout.trim().split('\n');
@ -280,8 +204,7 @@ test('triton manage workflow', opts, function (tt) {
// stop the machine
tt.test(' triton stop', function (t) {
h.safeTriton(t, ['stop', '-w', INST_ALIAS],
function (stdout) {
h.safeTriton(t, ['stop', '-w', INST_ALIAS], function (err, stdout) {
t.ok(stdout.match(/^Stop instance/, 'correct stdout'));
t.end();
});
@ -290,11 +213,9 @@ test('triton manage workflow', opts, function (tt) {
// wait for the machine to stop
tt.test(' triton confirm stopped', function (t) {
h.safeTriton(t, {json: true, args: ['inst', 'get', '-j', INST_ALIAS]},
function (d) {
function (err, d) {
instance = d;
t.equal(d.state, 'stopped', 'machine stopped');
t.end();
});
});
@ -302,7 +223,7 @@ test('triton manage workflow', opts, function (tt) {
// start the machine
tt.test(' triton start', function (t) {
h.safeTriton(t, ['start', '-w', INST_ALIAS],
function (stdout) {
function (err, stdout) {
t.ok(stdout.match(/^Start instance/, 'correct stdout'));
t.end();
});
@ -311,7 +232,7 @@ test('triton manage workflow', opts, function (tt) {
// wait for the machine to start
tt.test(' confirm running', function (t) {
h.safeTriton(t, {json: true, args: ['inst', 'get', '-j', INST_ALIAS]},
function (d) {
function (err, d) {
instance = d;
t.equal(d.state, 'running', 'machine running');
t.end();
@ -320,7 +241,7 @@ test('triton manage workflow', opts, function (tt) {
// remove test instance
tt.test(' cleanup (triton delete)', function (t) {
h.safeTriton(t, ['delete', '-w', instance.id], function (stdout) {
h.safeTriton(t, ['delete', '-w', instance.id], function () {
t.end();
});
});

View File

@ -5,7 +5,7 @@
*/
/*
* Copyright (c) 2015, Joyent, Inc.
* Copyright 2016, Joyent, Inc.
*/
/*
@ -36,7 +36,7 @@ if (opts.skip) {
test('triton profiles (read only)', function (tt) {
tt.test(' triton profile get env', function (t) {
h.safeTriton(t, {json: true, args: ['profile', 'get', '-j', 'env']},
function (p) {
function (err, p) {
t.equal(p.account, h.CONFIG.profile.account,
'env account correct');
@ -55,8 +55,7 @@ test('triton profiles (read only)', function (tt) {
test('triton profiles (read/write)', opts, function (tt) {
tt.test(' triton profile create', function (t) {
h.safeTriton(t, ['profile', 'create', '-f', PROFILE_FILE],
function (stdout) {
function (err, stdout) {
t.ok(stdout.match(/^Saved profile/), 'stdout correct');
t.end();
});
@ -65,17 +64,16 @@ test('triton profiles (read/write)', opts, function (tt) {
tt.test(' triton profile get', function (t) {
h.safeTriton(t,
{json: true, args: ['profile', 'get', '-j', PROFILE_DATA.name]},
function (p) {
function (err, p) {
t.deepEqual(p, PROFILE_DATA, 'profile matched');
t.end();
});
});
tt.test(' triton profile delete', function (t) {
h.safeTriton(t, ['profile', 'delete', '-f', PROFILE_DATA.name],
function (stdout) {
function (err, stdout) {
t.ok(stdout.match(/^Deleted profile/), 'stdout correct');
t.end();

View File

@ -0,0 +1 @@
{"foo": "bling"}

View File

@ -0,0 +1,2 @@
key=value
beep=boop

View File

@ -16,6 +16,7 @@ var error = console.error;
var assert = require('assert-plus');
var f = require('util').format;
var path = require('path');
var tabula = require('tabula');
var common = require('../../lib/common');
var mod_triton = require('../../');
@ -122,29 +123,28 @@ function triton(args, opts, cb) {
}
/*
* triton wrapper that:
* - tests no error is present
* - tests stdout is not empty
* `triton ...` wrapper that:
* - tests non-error exit
* - tests stderr is empty
*
* In the event that any of the above is false, this function will NOT
* fire the callback, which will result in the early terminate of these
* tests as `t.end()` will never be called.
*
* @param {Tape} t - tape test object
* @param {Object|Array} opts - options object
* @param {Function} cb - callback called like "cb(stdout)"
* @param {Object|Array} opts - options object, or just the `triton` args
* @param {Function} cb - `function (err, stdout)`
*/
function safeTriton(t, opts, cb) {
assert.object(t, 't');
if (Array.isArray(opts)) {
opts = {args: opts};
}
assert.object(opts, 'opts');
assert.arrayOfString(opts.args, 'opts.args');
assert.optionalBool(opts.json, 'opts.json');
assert.func(cb, 'cb');
t.comment(f('running: triton %s', opts.args.join(' ')));
triton(opts.args, function (err, stdout, stderr) {
t.error(err, 'no error running child process');
t.equal(stderr, '', 'no stderr produced');
t.notEqual(stdout, '', 'stdout produced');
if (opts.json) {
try {
stdout = JSON.parse(stdout);
@ -153,13 +153,96 @@ function safeTriton(t, opts, cb) {
return;
}
}
if (!err && stdout && !stderr)
cb(stdout);
cb(err, stdout);
});
}
/*
* Find and return an image that can be used for test provisions. We look
* for an available base or minimal image.
*
* @param {Tape} t - tape test object
* @param {Function} cb - `function (err, imgId)`
* where `imgId` is an image identifier (an image name, shortid, or id).
*/
function getTestImg(t, cb) {
if (CONFIG.image) {
t.ok(CONFIG.image, 'image from config: ' + CONFIG.image);
cb(null, CONFIG.image);
return;
}
var candidateImageNames = {
'base-64-lts': true,
'base-64': true,
'minimal-64': true,
'base-32-lts': true,
'base-32': true,
'minimal-32': true,
'base': true
};
safeTriton(t, ['img', 'ls', '-j'], function (err, stdout) {
var imgId;
var imgs = jsonStreamParse(stdout);
// Newest images first.
tabula.sortArrayOfObjects(imgs, ['-published_at']);
var imgRepr;
for (var i = 0; i < imgs.length; i++) {
var img = imgs[i];
if (candidateImageNames[img.name]) {
imgId = img.id;
imgRepr = f('%s@%s', img.name, img.version);
break;
}
}
t.ok(imgId, f('latest available base/minimal image: %s (%s)',
imgId, imgRepr));
cb(err, imgId);
});
}
/*
* Find and return an package that can be used for test provisions.
*
* @param {Tape} t - tape test object
* @param {Function} cb - `function (err, pkgId)`
* where `pkgId` is an package identifier (a name, shortid, or id).
*/
function getTestPkg(t, cb) {
if (CONFIG.package) {
t.ok(CONFIG.package, 'package from config: ' + CONFIG.package);
cb(null, CONFIG.package);
return;
}
safeTriton(t, ['pkg', 'ls', '-j'], function (err, stdout) {
var pkgs = jsonStreamParse(stdout);
// Smallest RAM first.
tabula.sortArrayOfObjects(pkgs, ['memory']);
var pkgId = pkgs[0].id;
t.ok(pkgId, f('smallest (RAM) available package: %s (%s)',
pkgId, pkgs[0].name));
cb(null, pkgId);
});
}
function jsonStreamParse(s) {
var results = [];
var lines = s.trim().split('\n');
for (var i = 0; i < lines.length; i++) {
var line = lines[i].trim();
if (line) {
results.push(JSON.parse(line));
}
}
return results;
}
/*
* Create a TritonApi client using the CLI.
*/
@ -179,5 +262,9 @@ module.exports = {
triton: triton,
safeTriton: safeTriton,
createClient: createClient,
getTestImg: getTestImg,
getTestPkg: getTestPkg,
jsonStreamParse: jsonStreamParse,
ifErr: testcommon.ifErr
};

View File

@ -9,7 +9,7 @@
*/
/*
* Unit tests for `tagsFromOpts()` used by `triton create ...`.
* Unit tests for `tagsFromCreateOpts()` used by `triton create ...`.
*/
var assert = require('assert-plus');
@ -17,7 +17,8 @@ var cmdln = require('cmdln');
var format = require('util').format;
var test = require('tape');
var tagsFromOpts = require('../../lib/metadataandtags').tagsFromOpts;
var tagsFromCreateOpts
= require('../../lib/metadataandtags').tagsFromCreateOpts;
// ---- globals
@ -140,7 +141,7 @@ var cases = [
// ---- test driver
test('tagsFromOpts', function (tt) {
test('tagsFromCreateOpts', function (tt) {
cases.forEach(function (c, num) {
var testName = format('case %d: %s', num, c.argv.join(' '));
tt.test(testName, function (t) {
@ -157,7 +158,7 @@ test('tagsFromOpts', function (tt) {
stderrChunks.push(s);
};
tagsFromOpts(opts, log, function (err, tags) {
tagsFromCreateOpts(opts, log, function (err, tags) {
// Restore stderr.
process.stderr.write = _oldStderrWrite;
var stderr = stderrChunks.join('');

View File

@ -0,0 +1,197 @@
/*
* 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) 2015, Joyent, Inc.
*/
/*
* Unit tests for `tagsFromCreateOpts()` used by `triton create ...`.
*/
var assert = require('assert-plus');
var cmdln = require('cmdln');
var format = require('util').format;
var test = require('tape');
var tagsFromCreateOpts
= require('../../lib/metadataandtags').tagsFromCreateOpts;
// ---- globals
var log = require('../lib/log');
var debug = function () {};
// debug = console.warn;
// ---- test cases
var OPTIONS = [
{
names: ['tag', 't'],
type: 'arrayOfString'
}
];
var cases = [
{
argv: ['triton', 'create', '-t', 'foo=bar'],
expect: {
tags: {foo: 'bar'}
}
},
{
argv: ['triton', 'create', '--tag', 'foo=bar'],
expect: {
tags: {foo: 'bar'}
}
},
{
argv: ['triton', 'create', '-t', 'foo=bar', '-t', 'bling=bloop'],
expect: {
tags: {
foo: 'bar',
bling: 'bloop'
}
}
},
{
argv: ['triton', 'create',
'-t', 'num=42',
'-t', 'pi=3.14',
'-t', 'yes=true',
'-t', 'no=false',
'-t', 'array=[1,2,3]'],
expect: {
tags: {
num: 42,
pi: 3.14,
yes: true,
no: false,
array: '[1,2,3]'
}
}
},
{
argv: ['triton', 'create',
'-t', '@' + __dirname + '/corpus/metadata.json'],
expect: {
tags: {
'foo': 'bar',
'one': 'four',
'num': 42
}
}
},
{
argv: ['triton', 'create',
'-t', '@' + __dirname + '/corpus/metadata.kv'],
expect: {
tags: {
'foo': 'bar',
'one': 'four',
'num': 42
}
}
},
{
argv: ['triton', 'create',
'-t', '@' + __dirname + '/corpus/metadata-illegal-types.json'],
expect: {
err: [
/* jsl:ignore */
/invalid tag value type/,
/\(from .*corpus\/metadata-illegal-types.json\)/,
/must be one of string/
/* jsl:end */
]
}
},
{
argv: ['triton', 'create',
'-t', '@' + __dirname + '/corpus/metadata-invalid-json.json'],
expect: {
err: [
/* jsl:ignore */
/is not valid JSON/,
/corpus\/metadata-invalid-json.json/
/* jsl:end */
]
}
},
{
argv: ['triton', 'create',
'-t', '{"foo":"bar","num":12}'],
expect: {
tags: {
'foo': 'bar',
'num': 12
}
}
}
];
// ---- test driver
test('tagsFromCreateOpts', function (tt) {
cases.forEach(function (c, num) {
var testName = format('case %d: %s', num, c.argv.join(' '));
tt.test(testName, function (t) {
debug('--', num);
debug('c: %j', c);
var parser = new cmdln.dashdash.Parser({options: OPTIONS});
var opts = parser.parse({argv: c.argv});
debug('opts: %j', opts);
// Capture stderr for warnings while running.
var stderrChunks = [];
var _oldStderrWrite = process.stderr.write;
process.stderr.write = function (s) {
stderrChunks.push(s);
};
tagsFromCreateOpts(opts, log, function (err, tags) {
// Restore stderr.
process.stderr.write = _oldStderrWrite;
var stderr = stderrChunks.join('');
if (c.expect.err) {
var errRegexps = (Array.isArray(c.expect.err)
? c.expect.err : [c.expect.err]);
errRegexps.forEach(function (regexp) {
assert.regexp(regexp, 'case.expect.err');
t.ok(err, 'expected an error');
t.ok(regexp.test(err.message), format(
'error message matches %s, actual %j',
regexp, err.message));
});
} else {
t.ifError(err);
}
if (c.expect.hasOwnProperty('tags')) {
t.deepEqual(tags, c.expect.tags);
}
if (c.expect.hasOwnProperty('stderr')) {
var stderrRegexps = (Array.isArray(c.expect.stderr)
? c.expect.stderr : [c.expect.stderr]);
stderrRegexps.forEach(function (regexp) {
assert.regexp(regexp, 'case.expect.stderr');
t.ok(regexp.test(stderr), format(
'error message matches %s, actual %j',
regexp, stderr));
});
}
t.end();
});
});
});
});

View File

@ -119,6 +119,7 @@
+define module
+define process
+define require
+define setImmediate
+define setInterval
+define setTimeout
+define Buffer