Merge branch 'master' of https://github.com/joyent/node-triton into snapshot-fwrules

This commit is contained in:
Marsell Kukuljevic 2016-02-12 23:50:09 +11:00
commit b7aa52dd0d
32 changed files with 2143 additions and 176 deletions

View File

@ -1,10 +1,34 @@
# node-triton changelog # node-triton changelog
## 4.4.2 (not yet released) ## 4.5.1 (not yet released)
(nothing yet) (nothing yet)
## 4.5.0
- #88 'triton inst tag ...' for managing instance tags.
## 4.4.4
- #90 Update sshpk and smartdc-auth to attempt to deal with multiple package
inter-deps.
## 4.4.3
- #86 Ensure `triton profile ls` and `triton profile set-current` work
when there is no current profile.
## 4.4.2
- Support `triton.createClient(...)` creation without requiring a
`configDir`. Basically this then fallsback to a `TritonApi` with the default
config.
## 4.4.1 ## 4.4.1
- #83, #84 Fix running `triton` on Windows. - #83, #84 Fix running `triton` on Windows.

View File

@ -0,0 +1,46 @@
#!/usr/bin/env node
/**
* Example using cloudapi2.js to call cloudapi's ListMachines endpoint.
*
* Usage:
* ./example-list-images.js | bunyan
*/
var p = console.log;
var bunyan = require('bunyan');
var triton = require('../'); // typically `require('triton');`
var URL = process.env.SDC_URL || 'https://us-sw-1.api.joyent.com';
var ACCOUNT = process.env.SDC_ACCOUNT || 'bob';
var KEY_ID = process.env.SDC_KEY_ID || 'b4:f0:b4:6c:18:3b:44:63:b4:4e:58:22:74:43:d4:bc';
var log = bunyan.createLogger({
name: 'test-list-instances',
level: process.env.LOG_LEVEL || 'trace'
});
/*
* More details on `createClient` options here:
* https://github.com/joyent/node-triton/blob/master/lib/index.js#L18-L61
* For example, if you want to use an existing `triton` CLI profile, you can
* pass that profile name in.
*/
var client = triton.createClient({
log: log,
profile: {
url: URL,
account: ACCOUNT,
keyId: KEY_ID
}
});
// TODO: Eventually the top-level TritonApi will have `.listInstances()` to use.
client.cloudapi.listMachines(function (err, insts) {
client.close(); // Remember to close the client to close TCP conn.
if (err) {
console.error('listInstances err:', err);
} else {
console.log(JSON.stringify(insts, null, 4));
}
});

View File

