clistyle: add support for account keys, expand subcommand tests,

some trivial bug fixes.
This commit is contained in:
Marsell Kukuljevic 2015-12-31 13:43:46 +11:00 committed by Trent Mick
parent f4246b5faf
commit 96216c6e61
13 changed files with 713 additions and 69 deletions

View File

@ -212,7 +212,7 @@ function CLI() {
{ group: 'Other Commands' },
'info',
'account',
'keys',
'key',
'services',
'datacenters'
],
@ -337,6 +337,9 @@ CLI.prototype.do_account = require('./do_account');
CLI.prototype.do_services = require('./do_services');
CLI.prototype.do_datacenters = require('./do_datacenters');
CLI.prototype.do_info = require('./do_info');
// Account keys
CLI.prototype.do_key = require('./do_key');
CLI.prototype.do_keys = require('./do_keys');
// Images

View File

@ -351,14 +351,103 @@ CloudApi.prototype.getAccount = function getAccount(opts, cb) {
this._passThrough(endpoint, opts, cb);
};
/**
* List account's SSH keys.
*
* @param {Object} opts (object)
* @param {Function} callback of the form `function (err, keys, res)`
*/
CloudApi.prototype.listKeys = function listKeys(opts, cb) {
assert.object(opts, 'opts');
assert.func(cb, 'cb');
var endpoint = format('/%s/keys', this.account);
this._passThrough(endpoint, opts, cb);
this._passThrough(endpoint, {}, cb);
};
/**
* Get an account's SSH key.
*
* @param {Object} opts (object)
* - {String} fingerprint (required*) The SSH key fingerprint. One of
* 'fingerprint' or 'name' is required.
* - {String} name (required*) The SSH key name. One of 'fingerprint'
* or 'name' is required.
* @param {Function} callback of the form `function (err, res)`
*/
CloudApi.prototype.getKey = function getKey(opts, cb) {
assert.object(opts, 'opts');
assert.optionalString(opts.fingerprint, 'opts.fingerprint');
assert.optionalString(opts.name, 'opts.name');
assert.func(cb, 'cb');
var identifier = opts.fingerprint || opts.name;
assert.ok(identifier, 'one of "fingerprint" or "name" is require');
var endpoint = format('/%s/keys/%s', this.account,
encodeURIComponent(identifier));
this._passThrough(endpoint, {}, cb);
};
/**
* Create/upload a new account SSH public key.
*
* @param {Object} opts (object)
* - {String} key (required) The SSH public key content.
* - {String} name (optional) A name for the key. If not given, the
* key fingerprint will be used.
* @param {Function} callback of the form `function (err, res)`
*/
CloudApi.prototype.createKey = function createKey(opts, cb) {
assert.object(opts, 'opts');
assert.string(opts.key, 'opts.key');
assert.optionalString(opts.name, 'opts.name');
assert.func(cb, 'cb');
var data = {
name: opts.name,
key: opts.key
};
this._request({
method: 'POST',
path: format('/%s/keys', this.account),
data: data
}, function (err, req, res, body) {
cb(err, body, res);
});
};
/**
* Delete an account's SSH key.
*
* @param {Object} opts (object)
* - {String} fingerprint (required*) The SSH key fingerprint. One of
* 'fingerprint' or 'name' is required.
* - {String} name (required*) The SSH key name. One of 'fingerprint'
* or 'name' is required.
* @param {Function} callback of the form `function (err, res)`
*/
CloudApi.prototype.deleteKey = function deleteKey(opts, cb) {
assert.object(opts, 'opts');
assert.optionalString(opts.fingerprint, 'opts.fingerprint');
assert.optionalString(opts.name, 'opts.name');
assert.func(cb, 'cb');
var identifier = opts.fingerprint || opts.name;
assert.ok(identifier, 'one of "fingerprint" or "name" is require');
this._request({
method: 'DELETE',
path: format('/%s/keys/%s', this.account,
encodeURIComponent(identifier))
}, function (err, req, res) {
cb(err, res);
});
};

140
lib/do_key/do_add.js Normal file
View File

