forked from TritonDataCenter/node-triton
-
Notifications
You must be signed in to change notification settings - Fork 0
/
index.js
271 lines (255 loc) · 9.96 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
/*
* 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.
*/
var assert = require('assert-plus');
var vasync = require('vasync');
var bunyannoop = require('./bunyannoop');
var mod_common = require('./common');
var mod_config = require('./config');
var mod_cloudapi2 = require('./cloudapi2');
var mod_tritonapi = require('./tritonapi');
/* BEGIN JSSTYLED */
/**
* A convenience wrapper around `tritonapi.TritonApi` for simpler usage.
* Conveniences are:
* - It wraps up the 3-step process of TritonApi client preparation into
* this one call.
* - It accepts optional `profileName` and `configDir` parameters that will
* load a profile by name and load a config, respectively.
*
* Client preparation is a 3-step process:
*
* 1. create the client object;
* 2. initialize it (mainly involves finding the SSH key identified by the
* `keyId`); and,
* 3. optionally unlock the SSH key (if it is passphrase-protected and not in
* an ssh-agent).
*
* The simplest usage that handles all of these is:
*
* var mod_triton = require('triton');
* mod_triton.createClient({
* profileName: 'env',
* unlockKeyFn: triton.promptPassphraseUnlockKey
* }, function (err, client) {
* if (err) {
* // handle err
* }
*
* // use `client`
* });
*
* Minimally, only of `profileName` or `profile` is required. Examples:
*
* // Manually specify profile parameters.
* mod_triton.createClient({
* profile: {
* url: "<cloudapi url>",
* account: "<account login for this cloud>",
* keyId: "<ssh key fingerprint for one of account's keys>"
* }
* }, function (err, client) { ... });
*
* // Loading a profile from the environment (the `TRITON_*` and/or
* // `SDC_*` environment variables).
* triton.createClient({profileName: 'env'},
* function (err, client) { ... });
*
* // Use one of the named profiles from the `triton` CLI.
* triton.createClient({
* configDir: '~/.triton',
* profileName: 'east1'
* }, function (err, client) { ... });
*
* // The same thing using the underlying APIs.
* triton.createClient({
* config: triton.loadConfig({configDir: '~/.triton'}),
* profile: triton.loadProfile({name: 'east1', configDir: '~/.triton'})
* }, function (err, client) { ... });
*
* TODO: The story for an app wanting to specify some Triton config but NOT
* have to have a triton $configDir/config.json is poor.
*
*
* # What is that `unlockKeyFn` about?
*
* Triton uses HTTP-Signature auth: an SSH private key is used to sign requests.
* The server-side authenticates by verifying that signature using the
* previously uploaded public key. For the client to sign a request it needs an
* unlocked private key: an SSH private key that (a) is not
* passphrase-protected, (b) is loaded in an ssh-agent, or (c) for which we
* have a passphrase.
*
* If `createClient` finds that its key is locked, it will use `unlockKeyFn`
* as follows to attempt to unlock it:
*
* unlockKeyFn({
* tritonapi: client
* }, function (unlockErr) {
* // ...
* });
*
* This package exports a convenience `promptPassphraseUnlockKey` function that
* will prompt the user for a passphrase on stdin. Your tooling can use this
* function, provide your own, or skip key unlocking.
*
* The failure mode for a locked key is an error like this:
*
* SigningError: error signing request: SSH private key id_rsa is locked (encrypted/password-protected). It must be unlocked before use.
* at SigningError._TritonBaseVError (/Users/trentm/tmp/node-triton/lib/errors.js:55:12)
* at new SigningError (/Users/trentm/tmp/node-triton/lib/errors.js:173:23)
* at CloudApi._getAuthHeaders (/Users/trentm/tmp/node-triton/lib/cloudapi2.js:185:22)
*
*
* @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 required. 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.
* - @param {Function} unlockKeyFn - Optional. A function to handle
* unlocking the SSH key found for this profile, if necessary. It must
* be of the form `function (opts, cb)` where `opts.tritonapi` is the
* initialized TritonApi client. If the caller is a command-line
* interface, then `triton.promptPassphraseUnlockKey` can be used to
* prompt on stdin for the SSH key passphrase, if needed.
* @param {Function} cb - `function (err, client)`
*/
/* END JSSTYLED */
function createClient(opts, cb) {
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.optionalFunc(opts.unlockKeyFn, 'opts.unlockKeyFn');
assert.func(cb, 'cb');
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;
var client;
vasync.pipeline({funcs: [
function theSyncPart(_, next) {
log = opts.log || new bunyannoop.BunyanNoopLogger();
var config;
if (opts.config) {
config = opts.config;
} else {
try {
config = mod_config.loadConfig(
{configDir: opts.configDir});
} catch (configErr) {
next(configErr);
return;
}
}
var profile;
if (opts.profile) {
profile = opts.profile;
/*
* Don't require one to arbitrarily have a profile.name if
* manually creating it.
*/
if (!profile.name) {
// TODO: might want this to be a hash/slug of params.
profile.name = '_';
}
} else {
try {
profile = mod_config.loadProfile({
name: opts.profileName,
configDir: config._configDir
});
} catch (profileErr) {
next(profileErr);
return;
}
}
try {
mod_config.validateProfile(profile);
} catch (valErr) {
next(valErr);
return;
}
client = mod_tritonapi.createClient({
log: log,
config: config,
profile: profile
});
next();
},
function initTheClient(_, next) {
client.init(next);
},
function optionallyUnlockKey(_, next) {
if (!opts.unlockKeyFn) {
next();
return;
}
opts.unlockKeyFn({tritonapi: client}, next);
}
]}, function (err) {
log.trace({err: err}, 'createClient complete');
if (err) {
cb(err);
} else {
cb(null, client);
}
});
}
module.exports = {
createClient: createClient,
promptPassphraseUnlockKey: mod_common.promptPassphraseUnlockKey,
/**
* `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,
/*
* For those wanting a lower-level TritonApi createClient, or an
* even *lower*-level raw CloudAPI client.
*/
createTritonApiClient: mod_tritonapi.createClient,
createCloudApiClient: mod_cloudapi2.createClient
};