clistyle: triton profile ...
This commit is contained in:
parent
632d5a6568
commit
f4246b5faf
21
CHANGES.md
21
CHANGES.md
@ -1,5 +1,26 @@
|
||||
# node-triton changelog
|
||||
|
||||
## 4.0.0 (not yet released)
|
||||
|
||||
- Add the ability to create a profile copying from an existing profile,
|
||||
via `triton profile create --copy NAME`.
|
||||
|
||||
- [backwards incompat] #66 `triton profile` now has list, get, etc. sub-commands.
|
||||
One backwards incompatible change here is that `triton profile NAME` is
|
||||
now `triton profile get NAME`. Note that for bwcompat `triton profiles` is
|
||||
a shortcut for `triton profile list`.
|
||||
|
||||
- [backwards incompat] #66 `triton image` now has list, get sub-commands.
|
||||
One backwards incompatible change here is that `triton image ID|NAME` is
|
||||
now `triton image get ID|NAME`. Note that for bwcompat `triton images` is
|
||||
a shortcut for `triton image list`.
|
||||
|
||||
- [backwards incompat] #66 `triton package` now has list, get sub-commands.
|
||||
One backwards incompatible change here is that `triton package ID|NAME` is
|
||||
now `triton package get ID|NAME`. Note that for bwcompat `triton packages` is
|
||||
a shortcut for `triton package list`.
|
||||
|
||||
|
||||
## 3.6.1 (not yet released)
|
||||
|
||||
(nothing yet)
|
||||
|
@ -192,7 +192,6 @@ function CLI() {
|
||||
},
|
||||
helpSubcmds: [
|
||||
'help',
|
||||
'profiles',
|
||||
'profile',
|
||||
{ group: 'Instances (aka VMs/Machines/Containers)' },
|
||||
'create-instance',
|
||||
|
@ -1,631 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2015 Joyent Inc.
|
||||
*
|
||||
* `triton profile ...`
|
||||
*/
|
||||
|
||||
var assert = require('assert-plus');
|
||||
var format = require('util').format;
|
||||
var fs = require('fs');
|
||||
var strsplit = require('strsplit');
|
||||
var sshpk = require('sshpk');
|
||||
var tilde = require('tilde-expansion');
|
||||
var vasync = require('vasync');
|
||||
|
||||
var common = require('./common');
|
||||
var errors = require('./errors');
|
||||
var mod_config = require('./config');
|
||||
|
||||
|
||||
|
||||
function _showProfile(opts, cb) {
|
||||
assert.object(opts.cli, 'opts.cli');
|
||||
assert.string(opts.name, 'opts.name');
|
||||
assert.func(cb, 'cb');
|
||||
var cli = opts.cli;
|
||||
|
||||
try {
|
||||
var profile = mod_config.loadProfile({
|
||||
configDir: cli.configDir,
|
||||
name: opts.name
|
||||
});
|
||||
} catch (err) {
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
if (profile.name === cli.tritonapi.profile.name) {
|
||||
cli._applyProfileOverrides(profile);
|
||||
profile.curr = true;
|
||||
} else {
|
||||
profile.curr = false;
|
||||
}
|
||||
|
||||
if (opts.json) {
|
||||
console.log(JSON.stringify(profile));
|
||||
} else {
|
||||
console.log('name: %s', profile.name);
|
||||
Object.keys(profile).sort().forEach(function (key) {
|
||||
if (key === 'name')
|
||||
return;
|
||||
if (profile[key] !== undefined)
|
||||
console.log('%s: %s', key, profile[key]);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function _currentProfile(opts, cb) {
|
||||
assert.object(opts.cli, 'opts.cli');
|
||||
assert.string(opts.name, 'opts.name');
|
||||
assert.func(cb, 'cb');
|
||||
var cli = opts.cli;
|
||||
|
||||
try {
|
||||
var profile = mod_config.loadProfile({
|
||||
configDir: cli.configDir,
|
||||
name: opts.name
|
||||
});
|
||||
} catch (err) {
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
if (cli.tritonapi.profile.name === profile.name) {
|
||||
console.log('"%s" is already the current profile', profile.name);
|
||||
return cb();
|
||||
}
|
||||
|
||||
mod_config.setConfigVar({
|
||||
configDir: cli.configDir,
|
||||
name: 'profile',
|
||||
value: profile.name
|
||||
}, function (err) {
|
||||
if (err) {
|
||||
return cb(err);
|
||||
}
|
||||
console.log('Set "%s" as current profile', profile.name);
|
||||
cb();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function _yamlishFromProfile(profile) {
|
||||
assert.object(profile, 'profile');
|
||||
|
||||
var keys = [];
|
||||
var skipKeys = ['curr', 'name'];
|
||||
Object.keys(profile).forEach(function (key) {
|
||||
if (skipKeys.indexOf(key) === -1) {
|
||||
keys.push(key);
|
||||
}
|
||||
});
|
||||
keys = keys.sort();
|
||||
|
||||
var lines = [];
|
||||
keys.forEach(function (key) {
|
||||
lines.push(format('%s: %s', key, profile[key]));
|
||||
});
|
||||
return lines.join('\n') + '\n';
|
||||
}
|
||||
|
||||
function _profileFromYamlish(yamlish) {
|
||||
assert.string(yamlish, 'yamlish');
|
||||
|
||||
var profile = {};
|
||||
var bools = ['insecure'];
|
||||
var lines = yamlish.split(/\n/g);
|
||||
lines.forEach(function (line) {
|
||||
var commentIdx = line.indexOf('#');
|
||||
if (commentIdx !== -1) {
|
||||
line = line.slice(0, commentIdx);
|
||||
}
|
||||
line = line.trim();
|
||||
if (!line) {
|
||||
return;
|
||||
}
|
||||
var parts = strsplit(line, ':', 2);
|
||||
var key = parts[0].trim();
|
||||
var value = parts[1].trim();
|
||||
if (bools.indexOf(key) !== -1) {
|
||||
value = common.boolFromString(value);
|
||||
}
|
||||
profile[key] = value;
|
||||
});
|
||||
|
||||
return profile;
|
||||
}
|
||||
|
||||
|
||||
function _editProfile(opts, cb) {
|
||||
assert.object(opts.cli, 'opts.cli');
|
||||
assert.string(opts.name, 'opts.name');
|
||||
assert.func(cb, 'cb');
|
||||
var cli = opts.cli;
|
||||
|
||||
if (opts.name === 'env') {
|
||||
return cb(new errors.UsageError('cannot edit "env" profile'));
|
||||
}
|
||||
|
||||
try {
|
||||
var profile = mod_config.loadProfile({
|
||||
configDir: cli.configDir,
|
||||
name: opts.name
|
||||
});
|
||||
} catch (err) {
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
var filename = format('profile-%s.txt', profile.name);
|
||||
var origText = _yamlishFromProfile(profile);
|
||||
|
||||
function editAttempt(text) {
|
||||
common.editInEditor({
|
||||
text: text,
|
||||
filename: filename
|
||||
}, function (err, afterText, changed) {
|
||||
if (err) {
|
||||
return cb(new errors.TritonError(err));
|
||||
} else if (!changed) {
|
||||
console.log('No change to profile');
|
||||
return cb();
|
||||
}
|
||||
|
||||
try {
|
||||
var editedProfile = _profileFromYamlish(afterText);
|
||||
editedProfile.name = profile.name;
|
||||
|
||||
if (_yamlishFromProfile(editedProfile) === origText) {
|
||||
// This YAMLish is the closest to a canonical form we have.
|
||||
console.log('No change to profile');
|
||||
return cb();
|
||||
}
|
||||
|
||||
mod_config.saveProfileSync({
|
||||
configDir: cli.configDir,
|
||||
profile: editedProfile
|
||||
});
|
||||
} catch (textErr) {
|
||||
console.error('Error with your changes: %s', textErr);
|
||||
common.promptEnter(
|
||||
'Press <Enter> to re-edit, Ctrl+C to abort.',
|
||||
function (aborted) {
|
||||
if (aborted) {
|
||||
console.log('\nAborting. ' +
|
||||
'No change made to profile');
|
||||
cb();
|
||||
} else {
|
||||
editAttempt(afterText);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
cb();
|
||||
});
|
||||
}
|
||||
|
||||
editAttempt(origText);
|
||||
}
|
||||
|
||||
|
||||
function _deleteProfile(opts, cb) {
|
||||
assert.object(opts.cli, 'opts.cli');
|
||||
assert.string(opts.name, 'opts.name');
|
||||
assert.bool(opts.force, 'opts.force');
|
||||
assert.func(cb, 'cb');
|
||||
var cli = opts.cli;
|
||||
|
||||
if (opts.name === 'env') {
|
||||
return cb(new errors.UsageError('cannot delete "env" profile'));
|
||||
}
|
||||
|
||||
try {
|
||||
var profile = mod_config.loadProfile({
|
||||
configDir: cli.configDir,
|
||||
name: opts.name
|
||||
});
|
||||
} catch (err) {
|
||||
if (opts.force) {
|
||||
cb();
|
||||
} else {
|
||||
cb(err);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (profile.name === cli.tritonapi.profile.name && !opts.force) {
|
||||
return cb(new errors.TritonError(
|
||||
'cannot delete the current profile (use --force to override)'));
|
||||
}
|
||||
|
||||
vasync.pipeline({funcs: [
|
||||
function confirm(_, next) {
|
||||
if (opts.force) {
|
||||
return next();
|
||||
}
|
||||
common.promptYesNo({
|
||||
msg: 'Delete profile "' + opts.name + '"? [y/n] '
|
||||
}, function (answer) {
|
||||
if (answer !== 'y') {
|
||||
console.error('Aborting');
|
||||
next(true); // early abort signal
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
function handleConfigVar(_, next) {
|
||||
if (profile.name === cli.tritonapi.profile.name) {
|
||||
_currentProfile({name: 'env', cli: cli}, next);
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
},
|
||||
|
||||
function deleteIt(_, next) {
|
||||
try {
|
||||
mod_config.deleteProfile({
|
||||
configDir: cli.configDir,
|
||||
name: opts.name
|
||||
});
|
||||
} catch (delErr) {
|
||||
return next(delErr);
|
||||
}
|
||||
console.log('Deleted profile "%s"', opts.name);
|
||||
next();
|
||||
}
|
||||
]}, function (err) {
|
||||
if (err === true) {
|
||||
err = null;
|
||||
}
|
||||
cb(err);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function _addProfile(opts, cb) {
|
||||
assert.object(opts.cli, 'opts.cli');
|
||||
assert.optionalString(opts.file, 'opts.file');
|
||||
assert.func(cb, 'cb');
|
||||
var cli = opts.cli;
|
||||
var log = cli.log;
|
||||
|
||||
var context;
|
||||
var data;
|
||||
|
||||
vasync.pipeline({arg: {}, funcs: [
|
||||
function getExistingProfiles(ctx, next) {
|
||||
try {
|
||||
ctx.profiles = mod_config.loadAllProfiles({
|
||||
configDir: cli.configDir,
|
||||
log: cli.log
|
||||
});
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
next();
|
||||
},
|
||||
function gatherDataStdin(_, next) {
|
||||
if (opts.file !== '-') {
|
||||
return next();
|
||||
}
|
||||
var stdin = '';
|
||||
process.stdin.resume();
|
||||
process.stdin.on('data', function (chunk) {
|
||||
stdin += chunk;
|
||||
});
|
||||
process.stdin.on('end', function () {
|
||||
try {
|
||||
data = JSON.parse(stdin);
|
||||
} catch (err) {
|
||||
log.trace({stdin: stdin}, 'invalid profile JSON on stdin');
|
||||
return next(new errors.TritonError(
|
||||
format('invalid profile JSON on stdin: %s', err)));
|
||||
}
|
||||
next();
|
||||
});
|
||||
},
|
||||
function gatherDataFile(_, next) {
|
||||
if (!opts.file || opts.file === '-') {
|
||||
return next();
|
||||
}
|
||||
context = opts.file;
|
||||
var input = fs.readFileSync(opts.file);
|
||||
try {
|
||||
data = JSON.parse(input);
|
||||
} catch (err) {
|
||||
return next(new errors.TritonError(format(
|
||||
'invalid profile JSON in "%s": %s', opts.file, err)));
|
||||
}
|
||||
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'));
|
||||
}
|
||||
|
||||
var fields = [ {
|
||||
desc: 'A profile name. A short string to identify a ' +
|
||||
'CloudAPI endpoint to the `triton` CLI.',
|
||||
key: 'name',
|
||||
validate: function validateName(value, valCb) {
|
||||
var regex = /^[a-z][a-z0-9_.-]*$/;
|
||||
if (!regex.test(value)) {
|
||||
return valCb(new Error('Must start with a lowercase ' +
|
||||
'letter followed by lowercase letters, numbers ' +
|
||||
'and "_", "." and "-".'));
|
||||
}
|
||||
for (var i = 0; i < ctx.profiles.length; i++) {
|
||||
if (ctx.profiles[i].name === value) {
|
||||
return valCb(new Error(format(
|
||||
'Profile "%s" already exists.', value)));
|
||||
}
|
||||
}
|
||||
valCb();
|
||||
}
|
||||
}, {
|
||||
desc: 'The CloudAPI endpoint URL.',
|
||||
default: 'https://us-sw-1.api.joyent.com',
|
||||
key: 'url'
|
||||
}, {
|
||||
desc: 'Your account login name.',
|
||||
key: 'account',
|
||||
validate: function validateAccount(value, valCb) {
|
||||
var regex = /^[^\\]{3,}$/;
|
||||
if (value.length < 3) {
|
||||
return valCb(new Error(
|
||||
'Must be at least 3 characters'));
|
||||
}
|
||||
if (!regex.test(value)) {
|
||||
return valCb(new Error('Must not container a "\\"'));
|
||||
}
|
||||
valCb();
|
||||
}
|
||||
}, {
|
||||
desc: 'The fingerprint of the SSH key you have registered ' +
|
||||
'for your account. You may enter a local path to a ' +
|
||||
'public or private key to have the fingerprint ' +
|
||||
'calculated for you.',
|
||||
key: 'keyId',
|
||||
validate: function validateKeyId(value, valCb) {
|
||||
// First try as a fingerprint.
|
||||
try {
|
||||
sshpk.parseFingerprint(value);
|
||||
return valCb();
|
||||
} catch (fpErr) {
|
||||
}
|
||||
|
||||
// Try as a local path.
|
||||
tilde(value, function (keyPath) {
|
||||
fs.stat(keyPath, function (statErr, stats) {
|
||||
if (statErr || !stats.isFile()) {
|
||||
return valCb(new Error(format(
|
||||
'"%s" is neither a valid fingerprint, ' +
|
||||
'nor an existing file', value)));
|
||||
}
|
||||
fs.readFile(keyPath, function (readErr, keyData) {
|
||||
if (readErr) {
|
||||
return valCb(readErr);
|
||||
}
|
||||
var keyType = (keyPath.slice(-4) === '.pub'
|
||||
? 'ssh' : 'pem');
|
||||
try {
|
||||
var key = sshpk.parseKey(keyData, keyType);
|
||||
} catch (keyErr) {
|
||||
return valCb(keyErr);
|
||||
}
|
||||
|
||||
var newVal = key.fingerprint('md5').toString();
|
||||
console.log('Fingerprint: %s', newVal);
|
||||
valCb(null, newVal);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
} ];
|
||||
|
||||
data = {};
|
||||
vasync.forEachPipeline({
|
||||
inputs: fields,
|
||||
func: function getField(field, nextField) {
|
||||
if (field.key !== 'name') console.log();
|
||||
common.promptField(field, function (err, value) {
|
||||
data[field.key] = value;
|
||||
nextField(err);
|
||||
});
|
||||
}
|
||||
}, function (err) {
|
||||
console.log();
|
||||
next(err);
|
||||
});
|
||||
},
|
||||
function guardAlreadyExists(ctx, next) {
|
||||
for (var i = 0; i < ctx.profiles.length; i++) {
|
||||
if (data.name === ctx.profiles[i].name) {
|
||||
return next(new errors.TritonError(format(
|
||||
'profile "%s" already exists', data.name)));
|
||||
}
|
||||
}
|
||||
next();
|
||||
},
|
||||
function validateIt(_, next) {
|
||||
// We ignore 'curr'. For now at least.
|
||||
delete data.curr;
|
||||
|
||||
try {
|
||||
mod_config.validateProfile(data, context);
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
next();
|
||||
},
|
||||
function saveIt(_, next) {
|
||||
try {
|
||||
mod_config.saveProfileSync({
|
||||
configDir: cli.configDir,
|
||||
profile: data
|
||||
});
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
next();
|
||||
},
|
||||
function setCurrIfTheOnlyProfile(ctx, next) {
|
||||
if (ctx.profiles.length !== 0) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
mod_config.setConfigVar({
|
||||
configDir: cli.configDir,
|
||||
name: 'profile',
|
||||
value: data.name
|
||||
}, function (err) {
|
||||
if (err) {
|
||||
next(err);
|
||||
return;
|
||||
}
|
||||
console.log('Set "%s" as current profile (because it is ' +
|
||||
'your only profile)', data.name);
|
||||
next();
|
||||
});
|
||||
}
|
||||
]}, cb);
|
||||
}
|
||||
|
||||
|
||||
|
||||
function do_profile(subcmd, opts, args, cb) {
|
||||
if (opts.help) {
|
||||
this.do_help('help', {}, [subcmd], cb);
|
||||
return;
|
||||
}
|
||||
|
||||
// Which action?
|
||||
var actions = [];
|
||||
if (opts.add) { actions.push('add'); }
|
||||
if (opts.current) { actions.push('current'); }
|
||||
if (opts.edit) { actions.push('edit'); }
|
||||
if (opts['delete']) { actions.push('delete'); }
|
||||
var action;
|
||||
if (actions.length === 0) {
|
||||
action = 'show';
|
||||
} else if (actions.length > 1) {
|
||||
return cb(new errors.UsageError(
|
||||
'only one action option may be used at once'));
|
||||
} else {
|
||||
action = actions[0];
|
||||
}
|
||||
|
||||
// Arg count validation.
|
||||
if (args.length > 1) {
|
||||
return cb(new errors.UsageError('too many arguments'));
|
||||
} else if (args.length === 0 &&
|
||||
['current', 'delete'].indexOf(action) !== -1)
|
||||
{
|
||||
return cb(new errors.UsageError('NAME argument is required'));
|
||||
}
|
||||
|
||||
switch (action) {
|
||||
case 'show':
|
||||
_showProfile({
|
||||
cli: this,
|
||||
name: args[0] || this.tritonapi.profile.name,
|
||||
json: opts.json
|
||||
}, cb);
|
||||
break;
|
||||
case 'current':
|
||||
_currentProfile({cli: this, name: args[0]}, cb);
|
||||
break;
|
||||
case 'edit':
|
||||
_editProfile({
|
||||
cli: this,
|
||||
name: args[0] || this.tritonapi.profile.name
|
||||
}, cb);
|
||||
break;
|
||||
case 'delete':
|
||||
_deleteProfile({
|
||||
cli: this,
|
||||
name: args[0],
|
||||
force: Boolean(opts.force)
|
||||
}, cb);
|
||||
break;
|
||||
case 'add':
|
||||
_addProfile({cli: this, file: args[0]}, cb);
|
||||
break;
|
||||
default:
|
||||
return cb(new errors.InternalError('unknown action: ' + action));
|
||||
}
|
||||
}
|
||||
|
||||
do_profile.options = [
|
||||
{
|
||||
names: ['help', 'h'],
|
||||
type: 'bool',
|
||||
help: 'Show this help.'
|
||||
},
|
||||
{
|
||||
names: ['json', 'j'],
|
||||
type: 'bool',
|
||||
help: 'JSON output when showing a profile.'
|
||||
},
|
||||
{
|
||||
names: ['force', 'f'],
|
||||
type: 'bool',
|
||||
help: 'Force deletion.'
|
||||
},
|
||||
{
|
||||
group: 'Action Options'
|
||||
},
|
||||
{
|
||||
names: ['current', 'c'],
|
||||
type: 'bool',
|
||||
help: 'Switch to the named profile.'
|
||||
},
|
||||
{
|
||||
names: ['edit', 'e'],
|
||||
type: 'bool',
|
||||
help: 'Edit the named profile in your $EDITOR.'
|
||||
},
|
||||
{
|
||||
names: ['add', 'a'],
|
||||
type: 'bool',
|
||||
help: 'Add a new profile.'
|
||||
},
|
||||
{
|
||||
names: ['delete', 'd'],
|
||||
type: 'bool',
|
||||
help: 'Delete the named profile.'
|
||||
}
|
||||
];
|
||||
|
||||
do_profile.help = [
|
||||
'Show, add, edit and delete `triton` CLI profiles.',
|
||||
'',
|
||||
'A profile is a configured Triton CloudAPI endpoint. I.e. the',
|
||||
'url, account, key, etc. information required to call a CloudAPI.',
|
||||
'You can then switch between profiles with `triton -p PROFILE`',
|
||||
'or the TRITON_PROFILE environment variable.',
|
||||
'',
|
||||
'Usage:',
|
||||
' {{name}} profile [NAME] # show NAME or current profile',
|
||||
' {{name}} profile -e|--edit [NAME] # edit a profile in $EDITOR',
|
||||
' {{name}} profile -c|--current NAME # set NAME as current profile',
|
||||
' {{name}} profile -d|--delete NAME # delete a profile',
|
||||
'',
|
||||
' {{name}} profile -a|--add [FILE]',
|
||||
' # Add a new profile. FILE must be a file path (to JSON of',
|
||||
' # the form from `triton profile -j`, "curr" field is ',
|
||||
' # ignored) or "-" to pass the profile in on stdin.',
|
||||
' # Or exclude FILE to interactively add.',
|
||||
'',
|
||||
'{{options}}'
|
||||
].join('\n');
|
||||
|
||||
|
||||
module.exports = do_profile;
|
347
lib/do_profile/do_create.js
Normal file
347
lib/do_profile/do_create.js
Normal file
@ -0,0 +1,347 @@
|
||||
/*
|
||||
* Copyright (c) 2015 Joyent Inc.
|
||||
*
|
||||
* `triton profile create ...`
|
||||
*/
|
||||
|
||||
var assert = require('assert-plus');
|
||||
var format = require('util').format;
|
||||
var fs = require('fs');
|
||||
var sshpk = require('sshpk');
|
||||
var tilde = require('tilde-expansion');
|
||||
var vasync = require('vasync');
|
||||
|
||||
var common = require('../common');
|
||||
var errors = require('../errors');
|
||||
var mod_config = require('../config');
|
||||
|
||||
|
||||
function _createProfile(opts, cb) {
|
||||
assert.object(opts.cli, 'opts.cli');
|
||||
assert.optionalString(opts.file, 'opts.file');
|
||||
assert.optionalString(opts.copy, 'opts.copy');
|
||||
assert.func(cb, 'cb');
|
||||
var cli = opts.cli;
|
||||
var log = cli.log;
|
||||
|
||||
var data;
|
||||
|
||||
vasync.pipeline({arg: {}, funcs: [
|
||||
function getExistingProfiles(ctx, next) {
|
||||
try {
|
||||
ctx.profiles = mod_config.loadAllProfiles({
|
||||
configDir: cli.configDir,
|
||||
log: cli.log
|
||||
});
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
next();
|
||||
},
|
||||
function getCopy(ctx, next) {
|
||||
if (!opts.copy) {
|
||||
return next();
|
||||
}
|
||||
for (var i = 0; i < ctx.profiles.length; i++) {
|
||||
if (ctx.profiles[i].name === opts.copy) {
|
||||
ctx.copy = ctx.profiles[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!ctx.copy) {
|
||||
next(new errors.UsageError(format(
|
||||
'no such profile from which to copy: "%s"', opts.copy)));
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
},
|
||||
function gatherDataStdin(_, next) {
|
||||
if (opts.file !== '-') {
|
||||
return next();
|
||||
}
|
||||
var stdin = '';
|
||||
process.stdin.resume();
|
||||
process.stdin.on('data', function (chunk) {
|
||||
stdin += chunk;
|
||||
});
|
||||
process.stdin.on('end', function () {
|
||||
try {
|
||||
data = JSON.parse(stdin);
|
||||
} catch (err) {
|
||||
log.trace({stdin: stdin}, 'invalid profile JSON on stdin');
|
||||
return next(new errors.TritonError(
|
||||
format('invalid profile JSON on stdin: %s', err)));
|
||||
}
|
||||
next();
|
||||
});
|
||||
},
|
||||
function gatherDataFile(ctx, next) {
|
||||
if (!opts.file || opts.file === '-') {
|
||||
return next();
|
||||
}
|
||||
ctx.filePath = opts.file;
|
||||
var input = fs.readFileSync(opts.file);
|
||||
try {
|
||||
data = JSON.parse(input);
|
||||
} catch (err) {
|
||||
return next(new errors.TritonError(format(
|
||||
'invalid profile JSON in "%s": %s', opts.file, err)));
|
||||
}
|
||||
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'));
|
||||
}
|
||||
|
||||
var defaults = {};
|
||||
if (ctx.copy) {
|
||||
defaults = ctx.copy;
|
||||
delete defaults.name; // we don't copy a profile name
|
||||
} else {
|
||||
defaults.url = 'https://us-sw-1.api.joyent.com';
|
||||
|
||||
var possibleDefaultFp = '~/.ssh/id_rsa';
|
||||
if (fs.existsSync(common.tildeSync(possibleDefaultFp))) {
|
||||
defaults.keyId = possibleDefaultFp;
|
||||
}
|
||||
}
|
||||
|
||||
var fields = [ {
|
||||
desc: 'A profile name. A short string to identify a ' +
|
||||
'CloudAPI endpoint to the `triton` CLI.',
|
||||
key: 'name',
|
||||
default: defaults.name,
|
||||
validate: function validateName(value, valCb) {
|
||||
var regex = /^[a-z][a-z0-9_.-]*$/;
|
||||
if (!regex.test(value)) {
|
||||
return valCb(new Error('Must start with a lowercase ' +
|
||||
'letter followed by lowercase letters, numbers ' +
|
||||
'and "_", "." and "-".'));
|
||||
}
|
||||
for (var i = 0; i < ctx.profiles.length; i++) {
|
||||
if (ctx.profiles[i].name === value) {
|
||||
return valCb(new Error(format(
|
||||
'Profile "%s" already exists.', value)));
|
||||
}
|
||||
}
|
||||
valCb();
|
||||
}
|
||||
}, {
|
||||
desc: 'The CloudAPI endpoint URL.',
|
||||
default: defaults.url,
|
||||
key: 'url'
|
||||
}, {
|
||||
desc: 'Your account login name.',
|
||||
key: 'account',
|
||||
default: defaults.account,
|
||||
validate: function validateAccount(value, valCb) {
|
||||
var regex = /^[^\\]{3,}$/;
|
||||
if (value.length < 3) {
|
||||
return valCb(new Error(
|
||||
'Must be at least 3 characters'));
|
||||
}
|
||||
if (!regex.test(value)) {
|
||||
return valCb(new Error('Must not container a "\\"'));
|
||||
}
|
||||
valCb();
|
||||
}
|
||||
}, {
|
||||
desc: 'The fingerprint of the SSH key you have registered ' +
|
||||
'for your account. Alternatively, You may enter a local ' +
|
||||
'path to a public or private SSH key to have the ' +
|
||||
'fingerprint calculated for you.',
|
||||
default: defaults.keyId,
|
||||
key: 'keyId',
|
||||
validate: function validateKeyId(value, valCb) {
|
||||
// First try as a fingerprint.
|
||||
try {
|
||||
sshpk.parseFingerprint(value);
|
||||
return valCb();
|
||||
} catch (fpErr) {
|
||||
}
|
||||
|
||||
// Try as a local path.
|
||||
tilde(value, function (keyPath) {
|
||||
fs.stat(keyPath, function (statErr, stats) {
|
||||
if (statErr || !stats.isFile()) {
|
||||
return valCb(new Error(format(
|
||||
'"%s" is neither a valid fingerprint, ' +
|
||||
'nor an existing file', value)));
|
||||
}
|
||||
fs.readFile(keyPath, function (readErr, keyData) {
|
||||
if (readErr) {
|
||||
return valCb(readErr);
|
||||
}
|
||||
var keyType = (keyPath.slice(-4) === '.pub'
|
||||
? 'ssh' : 'pem');
|
||||
try {
|
||||
var key = sshpk.parseKey(keyData, keyType);
|
||||
} catch (keyErr) {
|
||||
return valCb(keyErr);
|
||||
}
|
||||
|
||||
var newVal = key.fingerprint('md5').toString();
|
||||
console.log('Fingerprint: %s', newVal);
|
||||
valCb(null, newVal);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
} ];
|
||||
|
||||
data = {};
|
||||
|
||||
/*
|
||||
* There are some value profile fields that we don't yet prompt
|
||||
* for -- because they are experimental, optional, or I'm just
|
||||
* unsure about adding them yet. :) We should still *copy* those
|
||||
* over for a `triton profile create --copy ...`.
|
||||
*
|
||||
* Eventually the need for this block should go away.
|
||||
*/
|
||||
if (ctx.copy) {
|
||||
var promptKeys = fields.map(
|
||||
function (field) { return field.key; });
|
||||
Object.keys(ctx.copy).forEach(function (key) {
|
||||
if (promptKeys.indexOf(key) === -1) {
|
||||
data[key] = ctx.copy[key];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
vasync.forEachPipeline({
|
||||
inputs: fields,
|
||||
func: function getField(field, nextField) {
|
||||
if (field.key !== 'name') console.log();
|
||||
common.promptField(field, function (err, value) {
|
||||
data[field.key] = value;
|
||||
nextField(err);
|
||||
});
|
||||
}
|
||||
}, function (err) {
|
||||
console.log();
|
||||
next(err);
|
||||
});
|
||||
},
|
||||
function guardAlreadyExists(ctx, next) {
|
||||
for (var i = 0; i < ctx.profiles.length; i++) {
|
||||
if (data.name === ctx.profiles[i].name) {
|
||||
return next(new errors.TritonError(format(
|
||||
'profile "%s" already exists', data.name)));
|
||||
}
|
||||
}
|
||||
next();
|
||||
},
|
||||
function validateIt(ctx, next) {
|
||||
// We ignore 'curr'. For now at least.
|
||||
delete data.curr;
|
||||
|
||||
try {
|
||||
mod_config.validateProfile(data, ctx.filePath);
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
next();
|
||||
},
|
||||
function saveIt(_, next) {
|
||||
try {
|
||||
mod_config.saveProfileSync({
|
||||
configDir: cli.configDir,
|
||||
profile: data
|
||||
});
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
next();
|
||||
},
|
||||
function setCurrIfTheOnlyProfile(ctx, next) {
|
||||
if (ctx.profiles.length !== 0) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
mod_config.setConfigVar({
|
||||
configDir: cli.configDir,
|
||||
name: 'profile',
|
||||
value: data.name
|
||||
}, function (err) {
|
||||
if (err) {
|
||||
next(err);
|
||||
return;
|
||||
}
|
||||
console.log('Set "%s" as current profile (because it is ' +
|
||||
'your only profile)', data.name);
|
||||
next();
|
||||
});
|
||||
}
|
||||
]}, cb);
|
||||
}
|
||||
|
||||
|
||||
|
||||
function do_create(subcmd, opts, args, cb) {
|
||||
if (opts.help) {
|
||||
this.do_help('help', {}, [subcmd], cb);
|
||||
return;
|
||||
} else if (args.length !== 0) {
|
||||
return cb(new errors.UsageError('too many arguments'));
|
||||
} else if (opts.copy && opts.file) {
|
||||
return cb(new errors.UsageError(
|
||||
'cannot specify --file and --copy at the same time'));
|
||||
}
|
||||
|
||||
_createProfile({
|
||||
cli: this.top,
|
||||
file: opts.file,
|
||||
copy: opts.copy
|
||||
}, cb);
|
||||
}
|
||||
|
||||
do_create.options = [
|
||||
{
|
||||
names: ['help', 'h'],
|
||||
type: 'bool',
|
||||
help: 'Show this help.'
|
||||
},
|
||||
{
|
||||
names: ['file', 'f'],
|
||||
type: 'string',
|
||||
helpArg: 'FILE',
|
||||
help: 'A JSON file (of the same form as "triton profile get -j") ' +
|
||||
'with the profile, or "-" to read JSON from stdin.'
|
||||
},
|
||||
{
|
||||
names: ['copy'],
|
||||
type: 'string',
|
||||
helpArg: 'NAME',
|
||||
help: 'A profile from which to copy values.'
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
do_create.help = [
|
||||
'Create a Triton CLI profile.',
|
||||
'',
|
||||
'Usage:',
|
||||
' {{name}} create <options>',
|
||||
'',
|
||||
'{{options}}',
|
||||
'',
|
||||
'Examples:',
|
||||
' triton profile create # interactively create a profile',
|
||||
' triton profile create --copy env # ... copying from "env" profile',
|
||||
'',
|
||||
' # Or non-interactively create from stdin or a file:',
|
||||
' cat a-profile.json | triton profile create -f -',
|
||||
' triton profile create -f another-profile.json'
|
||||
].join('\n');
|
||||
|
||||
|
||||
module.exports = do_create;
|
135
lib/do_profile/do_delete.js
Normal file
135
lib/do_profile/do_delete.js
Normal file
@ -0,0 +1,135 @@
|
||||
/*
|
||||
* Copyright (c) 2015 Joyent Inc.
|
||||
*
|
||||
* `triton profile delete ...`
|
||||
*/
|
||||
|
||||
var assert = require('assert-plus');
|
||||
var vasync = require('vasync');
|
||||
|
||||
var common = require('../common');
|
||||
var errors = require('../errors');
|
||||
var mod_config = require('../config');
|
||||
var profilecommon = require('./profilecommon');
|
||||
|
||||
|
||||
|
||||
function _deleteProfile(opts, cb) {
|
||||
assert.object(opts.cli, 'opts.cli');
|
||||
assert.string(opts.name, 'opts.name');
|
||||
assert.bool(opts.force, 'opts.force');
|
||||
assert.func(cb, 'cb');
|
||||
var cli = opts.cli;
|
||||
|
||||
if (opts.name === 'env') {
|
||||
return cb(new errors.UsageError('cannot delete "env" profile'));
|
||||
}
|
||||
|
||||
try {
|
||||
var profile = mod_config.loadProfile({
|
||||
configDir: cli.configDir,
|
||||
name: opts.name
|
||||
});
|
||||
} catch (err) {
|
||||
if (opts.force) {
|
||||
cb();
|
||||
} else {
|
||||
cb(err);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (profile.name === cli.tritonapi.profile.name && !opts.force) {
|
||||
return cb(new errors.TritonError(
|
||||
'cannot delete the current profile (use --force to override)'));
|
||||
}
|
||||
|
||||
vasync.pipeline({funcs: [
|
||||
function confirm(_, next) {
|
||||
if (opts.force) {
|
||||
return next();
|
||||
}
|
||||
common.promptYesNo({
|
||||
msg: 'Delete profile "' + opts.name + '"? [y/n] '
|
||||
}, function (answer) {
|
||||
if (answer !== 'y') {
|
||||
console.error('Aborting');
|
||||
next(true); // early abort signal
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// If we are deleting the current profile, then revert the current
|
||||
// profile to 'env'.
|
||||
function handleConfigVar(_, next) {
|
||||
if (profile.name === cli.tritonapi.profile.name) {
|
||||
profilecommon.setCurrentProfile({name: 'env', cli: cli}, next);
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
},
|
||||
|
||||
function deleteIt(_, next) {
|
||||
try {
|
||||
mod_config.deleteProfile({
|
||||
configDir: cli.configDir,
|
||||
name: opts.name
|
||||
});
|
||||
} catch (delErr) {
|
||||
return next(delErr);
|
||||
}
|
||||
console.log('Deleted profile "%s"', opts.name);
|
||||
next();
|
||||
}
|
||||
]}, function (err) {
|
||||
if (err === true) {
|
||||
err = null;
|
||||
}
|
||||
cb(err);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function do_delete(subcmd, opts, args, cb) {
|
||||
if (opts.help) {
|
||||
this.do_help('help', {}, [subcmd], cb);
|
||||
return;
|
||||
} else if (args.length !== 1) {
|
||||
return cb(new errors.UsageError('NAME argument is required'));
|
||||
}
|
||||
|
||||
_deleteProfile({
|
||||
cli: this.top,
|
||||
name: args[0],
|
||||
force: Boolean(opts.force)
|
||||
}, cb);
|
||||
}
|
||||
|
||||
do_delete.options = [
|
||||
{
|
||||
names: ['help', 'h'],
|
||||
type: 'bool',
|
||||
help: 'Show this help.'
|
||||
},
|
||||
{
|
||||
names: ['force', 'f'],
|
||||
type: 'bool',
|
||||
help: 'Force deletion.'
|
||||
}
|
||||
];
|
||||
|
||||
do_delete.help = [
|
||||
'Delete a Triton CLI profile.',
|
||||
'',
|
||||
'Usage:',
|
||||
' {{name}} delete NAME',
|
||||
'',
|
||||
'{{options}}'
|
||||
].join('\n');
|
||||
|
||||
|
||||
do_delete.aliases = ['rm'];
|
||||
|
||||
module.exports = do_delete;
|
167
lib/do_profile/do_edit.js
Normal file
167
lib/do_profile/do_edit.js
Normal file
@ -0,0 +1,167 @@
|
||||
/*
|
||||
* Copyright (c) 2015 Joyent Inc.
|
||||
*
|
||||
* `triton profile edit ...`
|
||||
*/
|
||||
|
||||
var assert = require('assert-plus');
|
||||
var format = require('util').format;
|
||||
var strsplit = require('strsplit');
|
||||
|
||||
var common = require('../common');
|
||||
var errors = require('../errors');
|
||||
var mod_config = require('../config');
|
||||
|
||||
|
||||
function _yamlishFromProfile(profile) {
|
||||
assert.object(profile, 'profile');
|
||||
|
||||
var keys = [];
|
||||
var skipKeys = ['curr', 'name'];
|
||||
Object.keys(profile).forEach(function (key) {
|
||||
if (skipKeys.indexOf(key) === -1) {
|
||||
keys.push(key);
|
||||
}
|
||||
});
|
||||
keys = keys.sort();
|
||||
|
||||
var lines = [];
|
||||
keys.forEach(function (key) {
|
||||
lines.push(format('%s: %s', key, profile[key]));
|
||||
});
|
||||
return lines.join('\n') + '\n';
|
||||
}
|
||||
|
||||
function _profileFromYamlish(yamlish) {
|
||||
assert.string(yamlish, 'yamlish');
|
||||
|
||||
var profile = {};
|
||||
var bools = ['insecure'];
|
||||
var lines = yamlish.split(/\n/g);
|
||||
lines.forEach(function (line) {
|
||||
var commentIdx = line.indexOf('#');
|
||||
if (commentIdx !== -1) {
|
||||
line = line.slice(0, commentIdx);
|
||||
}
|
||||
line = line.trim();
|
||||
if (!line) {
|
||||
return;
|
||||
}
|
||||
var parts = strsplit(line, ':', 2);
|
||||
var key = parts[0].trim();
|
||||
var value = parts[1].trim();
|
||||
if (bools.indexOf(key) !== -1) {
|
||||
value = common.boolFromString(value);
|
||||
}
|
||||
profile[key] = value;
|
||||
});
|
||||
|
||||
return profile;
|
||||
}
|
||||
|
||||
|
||||
function _editProfile(opts, cb) {
|
||||
assert.object(opts.cli, 'opts.cli');
|
||||
assert.string(opts.name, 'opts.name');
|
||||
assert.func(cb, 'cb');
|
||||
var cli = opts.cli;
|
||||
|
||||
if (opts.name === 'env') {
|
||||
return cb(new errors.UsageError('cannot edit "env" profile'));
|
||||
}
|
||||
|
||||
try {
|
||||
var profile = mod_config.loadProfile({
|
||||
configDir: cli.configDir,
|
||||
name: opts.name
|
||||
});
|
||||
} catch (err) {
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
var filename = format('profile-%s.txt', profile.name);
|
||||
var origText = _yamlishFromProfile(profile);
|
||||
|
||||
function editAttempt(text) {
|
||||
common.editInEditor({
|
||||
text: text,
|
||||
filename: filename
|
||||
}, function (err, afterText, changed) {
|
||||
if (err) {
|
||||
return cb(new errors.TritonError(err));
|
||||
} else if (!changed) {
|
||||
console.log('No change to profile');
|
||||
return cb();
|
||||
}
|
||||
|
||||
try {
|
||||
var editedProfile = _profileFromYamlish(afterText);
|
||||
editedProfile.name = profile.name;
|
||||
|
||||
if (_yamlishFromProfile(editedProfile) === origText) {
|
||||
// This YAMLish is the closest to a canonical form we have.
|
||||
console.log('No change to profile');
|
||||
return cb();
|
||||
}
|
||||
|
||||
mod_config.saveProfileSync({
|
||||
configDir: cli.configDir,
|
||||
profile: editedProfile
|
||||
});
|
||||
} catch (textErr) {
|
||||
console.error('Error with your changes: %s', textErr);
|
||||
common.promptEnter(
|
||||
'Press <Enter> to re-edit, Ctrl+C to abort.',
|
||||
function (aborted) {
|
||||
if (aborted) {
|
||||
console.log('\nAborting. ' +
|
||||
'No change made to profile');
|
||||
cb();
|
||||
} else {
|
||||
editAttempt(afterText);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
cb();
|
||||
});
|
||||
}
|
||||
|
||||
editAttempt(origText);
|
||||
}
|
||||
|
||||
|
||||
function do_edit(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'));
|
||||
}
|
||||
|
||||
_editProfile({
|
||||
cli: this.top,
|
||||
name: args[0] || this.top.tritonapi.profile.name
|
||||
}, cb);
|
||||
}
|
||||
|
||||
do_edit.options = [
|
||||
{
|
||||
names: ['help', 'h'],
|
||||
type: 'bool',
|
||||
help: 'Show this help.'
|
||||
}
|
||||
];
|
||||
|
||||
do_edit.help = [
|
||||
'Edit a Triton CLI profile in your $EDITOR.',
|
||||
'',
|
||||
'Usage:',
|
||||
' {{name}} edit [NAME]',
|
||||
'',
|
||||
'{{options}}'
|
||||
].join('\n');
|
||||
|
||||
|
||||
module.exports = do_edit;
|
96
lib/do_profile/do_get.js
Normal file
96
lib/do_profile/do_get.js
Normal file
@ -0,0 +1,96 @@
|
||||
/*
|
||||
* Copyright (c) 2015 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 tilde = require('tilde-expansion');
|
||||
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) {
|
||||
assert.object(opts.cli, 'opts.cli');
|
||||
assert.string(opts.name, 'opts.name');
|
||||
assert.func(cb, 'cb');
|
||||
var cli = opts.cli;
|
||||
|
||||
try {
|
||||
var profile = mod_config.loadProfile({
|
||||
configDir: cli.configDir,
|
||||
name: opts.name
|
||||
});
|
||||
} catch (err) {
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
if (profile.name === cli.tritonapi.profile.name) {
|
||||
cli._applyProfileOverrides(profile);
|
||||
profile.curr = true;
|
||||
} else {
|
||||
profile.curr = false;
|
||||
}
|
||||
|
||||
if (opts.json) {
|
||||
console.log(JSON.stringify(profile));
|
||||
} else {
|
||||
console.log('name: %s', profile.name);
|
||||
Object.keys(profile).sort().forEach(function (key) {
|
||||
if (key === 'name')
|
||||
return;
|
||||
if (profile[key] !== undefined)
|
||||
console.log('%s: %s', key, profile[key]);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function do_get(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'));
|
||||
}
|
||||
|
||||
_showProfile({
|
||||
cli: this.top,
|
||||
name: args[0] || this.top.tritonapi.profile.name,
|
||||
json: opts.json
|
||||
}, cb);
|
||||
}
|
||||
|
||||
do_get.options = [
|
||||
{
|
||||
names: ['help', 'h'],
|
||||
type: 'bool',
|
||||
help: 'Show this help.'
|
||||
},
|
||||
{
|
||||
names: ['json', 'j'],
|
||||
type: 'bool',
|
||||
help: 'JSON output.'
|
||||
}
|
||||
];
|
||||
|
||||
do_get.help = [
|
||||
'Get a Triton CLI profile.',
|
||||
'',
|
||||
'Usage:',
|
||||
' {{name}} get [NAME]',
|
||||
'',
|
||||
'{{options}}',
|
||||
'If NAME is not specified, the current profile is shown.'
|
||||
].join('\n');
|
||||
|
||||
|
||||
module.exports = do_get;
|
116
lib/do_profile/do_list.js
Normal file
116
lib/do_profile/do_list.js
Normal file
@ -0,0 +1,116 @@
|
||||
/*
|
||||
* Copyright (c) 2015 Joyent Inc.
|
||||
*
|
||||
* `triton profiles ...`
|
||||
*/
|
||||
|
||||
var tabula = require('tabula');
|
||||
|
||||
var common = require('../common');
|
||||
var errors = require('../errors');
|
||||
var mod_config = require('../config');
|
||||
|
||||
|
||||
var sortDefault = 'name';
|
||||
var columnsDefault = 'name,curr,account,user,url';
|
||||
var columnsDefaultLong = 'name,curr,account,user,url,insecure,keyId';
|
||||
|
||||
function _listProfiles(cli, opts, args, cb) {
|
||||
var columns = columnsDefault;
|
||||
if (opts.o) {
|
||||
columns = opts.o;
|
||||
} else if (opts.long) {
|
||||
columns = columnsDefaultLong;
|
||||
}
|
||||
columns = columns.split(',');
|
||||
|
||||
var sort = opts.s.split(',');
|
||||
|
||||
// Load all the profiles. "env" is a special one managed by the CLI.
|
||||
var profiles;
|
||||
try {
|
||||
profiles = mod_config.loadAllProfiles({
|
||||
configDir: cli.configDir,
|
||||
log: cli.log
|
||||
});
|
||||
} catch (e) {
|
||||
return cb(e);
|
||||
}
|
||||
|
||||
// Current profile: Set 'curr' field. Apply CLI overrides.
|
||||
for (i = 0; i < profiles.length; i++) {
|
||||
var profile = profiles[i];
|
||||
if (profile.name === cli.tritonapi.profile.name) {
|
||||
cli._applyProfileOverrides(profile);
|
||||
if (opts.json) {
|
||||
profile.curr = true;
|
||||
} else {
|
||||
profile.curr = '*'; // tabular
|
||||
}
|
||||
} else {
|
||||
if (opts.json) {
|
||||
profile.curr = false;
|
||||
} else {
|
||||
profile.curr = ''; // tabular
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Display.
|
||||
var i;
|
||||
if (opts.json) {
|
||||
common.jsonStream(profiles);
|
||||
} else {
|
||||
tabula(profiles, {
|
||||
skipHeader: opts.H,
|
||||
columns: columns,
|
||||
sort: sort
|
||||
});
|
||||
}
|
||||
cb();
|
||||
}
|
||||
|
||||
|
||||
function do_list(subcmd, opts, args, cb) {
|
||||
if (opts.help) {
|
||||
return this.do_help('help', {}, [subcmd], cb);
|
||||
} else if (args.length > 0) {
|
||||
return cb(new errors.UsageError('too many args'));
|
||||
}
|
||||
|
||||
_listProfiles(this.top, opts, args, cb);
|
||||
}
|
||||
|
||||
do_list.options = [
|
||||
{
|
||||
names: ['help', 'h'],
|
||||
type: 'bool',
|
||||
help: 'Show this help.'
|
||||
}
|
||||
].concat(common.getCliTableOptions({
|
||||
includeLong: true,
|
||||
sortDefault: sortDefault
|
||||
}));
|
||||
do_list.help = [
|
||||
/* BEGIN JSSTYLED */
|
||||
'List Triton CLI profiles.',
|
||||
'',
|
||||
'A profile is a configured Triton CloudAPI endpoint and associated info.',
|
||||
'I.e. the URL, account name, SSH key fingerprint, etc. information required',
|
||||
'to call a CloudAPI endpoint in a Triton datacenter. You can then switch',
|
||||
'between profiles with `triton -p PROFILE`, the TRITON_PROFILE environment',
|
||||
'variable, or by setting your current profile.',
|
||||
'',
|
||||
'The "CURR" column indicates which profile is the current one.',
|
||||
'',
|
||||
'Usage:',
|
||||
' {{name}} list',
|
||||
'',
|
||||
'{{options}}'
|
||||
/* END JSSTYLED */
|
||||
].join('\n');
|
||||
|
||||
|
||||
do_list.aliases = ['ls'];
|
||||
|
||||
module.exports = do_list;
|
43
lib/do_profile/do_set_current.js
Normal file
43
lib/do_profile/do_set_current.js
Normal file
@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright (c) 2015 Joyent Inc.
|
||||
*
|
||||
* `triton profile set-current ...`
|
||||
*/
|
||||
|
||||
var errors = require('../errors');
|
||||
var profilecommon = require('./profilecommon');
|
||||
|
||||
|
||||
|
||||
function do_set_current(subcmd, opts, args, cb) {
|
||||
if (opts.help) {
|
||||
this.do_help('help', {}, [subcmd], cb);
|
||||
return;
|
||||
} else if (args.length !== 1) {
|
||||
return cb(new errors.UsageError('NAME argument is required'));
|
||||
}
|
||||
|
||||
profilecommon.setCurrentProfile({cli: this.top, name: args[0]}, cb);
|
||||
}
|
||||
|
||||
do_set_current.options = [
|
||||
{
|
||||
names: ['help', 'h'],
|
||||
type: 'bool',
|
||||
help: 'Show this help.'
|
||||
}
|
||||
];
|
||||
|
||||
do_set_current.help = [
|
||||
'Set the current Triton CLI profile.',
|
||||
'',
|
||||
'Usage:',
|
||||
' {{name}} set-current NAME',
|
||||
'',
|
||||
'{{options}}',
|
||||
'The "current" profile is the one used by default, unless overridden by',
|
||||
'`triton -p PROFILE-NAME ...` or the TRITON_PROFILE environment variable.'
|
||||
].join('\n');
|
||||
|
||||
|
||||
module.exports = do_set_current;
|
66
lib/do_profile/index.js
Normal file
66
lib/do_profile/index.js
Normal file
@ -0,0 +1,66 @@
|
||||
/*
|
||||
* 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 2015 Joyent, Inc.
|
||||
*
|
||||
* `triton image ...`
|
||||
*/
|
||||
|
||||
var Cmdln = require('cmdln').Cmdln;
|
||||
var util = require('util');
|
||||
|
||||
|
||||
|
||||
// ---- CLI class
|
||||
|
||||
function ProfileCLI(top) {
|
||||
this.top = top;
|
||||
Cmdln.call(this, {
|
||||
name: top.name + ' profile',
|
||||
/* BEGIN JSSTYLED */
|
||||
desc: [
|
||||
'List, get, create and update Triton CLI profiles.',
|
||||
'',
|
||||
'A profile is a configured Triton CloudAPI endpoint. I.e. the',
|
||||
'url, account, key, etc. information required to call a CloudAPI.',
|
||||
'You can then switch between profiles with `triton -p PROFILE`',
|
||||
'or the TRITON_PROFILE environment variable.'
|
||||
].join('\n'),
|
||||
/* END JSSTYLED */
|
||||
helpOpts: {
|
||||
minHelpCol: 24 /* line up with option help */
|
||||
},
|
||||
helpSubcmds: [
|
||||
'help',
|
||||
'list',
|
||||
'get',
|
||||
'set-current',
|
||||
'create',
|
||||
'edit',
|
||||
'delete'
|
||||
]
|
||||
});
|
||||
}
|
||||
util.inherits(ProfileCLI, Cmdln);
|
||||
|
||||
ProfileCLI.prototype.init = function init(opts, args, cb) {
|
||||
this.log = this.top.log;
|
||||
Cmdln.prototype.init.apply(this, arguments);
|
||||
};
|
||||
|
||||
ProfileCLI.prototype.do_list = require('./do_list');
|
||||
ProfileCLI.prototype.do_get = require('./do_get');
|
||||
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');
|
||||
|
||||
// TODO: Would like to `triton profile update foo account=trentm ...`
|
||||
// And then would like that same key=value syntax optional for create.
|
||||
//ProfileCLI.prototype.do_update = require('./do_update');
|
||||
|
||||
module.exports = ProfileCLI;
|
49
lib/do_profile/profilecommon.js
Normal file
49
lib/do_profile/profilecommon.js
Normal file
@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright (c) 2015 Joyent Inc.
|
||||
*
|
||||
* Shared stuff for `triton profile ...` handling.
|
||||
*/
|
||||
|
||||
var assert = require('assert-plus');
|
||||
|
||||
var mod_config = require('../config');
|
||||
|
||||
|
||||
|
||||
function setCurrentProfile(opts, cb) {
|
||||
assert.object(opts.cli, 'opts.cli');
|
||||
assert.string(opts.name, 'opts.name');
|
||||
assert.func(cb, 'cb');
|
||||
var cli = opts.cli;
|
||||
|
||||
try {
|
||||
var profile = mod_config.loadProfile({
|
||||
configDir: cli.configDir,
|
||||
name: opts.name
|
||||
});
|
||||
} catch (err) {
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
if (cli.tritonapi.profile.name === profile.name) {
|
||||
console.log('"%s" is already the current profile', profile.name);
|
||||
return cb();
|
||||
}
|
||||
|
||||
mod_config.setConfigVar({
|
||||
configDir: cli.configDir,
|
||||
name: 'profile',
|
||||
value: profile.name
|
||||
}, function (err) {
|
||||
if (err) {
|
||||
return cb(err);
|
||||
}
|
||||
console.log('Set "%s" as current profile', profile.name);
|
||||
cb();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
module.exports = {
|
||||
setCurrentProfile: setCurrentProfile
|
||||
};
|
@ -1,110 +1,29 @@
|
||||
/*
|
||||
* Copyright (c) 2015 Joyent Inc.
|
||||
*
|
||||
* `triton profiles ...`
|
||||
* 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/.
|
||||
*/
|
||||
|
||||
var common = require('./common');
|
||||
var errors = require('./errors');
|
||||
var mod_config = require('./config');
|
||||
var tabula = require('tabula');
|
||||
/*
|
||||
* Copyright 2015 Joyent, Inc.
|
||||
*
|
||||
* `triton profiles ...` bwcompat shortcut for `triton profile list ...`.
|
||||
*/
|
||||
|
||||
|
||||
var sortDefault = 'name';
|
||||
var columnsDefault = 'name,curr,account,user,url';
|
||||
var columnsDefaultLong = 'name,curr,account,user,url,insecure,keyId';
|
||||
|
||||
function _listProfiles(cli, opts, args, cb) {
|
||||
var columns = columnsDefault;
|
||||
if (opts.o) {
|
||||
columns = opts.o;
|
||||
} else if (opts.long) {
|
||||
columns = columnsDefaultLong;
|
||||
}
|
||||
columns = columns.split(',');
|
||||
|
||||
var sort = opts.s.split(',');
|
||||
|
||||
// Load all the profiles. "env" is a special one managed by the CLI.
|
||||
var profiles;
|
||||
try {
|
||||
profiles = mod_config.loadAllProfiles({
|
||||
configDir: cli.configDir,
|
||||
log: cli.log
|
||||
});
|
||||
} catch (e) {
|
||||
return cb(e);
|
||||
}
|
||||
|
||||
// Current profile: Set 'curr' field. Apply CLI overrides.
|
||||
for (i = 0; i < profiles.length; i++) {
|
||||
var profile = profiles[i];
|
||||
if (profile.name === cli.tritonapi.profile.name) {
|
||||
cli._applyProfileOverrides(profile);
|
||||
if (opts.json) {
|
||||
profile.curr = true;
|
||||
} else {
|
||||
profile.curr = '*'; // tabular
|
||||
}
|
||||
} else {
|
||||
if (opts.json) {
|
||||
profile.curr = false;
|
||||
} else {
|
||||
profile.curr = ''; // tabular
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Display.
|
||||
var i;
|
||||
if (opts.json) {
|
||||
common.jsonStream(profiles);
|
||||
} else {
|
||||
tabula(profiles, {
|
||||
skipHeader: opts.H,
|
||||
columns: columns,
|
||||
sort: sort
|
||||
});
|
||||
}
|
||||
cb();
|
||||
function do_profiles(subcmd, opts, args, callback) {
|
||||
var subcmdArgv = ['node', 'triton', 'profile', 'list'].concat(args);
|
||||
this.dispatch('profile', subcmdArgv, callback);
|
||||
}
|
||||
|
||||
function do_profiles(subcmd, opts, args, cb) {
|
||||
if (opts.help) {
|
||||
return this.do_help('help', {}, [subcmd], cb);
|
||||
} else if (args.length > 0) {
|
||||
return cb(new errors.UsageError('too many args'));
|
||||
}
|
||||
|
||||
_listProfiles(this, opts, args, cb);
|
||||
}
|
||||
|
||||
do_profiles.options = [
|
||||
{
|
||||
names: ['help', 'h'],
|
||||
type: 'bool',
|
||||
help: 'Show this help.'
|
||||
}
|
||||
].concat(common.getCliTableOptions({
|
||||
includeLong: true,
|
||||
sortDefault: sortDefault
|
||||
}));
|
||||
do_profiles.help = [
|
||||
'List `triton` CLI profiles.',
|
||||
'',
|
||||
'A profile is a configured Triton CloudAPI endpoint. I.e. the',
|
||||
'url, account, key, etc. information required to call a CloudAPI.',
|
||||
'You can then switch between profiles with `triton -p PROFILE`',
|
||||
'or the TRITON_PROFILE environment variable.',
|
||||
'',
|
||||
'The "CURR" column indicates which profile is the current one.',
|
||||
'A shortcut for "triton profile list".',
|
||||
'',
|
||||
'Usage:',
|
||||
' {{name}} profiles',
|
||||
'',
|
||||
'{{options}}'
|
||||
' {{name}} profiles ...'
|
||||
].join('\n');
|
||||
|
||||
do_profiles.aliases = ['imgs'];
|
||||
|
||||
do_profiles.hidden = true;
|
||||
|
||||
module.exports = do_profiles;
|
||||
|
@ -34,8 +34,8 @@ if (opts.skip) {
|
||||
}
|
||||
|
||||
test('triton profiles (read only)', function (tt) {
|
||||
tt.test(' triton profile env', function (t) {
|
||||
h.safeTriton(t, {json: true, args: ['profile', '-j', 'env']},
|
||||
tt.test(' triton profile get env', function (t) {
|
||||
h.safeTriton(t, {json: true, args: ['profile', 'get', '-j', 'env']},
|
||||
function (p) {
|
||||
|
||||
t.equal(p.account, h.CONFIG.profile.account,
|
||||
@ -54,7 +54,7 @@ test('triton profiles (read only)', function (tt) {
|
||||
|
||||
test('triton profiles (read/write)', opts, function (tt) {
|
||||
tt.test(' triton profile create', function (t) {
|
||||
h.safeTriton(t, ['profile', '-a', PROFILE_FILE],
|
||||
h.safeTriton(t, ['profile', 'create', '-f', PROFILE_FILE],
|
||||
function (stdout) {
|
||||
|
||||
t.ok(stdout.match(/^Saved profile/), 'stdout correct');
|
||||
@ -64,7 +64,7 @@ test('triton profiles (read/write)', opts, function (tt) {
|
||||
|
||||
tt.test(' triton profile get', function (t) {
|
||||
h.safeTriton(t,
|
||||
{json: true, args: ['profile', '-j', PROFILE_DATA.name]},
|
||||
{json: true, args: ['profile', 'get', '-j', PROFILE_DATA.name]},
|
||||
function (p) {
|
||||
|
||||
t.deepEqual(p, PROFILE_DATA, 'profile matched');
|
||||
@ -74,7 +74,7 @@ test('triton profiles (read/write)', opts, function (tt) {
|
||||
});
|
||||
|
||||
tt.test(' triton profile delete', function (t) {
|
||||
h.safeTriton(t, ['profile', '-df', PROFILE_DATA.name],
|
||||
h.safeTriton(t, ['profile', 'delete', '-f', PROFILE_DATA.name],
|
||||
function (stdout) {
|
||||
|
||||
t.ok(stdout.match(/^Deleted profile/), 'stdout correct');
|
||||
|
Reference in New Issue
Block a user