diff --git a/lib/common.js b/lib/common.js index 93f3b68..f438735 100644 --- a/lib/common.js +++ b/lib/common.js @@ -5,7 +5,7 @@ */ /* - * Copyright 2015 Joyent, Inc. + * Copyright 2017 Joyent, Inc. */ var assert = require('assert-plus'); @@ -627,7 +627,7 @@ function promptEnter(prompt, cb) { * string "cancelled". */ function promptField(field, cb) { - var wrap = wordwrap(Math.min(process.stdout.columns, 78)); + var wrap = wordwrap(Math.min(process.stdout.columns, 80)); var validate = field.validate; if (!validate && field.required) { @@ -673,7 +673,21 @@ function promptField(field, cb) { } if (field.desc) { - console.log(ansiStylize(wrap(field.desc), 'bold')); + // Wrap, if no newlines. + var wrapped = field.desc; + if (field.desc.indexOf('\n') === -1) { + wrapped = wrap(field.desc); + } + + // Bold up to the first period, or all of it, if no period. + var periodIdx = wrapped.indexOf('.'); + if (periodIdx !== -1) { + console.log( + ansiStylize(wrapped.slice(0, periodIdx + 1), 'bold') + + wrapped.slice(periodIdx + 1)); + } else { + console.log(ansiStylize(wrap(field.desc), 'bold')); + } } attempt(); } diff --git a/lib/do_profile/do_create.js b/lib/do_profile/do_create.js index 2431ac4..6a43a38 100644 --- a/lib/do_profile/do_create.js +++ b/lib/do_profile/do_create.js @@ -1,6 +1,14 @@ /* - * Copyright (c) 2015 Joyent Inc. - * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +/* + * Copyright 2017 Joyent, Inc. + */ + +/* * `triton profile create ...` */ @@ -10,6 +18,7 @@ var fs = require('fs'); var sshpk = require('sshpk'); var vasync = require('vasync'); var auth = require('smartdc-auth'); +var wordwrap = require('wordwrap'); var common = require('../common'); var errors = require('../errors'); @@ -27,6 +36,7 @@ function _createProfile(opts, cb) { var log = cli.log; var data; + var wrap80 = wordwrap(Math.min(process.stdout.columns, 80)); vasync.pipeline({arg: {}, funcs: [ function getExistingProfiles(ctx, next) { @@ -57,9 +67,30 @@ function _createProfile(opts, cb) { next(); } }, - function gatherDataStdin(_, next) { - if (opts.file !== '-') { - return next(); + function determineInputType(ctx, next) { + /* + * Are we gathering profile data from stdin, a file, or + * interactively? + */ + if (opts.file === '-') { + ctx.inputType = 'stdin'; + } else if (opts.file) { + ctx.inputType = 'file'; + } else if (!process.stdin.isTTY) { + return next(new errors.UsageError('cannot interactively ' + + 'create profile: stdin is not a TTY')); + } else if (!process.stdout.isTTY) { + return next(new errors.UsageError('cannot interactively ' + + 'create profile: stdout is not a TTY')); + } else { + ctx.inputType = 'interactive'; + } + next(); + }, + function gatherDataStdin(ctx, next) { + if (ctx.inputType !== 'stdin') { + next(); + return; } var stdin = ''; process.stdin.resume(); @@ -78,8 +109,9 @@ function _createProfile(opts, cb) { }); }, function gatherDataFile(ctx, next) { - if (!opts.file || opts.file === '-') { - return next(); + if (ctx.inputType !== 'file') { + next(); + return; } ctx.filePath = opts.file; var input = fs.readFileSync(opts.file); @@ -91,19 +123,47 @@ function _createProfile(opts, cb) { } next(); }, - function gatherDataInteractive(ctx, next) { - if (opts.file) { - return next(); - } else if (!process.stdin.isTTY) { - return next(new errors.UsageError('cannot interactively ' + - 'create profile: stdin is not a TTY')); - } else if (!process.stdout.isTTY) { - return next(new errors.UsageError('cannot interactively ' + - 'create profile: stdout is not a TTY')); + + function interactiveGatherKeyChoices(ctx, next) { + if (ctx.inputType !== 'interactive') { + next(); + return; } + /* + * The description of the `keyId` field includes discovered SSH keys + * for this user. + */ var kr = new auth.KeyRing(); - var keyChoices = {}; + ctx.keyChoices = []; + + kr.list(function (err, pairs) { + if (err) { + next(err); + return; + } + Object.keys(pairs).forEach(function (keyId) { + var valid = pairs[keyId].filter(function (kp) { + return (kp.canSign()); + }); + if (valid.length < 1) + return; + + ctx.keyChoices.push({ + keyId: keyId, + keyPairs: pairs[keyId], + pubKey: valid[0].getPublicKey() + }); + }); + next(); + }); + }, + + function gatherDataInteractive(ctx, next) { + if (ctx.inputType !== 'interactive') { + next(); + return; + } var defaults = {}; if (ctx.copy) { @@ -113,9 +173,53 @@ function _createProfile(opts, cb) { defaults.url = 'https://us-sw-1.api.joyent.com'; } + /* + * The description of the `keyId` field includes discovered SSH keys + * for this user. + */ + var keyIdDesc = 'The fingerprint of the SSH key you want to use ' + + 'to authenticate with CloudAPI.\n' + + 'Specify the fingerprint or the index of one of the found ' + + 'keys in the list\n' + + 'below. If the key you want to use is not listed, make sure ' + + 'it is either saved\n' + + 'in your SSH keys directory (~/.ssh) or loaded into your ' + + 'SSH agent.\n'; + if (ctx.keyChoices.length === 0) { + keyIdDesc += '\n(No SSH keys were found.)\n'; + } else { + var n = 1; + ctx.keyChoices.forEach(function (keyChoice) { + keyIdDesc += format('\n%d. Fingerprint "%s" (%s-bit %s)\n', + n, keyChoice.keyId, keyChoice.pubKey.size, + keyChoice.pubKey.type.toUpperCase()); + keyChoice.keyPairs.forEach(function (kp) { + var lockedStr = (kp.isLocked() ? ' (locked)' : ''); + var comment = kp.comment || kp.getPublicKey().comment; + var detailsStr; + switch (kp.plugin) { + case 'agent': + detailsStr = comment; + break; + case 'homedir': + detailsStr = format('$HOME/.ssh/%s (comment "%s")', + kp.source, comment); + break; + default: + detailsStr = format('%s %s', + comment, (kp.source || '')); + break; + } + keyIdDesc += format(' - in %s%s: %s\n', + kp.plugin, lockedStr, detailsStr); + }); + n++; + }); + } + var fields = [ { - desc: 'A profile name. A short string to identify a ' + - 'CloudAPI endpoint to the `triton` CLI.', + desc: 'A profile name. A short string to identify this ' + + 'profile to the `triton` command.', key: 'name', default: defaults.name, validate: function validateName(value, valCb) { @@ -137,8 +241,6 @@ 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', @@ -155,10 +257,7 @@ function _createProfile(opts, cb) { valCb(); } }, { - desc: 'The fingerprint of the SSH key you want to use, or ' + - 'its index in the list above. If the key you want to ' + - 'use is not listed, make sure it is either saved in your ' + - 'SSH keys directory or loaded into the SSH agent.', + desc: keyIdDesc, key: 'keyId', validate: function validateKeyId(value, valCb) { // First try as a fingerprint. @@ -168,13 +267,16 @@ function _createProfile(opts, cb) { } catch (fpErr) { } - // Try as a list index - if (keyChoices[value] !== undefined) { - return valCb(null, keyChoices[value]); + // Try as a list index. + var idx = Number(value) - 1; + if (ctx.keyChoices[idx] !== undefined) { + var keyId = ctx.keyChoices[idx].keyId; + console.log('Using key %s: %s', value, keyId); + return valCb(null, keyId); } valCb(new Error(format( - '"%s" is neither a valid fingerprint, not an index ' + + '"%s" is neither a valid fingerprint, nor an index ' + 'from the list of available keys', value))); } } ]; @@ -203,49 +305,12 @@ function _createProfile(opts, cb) { inputs: fields, func: function getField(field, nextField) { if (field.key !== 'name') - console.log(); - if (field.key === 'keyId') { - kr.list(function (err, pairs) { - if (err) { - nextField(err); - return; - } - var choice = 1; - console.log('Available SSH keys:'); - Object.keys(pairs).forEach(function (keyId) { - var valid = pairs[keyId].filter(function (kp) { - return (kp.canSign()); - }); - if (valid.length < 1) - return; - var pub = valid[0].getPublicKey(); - console.log( - ' %d. %d-bit %s key with fingerprint %s', - choice, pub.size, pub.type.toUpperCase(), - keyId); - pairs[keyId].forEach(function (kp) { - var comment = kp.comment || - kp.getPublicKey().comment; - console.log(' * [in %s] %s %s %s', - kp.plugin, comment, - (kp.source ? kp.source : ''), - (kp.isLocked() ? '[locked]' : '')); - }); - console.log(); - keyChoices[choice] = keyId; - ++choice; - }); - common.promptField(field, function (err2, value) { - data[field.key] = value; - nextField(err2); - }); - }); - } else { - common.promptField(field, function (err, value) { - data[field.key] = value; - nextField(err); - }); - } + console.log('\n'); + + common.promptField(field, function (err, value) { + data[field.key] = value; + nextField(err); + }); } }, function (err) { console.log(); @@ -290,6 +355,12 @@ function _createProfile(opts, cb) { return; } + console.log(common.ansiStylize('\n\n# Docker setup\n', 'bold')); + console.log(wrap80('This section will setup authentication to ' + + 'Triton DataCenter\'s Docker endpoint using your account ' + + 'and key information specified above. This is only required ' + + 'if you intend to use `docker` with this profile.')); + profilecommon.profileDockerSetup({ cli: cli, name: data.name, diff --git a/lib/do_profile/profilecommon.js b/lib/do_profile/profilecommon.js index f428b38..62b8c55 100644 --- a/lib/do_profile/profilecommon.js +++ b/lib/do_profile/profilecommon.js @@ -1,6 +1,14 @@ /* - * Copyright 2016 Joyent Inc. - * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +/* + * Copyright 2017 Joyent, Inc. + */ + +/* * Shared stuff for `triton profile ...` handling. */ @@ -155,21 +163,19 @@ function profileDockerSetup(opts, cb) { vasync.pipeline({arg: {tritonapi: tritonapi}, funcs: [ function dockerKeyWarning(arg, next) { - console.log(wordwrap( - '\nWARNING: Docker uses TLS-based authentication with a ' + - 'different security model from SSH keys. As a result, the ' + - 'Docker client cannot currently support encrypted ' + - '(password protected) keys or SSH agents. If you ' + - 'continue, the Triton CLI will attempt to format a copy ' + - 'of your SSH *private* key as an unencrypted TLS cert ' + - 'and place the copy in ~/.triton/docker for use by the ' + - 'Docker client.')); + console.log(wordwrap('\nWARNING: Docker uses authentication via ' + + 'client TLS certificates that do not support encrypted ' + + '(passphrase protected) keys or SSH agents. If you continue,' + + 'this profile setup will attempt to write a copy of your ' + + 'SSH private key formatted as an unencrypted TLS certificate ' + + 'in "~/.triton/docker" for use by the Docker client.\n')); common.promptYesNo({msg: 'Continue? [y/n] '}, function (answer) { if (answer !== 'y') { console.error('Skipping Docker setup (you can run ' + '"triton profile docker-setup" later).'); next(true); } else { + console.log(); next(); } }); @@ -408,14 +414,20 @@ function profileDockerSetup(opts, cb) { }, 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', + console.log([ + 'Successfully setup profile "%s" to use Docker%s.', + '', + 'To setup environment variables to use the Docker client, run:', + ' eval "$(triton env --docker %s)"', + ' docker%s info', + 'Or you can place the commands in your shell profile, e.g.:', + ' triton env --docker %s >> ~/.profile' + ].join('\n'), profile.name, (arg.dockerVersion ? format(' (v%s)', arg.dockerVersion) : ''), - (profile.name === cli.profileName ? '' : ' ' + profile.name), - (profile.insecure ? ' --tls' : '')); + profile.name, + (profile.insecure ? ' --tls' : ''), + profile.name); next(); }