@ -0,0 +1,140 @@
/*
* 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 key add ...`
*/
var assert = require('assert-plus');
var format = require('util').format;
var fs = require('fs');
var sshpk = require('sshpk');
var vasync = require('vasync');
var common = require('../common');
var errors = require('../errors');
function do_add(subcmd, opts, args, cb) {
assert.optionalString(opts.name, 'opts.name');
assert.func(cb, 'cb');
if (opts.help) {
this.do_help('help', {}, [subcmd], cb);
return;
}
if (args.length === 0) {
cb(new errors.UsageError('missing FILE argument'));
return;
} else if (args.length > 1) {
cb(new errors.UsageError('incorrect number of arguments'));
return;
}
var filePath = args[0];
var cli = this.top;
vasync.pipeline({arg: {}, funcs: [
function gatherDataStdin(ctx, next) {
if (filePath !== '-') {
return next();
}
var stdin = '';
process.stdin.resume();
process.stdin.on('data', function (chunk) {
stdin += chunk;
});
process.stdin.on('end', function () {
ctx.data = stdin;
ctx.from = '<stdin>';
next();
});
},
function gatherDataFile(ctx, next) {
if (!filePath || filePath === '-') {
return next();
}
ctx.data = fs.readFileSync(filePath);
ctx.from = filePath;
next();
},
function validateData(ctx, next) {
try {
sshpk.parseKey(ctx.data, 'ssh', ctx.from);
} catch (keyErr) {
next(keyErr);
return;
}
next();
},
function createIt(ctx, next) {
var createOpts = {
userId: opts.userId,
key: ctx.data.toString('utf8')
};
if (opts.name) {
createOpts.name = opts.name;
}
cli.tritonapi.cloudapi.createKey(createOpts, function (err, key) {
if (err) {
next(err);
return;
}
if (key.name) {
console.log('Added key "%s" (%s)',
key.name, key.fingerprint);
} else {
console.log('Added key %s', key.fingerprint);
}
next();
});
}
]}, cb);
}
do_add.options = [
{
names: ['help', 'h'],
type: 'bool',
help: 'Show this help.'
},
{
names: ['json', 'j'],
type: 'bool',
help: 'JSON stream output.'
},
{
names: ['name', 'n'],
type: 'string',
helpArg: 'NAME',
help: 'An optional name for an added key.'
}
];
do_add.help = [
'Add an SSH key to an account.',
'',
'Usage:',
' {{name}} key add FILE [<options>]',
'',
'{{options}}',
'',
'Where "FILE" must be a file path to an SSH public key, ',
'or "-" to pass the public key in on stdin.'
].join('\n');
module.exports = do_add;

120
lib/do_key/do_delete.js Normal file
View File

@ -0,0 +1,120 @@
/*
* 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 key delete ...`
*/
var assert = require('assert-plus');
var format = require('util').format;
var fs = require('fs');
var sshpk = require('sshpk');
var vasync = require('vasync');
var common = require('../common');
var errors = require('../errors');
function do_delete(subcmd, opts, args, cb) {
assert.func(cb, 'cb');
if (opts.help) {
this.do_help('help', {}, [subcmd], cb);
return;
}
if (args.length === 0) {
cb(new errors.UsageError('missing KEY argument(s)'));
return;
}
var cli = this.top;
vasync.pipeline({funcs: [
function confirm(_, next) {
if (opts.yes) {
return next();
}
var msg;
if (args.length === 1) {
msg = 'Delete key "' + args[0] + '"? [y/n] ';
} else {
msg = format('Delete %d keys (%s)? [y/n] ',
args.length, args.join(', '));
}
common.promptYesNo({msg: msg}, function (answer) {
if (answer !== 'y') {
console.error('Aborting');
next(true); // early abort signal
} else {
next();
}
});
},
function deleteThem(_, next) {
vasync.forEachPipeline({
inputs: args,
func: function deleteOne(id, nextId) {
var delOpts = {
fingerprint: id
};
cli.tritonapi.cloudapi.deleteKey(delOpts, function (err) {
if (err) {
nextId(err);
return;
}
console.log('Deleted key "%s"', id);
nextId();
});
}
}, next);
}
]}, function (err) {
if (err === true) {
err = null;
}
cb(err);
});
}
do_delete.options = [
{
names: ['help', 'h'],
type: 'bool',
help: 'Show this help.'
},
{
names: ['json', 'j'],
type: 'bool',
help: 'JSON stream output.'
},
{
names: ['yes', 'y'],
type: 'bool',
help: 'Answer yes to confirmation to delete.'
}
];
do_delete.help = [
'Remove an SSH key from an account.',
'',
'Usage:',
' {{name}} key delete FILE [<options>]',
'',
'{{options}}',
'',
'Where "KEY" is an SSH key "name" or "fingerprint".'
].join('\n');
do_delete.aliases = ['rm'];
module.exports = do_delete;

