Merge branch 'master' of https://github.com/joyent/node-triton into snapshot-fwrules
This commit is contained in:
commit
b7aa52dd0d
26
CHANGES.md
26
CHANGES.md
@ -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.
|
||||||
|
46
examples/example-list-instances.js
Executable file
46
examples/example-list-instances.js
Executable 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));
|
||||||
|
}
|
||||||
|
});
|
143
lib/cloudapi2.js
143
lib/cloudapi2.js
@ -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.
|
||||||
*
|
*
|
||||||
|
@ -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');
|
||||||
|
try {
|
||||||
var _user = JSON.parse(c);
|
var _user = JSON.parse(c);
|
||||||
var userConfig = 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;
|
||||||
|
if (configDir) {
|
||||||
config._configDir = 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(
|
||||||
|
@ -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;
|
||||||
|
@ -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)',
|
||||||
'',
|
'',
|
||||||
|
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;
|
@ -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'];
|
||||||
|
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
};
|
};
|
||||||
|
@ -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".
|
||||||
|
@ -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
|
||||||
};
|
};
|
||||||
|
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);
|
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.
|
||||||
*
|
*
|
||||||
|
@ -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",
|
||||||
|
@ -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(
|
||||||
|
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 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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -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();
|
||||||
|
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 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
|
||||||
};
|
};
|
||||||
|
@ -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('');
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -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.
|
||||||
|
@ -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
|
||||||
|
Reference in New Issue
Block a user