improvements for using node-triton as a module

This commit is contained in:
Trent Mick 2015-12-08 11:59:45 -08:00
parent a40e0d6f8c
commit 21164320c7
12 changed files with 420 additions and 47 deletions

View File

@ -1,8 +1,17 @@
# node-triton changelog
## 3.3.1 (not yet released)
## 3.4.0 (not yet released)
(nothing yet)
- Improvements for using node-triton as a module. E.g. a simple example:
var triton = require('triton');
var client = triton.createClient({profileName: 'env'});
client.listImages(function (err, imgs) {
console.log(err);
console.log(imgs);
});
See the README and "lib/index.js" for more info.
## 3.3.0

View File

@ -74,7 +74,7 @@ For a more permanent installation:
triton completion > /usr/local/etc/bash_completion.d/triton
## Examples
## `triton` CLI Usage
### Create and view instances
@ -214,6 +214,33 @@ as a single `DOCKER_HOST`. See the [Triton Docker
documentation](https://apidocs.joyent.com/docker) for more information.)
## `TritonApi` Module Usage
Node-triton can also be used as a node module for your own node.js tooling.
A basic example:
var triton = require('triton');
// See `createClient` block comment for full usage details:
// https://github.com/joyent/node-triton/blob/master/lib/index.js
var client = triton.createClient({
profile: {
url: URL,
account: ACCOUNT,
keyId: KEY_ID
}
});
client.listImages(function (err, images) {
client.close(); // Remember to close the client to close TCP conn.
if (err) {
console.error('listImages err:', err);
} else {
console.log(JSON.stringify(images, null, 4));
}
});
## Configuration
This section defines all the vars in a TritonApi config. The baked in defaults
@ -229,7 +256,8 @@ are in "etc/defaults.json" and can be overriden for the CLI in
## node-triton differences with node-smartdc
- There is a single `triton` command instead of a number of `sdc-*` commands.
- The `SDC_USER` env variable is accepted in preference to `SDC_ACCOUNT`.
- `TRITON_*` environment variables are preferred to the `SDC_*` environment
variables. However the `SDC_*` envvars are still supported.
## cloudapi2.js differences with node-smartdc/lib/cloudapi.js

27
lib/bunyannoop.js Normal file
View File

@ -0,0 +1,27 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
/*
* Copyright 2015 Joyent, Inc.
*/
/*
* A stub for a `bunyan.createLogger()` that does no logging.
*/
function BunyanNoopLogger() {}
BunyanNoopLogger.prototype.trace = function () {};
BunyanNoopLogger.prototype.debug = function () {};
BunyanNoopLogger.prototype.info = function () {};
BunyanNoopLogger.prototype.warn = function () {};
BunyanNoopLogger.prototype.error = function () {};
BunyanNoopLogger.prototype.fatal = function () {};
BunyanNoopLogger.prototype.child = function () { return this; };
BunyanNoopLogger.prototype.end = function () {};
module.exports = {
BunyanNoopLogger: BunyanNoopLogger
};

View File

@ -212,12 +212,6 @@ CLI.prototype.init = function (opts, args, callback) {
var self = this;
this.opts = opts;
if (opts.version) {
console.log(this.name, pkg.version);
callback(false);
return;
}
this.log = bunyan.createLogger({
name: this.name,
serializers: bunyan.stdSerializers,
@ -230,6 +224,12 @@ CLI.prototype.init = function (opts, args, callback) {
this.showErrStack = true;
}
if (opts.version) {
console.log(this.name, pkg.version);
callback(false);
return;
}
if (opts.url && opts.J) {
callback(new errors.UsageError(
'cannot use both "--url" and "-J" options'));
@ -268,6 +268,16 @@ CLI.prototype.init = function (opts, args, callback) {
};
CLI.prototype.fini = function fini(subcmd, err, cb) {
this.log.trace({err: err, subcmd: subcmd}, 'cli fini');
if (this._tritonapi) {
this._tritonapi.close();
delete this._tritonapi;
}
cb(err, subcmd);
};
/*
* Apply overrides from CLI options to the given profile object *in place*.
*/
@ -380,7 +390,14 @@ function main(argv) {
}
}
process.exit(exitStatus);
/*
* We'd like to NOT use `process.exit` because that doesn't always
* allow std handles to flush (e.g. all logging to complete). However
* I don't know of another way to exit non-zero.
*/
if (exitStatus !== 0) {
process.exit(exitStatus);
}
});
}

View File

@ -42,6 +42,7 @@ var querystring = require('querystring');
var vasync = require('vasync');
var auth = require('smartdc-auth');
var bunyannoop = require('./bunyannoop');
var errors = require('./errors');
var SaferJsonClient = require('./SaferJsonClient');
@ -55,21 +56,6 @@ var OS_PLATFORM = os.platform();
// ---- internal support stuff
// A no-op bunyan logger shim.
function BunyanNoopLogger() {}
BunyanNoopLogger.prototype.trace = function () {};
BunyanNoopLogger.prototype.debug = function () {};
BunyanNoopLogger.prototype.info = function () {};
BunyanNoopLogger.prototype.warn = function () {};
BunyanNoopLogger.prototype.error = function () {};
BunyanNoopLogger.prototype.fatal = function () {};
BunyanNoopLogger.prototype.child = function () { return this; };
BunyanNoopLogger.prototype.end = function () {};
// ---- client API
/**
@ -111,7 +97,7 @@ function CloudApi(options) {
this.account = options.account;
this.user = options.user; // optional RBAC subuser
this.sign = options.sign;
this.log = options.log || new BunyanNoopLogger();
this.log = options.log || new bunyannoop.BunyanNoopLogger();
if (!options.version) {
options.version = '*';
}
@ -132,6 +118,13 @@ function CloudApi(options) {
}
CloudApi.prototype.close = function close(callback) {
this.log.trace({host: this.client.url && this.client.url.host},
'close cloudapi http client');
this.client.close();
};
CloudApi.prototype._getAuthHeaders = function _getAuthHeaders(callback) {
assert.func(callback, 'callback');
var self = this;

View File

@ -330,19 +330,25 @@ function normShortId(s) {
}
/*
* take a "profile" object and return a slug based on the account name
* and DC URL. This is currently used to create a filesystem-safe name
* to use for caching
* Take a "profile" object and return a slug based on: account, url and user.
* This is currently used to create a filesystem-safe name to use for caching
*/
function profileSlug(o) {
assert.object(o, 'o');
assert.string(o.account, 'o.account');
assert.string(o.url, 'o.url');
var acct = o.account.replace(/[@]/g, '_');
var slug;
var account = o.account.replace(/[@]/g, '_');
var url = o.url.replace(/^https?:\/\//, '');
var s = format('%s@%s', acct, url).replace(/[!#$%\^&\*:'"\?\/\\\.]/g, '_');
return s;
if (o.user) {
var user = o.user.replace(/[@]/g, '_');
slug = format('%s-%s@%s', user, account, url);
} else {
slug = format('%s@%s', account, url);
}
slug = slug.replace(/[!#$%\^&\*:'"\?\/\\\.]/g, '_');
return slug;
}
@ -819,6 +825,28 @@ function deepEqual(a, b) {
}
/**
* Resolve "~/..." and "~" to an absolute path.
*
* Limitations: This does not handle "~user/...". This depends on the HOME
* envvar being defined. A better alternative is the "tilde-expansion"
* module (used elsewhere in node-triton), but that doesn't have a sync
* option.
*/
function tildeSync(s) {
var home = process.env.HOME;
if (!home) {
return s;
} else if (s === '~') {
return home;
} else if (s.slice(0, 2) === '~/') {
return home + s.slice(1);
} else {
return s;
}
}
//---- exports
module.exports = {
@ -847,6 +875,7 @@ module.exports = {
chomp: chomp,
generatePassword: generatePassword,
execPlus: execPlus,
deepEqual: deepEqual
deepEqual: deepEqual,
tildeSync: tildeSync
};
// vim: set softtabstop=4 shiftwidth=4:

View File

@ -88,7 +88,8 @@ function loadConfig(opts) {
assert.object(opts, 'opts');
assert.string(opts.configDir, 'opts.configDir');
var configPath = configPathFromDir(opts.configDir);
var configDir = common.tildeSync(opts.configDir);
var configPath = configPathFromDir(configDir);
var c = fs.readFileSync(DEFAULTS_PATH, 'utf8');
var _defaults = JSON.parse(c);
@ -120,7 +121,7 @@ function loadConfig(opts) {
config._user = _user;
}
config._defaults = _defaults;
config._configDir = opts.configDir;
config._configDir = configDir;
return config;
}
@ -272,15 +273,18 @@ function _profileFromPath(profilePath, name) {
}
function loadProfile(opts) {
assert.string(opts.configDir, 'opts.configDir');
assert.string(opts.name, 'opts.name');
assert.optionalString(opts.configDir, 'opts.configDir');
if (opts.name === 'env') {
return _loadEnvProfile();
} else if (!opts.configDir) {
throw new errors.TritonError(
'cannot load profiles (other than "env") without `opts.configDir`');
} else {
var profilePath = path.resolve(opts.configDir, 'profiles.d',
var profilePath = path.resolve(
common.tildeSync(opts.configDir), 'profiles.d',
opts.name + '.json');
return _profileFromPath(profilePath, opts.name);
}
@ -294,7 +298,7 @@ function loadAllProfiles(opts) {
_loadEnvProfile()
];
var d = path.join(opts.configDir, 'profiles.d');
var d = path.join(common.tildeSync(opts.configDir), 'profiles.d');
var files = fs.readdirSync(d);
files.forEach(function (file) {
file = path.join(d, file);

View File

@ -6,11 +6,160 @@
/*
* Copyright 2015 Joyent, Inc.
*
* module entry point
*/
var assert = require('assert-plus');
var bunyannoop = require('./bunyannoop');
var mod_config = require('./config');
var tritonapi = require('./tritonapi');
/**
* A convenience wrapper around `tritonapi.createClient` to for simpler usage.
*
* Minimally this only requires that one of `profileName` or `profile` be
* specified. Examples:
*
* var triton = require('triton');
* var client = triton.createClient({
* profile: {
* url: "<cloudapi url>",
* account: "<account login for this cloud>",
* keyId: "<ssh key fingerprint for one of account's keys>"
* }
* });
* --
* // Loading a profile from the environment (the `TRITON_*` and/or
* // `SDC_*` environment variables).
* var client = triton.createClient({profileName: 'env'});
* --
* var client = triton.createClient({
* configDir: '~/.triton', // use the CLI's config dir ...
* profileName: 'east1' // ... to find named profiles
* });
* --
* // The same thing using the underlying APIs.
* var client = triton.createClient({
* config: triton.loadConfig({configDir: '~/.triton'},
* profile: triton.loadProfile({name: 'east1', configDir: '~/.triton'})
* });
*
* A more complete example an app using triton internally might want:
*
* var triton = require('triton');
* var bunyan = require('bunyan');
*
* var appConfig = {
* // However the app handles its config.
* };
* var log = bunyan.createLogger({name: 'myapp', component: 'triton'});
* var client = triton.createClient({
* log: log,
* profile: appConfig.tritonProfile
* });
*
*
* TODO: The story for an app wanting to specify some Triton config but NOT
* have to have a triton $configDir/config.json is poor.
*
* @param opts {Object}:
* - @param profile {Object} A *Triton profile* object that includes the
* information required to connect to a CloudAPI -- minimally this:
* {
* "url": "<cloudapi url>",
* "account": "<account login for this cloud>",
* "keyId": "<ssh key fingerprint for one of account's keys>"
* }
* For example:
* {
* "url": "https://us-east-1.api.joyent.com",
* "account": "billy.bob",
* "keyId": "de:e7:73:9a:aa:91:bb:3e:72:8d:cc:62:ca:58:a2:ec"
* }
* Either `profile` or `profileName` is requires. See discussion above.
* - @param profileName {String} A Triton profile name. For any profile
* name other than "env", one must also provide either `configDir`
* or `config`.
* Either `profile` or `profileName` is requires. See discussion above.
* - @param configDir {String} A base config directory. This is used
* by TritonApi to find and store profiles, config, and cache data.
* For example, the `triton` CLI uses "~/.triton".
* One may not specify both `configDir` and `config`.
* - @param config {Object} A Triton config object loaded by
* `triton.loadConfig(...)`.
* One may not specify both `configDir` and `config`.
* - @param log {Bunyan Logger} Optional. A Bunyan logger. If not provided,
* a stub that does no logging will be used.
*/
function createClient(opts) {
assert.object(opts, 'opts');
assert.optionalObject(opts.profile, 'opts.profile');
assert.optionalString(opts.profileName, 'opts.profileName');
assert.optionalObject(opts.config, 'opts.config');
assert.optionalString(opts.configDir, 'opts.configDir');
assert.optionalObject(opts.log, 'opts.log');
assert.ok(!(opts.profile && opts.profileName),
'cannot specify both opts.profile and opts.profileName');
assert.ok(!(!opts.profile && !opts.profileName),
'must specify one opts.profile or opts.profileName');
assert.ok(!(opts.config && opts.configDir),
'cannot specify both opts.config and opts.configDir');
assert.ok(!(opts.config && opts.configDir),
'cannot specify both opts.config and opts.configDir');
if (opts.profileName && opts.profileName !== 'env') {
assert.ok(opts.configDir,
'must provide opts.configDir for opts.profileName!="env"');
}
var log = opts.log;
if (!opts.log) {
log = new bunyannoop.BunyanNoopLogger();
}
var config = opts.config;
if (!config) {
config = mod_config.loadConfig({configDir: opts.configDir});
}
var profile = opts.profile;
if (!profile) {
profile = mod_config.loadProfile({
name: opts.profileName,
configDir: config._configDir
});
}
// Don't require one to arbitrarily have a profile.name if manually
// creating it.
if (!profile.name) {
// TODO: might want this to be hash or slug of profile params.
profile.name = '_';
}
mod_config.validateProfile(profile);
var client = tritonapi.createClient({
log: log,
config: config,
profile: profile
});
return client;
}
module.exports = {
createTritonApiClient: require('./tritonapi').createClient,
createClient: createClient,
/**
* `createClient` provides convenience parameters to not *have* to call
* the following (i.e. passing in `configDir` and/or `profileName`), but
* some users of node-triton as a module may want to call these directly.
*/
loadConfig: mod_config.loadConfig,
loadProfile: mod_config.loadProfile,
loadAllProfiles: mod_config.loadAllProfiles,
createTritonApiClient: tritonapi.createClient,
// For those wanting a lower-level raw CloudAPI client.
createCloudApiClient: require('./cloudapi2').createClient
};

View File

@ -92,7 +92,7 @@ function TritonApi(opts) {
this.log = opts.log;
}
if (this.config.cacheDir) {
if (this.config._configDir) {
this.cacheDir = path.resolve(this.config._configDir,
this.config.cacheDir,
common.profileSlug(this.profile));
@ -111,6 +111,11 @@ function TritonApi(opts) {
}
TritonApi.prototype.close = function close() {
this.cloudapi.close();
delete this.cloudapi;
};
TritonApi.prototype._cloudapiFromProfile =
function _cloudapiFromProfile(profile)

View File

@ -1,7 +1,7 @@
{
"name": "triton",
"description": "Joyent Triton CLI and client (https://www.joyent.com/triton)",
"version": "3.3.1",
"version": "3.4.0",
"author": "Joyent (joyent.com)",
"dependencies": {
"assert-plus": "0.2.0",

View File

@ -0,0 +1,99 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
/*
* Copyright (c) 2015, Joyent, Inc.
*/
/*
* Integration tests for using image-related APIs as a module.
*/
var h = require('./helpers');
var test = require('tape');
var common = require('../../lib/common');
// --- Globals
// --- Tests
test('TritonApi images', function (tt) {
var client;
tt.test(' setup: client', function (t) {
client = h.createClient();
t.ok(client, 'client');
t.end();
});
var testOpts = {};
var img;
tt.test(' TritonApi listImages', function (t) {
client.listImages(function (err, images) {
if (h.ifErr(t, err))
return t.end();
t.ok(images, 'images');
t.ok(Array.isArray(images), 'images');
if (images.length) {
img = images[0];
t.ok(img, 'img');
t.ok(common.isUUID(img.id), 'img.id is a UUID');
t.ok(img.name, 'img.name');
t.ok(img.version, 'img.version');
} else {
testOpts.skip = true;
}
t.end();
});
});
tt.test(' TritonApi getImage by uuid', testOpts, function (t) {
client.getImage(img.id, function (err, image) {
if (h.ifErr(t, err))
return t.end();
t.equal(image.id, img.id);
t.end();
});
});
tt.test(' TritonApi getImage by name', testOpts, function (t) {
client.getImage(img.name, function (err, image) {
if (h.ifErr(t, err))
return t.end();
t.equal(image.name, img.name); // might not be the same ID
t.end();
});
});
tt.test(' TritonApi getImage by name (opts)', testOpts, function (t) {
client.getImage({name: img.name}, function (err, image) {
if (h.ifErr(t, err))
return t.end();
t.equal(image.name, img.name); // might not be the same ID
t.end();
});
});
tt.test(' TritonApi getImage by shortId', testOpts, function (t) {
var shortId = img.id.split('-')[0];
client.getImage(shortId, function (err, image) {
if (h.ifErr(t, err))
return t.end();
t.equal(image.id, img.id);
t.end();
});
});
tt.test(' teardown: client', function (t) {
client.close();
t.end();
});
});

View File

@ -18,7 +18,7 @@ var f = require('util').format;
var path = require('path');
var common = require('../../lib/common');
var mod_config = require('../../lib/config');
var mod_triton = require('../../');
var testcommon = require('../lib/testcommon');
@ -41,7 +41,7 @@ try {
assert.optionalBool(CONFIG.profile.insecure,
'CONFIG.profile.insecure');
} else if (CONFIG.profileName) {
CONFIG.profile = mod_config.loadProfile({
CONFIG.profile = mod_triton.loadProfile({
configDir: path.join(process.env.HOME, '.triton'),
name: CONFIG.profileName
});
@ -160,11 +160,24 @@ function safeTriton(t, opts, cb) {
}
/*
* Create a TritonApi client using the CLI.
*/
function createClient() {
return mod_triton.createClient({
log: LOG,
profile: CONFIG.profile,
configDir: '~/.triton' // piggy-back on Triton CLI config dir
});
}
// --- exports
module.exports = {
CONFIG: CONFIG,
triton: triton,
safeTriton: safeTriton,
createClient: createClient,
ifErr: testcommon.ifErr
};