Integration test config handling improvements. Add 'ResourceNotFound' error and fine tune exit status handling.

Fixes #37.
This commit is contained in:
Trent Mick 2015-10-06 23:09:52 -07:00
parent d79083b9a1
commit 8ece8d0024
14 changed files with 356 additions and 150 deletions

2
.gitignore vendored
View File

@ -1,5 +1,5 @@
/node_modules
/tmp
/test/config.json
/test/*.json
/npm-debug.log
/triton-*.tgz

View File

@ -1,8 +1,16 @@
# node-triton changelog
## 2.0.1 (not yet released)
## 2.1.0 (not yet released)
(nothing yet)
- Errors and exit status: Change `Usage` errors to always have an exit status
of `2` (per common practice in at least some tooling). Add `ResourceNotFound`
error for `triton {instance,package,image,network}` with exit status `3`.
This can help tooling (e.g. the test suite uses this in one place). Add
`triton help` docs on exit status.
- Test suite: Integration tests always require a config file
(either `$TRITON_TEST_CONFIG` path or "test/config.json").
Drop the other `TRITON_TEST_*` envvars.
## 2.0.0

View File

@ -269,29 +269,35 @@ section.
## Test suite
node-triton has both unit tests (`make test-unit`) and integration tests (`make
test-integration`). Integration tests require either:
test-integration`). Integration tests require a config file, by default at
"test/config.json". For example:
1. environment variables like:
$ cat test/config.json
{
"profileName": "east3b",
"destructiveAllowed": true,
"image": "minimal-64",
"package": "t4-standard-128M"
}
TRITON_TEST_PROFILE=<Triton profile name>
TRITON_TEST_DESTRUCTIVE_ALLOWED=1 # Optional
See "test/config.json.sample" for a description of all config vars. Minimally
just a "profileName" or "profile" is required.
2. or, a "./test/config.json" like this:
Run all tests:
{
"url": "<CloudAPI URL>",
"account": "<account>",
"keyId": "<ssh key fingerprint>",
"insecure": true|false, // optional
"destructiveAllowed": true|false // optional
}
make test
For example, a possible run could be:
You can use `TRITON_TEST_CONFIG` to override the test file, e.g.:
TRITON_TEST_PROFILE=coal TRITON_TEST_DESTRUCTIVE_ALLOWED=1 make test
$ cat test/coal.json
{
"profileName": "coal",
"destructiveAllowed": true
}
$ TRITON_TEST_CONFIG=test/coal.json make test
Where "coal" here refers to a development Triton (a.k.a SDC) ["Cloud On A
Laptop"](https://github.com/joyent/sdc#getting-started).
where "coal" here refers to a development Triton (a.k.a SDC) ["Cloud On A
Laptop"](https://github.com/joyent/sdc#getting-started) standup.
## License

View File

@ -172,7 +172,17 @@ function CLI() {
{ group: 'Networks' },
'networks',
'network'
]
],
helpBody: [
/* BEGIN JSSTYLED */
'Exit Status:',
' 0 Successful completion.',
' 1 An error occurred.',
' 2 Usage error.',
' 3 "ResourceNotFound" error. Returned when an instance, image,',
' package, etc. with the given name or id is not found.'
/* END JSSTYLED */
].join('\n')
});
}
util.inherits(CLI, Cmdln);

View File

@ -46,12 +46,17 @@ do_instance.options = [
}
];
do_instance.help = (
'Show a single instance.\n'
/* BEGIN JSSTYLED */
'Get an instance.\n'
+ '\n'
+ 'Usage:\n'
+ ' {{name}} instance <alias|id>\n'
+ '\n'
+ '{{options}}'
+ '\n'
+ 'Note: Currently this dumps prettified JSON by default. That might change\n'
+ 'in the future. Use "-j" to explicitly get JSON output.\n'
/* END JSSTYLED */
);
do_instance.aliases = ['inst'];

View File

