joyent/node-triton#54 'triton rbac apply --dev-create-keys-and-profiles'
This commit is contained in:
parent
6918fb93f7
commit
82443e2d67
@ -2,6 +2,9 @@
|
|||||||
|
|
||||||
## 3.0.1 (not yet released)
|
## 3.0.1 (not yet released)
|
||||||
|
|
||||||
|
- #54 `triton rbac apply --dev-create-keys-and-profiles` for
|
||||||
|
experimenting/dev/testing to quickly generate and add user keys and setup
|
||||||
|
Triton CLI profiles for all users in the RBAC config.
|
||||||
- #54 RBAC support, see <https://docs.joyent.com/public-cloud/rbac> to start.
|
- #54 RBAC support, see <https://docs.joyent.com/public-cloud/rbac> to start.
|
||||||
- `triton rbac info` improvements: better help, use brackets to show
|
- `triton rbac info` improvements: better help, use brackets to show
|
||||||
non-default roles.
|
non-default roles.
|
||||||
|
@ -1 +0,0 @@
|
|||||||
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDeDi6WjD2EOe+N8tkJXsDmjMii4qrJfEm4TX56Yezrjxr5QBSoPRcahvX+n352LYbkid+23lNrqGdBDlhracbFHoduWUetStRGoQWk52c7Piu13ETqWwoqxSlV9L9Ajb9h78AhBkTdtMlGp/0s6yREsjirUYyi/sGfHaj4WJU2Iqh3Ig1VsOcSUmm5npYC19UF+5hbEYTDEidkfRpjdDXA5EGTTce9ytsELOvFQaI/UuwXIS8Xd+1Eg/a+T3eFkx8VN8lAu067JfJNh8baaJcf5Bim3VcaTVDaYshwHJ0OKRxwvV/PqHLrEEbUA2zdN1oUY+Wyj/DQYdijCO8xGT41cZiJDINxfy0nJLEQxpfsnJw1QhRujezyekumdveQf0SoXZtUiBnWF0E/tMgMdQ8j6fFLUGgu08hIHZU0keIYUV1bAvJJhxJM58wemjmCch0BdZd0bBbujxeicFO+N1comeZjKhpLHQHwrQhRLu+oZgI41g4zNk+8lUuy6yI3pYdD+ThAsKrV34KpPCSIVA9KbLb4uIxZzpc41uh4AT6Xu1raXeqRn1ERW3XD3L2dmNaO444iEPgNdEzG7TG6Is172GOFZT75SkkdCV7UlsPFD0O7UXoDD9rdGjoi+LmIyX4MrTLXctOalNyXV9g+RiPETOfCz4dbH6cZjq/V6NdMBw== emma
|
|
@ -766,6 +766,58 @@ function generatePassword(opts) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience wrapper around `child_process.exec`, mostly oriented to
|
||||||
|
* run commands using pipes w/o having to deal with logging/error handling.
|
||||||
|
*
|
||||||
|
* @param args {Object}
|
||||||
|
* - cmd {String} Required. The command to run.
|
||||||
|
* - log {Bunyan Logger} Required. Use to log details at trace level.
|
||||||
|
* - opts {Object} Optional. child_process.exec execution Options.
|
||||||
|
* @param cb {Function} `function (err, stdout, stderr)` where `err` here is
|
||||||
|
* an `errors.InternalError` wrapper around the child_process error.
|
||||||
|
*/
|
||||||
|
function execPlus(args, cb) {
|
||||||
|
assert.object(args, 'args');
|
||||||
|
assert.string(args.cmd, 'args.cmd');
|
||||||
|
assert.object(args.log, 'args.log');
|
||||||
|
assert.optionalObject(args.opts, 'args.opts');
|
||||||
|
assert.func(cb);
|
||||||
|
|
||||||
|
var cmd = args.cmd;
|
||||||
|
var execOpts = args.opts || {};
|
||||||
|
var log = args.log;
|
||||||
|
|
||||||
|
log.trace({exec: true, cmd: cmd}, 'exec start');
|
||||||
|
child_process.exec(cmd, execOpts, function execPlusCb(err, stdout, stderr) {
|
||||||
|
log.trace({exec: true, cmd: cmd, err: err, stdout: stdout,
|
||||||
|
stderr: stderr}, 'exec done');
|
||||||
|
if (err) {
|
||||||
|
var msg = format(
|
||||||
|
'exec error:\n'
|
||||||
|
+ '\tcmd: %s\n'
|
||||||
|
+ '\texit status: %s\n'
|
||||||
|
+ '\tstdout:\n%s\n'
|
||||||
|
+ '\tstderr:\n%s',
|
||||||
|
cmd, err.code, stdout.trim(), stderr.trim());
|
||||||
|
cb(new errors.InternalError({message: msg, cause: err}),
|
||||||
|
stdout, stderr);
|
||||||
|
} else {
|
||||||
|
cb(null, stdout, stderr);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function deepEqual(a, b) {
|
||||||
|
try {
|
||||||
|
assert.deepEqual(a, b);
|
||||||
|
} catch (err) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
//---- exports
|
//---- exports
|
||||||
|
|
||||||
@ -793,6 +845,8 @@ module.exports = {
|
|||||||
ansiStylize: ansiStylize,
|
ansiStylize: ansiStylize,
|
||||||
indent: indent,
|
indent: indent,
|
||||||
chomp: chomp,
|
chomp: chomp,
|
||||||
generatePassword: generatePassword
|
generatePassword: generatePassword,
|
||||||
|
execPlus: execPlus,
|
||||||
|
deepEqual: deepEqual
|
||||||
};
|
};
|
||||||
// vim: set softtabstop=4 shiftwidth=4:
|
// vim: set softtabstop=4 shiftwidth=4:
|
||||||
|
@ -15,6 +15,7 @@ var format = require('util').format;
|
|||||||
var vasync = require('vasync');
|
var vasync = require('vasync');
|
||||||
|
|
||||||
var common = require('../common');
|
var common = require('../common');
|
||||||
|
var mod_config = require('../config');
|
||||||
var errors = require('../errors');
|
var errors = require('../errors');
|
||||||
var rbac = require('../rbac');
|
var rbac = require('../rbac');
|
||||||
|
|
||||||
@ -22,6 +23,7 @@ var ansiStylize = common.ansiStylize;
|
|||||||
|
|
||||||
|
|
||||||
function do_apply(subcmd, opts, args, cb) {
|
function do_apply(subcmd, opts, args, cb) {
|
||||||
|
var self = this;
|
||||||
if (opts.help) {
|
if (opts.help) {
|
||||||
this.do_help('help', {}, [subcmd], cb);
|
this.do_help('help', {}, [subcmd], cb);
|
||||||
return;
|
return;
|
||||||
@ -41,6 +43,93 @@ function do_apply(subcmd, opts, args, cb) {
|
|||||||
rbac.loadRbacConfig,
|
rbac.loadRbacConfig,
|
||||||
rbac.loadRbacState,
|
rbac.loadRbacState,
|
||||||
rbac.createRbacUpdatePlan,
|
rbac.createRbacUpdatePlan,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* For each user (in the target config):
|
||||||
|
* - if they don't have a key and one isn't being added in the plan
|
||||||
|
* already, then we want to create an ssh key pair and add it; and
|
||||||
|
* - add or replace the '$currprofile-user-$login' Triton CLI
|
||||||
|
* profile
|
||||||
|
*/
|
||||||
|
function devExtendKeys(ctx, next) {
|
||||||
|
if (!opts.dev_create_keys_and_profiles) {
|
||||||
|
next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.rbacConfig.users.forEach(function devUserKey(user) {
|
||||||
|
if (user.keys && user.keys.length > 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ctx.rbacUpdatePlan.push({
|
||||||
|
action: 'generate',
|
||||||
|
type: 'key',
|
||||||
|
desc: format('user %s key', user.login),
|
||||||
|
currProfile: ctx.tritonapi.profile,
|
||||||
|
user: user.login
|
||||||
|
});
|
||||||
|
});
|
||||||
|
next();
|
||||||
|
},
|
||||||
|
|
||||||
|
function devExtendProfiles(ctx, next) {
|
||||||
|
if (!opts.dev_create_keys_and_profiles) {
|
||||||
|
next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var profiles = mod_config.loadAllProfiles({
|
||||||
|
configDir: self.top.configDir,
|
||||||
|
log: self.log
|
||||||
|
});
|
||||||
|
var profileFromName = {};
|
||||||
|
profiles.forEach(function (p) {
|
||||||
|
profileFromName[p.name] = p;
|
||||||
|
});
|
||||||
|
|
||||||
|
ctx.rbacConfig.users.forEach(function devUserKey(user) {
|
||||||
|
var profileName = format('%s-user-%s',
|
||||||
|
ctx.tritonapi.profile.name, user.login);
|
||||||
|
var wantThing = {
|
||||||
|
name: profileName,
|
||||||
|
url: ctx.tritonapi.profile.url,
|
||||||
|
insecure: ctx.tritonapi.profile.insecure,
|
||||||
|
account: ctx.tritonapi.profile.account,
|
||||||
|
user: user.login,
|
||||||
|
// If we are adding a key, we won't have this fingerprint
|
||||||
|
// until after executing that part of the rbacUpdatePlan.
|
||||||
|
keyId: user.keys && user.keys[0].fingerprint
|
||||||
|
};
|
||||||
|
var existing = profileFromName[profileName];
|
||||||
|
if (existing) {
|
||||||
|
// If it is the same, avoid no-op update.
|
||||||
|
if (! common.deepEqual(wantThing, existing)) {
|
||||||
|
ctx.rbacUpdatePlan.push({
|
||||||
|
action: 'update',
|
||||||
|
type: 'profile',
|
||||||
|
desc: format('user %s CLI profile', user.login),
|
||||||
|
id: profileName,
|
||||||
|
haveThing: existing,
|
||||||
|
wantThing: wantThing,
|
||||||
|
user: user.login,
|
||||||
|
configDir: self.top.configDir
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ctx.rbacUpdatePlan.push({
|
||||||
|
action: 'create',
|
||||||
|
type: 'profile',
|
||||||
|
desc: format('user %s CLI profile', user.login),
|
||||||
|
id: profileName,
|
||||||
|
wantThing: wantThing,
|
||||||
|
user: user.login,
|
||||||
|
configDir: self.top.configDir
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
next();
|
||||||
|
},
|
||||||
|
|
||||||
function confirmApply(ctx, next) {
|
function confirmApply(ctx, next) {
|
||||||
if (opts.yes || ctx.rbacUpdatePlan.length === 0) {
|
if (opts.yes || ctx.rbacUpdatePlan.length === 0) {
|
||||||
next();
|
next();
|
||||||
@ -52,11 +141,8 @@ function do_apply(subcmd, opts, args, cb) {
|
|||||||
p('');
|
p('');
|
||||||
p('This will make the following RBAC config changes:');
|
p('This will make the following RBAC config changes:');
|
||||||
ctx.rbacUpdatePlan.forEach(function (c) {
|
ctx.rbacUpdatePlan.forEach(function (c) {
|
||||||
// TODO: consider having this summarize the changes, e.g.:
|
|
||||||
// Add 5 users (bob, linda, ...)
|
|
||||||
// Remove all 5 roles (...)
|
|
||||||
var extra = '';
|
var extra = '';
|
||||||
if (c.action === 'update') {
|
if (c.action === 'update' && c.diff) {
|
||||||
extra = format(' (%s)',
|
extra = format(' (%s)',
|
||||||
Object.keys(c.diff).map(function (field) {
|
Object.keys(c.diff).map(function (field) {
|
||||||
return c.diff[field] + ' ' + field;
|
return c.diff[field] + ' ' + field;
|
||||||
@ -64,9 +150,9 @@ function do_apply(subcmd, opts, args, cb) {
|
|||||||
}
|
}
|
||||||
p(' %s %s %s%s',
|
p(' %s %s %s%s',
|
||||||
{create: 'Create', 'delete': 'Delete',
|
{create: 'Create', 'delete': 'Delete',
|
||||||
update: 'Update'}[c.action],
|
update: 'Update', generate: 'Generate'}[c.action],
|
||||||
c.desc || c.type,
|
c.desc || c.type,
|
||||||
c.id,
|
c.id || '',
|
||||||
extra);
|
extra);
|
||||||
});
|
});
|
||||||
p('');
|
p('');
|
||||||
@ -108,6 +194,12 @@ do_apply.options = [
|
|||||||
type: 'string',
|
type: 'string',
|
||||||
helpArg: 'FILE',
|
helpArg: 'FILE',
|
||||||
help: 'RBAC config JSON file.'
|
help: 'RBAC config JSON file.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
names: ['dev-create-keys-and-profiles'],
|
||||||
|
type: 'bool',
|
||||||
|
help: 'Convenient option to generate keys and Triton CLI profiles ' +
|
||||||
|
'for all users. For experimenting only. See section below.'
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -128,6 +220,15 @@ do_apply.help = [
|
|||||||
'as they are replicated across data centers. This can result in unexpected',
|
'as they are replicated across data centers. This can result in unexpected',
|
||||||
'no-op updates with consecutive quick re-runs of this command.',
|
'no-op updates with consecutive quick re-runs of this command.',
|
||||||
'',
|
'',
|
||||||
|
'The "--dev-create-keys-and-profiles" option is provided for **experimenting',
|
||||||
|
'with, developing, or testing** Triton RBAC. It will create a key and setup a ',
|
||||||
|
'Triton CLI profile for each user (named "$currprofile-user-$login"). This ',
|
||||||
|
'simplies using the CLI as that user:',
|
||||||
|
' triton -p coal-user-bob create ...',
|
||||||
|
' triton -p coal-user-sarah imgs',
|
||||||
|
'Note that proper production usage of RBAC should have the administrator',
|
||||||
|
'never seeing each user\'s private key.',
|
||||||
|
'',
|
||||||
'TODO: Document the rbac.json configuration format.'
|
'TODO: Document the rbac.json configuration format.'
|
||||||
/* END JSSTYLED */
|
/* END JSSTYLED */
|
||||||
].join('\n');
|
].join('\n');
|
||||||
|
125
lib/rbac.js
125
lib/rbac.js
@ -13,11 +13,15 @@
|
|||||||
var assert = require('assert-plus');
|
var assert = require('assert-plus');
|
||||||
var format = require('util').format;
|
var format = require('util').format;
|
||||||
var fs = require('fs');
|
var fs = require('fs');
|
||||||
|
var mkdirp = require('mkdirp');
|
||||||
var path = require('path');
|
var path = require('path');
|
||||||
|
var rimraf = require('rimraf');
|
||||||
var sshpk = require('sshpk');
|
var sshpk = require('sshpk');
|
||||||
|
var tilde = require('tilde-expansion');
|
||||||
var vasync = require('vasync');
|
var vasync = require('vasync');
|
||||||
|
|
||||||
var common = require('./common');
|
var common = require('./common');
|
||||||
|
var mod_config = require('./config');
|
||||||
var errors = require('./errors');
|
var errors = require('./errors');
|
||||||
|
|
||||||
|
|
||||||
@ -272,6 +276,7 @@ function loadRbacConfig(ctx, cb) {
|
|||||||
var stat = fs.statSync(keysFile);
|
var stat = fs.statSync(keysFile);
|
||||||
} catch (statErr) {
|
} catch (statErr) {
|
||||||
if (implicit) {
|
if (implicit) {
|
||||||
|
delete user.keys;
|
||||||
nextUser();
|
nextUser();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -285,6 +290,7 @@ function loadRbacConfig(ctx, cb) {
|
|||||||
stat = fs.statSync(keysFile);
|
stat = fs.statSync(keysFile);
|
||||||
} catch (statErr) {
|
} catch (statErr) {
|
||||||
if (implicit) {
|
if (implicit) {
|
||||||
|
delete user.keys;
|
||||||
nextUser();
|
nextUser();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -295,6 +301,7 @@ function loadRbacConfig(ctx, cb) {
|
|||||||
}
|
}
|
||||||
if (!stat.isFile()) {
|
if (!stat.isFile()) {
|
||||||
if (implicit) {
|
if (implicit) {
|
||||||
|
delete user.keys;
|
||||||
nextUser();
|
nextUser();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -567,7 +574,7 @@ function executeRbacUpdatePlan(ctx, cb) {
|
|||||||
func: function executeOneChange(c, next) {
|
func: function executeOneChange(c, next) {
|
||||||
ctx.log.info({change: c, dryRun: ctx.rbacDryRun},
|
ctx.log.info({change: c, dryRun: ctx.rbacDryRun},
|
||||||
'execute rbac update change');
|
'execute rbac update change');
|
||||||
var extra, delOpts, updateOpts;
|
var extra, delOpts, updateOpts, i;
|
||||||
|
|
||||||
if (ctx.rbacDryRun) {
|
if (ctx.rbacDryRun) {
|
||||||
console.log('[dry-run] %s %s %s', c.action,
|
console.log('[dry-run] %s %s %s', c.action,
|
||||||
@ -582,7 +589,7 @@ function executeRbacUpdatePlan(ctx, cb) {
|
|||||||
if (! c.wantThing.hasOwnProperty('password')) {
|
if (! c.wantThing.hasOwnProperty('password')) {
|
||||||
c.wantThing.password = common.generatePassword();
|
c.wantThing.password = common.generatePassword();
|
||||||
}
|
}
|
||||||
ctx.cloudapi.createUser(c.wantThing, function (err, user) {
|
ctx.cloudapi.createUser(c.wantThing, function (err, user_) {
|
||||||
if (err) {
|
if (err) {
|
||||||
next(err);
|
next(err);
|
||||||
return;
|
return;
|
||||||
@ -601,7 +608,7 @@ function executeRbacUpdatePlan(ctx, cb) {
|
|||||||
updateOpts[field] = c.wantThing[field];
|
updateOpts[field] = c.wantThing[field];
|
||||||
extra.push(format('%s=%s', field, c.wantThing[field]));
|
extra.push(format('%s=%s', field, c.wantThing[field]));
|
||||||
});
|
});
|
||||||
ctx.cloudapi.updateUser(updateOpts, function (err, user) {
|
ctx.cloudapi.updateUser(updateOpts, function (err, user_) {
|
||||||
if (err) {
|
if (err) {
|
||||||
next(err);
|
next(err);
|
||||||
return;
|
return;
|
||||||
@ -764,6 +771,118 @@ function executeRbacUpdatePlan(ctx, cb) {
|
|||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'generate-key':
|
||||||
|
console.log('Generating and adding new SSH key for user %s:',
|
||||||
|
c.user);
|
||||||
|
vasync.pipeline({arg: {}, funcs: [
|
||||||
|
function vars(ctx2, next2) {
|
||||||
|
tilde('~', function (s) {
|
||||||
|
ctx2.homeDir = s;
|
||||||
|
ctx2.keyName = format('%s user %s',
|
||||||
|
c.currProfile.name, c.user);
|
||||||
|
ctx2.keyPath = format('%s/.ssh/%s-user-%s.id_rsa',
|
||||||
|
ctx2.homeDir, c.currProfile.name, c.user);
|
||||||
|
next2();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
function rmOldPrivKey(ctx2, next2) {
|
||||||
|
rimraf(ctx2.keyPath, next2);
|
||||||
|
},
|
||||||
|
function rmOldPubKey(ctx2, next2) {
|
||||||
|
rimraf(ctx2.keyPath + '.pub', next2);
|
||||||
|
},
|
||||||
|
function generateSshKey(ctx2, next2) {
|
||||||
|
var cmd = format(
|
||||||
|
'ssh-keygen -t rsa -C "%s" -f %s -b 4096',
|
||||||
|
ctx2.keyName, ctx2.keyPath);
|
||||||
|
console.log(' Generate 4096 bit RSA key: %s[.pub]',
|
||||||
|
ctx2.keyPath);
|
||||||
|
common.execPlus({cmd: cmd, log: ctx.log}, next2);
|
||||||
|
},
|
||||||
|
function loadPubKey(ctx2, next2) {
|
||||||
|
fs.readFile(ctx2.keyPath + '.pub', 'utf8',
|
||||||
|
function (err, content) {
|
||||||
|
ctx2.pubKeyContent = content;
|
||||||
|
next2();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
function pubKeyCopyInRbacConfigDir(ctx2, next2) {
|
||||||
|
mkdirp(DEFAULT_RBAC_USER_KEYS_DIR, function (err) {
|
||||||
|
if (err) {
|
||||||
|
next(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var configKeyPath = path.join(
|
||||||
|
DEFAULT_RBAC_USER_KEYS_DIR, c.user + '.pub');
|
||||||
|
console.log(' Copy pubkey to %s', configKeyPath);
|
||||||
|
fs.writeFile(configKeyPath, ctx2.pubKeyContent,
|
||||||
|
next2);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
function addKey(ctx2, next2) {
|
||||||
|
ctx.cloudapi.createUserKey({
|
||||||
|
userId: c.user,
|
||||||
|
key: ctx2.pubKeyContent,
|
||||||
|
name: ctx2.keyName
|
||||||
|
}, function (err, key) {
|
||||||
|
if (err) {
|
||||||
|
next2(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ctx2.key = key;
|
||||||
|
console.log(' Created user %s key %s%s', c.user,
|
||||||
|
key.fingerprint,
|
||||||
|
key.name ? format(' (%s)', key.name) : '');
|
||||||
|
next2();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
function addKeyToRbacConfig(ctx2, next2) {
|
||||||
|
for (i = 0; i < ctx.rbacConfig.users.length; i++) {
|
||||||
|
var u = ctx.rbacConfig.users[i];
|
||||||
|
if (u.login === c.user) {
|
||||||
|
assert.ok(!u.keys,
|
||||||
|
'expect no keys on user ' + c.user);
|
||||||
|
u.keys = [ctx2.key];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
next2();
|
||||||
|
}
|
||||||
|
]}, next);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'create-profile':
|
||||||
|
case 'update-profile':
|
||||||
|
if (!c.wantThing.keyId) {
|
||||||
|
// Add from the recently generated/added key.
|
||||||
|
for (i = 0; i < ctx.rbacConfig.users.length; i++) {
|
||||||
|
var user = ctx.rbacConfig.users[i];
|
||||||
|
if (user.login === c.user) {
|
||||||
|
c.wantThing.keyId = user.keys[0].fingerprint;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
mod_config.validateProfile(c.wantThing);
|
||||||
|
} catch (err) {
|
||||||
|
next(err);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
mod_config.saveProfileSync({
|
||||||
|
configDir: c.configDir,
|
||||||
|
profile: c.wantThing
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
return next(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
next();
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw new Error(format(
|
throw new Error(format(
|
||||||
'unknown action-type: %s-%s', c.action, c.type));
|
'unknown action-type: %s-%s', c.action, c.type));
|
||||||
|
@ -18,6 +18,7 @@
|
|||||||
"read": "1.0.7",
|
"read": "1.0.7",
|
||||||
"restify-clients": "1.1.0",
|
"restify-clients": "1.1.0",
|
||||||
"restify-errors": "3.0.0",
|
"restify-errors": "3.0.0",
|
||||||
|
"rimraf": "2.4.4",
|
||||||
"sshpk": "1.6.x >=1.6.2",
|
"sshpk": "1.6.x >=1.6.2",
|
||||||
"smartdc-auth": "2.2.3",
|
"smartdc-auth": "2.2.3",
|
||||||
"strsplit": "1.0.0",
|
"strsplit": "1.0.0",
|
||||||
|
Reference in New Issue
Block a user