joyent/node-triton#88: triton inst ...
support for updating tags
This commit is contained in:
parent
5b6980e490
commit
4760defd05
140
lib/cloudapi2.js
140
lib/cloudapi2.js
@ -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
|
||||
|
||||
/**
|
||||
|
@ -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;
|
||||
|
112
lib/do_instance/do_tag/do_delete.js
Normal file
112
lib/do_instance/do_tag/do_delete.js
Normal 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;
|
68
lib/do_instance/do_tag/do_get.js
Normal file
68
lib/do_instance/do_tag/do_get.js
Normal 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;
|
69
lib/do_instance/do_tag/do_list.js
Normal file
69
lib/do_instance/do_tag/do_list.js
Normal 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;
|
132
lib/do_instance/do_tag/do_replace_all.js
Normal file
132
lib/do_instance/do_tag/do_replace_all.js
Normal 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;
|
133
lib/do_instance/do_tag/do_set.js
Normal file
133
lib/do_instance/do_tag/do_set.js
Normal 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;
|
54
lib/do_instance/do_tag/index.js
Normal file
54
lib/do_instance/do_tag/index.js
Normal 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;
|
25
lib/do_instance/do_tags.js
Normal file
25
lib/do_instance/do_tags.js
Normal 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;
|
@ -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'];
|
||||
|
||||
|
@ -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
|
||||
};
|
||||
|
@ -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
|
||||
};
|
||||
|
597
lib/tritonapi.js
597
lib/tritonapi.js
@ -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.
|
||||
*
|
||||
|
264
test/integration/cli-instance-tag.test.js
Normal file
264
test/integration/cli-instance-tag.test.js
Normal 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();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
@ -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();
|
||||
|
1
test/integration/data/tags.json
Normal file
1
test/integration/data/tags.json
Normal file
@ -0,0 +1 @@
|
||||
{"foo": "bling"}
|
2
test/integration/data/tags.kv
Normal file
2
test/integration/data/tags.kv
Normal file
@ -0,0 +1,2 @@
|
||||
key=value
|
||||
beep=boop
|
@ -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
|
||||
};
|
||||
|
@ -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('');
|
197
test/unit/tagsFromSetArgs.test.js
Normal file
197
test/unit/tagsFromSetArgs.test.js
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -119,6 +119,7 @@
|
||||
+define module
|
||||
+define process
|
||||
+define require
|
||||
+define setImmediate
|
||||
+define setInterval
|
||||
+define setTimeout
|
||||
+define Buffer
|
||||
|
Reference in New Issue
Block a user