Integration test config handling improvements. Add 'ResourceNotFound' error and fine tune exit status handling.
Fixes #37.
This commit is contained in:
parent
d79083b9a1
commit
8ece8d0024
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,5 +1,5 @@
|
|||||||
/node_modules
|
/node_modules
|
||||||
/tmp
|
/tmp
|
||||||
/test/config.json
|
/test/*.json
|
||||||
/npm-debug.log
|
/npm-debug.log
|
||||||
/triton-*.tgz
|
/triton-*.tgz
|
||||||
|
12
CHANGES.md
12
CHANGES.md
@ -1,8 +1,16 @@
|
|||||||
# node-triton changelog
|
# 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
|
## 2.0.0
|
||||||
|
40
README.md
40
README.md
@ -269,29 +269,35 @@ section.
|
|||||||
## Test suite
|
## Test suite
|
||||||
|
|
||||||
node-triton has both unit tests (`make test-unit`) and integration tests (`make
|
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:
|
|
||||||
|
|
||||||
TRITON_TEST_PROFILE=<Triton profile name>
|
|
||||||
TRITON_TEST_DESTRUCTIVE_ALLOWED=1 # Optional
|
|
||||||
|
|
||||||
2. or, a "./test/config.json" like this:
|
|
||||||
|
|
||||||
|
$ cat test/config.json
|
||||||
{
|
{
|
||||||
"url": "<CloudAPI URL>",
|
"profileName": "east3b",
|
||||||
"account": "<account>",
|
"destructiveAllowed": true,
|
||||||
"keyId": "<ssh key fingerprint>",
|
"image": "minimal-64",
|
||||||
"insecure": true|false, // optional
|
"package": "t4-standard-128M"
|
||||||
"destructiveAllowed": true|false // optional
|
|
||||||
}
|
}
|
||||||
|
|
||||||
For example, a possible run could be:
|
See "test/config.json.sample" for a description of all config vars. Minimally
|
||||||
|
just a "profileName" or "profile" is required.
|
||||||
|
|
||||||
TRITON_TEST_PROFILE=coal TRITON_TEST_DESTRUCTIVE_ALLOWED=1 make test
|
Run all tests:
|
||||||
|
|
||||||
Where "coal" here refers to a development Triton (a.k.a SDC) ["Cloud On A
|
make test
|
||||||
Laptop"](https://github.com/joyent/sdc#getting-started).
|
|
||||||
|
You can use `TRITON_TEST_CONFIG` to override the test file, e.g.:
|
||||||
|
|
||||||
|
$ 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) standup.
|
||||||
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
12
lib/cli.js
12
lib/cli.js
@ -172,7 +172,17 @@ function CLI() {
|
|||||||
{ group: 'Networks' },
|
{ group: 'Networks' },
|
||||||
'networks',
|
'networks',
|
||||||
'network'
|
'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);
|
util.inherits(CLI, Cmdln);
|
||||||
|
@ -46,12 +46,17 @@ do_instance.options = [
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
do_instance.help = (
|
do_instance.help = (
|
||||||
'Show a single instance.\n'
|
/* BEGIN JSSTYLED */
|
||||||
|
'Get an instance.\n'
|
||||||
+ '\n'
|
+ '\n'
|
||||||
+ 'Usage:\n'
|
+ 'Usage:\n'
|
||||||
+ ' {{name}} instance <alias|id>\n'
|
+ ' {{name}} instance <alias|id>\n'
|
||||||
+ '\n'
|
+ '\n'
|
||||||
+ '{{options}}'
|
+ '{{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'];
|
do_instance.aliases = ['inst'];
|
||||||
|
@ -14,7 +14,8 @@ var util = require('util'),
|
|||||||
format = util.format;
|
format = util.format;
|
||||||
var assert = require('assert-plus');
|
var assert = require('assert-plus');
|
||||||
var verror = require('verror'),
|
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
|
* Base error. Instances will always have a string `message` and
|
||||||
* a string `code` (a CamelCase string).
|
* a string `code` (a CamelCase string).
|
||||||
*/
|
*/
|
||||||
function _TritonBaseError(options) {
|
function _TritonBaseVError(options) {
|
||||||
assert.object(options, 'options');
|
assert.object(options, 'options');
|
||||||
assert.string(options.message, 'options.message');
|
assert.string(options.message, 'options.message');
|
||||||
assert.optionalString(options.code, 'options.code');
|
assert.optionalString(options.code, 'options.code');
|
||||||
@ -43,7 +44,33 @@ function _TritonBaseError(options) {
|
|||||||
self[k] = options[k];
|
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.
|
* A generic (i.e. a cop out) code-less error.
|
||||||
@ -54,13 +81,13 @@ function TritonError(cause, message) {
|
|||||||
cause = undefined;
|
cause = undefined;
|
||||||
}
|
}
|
||||||
assert.string(message);
|
assert.string(message);
|
||||||
_TritonBaseError.call(this, {
|
_TritonBaseVError.call(this, {
|
||||||
cause: cause,
|
cause: cause,
|
||||||
message: message,
|
message: message,
|
||||||
exitStatus: 1
|
exitStatus: 1
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
util.inherits(TritonError, _TritonBaseError);
|
util.inherits(TritonError, _TritonBaseVError);
|
||||||
|
|
||||||
|
|
||||||
function InternalError(cause, message) {
|
function InternalError(cause, message) {
|
||||||
@ -69,14 +96,14 @@ function InternalError(cause, message) {
|
|||||||
cause = undefined;
|
cause = undefined;
|
||||||
}
|
}
|
||||||
assert.string(message);
|
assert.string(message);
|
||||||
_TritonBaseError.call(this, {
|
_TritonBaseVError.call(this, {
|
||||||
cause: cause,
|
cause: cause,
|
||||||
message: message,
|
message: message,
|
||||||
code: 'InternalError',
|
code: 'InternalError',
|
||||||
exitStatus: 1
|
exitStatus: 1
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
util.inherits(InternalError, _TritonBaseError);
|
util.inherits(InternalError, _TritonBaseVError);
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -88,14 +115,14 @@ function ConfigError(cause, message) {
|
|||||||
cause = undefined;
|
cause = undefined;
|
||||||
}
|
}
|
||||||
assert.string(message);
|
assert.string(message);
|
||||||
_TritonBaseError.call(this, {
|
_TritonBaseVError.call(this, {
|
||||||
cause: cause,
|
cause: cause,
|
||||||
message: message,
|
message: message,
|
||||||
code: 'Config',
|
code: 'Config',
|
||||||
exitStatus: 1
|
exitStatus: 1
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
util.inherits(ConfigError, _TritonBaseError);
|
util.inherits(ConfigError, _TritonBaseVError);
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -107,28 +134,28 @@ function UsageError(cause, message) {
|
|||||||
cause = undefined;
|
cause = undefined;
|
||||||
}
|
}
|
||||||
assert.string(message);
|
assert.string(message);
|
||||||
_TritonBaseError.call(this, {
|
_TritonBaseVError.call(this, {
|
||||||
cause: cause,
|
cause: cause,
|
||||||
message: message,
|
message: message,
|
||||||
code: 'Usage',
|
code: 'Usage',
|
||||||
exitStatus: 1
|
exitStatus: 2
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
util.inherits(UsageError, _TritonBaseError);
|
util.inherits(UsageError, _TritonBaseVError);
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An error signing a request.
|
* An error signing a request.
|
||||||
*/
|
*/
|
||||||
function SigningError(cause) {
|
function SigningError(cause) {
|
||||||
_TritonBaseError.call(this, {
|
_TritonBaseVError.call(this, {
|
||||||
cause: cause,
|
cause: cause,
|
||||||
message: 'error signing request',
|
message: 'error signing request',
|
||||||
code: 'Signing',
|
code: 'Signing',
|
||||||
exitStatus: 1
|
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 ' +
|
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);
|
||||||
_TritonBaseError.call(this, {
|
_TritonBaseVError.call(this, {
|
||||||
cause: cause,
|
cause: cause,
|
||||||
message: msg,
|
message: msg,
|
||||||
code: 'SelfSignedCert',
|
code: 'SelfSignedCert',
|
||||||
exitStatus: 1
|
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];
|
var err = errs[i];
|
||||||
lines.push(format(' error (%s): %s', err.code, err.message));
|
lines.push(format(' error (%s): %s', err.code, err.message));
|
||||||
}
|
}
|
||||||
_TritonBaseError.call(this, {
|
_TritonBaseVError.call(this, {
|
||||||
cause: errs[0],
|
cause: errs[0],
|
||||||
message: lines.join('\n'),
|
message: lines.join('\n'),
|
||||||
code: 'MultiError',
|
code: 'MultiError',
|
||||||
@ -166,7 +211,7 @@ function MultiError(errs) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
MultiError.description = 'Multiple errors.';
|
MultiError.description = 'Multiple errors.';
|
||||||
util.inherits(MultiError, _TritonBaseError);
|
util.inherits(MultiError, _TritonBaseVError);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -179,6 +224,7 @@ module.exports = {
|
|||||||
UsageError: UsageError,
|
UsageError: UsageError,
|
||||||
SigningError: SigningError,
|
SigningError: SigningError,
|
||||||
SelfSignedCertError: SelfSignedCertError,
|
SelfSignedCertError: SelfSignedCertError,
|
||||||
|
ResourceNotFoundError: ResourceNotFoundError,
|
||||||
MultiError: MultiError
|
MultiError: MultiError
|
||||||
};
|
};
|
||||||
// vim: set softtabstop=4 shiftwidth=4:
|
// vim: set softtabstop=4 shiftwidth=4:
|
||||||
|
@ -290,6 +290,10 @@ TritonApi.prototype.getImage = function getImage(opts, cb) {
|
|||||||
}
|
}
|
||||||
self.cloudapi.getImage({id: opts.name}, function (err, _img) {
|
self.cloudapi.getImage({id: opts.name}, function (err, _img) {
|
||||||
img = _img;
|
img = _img;
|
||||||
|
if (err && err.restCode === 'ResourceNotFound') {
|
||||||
|
err = new errors.ResourceNotFoundError(err,
|
||||||
|
format('image with id %s was not found', name));
|
||||||
|
}
|
||||||
next(err);
|
next(err);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -298,7 +302,8 @@ TritonApi.prototype.getImage = function getImage(opts, cb) {
|
|||||||
if (err) {
|
if (err) {
|
||||||
cb(err);
|
cb(err);
|
||||||
} else if (img.state !== 'active') {
|
} 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 {
|
} else {
|
||||||
cb(null, img);
|
cb(null, img);
|
||||||
}
|
}
|
||||||
@ -340,10 +345,11 @@ TritonApi.prototype.getImage = function getImage(opts, cb) {
|
|||||||
} else if (shortIdMatches.length === 1) {
|
} else if (shortIdMatches.length === 1) {
|
||||||
cb(null, shortIdMatches[0]);
|
cb(null, shortIdMatches[0]);
|
||||||
} else if (shortIdMatches.length === 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)));
|
'no image with name or short id "%s" was found', name)));
|
||||||
} else {
|
} 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)));
|
+ 'and "%s" is an ambiguous short id', name)));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -363,9 +369,14 @@ TritonApi.prototype.getPackage = function getPackage(name, cb) {
|
|||||||
if (common.isUUID(name)) {
|
if (common.isUUID(name)) {
|
||||||
this.cloudapi.getPackage({id: name}, function (err, pkg) {
|
this.cloudapi.getPackage({id: name}, function (err, pkg) {
|
||||||
if (err) {
|
if (err) {
|
||||||
|
if (err.restCode === 'ResourceNotFound') {
|
||||||
|
err = new errors.ResourceNotFoundError(err,
|
||||||
|
format('package with id %s was not found', name));
|
||||||
|
}
|
||||||
cb(err);
|
cb(err);
|
||||||
} else if (!pkg.active) {
|
} 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 {
|
} else {
|
||||||
cb(null, pkg);
|
cb(null, pkg);
|
||||||
}
|
}
|
||||||
@ -391,16 +402,17 @@ TritonApi.prototype.getPackage = function getPackage(name, cb) {
|
|||||||
if (nameMatches.length === 1) {
|
if (nameMatches.length === 1) {
|
||||||
cb(null, nameMatches[0]);
|
cb(null, nameMatches[0]);
|
||||||
} else if (nameMatches.length > 1) {
|
} else if (nameMatches.length > 1) {
|
||||||
cb(new Error(format(
|
cb(new errors.TritonError(format(
|
||||||
'package name "%s" is ambiguous: matches %d packages',
|
'package name "%s" is ambiguous: matches %d packages',
|
||||||
name, nameMatches.length)));
|
name, nameMatches.length)));
|
||||||
} else if (shortIdMatches.length === 1) {
|
} else if (shortIdMatches.length === 1) {
|
||||||
cb(null, shortIdMatches[0]);
|
cb(null, shortIdMatches[0]);
|
||||||
} else if (shortIdMatches.length === 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)));
|
'no package with name or short id "%s" was found', name)));
|
||||||
} else {
|
} 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)));
|
+ 'and "%s" is an ambiguous short id', name)));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -420,6 +432,11 @@ TritonApi.prototype.getNetwork = function getNetwork(name, cb) {
|
|||||||
if (common.isUUID(name)) {
|
if (common.isUUID(name)) {
|
||||||
this.cloudapi.getNetwork(name, function (err, net) {
|
this.cloudapi.getNetwork(name, function (err, net) {
|
||||||
if (err) {
|
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);
|
cb(err);
|
||||||
} else {
|
} else {
|
||||||
cb(null, net);
|
cb(null, net);
|
||||||
@ -446,16 +463,17 @@ TritonApi.prototype.getNetwork = function getNetwork(name, cb) {
|
|||||||
if (nameMatches.length === 1) {
|
if (nameMatches.length === 1) {
|
||||||
cb(null, nameMatches[0]);
|
cb(null, nameMatches[0]);
|
||||||
} else if (nameMatches.length > 1) {
|
} else if (nameMatches.length > 1) {
|
||||||
cb(new Error(format(
|
cb(new errors.TritonError(format(
|
||||||
'network name "%s" is ambiguous: matches %d networks',
|
'network name "%s" is ambiguous: matches %d networks',
|
||||||
name, nameMatches.length)));
|
name, nameMatches.length)));
|
||||||
} else if (shortIdMatches.length === 1) {
|
} else if (shortIdMatches.length === 1) {
|
||||||
cb(null, shortIdMatches[0]);
|
cb(null, shortIdMatches[0]);
|
||||||
} else if (shortIdMatches.length === 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)));
|
'no network with name or short id "%s" was found', name)));
|
||||||
} else {
|
} 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)));
|
+ '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_) {
|
self.cloudapi.getMachine(uuid, function (err, inst_) {
|
||||||
inst = 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);
|
next(err);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@ -534,7 +557,7 @@ TritonApi.prototype.getInstance = function getInstance(name, cb) {
|
|||||||
while ((candidate = s.read()) !== null) {
|
while ((candidate = s.read()) !== null) {
|
||||||
if (candidate.id.slice(0, shortId.length) === shortId) {
|
if (candidate.id.slice(0, shortId.length) === shortId) {
|
||||||
if (match) {
|
if (match) {
|
||||||
return nextOnce(new Error(
|
return nextOnce(new errors.TritonError(
|
||||||
'instance short id "%s" is ambiguous',
|
'instance short id "%s" is ambiguous',
|
||||||
shortId));
|
shortId));
|
||||||
} else {
|
} else {
|
||||||
@ -556,7 +579,7 @@ TritonApi.prototype.getInstance = function getInstance(name, cb) {
|
|||||||
} else if (inst) {
|
} else if (inst) {
|
||||||
cb(null, inst);
|
cb(null, inst);
|
||||||
} else {
|
} else {
|
||||||
cb(new Error(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', name)));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -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": "2.0.1",
|
"version": "2.1.0",
|
||||||
"author": "Joyent (joyent.com)",
|
"author": "Joyent (joyent.com)",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"assert-plus": "0.1.5",
|
"assert-plus": "0.1.5",
|
||||||
|
23
test/config.json.sample
Normal file
23
test/config.json.sample
Normal 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>"
|
||||||
|
}
|
@ -47,7 +47,8 @@ test('triton account', function (tt) {
|
|||||||
h.triton('account', function (err, stdout, stderr) {
|
h.triton('account', function (err, stdout, stderr) {
|
||||||
if (h.ifErr(t, err))
|
if (h.ifErr(t, err))
|
||||||
return t.end();
|
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();
|
t.end();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -57,7 +58,7 @@ test('triton account', function (tt) {
|
|||||||
if (h.ifErr(t, err))
|
if (h.ifErr(t, err))
|
||||||
return t.end();
|
return t.end();
|
||||||
var account = JSON.parse(stdout);
|
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();
|
t.end();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -13,17 +13,18 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
var f = require('util').format;
|
var f = require('util').format;
|
||||||
|
var os = require('os');
|
||||||
|
var tabula = require('tabula');
|
||||||
|
var test = require('tape');
|
||||||
var vasync = require('vasync');
|
var vasync = require('vasync');
|
||||||
|
|
||||||
var h = require('./helpers');
|
|
||||||
var test = require('tape');
|
|
||||||
|
|
||||||
var common = require('../../lib/common');
|
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';
|
// --- globals
|
||||||
var VM_PACKAGE = 't4-standard-128M';
|
|
||||||
|
var INST_ALIAS = f('node-triton-test-%s-vm1', os.hostname());
|
||||||
|
|
||||||
var opts = {
|
var opts = {
|
||||||
skip: !h.CONFIG.destructiveAllowed
|
skip: !h.CONFIG.destructiveAllowed
|
||||||
@ -33,6 +34,21 @@ 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) {
|
||||||
@ -40,15 +56,93 @@ if (opts.skip) {
|
|||||||
console.error('** set "destructiveAllowed" to enable');
|
console.error('** set "destructiveAllowed" to enable');
|
||||||
}
|
}
|
||||||
test('triton manage workflow', opts, function (tt) {
|
test('triton manage workflow', opts, function (tt) {
|
||||||
tt.comment('using test profile:');
|
tt.comment('Test config:');
|
||||||
Object.keys(h.CONFIG).forEach(function (key) {
|
Object.keys(h.CONFIG).forEach(function (key) {
|
||||||
var value = h.CONFIG[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
|
// create a test machine (blocking) and output JSON
|
||||||
tt.test(' triton create', function (t) {
|
tt.test(' triton create', function (t) {
|
||||||
h.safeTriton(t, ['create', '-wjn', VM_ALIAS, VM_IMAGE, VM_PACKAGE],
|
h.safeTriton(t, ['create', '-wjn', INST_ALIAS, imgId, pkgId],
|
||||||
function (stdout) {
|
function (stdout) {
|
||||||
|
|
||||||
// parse JSON response
|
// parse JSON response
|
||||||
@ -78,7 +172,7 @@ test('triton manage workflow', opts, function (tt) {
|
|||||||
vasync.parallel({
|
vasync.parallel({
|
||||||
funcs: [
|
funcs: [
|
||||||
function (cb) {
|
function (cb) {
|
||||||
h.safeTriton(t, ['instance', '-j', VM_ALIAS],
|
h.safeTriton(t, ['instance', '-j', INST_ALIAS],
|
||||||
function (stdout) {
|
function (stdout) {
|
||||||
cb(null, stdout);
|
cb(null, stdout);
|
||||||
});
|
});
|
||||||
@ -127,7 +221,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', VM_ALIAS, VM_IMAGE, VM_PACKAGE],
|
h.safeTriton(t, ['create', '-jn', INST_ALIAS, imgId, pkgId],
|
||||||
function (stdout) {
|
function (stdout) {
|
||||||
|
|
||||||
// parse JSON response
|
// parse JSON response
|
||||||
@ -168,7 +262,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', VM_ALIAS],
|
h.safeTriton(t, ['stop', '-w', INST_ALIAS],
|
||||||
function (stdout) {
|
function (stdout) {
|
||||||
t.ok(stdout.match(/^Stop instance/, 'correct stdout'));
|
t.ok(stdout.match(/^Stop instance/, 'correct stdout'));
|
||||||
t.end();
|
t.end();
|
||||||
@ -177,7 +271,7 @@ 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: ['instance', '-j', VM_ALIAS]},
|
h.safeTriton(t, {json: true, args: ['instance', '-j', INST_ALIAS]},
|
||||||
function (d) {
|
function (d) {
|
||||||
instance = d;
|
instance = d;
|
||||||
|
|
||||||
@ -189,7 +283,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', VM_ALIAS],
|
h.safeTriton(t, ['start', '-w', INST_ALIAS],
|
||||||
function (stdout) {
|
function (stdout) {
|
||||||
t.ok(stdout.match(/^Start instance/, 'correct stdout'));
|
t.ok(stdout.match(/^Start instance/, 'correct stdout'));
|
||||||
t.end();
|
t.end();
|
||||||
@ -197,20 +291,17 @@ test('triton manage workflow', opts, function (tt) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// wait for the machine to start
|
// wait for the machine to start
|
||||||
tt.test('triton confirm running', function (t) {
|
tt.test(' confirm running', function (t) {
|
||||||
h.safeTriton(t, {json: true, args: ['instance', '-j', VM_ALIAS]},
|
h.safeTriton(t, {json: true, args: ['instance', '-j', INST_ALIAS]},
|
||||||
function (d) {
|
function (d) {
|
||||||
|
|
||||||
instance = d;
|
instance = d;
|
||||||
|
|
||||||
t.equal(d.state, 'running', 'machine running');
|
t.equal(d.state, 'running', 'machine running');
|
||||||
|
|
||||||
t.end();
|
t.end();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// remove test instance
|
// 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) {
|
h.safeTriton(t, ['delete', '-w', instance.id], function (stdout) {
|
||||||
t.end();
|
t.end();
|
||||||
});
|
});
|
||||||
|
@ -37,14 +37,11 @@ test('triton profiles (read only)', function (tt) {
|
|||||||
h.safeTriton(t, {json: true, args: ['profile', '-j', 'env']},
|
h.safeTriton(t, {json: true, args: ['profile', '-j', 'env']},
|
||||||
function (p) {
|
function (p) {
|
||||||
|
|
||||||
t.equal(p.account,
|
t.equal(p.account, h.CONFIG.profile.account,
|
||||||
process.env.TRITON_ACCOUNT || process.env.SDC_ACCOUNT,
|
|
||||||
'env account correct');
|
'env account correct');
|
||||||
t.equal(p.keyId,
|
t.equal(p.keyId, h.CONFIG.profile.keyId,
|
||||||
process.env.TRITON_KEY_ID || process.env.SDC_KEY_ID,
|
|
||||||
'env keyId correct');
|
'env keyId correct');
|
||||||
t.equal(p.url,
|
t.equal(p.url, h.CONFIG.profile.url,
|
||||||
process.env.TRITON_URL || process.env.SDC_URL,
|
|
||||||
'env url correct');
|
'env url correct');
|
||||||
|
|
||||||
t.end();
|
t.end();
|
||||||
|
@ -22,56 +22,54 @@ var mod_config = require('../../lib/config');
|
|||||||
var testcommon = require('../lib/testcommon');
|
var testcommon = require('../lib/testcommon');
|
||||||
|
|
||||||
|
|
||||||
// --- globals
|
|
||||||
|
|
||||||
var CONFIG;
|
var CONFIG;
|
||||||
if (process.env.TRITON_TEST_PROFILE) {
|
var configPath = process.env.TRITON_TEST_CONFIG
|
||||||
CONFIG = mod_config.loadProfile({
|
? path.resolve(process.cwd(), process.env.TRITON_TEST_CONFIG)
|
||||||
configDir: path.join(process.env.HOME, '.triton'),
|
: path.resolve(__dirname, '..', 'config.json');
|
||||||
name: process.env.TRITON_TEST_PROFILE
|
|
||||||
});
|
|
||||||
CONFIG.destructiveAllowed = common.boolFromString(
|
|
||||||
process.env.TRITON_TEST_DESTRUCTIVE_ALLOWED);
|
|
||||||
} else {
|
|
||||||
try {
|
try {
|
||||||
CONFIG = require('../config.json');
|
CONFIG = require(configPath);
|
||||||
assert.object(CONFIG, 'test/config.json');
|
assert.object(CONFIG, configPath);
|
||||||
assert.string(CONFIG.url, 'test/config.json#url');
|
if (CONFIG.profile && CONFIG.profileName) {
|
||||||
assert.string(CONFIG.account, 'test/config.json#account');
|
throw new Error(
|
||||||
assert.string(CONFIG.keyId, 'test/config.json#keyId');
|
'cannot specify both "profile" and "profileName" in ' +
|
||||||
assert.optionalBool(CONFIG.insecure,
|
configPath);
|
||||||
'test/config.json#insecure');
|
} 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,
|
assert.optionalBool(CONFIG.destructiveAllowed,
|
||||||
'test/config.json#destructiveAllowed');
|
'test/config.json#destructiveAllowed');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error('* * *');
|
error('* * *');
|
||||||
error('node-triton integration tests require either:');
|
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('');
|
||||||
error('1. environment variables like:');
|
error(' TRITON_TEST_CONFIG=test/coal.json make test');
|
||||||
error('');
|
error('');
|
||||||
error(' TRITON_TEST_PROFILE=<Triton profile name>');
|
error('See "test/config.json.sample" for a starting point for a config.');
|
||||||
error(' TRITON_TEST_DESTRUCTIVE_ALLOWED=1 # Optional');
|
|
||||||
error('');
|
error('');
|
||||||
error('2. or, a "./test/config.json" like this:');
|
error('Warning: This test suite will create machines, images, etc. ');
|
||||||
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('using this CloudAPI and account. While it will do its best');
|
||||||
error('to clean up all resources, running the test suite against');
|
error('to clean up all resources, running the test suite against');
|
||||||
error('a public cloud could *cost* you money. :)');
|
error('a public cloud could *cost* you money. :)');
|
||||||
error('* * *');
|
error('* * *');
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
if (CONFIG.profile.insecure === undefined)
|
||||||
if (CONFIG.insecure === undefined)
|
CONFIG.profile.insecure = false;
|
||||||
CONFIG.insecure = false;
|
|
||||||
if (CONFIG.destructiveAllowed === undefined)
|
if (CONFIG.destructiveAllowed === undefined)
|
||||||
CONFIG.destructiveAllowed = false;
|
CONFIG.destructiveAllowed = false;
|
||||||
|
|
||||||
@ -82,8 +80,6 @@ var LOG = require('../lib/log');
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
// --- internal support routines
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Call the `triton` CLI with the given args.
|
* Call the `triton` CLI with the given args.
|
||||||
*/
|
*/
|
||||||
@ -101,10 +97,10 @@ function triton(args, cb) {
|
|||||||
HOME: process.env.HOME,
|
HOME: process.env.HOME,
|
||||||
SSH_AUTH_SOCK: process.env.SSH_AUTH_SOCK,
|
SSH_AUTH_SOCK: process.env.SSH_AUTH_SOCK,
|
||||||
TRITON_PROFILE: 'env',
|
TRITON_PROFILE: 'env',
|
||||||
TRITON_URL: CONFIG.url,
|
TRITON_URL: CONFIG.profile.url,
|
||||||
TRITON_ACCOUNT: CONFIG.account,
|
TRITON_ACCOUNT: CONFIG.profile.account,
|
||||||
TRITON_KEY_ID: CONFIG.keyId,
|
TRITON_KEY_ID: CONFIG.profile.keyId,
|
||||||
TRITON_TLS_INSECURE: CONFIG.insecure
|
TRITON_TLS_INSECURE: CONFIG.profile.insecure
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
log: LOG
|
log: LOG
|
||||||
|
@ -45,16 +45,16 @@ function execPlus(args, cb) {
|
|||||||
args.log.trace({exec: true, command: command, execOpts: execOpts,
|
args.log.trace({exec: true, command: command, execOpts: execOpts,
|
||||||
err: err, stdout: stdout, stderr: stderr}, 'exec done');
|
err: err, stdout: stdout, stderr: stderr}, 'exec done');
|
||||||
if (err) {
|
if (err) {
|
||||||
cb(
|
var niceErr = new VError(err,
|
||||||
new VError(err,
|
|
||||||
'%s:\n'
|
'%s:\n'
|
||||||
+ '\tcommand: %s\n'
|
+ '\tcommand: %s\n'
|
||||||
+ '\texit status: %s\n'
|
+ '\texit status: %s\n'
|
||||||
+ '\tstdout:\n%s\n'
|
+ '\tstdout:\n%s\n'
|
||||||
+ '\tstderr:\n%s',
|
+ '\tstderr:\n%s',
|
||||||
args.errMsg || 'exec error', command, err.code,
|
args.errMsg || 'exec error', command, err.code,
|
||||||
stdout.trim(), stderr.trim()),
|
stdout.trim(), stderr.trim());
|
||||||
stdout, stderr);
|
niceErr.code = err.code;
|
||||||
|
cb(niceErr, stdout, stderr);
|
||||||
} else {
|
} else {
|
||||||
cb(null, stdout, stderr);
|
cb(null, stdout, stderr);
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user