diff --git a/CHANGES.md b/CHANGES.md index f14b861..21b3ac5 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -22,6 +22,8 @@ configuration. This command is still in development. - `triton rbac instance-role-tags ...` to list and manage role tags on an instance. + - `triton rbac role-tags ...` lower-level command for managing role + tags directly on cloudapi RBAC resource *urls*. ## 2.1.4 diff --git a/lib/SaferJsonClient.js b/lib/SaferJsonClient.js index 092ef6d..64ab172 100644 --- a/lib/SaferJsonClient.js +++ b/lib/SaferJsonClient.js @@ -103,7 +103,9 @@ SaferJsonClient.prototype.parse = function parse(req, callback) { // Content-Length check 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( 'Incomplete content: Content-Length:%s but got %s bytes', contentLength, len)); diff --git a/lib/cloudapi2.js b/lib/cloudapi2.js index 725dd76..f097f6e 100644 --- a/lib/cloudapi2.js +++ b/lib/cloudapi2.js @@ -1170,6 +1170,83 @@ CloudApi.prototype.deletePolicy = function deletePolicy(opts, cb) { }; +// ---- RBAC role tag support functions + +/** + * + * 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); + }); +}; + + /** * * Set RBAC role-tags on a resource. @@ -1191,7 +1268,6 @@ CloudApi.prototype.setRoleTags = function setRoleTags(opts, cb) { assert.func(cb, 'cb'); // Validate `resource`. - // XXX Do we need to massage '/my' to '/:account' as old cloudapi.js does? var resourceRe = new RegExp('^/[^/]{2,}/[^/]+'); if (! resourceRe.test(opts.resource)) { throw new TypeError(format('invalid resource "%s": must match ' + diff --git a/lib/common.js b/lib/common.js index 0369b6f..2ff2637 100644 --- a/lib/common.js +++ b/lib/common.js @@ -333,7 +333,7 @@ function normShortId(s) { * and DC URL. This is currently used to create a filesystem-safe name * to use for caching */ -function slug(o) { +function profileSlug(o) { assert.object(o, 'o'); assert.string(o.account, 'o.account'); assert.string(o.url, 'o.url'); @@ -344,6 +344,17 @@ function slug(o) { 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 * tabula @@ -723,7 +734,8 @@ module.exports = { capitalize: capitalize, normShortId: normShortId, uuidToShortId: uuidToShortId, - slug: slug, + profileSlug: profileSlug, + filenameSlug: filenameSlug, getCliTableOptions: getCliTableOptions, promptYesNo: promptYesNo, promptEnter: promptEnter, diff --git a/lib/do_rbac/do_role_tags.js b/lib/do_rbac/do_role_tags.js index f309115..426fa67 100644 --- a/lib/do_rbac/do_role_tags.js +++ b/lib/do_rbac/do_role_tags.js @@ -27,90 +27,6 @@ var errors = require('../errors'); // ---- 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) { 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) { assert.object(opts.cli, 'opts.cli'); + assert.string(opts.resourceType, 'opts.resourceType'); assert.string(opts.resourceId, 'opts.resourceId'); assert.func(cb, 'cb'); var cli = opts.cli; - var account = cli.tritonapi.profile.account; - var id; var roleTags; var filename; var origText; @@ -197,8 +199,9 @@ function _editRoleTags(opts, cb) { } // Save changes. - cli.tritonapi.cloudapi.setRoleTags({ - resource: _resourceUrlFromId(account, id), + cli.tritonapi.setRoleTags({ + resourceType: opts.resourceType, + id: opts.resourceId, roleTags: edited }, function (setErr) { if (setErr) { @@ -215,18 +218,20 @@ function _editRoleTags(opts, cb) { } - cli.tritonapi.getInstanceRoleTags(opts.resourceId, - function (err, roleTags_, inst) { + cli.tritonapi.getRoleTags({ + resourceType: opts.resourceType, + id: opts.resourceId + }, function (err, roleTags_) { if (err) { cb(err); return; } - id = inst.id; roleTags = roleTags_; - filename = format('%s-inst-%s-roleTags.txt', + filename = format('%s-%s-%s-roleTags.txt', cli.tritonapi.profile.account, - opts.resourceId); + opts.resourceType, + common.filenameSlug(opts.resourceId)); origText = _reprFromRoleTags(roleTags); editAttempt(origText); }); @@ -235,6 +240,7 @@ function _editRoleTags(opts, cb) { function _setRoleTags(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.optionalBool(opts.yes, 'opts.yes'); @@ -242,25 +248,12 @@ function _setRoleTags(opts, cb) { var cli = opts.cli; 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) { if (opts.yes) { return next(); } - var msg = format('Set role tags on instance "%s"? [y/n] ', - ctx.inst.name); + var msg = format('Set role tags on %s "%s"? [y/n] ', + opts.resourceType, opts.resourceId); common.promptYesNo({msg: msg}, function (answer) { if (answer !== 'y') { console.error('Aborting'); @@ -272,10 +265,11 @@ function _setRoleTags(opts, cb) { }, function setThem(ctx, next) { - console.log('Setting role tags on instance "%s"', ctx.inst.name); - cli.tritonapi.cloudapi.setRoleTags({ - resource: _resourceUrlFromId( - cli.tritonapi.profile.account, ctx.inst.id), + console.log('Setting role tags on %s "%s"', + opts.resourceType, opts.resourceId); + cli.tritonapi.setRoleTags({ + resourceType: opts.resourceType, + id: opts.resourceId, roleTags: opts.roleTags }, next); } @@ -288,6 +282,7 @@ function _setRoleTags(opts, cb) { function _deleteRoleTags(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'); @@ -296,15 +291,18 @@ function _deleteRoleTags(opts, cb) { vasync.pipeline({arg: {}, funcs: [ function getCurrRoleTags(ctx, next) { - cli.tritonapi.getInstanceRoleTags(opts.resourceId, - function (err, roleTags, inst) { + cli.tritonapi.getRoleTags({ + resourceType: opts.resourceType, + id: opts.resourceId + }, function (err, roleTags, resource) { if (err) { next(err); return; } ctx.roleTags = roleTags; - ctx.inst = inst; - log.trace({inst: inst, roleTags: roleTags}, 'curr role tags'); + ctx.resource = resource; + log.trace({resource: resource, roleTags: roleTags}, + 'curr role tags'); next(); }); }, @@ -328,9 +326,10 @@ function _deleteRoleTags(opts, cb) { return next(); } 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.join(', '), ctx.inst.name); + ctx.toDelete.join(', '), + opts.resourceType, opts.resourceId); common.promptYesNo({msg: msg}, function (answer) { if (answer !== 'y') { console.error('Aborting'); @@ -346,12 +345,14 @@ function _deleteRoleTags(opts, cb) { next(); 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.join(', '), ctx.inst.name); - cli.tritonapi.cloudapi.setRoleTags({ - resource: _resourceUrlFromId( - cli.tritonapi.profile.account, ctx.inst.id), + ctx.toDelete.join(', '), + opts.resourceType, opts.resourceId); + cli.tritonapi.setRoleTags({ + resourceType: opts.resourceType, + id: (opts.resourceType === 'resource' + ? opts.resourceId : ctx.resource.id), roleTags: ctx.roleTagsToKeep }, next); } @@ -363,30 +364,18 @@ function _deleteRoleTags(opts, cb) { function _deleteAllRoleTags(opts, cb) { assert.object(opts.cli, 'opts.cli'); + assert.string(opts.resourceType, 'opts.resourceType'); assert.string(opts.resourceId, 'opts.resourceId'); assert.func(cb, 'cb'); var cli = opts.cli; 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) { if (opts.yes) { return next(); } - var msg = format('Delete all role tags from instance "%s"? [y/n] ', - ctx.inst.name); + var msg = format('Delete all role tags from %s "%s"? [y/n] ', + opts.resourceType, opts.resourceId); common.promptYesNo({msg: msg}, function (answer) { if (answer !== 'y') { console.error('Aborting'); @@ -398,11 +387,11 @@ function _deleteAllRoleTags(opts, cb) { }, function deleteAllRoleTags(ctx, next) { - console.log('Deleting all role tags from instance "%s"', - ctx.inst.name); - cli.tritonapi.cloudapi.setRoleTags({ - resource: _resourceUrlFromId( - cli.tritonapi.profile.account, ctx.inst.id), + console.log('Deleting all role tags from %s "%s"', + opts.resourceType, opts.resourceId); + cli.tritonapi.setRoleTags({ + resourceType: opts.resourceType, + id: opts.resourceId, roleTags: [] }, 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? - var actions = []; - if (opts.add) { actions.push('add'); } - if (opts.edit) { actions.push('edit'); } - if (opts.set) { actions.push('set'); } - if (opts['delete']) { actions.push('delete'); } - if (opts.delete_all) { actions.push('deleteAll'); } - var action; - if (actions.length === 0) { - action = 'list'; - } else if (actions.length > 1) { - return cb(new errors.UsageError( - 'only one action option may be used at once')); - } else { - action = actions[0]; - } +//do_role_tags.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.' +// } +//]; +//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. - if (args.length === 0) { - return cb(new errors.UsageError('INST argument is required')); - } else if (args.length > 1) { - return cb(new errors.UsageError('too many arguments')); - } +function makeRoleTagsFunc(cfg) { + assert.string(cfg.resourceType, 'cfg.resourceType'); + assert.string(cfg.funcName, 'cfg.funcName'); + assert.string(cfg.cmdName, 'cfg.cmdName'); + assert.string(cfg.argName, 'cfg.argName'); + assert.string(cfg.helpArgIs, 'cfg.helpArgIs'); - switch (action) { - case 'list': - _listRoleTags({ - cli: this.top, - resourceId: args[0], - json: opts.json - }, cb); - break; - case 'add': - _addRoleTags({ - cli: this.top, - resourceId: args[0], - roleTags: _roleTagsFromArrayOfString(opts.add) - }, cb); - break; - case 'edit': - _editRoleTags({ - cli: this.top, - resourceId: args[0] - }, cb); - break; - case 'set': - _setRoleTags({ - cli: this.top, - resourceId: args[0], - roleTags: _roleTagsFromArrayOfString(opts.set), - yes: opts.yes - }, cb); - break; - case 'delete': - _deleteRoleTags({ - cli: this.top, - resourceId: args[0], - roleTags: _roleTagsFromArrayOfString(opts['delete']), - yes: opts.yes - }, cb); - break; - case 'deleteAll': - _deleteAllRoleTags({ - cli: this.top, - resourceId: args[0], - yes: opts.yes - }, cb); - break; - default: - return cb(new errors.InternalError('unknown action: ' + action)); - } + var func = function (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.edit) { actions.push('edit'); } + if (opts.set) { actions.push('set'); } + if (opts['delete']) { actions.push('delete'); } + if (opts.delete_all) { actions.push('deleteAll'); } + var action; + if (actions.length === 0) { + action = 'list'; + } 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 === 0) { + return cb(new errors.UsageError(cfg.argName + + ' argument is required')); + } else if (args.length > 1) { + return cb(new errors.UsageError('too many arguments')); + } + + switch (action) { + case 'list': + _listRoleTags({ + cli: this.top, + resourceType: cfg.resourceType, + resourceId: args[0], + json: opts.json + }, cb); + break; + case 'add': + _addRoleTags({ + cli: this.top, + resourceType: cfg.resourceType, + resourceId: args[0], + 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 = [ - { - 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.' - } -]; -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'); + +var do_role_tags = makeRoleTagsFunc({ + resourceType: 'resource', + funcName: 'do_role_tags', + cmdName: 'role-tags', + argName: 'RESOURCE-URL', + helpArgIs: 'an RBAC resource URL' +}); + +var do_instance_role_tags = makeRoleTagsFunc({ + resourceType: 'instance', + funcName: 'do_instance_role_tags', + cmdName: 'instance-role-tags', + argName: 'INST', + helpArgIs: 'an instance "id", "name" or short id' +}); + module.exports = { - //do_role_tags: do_role_tags, + do_role_tags: do_role_tags, do_instance_role_tags: do_instance_role_tags }; diff --git a/lib/do_rbac/index.js b/lib/do_rbac/index.js index e7eb8bd..1bf0099 100644 --- a/lib/do_rbac/index.js +++ b/lib/do_rbac/index.js @@ -42,7 +42,8 @@ function RbacCLI(top) { 'roles', 'role', { 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'); 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; module.exports = RbacCLI; diff --git a/lib/tritonapi.js b/lib/tritonapi.js index 6183df9..65ba6f2 100644 --- a/lib/tritonapi.js +++ b/lib/tritonapi.js @@ -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 /** @@ -66,7 +90,7 @@ function TritonApi(opts) { if (this.config.cacheDir) { this.cacheDir = path.resolve(this.config._configDir, this.config.cacheDir, - common.slug(this.profile)); + common.profileSlug(this.profile)); this.log.trace({cacheDir: this.cacheDir}, 'cache dir'); // TODO perhaps move this to an async .init() 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 {Function} callback `function (err, roleTags, inst)` + * @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. + * @param {Function} callback `function (err, roleTags, resource)` */ -TritonApi.prototype.getInstanceRoleTags = -function getInstanceRoleTags(name, cb) { +TritonApi.prototype.getRoleTags = function getRoleTags(opts, cb) { 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'); + function roleTagsFromRes(res) { + return ( + (res.headers['role-tag'] || '') + /* JSSTYLED */ + .split(/\s*,\s*/) + .filter(function (r) { return r.trim(); }) + ); + } + + var roleTags; - var inst; + var resource; vasync.pipeline({arg: {}, funcs: [ - function getInst(ctx, next) { - self.getInstance(name, function (err, inst_, res) { - 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) { + function resolveResourceId(ctx, next) { + if (opts.resourceType === 'resource') { next(); 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) { next(err); 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(); }); }, - function getRolesFromRes(ctx, next) { - roleTags = (ctx.res.headers['role-tag'] || '') - /* JSSTYLED */ - .split(/\s*,\s*/) - .filter(function (r) { return r.trim(); }); - next(); + function getResourceIfNecessary(ctx, next) { + if (roleTags) { + next(); + return; + } + + 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) { - 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); }); };