joyent/node-triton#75 'triton account update ...'
This commit is contained in:
parent
6fe9de3adf
commit
5ee94b04af
@ -1,8 +1,8 @@
|
|||||||
# node-triton changelog
|
# node-triton changelog
|
||||||
|
|
||||||
## 4.1.1 (not yet released)
|
## 4.2.0 (not yet released)
|
||||||
|
|
||||||
(nothing yet)
|
- #75 `triton account update ...`
|
||||||
|
|
||||||
|
|
||||||
## 4.1.0
|
## 4.1.0
|
||||||
|
@ -273,6 +273,7 @@ CloudApi.prototype._passThrough = function _passThrough(endpoint, opts, cb) {
|
|||||||
* at SecurePair.emit (events.js:92:17)
|
* at SecurePair.emit (events.js:92:17)
|
||||||
*
|
*
|
||||||
* TODO: could generalize this into a wrapErr method.
|
* TODO: could generalize this into a wrapErr method.
|
||||||
|
* TODO: this should be on _request, no? So that PUT, POST, etc. get it.
|
||||||
*/
|
*/
|
||||||
if (err && err.message === 'DEPTH_ZERO_SELF_SIGNED_CERT' &&
|
if (err && err.message === 'DEPTH_ZERO_SELF_SIGNED_CERT' &&
|
||||||
self.client.rejectUnauthorized)
|
self.client.rejectUnauthorized)
|
||||||
@ -352,6 +353,60 @@ CloudApi.prototype.getAccount = function getAccount(opts, cb) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// <updatable account field> -> <expected typeof>
|
||||||
|
CloudApi.prototype.UPDATE_ACCOUNT_FIELDS = {
|
||||||
|
email: 'string',
|
||||||
|
companyName: 'string',
|
||||||
|
firstName: 'string',
|
||||||
|
lastName: 'string',
|
||||||
|
address: 'string',
|
||||||
|
postalCode: 'string',
|
||||||
|
city: 'string',
|
||||||
|
state: 'string',
|
||||||
|
country: 'string',
|
||||||
|
phone: 'string',
|
||||||
|
triton_cns_enabled: 'boolean'
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update account fields.
|
||||||
|
* <https://apidocs.joyent.com/cloudapi/#UpdateAccount>
|
||||||
|
*
|
||||||
|
* @param opts {Object} A key for each account field to update.
|
||||||
|
* @param cb {Function} `function (err, updatedAccount, res)`
|
||||||
|
*/
|
||||||
|
CloudApi.prototype.updateAccount = function updateAccount(opts, cb) {
|
||||||
|
assert.object(opts, 'opts');
|
||||||
|
assert.func(cb, 'cb');
|
||||||
|
|
||||||
|
var self = this;
|
||||||
|
var update = {};
|
||||||
|
var unexpectedFields = [];
|
||||||
|
Object.keys(opts).forEach(function (field) {
|
||||||
|
var type = self.UPDATE_ACCOUNT_FIELDS[field];
|
||||||
|
if (type) {
|
||||||
|
assert[type === 'boolean' ? 'bool' : type](opts[field],
|
||||||
|
'opts.'+field);
|
||||||
|
update[field] = opts[field];
|
||||||
|
} else {
|
||||||
|
unexpectedFields.push(field);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (unexpectedFields.length > 0) {
|
||||||
|
throw new Error(format('unknown field(s) for UpdateAccount: %s',
|
||||||
|
unexpectedFields.sort().join(', ')));
|
||||||
|
}
|
||||||
|
|
||||||
|
this._request({
|
||||||
|
method: 'POST',
|
||||||
|
path: format('/%s', this.account),
|
||||||
|
data: update
|
||||||
|
}, function (err, req, res, body) {
|
||||||
|
cb(err, body, res);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List account's SSH keys.
|
* List account's SSH keys.
|
||||||
*
|
*
|
||||||
@ -1400,6 +1455,10 @@ CloudApi.prototype.setRoleTags = function setRoleTags(opts, cb) {
|
|||||||
|
|
||||||
// --- Exports
|
// --- Exports
|
||||||
|
|
||||||
module.exports.createClient = function (options) {
|
module.exports = {
|
||||||
return new CloudApi(options);
|
createClient: function createClient(options) {
|
||||||
};
|
return new CloudApi(options);
|
||||||
|
},
|
||||||
|
|
||||||
|
CloudApi: CloudApi
|
||||||
|
};
|
@ -15,6 +15,7 @@ var fs = require('fs');
|
|||||||
var os = require('os');
|
var os = require('os');
|
||||||
var path = require('path');
|
var path = require('path');
|
||||||
var read = require('read');
|
var read = require('read');
|
||||||
|
var strsplit = require('strsplit');
|
||||||
var tty = require('tty');
|
var tty = require('tty');
|
||||||
var util = require('util'),
|
var util = require('util'),
|
||||||
format = util.format;
|
format = util.format;
|
||||||
@ -873,6 +874,87 @@ function tildeSync(s) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transform an array of 'key=value' CLI arguments to an object.
|
||||||
|
*
|
||||||
|
* - The use of '.' in the key allows sub-object assignment (only one level
|
||||||
|
* deep). This can be disabled with `opts.disableDotted`.
|
||||||
|
* - An attempt will be made the `JSON.parse` a given value, such that
|
||||||
|
* booleans, numbers, objects, arrays can be specified; at the expense
|
||||||
|
* of not being able to specify, e.g., a literal 'true' string.
|
||||||
|
* If `opts.typeHintFromKey` states that a key is a string, this JSON.parse
|
||||||
|
* is NOT done.
|
||||||
|
* - An empty 'value' is transformed to `null`. Note that 'null' also
|
||||||
|
* JSON.parse's as `null`.
|
||||||
|
*
|
||||||
|
* Example:
|
||||||
|
* > objFromKeyValueArgs(['nm=foo', 'tag.blah=true', 'empty=', 'nada=null']);
|
||||||
|
* { nm: 'foo',
|
||||||
|
* tag: { blah: true },
|
||||||
|
* empty: null,
|
||||||
|
* nada: null }
|
||||||
|
*
|
||||||
|
* @param args {Array} Array of string args to process.
|
||||||
|
* @param opts {Object} Optional.
|
||||||
|
* - @param disableDotted {Boolean} Optional. Set to true to disable
|
||||||
|
* dotted keys.
|
||||||
|
* - @param typeHintFromKey {Object} Optional. Type hints for input keys.
|
||||||
|
* E.g. if parsing 'foo=false' and `typeHintFromKey={foo: 'string'}`,
|
||||||
|
* then we do NOT parse it to a boolean `false`.
|
||||||
|
*/
|
||||||
|
function objFromKeyValueArgs(args, opts)
|
||||||
|
{
|
||||||
|
assert.arrayOfString(args, 'args');
|
||||||
|
assert.optionalObject(opts, 'opts');
|
||||||
|
opts = opts || {};
|
||||||
|
assert.optionalBool(opts.disableDotted, 'opts.disableDotted');
|
||||||
|
assert.optionalObject(opts.typeHintFromKey, opts.typeHintFromKey);
|
||||||
|
var typeHintFromKey = opts.typeHintFromKey || {};
|
||||||
|
|
||||||
|
var obj = {};
|
||||||
|
args.forEach(function (arg) {
|
||||||
|
var kv = strsplit(arg, '=', 2);
|
||||||
|
if (kv.length < 2) {
|
||||||
|
throw new TypeError(format('invalid key=value argument: "%s"'));
|
||||||
|
}
|
||||||
|
|
||||||
|
var k = kv[0];
|
||||||
|
var t = typeHintFromKey[k];
|
||||||
|
|
||||||
|
var v = kv[1];
|
||||||
|
if (t === 'string') {
|
||||||
|
// Leave `v` a string.
|
||||||
|
/* jsl:pass */
|
||||||
|
} else if (v === '') {
|
||||||
|
v = null;
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
v = JSON.parse(v);
|
||||||
|
} catch (e) {
|
||||||
|
/* pass */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.disableDotted) {
|
||||||
|
obj[k] = v;
|
||||||
|
} else {
|
||||||
|
var dotted = strsplit(k, '.', 2);
|
||||||
|
if (dotted.length > 1) {
|
||||||
|
if (!obj[dotted[0]]) {
|
||||||
|
obj[dotted[0]] = {};
|
||||||
|
}
|
||||||
|
obj[dotted[0]][dotted[1]] = v;
|
||||||
|
} else {
|
||||||
|
obj[k] = v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
//---- exports
|
//---- exports
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
@ -903,6 +985,7 @@ module.exports = {
|
|||||||
generatePassword: generatePassword,
|
generatePassword: generatePassword,
|
||||||
execPlus: execPlus,
|
execPlus: execPlus,
|
||||||
deepEqual: deepEqual,
|
deepEqual: deepEqual,
|
||||||
tildeSync: tildeSync
|
tildeSync: tildeSync,
|
||||||
|
objFromKeyValueArgs: objFromKeyValueArgs
|
||||||
};
|
};
|
||||||
// vim: set softtabstop=4 shiftwidth=4:
|
// vim: set softtabstop=4 shiftwidth=4:
|
||||||
|
172
lib/do_account/do_update.js
Normal file
172
lib/do_account/do_update.js
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
/*
|
||||||
|
* 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 2016 Joyent, Inc.
|
||||||
|
*
|
||||||
|
* `triton account update ...`
|
||||||
|
*/
|
||||||
|
|
||||||
|
var assert = require('assert-plus');
|
||||||
|
var format = require('util').format;
|
||||||
|
var fs = require('fs');
|
||||||
|
var vasync = require('vasync');
|
||||||
|
|
||||||
|
var common = require('../common');
|
||||||
|
var errors = require('../errors');
|
||||||
|
var UPDATE_ACCOUNT_FIELDS
|
||||||
|
= require('../cloudapi2').CloudApi.prototype.UPDATE_ACCOUNT_FIELDS;
|
||||||
|
|
||||||
|
|
||||||
|
function do_update(subcmd, opts, args, callback) {
|
||||||
|
if (opts.help) {
|
||||||
|
this.do_help('help', {}, [subcmd], callback);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var log = this.log;
|
||||||
|
var tritonapi = this.top.tritonapi;
|
||||||
|
|
||||||
|
vasync.pipeline({arg: {}, funcs: [
|
||||||
|
function gatherDataArgs(ctx, next) {
|
||||||
|
if (opts.file) {
|
||||||
|
next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
ctx.data = common.objFromKeyValueArgs(args, {
|
||||||
|
disableDotted: true,
|
||||||
|
typeHintFromKey: UPDATE_ACCOUNT_FIELDS
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
next(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
},
|
||||||
|
|
||||||
|
function gatherDataFile(ctx, next) {
|
||||||
|
if (!opts.file || opts.file === '-') {
|
||||||
|
next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var input = fs.readFileSync(opts.file, 'utf8');
|
||||||
|
try {
|
||||||
|
ctx.data = JSON.parse(input);
|
||||||
|
} catch (err) {
|
||||||
|
next(new errors.TritonError(format(
|
||||||
|
'invalid JSON for account update in "%s": %s',
|
||||||
|
opts.file, err)));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
},
|
||||||
|
|
||||||
|
function gatherDataStdin(ctx, next) {
|
||||||
|
if (opts.file !== '-') {
|
||||||
|
next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var stdin = '';
|
||||||
|
process.stdin.resume();
|
||||||
|
process.stdin.on('data', function (chunk) {
|
||||||
|
stdin += chunk;
|
||||||
|
});
|
||||||
|
process.stdin.on('end', function () {
|
||||||
|
try {
|
||||||
|
ctx.data = JSON.parse(stdin);
|
||||||
|
} catch (err) {
|
||||||
|
log.trace({stdin: stdin},
|
||||||
|
'invalid account update JSON on stdin');
|
||||||
|
next(new errors.TritonError(format(
|
||||||
|
'invalid JSON for account update on stdin: %s', err)));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
function validateIt(ctx, next) {
|
||||||
|
var keys = Object.keys(ctx.data);
|
||||||
|
for (var i = 0; i < keys.length; i++) {
|
||||||
|
var key = keys[i];
|
||||||
|
var value = ctx.data[key];
|
||||||
|
var type = UPDATE_ACCOUNT_FIELDS[key];
|
||||||
|
if (!type) {
|
||||||
|
next(new errors.UsageError(format('unknown or ' +
|
||||||
|
'unupdateable field: %s (updateable fields are: %s)',
|
||||||
|
key,
|
||||||
|
Object.keys(UPDATE_ACCOUNT_FIELDS).sort().join(', '))));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof (value) !== type) {
|
||||||
|
next(new errors.UsageError(format('field "%s" must be ' +
|
||||||
|
'of type "%s", but got a value of type "%s"', key,
|
||||||
|
type, typeof (value))));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
},
|
||||||
|
|
||||||
|
function updateAway(ctx, next) {
|
||||||
|
var keys = Object.keys(ctx.data);
|
||||||
|
if (keys.length === 0) {
|
||||||
|
console.log('No fields given for account update');
|
||||||
|
next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
tritonapi.cloudapi.updateAccount(ctx.data, function (err) {
|
||||||
|
if (err) {
|
||||||
|
next(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log('Updated account "%s" (fields: %s)',
|
||||||
|
tritonapi.profile.account, keys.join(', '));
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
]}, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
do_update.options = [
|
||||||
|
{
|
||||||
|
names: ['help', 'h'],
|
||||||
|
type: 'bool',
|
||||||
|
help: 'Show this help.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
names: ['file', 'f'],
|
||||||
|
type: 'string',
|
||||||
|
helpArg: 'FILE',
|
||||||
|
help: 'A file holding a JSON file of updates, or "-" to read ' +
|
||||||
|
'JSON from stdin.'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
do_update.help = [
|
||||||
|
/* BEGIN JSSTYLED */
|
||||||
|
'Update account information',
|
||||||
|
'',
|
||||||
|
'Usage:',
|
||||||
|
' {{name}} update [FIELD=VALUE ...]',
|
||||||
|
' {{name}} update -f JSON-FILE',
|
||||||
|
'',
|
||||||
|
'{{options}}',
|
||||||
|
|
||||||
|
'Updateable fields:',
|
||||||
|
' ' + Object.keys(UPDATE_ACCOUNT_FIELDS).sort().map(function (field) {
|
||||||
|
return field + ' (' + UPDATE_ACCOUNT_FIELDS[field] + ')';
|
||||||
|
}).join('\n '),
|
||||||
|
|
||||||
|
'',
|
||||||
|
'Note that because of cross-data center replication of account information, ',
|
||||||
|
'an update might not be immediately reflected in a get.'
|
||||||
|
/* END JSSTYLED */
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
module.exports = do_update;
|
@ -31,7 +31,8 @@ function AccountCLI(top) {
|
|||||||
},
|
},
|
||||||
helpSubcmds: [
|
helpSubcmds: [
|
||||||
'help',
|
'help',
|
||||||
'get'
|
'get',
|
||||||
|
'update'
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -43,6 +44,7 @@ AccountCLI.prototype.init = function init(opts, args, cb) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
AccountCLI.prototype.do_get = require('./do_get');
|
AccountCLI.prototype.do_get = require('./do_get');
|
||||||
|
AccountCLI.prototype.do_update = require('./do_update');
|
||||||
|
|
||||||
|
|
||||||
module.exports = AccountCLI;
|
module.exports = AccountCLI;
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "triton",
|
"name": "triton",
|
||||||
"description": "Joyent Triton CLI and client (https://www.joyent.com/triton)",
|
"description": "Joyent Triton CLI and client (https://www.joyent.com/triton)",
|
||||||
"version": "4.1.1",
|
"version": "4.2.0",
|
||||||
"author": "Joyent (joyent.com)",
|
"author": "Joyent (joyent.com)",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"assert-plus": "0.2.0",
|
"assert-plus": "0.2.0",
|
||||||
|
@ -19,6 +19,9 @@ var test = require('tape');
|
|||||||
|
|
||||||
// --- Globals
|
// --- Globals
|
||||||
|
|
||||||
|
var writeTestOpts = {
|
||||||
|
skip: !h.CONFIG.allowWriteActions
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
// --- Tests
|
// --- Tests
|
||||||
@ -53,13 +56,55 @@ test('triton account', function (tt) {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
var account;
|
||||||
tt.test(' triton account get -j', function (t) {
|
tt.test(' triton account get -j', function (t) {
|
||||||
h.triton('account get -j', function (err, stdout, stderr) {
|
h.triton('account get -j', function (err, stdout, stderr) {
|
||||||
if (h.ifErr(t, err))
|
if (h.ifErr(t, err))
|
||||||
return t.end();
|
return t.end();
|
||||||
var account = JSON.parse(stdout);
|
account = JSON.parse(stdout);
|
||||||
t.equal(account.login, h.CONFIG.profile.account, 'account.login');
|
t.equal(account.login, h.CONFIG.profile.account, 'account.login');
|
||||||
t.end();
|
t.end();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
tt.test(' triton account update foo=bar', writeTestOpts, function (t) {
|
||||||
|
h.triton('account update foo=bar', function (err, stdout, stderr) {
|
||||||
|
t.ok(err);
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
tt.test(' triton account update companyName=foo', writeTestOpts,
|
||||||
|
function (t) {
|
||||||
|
h.triton('account update companyName=foo', function (err, _o, _e) {
|
||||||
|
if (h.ifErr(t, err))
|
||||||
|
return t.end();
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Limitation: because x-dc replication, the update might not be
|
||||||
|
* reflected in a get right away.
|
||||||
|
* TODO: poll 'account get' until a timeout, or implement that
|
||||||
|
* with 'triton account update -w' and use that.
|
||||||
|
*/
|
||||||
|
//h.triton('account get -j', function (err2, stdout, stderr) {
|
||||||
|
// if (h.ifErr(t, err2))
|
||||||
|
// return t.end();
|
||||||
|
// var updatedAccount = JSON.parse(stdout);
|
||||||
|
// t.equal(updatedAccount.companyName, 'foo',
|
||||||
|
// '<updated account>.companyName');
|
||||||
|
// t.end();
|
||||||
|
//});
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
tt.test(' triton account update companyName=<oldvalue>', writeTestOpts,
|
||||||
|
function (t) {
|
||||||
|
h.triton('account update companyName=' + account.companyName || '',
|
||||||
|
function (err, _o, _e) {
|
||||||
|
if (h.ifErr(t, err))
|
||||||
|
return t.end();
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -30,6 +30,7 @@ var subs = [
|
|||||||
['profile delete', 'profile rm'],
|
['profile delete', 'profile rm'],
|
||||||
['account'],
|
['account'],
|
||||||
['account get'],
|
['account get'],
|
||||||
|
['account update'],
|
||||||
['services'],
|
['services'],
|
||||||
['datacenters'],
|
['datacenters'],
|
||||||
['instance', 'inst'],
|
['instance', 'inst'],
|
||||||
|
Reference in New Issue
Block a user