@ -14,7 +14,8 @@ var util = require('util'),
format = util.format;
var assert = require('assert-plus');
var verror = require('verror'),
VError = verror.VError;
VError = verror.VError,
WError = verror.WError;
@ -24,7 +25,7 @@ var verror = require('verror'),
* Base error. Instances will always have a string `message` and
* a string `code` (a CamelCase string).
*/
function _TritonBaseError(options) {
function _TritonBaseVError(options) {
assert.object(options, 'options');
assert.string(options.message, 'options.message');
assert.optionalString(options.code, 'options.code');
@ -43,7 +44,33 @@ function _TritonBaseError(options) {
self[k] = options[k];
});
}
util.inherits(_TritonBaseError, VError);
util.inherits(_TritonBaseVError, VError);
/*
* Base error class that doesn't include a 'cause' message in its message.
* This is useful in cases where we are wrapping CloudAPI errors with
* onces that should *replace* the CloudAPI error message.
*/
function _TritonBaseWError(options) {
assert.object(options, 'options');
assert.string(options.message, 'options.message');
assert.optionalString(options.code, 'options.code');
assert.optionalObject(options.cause, 'options.cause');
assert.optionalNumber(options.statusCode, 'options.statusCode');
var self = this;
var args = [];
if (options.cause) args.push(options.cause);
args.push(options.message);
WError.apply(this, args);
var extra = Object.keys(options).filter(
function (k) { return ['cause', 'message'].indexOf(k) === -1; });
extra.forEach(function (k) {
self[k] = options[k];
});
}
util.inherits(_TritonBaseWError, WError);
/*
* A generic (i.e. a cop out) code-less error.
@ -54,13 +81,13 @@ function TritonError(cause, message) {
cause = undefined;
}
assert.string(message);
_TritonBaseError.call(this, {
_TritonBaseVError.call(this, {
cause: cause,
message: message,
exitStatus: 1
});
}
util.inherits(TritonError, _TritonBaseError);
util.inherits(TritonError, _TritonBaseVError);
function InternalError(cause, message) {
@ -69,14 +96,14 @@ function InternalError(cause, message) {
cause = undefined;
}
assert.string(message);
_TritonBaseError.call(this, {
_TritonBaseVError.call(this, {
cause: cause,
message: message,
code: 'InternalError',
exitStatus: 1
});
}
util.inherits(InternalError, _TritonBaseError);
util.inherits(InternalError, _TritonBaseVError);
/**
@ -88,14 +115,14 @@ function ConfigError(cause, message) {
cause = undefined;
}
assert.string(message);
_TritonBaseError.call(this, {
_TritonBaseVError.call(this, {
cause: cause,
message: message,
code: 'Config',
exitStatus: 1
});
}
util.inherits(ConfigError, _TritonBaseError);
util.inherits(ConfigError, _TritonBaseVError);
/**
@ -107,28 +134,28 @@ function UsageError(cause, message) {
cause = undefined;
}
assert.string(message);
_TritonBaseError.call(this, {
_TritonBaseVError.call(this, {
cause: cause,
message: message,
code: 'Usage',
exitStatus: 1
exitStatus: 2
});
}
util.inherits(UsageError, _TritonBaseError);
util.inherits(UsageError, _TritonBaseVError);
/**
* An error signing a request.
*/
function SigningError(cause) {
_TritonBaseError.call(this, {
_TritonBaseVError.call(this, {
cause: cause,
message: 'error signing request',
code: 'Signing',
exitStatus: 1
});
}
util.inherits(SigningError, _TritonBaseError);
util.inherits(SigningError, _TritonBaseVError);
/**
@ -138,14 +165,32 @@ function SelfSignedCertError(cause, url) {
var msg = format('could not access CloudAPI %s because it uses a ' +
'self-signed TLS certificate and your current profile is not ' +
'configured for insecure access', url);
_TritonBaseError.call(this, {
_TritonBaseVError.call(this, {
cause: cause,
message: msg,
code: 'SelfSignedCert',
exitStatus: 1
});
}
util.inherits(SelfSignedCertError, _TritonBaseError);
util.inherits(SelfSignedCertError, _TritonBaseVError);
/**
* A resource (instance, image, ...) was not found.
*/
function ResourceNotFoundError(cause, msg) {
if (msg === undefined) {
msg = cause;
cause = undefined;
}
_TritonBaseWError.call(this, {
cause: cause,
message: msg,
code: 'ResourceNotFound',
exitStatus: 3
});
}
util.inherits(ResourceNotFoundError, _TritonBaseWError);
/**
@ -158,7 +203,7 @@ function MultiError(errs) {
var err = errs[i];
lines.push(format(' error (%s): %s', err.code, err.message));
}
_TritonBaseError.call(this, {
_TritonBaseVError.call(this, {
cause: errs[0],
message: lines.join('\n'),
code: 'MultiError',
@ -166,7 +211,7 @@ function MultiError(errs) {
});
}
MultiError.description = 'Multiple errors.';
util.inherits(MultiError, _TritonBaseError);
util.inherits(MultiError, _TritonBaseVError);
@ -179,6 +224,7 @@ module.exports = {
UsageError: UsageError,
SigningError: SigningError,
SelfSignedCertError: SelfSignedCertError,
ResourceNotFoundError: ResourceNotFoundError,
MultiError: MultiError
};
// vim: set softtabstop=4 shiftwidth=4:

View File

@ -290,6 +290,10 @@ TritonApi.prototype.getImage = function getImage(opts, cb) {
}
self.cloudapi.getImage({id: opts.name}, function (err, _img) {
img = _img;
if (err && err.restCode === 'ResourceNotFound') {
err = new errors.ResourceNotFoundError(err,
format('image with id %s was not found', name));
}
next(err);
});
}
@ -298,7 +302,8 @@ TritonApi.prototype.getImage = function getImage(opts, cb) {
if (err) {
cb(err);
} else if (img.state !== 'active') {
cb(new Error(format('image %s is not active', opts.name)));
cb(new errors.TritonError(
format('image %s is not active', opts.name)));
} else {
cb(null, img);
}
@ -340,10 +345,11 @@ TritonApi.prototype.getImage = function getImage(opts, cb) {
} else if (shortIdMatches.length === 1) {
cb(null, shortIdMatches[0]);
} else if (shortIdMatches.length === 0) {
cb(new Error(format(
cb(new errors.ResourceNotFoundError(format(
'no image with name or short id "%s" was found', name)));
} else {
cb(new Error(format('no image with name "%s" was found '
cb(new errors.ResourceNotFoundError(
format('no image with name "%s" was found '
+ 'and "%s" is an ambiguous short id', name)));
}
});
@ -363,9 +369,14 @@ TritonApi.prototype.getPackage = function getPackage(name, cb) {
if (common.isUUID(name)) {
this.cloudapi.getPackage({id: name}, function (err, pkg) {
if (err) {
if (err.restCode === 'ResourceNotFound') {
err = new errors.ResourceNotFoundError(err,
format('package with id %s was not found', name));
}
cb(err);
} else if (!pkg.active) {
cb(new Error(format('package %s is not active', name)));
cb(new errors.TritonError(
format('package %s is not active', name)));
} else {
cb(null, pkg);
}
@ -391,16 +402,17 @@ TritonApi.prototype.getPackage = function getPackage(name, cb) {
if (nameMatches.length === 1) {
cb(null, nameMatches[0]);
} else if (nameMatches.length > 1) {
cb(new Error(format(
cb(new errors.TritonError(format(
'package name "%s" is ambiguous: matches %d packages',
name, nameMatches.length)));
} else if (shortIdMatches.length === 1) {
cb(null, shortIdMatches[0]);
} else if (shortIdMatches.length === 0) {
cb(new Error(format(
cb(new errors.ResourceNotFoundError(format(
'no package with name or short id "%s" was found', name)));
} else {
cb(new Error(format('no package with name "%s" was found '
cb(new errors.ResourceNotFoundError(
format('no package with name "%s" was found '
+ 'and "%s" is an ambiguous short id', name)));
}
});
@ -420,6 +432,11 @@ TritonApi.prototype.getNetwork = function getNetwork(name, cb) {
if (common.isUUID(name)) {
this.cloudapi.getNetwork(name, function (err, net) {
if (err) {
if (err.restCode === 'ResourceNotFound') {
// Wrap with *our* ResourceNotFound for exitStatus=3.
err = new errors.ResourceNotFoundError(err,
format('network with id %s was not found', name));
}
cb(err);
} else {
cb(null, net);
@ -446,16 +463,17 @@ TritonApi.prototype.getNetwork = function getNetwork(name, cb) {
if (nameMatches.length === 1) {
cb(null, nameMatches[0]);
} else if (nameMatches.length > 1) {
cb(new Error(format(
cb(new errors.TritonError(format(
'network name "%s" is ambiguous: matches %d networks',
name, nameMatches.length)));
} else if (shortIdMatches.length === 1) {
cb(null, shortIdMatches[0]);
} else if (shortIdMatches.length === 0) {
cb(new Error(format(
cb(new errors.ResourceNotFoundError(format(
'no network with name or short id "%s" was found', name)));
} else {
cb(new Error(format('no network with name "%s" was found '
cb(new errors.ResourceNotFoundError(format(
'no network with name "%s" was found '
+ 'and "%s" is an ambiguous short id', name)));
}
});
@ -493,6 +511,11 @@ TritonApi.prototype.getInstance = function getInstance(name, cb) {
}
self.cloudapi.getMachine(uuid, function (err, inst_) {
inst = inst_;
if (err && err.restCode === 'ResourceNotFound') {
// The CloudApi 404 error message sucks: "VM not found".
err = new errors.ResourceNotFoundError(err,
format('instance with id %s was not found', name));
}
next(err);
});
},
@ -534,7 +557,7 @@ TritonApi.prototype.getInstance = function getInstance(name, cb) {
while ((candidate = s.read()) !== null) {
if (candidate.id.slice(0, shortId.length) === shortId) {
if (match) {
return nextOnce(new Error(
return nextOnce(new errors.TritonError(
'instance short id "%s" is ambiguous',
shortId));
} else {
@ -556,7 +579,7 @@ TritonApi.prototype.getInstance = function getInstance(name, cb) {
} else if (inst) {
cb(null, inst);
} else {
cb(new Error(format(
cb(new errors.ResourceNotFoundError(format(
'no instance with name or short id "%s" was found', name)));
}
});

View File

@ -1,7 +1,7 @@
{
"name": "triton",
"description": "Joyent Triton CLI and client (https://www.joyent.com/triton)",
"version": "2.0.1",
"version": "2.1.0",
"author": "Joyent (joyent.com)",
"dependencies": {
"assert-plus": "0.1.5",

23
test/config.json.sample Normal file
View File

@ -0,0 +1,23 @@
{
// This is JSON so, obviously, you need to turf the comment lines.
// Minimally you must define *one* of "profileName" ...
"profileName": "env",
// ... or "profile":
"profile": {
"url": "https://us-east-3b.api.joyent.com",
"account": "joe.blow",
"keyId": "de:e7:73:32:b0:ab:31:cd:72:ef:9f:62:ca:58:a2:ec",
"insecure": false
}
// Optional. Set this to allow the parts of the test suite
// that create and destroy resources: instances, images, networks, etc.
// This is essentially the "safe" guard.
"destructiveAllowed": true
// The params used for test provisions. By default the tests use:
// the smallest RAM package, the latest base* image.
"package": "<package name or uuid>",
"image": "<image uuid, name or name@version>"
}

View File

@ -47,7 +47,8 @@ test('triton account', function (tt) {
h.triton('account', function (err, stdout, stderr) {
if (h.ifErr(t, err))
return t.end();
t.ok(new RegExp('^login: ' + h.CONFIG.account, 'm').test(stdout));
t.ok(new RegExp(
'^login: ' + h.CONFIG.profile.account, 'm').test(stdout));
t.end();
});
});
@ -57,7 +58,7 @@ test('triton account', function (tt) {
if (h.ifErr(t, err))
return t.end();
var account = JSON.parse(stdout);
t.equal(account.login, h.CONFIG.account, 'account.login');
t.equal(account.login, h.CONFIG.profile.account, 'account.login');
t.end();
});
});

View File

@ -13,17 +13,18 @@
*/
var f = require('util').format;
var os = require('os');
var tabula = require('tabula');
var test = require('tape');
var vasync = require('vasync');
var h = require('./helpers');
var test = require('tape');
var common = require('../../lib/common');
var h = require('./helpers');
var VM_ALIAS = 'node-triton-test-vm-1';
var VM_IMAGE = 'base-64@15.2.0';
var VM_PACKAGE = 't4-standard-128M';
// --- globals
var INST_ALIAS = f('node-triton-test-%s-vm1', os.hostname());
var opts = {
skip: !h.CONFIG.destructiveAllowed
@ -33,6 +34,21 @@ var opts = {
var instance;
// --- internal support stuff
function _jsonStreamParse(s) {
var results = [];
var lines = s.split('\n');
for (var i = 0; i < lines.length; i++) {
var line = lines[i].trim();
if (line) {
results.push(JSON.parse(line));
}
}
return results;
}
// --- Tests
if (opts.skip) {
@ -40,15 +56,93 @@ if (opts.skip) {
console.error('** set "destructiveAllowed" to enable');
}
test('triton manage workflow', opts, function (tt) {
tt.comment('using test profile:');
tt.comment('Test config:');
Object.keys(h.CONFIG).forEach(function (key) {
var value = h.CONFIG[key];
tt.comment(f(' %s: %s', key, value));
tt.comment(f('- %s: %j', key, value));
});
tt.test(' cleanup existing inst with alias ' + INST_ALIAS, function (t) {
h.triton(['inst', '-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, err);
t.end();
}
} else {
var inst = JSON.parse(stdout);
h.safeTriton(t, ['delete', '-w', inst.id], function () {
t.ok(true, 'deleted inst ' + inst.id);
t.end();
});
}
});
});
var imgId;
tt.test(' find image to use', function (t) {
if (h.CONFIG.image) {
imgId = h.CONFIG.image;
t.ok(imgId, 'image from config: ' + imgId);
t.end();
return;
}
var candidateImageNames = {
'base-64-lts': true,
'base-64': true,
'minimal-64': true,
'base-32-lts': true,
'base-32': true,
'minimal-32': true,
'base': true
};
h.safeTriton(t, ['imgs', '-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();
});
});
var pkgId;
tt.test(' find package to use', function (t) {
if (h.CONFIG.package) {
pkgId = h.CONFIG.package;
t.ok(pkgId, 'package from config: ' + pkgId);
t.end();
return;
}
h.safeTriton(t, ['pkgs', '-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();
});
});
// create a test machine (blocking) and output JSON
tt.test('triton create', function (t) {
h.safeTriton(t, ['create', '-wjn', VM_ALIAS, VM_IMAGE, VM_PACKAGE],
tt.test(' triton create', function (t) {
h.safeTriton(t, ['create', '-wjn', INST_ALIAS, imgId, pkgId],
function (stdout) {
// parse JSON response
@ -72,13 +166,13 @@ test('triton manage workflow', opts, function (tt) {
});
// test `triton instance -j` with the UUID, the alias, and the short ID
tt.test('triton instance', function (t) {
tt.test(' triton instance', function (t) {
var uuid = instance.id;
var shortId = common.uuidToShortId(uuid);
vasync.parallel({
funcs: [
function (cb) {
h.safeTriton(t, ['instance', '-j', VM_ALIAS],
h.safeTriton(t, ['instance', '-j', INST_ALIAS],
function (stdout) {
cb(null, stdout);
});
@ -119,15 +213,15 @@ test('triton manage workflow', opts, function (tt) {
});
// remove instance
tt.test('triton delete', function (t) {
tt.test(' triton delete', function (t) {
h.safeTriton(t, ['delete', '-w', instance.id], function (stdout) {
t.end();
});
});
// create a test machine (non-blocking)
tt.test('triton create', function (t) {
h.safeTriton(t, ['create', '-jn', VM_ALIAS, VM_IMAGE, VM_PACKAGE],
tt.test(' triton create', function (t) {
h.safeTriton(t, ['create', '-jn', INST_ALIAS, imgId, pkgId],
function (stdout) {
// parse JSON response
@ -149,7 +243,7 @@ test('triton manage workflow', opts, function (tt) {
});
// wait for the machine to start
tt.test('triton wait', function (t) {
tt.test(' triton wait', function (t) {
h.safeTriton(t, ['wait', instance.id],
function (stdout) {
@ -167,8 +261,8 @@ test('triton manage workflow', opts, function (tt) {
});
// stop the machine
tt.test('triton stop', function (t) {
h.safeTriton(t, ['stop', '-w', VM_ALIAS],
tt.test(' triton stop', function (t) {
h.safeTriton(t, ['stop', '-w', INST_ALIAS],
function (stdout) {
t.ok(stdout.match(/^Stop instance/, 'correct stdout'));
t.end();
@ -176,8 +270,8 @@ test('triton manage workflow', opts, function (tt) {
});
// wait for the machine to stop
tt.test('triton confirm stopped', function (t) {
h.safeTriton(t, {json: true, args: ['instance', '-j', VM_ALIAS]},
tt.test(' triton confirm stopped', function (t) {
h.safeTriton(t, {json: true, args: ['instance', '-j', INST_ALIAS]},
function (d) {
instance = d;
@ -188,8 +282,8 @@ test('triton manage workflow', opts, function (tt) {
});
// start the machine
tt.test('triton start', function (t) {
h.safeTriton(t, ['start', '-w', VM_ALIAS],
tt.test(' triton start', function (t) {
h.safeTriton(t, ['start', '-w', INST_ALIAS],
function (stdout) {
t.ok(stdout.match(/^Start instance/, 'correct stdout'));
t.end();
@ -197,20 +291,17 @@ test('triton manage workflow', opts, function (tt) {
});
// wait for the machine to start
tt.test('triton confirm running', function (t) {
h.safeTriton(t, {json: true, args: ['instance', '-j', VM_ALIAS]},
function (d) {
tt.test(' confirm running', function (t) {
h.safeTriton(t, {json: true, args: ['instance', '-j', INST_ALIAS]},
function (d) {
instance = d;
t.equal(d.state, 'running', 'machine running');
t.end();
});
});
// remove test instance
tt.test('triton cleanup (delete)', function (t) {
tt.test(' cleanup (triton delete)', function (t) {
h.safeTriton(t, ['delete', '-w', instance.id], function (stdout) {
t.end();
});

View File

@ -33,18 +33,15 @@ if (opts.skip) {
}
test('triton profiles (read only)', function (tt) {
tt.test('triton profile env', function (t) {
tt.test(' triton profile env', function (t) {
h.safeTriton(t, {json: true, args: ['profile', '-j', 'env']},
function (p) {
t.equal(p.account,
process.env.TRITON_ACCOUNT || process.env.SDC_ACCOUNT,
t.equal(p.account, h.CONFIG.profile.account,
'env account correct');
t.equal(p.keyId,
process.env.TRITON_KEY_ID || process.env.SDC_KEY_ID,
t.equal(p.keyId, h.CONFIG.profile.keyId,
'env keyId correct');
t.equal(p.url,
process.env.TRITON_URL || process.env.SDC_URL,
t.equal(p.url, h.CONFIG.profile.url,
'env url correct');
t.end();
@ -55,7 +52,7 @@ test('triton profiles (read only)', 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', '-a', PROFILE_FILE],
function (stdout) {
@ -64,7 +61,7 @@ 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,
{json: true, args: ['profile', '-j', PROFILE_DATA.name]},
function (p) {
@ -75,7 +72,7 @@ test('triton profiles (read/write)', opts, function (tt) {
});
});
tt.test('triton profile delete', function (t) {
tt.test(' triton profile delete', function (t) {
h.safeTriton(t, ['profile', '-df', PROFILE_DATA.name],
function (stdout) {

View File

@ -22,56 +22,54 @@ var mod_config = require('../../lib/config');
var testcommon = require('../lib/testcommon');
// --- globals
var CONFIG;
if (process.env.TRITON_TEST_PROFILE) {
CONFIG = mod_config.loadProfile({
configDir: path.join(process.env.HOME, '.triton'),
name: process.env.TRITON_TEST_PROFILE
});
CONFIG.destructiveAllowed = common.boolFromString(
process.env.TRITON_TEST_DESTRUCTIVE_ALLOWED);
} else {
try {
CONFIG = require('../config.json');
assert.object(CONFIG, 'test/config.json');
assert.string(CONFIG.url, 'test/config.json#url');
assert.string(CONFIG.account, 'test/config.json#account');
assert.string(CONFIG.keyId, 'test/config.json#keyId');
assert.optionalBool(CONFIG.insecure,
'test/config.json#insecure');
assert.optionalBool(CONFIG.destructiveAllowed,
'test/config.json#destructiveAllowed');
} catch (e) {
error('* * *');
error('node-triton integration tests require either:');
error('');
error('1. environment variables like:');
error('');
error(' TRITON_TEST_PROFILE=<Triton profile name>');
error(' TRITON_TEST_DESTRUCTIVE_ALLOWED=1 # Optional');
error('');
error('2. or, a "./test/config.json" like this:');
error('');
error(' {');
error(' "url": "<CloudAPI URL>",');
error(' "account": "<account>",');
error(' "keyId": "<ssh key fingerprint>",');
error(' "insecure": true|false, // optional');
error(' "destructiveAllowed": true|false // optional');
error(' }');
error('');
error('Note: This test suite will create machines, images, etc. ');
error('using this CloudAPI and account. While it will do its best');
error('to clean up all resources, running the test suite against');
error('a public cloud could *cost* you money. :)');
error('* * *');
throw e;
var configPath = process.env.TRITON_TEST_CONFIG
? path.resolve(process.cwd(), process.env.TRITON_TEST_CONFIG)
: path.resolve(__dirname, '..', 'config.json');
try {
CONFIG = require(configPath);
assert.object(CONFIG, configPath);
if (CONFIG.profile && CONFIG.profileName) {
throw new Error(
'cannot specify both "profile" and "profileName" in ' +
configPath);
} else if (CONFIG.profile) {
assert.string(CONFIG.profile.url, 'CONFIG.profile.url');
assert.string(CONFIG.profile.account, 'CONFIG.profile.account');
assert.string(CONFIG.profile.keyId, 'CONFIG.profile.keyId');
assert.optionalBool(CONFIG.profile.insecure,
'CONFIG.profile.insecure');
} else if (CONFIG.profileName) {
CONFIG.profile = mod_config.loadProfile({
configDir: path.join(process.env.HOME, '.triton'),
name: CONFIG.profileName
});
} else {
throw new Error('one of "profile" or "profileName" must be defined ' +
'in ' + configPath);
}
assert.optionalBool(CONFIG.destructiveAllowed,
'test/config.json#destructiveAllowed');
} catch (e) {
error('* * *');
error('node-triton integration tests require a config file. By default');
error('it looks for "test/config.json". Or you can set the');
error('TRITON_TEST_CONFIG envvar. E.g.:');
error('');
error(' TRITON_TEST_CONFIG=test/coal.json make test');
error('');
error('See "test/config.json.sample" for a starting point for a config.');
error('');
error('Warning: This test suite will create machines, images, etc. ');
error('using this CloudAPI and account. While it will do its best');
error('to clean up all resources, running the test suite against');
error('a public cloud could *cost* you money. :)');
error('* * *');
throw e;
}
if (CONFIG.insecure === undefined)
CONFIG.insecure = false;
if (CONFIG.profile.insecure === undefined)
CONFIG.profile.insecure = false;
if (CONFIG.destructiveAllowed === undefined)
CONFIG.destructiveAllowed = false;
@ -82,8 +80,6 @@ var LOG = require('../lib/log');
// --- internal support routines
/*
* Call the `triton` CLI with the given args.
*/
@ -101,10 +97,10 @@ function triton(args, cb) {
HOME: process.env.HOME,
SSH_AUTH_SOCK: process.env.SSH_AUTH_SOCK,
TRITON_PROFILE: 'env',
TRITON_URL: CONFIG.url,
TRITON_ACCOUNT: CONFIG.account,
TRITON_KEY_ID: CONFIG.keyId,
TRITON_TLS_INSECURE: CONFIG.insecure
TRITON_URL: CONFIG.profile.url,
TRITON_ACCOUNT: CONFIG.profile.account,
TRITON_KEY_ID: CONFIG.profile.keyId,
TRITON_TLS_INSECURE: CONFIG.profile.insecure
}
},
log: LOG

View File

@ -45,16 +45,16 @@ function execPlus(args, cb) {
args.log.trace({exec: true, command: command, execOpts: execOpts,
err: err, stdout: stdout, stderr: stderr}, 'exec done');
if (err) {
cb(
new VError(err,
var niceErr = new VError(err,
'%s:\n'
+ '\tcommand: %s\n'
+ '\texit status: %s\n'
+ '\tstdout:\n%s\n'
+ '\tstderr:\n%s',
args.errMsg || 'exec error', command, err.code,
stdout.trim(), stderr.trim()),
stdout, stderr);
stdout.trim(), stderr.trim());
niceErr.code = err.code;
cb(niceErr, stdout, stderr);
} else {
cb(null, stdout, stderr);
}