whaler
Version:
Define and run multi-container applications with Docker
414 lines (359 loc) • 13.8 kB
JavaScript
;
const fs = require('fs').promises;
const path = require('path');
const yaml = require('../../lib/yaml');
const util = require('dockerode/lib/util');
const parseEnv = require('../../lib/parse-env');
const renderTemplate = require('../../lib/render-template');
module.exports = exports;
module.exports.__cmd = require('./cmd');
/**
* @param whaler
*/
async function exports (whaler) {
whaler.on('config', async ctx => {
const { default: storage } = await whaler.fetch('apps');
const app = await storage.get(ctx.options['name']);
const update = {};
if (ctx.options['setEnv']) {
app.env = update['env'] = ctx.options['setEnv'];
}
if (ctx.options['update']) {
update['config'] = prepareOutput(
await loadConfig(app, ctx.options)
);
}
if (Object.keys(update).length > 0) {
await storage.update(ctx.options['name'], update);
ctx.result = update['config'] || app.config;
} else {
let config = app.config;
if (ctx.options['file']) {
config = prepareOutput(
await loadConfig(app, ctx.options)
);
}
ctx.result = config;
}
});
// TODO: experimental
whaler.on('config:env', async ctx => {
const { default: storage } = await whaler.fetch('apps');
const app = await storage.get(ctx.options['name']);
ctx.result = app.env;
});
// TODO: experimental
whaler.on('config:prepare', async ctx => {
ctx.result = await prepareConfig(
ctx.options['config'],
ctx.options['app'].env,
opts => loadConfig(ctx.options['app'], opts)
);
});
const loadConfig = async (app, options) => {
let data;
const vars = await prepareVars(app, options);
const file = options['file'] || app.config['file'] || app.path + '/whaler.yml';
// deprecated
if (options['yml']) {
const tmpFile = file + '.tmp';
await fs.writeFile(tmpFile, options['yml'], 'utf8');
data = await renderTemplate(tmpFile, vars);
await fs.unlink(tmpFile);
} else {
if (!path.isAbsolute(file)) {
throw new Error('Config path must be absolute.');
}
try {
await fs.stat(file);
} catch (e) {
throw new Error('Config file `' + file + '` not exists.');
}
data = await renderTemplate(file, vars);
}
// data = data.replace('[app_name]', options['name']);
// data = data.replace('[app_path]', app.path);
data = yaml.load(data);
return {
file: file,
//data: await prepareConfig(data, app.env, (opts) => loadConfig(app, opts))
data: await whaler.emit('config:prepare', {
app: app,
config: data
})
};
};
const prepareVars = async (app, options) => {
const vars = await whaler.emit('vars');
try {
const content = await fs.readFile(app.path + '/.env', 'utf8');
const env = parseEnv(content || '');
for (let key in env) {
vars[key] = env[key];
}
} catch (e) {}
try {
const content = await fs.readFile(app.path + '/.env.local', 'utf8');
const env = parseEnv(content || '');
for (let key in env) {
vars[key] = env[key];
}
} catch (e) {}
vars['APP_NAME'] = options['name'];
vars['APP_PATH'] = app.path;
vars['APP_ENV'] = app.env;
return vars;
};
}
// PRIVATE
/**
* @param arr
* @returns {Object}
*/
function convertArrayToObject (arr, separator = ';') {
const obj = {};
if (arr) {
for (let str of arr) {
const [ key, ...rest ] = str.split(separator);
const value = rest && rest.join(separator);
if (value.length) {
obj[key] = value;
} else {
obj[key] = null;
}
}
}
return obj;
}
/**
* @param obj
* @returns {Array}
*/
function convertObjectToArray (obj, separator = ';') {
const arr = [];
if (obj) {
for (let key in obj) {
if ('object' == typeof obj[key] || 'undefined' == typeof obj[key]) {
arr.push(key);
} else {
arr.push(key + separator + obj[key]);
}
}
}
return arr;
}
/**
* @param config
* @returns {Object}
*/
function prepareOutput (config) {
const tmp = [];
const convert = (original, separator) => {
let converted;
const found = tmp.find(value => value.original === original);
if (found) {
converted = found.converted;
} else {
converted = convertObjectToArray(original, separator);
tmp.push({ original, converted });
}
return converted;
};
for (let key in config['data']['services']) {
const service = config['data']['services'][key];
if (service['env'] && !Array.isArray(service['env'])) {
service['env'] = convert(service['env'], '=');
}
if (service['volumes'] && !Array.isArray(service['volumes'])) {
service['volumes'] = convert(service['volumes'], ':');
}
}
config['data'] = yaml.load(yaml.dump(config['data']));
return config;
}
/**
* @param config
* @param env
* @returns {Object}
*/
async function prepareConfig (config, env, loader) {
config = config || {};
config = prepareConfigEnv(config, env);
if (config['services'] || null) {
const scale = {};
const services = config['services'];
for (let key in services) {
if (!/^[a-z0-9-]+$/.test(key)) {
throw new Error('Service name `' + key + '` includes invalid characters, only `[a-z0-9-]` are allowed.');
}
if (services[key]['env'] && Array.isArray(services[key]['env'])) {
services[key]['env'] = convertArrayToObject(services[key]['env'], '=');
}
if (services[key]['labels'] && Array.isArray(services[key]['labels'])) {
services[key]['labels'] = convertArrayToObject(services[key]['labels'], '=');
}
if (services[key]['volumes'] && Array.isArray(services[key]['volumes'])) {
services[key]['volumes'] = convertArrayToObject(services[key]['volumes'], ':');
}
if (services[key]['extends']) {
const ex = await loader({
file: path.resolve(services[key]['extends']['file'])
});
const data = ex['data']['services'][services[key]['extends']['service']];
const fn = services[key]['extends']['fn'];
delete services[key]['extends'];
// TODO: experimental
if (fn) {
const service = fn({ service: data, local: services[key] });
if (service) {
services[key] = util.extend({}, services[key], service);
}
}
services[key] = util.extend({}, data, services[key]);
}
if (services[key]['extend']) {
if ('string' === typeof services[key]['extend']) {
let service = services[key]['extend'];
let type = 'exclude';
let keys = [];
if (service.indexOf('&') !== -1) {
type = 'include';
const parts = service.split('&');
service = parts[0];
keys = parts[1].split(',');
} else if (service.indexOf('!') !== -1) {
type = 'exclude';
const parts = service.split('!');
service = parts[0];
keys = parts[1].split(',');
}
services[key]['extend'] = { service, type, keys };
} else {
if (!services[key]['extend']['type']) {
services[key]['extend']['type'] = 'exclude';
}
if (!services[key]['extend']['keys']) {
services[key]['extend']['keys'] = [];
}
}
// TODO: BC
if (services[key]['extend']['include']) {
services[key]['extend']['type'] = 'include';
services[key]['extend']['keys'] = services[key]['extend']['include'];
} else if (services[key]['extend']['exclude']) {
services[key]['extend']['type'] = 'exclude';
services[key]['extend']['keys'] = services[key]['extend']['exclude'];
}
let data = {};
if (services[key]['extend']['keys']) {
if ('include' === services[key]['extend']['type']) {
for (let include of services[key]['extend']['keys']) {
if (services[services[key]['extend']['service']].hasOwnProperty(include)) {
data[include] = services[services[key]['extend']['service']][include];
}
}
} else {
data = util.extend({}, services[services[key]['extend']['service']]);
for (let exclude of services[key]['extend']['keys']) {
if (data.hasOwnProperty(exclude)) {
delete data[exclude];
}
}
}
}
const fn = services[key]['extend']['fn'];
delete services[key]['extend'];
if (data.hasOwnProperty('ports')) {
delete data['ports'];
}
let baseExtend = true;
// TODO: experimental
if (fn) {
const service = fn({ service: data, local: services[key] });
if (service) {
baseExtend = false;
services[key] = util.extend({}, service);
}
}
if (baseExtend) {
services[key] = util.extend({}, data, services[key]);
}
}
if ('object' === typeof services[key]['build'] && services[key]['build'].hasOwnProperty('args')) {
if (!Array.isArray(services[key]['build']['args'])) {
const buildargs = [];
for (let arg in services[key]['build']['args']) {
if ('object' == typeof services[key]['build']['args'][arg]) {
buildargs.push(arg);
} else {
buildargs.push(arg + '=' + services[key]['build']['args'][arg]);
}
}
services[key]['build']['args'] = buildargs;
}
}
if (services[key]['wait'] && 'string' !== typeof services[key]['wait']) {
services[key]['wait'] = services[key]['wait'] + 's';
}
// TODO: experimental
if (services[key].hasOwnProperty('scale')) {
if (services[key]['scale']) {
scale[key] = services[key]['scale'];
}
delete services[key]['scale'];
}
}
// TODO: experimental
if (Object.keys(scale).length) {
const newServices = {};
for (let key in services) {
if (scale[key] || false) {
for (let i = 1; i <= scale[key]; i++) {
newServices[key + i] = services[key];
}
} else {
newServices[key] = services[key];
}
}
config['services'] = newServices;
}
} else {
config['services'] = {};
}
return config;
}
/**
* @param config
* @param env
* @returns {Object}
*/
function prepareConfigEnv (config, env) {
if ('object' === typeof config && null !== config && Object.keys(config).length) {
env = env.split(',');
const keys = Object.keys(config);
for (let key of keys) {
if ('~' == key[0]) {
const parts = key.split('~')[1].split(',');
for (let e of env) {
if (parts.includes(e)) {
const tmpConfig = {};
for (let index in config) {
if (index == key) {
util.extend(tmpConfig, config[key]);
} else if (!tmpConfig.hasOwnProperty(index)) {
tmpConfig[index] = config[index];
}
}
config = tmpConfig;
//config = util.extend({}, config, config[key]);
}
}
delete config[key];
config = prepareConfigEnv(config, env.join(','));
} else {
config[key] = prepareConfigEnv(config[key], env.join(','));
}
}
}
return config;
}