joyent/node-triton#54 'triton rbac role-tags' and refactoring to make adding other *-role-tags easy

This commit is contained in:
Trent Mick 2015-11-12 16:04:12 -08:00
parent 4e45e4061f
commit cd611dafde
7 changed files with 647 additions and 323 deletions

View File

@ -22,6 +22,8 @@
configuration. This command is still in development. configuration. This command is still in development.
- `triton rbac instance-role-tags ...` to list and manage role tags - `triton rbac instance-role-tags ...` to list and manage role tags
on an instance. on an instance.
- `triton rbac role-tags ...` lower-level command for managing role
tags directly on cloudapi RBAC resource *urls*.
## 2.1.4 ## 2.1.4

View File

@ -103,7 +103,9 @@ SaferJsonClient.prototype.parse = function parse(req, callback) {
// Content-Length check // Content-Length check
var contentLength = Number(res.headers['content-length']); var contentLength = Number(res.headers['content-length']);
if (!isNaN(contentLength) && len !== contentLength) { if (req.method !== 'HEAD' &&
!isNaN(contentLength) && len !== contentLength)
{
resErr = new errors.InvalidContentError(util.format( resErr = new errors.InvalidContentError(util.format(
'Incomplete content: Content-Length:%s but got %s bytes', 'Incomplete content: Content-Length:%s but got %s bytes',
contentLength, len)); contentLength, len));

View File

@ -1170,6 +1170,83 @@ CloudApi.prototype.deletePolicy = function deletePolicy(opts, cb) {
}; };
// ---- RBAC role tag support functions
/**
* <http://apidocs.joyent.com/cloudapi/#role-tags>
* Get RBAC role-tags on a resource. Technically there isn't a separate
* specific API endpoint for this -- it is the Get$Resource endpoint instead.
*
* @param {Object} opts:
* - {String} resource (required) The resource URL. E.g.
* '/:account/machines/:uuid' to tag a particular machine instance.
* @param {Function} cb of the form `function (err, roleTags, resource, res)`
* @throws {AssertionError, TypeError} on invalid inputs
*/
CloudApi.prototype.getRoleTags = function getRoleTags(opts, cb) {
assert.object(opts, 'opts');
assert.string(opts.resource, 'opts.resource');
assert.func(cb, 'cb');
// Validate `resource`.
// TODO: share this validation with `setRoleTags`.
var resourceRe = new RegExp('^/[^/]{2,}/[^/]+');
if (! resourceRe.test(opts.resource)) {
throw new TypeError(format('invalid resource "%s": must match ' +
'"/:account/:type..."', opts.resource));
}
var validResources = [
'machines',
'packages',
'images',
'fwrules',
'networks',
// TODO: validate/test role tags on these rbac resources
'users',
'roles',
'policies',
// TODO: validate, test
'keys',
'datacenters',
'analytics',
'instrumentations'
];
var parts = opts.resource.split('/');
if (validResources.indexOf(parts[2]) === -1) {
throw new TypeError(format('invalid resource "%s": resource type ' +
'must be one of: %s', opts.resource, validResources.join(', ')));
}
function roleTagsFromRes(res) {
return (
(res.headers['role-tag'] || '')
/* JSSTYLED */
.split(/\s*,\s*/)
.filter(function (r) { return r.trim(); })
);
}
this._request({
/*
* We use GET instead of HEAD to also be able to return the
* resource JSON. Technically we *could* drop support for that from
* this API, but `tritonapi.js` is using it.
*/
method: 'GET',
path: opts.resource
}, function (err, req, res, body) {
if (err) {
cb(err, null, res);
return;
}
var roleTags = roleTagsFromRes(res);
cb(err, roleTags, body, res);
});
};
/** /**
* <http://apidocs.joyent.com/cloudapi/#SetRoleTags> * <http://apidocs.joyent.com/cloudapi/#SetRoleTags>
* Set RBAC role-tags on a resource. * Set RBAC role-tags on a resource.
@ -1191,7 +1268,6 @@ CloudApi.prototype.setRoleTags = function setRoleTags(opts, cb) {
assert.func(cb, 'cb'); assert.func(cb, 'cb');
// Validate `resource`. // Validate `resource`.
// XXX Do we need to massage '/my' to '/:account' as old cloudapi.js does?
var resourceRe = new RegExp('^/[^/]{2,}/[^/]+'); var resourceRe = new RegExp('^/[^/]{2,}/[^/]+');
if (! resourceRe.test(opts.resource)) { if (! resourceRe.test(opts.resource)) {
throw new TypeError(format('invalid resource "%s": must match ' + throw new TypeError(format('invalid resource "%s": must match ' +

View File

@ -333,7 +333,7 @@ function normShortId(s) {
* and DC URL. This is currently used to create a filesystem-safe name * and DC URL. This is currently used to create a filesystem-safe name
* to use for caching * to use for caching
*/ */
function slug(o) { function profileSlug(o) {
assert.object(o, 'o'); assert.object(o, 'o');
assert.string(o.account, 'o.account'); assert.string(o.account, 'o.account');
assert.string(o.url, 'o.url'); assert.string(o.url, 'o.url');
@ -344,6 +344,17 @@ function slug(o) {
return s; return s;
} }
/*
* Return a filename-safe slug for the given string.
*/
function filenameSlug(str) {
return str
.toLowerCase()
.replace(/ +/g, '-')
.replace(/[^-\w]/g, '');
}
/* /*
* take some basic information and return node-cmdln options suitable for * take some basic information and return node-cmdln options suitable for
* tabula * tabula
@ -723,7 +734,8 @@ module.exports = {
capitalize: capitalize, capitalize: capitalize,
normShortId: normShortId, normShortId: normShortId,
uuidToShortId: uuidToShortId, uuidToShortId: uuidToShortId,
slug: slug, profileSlug: profileSlug,
filenameSlug: filenameSlug,
getCliTableOptions: getCliTableOptions, getCliTableOptions: getCliTableOptions,
promptYesNo: promptYesNo, promptYesNo: promptYesNo,
promptEnter: promptEnter, promptEnter: promptEnter,

View File

@ -27,90 +27,6 @@ var errors = require('../errors');
// ---- internal support stuff // ---- internal support stuff
function _listRoleTags(opts, cb) {
assert.object(opts.cli, 'opts.cli');
assert.string(opts.resourceId, 'opts.resourceId');
assert.optionalBool(opts.json, 'opts.json');
assert.func(cb, 'cb');
var cli = opts.cli;
cli.tritonapi.getInstanceRoleTags(opts.resourceId,
function (err, roleTags) {
if (err) {
cb(err);
return;
}
if (opts.json) {
console.log(JSON.stringify(roleTags));
} else {
roleTags.forEach(function (r) {
console.log(r);
});
}
cb();
});
}
function _addRoleTags(opts, cb) {
assert.object(opts.cli, 'opts.cli');
assert.string(opts.resourceId, 'opts.resourceId');
assert.arrayOfString(opts.roleTags, 'opts.roleTags');
assert.func(cb, 'cb');
var cli = opts.cli;
var log = cli.log;
vasync.pipeline({arg: {}, funcs: [
function getCurrRoleTags(ctx, next) {
cli.tritonapi.getInstanceRoleTags(opts.resourceId,
function (err, roleTags, inst) {
if (err) {
next(err);
return;
}
ctx.roleTags = roleTags;
ctx.inst = inst;
log.trace({inst: inst, roleTags: roleTags}, 'curr role tags');
next();
});
},
function addRoleTags(ctx, next) {
var adding = [];
for (var i = 0; i < opts.roleTags.length; i++) {
var r = opts.roleTags[i];
if (ctx.roleTags.indexOf(r) === -1) {
ctx.roleTags.push(r);
adding.push(r);
}
}
if (adding.length === 0) {
next();
return;
} else {
console.log('Adding %d role tag%s (%s) to instance "%s"',
adding.length, adding.length === 1 ? '' : 's',
adding.join(', '), ctx.inst.name);
}
cli.tritonapi.cloudapi.setRoleTags({
resource: _resourceUrlFromId(
cli.tritonapi.profile.account, ctx.inst.id),
roleTags: ctx.roleTags
}, next);
}
]}, function (err) {
cb(err);
});
}
// TODO: resource URL should be in tritonapi.js,
// E.g. perhaps `TritonApi.setInstanceRoleTags`?
function _resourceUrlFromId(account, id) {
return format('/%s/machines/%s', account, id);
}
function _reprFromRoleTags(roleTags) { function _reprFromRoleTags(roleTags) {
assert.arrayOfString(roleTags, 'roleTags'); assert.arrayOfString(roleTags, 'roleTags');
@ -146,14 +62,100 @@ function _roleTagsFromRepr(repr) {
} }
function _listRoleTags(opts, cb) {
assert.object(opts.cli, 'opts.cli');
assert.string(opts.resourceType, 'opts.resourceType');
assert.string(opts.resourceId, 'opts.resourceId');
assert.optionalBool(opts.json, 'opts.json');
assert.func(cb, 'cb');
var cli = opts.cli;
cli.tritonapi.getRoleTags({
resourceType: opts.resourceType,
id: opts.resourceId
}, function (err, roleTags) {
if (err) {
cb(err);
return;
}
if (opts.json) {
console.log(JSON.stringify(roleTags));
} else {
roleTags.forEach(function (r) {
console.log(r);
});
}
cb();
});
}
function _addRoleTags(opts, cb) {
assert.object(opts.cli, 'opts.cli');
assert.string(opts.resourceType, 'opts.resourceType');
assert.string(opts.resourceId, 'opts.resourceId');
assert.arrayOfString(opts.roleTags, 'opts.roleTags');
assert.func(cb, 'cb');
var cli = opts.cli;
var log = cli.log;
vasync.pipeline({arg: {}, funcs: [
function getCurrRoleTags(ctx, next) {
cli.tritonapi.getRoleTags({
resourceType: opts.resourceType,
id: opts.resourceId
}, function (err, roleTags, resource) {
if (err) {
next(err);
return;
}
ctx.roleTags = roleTags;
ctx.resource = resource;
log.trace({resource: resource, roleTags: roleTags},
'curr role tags');
next();
});
},
function addRoleTags(ctx, next) {
var adding = [];
for (var i = 0; i < opts.roleTags.length; i++) {
var r = opts.roleTags[i];
if (ctx.roleTags.indexOf(r) === -1) {
ctx.roleTags.push(r);
adding.push(r);
}
}
if (adding.length === 0) {
next();
return;
} else {
console.log('Adding %d role tag%s (%s) to %s "%s"',
adding.length, adding.length === 1 ? '' : 's',
adding.join(', '), opts.resourceType,
opts.resourceId);
}
cli.tritonapi.setRoleTags({
resourceType: opts.resourceType,
id: (opts.resourceType === 'resource'
? opts.resourceId : ctx.resource.id),
roleTags: ctx.roleTags
}, next);
}
]}, function (err) {
cb(err);
});
}
function _editRoleTags(opts, cb) { function _editRoleTags(opts, cb) {
assert.object(opts.cli, 'opts.cli'); assert.object(opts.cli, 'opts.cli');
assert.string(opts.resourceType, 'opts.resourceType');
assert.string(opts.resourceId, 'opts.resourceId'); assert.string(opts.resourceId, 'opts.resourceId');
assert.func(cb, 'cb'); assert.func(cb, 'cb');
var cli = opts.cli; var cli = opts.cli;
var account = cli.tritonapi.profile.account;
var id;
var roleTags; var roleTags;
var filename; var filename;
var origText; var origText;
@ -197,8 +199,9 @@ function _editRoleTags(opts, cb) {
} }
// Save changes. // Save changes.
cli.tritonapi.cloudapi.setRoleTags({ cli.tritonapi.setRoleTags({
resource: _resourceUrlFromId(account, id), resourceType: opts.resourceType,
id: opts.resourceId,
roleTags: edited roleTags: edited
}, function (setErr) { }, function (setErr) {
if (setErr) { if (setErr) {
@ -215,18 +218,20 @@ function _editRoleTags(opts, cb) {
} }
cli.tritonapi.getInstanceRoleTags(opts.resourceId, cli.tritonapi.getRoleTags({
function (err, roleTags_, inst) { resourceType: opts.resourceType,
id: opts.resourceId
}, function (err, roleTags_) {
if (err) { if (err) {
cb(err); cb(err);
return; return;
} }
id = inst.id;
roleTags = roleTags_; roleTags = roleTags_;
filename = format('%s-inst-%s-roleTags.txt', filename = format('%s-%s-%s-roleTags.txt',
cli.tritonapi.profile.account, cli.tritonapi.profile.account,
opts.resourceId); opts.resourceType,
common.filenameSlug(opts.resourceId));
origText = _reprFromRoleTags(roleTags); origText = _reprFromRoleTags(roleTags);
editAttempt(origText); editAttempt(origText);
}); });
@ -235,6 +240,7 @@ function _editRoleTags(opts, cb) {
function _setRoleTags(opts, cb) { function _setRoleTags(opts, cb) {
assert.object(opts.cli, 'opts.cli'); assert.object(opts.cli, 'opts.cli');
assert.string(opts.resourceType, 'opts.resourceType');
assert.string(opts.resourceId, 'opts.resourceId'); assert.string(opts.resourceId, 'opts.resourceId');
assert.arrayOfString(opts.roleTags, 'opts.roleTags'); assert.arrayOfString(opts.roleTags, 'opts.roleTags');
assert.optionalBool(opts.yes, 'opts.yes'); assert.optionalBool(opts.yes, 'opts.yes');
@ -242,25 +248,12 @@ function _setRoleTags(opts, cb) {
var cli = opts.cli; var cli = opts.cli;
vasync.pipeline({arg: {}, funcs: [ vasync.pipeline({arg: {}, funcs: [
// TODO: consider shorter path if the instance UUID is given
// (but what if the instance has a UUID for an *alias*)?
function getResource(ctx, next) {
cli.tritonapi.getInstance(opts.resourceId, function (err, inst) {
if (err) {
next(err);
return;
}
ctx.inst = inst;
next();
});
},
function confirm(ctx, next) { function confirm(ctx, next) {
if (opts.yes) { if (opts.yes) {
return next(); return next();
} }
var msg = format('Set role tags on instance "%s"? [y/n] ', var msg = format('Set role tags on %s "%s"? [y/n] ',
ctx.inst.name); opts.resourceType, opts.resourceId);
common.promptYesNo({msg: msg}, function (answer) { common.promptYesNo({msg: msg}, function (answer) {
if (answer !== 'y') { if (answer !== 'y') {
console.error('Aborting'); console.error('Aborting');
@ -272,10 +265,11 @@ function _setRoleTags(opts, cb) {
}, },
function setThem(ctx, next) { function setThem(ctx, next) {
console.log('Setting role tags on instance "%s"', ctx.inst.name); console.log('Setting role tags on %s "%s"',
cli.tritonapi.cloudapi.setRoleTags({ opts.resourceType, opts.resourceId);
resource: _resourceUrlFromId( cli.tritonapi.setRoleTags({
cli.tritonapi.profile.account, ctx.inst.id), resourceType: opts.resourceType,
id: opts.resourceId,
roleTags: opts.roleTags roleTags: opts.roleTags
}, next); }, next);
} }
@ -288,6 +282,7 @@ function _setRoleTags(opts, cb) {
function _deleteRoleTags(opts, cb) { function _deleteRoleTags(opts, cb) {
assert.object(opts.cli, 'opts.cli'); assert.object(opts.cli, 'opts.cli');
assert.string(opts.resourceType, 'opts.resourceType');
assert.string(opts.resourceId, 'opts.resourceId'); assert.string(opts.resourceId, 'opts.resourceId');
assert.arrayOfString(opts.roleTags, 'opts.roleTags'); assert.arrayOfString(opts.roleTags, 'opts.roleTags');
assert.func(cb, 'cb'); assert.func(cb, 'cb');
@ -296,15 +291,18 @@ function _deleteRoleTags(opts, cb) {
vasync.pipeline({arg: {}, funcs: [ vasync.pipeline({arg: {}, funcs: [
function getCurrRoleTags(ctx, next) { function getCurrRoleTags(ctx, next) {
cli.tritonapi.getInstanceRoleTags(opts.resourceId, cli.tritonapi.getRoleTags({
function (err, roleTags, inst) { resourceType: opts.resourceType,
id: opts.resourceId
}, function (err, roleTags, resource) {
if (err) { if (err) {
next(err); next(err);
return; return;
} }
ctx.roleTags = roleTags; ctx.roleTags = roleTags;
ctx.inst = inst; ctx.resource = resource;
log.trace({inst: inst, roleTags: roleTags}, 'curr role tags'); log.trace({resource: resource, roleTags: roleTags},
'curr role tags');
next(); next();
}); });
}, },
@ -328,9 +326,10 @@ function _deleteRoleTags(opts, cb) {
return next(); return next();
} }
var msg = format( var msg = format(
'Delete %d role tag%s (%s) from instance "%s"? [y/n] ', 'Delete %d role tag%s (%s) from %s "%s"? [y/n] ',
ctx.toDelete.length, ctx.toDelete.length === 1 ? '' : 's', ctx.toDelete.length, ctx.toDelete.length === 1 ? '' : 's',
ctx.toDelete.join(', '), ctx.inst.name); ctx.toDelete.join(', '),
opts.resourceType, opts.resourceId);
common.promptYesNo({msg: msg}, function (answer) { common.promptYesNo({msg: msg}, function (answer) {
if (answer !== 'y') { if (answer !== 'y') {
console.error('Aborting'); console.error('Aborting');
@ -346,12 +345,14 @@ function _deleteRoleTags(opts, cb) {
next(); next();
return; return;
} }
console.log('Deleting %d role tag%s (%s) from instance "%s"', console.log('Deleting %d role tag%s (%s) from %s "%s"',
ctx.toDelete.length, ctx.toDelete.length === 1 ? '' : 's', ctx.toDelete.length, ctx.toDelete.length === 1 ? '' : 's',
ctx.toDelete.join(', '), ctx.inst.name); ctx.toDelete.join(', '),
cli.tritonapi.cloudapi.setRoleTags({ opts.resourceType, opts.resourceId);
resource: _resourceUrlFromId( cli.tritonapi.setRoleTags({
cli.tritonapi.profile.account, ctx.inst.id), resourceType: opts.resourceType,
id: (opts.resourceType === 'resource'
? opts.resourceId : ctx.resource.id),
roleTags: ctx.roleTagsToKeep roleTags: ctx.roleTagsToKeep
}, next); }, next);
} }
@ -363,30 +364,18 @@ function _deleteRoleTags(opts, cb) {
function _deleteAllRoleTags(opts, cb) { function _deleteAllRoleTags(opts, cb) {
assert.object(opts.cli, 'opts.cli'); assert.object(opts.cli, 'opts.cli');
assert.string(opts.resourceType, 'opts.resourceType');
assert.string(opts.resourceId, 'opts.resourceId'); assert.string(opts.resourceId, 'opts.resourceId');
assert.func(cb, 'cb'); assert.func(cb, 'cb');
var cli = opts.cli; var cli = opts.cli;
vasync.pipeline({arg: {}, funcs: [ vasync.pipeline({arg: {}, funcs: [
// TODO: consider shorter path if the instance UUID is given
// (but what if the instance has a UUID for an *alias*)?
function getResource(ctx, next) {
cli.tritonapi.getInstance(opts.resourceId, function (err, inst) {
if (err) {
next(err);
return;
}
ctx.inst = inst;
next();
});
},
function confirm(ctx, next) { function confirm(ctx, next) {
if (opts.yes) { if (opts.yes) {
return next(); return next();
} }
var msg = format('Delete all role tags from instance "%s"? [y/n] ', var msg = format('Delete all role tags from %s "%s"? [y/n] ',
ctx.inst.name); opts.resourceType, opts.resourceId);
common.promptYesNo({msg: msg}, function (answer) { common.promptYesNo({msg: msg}, function (answer) {
if (answer !== 'y') { if (answer !== 'y') {
console.error('Aborting'); console.error('Aborting');
@ -398,11 +387,11 @@ function _deleteAllRoleTags(opts, cb) {
}, },
function deleteAllRoleTags(ctx, next) { function deleteAllRoleTags(ctx, next) {
console.log('Deleting all role tags from instance "%s"', console.log('Deleting all role tags from %s "%s"',
ctx.inst.name); opts.resourceType, opts.resourceId);
cli.tritonapi.cloudapi.setRoleTags({ cli.tritonapi.setRoleTags({
resource: _resourceUrlFromId( resourceType: opts.resourceType,
cli.tritonapi.profile.account, ctx.inst.id), id: opts.resourceId,
roleTags: [] roleTags: []
}, next); }, next);
} }
@ -426,157 +415,270 @@ function _roleTagsFromArrayOfString(arr) {
} }
// ---- `triton rbac instance-role-tags` // ---- `triton rbac role-tags RESOURCE-URL`
function do_instance_role_tags(subcmd, opts, args, cb) {
if (opts.help) {
this.do_help('help', {}, [subcmd], cb);
return;
}
// Which action? //do_role_tags.options = [
var actions = []; // {
if (opts.add) { actions.push('add'); } // names: ['help', 'h'],
if (opts.edit) { actions.push('edit'); } // type: 'bool',
if (opts.set) { actions.push('set'); } // help: 'Show this help.'
if (opts['delete']) { actions.push('delete'); } // },
if (opts.delete_all) { actions.push('deleteAll'); } // {
var action; // names: ['json', 'j'],
if (actions.length === 0) { // type: 'bool',
action = 'list'; // help: 'JSON stream output.'
} else if (actions.length > 1) { // },
return cb(new errors.UsageError( // {
'only one action option may be used at once')); // names: ['yes', 'y'],
} else { // type: 'bool',
action = actions[0]; // help: 'Answer yes to confirmations, e.g. confirmation of deletion.'
} // },
// {
// group: 'Action Options'
// },
// {
// names: ['add', 'a'],
// type: 'arrayOfString',
// helpArg: 'ROLE[,ROLE...]',
// help: 'Add the given role tags. Can be specified multiple times.'
// },
// {
// names: ['set', 's'],
// type: 'arrayOfString',
// helpArg: 'ROLE[,ROLE...]',
// help: 'Set role tags to the given value(s). Can be specified ' +
// 'multiple times.'
// },
// {
// names: ['edit', 'e'],
// type: 'bool',
// help: 'Edit role tags in your $EDITOR.'
// },
// {
// names: ['delete', 'd'],
// type: 'arrayOfString',
// helpArg: 'ROLE[,ROLE...]',
// help: 'Delete the given role tags. Can be specified multiple times.'
// },
// {
// names: ['delete-all', 'D'],
// type: 'bool',
// help: 'Delete all role tags from the given resource.'
// }
//];
//do_role_tags.help = [
// /* BEGIN JSSTYLED */
// 'List and manage role tags for the given instance.',
// '',
// 'Usage:',
// ' {{name}} instance-role-tags INST # list role tags',
// ' {{name}} instance-role-tags -a ROLE[,ROLE...] INST # add',
// ' {{name}} instance-role-tags -s ROLE[,ROLE...] INST # set/replace',
// ' {{name}} instance-role-tags -e INST # edit in $EDITOR',
// ' {{name}} instance-role-tags -d ROLE[,ROLE...] INST # delete',
// ' {{name}} instance-role-tags -D INST # delete all',
// '',
// '{{options}}',
// 'Where "ROLE" is a role tag name (see `triton rbac roles`) and INST is',
// 'an instance "id", "name" or short id.'
// /* END JSSTYLED */
//].join('\n');
// Arg count validation. function makeRoleTagsFunc(cfg) {
if (args.length === 0) { assert.string(cfg.resourceType, 'cfg.resourceType');
return cb(new errors.UsageError('INST argument is required')); assert.string(cfg.funcName, 'cfg.funcName');
} else if (args.length > 1) { assert.string(cfg.cmdName, 'cfg.cmdName');
return cb(new errors.UsageError('too many arguments')); assert.string(cfg.argName, 'cfg.argName');
} assert.string(cfg.helpArgIs, 'cfg.helpArgIs');
switch (action) { var func = function (subcmd, opts, args, cb) {
case 'list': if (opts.help) {
_listRoleTags({ this.do_help('help', {}, [subcmd], cb);
cli: this.top, return;
resourceId: args[0], }
json: opts.json
}, cb); // Which action?
break; var actions = [];
case 'add': if (opts.add) { actions.push('add'); }
_addRoleTags({ if (opts.edit) { actions.push('edit'); }
cli: this.top, if (opts.set) { actions.push('set'); }
resourceId: args[0], if (opts['delete']) { actions.push('delete'); }
roleTags: _roleTagsFromArrayOfString(opts.add) if (opts.delete_all) { actions.push('deleteAll'); }
}, cb); var action;
break; if (actions.length === 0) {
case 'edit': action = 'list';
_editRoleTags({ } else if (actions.length > 1) {
cli: this.top, return cb(new errors.UsageError(
resourceId: args[0] 'only one action option may be used at once'));
}, cb); } else {
break; action = actions[0];
case 'set': }
_setRoleTags({
cli: this.top, // Arg count validation.
resourceId: args[0], if (args.length === 0) {
roleTags: _roleTagsFromArrayOfString(opts.set), return cb(new errors.UsageError(cfg.argName +
yes: opts.yes ' argument is required'));
}, cb); } else if (args.length > 1) {
break; return cb(new errors.UsageError('too many arguments'));
case 'delete': }
_deleteRoleTags({
cli: this.top, switch (action) {
resourceId: args[0], case 'list':
roleTags: _roleTagsFromArrayOfString(opts['delete']), _listRoleTags({
yes: opts.yes cli: this.top,
}, cb); resourceType: cfg.resourceType,
break; resourceId: args[0],
case 'deleteAll': json: opts.json
_deleteAllRoleTags({ }, cb);
cli: this.top, break;
resourceId: args[0], case 'add':
yes: opts.yes _addRoleTags({
}, cb); cli: this.top,
break; resourceType: cfg.resourceType,
default: resourceId: args[0],
return cb(new errors.InternalError('unknown action: ' + action)); roleTags: _roleTagsFromArrayOfString(opts.add)
} }, cb);
break;
case 'edit':
_editRoleTags({
cli: this.top,
resourceType: cfg.resourceType,
resourceId: args[0]
}, cb);
break;
case 'set':
_setRoleTags({
cli: this.top,
resourceType: cfg.resourceType,
resourceId: args[0],
roleTags: _roleTagsFromArrayOfString(opts.set),
yes: opts.yes
}, cb);
break;
case 'delete':
_deleteRoleTags({
cli: this.top,
resourceType: cfg.resourceType,
resourceId: args[0],
roleTags: _roleTagsFromArrayOfString(opts['delete']),
yes: opts.yes
}, cb);
break;
case 'deleteAll':
_deleteAllRoleTags({
cli: this.top,
resourceType: cfg.resourceType,
resourceId: args[0],
yes: opts.yes
}, cb);
break;
default:
return cb(new errors.InternalError('unknown action: ' + action));
}
};
func.name = cfg.funcName;
func.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 confirmations, e.g. confirmation of deletion.'
},
{
group: 'Action Options'
},
{
names: ['add', 'a'],
type: 'arrayOfString',
helpArg: 'ROLE[,ROLE...]',
help: 'Add the given role tags. Can be specified multiple times.'
},
{
names: ['set', 's'],
type: 'arrayOfString',
helpArg: 'ROLE[,ROLE...]',
help: 'Set role tags to the given value(s). Can be specified ' +
'multiple times.'
},
{
names: ['edit', 'e'],
type: 'bool',
help: 'Edit role tags in your $EDITOR.'
},
{
names: ['delete', 'd'],
type: 'arrayOfString',
helpArg: 'ROLE[,ROLE...]',
help: 'Delete the given role tags. Can be specified multiple times.'
},
{
names: ['delete-all', 'D'],
type: 'bool',
help: 'Delete all role tags from the given resource.'
}
];
func.helpOpts = {
helpCol: 23
};
func.help = [
/* BEGIN JSSTYLED */
'List and manage role tags for the given $resourceType.',
'',
'Usage:',
' {{name}} $cmdName $argName # list role tags',
' {{name}} $cmdName -a ROLE[,ROLE...] $argName # add',
' {{name}} $cmdName -s ROLE[,ROLE...] $argName # set/replace',
' {{name}} $cmdName -e $argName # edit in $EDITOR',
' {{name}} $cmdName -d ROLE[,ROLE...] $argName # delete',
' {{name}} $cmdName -D $argName # delete all',
'',
'{{options}}',
'Where "ROLE" is a role tag name (see `triton rbac roles`) and',
'$argName is $helpArgIs.'
/* END JSSTYLED */
].join('\n');
['resourceType', 'cmdName', 'argName', 'helpArgIs'].forEach(function (key) {
func.help = func.help.replace(new RegExp('\\$' + key, 'g'), cfg[key]);
});
return func;
} }
do_instance_role_tags.options = [
{ var do_role_tags = makeRoleTagsFunc({
names: ['help', 'h'], resourceType: 'resource',
type: 'bool', funcName: 'do_role_tags',
help: 'Show this help.' cmdName: 'role-tags',
}, argName: 'RESOURCE-URL',
{ helpArgIs: 'an RBAC resource URL'
names: ['json', 'j'], });
type: 'bool',
help: 'JSON stream output.' var do_instance_role_tags = makeRoleTagsFunc({
}, resourceType: 'instance',
{ funcName: 'do_instance_role_tags',
names: ['yes', 'y'], cmdName: 'instance-role-tags',
type: 'bool', argName: 'INST',
help: 'Answer yes to confirmations, e.g. confirmation of deletion.' helpArgIs: 'an instance "id", "name" or short id'
}, });
{
group: 'Action Options'
},
{
names: ['add', 'a'],
type: 'arrayOfString',
helpArg: 'ROLE[,ROLE...]',
help: 'Add the given role tags. Can be specified multiple times.'
},
{
names: ['set', 's'],
type: 'arrayOfString',
helpArg: 'ROLE[,ROLE...]',
help: 'Set role tags to the given value(s). Can be specified ' +
'multiple times.'
},
{
names: ['edit', 'e'],
type: 'bool',
help: 'Edit role tags in your $EDITOR.'
},
{
names: ['delete', 'd'],
type: 'arrayOfString',
helpArg: 'ROLE[,ROLE...]',
help: 'Delete the given role tags. Can be specified multiple times.'
},
{
names: ['delete-all', 'D'],
type: 'bool',
help: 'Delete all role tags from the given resource.'
}
];
do_instance_role_tags.help = [
/* BEGIN JSSTYLED */
'List and manage role tags for the given instance.',
'',
'Usage:',
' {{name}} instance-role-tags INST # list role tags',
' {{name}} instance-role-tags -a ROLE[,ROLE...] INST # add',
' {{name}} instance-role-tags -s ROLE[,ROLE...] INST # set/replace',
' {{name}} instance-role-tags -e INST # edit in $EDITOR',
' {{name}} instance-role-tags -d ROLE[,ROLE...] INST # delete',
' {{name}} instance-role-tags -D INST # delete all',
'',
'{{options}}',
'Where "ROLE" is a role tag name (see `triton rbac roles`) and INST is',
'an instance "id", "name" or short id.'
/* END JSSTYLED */
].join('\n');
module.exports = { module.exports = {
//do_role_tags: do_role_tags, do_role_tags: do_role_tags,
do_instance_role_tags: do_instance_role_tags do_instance_role_tags: do_instance_role_tags
}; };

