diff --git a/TODO.txt b/TODO.txt index 3b30590..a7b1894 100644 --- a/TODO.txt +++ b/TODO.txt @@ -24,28 +24,6 @@ triton images for 'linux' ones. That might hit that stupid naming problem. -# profiles - -triton profile # list all profiles -triton profile NAME # show NAME profile -triton profile -a NAME # sets it as active -triton profile -n|--new # ??? - -For today: only the implicit 'env' profile. - - - -# config - -~/.triton/ - config.json - {"currProfile": "east3b"} - east3b/ # profile - PROFILE2/ - ... - - - # another day triton config get|set|list # see 'npm config' diff --git a/lib/cli.js b/lib/cli.js index 24133d6..0d2369f 100644 --- a/lib/cli.js +++ b/lib/cli.js @@ -134,7 +134,8 @@ function CLI() { }, helpSubcmds: [ 'help', - 'profiles', + // TODO: hide until the command is fully implemented: + // 'profiles', { group: 'Other Commands' }, 'info', 'account', @@ -192,11 +193,12 @@ CLI.prototype.init = function (opts, args, callback) { opts.url = format('https://%s.api.joyent.com', opts.J); } this.envProfile = mod_config.loadEnvProfile(opts); + this.configDir = CONFIG_DIR; this.__defineGetter__('tritonapi', function () { if (self._triton === undefined) { var config = mod_config.loadConfig({ - configDir: CONFIG_DIR + configDir: self.configDir }); self.log.trace({config: config}, 'loaded config'); var profileName = opts.profile || config.profile || 'env'; @@ -205,7 +207,7 @@ CLI.prototype.init = function (opts, args, callback) { profile = self.envProfile; } else { profile = mod_config.loadProfile({ - configDir: CONFIG_DIR, + configDir: self.configDir, name: profileName }); } diff --git a/lib/config.js b/lib/config.js index e23519d..72f49c2 100644 --- a/lib/config.js +++ b/lib/config.js @@ -8,18 +8,51 @@ * Copyright 2015 Joyent, Inc. */ +/* + * This module provides functions to read and write (a) a TritonApi config + * and (b) TritonApi profiles. + * + * The config is a JSON object loaded from "etc/defaults.json" (shipped with + * node-triton) plus possibly overrides from "$configDir/config.json" -- + * which is "~/.triton/config.json" for the `triton` CLI. The config has + * a strict set of allowed keys. + * + * A profile is a small object that includes the necessary info for talking + * to a CloudAPI. E.g.: + * { + * "name": "east1", + * "account": "billy.bob", + * "keyId": "de:e7:73:9a:aa:91:bb:3e:72:8d:cc:62:ca:58:a2:ec", + * "url": "https://us-east-1.api.joyent.com" + * } + * + * Profiles are stored as separate JSON files in + * "$configDir/profiles.d/$name.json". Typically `triton profiles ...` is + * used to manage them. In addition there is the special "env" profile that + * is constructed from the "SDC_*" environment variables. + */ + var assert = require('assert-plus'); var format = require('util').format; var fs = require('fs'); +var mkdirp = require('mkdirp'); var glob = require('glob'); var path = require('path'); +var vasync = require('vasync'); var common = require('./common'); var errors = require('./errors'); var DEFAULTS_PATH = path.resolve(__dirname, '..', 'etc', 'defaults.json'); -var OVERRIDE_KEYS = []; // config object keys to do a one-level deep override +var OVERRIDE_NAMES = []; // config object keys to do a one-level deep override + +// TODO: use this const to create the "Configuration" docs table. +var CONFIG_VAR_NAMES = [ + 'profile', + 'cacheDir' +]; + // --- internal support stuff @@ -36,8 +69,12 @@ function _validateProfile(profile) { } +function configPathFromDir(configDir) { + return path.resolve(configDir, 'config.json'); +} -// --- exported functions + +// --- Config /** * Load the TritonApi config. This is a merge of the built-in "defaults" (at @@ -55,7 +92,7 @@ function loadConfig(opts) { assert.object(opts, 'opts'); assert.string(opts.configDir, 'opts.configDir'); - var configPath = path.resolve(opts.configDir, 'config.json'); + var configPath = configPathFromDir(opts.configDir); var c = fs.readFileSync(DEFAULTS_PATH, 'utf8'); var _defaults = JSON.parse(c); @@ -71,7 +108,7 @@ function loadConfig(opts) { // These special keys are merged into the key of the same name in the // base "defaults.json". Object.keys(userConfig).forEach(function (key) { - if (~OVERRIDE_KEYS.indexOf(key) && config[key] !== undefined) { + if (~OVERRIDE_NAMES.indexOf(key) && config[key] !== undefined) { Object.keys(userConfig[key]).forEach(function (subKey) { if (userConfig[key][subKey] === null) { delete config[key][subKey]; @@ -93,6 +130,61 @@ function loadConfig(opts) { } +function setConfigVar(opts, cb) { + assert.object(opts, 'opts'); + assert.string(opts.configDir, 'opts.configDir'); + assert.string(opts.name, 'opts.name'); + assert.string(opts.value, 'opts.value'); + assert.ok(opts.name.indexOf('.') === -1, + 'dotted config name not yet supported'); + assert.ok(CONFIG_VAR_NAMES.indexOf(opts.name) !== -1, + 'unknown config var name: ' + opts.name); + + var configPath = configPathFromDir(opts.configDir); + + var config; + vasync.pipeline({funcs: [ + function loadExisting(_, next) { + fs.exists(configPath, function (exists) { + if (!exists) { + config = {}; + return next(); + } + fs.readFile(configPath, function (err, data) { + if (err) { + return next(err); + } + try { + config = JSON.parse(data); + } catch (e) { + return next(e); + } + next(); + }); + }); + }, + + function mkConfigDir(_, next) { + fs.exists(opts.configDir, function (exists) { + if (!exists) { + mkdirp(opts.configDir, next); + } else { + next(); + } + }); + }, + + function updateAndSave(_, next) { + config[opts.name] = opts.value; + fs.writeFile(configPath, JSON.stringify(config, null, 4), next); + } + ]}, cb); +} + + + +// --- Profiles + /** * Load the special 'env' profile, which handles some details of getting * values from envvars. *Most* of that is done already via the @@ -183,6 +275,7 @@ function loadAllProfiles(opts) { module.exports = { loadConfig: loadConfig, + setConfigVar: setConfigVar, loadEnvProfile: loadEnvProfile, loadProfile: loadProfile, loadAllProfiles: loadAllProfiles diff --git a/lib/do_profiles.js b/lib/do_profiles.js index 6df5d71..eb20c3e 100644 --- a/lib/do_profiles.js +++ b/lib/do_profiles.js @@ -1,7 +1,7 @@ /* * Copyright (c) 2015 Joyent Inc. All rights reserved. * - * `triton profile ...` + * `triton profiles ...` */ var common = require('./common'); @@ -14,14 +14,7 @@ var sortDefault = 'name'; var columnsDefault = 'name,curr,account,url'; var columnsDefaultLong = 'name,curr,account,url,insecure,keyId'; -function do_profiles(subcmd, opts, args, cb) { - if (opts.help) { - this.do_help('help', {}, [subcmd], cb); - return; - } else if (args.length > 1) { - return cb(new errors.UsageError('too many args')); - } - +function _listProfiles(_, opts, cb) { var columns = columnsDefault; if (opts.o) { columns = opts.o; @@ -48,7 +41,8 @@ function do_profiles(subcmd, opts, args, cb) { var i; if (opts.json) { for (i = 0; i < profiles.length; i++) { - profiles[i].curr = (profiles[i].name === this.tritonapi.profile.name); + profiles[i].curr = (profiles[i].name === + this.tritonapi.profile.name); } common.jsonStream(profiles); } else { @@ -65,12 +59,136 @@ function do_profiles(subcmd, opts, args, cb) { cb(); } +function _currentProfile(profile, opts, cb) { + if (this.tritonapi.profile.name === profile.name) { + console.log('"%s" is already the current profile', profile.name); + return cb(); + } + + mod_config.setConfigVar({ + configDir: this.configDir, + name: 'profile', + value: profile.name + }, function (err) { + if (err) { + return cb(err); + } + console.log('Switched to "%s" profile', profile.name); + cb(); + }); +} + +// TODO: finish the implementation +//function _addProfile(profile, opts, cb) { +//} +// +//function _editProfile(profile, opts, cb) { +//} +// +//function _deleteProfile(profile, opts, cb) { +//} + + +function do_profiles(subcmd, opts, args, cb) { + if (opts.help) { + this.do_help('help', {}, [subcmd], cb); + return; + } + + var actions = []; + if (opts.add) { actions.push('add'); } + if (opts.current) { actions.push('current'); } + if (opts.edit) { actions.push('edit'); } + if (opts['delete']) { actions.push('delete'); } + 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]; + } + + var name; + switch (action) { + case 'add': + if (args.length === 1) { + name = args[0]; + } else if (args.length > 1) { + return cb(new errors.UsageError('too many args')); + } + break; + case 'list': + case 'current': + case 'edit': + case 'delete': + name = opts.current || opts.edit || opts['delete']; + if (args.length > 0) { + return cb(new errors.UsageError('too many args')); + } + break; + default: + throw new Error('unknown action: ' + action); + } + + var profile; + if (name) { + if (name === 'env') { + profile = this.envProfile; + } else { + profile = mod_config.loadProfile({ + configDir: this.configDir, + name: name + }); + } + } + + var func = { + list: _listProfiles, + current: _currentProfile + // TODO: finish the implementation + //add: _addProfile, + //edit: _editProfile, + //'delete': _deleteProfile + }[action].bind(this); + func(profile, opts, cb); +} + do_profiles.options = [ { names: ['help', 'h'], type: 'bool', help: 'Show this help.' + }, + { + group: 'Action Options' + }, + { + names: ['current', 'c'], + type: 'string', + helpArg: 'NAME', + help: 'Switch to the given profile.' } + // TODO: finish the implementation + //{ + // names: ['add', 'a'], + // type: 'bool', + // help: 'Add a new profile.' + //}, + //{ + // names: ['edit', 'e'], + // type: 'string', + // helpArg: 'NAME', + // help: 'Edit profile NAME in your $EDITOR.' + //}, + //{ + // names: ['delete', 'd'], + // type: 'string', + // helpArg: 'NAME', + // help: 'Delete profile NAME.' + //} + ].concat(common.getCliTableOptions({ includeLong: true, sortDefault: sortDefault @@ -86,10 +204,17 @@ do_profiles.help = [ 'The "CURR" column indicates which profile is the current one.', '', 'Usage:', - ' {{name}} profiles', + ' {{name}} profiles # list profiles', + ' {{name}} profiles -c|--current NAME # set NAME as current profile', + // TODO: finish the implementation + //' {{name}} profiles -a|--add [NAME] # add a new profile', + //' {{name}} profiles -e|--edit NAME # edit a profile in $EDITOR', + //' {{name}} profiles -d|--delete NAME # delete a profile', '', '{{options}}' ].join('\n'); +do_profiles.hidden = true; // TODO: until -a,-e,-d are implemented + module.exports = do_profiles;