@ -264,7 +264,7 @@ CloudApi.prototype._passThrough = function _passThrough(endpoint, opts, cb) {
assert.func(cb, 'cb'); assert.func(cb, 'cb');
var p = this._path(endpoint, opts); 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: * Improve this kind of error message:
* *
@ -939,6 +939,119 @@ 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/#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);
});
};
// --- snapshots // --- snapshots
@ -1136,7 +1249,6 @@ function createFirewallRule(opts, cb) {
}); });
}; };
/** /**
* Lists all your Firewall Rules. * Lists all your Firewall Rules.
* *
@ -1252,7 +1364,7 @@ function disableFirewallRule(id, cb) {
/** /**
* <http://apidocs.joyent.com/cloudapi/#DeleteUser> * Remove a Firewall Rule.
* *
* @param {Object} opts (object) * @param {Object} opts (object)
* - {String} id (required) for your firewall. * - {String} id (required) for your firewall.
@ -1273,6 +1385,31 @@ function deleteFirewallRule(opts, cb) {
}; };
/**
* <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);
});
};
/** /**
* Lists all the Firewall Rules affecting a given machine. * Lists all the Firewall Rules affecting a given machine.
* *

View File

@ -5,7 +5,7 @@
*/ */
/* /*
* Copyright 2015 Joyent, Inc. * Copyright 2016 Joyent, Inc.
*/ */
/* /*
@ -79,25 +79,35 @@ function configPathFromDir(configDir) {
* *
* This includes some internal data on keys with a leading underscore: * This includes some internal data on keys with a leading underscore:
* _defaults the defaults.json object * _defaults the defaults.json object
* _user the "user" config.json object * _configDir the user config dir (if one is provided)
* _configDir the user config dir * _user the "user" config.json object (if exists)
* *
* @param opts.configDir {String} Optional. A base dir for TritonApi config.
* @returns {Object} The loaded config. * @returns {Object} The loaded config.
*/ */
function loadConfig(opts) { function loadConfig(opts) {
assert.object(opts, 'opts'); assert.object(opts, 'opts');
assert.string(opts.configDir, 'opts.configDir'); assert.optionalString(opts.configDir, 'opts.configDir');
var configDir = common.tildeSync(opts.configDir); var configDir;
var configPath = configPathFromDir(configDir); var configPath;
if (opts.configDir) {
configDir = common.tildeSync(opts.configDir);
configPath = configPathFromDir(configDir);
}
var c = fs.readFileSync(DEFAULTS_PATH, 'utf8'); var c = fs.readFileSync(DEFAULTS_PATH, 'utf8');
var _defaults = JSON.parse(c); var _defaults = JSON.parse(c);
var config = JSON.parse(c); var config = JSON.parse(c);
if (fs.existsSync(configPath)) { if (configPath && fs.existsSync(configPath)) {
c = fs.readFileSync(configPath, 'utf8'); c = fs.readFileSync(configPath, 'utf8');
var _user = JSON.parse(c); try {
var userConfig = JSON.parse(c); var _user = JSON.parse(c);
var userConfig = JSON.parse(c);
} catch (userConfigParseErr) {
throw new errors.ConfigError(
format('"%s" is invalid JSON', configPath));
}
if (typeof (userConfig) !== 'object' || Array.isArray(userConfig)) { if (typeof (userConfig) !== 'object' || Array.isArray(userConfig)) {
throw new errors.ConfigError( throw new errors.ConfigError(
format('"%s" is not an object', configPath)); format('"%s" is not an object', configPath));
@ -121,7 +131,9 @@ function loadConfig(opts) {
config._user = _user; config._user = _user;
} }
config._defaults = _defaults; config._defaults = _defaults;
config._configDir = configDir; if (configDir) {
config._configDir = configDir;
}
return config; return config;
} }
@ -304,9 +316,14 @@ function loadProfile(opts) {
assert.optionalString(opts.configDir, 'opts.configDir'); assert.optionalString(opts.configDir, 'opts.configDir');
if (opts.name === 'env') { if (opts.name === 'env') {
return _loadEnvProfile(); var envProfile = _loadEnvProfile();
if (!envProfile) {
throw new errors.ConfigError('could not load "env" profile '
+ '(missing TRITON_*, or SDC_*, environment variables)');
}
return envProfile;
} else if (!opts.configDir) { } else if (!opts.configDir) {
throw new errors.TritonError( throw new errors.ConfigError(
'cannot load profiles (other than "env") without `opts.configDir`'); 'cannot load profiles (other than "env") without `opts.configDir`');
} else { } else {
var profilePath = path.resolve( var profilePath = path.resolve(

View File

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

View File

@ -169,7 +169,7 @@ do_list.help = [
'{{options}}', '{{options}}',
'Filters:', 'Filters:',
' FIELD=VALUE Equality filter. Supported fields: type, brand, name,', ' FIELD=VALUE Equality filter. Supported fields: type, brand, name,',
' image, state, memory, and tag', ' image, state, and memory',
' FIELD=true|false Boolean filter. Supported fields: docker (added in', ' FIELD=true|false Boolean filter. Supported fields: docker (added in',
' CloudAPI 8.0.0)', ' CloudAPI 8.0.0)',
'', '',

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

@ -42,7 +42,8 @@ function InstanceCLI(top) {
'ssh', 'ssh',
'wait', 'wait',
'audit', 'audit',
'fwrules' 'fwrules',
'tag'
] ]
}); });
} }
@ -66,6 +67,8 @@ InstanceCLI.prototype.do_ssh = require('./do_ssh');
InstanceCLI.prototype.do_wait = require('./do_wait'); InstanceCLI.prototype.do_wait = require('./do_wait');
InstanceCLI.prototype.do_audit = require('./do_audit'); InstanceCLI.prototype.do_audit = require('./do_audit');
InstanceCLI.prototype.do_fwrules = require('./do_fwrules'); InstanceCLI.prototype.do_fwrules = require('./do_fwrules');
InstanceCLI.prototype.do_tag = require('./do_tag');
InstanceCLI.prototype.do_tags = require('./do_tags');
InstanceCLI.aliases = ['inst']; InstanceCLI.aliases = ['inst'];

View File

@ -1,7 +1,7 @@
/* /*
* Copyright (c) 2015 Joyent Inc. * Copyright 2016 Joyent Inc.
* *
* `triton profiles ...` * `triton profile list ...`
*/ */
var tabula = require('tabula'); var tabula = require('tabula');
@ -38,9 +38,20 @@ function _listProfiles(cli, opts, args, cb) {
} }
// Current profile: Set 'curr' field. Apply CLI overrides. // Current profile: Set 'curr' field. Apply CLI overrides.
var currProfile;
try {
currProfile = cli.tritonapi.profile;
} catch (err) {
// Ignore inability to load a profile.
if (!(err instanceof errors.ConfigError)) {
throw err;
}
}
var haveCurr = false;
for (i = 0; i < profiles.length; i++) { for (i = 0; i < profiles.length; i++) {
var profile = profiles[i]; var profile = profiles[i];
if (profile.name === cli.tritonapi.profile.name) { if (currProfile && profile.name === currProfile.name) {
haveCurr = true;
cli._applyProfileOverrides(profile); cli._applyProfileOverrides(profile);
if (opts.json) { if (opts.json) {
profile.curr = true; profile.curr = true;
@ -66,6 +77,10 @@ function _listProfiles(cli, opts, args, cb) {
columns: columns, columns: columns,
sort: sort sort: sort
}); });
if (!haveCurr) {
process.stderr.write('\nWarning: There is no current profile. '
+ 'Use "triton profile set-current ..."\nto set one.\n');
}
} }
cb(); cb();
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2015 Joyent Inc. * Copyright 2016 Joyent Inc.
* *
* Shared stuff for `triton profile ...` handling. * Shared stuff for `triton profile ...` handling.
*/ */
@ -7,7 +7,7 @@
var assert = require('assert-plus'); var assert = require('assert-plus');
var mod_config = require('../config'); var mod_config = require('../config');
var errors = require('../errors');
function setCurrentProfile(opts, cb) { function setCurrentProfile(opts, cb) {
@ -25,7 +25,16 @@ function setCurrentProfile(opts, cb) {
return cb(err); return cb(err);
} }
if (cli.tritonapi.profile.name === profile.name) { var currProfile;
try {
currProfile = cli.tritonapi.profile;
} catch (err) {
// Ignore inability to load a profile.
if (!(err instanceof errors.ConfigError)) {
throw err;
}
}
if (currProfile && currProfile.name === profile.name) {
console.log('"%s" is already the current profile', profile.name); console.log('"%s" is already the current profile', profile.name);
return cb(); return cb();
} }

View File

@ -182,6 +182,7 @@ util.inherits(SigningError, _TritonBaseVError);
* A 'DEPTH_ZERO_SELF_SIGNED_CERT' An error signing a request. * A 'DEPTH_ZERO_SELF_SIGNED_CERT' An error signing a request.
*/ */
function SelfSignedCertError(cause, url) { function SelfSignedCertError(cause, url) {
assert.string(url, 'url');
var msg = format('could not access CloudAPI %s because it uses a ' + var msg = format('could not access CloudAPI %s because it uses a ' +
'self-signed TLS certificate and your current profile is not ' + 'self-signed TLS certificate and your current profile is not ' +
'configured for insecure access', url); 'configured for insecure access', url);
@ -195,6 +196,25 @@ function SelfSignedCertError(cause, url) {
util.inherits(SelfSignedCertError, _TritonBaseVError); 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. * A resource (instance, image, ...) was not found.
*/ */
@ -244,6 +264,7 @@ module.exports = {
UsageError: UsageError, UsageError: UsageError,
SigningError: SigningError, SigningError: SigningError,
SelfSignedCertError: SelfSignedCertError, SelfSignedCertError: SelfSignedCertError,
TimeoutError: TimeoutError,
ResourceNotFoundError: ResourceNotFoundError, ResourceNotFoundError: ResourceNotFoundError,
MultiError: MultiError MultiError: MultiError
}; };

View File

@ -81,7 +81,7 @@ var tritonapi = require('./tritonapi');
* - @param profileName {String} A Triton profile name. For any profile * - @param profileName {String} A Triton profile name. For any profile
* name other than "env", one must also provide either `configDir` * name other than "env", one must also provide either `configDir`
* or `config`. * or `config`.
* Either `profile` or `profileName` is requires. See discussion above. * Either `profile` or `profileName` is required. See discussion above.
* - @param configDir {String} A base config directory. This is used * - @param configDir {String} A base config directory. This is used
* by TritonApi to find and store profiles, config, and cache data. * by TritonApi to find and store profiles, config, and cache data.
* For example, the `triton` CLI uses "~/.triton". * For example, the `triton` CLI uses "~/.triton".

View File

@ -84,7 +84,7 @@ function metadataFromOpts(opts, log, cb) {
* <https://github.com/joyent/sdc-vmapi/blob/master/docs/index.md#vm-metadata> * <https://github.com/joyent/sdc-vmapi/blob/master/docs/index.md#vm-metadata>
* says values may be string, num or bool. * 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.arrayOfObject(opts._order, 'opts._order');
assert.object(log, 'log'); assert.object(log, 'log');
assert.func(cb, 'cb'); 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']; var allowedTypes = ['string', 'number', 'boolean'];
function _addMetadatum(ilk, metadata, key, value, from, cb) { function _addMetadatum(ilk, metadata, key, value, from, cb) {
assert.string(ilk, 'ilk'); assert.string(ilk, 'ilk');
@ -221,6 +275,10 @@ function _addMetadataFromFile(ilk, metadata, file, cb) {
function _addMetadataFromKvStr(ilk, metadata, s, from, cb) { function _addMetadataFromKvStr(ilk, metadata, s, from, cb) {
assert.string(ilk, 'ilk'); 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); var parts = strsplit(s, '=', 2);
if (parts.length !== 2) { if (parts.length !== 2) {
@ -285,5 +343,6 @@ function _addMetadatumFromFile(ilk, metadata, key, file, from, cb) {
module.exports = { module.exports = {
metadataFromOpts: metadataFromOpts, 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); 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 //---- TritonApi class
@ -585,16 +611,28 @@ TritonApi.prototype.getFirewallRule = function getFirewallRule(id, 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)` * @param {Function} callback `function (err, inst, res)`
* Where, on success, `res` is the response object from a `GetMachine` call * On success, `res` is the response object from a `GetMachine`, if one
* if one was made. * 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; 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'); assert.func(cb, 'cb');
var res; var res;
@ -605,10 +643,10 @@ TritonApi.prototype.getInstance = function getInstance(name, cb) {
vasync.pipeline({funcs: [ vasync.pipeline({funcs: [
function tryUuid(_, next) { function tryUuid(_, next) {
var uuid; var uuid;
if (common.isUUID(name)) { if (common.isUUID(opts.id)) {
uuid = name; uuid = opts.id;
} else { } else {
shortId = common.normShortId(name); shortId = common.normShortId(opts.id);
if (shortId && common.isUUID(shortId)) { if (shortId && common.isUUID(shortId)) {
// E.g. a >32-char docker container ID normalized to a UUID. // E.g. a >32-char docker container ID normalized to a UUID.
uuid = shortId; uuid = shortId;
@ -622,7 +660,7 @@ TritonApi.prototype.getInstance = function getInstance(name, cb) {
if (err && err.restCode === 'ResourceNotFound') { if (err && err.restCode === 'ResourceNotFound') {
// The CloudApi 404 error message sucks: "VM not found". // The CloudApi 404 error message sucks: "VM not found".
err = new errors.ResourceNotFoundError(err, 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); next(err);
}); });
@ -633,12 +671,12 @@ TritonApi.prototype.getInstance = function getInstance(name, cb) {
return next(); return next();
} }
self.cloudapi.listMachines({name: name}, function (err, insts) { self.cloudapi.listMachines({name: opts.id}, function (err, insts) {
if (err) { if (err) {
return next(err); return next(err);
} }
for (var i = 0; i < insts.length; i++) { for (var i = 0; i < insts.length; i++) {
if (insts[i].name === name) { if (insts[i].name === opts.id) {
instFromList = insts[i]; instFromList = insts[i];
// Relying on rule that instance name is unique // Relying on rule that instance name is unique
// for a user and DC. // for a user and DC.
@ -692,7 +730,22 @@ TritonApi.prototype.getInstance = function getInstance(name, cb) {
if (inst || !instFromList) { if (inst || !instFromList) {
next(); next();
return; 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; var uuid = instFromList.id;
self.cloudapi.getMachine(uuid, function (err, inst_, res_) { self.cloudapi.getMachine(uuid, function (err, inst_, res_) {
res = res_; res = res_;
@ -700,7 +753,7 @@ TritonApi.prototype.getInstance = function getInstance(name, cb) {
if (err && err.restCode === 'ResourceNotFound') { if (err && err.restCode === 'ResourceNotFound') {
// The CloudApi 404 error message sucks: "VM not found". // The CloudApi 404 error message sucks: "VM not found".
err = new errors.ResourceNotFoundError(err, 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); next(err);
}); });
@ -712,12 +765,528 @@ TritonApi.prototype.getInstance = function getInstance(name, cb) {
cb(null, inst, res); cb(null, inst, res);
} else { } else {
cb(new errors.ResourceNotFoundError(format( 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. * Get role tags for a resource.
* *

View File

@ -1,7 +1,7 @@
{ {
"name": "triton", "name": "triton",
"description": "Joyent Triton CLI and client (https://www.joyent.com/triton)", "description": "Joyent Triton CLI and client (https://www.joyent.com/triton)",
"version": "4.4.2", "version": "4.5.1",
"author": "Joyent (joyent.com)", "author": "Joyent (joyent.com)",
"dependencies": { "dependencies": {
"assert-plus": "0.2.0", "assert-plus": "0.2.0",
@ -18,8 +18,8 @@
"restify-clients": "1.1.0", "restify-clients": "1.1.0",
"restify-errors": "3.0.0", "restify-errors": "3.0.0",
"rimraf": "2.4.4", "rimraf": "2.4.4",
"sshpk": "1.6.x >=1.6.2", "sshpk": "1.7.x",
"smartdc-auth": "2.2.3", "smartdc-auth": "2.3.1",
"strsplit": "1.0.0", "strsplit": "1.0.0",
"tabula": "1.7.0", "tabula": "1.7.0",
"vasync": "1.6.3", "vasync": "1.6.3",

View File

@ -5,7 +5,7 @@
*/ */
/* /*
* Copyright (c) 2015, Joyent, Inc. * Copyright 2016, Joyent, Inc.
*/ */
/* /*
@ -47,7 +47,7 @@ test('triton account', function (tt) {
}); });
tt.test(' triton account get', function (t) { tt.test(' triton account get', function (t) {
h.triton('account get', function (err, stdout, stderr) { h.triton('-v account get', function (err, stdout, stderr) {
if (h.ifErr(t, err)) if (h.ifErr(t, err))
return t.end(); return t.end();
t.ok(new RegExp( t.ok(new RegExp(

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 f = require('util').format;
var os = require('os'); var os = require('os');
var tabula = require('tabula');
var test = require('tape'); var test = require('tape');
var vasync = require('vasync'); var vasync = require('vasync');
@ -24,7 +23,7 @@ var h = require('./helpers');
// --- globals // --- globals
var INST_ALIAS = f('node-triton-test-%s-vm1', os.hostname()); var INST_ALIAS = f('nodetritontest-managewf-%s', os.hostname());
var opts = { var opts = {
skip: !h.CONFIG.allowWriteActions skip: !h.CONFIG.allowWriteActions
@ -34,21 +33,6 @@ var opts = {
var instance; 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 // --- Tests
if (opts.skip) { if (opts.skip) {
@ -75,7 +59,7 @@ test('triton manage workflow', opts, function (tt) {
} }
} else { } else {
var inst = JSON.parse(stdout); 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.ok(true, 'deleted inst ' + inst.id);
t.end(); t.end();
}); });
@ -84,65 +68,25 @@ test('triton manage workflow', opts, function (tt) {
}); });
var imgId; var imgId;
tt.test(' find image to use', function (t) { tt.test(' setup: find test image', function (t) {
if (h.CONFIG.image) { h.getTestImg(t, function (err, imgId_) {
imgId = h.CONFIG.image; t.ifError(err, 'getTestImg' + (err ? ': ' + err : ''));
t.ok(imgId, 'image from config: ' + imgId); imgId = 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));
t.end(); t.end();
}); });
}); });
var pkgId; var pkgId;
tt.test(' find package to use', function (t) { tt.test(' setup: find test package', function (t) {
if (h.CONFIG.package) { h.getTestPkg(t, function (err, pkgId_) {
pkgId = h.CONFIG.package; t.ifError(err, 'getTestPkg' + (err ? ': ' + err : ''));
t.ok(pkgId, 'package from config: ' + pkgId); pkgId = 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));
t.end(); t.end();
}); });
}); });
// create a test machine (blocking) and output JSON // create a test machine (blocking) and output JSON
tt.test(' triton create', function (t) { tt.test(' setup: triton create', function (t) {
var argv = [ var argv = [
'create', 'create',
'-wj', '-wj',
@ -153,19 +97,8 @@ test('triton manage workflow', opts, function (tt) {
imgId, pkgId imgId, pkgId
]; ];
h.safeTriton(t, argv, function (stdout) { h.safeTriton(t, argv, function (err, stdout) {
// parse JSON response var lines = h.jsonStreamParse(stdout);
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();
}
instance = lines[1]; instance = lines[1];
t.equal(lines[0].id, lines[1].id, 'correct UUID given'); t.equal(lines[0].id, lines[1].id, 'correct UUID given');
t.equal(lines[0].metadata.foo, 'bar', 'foo metadata set'); t.equal(lines[0].metadata.foo, 'bar', 'foo metadata set');
@ -184,22 +117,13 @@ test('triton manage workflow', opts, function (tt) {
vasync.parallel({ vasync.parallel({
funcs: [ funcs: [
function (cb) { function (cb) {
h.safeTriton(t, ['instance', 'get', '-j', INST_ALIAS], h.safeTriton(t, ['instance', 'get', '-j', INST_ALIAS], cb);
function (stdout) {
cb(null, stdout);
});
}, },
function (cb) { function (cb) {
h.safeTriton(t, ['instance', 'get', '-j', uuid], h.safeTriton(t, ['instance', 'get', '-j', uuid], cb);
function (stdout) {
cb(null, stdout);
});
}, },
function (cb) { function (cb) {
h.safeTriton(t, ['instance', 'get', '-j', shortId], h.safeTriton(t, ['instance', 'get', '-j', shortId], cb);
function (stdout) {
cb(null, stdout);
});
} }
] ]
}, function (err, results) { }, 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 // have a way to know if the attempt failed or if it is just taking a
// really long time. // really long time.
tt.test(' triton delete', {timeout: 10 * 60 * 1000}, function (t) { 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(); t.end();
}); });
}); });
@ -240,7 +164,7 @@ test('triton manage workflow', opts, function (tt) {
// create a test machine (non-blocking) // create a test machine (non-blocking)
tt.test(' triton create', function (t) { tt.test(' triton create', function (t) {
h.safeTriton(t, ['create', '-jn', INST_ALIAS, imgId, pkgId], h.safeTriton(t, ['create', '-jn', INST_ALIAS, imgId, pkgId],
function (stdout) { function (err, stdout) {
// parse JSON response // parse JSON response
var lines = stdout.trim().split('\n'); var lines = stdout.trim().split('\n');
@ -263,7 +187,7 @@ test('triton manage workflow', opts, function (tt) {
// wait for the machine to start // wait for the machine to start
tt.test(' triton inst wait', function (t) { tt.test(' triton inst wait', function (t) {
h.safeTriton(t, ['inst', 'wait', instance.id], h.safeTriton(t, ['inst', 'wait', instance.id],
function (stdout) { function (err, stdout) {
// parse JSON response // parse JSON response
var lines = stdout.trim().split('\n'); var lines = stdout.trim().split('\n');
@ -280,8 +204,7 @@ test('triton manage workflow', opts, function (tt) {
// stop the machine // stop the machine
tt.test(' triton stop', function (t) { tt.test(' triton stop', function (t) {
h.safeTriton(t, ['stop', '-w', INST_ALIAS], h.safeTriton(t, ['stop', '-w', INST_ALIAS], function (err, stdout) {
function (stdout) {
t.ok(stdout.match(/^Stop instance/, 'correct stdout')); t.ok(stdout.match(/^Stop instance/, 'correct stdout'));
t.end(); t.end();
}); });
@ -290,11 +213,9 @@ test('triton manage workflow', opts, function (tt) {
// wait for the machine to stop // wait for the machine to stop
tt.test(' triton confirm stopped', function (t) { tt.test(' triton confirm stopped', function (t) {
h.safeTriton(t, {json: true, args: ['inst', 'get', '-j', INST_ALIAS]}, h.safeTriton(t, {json: true, args: ['inst', 'get', '-j', INST_ALIAS]},
function (d) { function (err, d) {
instance = d; instance = d;
t.equal(d.state, 'stopped', 'machine stopped'); t.equal(d.state, 'stopped', 'machine stopped');
t.end(); t.end();
}); });
}); });
@ -302,7 +223,7 @@ test('triton manage workflow', opts, function (tt) {
// start the machine // start the machine
tt.test(' triton start', function (t) { tt.test(' triton start', function (t) {
h.safeTriton(t, ['start', '-w', INST_ALIAS], h.safeTriton(t, ['start', '-w', INST_ALIAS],
function (stdout) { function (err, stdout) {
t.ok(stdout.match(/^Start instance/, 'correct stdout')); t.ok(stdout.match(/^Start instance/, 'correct stdout'));
t.end(); t.end();
}); });
@ -311,7 +232,7 @@ test('triton manage workflow', opts, function (tt) {
// wait for the machine to start // wait for the machine to start
tt.test(' confirm running', function (t) { tt.test(' confirm running', function (t) {
h.safeTriton(t, {json: true, args: ['inst', 'get', '-j', INST_ALIAS]}, h.safeTriton(t, {json: true, args: ['inst', 'get', '-j', INST_ALIAS]},
function (d) { function (err, d) {
instance = d; instance = d;
t.equal(d.state, 'running', 'machine running'); t.equal(d.state, 'running', 'machine running');
t.end(); t.end();
@ -320,7 +241,7 @@ test('triton manage workflow', opts, function (tt) {
// remove test instance // remove test instance
tt.test(' cleanup (triton delete)', function (t) { 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(); 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) { test('triton profiles (read only)', function (tt) {
tt.test(' triton profile get env', function (t) { tt.test(' triton profile get env', function (t) {
h.safeTriton(t, {json: true, args: ['profile', 'get', '-j', 'env']}, h.safeTriton(t, {json: true, args: ['profile', 'get', '-j', 'env']},
function (p) { function (err, p) {
t.equal(p.account, h.CONFIG.profile.account, t.equal(p.account, h.CONFIG.profile.account,
'env account correct'); 'env account correct');
@ -55,8 +55,7 @@ test('triton profiles (read only)', function (tt) {
test('triton profiles (read/write)', opts, function (tt) { test('triton profiles (read/write)', opts, function (tt) {
tt.test(' triton profile create', function (t) { tt.test(' triton profile create', function (t) {
h.safeTriton(t, ['profile', 'create', '-f', PROFILE_FILE], h.safeTriton(t, ['profile', 'create', '-f', PROFILE_FILE],
function (stdout) { function (err, stdout) {
t.ok(stdout.match(/^Saved profile/), 'stdout correct'); t.ok(stdout.match(/^Saved profile/), 'stdout correct');
t.end(); t.end();
}); });
@ -65,17 +64,16 @@ test('triton profiles (read/write)', opts, function (tt) {
tt.test(' triton profile get', function (t) { tt.test(' triton profile get', function (t) {
h.safeTriton(t, h.safeTriton(t,
{json: true, args: ['profile', 'get', '-j', PROFILE_DATA.name]}, {json: true, args: ['profile', 'get', '-j', PROFILE_DATA.name]},
function (p) { function (err, p) {
t.deepEqual(p, PROFILE_DATA, 'profile matched'); t.deepEqual(p, PROFILE_DATA, 'profile matched');
t.end(); t.end();
}); });
}); });
tt.test(' triton profile delete', function (t) { tt.test(' triton profile delete', function (t) {
h.safeTriton(t, ['profile', 'delete', '-f', PROFILE_DATA.name], h.safeTriton(t, ['profile', 'delete', '-f', PROFILE_DATA.name],
function (stdout) { function (err, stdout) {
t.ok(stdout.match(/^Deleted profile/), 'stdout correct'); t.ok(stdout.match(/^Deleted profile/), 'stdout correct');
t.end(); 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 assert = require('assert-plus');
var f = require('util').format; var f = require('util').format;
var path = require('path'); var path = require('path');
var tabula = require('tabula');
var common = require('../../lib/common'); var common = require('../../lib/common');
var mod_triton = require('../../'); var mod_triton = require('../../');
@ -124,29 +125,28 @@ function triton(args, opts, cb) {
/* /*
* triton wrapper that: * `triton ...` wrapper that:
* - tests no error is present * - tests non-error exit
* - tests stdout is not empty
* - tests stderr is empty * - 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 {Tape} t - tape test object
* @param {Object|Array} opts - options object * @param {Object|Array} opts - options object, or just the `triton` args
* @param {Function} cb - callback called like "cb(stdout)" * @param {Function} cb - `function (err, stdout)`
*/ */
function safeTriton(t, opts, cb) { function safeTriton(t, opts, cb) {
assert.object(t, 't');
if (Array.isArray(opts)) { if (Array.isArray(opts)) {
opts = {args: 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(' '))); t.comment(f('running: triton %s', opts.args.join(' ')));
triton(opts.args, function (err, stdout, stderr) { triton(opts.args, function (err, stdout, stderr) {
t.error(err, 'no error running child process'); t.error(err, 'no error running child process');
t.equal(stderr, '', 'no stderr produced'); t.equal(stderr, '', 'no stderr produced');
t.notEqual(stdout, '', 'stdout produced');
if (opts.json) { if (opts.json) {
try { try {
stdout = JSON.parse(stdout); stdout = JSON.parse(stdout);
@ -155,13 +155,96 @@ function safeTriton(t, opts, cb) {
return; return;
} }
} }
cb(err, stdout);
if (!err && stdout && !stderr)
cb(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. * Create a TritonApi client using the CLI.
*/ */
@ -230,5 +313,9 @@ module.exports = {
safeTriton: safeTriton, safeTriton: safeTriton,
createClient: createClient, createClient: createClient,
createMachine: createMachine, createMachine: createMachine,
getTestImg: getTestImg,
getTestPkg: getTestPkg,
jsonStreamParse: jsonStreamParse,
ifErr: testcommon.ifErr 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'); var assert = require('assert-plus');
@ -17,7 +17,8 @@ var cmdln = require('cmdln');
var format = require('util').format; var format = require('util').format;
var test = require('tape'); var test = require('tape');
var tagsFromOpts = require('../../lib/metadataandtags').tagsFromOpts; var tagsFromCreateOpts
= require('../../lib/metadataandtags').tagsFromCreateOpts;
// ---- globals // ---- globals
@ -140,7 +141,7 @@ var cases = [
// ---- test driver // ---- test driver
test('tagsFromOpts', function (tt) { test('tagsFromCreateOpts', function (tt) {
cases.forEach(function (c, num) { cases.forEach(function (c, num) {
var testName = format('case %d: %s', num, c.argv.join(' ')); var testName = format('case %d: %s', num, c.argv.join(' '));
tt.test(testName, function (t) { tt.test(testName, function (t) {
@ -157,7 +158,7 @@ test('tagsFromOpts', function (tt) {
stderrChunks.push(s); stderrChunks.push(s);
}; };
tagsFromOpts(opts, log, function (err, tags) { tagsFromCreateOpts(opts, log, function (err, tags) {
// Restore stderr. // Restore stderr.
process.stderr.write = _oldStderrWrite; process.stderr.write = _oldStderrWrite;
var stderr = stderrChunks.join(''); 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

@ -26,6 +26,7 @@ import codecs
import logging import logging
import optparse import optparse
import json import json
import time
@ -159,7 +160,8 @@ def cutarelease(project_name, version_files, dry_run=False):
curr_tags = set(t for t in _capture_stdout(["git", "tag", "-l"]).split('\n') if t) curr_tags = set(t for t in _capture_stdout(["git", "tag", "-l"]).split('\n') if t)
if not dry_run and version not in curr_tags: if not dry_run and version not in curr_tags:
log.info("tag the release") log.info("tag the release")
run('git tag -a "%s" -m "version %s"' % (version, version)) date = time.strftime("%Y-%m-%d")
run('git tag -a "%s" -m "version %s (%s)"' % (version, version, date))
run('git push --tags') run('git push --tags')
# Optionally release. # Optionally release.

View File

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