varnish-generator
Version:
The simple way of generating general vcl file for varnish
333 lines (314 loc) • 8.9 kB
JavaScript
const _ = require('lodash');
const path = require('path');
const fs = require('fs');
const yaml = require('js-yaml');
const os = require('os');
const defaultTimeout = {
connect: 2,
firstByte: 5,
betweenBytes: 2,
};
/**
* [readFilePromise description]
* @param {[type]} file [description]
* @return {[type]} [description]
*/
function readFilePromise(file) {
return new Promise((resolve, reject) => {
fs.readFile(file, 'utf8', (err, data) => {
/* istanbul ignore if */
if (err) {
reject(err);
} else {
resolve(data);
}
});
});
}
/* istanbul ignore next */
function writeFilePromise(file, data) {
return new Promise((resolve, reject) => {
fs.writeFile(file, data, (err) => {
if (err) {
reject(err);
} else {
resolve();
}
});
});
}
function sort(items) {
const arr = _.map(items, (item) => {
const tmp = _.extend({}, item);
let sortWeight = 0;
if (!item.service) {
sortWeight += 8;
}
if (!item.host) {
sortWeight += 4;
}
if (!item.prefix) {
sortWeight += 2;
}
tmp.sortKey = `${_.padStart(sortWeight, 2, '0')}-${item.name}`;
return tmp;
});
return _.sortBy(arr, item => item.sortKey);
}
function convertBackends(backends) {
const result = [];
_.forEach(backends, (backend) => {
if (_.isString(backend.port)) {
const port = backend.port;
let portList = [];
if (port.indexOf(',') !== -1) {
portList = port.split(',');
} else if (port.indexOf('..') !== -1) {
const arr = port.split('..');
portList = _.range(parseInt(arr[0], 10), parseInt(arr[1], 10));
} else {
portList.push(port);
}
_.forEach(portList, (item) => {
result.push(_.extend({}, backend, {
port: parseInt(item, 10),
}));
});
} else {
result.push(backend);
}
});
return result;
}
function getBackendConfig(directors) {
const sortedDirectors = sort(directors);
return readFilePromise(path.join(__dirname,
'template/backend.tpl'))
.then((tpl) => {
const template = _.template(tpl);
const arr = [];
_.forEach(sortedDirectors, (director) => {
const backends = convertBackends(director.backends);
_.forEach(backends, (backend, i) => {
const tmp = _.extend({
timeout: _.extend({}, defaultTimeout, director.timeout),
ping: director.ping || '/ping',
}, backend);
tmp.name = `${_.camelCase(director.name)}${i}`;
try {
arr.push(template(tmp));
} catch (err) {
/* istanbul ignore next */
console.error(err);
}
});
});
return arr.join('\n');
});
}
function getInitConfig(directors) {
const sortedDirectors = sort(directors);
return readFilePromise(path.join(__dirname, 'template/init.tpl'))
.then((tpl) => {
const template = _.template(tpl);
const arr = [];
_.forEach(sortedDirectors, (director) => {
const name = _.camelCase(director.name);
const type = director.type || 'round_robin';
arr.push(`new ${name} = directors.${type}();`);
const backends = convertBackends(director.backends);
_.forEach(backends, (server, i) => {
if (type === 'random' || type === 'hash') {
arr.push(`${name}.add_backend(${name + i}, ${server.weight || 1});`);
} else {
arr.push(`${name}.add_backend(${name + i});`);
}
});
});
_.forEach(arr, (tmp, i) => {
arr[i] = ` ${tmp}`;
});
return template({
directors: arr.join('\n'),
});
});
}
function getBackendSelectConfig(directors) {
const sortedDirectors = sort(directors);
const result = [];
let defaultDirector;
_.forEach(sortedDirectors, (director) => {
const arr = [];
/* istanbul ignore else */
if (director.service) {
arr.push(`req.http.X-Service == "${director.service}"`);
}
/* istanbul ignore else */
if (director.host) {
if (_.isArray(director.host)) {
const hostEqList = _.map(director.host, item => `req.http.host == "${item}"`);
arr.push(`(${hostEqList.join(' || ')})`);
} else {
arr.push(`req.http.host == "${director.host}"`);
}
}
/* istanbul ignore else */
if (director.prefix) {
arr.push(`req.url ~ "^${director.prefix}"`);
}
const condition = arr.join(' && ');
if (condition) {
result.push({
name: director.name,
condition,
type: director.type,
hashKey: director.hashKey,
});
} else {
defaultDirector = director;
}
});
const getBackendHint = (director) => {
if (director.type === 'hash') {
const hashKey = director.hashKey || 'req.url';
return `set req.backend_hint = ${_.camelCase(director.name)}.backend(${hashKey});`;
}
return `set req.backend_hint = ${_.camelCase(director.name)}.backend();`;
};
const arr = [];
if (defaultDirector) {
arr.push(getBackendHint(defaultDirector));
}
_.forEach(result, (item, i) => {
if (i === 0) {
arr.push(`if (${item.condition}) {`);
} else {
arr.push(`} elsif (${item.condition}) {`);
}
arr.push(` ${getBackendHint(item)}`);
});
/* istanbul ignore else */
if (arr.length) {
if (arr.length > 1) {
arr.push('}');
}
_.forEach(arr, (tmp, i) => {
arr[i] = ` ${tmp}`;
});
}
return arr.join('\n');
}
/**
* [getConfig description]
* @param {[type]} serverList [description]
* @return {[type]} [description]
*/
function getConfig(directors) {
const data = {};
return getBackendConfig(directors).then((config) => {
data.backendConfig = config;
return getInitConfig(directors);
}).then((config) => {
data.initConfig = config;
data.selectConfig = getBackendSelectConfig(directors);
return data;
});
}
function getPassRules(urls) {
const rules = ['req.http.Cache-Control == "no-cache"'];
_.forEach(urls, url => rules.push(`req.url ~ "${url}"`));
return rules.join(' || ');
}
function getHash(hashConfig) {
const arr = [];
_.forEach(hashConfig, (item) => {
if (_.isString(item)) {
arr.push(`hash_data(${item});`);
return;
}
const total = item.length;
const tmpArr = [];
_.forEach(item, (subItem, index) => {
const hashStr = ` hash_data(${subItem});`;
if (index === 0) {
tmpArr.push(`if (${subItem}) {`);
tmpArr.push(hashStr);
} else if (index === total - 1) {
tmpArr.push('} else {');
tmpArr.push(hashStr);
tmpArr.push('}');
} else {
tmpArr.push(`} elsif (${subItem}) {`);
tmpArr.push(hashStr);
}
});
arr.push(...tmpArr);
});
return _.map(arr, item => ` ${item}`).join('\n');
}
/**
* [getVcl description]
* @param {[type]} config [description]
* @return {[type]} [description]
*/
function getVcl(conf) {
const config = _.extend({
version: new Date().toISOString(),
timeout: defaultTimeout,
urlPassList: ['cache-control=no-cache'],
hash: [
'req.url',
[
'req.http.host',
'server.ip',
],
],
hisForPassTTL: 120,
}, conf);
/* istanbul ignore if */
if (!config.directors || !config.name) {
throw new Error('directors, name can not be null');
}
if (!config.directors.length) {
throw new Error('directors can not be empty');
}
if (config.name === '$HOSTNAME') {
config.name = os.hostname();
} else if (config.name[0] === '$') {
config.name = process.env[config.name.substring(1)];
}
_.forEach(config.directors, (item) => {
if (!item.timeout) {
/* eslint no-param-reassign:0 */
item.timeout = config.timeout;
}
if (item.type === 'shard') {
throw new Error('shard director is not support');
}
});
const cloneConfig = _.extend({
stale: 3,
varnish: '5',
hashConfig: getHash(config.hash),
passRules: getPassRules(config.urlPassList),
}, config);
return getConfig(cloneConfig.directors).then((data) => {
_.extend(cloneConfig, data);
return readFilePromise(path.join(__dirname, 'template/varnish.tpl'));
}).then(tpl => _.template(tpl)(cloneConfig));
}
function getVclFromFile(file) {
const extname = path.extname(file);
return readFilePromise(file)
.then((data) => {
const json = extname === '.yml' ? yaml.safeLoad(data) : JSON.parse(data);
return getVcl(json);
});
}
exports.getBackendConfig = getBackendConfig;
exports.getInitConfig = getInitConfig;
exports.getBackendSelectConfig = getBackendSelectConfig;
exports.getConfig = getConfig;
exports.getVcl = getVcl;
exports.getVclFromFile = getVclFromFile;
exports.writeVclToFile = writeFilePromise;