80
lib/do_key/do_get.js Normal file
View File

@ -0,0 +1,80 @@
/*
* 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 key get ...`
*/
var assert = require('assert-plus');
var common = require('../common');
var errors = require('../errors');
function do_get(subcmd, opts, args, cb) {
assert.func(cb, 'cb');
if (opts.help) {
this.do_help('help', {}, [subcmd], cb);
return;
}
if (args.length === 0) {
cb(new errors.UsageError('missing KEY argument'));
return;
} else if (args.length > 1) {
cb(new errors.UsageError('incorrect number of arguments'));
return;
}
var id = args[0];
var cli = this.top;
cli.tritonapi.cloudapi.getKey({
// Currently `cloudapi.getUserKey` isn't picky about the `name` being
// passed in as the `opts.fingerprint` arg.
fingerprint: id
}, function onKey(err, key) {
if (err) {
cb(err);
return;
}
if (opts.json) {
console.log(JSON.stringify(key));
} else {
console.log(common.chomp(key.key));
}
cb();
});
}
do_get.options = [
{
names: ['help', 'h'],
type: 'bool',
help: 'Show this help.'
},
{
names: ['json', 'j'],
type: 'bool',
help: 'JSON stream output.'
}
];
do_get.help = [
'Show a specific SSH key in an account.',
'',
'Usage:',
' {{name}} key get KEY # show account\'s KEY',
'',
'{{options}}',
'Where "KEY" is an SSH key "name" or "fingerprint".'
].join('\n');
module.exports = do_get;

79
lib/do_key/do_list.js Normal file
View File

@ -0,0 +1,79 @@
/*
* 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 key list ...`
*/
var assert = require('assert-plus');
var common = require('../common');
var errors = require('../errors');
function do_list(subcmd, opts, args, cb) {
assert.func(cb, 'cb');
if (opts.help) {
this.do_help('help', {}, [subcmd], cb);
return;
}
if (args.length > 0) {
cb(new errors.UsageError('incorrect number of arguments'));
return;
}
var cli = this.top;
cli.tritonapi.cloudapi.listKeys({}, function onKeys(err, keys) {
if (err) {
cb(err);
return;
}
if (opts.json) {
console.log(JSON.stringify(keys));
} else {
keys.forEach(function (key) {
console.log(common.chomp(key.key));
});
}
cb();
});
}
do_list.options = [
{
names: ['help', 'h'],
type: 'bool',
help: 'Show this help.'
},
{
names: ['json', 'j'],
type: 'bool',
help: 'JSON stream output.'
}
];
do_list.help = [
'Show all of an account\'s SSH keys.',
'',
'Usage:',
' {{name}} key list [<options>]',
'',
'{{options}}',
'',
'By default this lists just the key content for each key -- in other',
'words, content appropriate for a "~/.ssh/authorized_keys" file.',
'Use `triton keys -j` to see all fields.'
].join('\n');
do_list.aliases = ['ls'];
module.exports = do_list;

48
lib/do_key/index.js Normal file
View File

@ -0,0 +1,48 @@
/*
* 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 key ...`
*/
var Cmdln = require('cmdln').Cmdln;
var util = require('util');
// ---- CLI class
function KeyCLI(top) {
this.top = top;
Cmdln.call(this, {
name: top.name + ' key',
desc: 'Account SSH key commands.',
helpSubcmds: [
'help',
{ group: 'Key Resources' },
'add',
'list',
'get',
'delete'
]
});
}
util.inherits(KeyCLI, Cmdln);
KeyCLI.prototype.init = function init(opts, args, cb) {
this.log = this.top.log;
Cmdln.prototype.init.apply(this, arguments);
};
KeyCLI.prototype.do_add = require('./do_add');
KeyCLI.prototype.do_get = require('./do_get');
KeyCLI.prototype.do_list = require('./do_list');
KeyCLI.prototype.do_delete = require('./do_delete');
module.exports = KeyCLI;

View File

@ -7,62 +7,21 @@
/*
* Copyright 2015 Joyent, Inc.
*
* `triton keys ...`
* `triton keys ...` bwcompat shortcut for `triton keys list ...`.
*/
var common = require('./common');
var errors = require('./errors');
function do_keys(subcmd, opts, args, cb) {
if (opts.help) {
this.do_help('help', {}, [subcmd], cb);
return;
} else if (args.length !== 0) {
cb(new errors.UsageError('invalid args: ' + args));
return;
function do_keys(subcmd, opts, args, callback) {
var subcmdArgv = ['node', 'triton', 'key', 'list'].concat(args);
this.dispatch('key', subcmdArgv, callback);
}
this.tritonapi.cloudapi.listKeys(function (err, keys) {
if (err) {
cb(err);
return;
}
do_keys.help = [
'A shortcut for "triton key list".',
'',
'Usage:',
' {{name}} key ...'
].join('\n');
if (opts.json) {
common.jsonStream(keys);
} else {
keys.forEach(function (key) {
console.log(common.chomp(key.key));
});
}
cb();
});
}
do_keys.options = [
{
names: ['help', 'h'],
type: 'bool',
help: 'Show this help.'
},
{
names: ['json', 'j'],
type: 'bool',
help: 'JSON output.'
}
];
do_keys.help = (
'Show account SSH keys.\n'
+ '\n'
+ 'Usage:\n'
+ ' {{name}} keys [<options>]\n'
+ '\n'
+ '{{options}}'
+ '\n'
+ 'By default this lists just the key content for each key -- in other\n'
+ 'words, content appropriate for a "~/.ssh/authorized_keys" file.\n'
+ 'Use `triton keys -j` to see all fields.\n'
);
do_keys.hidden = true;
module.exports = do_keys;

View File

@ -156,7 +156,7 @@ do_list.options = [
do_list.help = [
/* BEGIN JSSTYLED */
'List packgaes.',
'List packages.',
'',
'Usage:',
' {{name}} package list [<filters>]',

View File

@ -22,8 +22,6 @@ do_profiles.help = [
' {{name}} profiles ...'
].join('\n');
do_profiles.aliases = ['imgs'];
do_profiles.hidden = true;
module.exports = do_profiles;

View File

@ -0,0 +1,95 @@
/*
* 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 (c) 2015, Joyent, Inc.
*/
/*
* Integration tests for `triton key ...`
*/
var h = require('./helpers');
var test = require('tape');
var backoff = require('backoff');
// --- Globals
var KEY_PATH = 'data/id_rsa.pub';
var KEY_SIG = '66:ca:1c:09:75:99:35:69:be:91:08:25:03:c0:17:c0';
var KEY_EMAIL = 'test@localhost.local';
var MAX_CHECK_KEY_TRIES = 10;
// --- Tests
test('triton key', function (tt) {
tt.test(' triton key add', function (t) {
h.triton('key add ' + KEY_PATH, function (err, stdout, stderr) {
if (h.ifErr(t, err, 'triton key add'))
return t.end();
t.ok(stdout.match('Added key "' + KEY_SIG + '"'));
t.end();
});
});
tt.test(' triton key get', function (t) {
h.triton('key get ' + KEY_SIG, function (err, stdout, stderr) {
if (h.ifErr(t, err, 'triton key get'))
return t.end();
t.ok(stdout.match(KEY_EMAIL));
t.end();
});
});
tt.test(' triton key list', function (t) {
h.triton('key list', function (err, stdout, stderr) {
if (h.ifErr(t, err, 'triton key list'))
return t.end();
// there should always be at least two keys -- the original
// account's key, and the test key these tests added
var keys = stdout.split('\n');
t.ok(keys.length > 2, 'triton key list expected key num');
var testKeys = keys.filter(function (key) {
return key.match(KEY_EMAIL);
});
// this test is a tad dodgy, since it's plausible that there might
// be other test keys with different signatures lying around
t.equal(testKeys.length, 1, 'triton key list test key found');
t.end();
});
});
tt.test(' triton key delete', function (t) {
var cmd = 'key delete ' + KEY_SIG + ' --yes';
h.triton(cmd, function (err, stdout, stderr) {
if (h.ifErr(t, err, 'triton key delete'))
return t.end();
t.ok(stdout.match('Deleted key "' + KEY_SIG + '"'));
// verify key is gone, which sometimes takes a while
var call = backoff.call(function checkKey(next) {
h.triton('key get ' + KEY_SIG, function (err2) {
next(!err2);
});
}, function (err3) {
h.ifErr(t, err3, 'triton key delete did not remove key');
t.end();
});
call.failAfter(MAX_CHECK_KEY_TRIES);
call.start();
});
});
});

View File

@ -22,9 +22,13 @@ var common = require('../../lib/common');
var subs = [
['info'],
['profile'],
['profiles'],
['profile list', 'profile ls', 'profiles'],
['profile get'],
['profile set-current'],
['profile create'],
['profile edit'],
['profile delete', 'profile rm'],
['account', 'whoami'],
['keys'],
['services'],
['datacenters'],
['create-instance', 'create'],
@ -37,12 +41,35 @@ var subs = [
['delete-instance', 'delete'],
['wait-instance', 'wait'],
['ssh'],
['images', 'imgs'],
['image', 'img'],
['packages', 'pkgs'],
['package', 'pkg'],
['networks'],
['network']
['network'],
['key'],
['key add'],
['key list', 'key ls', 'keys'],
['key get'],
['key delete', 'key rm'],
['image', 'img'],
['image get'],
['image list', 'images', 'imgs'],
['package', 'pkg'],
['package get'],
['package list', 'packages', 'pkgs'],
['rbac'],
['rbac info'],
['rbac apply'],
['rbac users'],
['rbac user'],
['rbac keys'],
['rbac key'],
['rbac policies'],
['rbac policy'],
['rbac roles'],
['rbac role'],
['rbac instance-role-tags'],
['rbac image-role-tags'],
['rbac network-role-tags'],
['rbac package-role-tags'],
['rbac role-tags']
];
// --- Tests
@ -58,8 +85,11 @@ test('triton subcommands', function (ttt) {
// triton help <subcmd>
// triton <subcmd> -h
subcmds.forEach(function (subcmd) {
tt.test(f(' triton help %s', subcmd), function (t) {
h.triton(['help', subcmd], function (err, stdout, stderr) {
var helpArgs = subcmd.split(' ');
helpArgs.splice(helpArgs.length - 1, 0, 'help');
tt.test(f(' triton %s', helpArgs.join(' ')), function (t) {
h.triton(helpArgs, function (err, stdout, stderr) {
if (h.ifErr(t, err, 'no error'))
return t.end();
t.equal(stderr, '', 'stderr produced');
@ -69,8 +99,10 @@ test('triton subcommands', function (ttt) {
});
});
tt.test(f(' triton %s -h', subcmd), function (t) {
h.triton([subcmd, '-h'], function (err, stdout, stderr) {
var flagArgs = subcmd.split(' ').concat('-h');
tt.test(f(' triton %s', flagArgs.join(' ')), function (t) {
h.triton(flagArgs, function (err, stdout, stderr) {
if (h.ifErr(t, err, 'no error'))
return t.end();
t.equal(stderr, '', 'stderr produced');

View File

@ -0,0 +1 @@
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDNr4q9zMKtylAZGr17fjtUfH2gS+6Gx4m3TJ1H5QwC97JCmtTgke/PBRSEacbXjsYlBjJ9DifNpIbrZrP9hOGhknPDyC3EaRUe/TCUCVGRiHFspurxZAiHvfENCQcvDaVcu9/tO3QyGjDSoYSaQ6NNvl8+yPZ6+mGtXeMnXlCWEvhy/fe3yNVp0isvSIinB2paI+pQqmytJ8omCGShdLqq4/Lvw/zbROe6gEb78+mvwqS+fqYDuPjGc5DXZATqM8rjOSKSPllzNILh9MbR3cHDBfhVi77jL9P8FfQv1U1SZBTbZj78xFcBRnGxrxWE0H7FcXldK5vJvafqWgxZZpgb test@localhost.local