joyent/node-triton#179 proposed UX tweaks to interactive triton profile create
Reviewed by: Todd Whiteman <todd.whiteman@joyent.com> Approved by: Todd Whiteman <todd.whiteman@joyent.com>
This commit is contained in:
parent
98ba032b5d
commit
86c689e809
@ -5,7 +5,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright 2015 Joyent, Inc.
|
* Copyright 2017 Joyent, Inc.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var assert = require('assert-plus');
|
var assert = require('assert-plus');
|
||||||
@ -627,7 +627,7 @@ function promptEnter(prompt, cb) {
|
|||||||
* string "cancelled".
|
* string "cancelled".
|
||||||
*/
|
*/
|
||||||
function promptField(field, cb) {
|
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;
|
var validate = field.validate;
|
||||||
if (!validate && field.required) {
|
if (!validate && field.required) {
|
||||||
@ -673,7 +673,21 @@ function promptField(field, cb) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (field.desc) {
|
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();
|
attempt();
|
||||||
}
|
}
|
||||||
|
@ -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 ...`
|
* `triton profile create ...`
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@ -10,6 +18,7 @@ var fs = require('fs');
|
|||||||
var sshpk = require('sshpk');
|
var sshpk = require('sshpk');
|
||||||
var vasync = require('vasync');
|
var vasync = require('vasync');
|
||||||
var auth = require('smartdc-auth');
|
var auth = require('smartdc-auth');
|
||||||
|
var wordwrap = require('wordwrap');
|
||||||
|
|
||||||
var common = require('../common');
|
var common = require('../common');
|
||||||
var errors = require('../errors');
|
var errors = require('../errors');
|
||||||
@ -27,6 +36,7 @@ function _createProfile(opts, cb) {
|
|||||||
var log = cli.log;
|
var log = cli.log;
|
||||||
|
|
||||||
var data;
|
var data;
|
||||||
|
var wrap80 = wordwrap(Math.min(process.stdout.columns, 80));
|
||||||
|
|
||||||
vasync.pipeline({arg: {}, funcs: [
|
vasync.pipeline({arg: {}, funcs: [
|
||||||
function getExistingProfiles(ctx, next) {
|
function getExistingProfiles(ctx, next) {
|
||||||
@ -57,9 +67,30 @@ function _createProfile(opts, cb) {
|
|||||||
next();
|
next();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
function gatherDataStdin(_, next) {
|
function determineInputType(ctx, next) {
|
||||||
if (opts.file !== '-') {
|
/*
|
||||||
return 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 = '';
|
var stdin = '';
|
||||||
process.stdin.resume();
|
process.stdin.resume();
|
||||||
@ -78,8 +109,9 @@ function _createProfile(opts, cb) {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
function gatherDataFile(ctx, next) {
|
function gatherDataFile(ctx, next) {
|
||||||
if (!opts.file || opts.file === '-') {
|
if (ctx.inputType !== 'file') {
|
||||||
return next();
|
next();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
ctx.filePath = opts.file;
|
ctx.filePath = opts.file;
|
||||||
var input = fs.readFileSync(opts.file);
|
var input = fs.readFileSync(opts.file);
|
||||||
@ -91,19 +123,47 @@ function _createProfile(opts, cb) {
|
|||||||
}
|
}
|
||||||
next();
|
next();
|
||||||
},
|
},
|
||||||
function gatherDataInteractive(ctx, next) {
|
|
||||||
if (opts.file) {
|
function interactiveGatherKeyChoices(ctx, next) {
|
||||||
return next();
|
if (ctx.inputType !== 'interactive') {
|
||||||
} else if (!process.stdin.isTTY) {
|
next();
|
||||||
return next(new errors.UsageError('cannot interactively ' +
|
return;
|
||||||
'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'));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The description of the `keyId` field includes discovered SSH keys
|
||||||
|
* for this user.
|
||||||
|
*/
|
||||||
var kr = new auth.KeyRing();
|
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 = {};
|
var defaults = {};
|
||||||
if (ctx.copy) {
|
if (ctx.copy) {
|
||||||
@ -113,9 +173,53 @@ function _createProfile(opts, cb) {
|
|||||||
defaults.url = 'https://us-sw-1.api.joyent.com';
|
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 = [ {
|
var fields = [ {
|
||||||
desc: 'A profile name. A short string to identify a ' +
|
desc: 'A profile name. A short string to identify this ' +
|
||||||
'CloudAPI endpoint to the `triton` CLI.',
|
'profile to the `triton` command.',
|
||||||
key: 'name',
|
key: 'name',
|
||||||
default: defaults.name,
|
default: defaults.name,
|
||||||
validate: function validateName(value, valCb) {
|
validate: function validateName(value, valCb) {
|
||||||
@ -137,8 +241,6 @@ 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',
|
||||||
@ -155,10 +257,7 @@ function _createProfile(opts, cb) {
|
|||||||
valCb();
|
valCb();
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
desc: 'The fingerprint of the SSH key you want to use, or ' +
|
desc: keyIdDesc,
|
||||||
'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.',
|
|
||||||
key: 'keyId',
|
key: 'keyId',
|
||||||
validate: function validateKeyId(value, valCb) {
|
validate: function validateKeyId(value, valCb) {
|
||||||
// First try as a fingerprint.
|
// First try as a fingerprint.
|
||||||
@ -168,13 +267,16 @@ function _createProfile(opts, cb) {
|
|||||||
} catch (fpErr) {
|
} catch (fpErr) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try as a list index
|
// Try as a list index.
|
||||||
if (keyChoices[value] !== undefined) {
|
var idx = Number(value) - 1;
|
||||||
return valCb(null, keyChoices[value]);
|
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(
|
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)));
|
'from the list of available keys', value)));
|
||||||
}
|
}
|
||||||
} ];
|
} ];
|
||||||
@ -203,49 +305,12 @@ function _createProfile(opts, cb) {
|
|||||||
inputs: fields,
|
inputs: fields,
|
||||||
func: function getField(field, nextField) {
|
func: function getField(field, nextField) {
|
||||||
if (field.key !== 'name')
|
if (field.key !== 'name')
|
||||||
console.log();
|
console.log('\n');
|
||||||
if (field.key === 'keyId') {
|
|
||||||
kr.list(function (err, pairs) {
|
common.promptField(field, function (err, value) {
|
||||||
if (err) {
|
data[field.key] = value;
|
||||||
nextField(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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, function (err) {
|
}, function (err) {
|
||||||
console.log();
|
console.log();
|
||||||
@ -290,6 +355,12 @@ function _createProfile(opts, cb) {
|
|||||||
return;
|
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({
|
profilecommon.profileDockerSetup({
|
||||||
cli: cli,
|
cli: cli,
|
||||||
name: data.name,
|
name: data.name,
|
||||||
|
@ -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.
|
* Shared stuff for `triton profile ...` handling.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@ -155,21 +163,19 @@ function profileDockerSetup(opts, cb) {
|
|||||||
|
|
||||||
vasync.pipeline({arg: {tritonapi: tritonapi}, funcs: [
|
vasync.pipeline({arg: {tritonapi: tritonapi}, funcs: [
|
||||||
function dockerKeyWarning(arg, next) {
|
function dockerKeyWarning(arg, next) {
|
||||||
console.log(wordwrap(
|
console.log(wordwrap('\nWARNING: Docker uses authentication via ' +
|
||||||
'\nWARNING: Docker uses TLS-based authentication with a ' +
|
'client TLS certificates that do not support encrypted ' +
|
||||||
'different security model from SSH keys. As a result, the ' +
|
'(passphrase protected) keys or SSH agents. If you continue,' +
|
||||||
'Docker client cannot currently support encrypted ' +
|
'this profile setup will attempt to write a copy of your ' +
|
||||||
'(password protected) keys or SSH agents. If you ' +
|
'SSH private key formatted as an unencrypted TLS certificate ' +
|
||||||
'continue, the Triton CLI will attempt to format a copy ' +
|
'in "~/.triton/docker" for use by the Docker client.\n'));
|
||||||
'of your SSH *private* key as an unencrypted TLS cert ' +
|
|
||||||
'and place the copy in ~/.triton/docker for use by the ' +
|
|
||||||
'Docker client.'));
|
|
||||||
common.promptYesNo({msg: 'Continue? [y/n] '}, function (answer) {
|
common.promptYesNo({msg: 'Continue? [y/n] '}, function (answer) {
|
||||||
if (answer !== 'y') {
|
if (answer !== 'y') {
|
||||||
console.error('Skipping Docker setup (you can run '
|
console.error('Skipping Docker setup (you can run '
|
||||||
+ '"triton profile docker-setup" later).');
|
+ '"triton profile docker-setup" later).');
|
||||||
next(true);
|
next(true);
|
||||||
} else {
|
} else {
|
||||||
|
console.log();
|
||||||
next();
|
next();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -408,14 +414,20 @@ function profileDockerSetup(opts, cb) {
|
|||||||
},
|
},
|
||||||
|
|
||||||
function mentionSuccess(arg, next) {
|
function mentionSuccess(arg, next) {
|
||||||
console.log(
|
console.log([
|
||||||
'Setup profile "%s" to use Docker%s. Try this:\n'
|
'Successfully setup profile "%s" to use Docker%s.',
|
||||||
+ ' eval "$(triton env --docker%s)"\n'
|
'',
|
||||||
+ ' docker%s info',
|
'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,
|
profile.name,
|
||||||
(arg.dockerVersion ? format(' (v%s)', arg.dockerVersion) : ''),
|
(arg.dockerVersion ? format(' (v%s)', arg.dockerVersion) : ''),
|
||||||
(profile.name === cli.profileName ? '' : ' ' + profile.name),
|
profile.name,
|
||||||
(profile.insecure ? ' --tls' : ''));
|
(profile.insecure ? ' --tls' : ''),
|
||||||
|
profile.name);
|
||||||
next();
|
next();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user