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:
Trent Mick 2016-03-18 16:51:22 -07:00
parent 5929632e08
commit 8cd4dd80eb
13 changed files with 698 additions and 47 deletions

View File

@ -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

View File

@ -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');

View File

@ -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

View File

@ -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);
} }

View File

@ -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).'
} }
]; ];

View File

@ -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.'
} }
]; ];

View 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;

View File

@ -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) {

View File

@ -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();
} }

View File

@ -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.

View File

@ -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
}; };

View File

@ -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,

View File

@ -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": {