View File

@ -42,7 +42,8 @@ function RbacCLI(top) {
'roles', 'roles',
'role', 'role',
{ group: 'Role Tags' }, { group: 'Role Tags' },
'instance-role-tags' 'instance-role-tags',
'role-tags'
] ]
}); });
} }
@ -67,7 +68,7 @@ RbacCLI.prototype.do_roles = require('./do_roles');
RbacCLI.prototype.do_role = require('./do_role'); RbacCLI.prototype.do_role = require('./do_role');
var doRoleTags = require('./do_role_tags'); var doRoleTags = require('./do_role_tags');
//RbacCLI.prototype.do_role_tags = doRoleTags.do_role_tags; RbacCLI.prototype.do_role_tags = doRoleTags.do_role_tags;
RbacCLI.prototype.do_instance_role_tags = doRoleTags.do_instance_role_tags; RbacCLI.prototype.do_instance_role_tags = doRoleTags.do_instance_role_tags;
module.exports = RbacCLI; module.exports = RbacCLI;

View File

@ -32,6 +32,30 @@ var loadConfigSync = require('./config').loadConfigSync;
// ---- internal support stuff
function _assertRoleTagResourceType(resourceType, errName) {
assert.string(resourceType, errName);
var knownResourceTypes = ['resource', 'instance', 'image',
'package', 'network'];
assert.ok(knownResourceTypes.indexOf(resourceType) !== -1,
'unknown resource type: ' + resourceType);
}
function _roleTagResourceUrl(account, type, id) {
var ns = {
instance: 'machines',
image: 'images',
'package': 'packages',
network: 'networks'
}[type];
assert.ok(ns, 'unknown resource type: ' + type);
return format('/%s/%s/%s', account, ns, id);
}
//---- TritonApi class //---- TritonApi class
/** /**
@ -66,7 +90,7 @@ function TritonApi(opts) {
if (this.config.cacheDir) { if (this.config.cacheDir) {
this.cacheDir = path.resolve(this.config._configDir, this.cacheDir = path.resolve(this.config._configDir,
this.config.cacheDir, this.config.cacheDir,
common.slug(this.profile)); common.profileSlug(this.profile));
this.log.trace({cacheDir: this.cacheDir}, 'cache dir'); this.log.trace({cacheDir: this.cacheDir}, 'cache dir');
// TODO perhaps move this to an async .init() // TODO perhaps move this to an async .init()
if (!fs.existsSync(this.cacheDir)) { if (!fs.existsSync(this.cacheDir)) {
@ -593,58 +617,163 @@ TritonApi.prototype.getInstance = function getInstance(name, cb) {
/** /**
* Get instance role tags. * Get role tags for a resource.
* *
* @param {String} name The instance id, name, or short id. * @param {Object} opts
* @param {Function} callback `function (err, roleTags, inst)` * - resourceType {String} One of:
* resource (a raw RBAC resource URL)
* instance
* image
* package
* network
* - id {String} The resource identifier. E.g. for an instance this can be
* the ID (a UUID), login or short id. Whatever `triton` typically allows
* for identification.
* @param {Function} callback `function (err, roleTags, resource)`
*/ */
TritonApi.prototype.getInstanceRoleTags = TritonApi.prototype.getRoleTags = function getRoleTags(opts, cb) {
function getInstanceRoleTags(name, cb) {
var self = this; var self = this;
assert.string(name, 'name'); assert.object(opts, 'opts');
_assertRoleTagResourceType(opts.resourceType, 'opts.resourceType');
assert.string(opts.id, 'opts.id');
assert.func(cb, 'cb'); assert.func(cb, 'cb');
function roleTagsFromRes(res) {
return (
(res.headers['role-tag'] || '')
/* JSSTYLED */
.split(/\s*,\s*/)
.filter(function (r) { return r.trim(); })
);
}
var roleTags; var roleTags;
var inst; var resource;
vasync.pipeline({arg: {}, funcs: [ vasync.pipeline({arg: {}, funcs: [
function getInst(ctx, next) { function resolveResourceId(ctx, next) {
self.getInstance(name, function (err, inst_, res) { if (opts.resourceType === 'resource') {
if (err) {
next(err);
return;
}
inst = inst_;
ctx.res = res;
next();
});
},
function getMachineIfNecessary(ctx, next) {
// Sometimes `getInstance` returns a CloudAPI `GetMachine` res on
// which there is a 'role-tag' header that we want. Sometimes not.
if (ctx.res) {
next(); next();
return; return;
} }
self.cloudapi.getMachine(inst.id, function (err, inst_, res) { var getFuncName = {
instance: 'getInstance',
image: 'getImage',
'package': 'getPackage',
network: 'getNetwork'
}[opts.resourceType];
self[getFuncName](opts.id, function (err, resource_, res) {
if (err) { if (err) {
next(err); next(err);
return; return;
} }
ctx.res = res; resource = resource_;
/*
* Sometimes `getInstance` et al return a CloudAPI `GetMachine`
* res on which there is a 'role-tag' header that we want.
*/
if (res) {
roleTags = roleTagsFromRes(res);
}
next(); next();
}); });
}, },
function getRolesFromRes(ctx, next) { function getResourceIfNecessary(ctx, next) {
roleTags = (ctx.res.headers['role-tag'] || '') if (roleTags) {
/* JSSTYLED */ next();
.split(/\s*,\s*/) return;
.filter(function (r) { return r.trim(); }); }
next();
var resourceUrl = (opts.resourceType === 'resource'
? opts.id
: _roleTagResourceUrl(self.profile.account,
opts.resourceType, resource.id));
self.cloudapi.getRoleTags({resource: resourceUrl},
function (err, roleTags_, resource_) {
if (err) {
next(err);
return;
}
roleTags = roleTags_;
resource = resource_;
next();
});
} }
]}, function (err) { ]}, function (err) {
cb(err, roleTags, inst); cb(err, roleTags, resource);
});
};
/**
* Set role tags for a resource.
*
* @param {Object} opts
* - resourceType {String} One of:
* resource (a raw RBAC resource URL)
* instance
* image
* package
* network
* - id {String} The resource identifier. E.g. for an instance this can be
* the ID (a UUID), login or short id. Whatever `triton` typically allows
* for identification.
* - roleTags {Array}
* @param {Function} callback `function (err)`
*/
TritonApi.prototype.setRoleTags = function setRoleTags(opts, cb) {
var self = this;
assert.object(opts, 'opts');
_assertRoleTagResourceType(opts.resourceType, 'opts.resourceType');
assert.string(opts.id, 'opts.id');
assert.arrayOfString(opts.roleTags, 'opts.roleTags');
assert.func(cb, 'cb');
vasync.pipeline({arg: {}, funcs: [
function resolveResourceId(ctx, next) {
if (opts.resourceType === 'resource') {
next();
return;
}
var getFuncName = {
instance: 'getInstance',
image: 'getImage',
'package': 'getPackage',
network: 'getNetwork'
}[opts.resourceType];
self[getFuncName](opts.id, function (err, resource, res) {
if (err) {
next(err);
return;
}
ctx.resource = resource;
next();
});
},
function setTheRoleTags(ctx, next) {
var resourceUrl = (opts.resourceType === 'resource'
? opts.id
: _roleTagResourceUrl(self.profile.account,
opts.resourceType, ctx.resource.id));
self.cloudapi.setRoleTags({
resource: resourceUrl,
roleTags: opts.roleTags
}, function (err) {
if (err) {
next(err);
return;
}
next();
});
}
]}, function (err) {
cb(err);
}); });
}; };