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.
|
||||
|
||||
|
||||
## 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
|
||||
|
70
lib/cli.js
70
lib/cli.js
@ -276,25 +276,38 @@ CLI.prototype.init = function (opts, args, callback) {
|
||||
|
||||
this.configDir = CONFIG_DIR;
|
||||
|
||||
this.__defineGetter__('tritonapi', function () {
|
||||
if (self._tritonapi === undefined) {
|
||||
var config = mod_config.loadConfig({
|
||||
this.__defineGetter__('config', function getConfig() {
|
||||
if (self._config === undefined) {
|
||||
self._config = mod_config.loadConfig({
|
||||
configDir: self.configDir
|
||||
});
|
||||
self.log.trace({config: config}, 'loaded config');
|
||||
|
||||
var profileName = opts.profile || config.profile || 'env';
|
||||
var profile = mod_config.loadProfile({
|
||||
configDir: self.configDir,
|
||||
name: profileName
|
||||
self.log.trace({config: self._config}, 'loaded config');
|
||||
}
|
||||
return self._config;
|
||||
});
|
||||
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({
|
||||
log: self.log,
|
||||
profile: profile,
|
||||
config: config
|
||||
profile: self.profile,
|
||||
config: self.config
|
||||
});
|
||||
}
|
||||
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
|
||||
CLI.prototype.do_completion = require('./do_completion');
|
||||
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
|
||||
|
||||
|
@ -424,7 +424,7 @@ function saveProfileSync(opts) {
|
||||
mkdirp.sync(path.dirname(profilePath));
|
||||
}
|
||||
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 ...`
|
||||
*/
|
||||
@ -7,6 +7,7 @@
|
||||
var assert = require('assert-plus');
|
||||
var format = require('util').format;
|
||||
var fs = require('fs');
|
||||
var path = require('path');
|
||||
var strsplit = require('strsplit');
|
||||
var sshpk = require('sshpk');
|
||||
var vasync = require('vasync');
|
||||
@ -18,6 +19,7 @@ var mod_config = require('./config');
|
||||
|
||||
|
||||
function do_env(subcmd, opts, args, cb) {
|
||||
var self = this;
|
||||
if (opts.help) {
|
||||
this.do_help('help', {}, [subcmd], cb);
|
||||
return;
|
||||
@ -27,16 +29,27 @@ function do_env(subcmd, opts, args, cb) {
|
||||
}
|
||||
|
||||
var profileName = args[0] || this.tritonapi.profile.name;
|
||||
var allClientTypes = ['smartdc', 'triton'];
|
||||
var allClientTypes = ['triton', 'docker', 'smartdc'];
|
||||
var clientTypes = [];
|
||||
if (opts.smartdc) {
|
||||
clientTypes.push('smartdc');
|
||||
}
|
||||
var explicit;
|
||||
var shortOpts = '';
|
||||
if (opts.triton) {
|
||||
shortOpts += 't';
|
||||
clientTypes.push('triton');
|
||||
}
|
||||
if (opts.docker) {
|
||||
shortOpts += 'd';
|
||||
clientTypes.push('docker');
|
||||
}
|
||||
if (opts.smartdc) {
|
||||
shortOpts += 's';
|
||||
clientTypes.push('smartdc');
|
||||
}
|
||||
if (clientTypes.length === 0) {
|
||||
explicit = false;
|
||||
clientTypes = allClientTypes;
|
||||
} else {
|
||||
explicit = true;
|
||||
}
|
||||
|
||||
try {
|
||||
@ -52,15 +65,40 @@ function do_env(subcmd, opts, args, cb) {
|
||||
}
|
||||
|
||||
var p = console.log;
|
||||
var shortOpts = '';
|
||||
clientTypes.forEach(function (clientType) {
|
||||
switch (clientType) {
|
||||
case 'triton':
|
||||
shortOpts += 't';
|
||||
p('export TRITON_PROFILE="%s"', profile.name);
|
||||
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':
|
||||
shortOpts += 's';
|
||||
p('export SDC_URL="%s"', profile.url);
|
||||
p('export SDC_ACCOUNT="%s"', profile.account);
|
||||
if (profile.user) {
|
||||
@ -82,8 +120,10 @@ function do_env(subcmd, opts, args, cb) {
|
||||
});
|
||||
|
||||
p('# Run this command to configure your shell:');
|
||||
p('# eval "$(triton env%s %s)"',
|
||||
(shortOpts ? ' -'+shortOpts : ''), profile.name);
|
||||
p('# eval "$(triton env%s%s)"',
|
||||
(shortOpts ? ' -'+shortOpts : ''),
|
||||
(profile.name === this.tritonapi.profile.name
|
||||
? '' : ' ' + profile.name));
|
||||
}
|
||||
|
||||
do_env.options = [
|
||||
@ -92,16 +132,21 @@ do_env.options = [
|
||||
type: 'bool',
|
||||
help: 'Show this help.'
|
||||
},
|
||||
{
|
||||
names: ['smartdc', 's'],
|
||||
type: 'bool',
|
||||
help: 'Emit environment for node-smartdc (i.e. the "SDC_*" variables).'
|
||||
},
|
||||
{
|
||||
names: ['triton', 't'],
|
||||
type: 'bool',
|
||||
help: 'Emit environment commands for node-triton itself (i.e. the ' +
|
||||
'"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 errors = require('../errors');
|
||||
var mod_config = require('../config');
|
||||
var profilecommon = require('./profilecommon');
|
||||
|
||||
|
||||
function _createProfile(opts, cb) {
|
||||
assert.object(opts.cli, 'opts.cli');
|
||||
assert.optionalString(opts.file, 'opts.file');
|
||||
assert.optionalString(opts.copy, 'opts.copy');
|
||||
assert.optionalBool(opts.noDocker, 'opts.noDocker');
|
||||
assert.func(cb, 'cb');
|
||||
var cli = opts.cli;
|
||||
var log = cli.log;
|
||||
@ -136,6 +138,8 @@ function _createProfile(opts, cb) {
|
||||
desc: 'The CloudAPI endpoint URL.',
|
||||
default: defaults.url,
|
||||
key: 'url'
|
||||
// TODO: shortcut to allow 'ssh nightly1' to have this ssh
|
||||
// in and find cloudapi for me
|
||||
}, {
|
||||
desc: 'Your account login name.',
|
||||
key: 'account',
|
||||
@ -186,6 +190,19 @@ function _createProfile(opts, cb) {
|
||||
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();
|
||||
console.log('Fingerprint: %s', newVal);
|
||||
valCb(null, newVal);
|
||||
@ -259,6 +276,21 @@ function _createProfile(opts, cb) {
|
||||
}
|
||||
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) {
|
||||
if (ctx.profiles.length !== 0) {
|
||||
next();
|
||||
@ -276,7 +308,7 @@ function _createProfile(opts, cb) {
|
||||
return;
|
||||
}
|
||||
console.log('Set "%s" as current profile (because it is ' +
|
||||
'your only profile)', data.name);
|
||||
'your only profile).', data.name);
|
||||
next();
|
||||
});
|
||||
}
|
||||
@ -299,7 +331,8 @@ function do_create(subcmd, opts, args, cb) {
|
||||
_createProfile({
|
||||
cli: this.top,
|
||||
file: opts.file,
|
||||
copy: opts.copy
|
||||
copy: opts.copy,
|
||||
noDocker: opts.no_docker
|
||||
}, cb);
|
||||
}
|
||||
|
||||
@ -321,6 +354,14 @@ do_create.options = [
|
||||
type: 'string',
|
||||
helpArg: 'NAME',
|
||||
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 ...`
|
||||
*/
|
||||
|
||||
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 mod_config = require('../config');
|
||||
var profilecommon = require('./profilecommon');
|
||||
|
||||
|
||||
function _showProfile(opts, cb) {
|
||||
|
@ -78,11 +78,16 @@ function _listProfiles(cli, opts, args, cb) {
|
||||
sort: sort
|
||||
});
|
||||
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. '
|
||||
+ 'Use "triton profile set-current ..."\n'
|
||||
+ 'to set one or "triton profile create" to create one.\n');
|
||||
}
|
||||
}
|
||||
}
|
||||
cb();
|
||||
}
|
||||
|
||||
|
@ -41,7 +41,8 @@ function ProfileCLI(top) {
|
||||
'set-current',
|
||||
'create',
|
||||
'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_delete = require('./do_delete');
|
||||
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 ...`
|
||||
// And then would like that same key=value syntax optional for create.
|
||||
|
@ -5,11 +5,68 @@
|
||||
*/
|
||||
|
||||
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 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) {
|
||||
assert.object(opts.cli, 'opts.cli');
|
||||
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 = {
|
||||
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) {
|
||||
if (message === undefined) {
|
||||
@ -164,6 +164,26 @@ function UsageError(cause, message) {
|
||||
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.
|
||||
*/
|
||||
@ -280,6 +300,7 @@ module.exports = {
|
||||
InternalError: InternalError,
|
||||
ConfigError: ConfigError,
|
||||
UsageError: UsageError,
|
||||
SetupError: SetupError,
|
||||
SigningError: SigningError,
|
||||
SelfSignedCertError: SelfSignedCertError,
|
||||
TimeoutError: TimeoutError,
|
||||
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "triton",
|
||||
"description": "Joyent Triton CLI and client (https://www.joyent.com/triton)",
|
||||
"version": "4.8.1",
|
||||
"version": "4.9.0",
|
||||
"author": "Joyent (joyent.com)",
|
||||
"dependencies": {
|
||||
"assert-plus": "0.2.0",
|
||||
@ -18,12 +18,14 @@
|
||||
"restify-clients": "1.1.0",
|
||||
"restify-errors": "3.0.0",
|
||||
"rimraf": "2.4.4",
|
||||
"semver": "5.1.0",
|
||||
"smartdc-auth": "git+https://github.com/joyent/node-smartdc-auth.git#05d9077",
|
||||
"sshpk": "1.7.x",
|
||||
"smartdc-auth": "2.3.1",
|
||||
"strsplit": "1.0.0",
|
||||
"tabula": "1.7.0",
|
||||
"vasync": "1.6.3",
|
||||
"verror": "1.6.0",
|
||||
"which": "1.2.4",
|
||||
"wordwrap": "1.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
Reference in New Issue
Block a user