diff --git a/.gitignore b/.gitignore index 4bf1ad9..9950c21 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ /node_modules /tmp -/test/config.json +/test/*.json /npm-debug.log /triton-*.tgz diff --git a/CHANGES.md b/CHANGES.md index f034b09..278fbad 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -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 diff --git a/README.md b/README.md index 416069b..39f5d50 100644 --- a/README.md +++ b/README.md @@ -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_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": "", - "account": "", - "keyId": "", - "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 diff --git a/lib/cli.js b/lib/cli.js index b169def..9abb086 100644 --- a/lib/cli.js +++ b/lib/cli.js @@ -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); diff --git a/lib/do_instance.js b/lib/do_instance.js index 99ef4f1..127c053 100644 --- a/lib/do_instance.js +++ b/lib/do_instance.js @@ -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 \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']; diff --git a/lib/errors.js b/lib/errors.js index a7a8391..ea7834e 100644 --- a/lib/errors.js +++ b/lib/errors.js @@ -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: diff --git a/lib/tritonapi.js b/lib/tritonapi.js index 1590a50..70a20b7 100644 --- a/lib/tritonapi.js +++ b/lib/tritonapi.js @@ -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))); } }); diff --git a/package.json b/package.json index ae32e79..a5162e1 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/test/config.json.sample b/test/config.json.sample new file mode 100644 index 0000000..683bab8 --- /dev/null +++ b/test/config.json.sample @@ -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": "", + "image": "" +} diff --git a/test/integration/cli-account.test.js b/test/integration/cli-account.test.js index 9f176fa..fb4d8d7 100644 --- a/test/integration/cli-account.test.js +++ b/test/integration/cli-account.test.js @@ -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(); }); }); diff --git a/test/integration/cli-manage-workflow.test.js b/test/integration/cli-manage-workflow.test.js index 0e53580..f39ae28 100644 --- a/test/integration/cli-manage-workflow.test.js +++ b/test/integration/cli-manage-workflow.test.js @@ -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(); }); diff --git a/test/integration/cli-profiles.test.js b/test/integration/cli-profiles.test.js index 4fab4a3..df3547c 100644 --- a/test/integration/cli-profiles.test.js +++ b/test/integration/cli-profiles.test.js @@ -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) { diff --git a/test/integration/helpers.js b/test/integration/helpers.js index b590c16..2898411 100644 --- a/test/integration/helpers.js +++ b/test/integration/helpers.js @@ -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='); - error(' TRITON_TEST_DESTRUCTIVE_ALLOWED=1 # Optional'); - error(''); - error('2. or, a "./test/config.json" like this:'); - error(''); - error(' {'); - error(' "url": "",'); - error(' "account": "",'); - error(' "keyId": "",'); - 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 diff --git a/test/lib/testcommon.js b/test/lib/testcommon.js index e6534ba..4feeac3 100644 --- a/test/lib/testcommon.js +++ b/test/lib/testcommon.js @@ -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); }