joyent/node-triton#46 Triton help for setting up and using Docker against a Triton DC
This implements all but step #4 (`triton docker ...`) in the proposal.
This commit is contained in:
parent
5929632e08
commit
8cd4dd80eb
29
CHANGES.md
29
CHANGES.md
@ -5,9 +5,34 @@ Known issues:
|
|||||||
- `triton ssh ...` disables ssh ControlMaster to avoid issue #52.
|
- `triton ssh ...` disables ssh ControlMaster to avoid issue #52.
|
||||||
|
|
||||||
|
|
||||||
## 4.8.1 (not yet released)
|
## 4.9.0 (not yet released)
|
||||||
|
|
||||||
(nothing yet)
|
- #46 Initial support for `triton` helping setup and manage configuration for
|
||||||
|
using `docker` against a Triton datacenter. Triton datacenters can provide
|
||||||
|
a Docker Remote API endpoint against which you can run the normal `docker`
|
||||||
|
client. See <https://www.joyent.com/triton> for and overview and
|
||||||
|
<https://github.com/joyent/sdc-docker> for developer details.
|
||||||
|
|
||||||
|
- `triton profile create` will now setup the profile for running Docker,
|
||||||
|
if the Triton datacenter provides a docker endpoint. The typical flow
|
||||||
|
would be:
|
||||||
|
|
||||||
|
$ triton profile create
|
||||||
|
name: foo
|
||||||
|
...
|
||||||
|
$ triton profile set foo # make foo my default profile
|
||||||
|
$ eval "$(triton env --docker)" # set 'DOCKER_' envvars
|
||||||
|
$ docker info
|
||||||
|
|
||||||
|
- For existing Triton CLI profiles, there is a new `triton profile
|
||||||
|
docker-setup [PROFILE]`.
|
||||||
|
|
||||||
|
$ triton profile docker-setup
|
||||||
|
$ eval "$(triton env --docker)"
|
||||||
|
$ docker info
|
||||||
|
|
||||||
|
- `triton env` will now emit commands to setup `DOCKER_` envvars. That
|
||||||
|
can be limited to just the Docker-relevant env via `triton env --docker`.
|
||||||
|
|
||||||
|
|
||||||
## 4.8.0
|
## 4.8.0
|
||||||
|
70
lib/cli.js
70
lib/cli.js
@ -276,25 +276,38 @@ CLI.prototype.init = function (opts, args, callback) {
|
|||||||
|
|
||||||
this.configDir = CONFIG_DIR;
|
this.configDir = CONFIG_DIR;
|
||||||
|
|
||||||
this.__defineGetter__('tritonapi', function () {
|
this.__defineGetter__('config', function getConfig() {
|
||||||
if (self._tritonapi === undefined) {
|
if (self._config === undefined) {
|
||||||
var config = mod_config.loadConfig({
|
self._config = mod_config.loadConfig({
|
||||||
configDir: self.configDir
|
configDir: self.configDir
|
||||||
});
|
});
|
||||||
self.log.trace({config: config}, 'loaded config');
|
self.log.trace({config: self._config}, 'loaded config');
|
||||||
|
}
|
||||||
var profileName = opts.profile || config.profile || 'env';
|
return self._config;
|
||||||
var profile = mod_config.loadProfile({
|
|
||||||
configDir: self.configDir,
|
|
||||||
name: profileName
|
|
||||||
});
|
});
|
||||||
self._applyProfileOverrides(profile);
|
|
||||||
self.log.trace({profile: profile}, 'loaded profile');
|
|
||||||
|
|
||||||
|
this.__defineGetter__('profileName', function getProfileName() {
|
||||||
|
return (opts.profile || self.config.profile || 'env');
|
||||||
|
});
|
||||||
|
|
||||||
|
this.__defineGetter__('profile', function getProfile() {
|
||||||
|
if (self._profile === undefined) {
|
||||||
|
self._profile = mod_config.loadProfile({
|
||||||
|
configDir: self.configDir,
|
||||||
|
name: self.profileName
|
||||||
|
});
|
||||||
|
self._applyProfileOverrides(self._profile);
|
||||||
|
self.log.trace({profile: self._profile}, 'loaded profile');
|
||||||
|
}
|
||||||
|
return self._profile;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.__defineGetter__('tritonapi', function getTritonapi() {
|
||||||
|
if (self._tritonapi === undefined) {
|
||||||
self._tritonapi = tritonapi.createClient({
|
self._tritonapi = tritonapi.createClient({
|
||||||
log: self.log,
|
log: self.log,
|
||||||
profile: profile,
|
profile: self.profile,
|
||||||
config: config
|
config: self.config
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return self._tritonapi;
|
return self._tritonapi;
|
||||||
@ -538,6 +551,37 @@ CLI.prototype._applyProfileOverrides =
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Create and return a TritonApi instance for the given profile name and using
|
||||||
|
* the CLI's config. Callers of this should remember to `tritonapi.close()`
|
||||||
|
* when complete... otherwise an HTTP Agent using keep-alive will keep node
|
||||||
|
* from exiting until it times out.
|
||||||
|
*/
|
||||||
|
CLI.prototype.tritonapiFromProfileName =
|
||||||
|
function tritonapiFromProfileName(opts) {
|
||||||
|
assert.object(opts, 'opts');
|
||||||
|
assert.string(opts.profileName, 'opts.profileName');
|
||||||
|
|
||||||
|
var profile;
|
||||||
|
if (opts.profileName === this.profileName) {
|
||||||
|
profile = this.profile;
|
||||||
|
} else {
|
||||||
|
profile = mod_config.loadProfile({
|
||||||
|
configDir: this.configDir,
|
||||||
|
name: opts.profileName
|
||||||
|
});
|
||||||
|
this.log.trace({profile: profile},
|
||||||
|
'tritonapiFromProfileName: loaded profile');
|
||||||
|
}
|
||||||
|
|
||||||
|
return tritonapi.createClient({
|
||||||
|
log: this.log,
|
||||||
|
profile: profile,
|
||||||
|
config: this.config
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
// Meta
|
// Meta
|
||||||
CLI.prototype.do_completion = require('./do_completion');
|
CLI.prototype.do_completion = require('./do_completion');
|
||||||
CLI.prototype.do_profiles = require('./do_profiles');
|
CLI.prototype.do_profiles = require('./do_profiles');
|
||||||
|
@ -286,6 +286,22 @@ CloudApi.prototype._passThrough = function _passThrough(endpoint, opts, cb) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// ---- ping
|
||||||
|
|
||||||
|
CloudApi.prototype.ping = function ping(opts, cb) {
|
||||||
|
assert.object(opts, 'opts');
|
||||||
|
assert.func(cb, 'cb');
|
||||||
|
|
||||||
|
var reqOpts = {
|
||||||
|
path: '/--ping',
|
||||||
|
// Ping should be fast. We don't want 15s of retrying.
|
||||||
|
retry: false
|
||||||
|
};
|
||||||
|
this.client.get(reqOpts, function (err, req, res, body) {
|
||||||
|
cb(err, body, res);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
// ---- networks
|
// ---- networks
|
||||||
|
|
||||||
|
@ -424,7 +424,7 @@ function saveProfileSync(opts) {
|
|||||||
mkdirp.sync(path.dirname(profilePath));
|
mkdirp.sync(path.dirname(profilePath));
|
||||||
}
|
}
|
||||||
fs.writeFileSync(profilePath, JSON.stringify(toSave, null, 4), 'utf8');
|
fs.writeFileSync(profilePath, JSON.stringify(toSave, null, 4), 'utf8');
|
||||||
console.log('Saved profile "%s"', name);
|
console.log('Saved profile "%s".', name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2015 Joyent Inc.
|
* Copyright 2016 Joyent Inc.
|
||||||
*
|
*
|
||||||
* `triton env ...`
|
* `triton env ...`
|
||||||
*/
|
*/
|
||||||
@ -7,6 +7,7 @@
|
|||||||
var assert = require('assert-plus');
|
var assert = require('assert-plus');
|
||||||
var format = require('util').format;
|
var format = require('util').format;
|
||||||
var fs = require('fs');
|
var fs = require('fs');
|
||||||
|
var path = require('path');
|
||||||
var strsplit = require('strsplit');
|
var strsplit = require('strsplit');
|
||||||
var sshpk = require('sshpk');
|
var sshpk = require('sshpk');
|
||||||
var vasync = require('vasync');
|
var vasync = require('vasync');
|
||||||
@ -18,6 +19,7 @@ var mod_config = require('./config');
|
|||||||
|
|
||||||
|
|
||||||
function do_env(subcmd, opts, args, cb) {
|
function do_env(subcmd, opts, args, cb) {
|
||||||
|
var self = this;
|
||||||
if (opts.help) {
|
if (opts.help) {
|
||||||
this.do_help('help', {}, [subcmd], cb);
|
this.do_help('help', {}, [subcmd], cb);
|
||||||
return;
|
return;
|
||||||
@ -27,16 +29,27 @@ function do_env(subcmd, opts, args, cb) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var profileName = args[0] || this.tritonapi.profile.name;
|
var profileName = args[0] || this.tritonapi.profile.name;
|
||||||
var allClientTypes = ['smartdc', 'triton'];
|
var allClientTypes = ['triton', 'docker', 'smartdc'];
|
||||||
var clientTypes = [];
|
var clientTypes = [];
|
||||||
if (opts.smartdc) {
|
var explicit;
|
||||||
clientTypes.push('smartdc');
|
var shortOpts = '';
|
||||||
}
|
|
||||||
if (opts.triton) {
|
if (opts.triton) {
|
||||||
|
shortOpts += 't';
|
||||||
clientTypes.push('triton');
|
clientTypes.push('triton');
|
||||||
}
|
}
|
||||||
|
if (opts.docker) {
|
||||||
|
shortOpts += 'd';
|
||||||
|
clientTypes.push('docker');
|
||||||
|
}
|
||||||
|
if (opts.smartdc) {
|
||||||
|
shortOpts += 's';
|
||||||
|
clientTypes.push('smartdc');
|
||||||
|
}
|
||||||
if (clientTypes.length === 0) {
|
if (clientTypes.length === 0) {
|
||||||
|
explicit = false;
|
||||||
clientTypes = allClientTypes;
|
clientTypes = allClientTypes;
|
||||||
|
} else {
|
||||||
|
explicit = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -52,15 +65,40 @@ function do_env(subcmd, opts, args, cb) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var p = console.log;
|
var p = console.log;
|
||||||
var shortOpts = '';
|
|
||||||
clientTypes.forEach(function (clientType) {
|
clientTypes.forEach(function (clientType) {
|
||||||
switch (clientType) {
|
switch (clientType) {
|
||||||
case 'triton':
|
case 'triton':
|
||||||
shortOpts += 't';
|
|
||||||
p('export TRITON_PROFILE="%s"', profile.name);
|
p('export TRITON_PROFILE="%s"', profile.name);
|
||||||
break;
|
break;
|
||||||
|
case 'docker':
|
||||||
|
var setupJson = path.resolve(self.configDir, 'docker',
|
||||||
|
common.profileSlug(profile), 'setup.json');
|
||||||
|
if (fs.existsSync(setupJson)) {
|
||||||
|
var setup;
|
||||||
|
try {
|
||||||
|
setup = JSON.parse(fs.readFileSync(setupJson));
|
||||||
|
} catch (err) {
|
||||||
|
cb(new errors.ConfigError(err, format(
|
||||||
|
'error determining Docker environment from "%s": %s',
|
||||||
|
setupJson, err)));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Object.keys(setup.env).forEach(function (key) {
|
||||||
|
var val = setup.env[key];
|
||||||
|
if (val === null) {
|
||||||
|
p('unset %s', key);
|
||||||
|
} else {
|
||||||
|
p('export %s=%s', key, val);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else if (explicit) {
|
||||||
|
cb(new errors.ConfigError(format('could not find Docker '
|
||||||
|
+ 'environment setup for profile "%s":\n Run `triton '
|
||||||
|
+ 'profile docker-setup %s` to setup.',
|
||||||
|
profile.name, profile.name)));
|
||||||
|
}
|
||||||
|
break;
|
||||||
case 'smartdc':
|
case 'smartdc':
|
||||||
shortOpts += 's';
|
|
||||||
p('export SDC_URL="%s"', profile.url);
|
p('export SDC_URL="%s"', profile.url);
|
||||||
p('export SDC_ACCOUNT="%s"', profile.account);
|
p('export SDC_ACCOUNT="%s"', profile.account);
|
||||||
if (profile.user) {
|
if (profile.user) {
|
||||||
@ -82,8 +120,10 @@ function do_env(subcmd, opts, args, cb) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
p('# Run this command to configure your shell:');
|
p('# Run this command to configure your shell:');
|
||||||
p('# eval "$(triton env%s %s)"',
|
p('# eval "$(triton env%s%s)"',
|
||||||
(shortOpts ? ' -'+shortOpts : ''), profile.name);
|
(shortOpts ? ' -'+shortOpts : ''),
|
||||||
|
(profile.name === this.tritonapi.profile.name
|
||||||
|
? '' : ' ' + profile.name));
|
||||||
}
|
}
|
||||||
|
|
||||||
do_env.options = [
|
do_env.options = [
|
||||||
@ -92,16 +132,21 @@ do_env.options = [
|
|||||||
type: 'bool',
|
type: 'bool',
|
||||||
help: 'Show this help.'
|
help: 'Show this help.'
|
||||||
},
|
},
|
||||||
{
|
|
||||||
names: ['smartdc', 's'],
|
|
||||||
type: 'bool',
|
|
||||||
help: 'Emit environment for node-smartdc (i.e. the "SDC_*" variables).'
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
names: ['triton', 't'],
|
names: ['triton', 't'],
|
||||||
type: 'bool',
|
type: 'bool',
|
||||||
help: 'Emit environment commands for node-triton itself (i.e. the ' +
|
help: 'Emit environment commands for node-triton itself (i.e. the ' +
|
||||||
'"TRITON_PROFILE" variable).'
|
'"TRITON_PROFILE" variable).'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
names: ['docker', 'd'],
|
||||||
|
type: 'bool',
|
||||||
|
help: 'Emit environment commands for docker ("DOCKER_HOST" et al).'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
names: ['smartdc', 's'],
|
||||||
|
type: 'bool',
|
||||||
|
help: 'Emit environment for node-smartdc (i.e. the "SDC_*" variables).'
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -13,12 +13,14 @@ var vasync = require('vasync');
|
|||||||
var common = require('../common');
|
var common = require('../common');
|
||||||
var errors = require('../errors');
|
var errors = require('../errors');
|
||||||
var mod_config = require('../config');
|
var mod_config = require('../config');
|
||||||
|
var profilecommon = require('./profilecommon');
|
||||||
|
|
||||||
|
|
||||||
function _createProfile(opts, cb) {
|
function _createProfile(opts, cb) {
|
||||||
assert.object(opts.cli, 'opts.cli');
|
assert.object(opts.cli, 'opts.cli');
|
||||||
assert.optionalString(opts.file, 'opts.file');
|
assert.optionalString(opts.file, 'opts.file');
|
||||||
assert.optionalString(opts.copy, 'opts.copy');
|
assert.optionalString(opts.copy, 'opts.copy');
|
||||||
|
assert.optionalBool(opts.noDocker, 'opts.noDocker');
|
||||||
assert.func(cb, 'cb');
|
assert.func(cb, 'cb');
|
||||||
var cli = opts.cli;
|
var cli = opts.cli;
|
||||||
var log = cli.log;
|
var log = cli.log;
|
||||||
@ -136,6 +138,8 @@ function _createProfile(opts, cb) {
|
|||||||
desc: 'The CloudAPI endpoint URL.',
|
desc: 'The CloudAPI endpoint URL.',
|
||||||
default: defaults.url,
|
default: defaults.url,
|
||||||
key: 'url'
|
key: 'url'
|
||||||
|
// TODO: shortcut to allow 'ssh nightly1' to have this ssh
|
||||||
|
// in and find cloudapi for me
|
||||||
}, {
|
}, {
|
||||||
desc: 'Your account login name.',
|
desc: 'Your account login name.',
|
||||||
key: 'account',
|
key: 'account',
|
||||||
@ -186,6 +190,19 @@ function _createProfile(opts, cb) {
|
|||||||
return valCb(keyErr);
|
return valCb(keyErr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Save the user's explicit given key path. We will
|
||||||
|
* using it later for Docker setup. Trying to use
|
||||||
|
* the same format as node-smartdc's loadSSHKey
|
||||||
|
* `keyPaths` param.
|
||||||
|
*/
|
||||||
|
ctx.keyPaths = {};
|
||||||
|
if (keyType === 'ssh') {
|
||||||
|
ctx.keyPaths.public = keyPath;
|
||||||
|
} else {
|
||||||
|
ctx.keyPaths.private = keyPath;
|
||||||
|
}
|
||||||
|
|
||||||
var newVal = key.fingerprint('md5').toString();
|
var newVal = key.fingerprint('md5').toString();
|
||||||
console.log('Fingerprint: %s', newVal);
|
console.log('Fingerprint: %s', newVal);
|
||||||
valCb(null, newVal);
|
valCb(null, newVal);
|
||||||
@ -259,6 +276,21 @@ function _createProfile(opts, cb) {
|
|||||||
}
|
}
|
||||||
next();
|
next();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
function dockerSetup(ctx, next) {
|
||||||
|
if (opts.noDocker || process.platform === 'win32') {
|
||||||
|
next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
profilecommon.profileDockerSetup({
|
||||||
|
cli: cli,
|
||||||
|
name: data.name,
|
||||||
|
keyPaths: ctx.keyPaths,
|
||||||
|
implicit: true
|
||||||
|
}, next);
|
||||||
|
},
|
||||||
|
|
||||||
function setCurrIfTheOnlyProfile(ctx, next) {
|
function setCurrIfTheOnlyProfile(ctx, next) {
|
||||||
if (ctx.profiles.length !== 0) {
|
if (ctx.profiles.length !== 0) {
|
||||||
next();
|
next();
|
||||||
@ -276,7 +308,7 @@ function _createProfile(opts, cb) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.log('Set "%s" as current profile (because it is ' +
|
console.log('Set "%s" as current profile (because it is ' +
|
||||||
'your only profile)', data.name);
|
'your only profile).', data.name);
|
||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -299,7 +331,8 @@ function do_create(subcmd, opts, args, cb) {
|
|||||||
_createProfile({
|
_createProfile({
|
||||||
cli: this.top,
|
cli: this.top,
|
||||||
file: opts.file,
|
file: opts.file,
|
||||||
copy: opts.copy
|
copy: opts.copy,
|
||||||
|
noDocker: opts.no_docker
|
||||||
}, cb);
|
}, cb);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -321,6 +354,14 @@ do_create.options = [
|
|||||||
type: 'string',
|
type: 'string',
|
||||||
helpArg: 'NAME',
|
helpArg: 'NAME',
|
||||||
help: 'A profile from which to copy values.'
|
help: 'A profile from which to copy values.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
names: ['no-docker'],
|
||||||
|
type: 'bool',
|
||||||
|
help: 'As of Triton CLI 4.9, creating a profile will attempt (on '
|
||||||
|
+ 'non-Windows) to also setup for running Docker. This is '
|
||||||
|
+ 'experimental and might fail. Use this option to disable '
|
||||||
|
+ 'the attempt.'
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
58
lib/do_profile/do_docker_setup.js
Normal file
58
lib/do_profile/do_docker_setup.js
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2016 Joyent Inc.
|
||||||
|
*
|
||||||
|
* `triton profile docker-setup ...`
|
||||||
|
*/
|
||||||
|
|
||||||
|
var assert = require('assert-plus');
|
||||||
|
|
||||||
|
var errors = require('../errors');
|
||||||
|
var profilecommon = require('./profilecommon');
|
||||||
|
|
||||||
|
|
||||||
|
function do_docker_setup(subcmd, opts, args, cb) {
|
||||||
|
if (opts.help) {
|
||||||
|
this.do_help('help', {}, [subcmd], cb);
|
||||||
|
return;
|
||||||
|
} else if (args.length > 1) {
|
||||||
|
return cb(new errors.UsageError('too many arguments'));
|
||||||
|
}
|
||||||
|
|
||||||
|
var profileName = args[0] || this.top.tritonapi.profile.name;
|
||||||
|
profilecommon.profileDockerSetup({
|
||||||
|
cli: this.top,
|
||||||
|
name: profileName
|
||||||
|
}, cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
do_docker_setup.options = [
|
||||||
|
{
|
||||||
|
names: ['help', 'h'],
|
||||||
|
type: 'bool',
|
||||||
|
help: 'Show this help.'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
do_docker_setup.help = [
|
||||||
|
/* BEGIN JSSTYLED */
|
||||||
|
'Setup for using Docker with the current Triton CLI profile.',
|
||||||
|
'',
|
||||||
|
'Usage:',
|
||||||
|
' {{name}} docker-setup [PROFILE]',
|
||||||
|
'',
|
||||||
|
'{{options}}',
|
||||||
|
'A Triton datacenter can act as a virtual Docker Engine, where the entire',
|
||||||
|
'datacenter is available on for running containers. The datacenter provides',
|
||||||
|
'an endpoint against which you can run the regular `docker` client. This',
|
||||||
|
'requires a one time setup to (a) generate a client TLS certificate to enable',
|
||||||
|
'secure authentication with the Triton Docker Engine, and (b) to determine',
|
||||||
|
'the DOCKER_HOST and related environment variables.',
|
||||||
|
'',
|
||||||
|
'After running this, you can setup your shell environment for `docker` via:',
|
||||||
|
' eval "$(triton env --docker)"',
|
||||||
|
'or the equivalent. See `triton env --help` for details.'
|
||||||
|
/* END JSSTYLED */
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = do_docker_setup;
|
@ -1,20 +1,13 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2015 Joyent Inc.
|
* Copyright 2016 Joyent Inc.
|
||||||
*
|
*
|
||||||
* `triton profile get ...`
|
* `triton profile get ...`
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var assert = require('assert-plus');
|
var assert = require('assert-plus');
|
||||||
var format = require('util').format;
|
|
||||||
var fs = require('fs');
|
|
||||||
var strsplit = require('strsplit');
|
|
||||||
var sshpk = require('sshpk');
|
|
||||||
var vasync = require('vasync');
|
|
||||||
|
|
||||||
var common = require('../common');
|
|
||||||
var errors = require('../errors');
|
var errors = require('../errors');
|
||||||
var mod_config = require('../config');
|
var mod_config = require('../config');
|
||||||
var profilecommon = require('./profilecommon');
|
|
||||||
|
|
||||||
|
|
||||||
function _showProfile(opts, cb) {
|
function _showProfile(opts, cb) {
|
||||||
|
@ -78,11 +78,16 @@ function _listProfiles(cli, opts, args, cb) {
|
|||||||
sort: sort
|
sort: sort
|
||||||
});
|
});
|
||||||
if (!haveCurr) {
|
if (!haveCurr) {
|
||||||
|
if (profiles.length === 0) {
|
||||||
|
process.stderr.write('\nWarning: There is no current profile. '
|
||||||
|
+ 'Use "triton profile create" to create one.\n');
|
||||||
|
} else {
|
||||||
process.stderr.write('\nWarning: There is no current profile. '
|
process.stderr.write('\nWarning: There is no current profile. '
|
||||||
+ 'Use "triton profile set-current ..."\n'
|
+ 'Use "triton profile set-current ..."\n'
|
||||||
+ 'to set one or "triton profile create" to create one.\n');
|
+ 'to set one or "triton profile create" to create one.\n');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
cb();
|
cb();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -41,7 +41,8 @@ function ProfileCLI(top) {
|
|||||||
'set-current',
|
'set-current',
|
||||||
'create',
|
'create',
|
||||||
'edit',
|
'edit',
|
||||||
'delete'
|
'delete',
|
||||||
|
'docker-setup'
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -58,6 +59,7 @@ ProfileCLI.prototype.do_set_current = require('./do_set_current');
|
|||||||
ProfileCLI.prototype.do_create = require('./do_create');
|
ProfileCLI.prototype.do_create = require('./do_create');
|
||||||
ProfileCLI.prototype.do_delete = require('./do_delete');
|
ProfileCLI.prototype.do_delete = require('./do_delete');
|
||||||
ProfileCLI.prototype.do_edit = require('./do_edit');
|
ProfileCLI.prototype.do_edit = require('./do_edit');
|
||||||
|
ProfileCLI.prototype.do_docker_setup = require('./do_docker_setup');
|
||||||
|
|
||||||
// TODO: Would like to `triton profile update foo account=trentm ...`
|
// TODO: Would like to `triton profile update foo account=trentm ...`
|
||||||
// And then would like that same key=value syntax optional for create.
|
// And then would like that same key=value syntax optional for create.
|
||||||
|
@ -5,11 +5,68 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
var assert = require('assert-plus');
|
var assert = require('assert-plus');
|
||||||
|
var auth = require('smartdc-auth');
|
||||||
|
var format = require('util').format;
|
||||||
|
var fs = require('fs');
|
||||||
|
var https = require('https');
|
||||||
|
var mkdirp = require('mkdirp');
|
||||||
|
var path = require('path');
|
||||||
|
var rimraf = require('rimraf');
|
||||||
|
var semver = require('semver');
|
||||||
|
var sshpk = require('sshpk');
|
||||||
|
var mod_url = require('url');
|
||||||
|
var vasync = require('vasync');
|
||||||
|
var which = require('which');
|
||||||
|
var wordwrap = require('wordwrap')(78);
|
||||||
|
|
||||||
|
var common = require('../common');
|
||||||
var mod_config = require('../config');
|
var mod_config = require('../config');
|
||||||
var errors = require('../errors');
|
var errors = require('../errors');
|
||||||
|
|
||||||
|
|
||||||
|
// --- internal support functions
|
||||||
|
|
||||||
|
function portalUrlFromCloudapiUrl(url) {
|
||||||
|
assert.string(url, 'url');
|
||||||
|
var portalUrl;
|
||||||
|
|
||||||
|
var JPC_RE = /^https:\/\/([a-z0-9-]+)\.api\.joyent(cloud)?\.com\/?$/;
|
||||||
|
if (JPC_RE.test(url)) {
|
||||||
|
return 'https://my.joyent.com';
|
||||||
|
}
|
||||||
|
return portalUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function downloadUrl(opts, cb) {
|
||||||
|
assert.string(opts.url, 'opts.url');
|
||||||
|
assert.ok(/^https:/.test(opts.url));
|
||||||
|
assert.string(opts.dest, 'opts.dest');
|
||||||
|
assert.optionalBool(opts.insecure, 'opts.insecure');
|
||||||
|
assert.func(cb, 'cb');
|
||||||
|
|
||||||
|
var reqOpts = mod_url.parse(opts.url);
|
||||||
|
if (opts.insecure) {
|
||||||
|
reqOpts.rejectUnauthorized = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var file = fs.createWriteStream(opts.dest);
|
||||||
|
var req = https.get(reqOpts, function (res) {
|
||||||
|
res.pipe(file);
|
||||||
|
file.on('finish', function () {
|
||||||
|
file.close(cb);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
req.on('error', function (err) {
|
||||||
|
fs.unlink(opts.dest);
|
||||||
|
cb(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// --- exported functions
|
||||||
|
|
||||||
function setCurrentProfile(opts, cb) {
|
function setCurrentProfile(opts, cb) {
|
||||||
assert.object(opts.cli, 'opts.cli');
|
assert.object(opts.cli, 'opts.cli');
|
||||||
assert.string(opts.name, 'opts.name');
|
assert.string(opts.name, 'opts.name');
|
||||||
@ -63,6 +120,348 @@ function setCurrentProfile(opts, cb) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup the given profile for Docker usage. This means checking the cloudapi
|
||||||
|
* has a Docker service (ListServices), finding the user's SSH *private* key,
|
||||||
|
* creating the client certs that will be used to talk to the Triton Docker
|
||||||
|
* Engine.
|
||||||
|
*
|
||||||
|
* @param {Object} opts: Required.
|
||||||
|
* - {Object} cli: Required. The Triton CLI object.
|
||||||
|
* - {String} name: Required. The profile name.
|
||||||
|
* - {Boolean} implicit: Optional. Boolean indicating if the Docker setup
|
||||||
|
* is implicit (e.g. as a default part of `triton profile create`). If
|
||||||
|
* implicit, we silently skip if ListServices shows no Docker service.
|
||||||
|
* - {Object} keyPaths: Optional. An object with `private` and/or `public`
|
||||||
|
* properties pointing to a full path to an SSH private and/or public
|
||||||
|
* key to use for cert signing.
|
||||||
|
*/
|
||||||
|
function profileDockerSetup(opts, cb) {
|
||||||
|
assert.object(opts.cli, 'opts.cli');
|
||||||
|
assert.string(opts.name, 'opts.name');
|
||||||
|
assert.optionalBool(opts.implicit, 'opts.implicit');
|
||||||
|
assert.optionalObject(opts.keyPaths, 'opts.keyPaths');
|
||||||
|
assert.func(cb, 'cb');
|
||||||
|
|
||||||
|
var implicit = Boolean(opts.implicit);
|
||||||
|
var cli = opts.cli;
|
||||||
|
var log = cli.log;
|
||||||
|
var tritonapi = cli.tritonapiFromProfileName({profileName: opts.name});
|
||||||
|
var profile = tritonapi.profile;
|
||||||
|
var dockerHost;
|
||||||
|
|
||||||
|
vasync.pipeline({arg: {}, funcs: [
|
||||||
|
function checkCloudapiStatus(arg, next) {
|
||||||
|
tritonapi.cloudapi.ping({}, function (err, pong, res) {
|
||||||
|
if (!res) {
|
||||||
|
next(new errors.SetupError(err, format(
|
||||||
|
'error pinging CloudAPI <%s>', profile.url)));
|
||||||
|
} else if (res.statusCode === 503) {
|
||||||
|
// TODO: Use maint res headers to estimate time back up.
|
||||||
|
next(new errors.SetupError(err, format('CloudAPI <%s> is ',
|
||||||
|
+ 'in maintenance, please try again later',
|
||||||
|
profile.url)));
|
||||||
|
} else if (res.statusCode === 200) {
|
||||||
|
next();
|
||||||
|
} else {
|
||||||
|
next(new errors.SetupError(err, format(
|
||||||
|
'error pinging CloudAPI <%s>: %s status code',
|
||||||
|
profile.url, res.statusCode)));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
function checkForDockerService(arg, next) {
|
||||||
|
tritonapi.cloudapi.listServices({}, function (err, svcs, res) {
|
||||||
|
if (!res) {
|
||||||
|
next(new errors.SetupError(err, format(
|
||||||
|
'could not list services on cloudapi %s',
|
||||||
|
profile.url)));
|
||||||
|
} else if (res.statusCode === 401) {
|
||||||
|
var portalUrl = portalUrlFromCloudapiUrl(profile.url);
|
||||||
|
if (portalUrl) {
|
||||||
|
next(new errors.SetupError(err, format(
|
||||||
|
'invalid credentials. Visit <%s> to create the '
|
||||||
|
+ '"%s" account and/or add your SSH public key',
|
||||||
|
portalUrl, profile.account)));
|
||||||
|
} else {
|
||||||
|
next(new errors.SetupError(err, format(
|
||||||
|
'invalid credentials. You must create the '
|
||||||
|
+ '"%s" account and/or add your SSH public key',
|
||||||
|
profile.account)));
|
||||||
|
}
|
||||||
|
} else if (res.statusCode === 200) {
|
||||||
|
if (svcs.docker) {
|
||||||
|
dockerHost = svcs.docker;
|
||||||
|
log.trace({dockerHost: dockerHost},
|
||||||
|
'profileDockerSetup: checkForDockerService');
|
||||||
|
next();
|
||||||
|
} else if (implicit) {
|
||||||
|
/*
|
||||||
|
* No Docker service on this CloudAPI and this is an
|
||||||
|
* implicit attempt to setup for Docker, so that's fine.
|
||||||
|
* Use the early-abort signal.
|
||||||
|
*/
|
||||||
|
next(true);
|
||||||
|
} else {
|
||||||
|
next(new errors.SetupError(err, format(
|
||||||
|
'no "docker" service on this datacenter',
|
||||||
|
profile.url)));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// TODO: If this doesn't show res details, add that.
|
||||||
|
next(new errors.SetupError(err, format(
|
||||||
|
'unexpected response from cloudapi %s: %s status code',
|
||||||
|
profile.url, res.statusCode)));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
function mentionSettingUp(arg, next) {
|
||||||
|
console.log('Setting up profile "%s" to use Docker.', profile.name);
|
||||||
|
next();
|
||||||
|
},
|
||||||
|
|
||||||
|
function findSshPrivKey_keyPaths(arg, next) {
|
||||||
|
if (!opts.keyPaths) {
|
||||||
|
next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var privKeyPath = opts.keyPaths.private;
|
||||||
|
if (!privKeyPath) {
|
||||||
|
assert.string(opts.keyPaths.public);
|
||||||
|
assert.ok(opts.keyPaths.public.slice(-4) === '.pub');
|
||||||
|
privKeyPath = opts.keyPaths.public.slice(0, -4);
|
||||||
|
if (!fs.existsSync(privKeyPath)) {
|
||||||
|
cb(new errors.SetupError(format('could not find SSH '
|
||||||
|
+ 'private key file from public key file "%s": "%s" '
|
||||||
|
+ 'does not exist', opts.keyPaths.public,
|
||||||
|
privKeyPath)));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
arg.sshKeyPaths = {
|
||||||
|
private: privKeyPath,
|
||||||
|
public: opts.keyPaths.public
|
||||||
|
};
|
||||||
|
|
||||||
|
fs.readFile(privKeyPath, function (readErr, keyData) {
|
||||||
|
if (readErr) {
|
||||||
|
cb(readErr);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
arg.sshPrivKey = sshpk.parseKey(keyData, 'pem');
|
||||||
|
} catch (keyErr) {
|
||||||
|
cb(keyErr);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
log.trace({sshKeyPaths: arg.sshKeyPaths},
|
||||||
|
'profileDockerSetup: findSshPrivKey_keyPaths');
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
function findSshPrivKey_keyId(arg, next) {
|
||||||
|
if (opts.keyPaths) {
|
||||||
|
next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: keyPaths here is using a non-#master of node-smartdc-auth.
|
||||||
|
// Change back to a smartdc-auth release when
|
||||||
|
// https://github.com/joyent/node-smartdc-auth/pull/5 is in.
|
||||||
|
auth.loadSSHKey(profile.keyId, function (err, key, keyPaths) {
|
||||||
|
if (err) {
|
||||||
|
// TODO: better error message here.
|
||||||
|
next(err);
|
||||||
|
} else {
|
||||||
|
assert.ok(key, 'key from auth.loadSSHKey');
|
||||||
|
log.trace({keyId: profile.keyId, sshKeyPaths: keyPaths},
|
||||||
|
'profileDockerSetup: findSshPrivKey');
|
||||||
|
arg.sshKeyPaths = keyPaths;
|
||||||
|
arg.sshPrivKey = key;
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Find the `docker` version, if we can. This can be used later to
|
||||||
|
* control some envvars that depend on the docker version.
|
||||||
|
*/
|
||||||
|
function whichDocker(arg, next) {
|
||||||
|
which('docker', function (err, dockerPath) {
|
||||||
|
if (err) {
|
||||||
|
console.log(wordwrap('\nNote: No "docker" was found on '
|
||||||
|
+ 'your PATH. It is not needed for this setup, but '
|
||||||
|
+ 'will be to run docker commands against Triton. '
|
||||||
|
+ 'You can find out how to install it at '
|
||||||
|
+ '<https://docs.docker.com/engine/installation/>.'));
|
||||||
|
} else {
|
||||||
|
arg.dockerPath = dockerPath;
|
||||||
|
log.trace({dockerPath: dockerPath},
|
||||||
|
'profileDockerSetup: whichDocker');
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
function getDockerClientVersion(arg, next) {
|
||||||
|
if (!arg.dockerPath) {
|
||||||
|
next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
common.execPlus({
|
||||||
|
cmd: arg.dockerPath + ' --version',
|
||||||
|
log: log
|
||||||
|
}, function (err, stdout, stderr) {
|
||||||
|
if (err) {
|
||||||
|
console.log(
|
||||||
|
'\nWarning: Could not determine Docker version:\n%s',
|
||||||
|
common.indent(err.toString()));
|
||||||
|
} else {
|
||||||
|
// E.g.: 'Docker version 1.9.1, build a34a1d5'
|
||||||
|
// JSSTYLED
|
||||||
|
var DOCKER_VER_RE = /^Docker version (.*?), build/;
|
||||||
|
var match = DOCKER_VER_RE.exec(stdout);
|
||||||
|
if (!match) {
|
||||||
|
console.log('\nWarning: Could not determine Docker '
|
||||||
|
+ 'version: output of `%s --version` does not '
|
||||||
|
+ 'match %s: %j', arg.dockerPath, DOCKER_VER_RE,
|
||||||
|
stdout);
|
||||||
|
} else {
|
||||||
|
arg.dockerVersion = match[1];
|
||||||
|
log.trace({dockerVersion: arg.dockerVersion},
|
||||||
|
'profileDockerSetup: getDockerClientVersion');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
function genClientCert_dir(arg, next) {
|
||||||
|
arg.dockerCertPath = path.resolve(cli.configDir,
|
||||||
|
'docker', common.profileSlug(profile));
|
||||||
|
mkdirp(arg.dockerCertPath, next);
|
||||||
|
},
|
||||||
|
function genClientCert_key(arg, next) {
|
||||||
|
arg.keyPath = path.resolve(arg.dockerCertPath, 'key.pem');
|
||||||
|
common.execPlus({
|
||||||
|
cmd: format('openssl rsa -in %s -out %s -outform pem',
|
||||||
|
arg.sshKeyPaths.private, arg.keyPath),
|
||||||
|
log: log
|
||||||
|
}, next);
|
||||||
|
},
|
||||||
|
function genClientCert_csr(arg, next) {
|
||||||
|
arg.csrPath = path.resolve(arg.dockerCertPath, 'csr.pem');
|
||||||
|
common.execPlus({
|
||||||
|
cmd: format('openssl req -new -key %s -out %s -subj "/CN=%s"',
|
||||||
|
arg.keyPath, arg.csrPath, profile.account),
|
||||||
|
log: log
|
||||||
|
}, next);
|
||||||
|
},
|
||||||
|
function genClientCert_cert(arg, next) {
|
||||||
|
arg.certPath = path.resolve(arg.dockerCertPath, 'cert.pem');
|
||||||
|
common.execPlus({
|
||||||
|
cmd: format(
|
||||||
|
'openssl x509 -req -days 365 -in %s -signkey %s -out %s',
|
||||||
|
arg.csrPath, arg.keyPath, arg.certPath),
|
||||||
|
log: log
|
||||||
|
}, next);
|
||||||
|
},
|
||||||
|
function genClientCert_deleteCsr(arg, next) {
|
||||||
|
rimraf(arg.csrPath, next);
|
||||||
|
},
|
||||||
|
|
||||||
|
function getServerCa(arg, next) {
|
||||||
|
arg.caPath = path.resolve(arg.dockerCertPath, 'ca.pem');
|
||||||
|
var caUrl = dockerHost.replace(/^tcp:/, 'https:') + '/ca.pem';
|
||||||
|
downloadUrl({
|
||||||
|
url: caUrl,
|
||||||
|
dest: arg.caPath,
|
||||||
|
insecure: profile.insecure
|
||||||
|
}, next);
|
||||||
|
},
|
||||||
|
|
||||||
|
function writeSetupJson(arg, next) {
|
||||||
|
var setupJson = path.resolve(arg.dockerCertPath, 'setup.json');
|
||||||
|
var setup = {
|
||||||
|
profile: profile,
|
||||||
|
time: (new Date()).toISOString(),
|
||||||
|
env: {
|
||||||
|
DOCKER_CERT_PATH: arg.dockerCertPath,
|
||||||
|
DOCKER_HOST: dockerHost,
|
||||||
|
DOCKER_TLS_VERIFY: '1'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (profile.insecure) {
|
||||||
|
setup.env.DOCKER_TLS_VERIFY = null; // signal to unset it
|
||||||
|
} else {
|
||||||
|
setup.env.DOCKER_TLS_VERIFY = '1';
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Docker version 1.9.0 was released at the same time as
|
||||||
|
* Docker Compose 1.5.0. In that version they changed from using
|
||||||
|
* DOCKER_CLIENT_TIMEOUT to COMPOSE_HTTP_TIMEOUT and,
|
||||||
|
* annoyingly, added a warning that the user sees for all
|
||||||
|
* `compose ...` runs about DOCKER_CLIENT_TIMEOUT being
|
||||||
|
* deprecated. We want to avoid that deprecation message,
|
||||||
|
* but need the timeout value.
|
||||||
|
*
|
||||||
|
* This isn't fool proof (using mismatched `docker` and
|
||||||
|
* `docker-compose` versions). It is debatable if we want to
|
||||||
|
* play this game. E.g. someone moving from Docker 1.8 to newer,
|
||||||
|
* *but not re-setting up envvars* may start hitting timeouts.
|
||||||
|
*/
|
||||||
|
if (!arg.dockerVersion) {
|
||||||
|
setup.env.DOCKER_CLIENT_TIMEOUT = '300';
|
||||||
|
setup.env.COMPOSE_HTTP_TIMEOUT = '300';
|
||||||
|
} else if (semver.gte(arg.dockerVersion, '1.9.0')) {
|
||||||
|
setup.env.COMPOSE_HTTP_TIMEOUT = '300';
|
||||||
|
} else {
|
||||||
|
setup.env.DOCKER_CLIENT_TIMEOUT = '300';
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.writeFile(setupJson,
|
||||||
|
JSON.stringify(setup, null, 4) + '\n',
|
||||||
|
next);
|
||||||
|
},
|
||||||
|
|
||||||
|
function mentionSuccess(arg, next) {
|
||||||
|
console.log(
|
||||||
|
'Setup profile "%s" to use Docker%s. Try this:\n'
|
||||||
|
+ ' eval "$(triton env --docker%s)"\n'
|
||||||
|
+ ' docker%s info',
|
||||||
|
profile.name,
|
||||||
|
(arg.dockerVersion ? format(' (v%s)', arg.dockerVersion) : ''),
|
||||||
|
(profile.name === cli.profileName ? '' : ' ' + profile.name),
|
||||||
|
(profile.insecure ? ' --tls' : ''));
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
|
||||||
|
]}, function (err) {
|
||||||
|
tritonapi.close();
|
||||||
|
|
||||||
|
if (err === true) { // Early-abort signal.
|
||||||
|
err = null;
|
||||||
|
}
|
||||||
|
if (err && !dockerHost && implicit) {
|
||||||
|
console.error('Warning: Error determining ' +
|
||||||
|
'if CloudAPI "%s" provides a Docker service:\n %s',
|
||||||
|
profile.url, err);
|
||||||
|
err = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
cb(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
setCurrentProfile: setCurrentProfile
|
setCurrentProfile: setCurrentProfile,
|
||||||
|
profileDockerSetup: profileDockerSetup
|
||||||
};
|
};
|
||||||
|
@ -127,7 +127,7 @@ util.inherits(InternalError, _TritonBaseVError);
|
|||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* CLI usage error
|
* Error in config or profile data.
|
||||||
*/
|
*/
|
||||||
function ConfigError(cause, message) {
|
function ConfigError(cause, message) {
|
||||||
if (message === undefined) {
|
if (message === undefined) {
|
||||||
@ -164,6 +164,26 @@ function UsageError(cause, message) {
|
|||||||
util.inherits(UsageError, _TritonBaseVError);
|
util.inherits(UsageError, _TritonBaseVError);
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error in setting up (typically in profile update/creation).
|
||||||
|
*/
|
||||||
|
function SetupError(cause, message) {
|
||||||
|
if (message === undefined) {
|
||||||
|
message = cause;
|
||||||
|
cause = undefined;
|
||||||
|
}
|
||||||
|
assert.string(message);
|
||||||
|
_TritonBaseVError.call(this, {
|
||||||
|
cause: cause,
|
||||||
|
message: message,
|
||||||
|
code: 'Setup',
|
||||||
|
exitStatus: 1
|
||||||
|
});
|
||||||
|
}
|
||||||
|
util.inherits(SetupError, _TritonBaseVError);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An error signing a request.
|
* An error signing a request.
|
||||||
*/
|
*/
|
||||||
@ -280,6 +300,7 @@ module.exports = {
|
|||||||
InternalError: InternalError,
|
InternalError: InternalError,
|
||||||
ConfigError: ConfigError,
|
ConfigError: ConfigError,
|
||||||
UsageError: UsageError,
|
UsageError: UsageError,
|
||||||
|
SetupError: SetupError,
|
||||||
SigningError: SigningError,
|
SigningError: SigningError,
|
||||||
SelfSignedCertError: SelfSignedCertError,
|
SelfSignedCertError: SelfSignedCertError,
|
||||||
TimeoutError: TimeoutError,
|
TimeoutError: TimeoutError,
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "triton",
|
"name": "triton",
|
||||||
"description": "Joyent Triton CLI and client (https://www.joyent.com/triton)",
|
"description": "Joyent Triton CLI and client (https://www.joyent.com/triton)",
|
||||||
"version": "4.8.1",
|
"version": "4.9.0",
|
||||||
"author": "Joyent (joyent.com)",
|
"author": "Joyent (joyent.com)",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"assert-plus": "0.2.0",
|
"assert-plus": "0.2.0",
|
||||||
@ -18,12 +18,14 @@
|
|||||||
"restify-clients": "1.1.0",
|
"restify-clients": "1.1.0",
|
||||||
"restify-errors": "3.0.0",
|
"restify-errors": "3.0.0",
|
||||||
"rimraf": "2.4.4",
|
"rimraf": "2.4.4",
|
||||||
|
"semver": "5.1.0",
|
||||||
|
"smartdc-auth": "git+https://github.com/joyent/node-smartdc-auth.git#05d9077",
|
||||||
"sshpk": "1.7.x",
|
"sshpk": "1.7.x",
|
||||||
"smartdc-auth": "2.3.1",
|
|
||||||
"strsplit": "1.0.0",
|
"strsplit": "1.0.0",
|
||||||
"tabula": "1.7.0",
|
"tabula": "1.7.0",
|
||||||
"vasync": "1.6.3",
|
"vasync": "1.6.3",
|
||||||
"verror": "1.6.0",
|
"verror": "1.6.0",
|
||||||
|
"which": "1.2.4",
|
||||||
"wordwrap": "1.0.0"
|
"wordwrap": "1.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
Reference in New Issue
Block a user