This repository has been archived on 2020-01-20. You can view files and clone it, but cannot push or open issues or pull requests.
node-spearhead/lib/config.js
2015-09-21 14:01:00 -04:00

313 lines
9.3 KiB
JavaScript

/*
* 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.
*/
/*
* 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 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_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'
];
// TODO: use this to create a profile doc table?
var PROFILE_FIELDS = {
name: true,
url: true,
account: true,
keyId: true,
insecure: true
};
// --- internal support stuff
// TODO: improve this validation: use ConfigError's instead of asserts
function _validateProfile(profile, profilePath) {
assert.object(profile, 'profile');
assert.string(profile.name, 'profile.name');
assert.string(profile.url, 'profile.url');
assert.string(profile.account, 'profile.account');
assert.string(profile.keyId, 'profile.keyId');
assert.optionalBool(profile.insecure, 'profile.insecure');
assert.optionalString(profilePath, 'profilePath');
var bogusFields = [];
Object.keys(profile).forEach(function (field) {
if (!PROFILE_FIELDS[field]) {
bogusFields.push(field);
}
});
if (bogusFields.length) {
throw new errors.ConfigError(format(
'extraneous fields in "%s" profile: %s%s', profile.name,
(profilePath ? profilePath + ': ' : ''), bogusFields.join(', ')));
}
}
function configPathFromDir(configDir) {
return path.resolve(configDir, 'config.json');
}
// --- Config
/**
* Load the TritonApi config. This is a merge of the built-in "defaults" (at
* etc/defaults.json) and the "user" config (at "$configDir/config.json",
* typically "~/.triton/config.json", if it exists).
*
* This includes some internal data on keys with a leading underscore:
* _defaults the defaults.json object
* _user the "user" config.json object
* _configDir the user config dir
*
* @returns {Object} The loaded config.
*/
function loadConfig(opts) {
assert.object(opts, 'opts');
assert.string(opts.configDir, 'opts.configDir');
var configPath = configPathFromDir(opts.configDir);
var c = fs.readFileSync(DEFAULTS_PATH, 'utf8');
var _defaults = JSON.parse(c);
var config = JSON.parse(c);
if (fs.existsSync(configPath)) {
c = fs.readFileSync(configPath, 'utf8');
var _user = JSON.parse(c);
var userConfig = JSON.parse(c);
if (typeof (userConfig) !== 'object' || Array.isArray(userConfig)) {
throw new errors.ConfigError(
format('"%s" is not an object', configPath));
}
// 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_NAMES.indexOf(key) && config[key] !== undefined) {
Object.keys(userConfig[key]).forEach(function (subKey) {
if (userConfig[key][subKey] === null) {
delete config[key][subKey];
} else {
config[key][subKey] = userConfig[key][subKey];
}
});
} else {
config[key] = userConfig[key];
}
});
config._user = _user;
}
config._defaults = _defaults;
config._configDir = opts.configDir;
return config;
}
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
* `opts` dashdash Options object.
*
* @returns {Object} The 'env' profile.
*/
function loadEnvProfile(opts) {
// XXX support keyId being a priv or pub key path, a la imgapi-cli
// XXX Add TRITON_* envvars.
var envProfile = {
name: 'env',
account: opts.account,
url: opts.url,
keyId: opts.keyId,
insecure: opts.insecure
};
// If --insecure not given, look at envvar(s) for that.
var specifiedInsecureOpt = opts._order.filter(
function (opt) { return opt.key === 'insecure'; }).length > 0;
if (!specifiedInsecureOpt && process.env.SDC_TESTING) {
envProfile.insecure = common.boolFromString(
process.env.SDC_TESTING,
false, '"SDC_TESTING" envvar');
}
_validateProfile(envProfile);
return envProfile;
}
function _profileFromPath(profilePath, name) {
if (! fs.existsSync(profilePath)) {
throw new errors.ConfigError('no such profile: ' + name);
}
var profile;
try {
profile = JSON.parse(fs.readFileSync(profilePath, 'utf8'));
} catch (e) {
throw new errors.ConfigError(e, format(
'error in "%s" profile: %s: %s', name,
profilePath, e.message));
}
if (profile.name) {
throw new errors.ConfigError(format(
'error in "%s" profile: %s: file must not include "name" field',
name, profilePath));
}
profile.name = name;
_validateProfile(profile, profilePath);
return profile;
}
function loadProfile(opts) {
assert.string(opts.configDir, 'opts.configDir');
assert.string(opts.name, 'opts.name');
var profilePath = path.resolve(opts.configDir, 'profiles.d',
opts.name + '.json');
return _profileFromPath(profilePath, opts.name);
}
function loadAllProfiles(opts) {
assert.string(opts.configDir, 'opts.configDir');
assert.object(opts.log, 'opts.log');
var profiles = [];
var d = path.join(opts.configDir, 'profiles.d');
var files = fs.readdirSync(d);
files.forEach(function (file) {
file = path.join(d, file);
var ext = path.extname(file);
if (ext !== '.json')
return;
var name = path.basename(file).slice(0, - path.extname(file).length);
if (name.toLowerCase() === 'env') {
// Skip the special 'env'.
opts.log.debug('skip reserved name "env" profile: %s', file);
return;
}
try {
profiles.push(_profileFromPath(file, name));
} catch (e) {
opts.log.warn({err: e, profilePath: file},
'error loading profile; skipping');
}
});
return profiles;
}
//---- exports
module.exports = {
loadConfig: loadConfig,
setConfigVar: setConfigVar,
loadEnvProfile: loadEnvProfile,
loadProfile: loadProfile,
loadAllProfiles: loadAllProfiles
};
// vim: set softtabstop=4 shiftwidth=4: