This repository has been archived on 2024-07-12. You can view files and clone it, but cannot push or open issues or pull requests.
docker-discover/index.js

343 lines
9.0 KiB
JavaScript
Raw Normal View History

2020-01-28 13:57:24 +00:00
require('require-yaml');
2021-11-22 14:45:49 +00:00
const Docker = require('dockerode');
const handlebars = require('handlebars');
const ssh = require('node-ssh');
const fs = require('fs-extra');
const shajs = require('sha.js');
const conf = require('./config.yml');
const package = require('./package.json');
2020-01-27 16:25:39 +00:00
let docker;
let template;
let lastInfoHash;
2021-11-22 14:45:49 +00:00
const appName = package.name;
const isProduction = process.env.NODE_ENV === 'production';
const tmpDir = isProduction ? `/tmp/${appName}` : `${__dirname}/tmp`;
const hashFile = `${tmpDir}/config.hash`;
const confDir = conf.rproxy.confDir;
2020-01-27 16:25:39 +00:00
2021-11-22 16:02:55 +00:00
async function updateProxy(firstRun) {
2020-01-28 13:57:24 +00:00
console.log('Updating reverse proxy configuration.');
// Obtaining Docker settings
2020-01-28 13:57:24 +00:00
let info;
2020-01-27 16:25:39 +00:00
2020-01-28 13:57:24 +00:00
if (!isProduction) {
info = require('./test.json');
2020-01-27 16:25:39 +00:00
} else {
2020-01-28 13:57:24 +00:00
info = {
2020-01-27 16:25:39 +00:00
services: await docker.listServices(),
nodes: await docker.listNodes()
};
}
2020-01-29 16:01:39 +00:00
function sortFn(a, b) {
return a.name > b.name ? 1 : a.name < b.name ? -1 : 0;
}
2021-11-22 14:45:49 +00:00
const nodes = [];
for (const node of info.nodes) {
const address = node.ManagerStatus
2020-01-29 16:01:39 +00:00
? node.ManagerStatus.Addr.split(':')[0]
: node.Status.Addr;
2021-11-22 14:45:49 +00:00
const role = node.Spec && node.Spec.Role
2020-01-29 16:01:39 +00:00
nodes.push({
name: node.Description.Hostname,
2021-11-22 14:45:49 +00:00
address,
isWorker: role == 'worker'
2020-01-29 16:01:39 +00:00
});
}
2021-11-22 14:45:49 +00:00
nodes.sort(sortFn);
2020-01-29 16:01:39 +00:00
2021-11-22 14:45:49 +00:00
const services = [];
for (const service of info.services) {
const ports = service.Endpoint.Ports;
2020-01-28 13:57:24 +00:00
if (!Array.isArray(ports) || !ports.length) continue;
2020-01-29 16:14:08 +00:00
let name = service.Spec.Name;
2021-11-22 14:45:49 +00:00
const match = name.match(/^(.+)_main$/);
2020-01-28 13:57:24 +00:00
if (match) name = match[1];
2021-11-22 14:45:49 +00:00
const port = ports[0];
const protocol = port.Protocol;
2021-02-22 10:22:35 +00:00
2020-01-29 16:01:39 +00:00
services.push({
2020-01-28 13:57:24 +00:00
name,
2021-02-22 10:22:35 +00:00
port: port.PublishedPort,
protocol,
2021-11-22 14:45:49 +00:00
isTcp: protocol == 'tcp'
2020-01-29 16:01:39 +00:00
});
2020-01-28 13:57:24 +00:00
}
2021-11-22 14:45:49 +00:00
services.sort(sortFn);
2020-01-28 13:57:24 +00:00
2021-11-22 14:45:49 +00:00
const configString = template({
services,
nodes,
2021-11-22 16:02:55 +00:00
info
2021-11-22 14:45:49 +00:00
});
2021-01-15 17:19:30 +00:00
// Cheking settings hash
2021-11-22 14:45:49 +00:00
const infoHash = shajs('sha256')
2021-01-15 17:19:30 +00:00
.update(configString)
.digest('hex');
console.log('Settings hash:', infoHash);
2021-11-22 16:02:55 +00:00
if (lastInfoHash == infoHash && !firstRun) {
console.log(`Settings haven't changed, aborting.`);
return;
}
// Creating configuration file
2021-11-22 14:45:49 +00:00
const tmpConf = `${tmpDir}/config.cfg`;
2020-01-27 16:25:39 +00:00
fs.writeFileSync(tmpConf, configString);
if (conf.debug) {
2021-11-22 14:45:49 +00:00
const delimiter = '#' + '='.repeat(79);
console.log(delimiter);
console.log(`# ${confDir}`);
2020-01-27 16:25:39 +00:00
console.log(delimiter);
console.log(configString);
console.log(delimiter);
}
// Updating reverse proxies
2021-11-22 14:45:49 +00:00
const files = {
2020-01-27 16:25:39 +00:00
local: tmpConf,
2021-11-22 14:45:49 +00:00
remote: `${confDir}/haproxy.cfg`
2020-01-27 16:25:39 +00:00
};
2021-11-22 14:45:49 +00:00
for (const host of conf.rproxy.hosts) {
2020-01-27 16:25:39 +00:00
console.log(`Updating host: ${host}`);
2020-01-28 13:57:24 +00:00
if (!isProduction) continue;
2021-11-22 14:45:49 +00:00
const sshClient = new ssh();
2020-01-28 13:57:24 +00:00
await sshClient.connect(Object.assign({host}, conf.rproxy.auth));
2020-01-27 16:25:39 +00:00
await sshClient.putFiles([files]);
2021-11-22 16:02:55 +00:00
if (firstRun)
await sshClient.putDirectory(
`${tmpDir}/maps`,
`${confDir}/maps`,
{recursive: true}
);
2020-01-28 13:57:24 +00:00
if (conf.rproxy.reloadCmd)
2020-01-28 15:43:43 +00:00
await sshClient.exec(conf.rproxy.reloadCmd);
2020-01-27 16:25:39 +00:00
await sshClient.dispose();
}
2020-02-06 10:32:09 +00:00
// Saving applied config hash
lastInfoHash = infoHash;
fs.writeFileSync(hashFile, infoHash);
2020-01-27 16:25:39 +00:00
console.log('Configuration updated.');
}
(async() => {
console.log('Initializing.');
2020-01-27 16:25:39 +00:00
let timeoutId;
docker = new Docker(conf.docker);
template = handlebars.compile(fs.readFileSync('rproxy.handlebars', 'utf8'));
try {
fs.mkdirSync(tmpDir);
} catch (err) {
if (err.code != 'EEXIST') throw err;
}
if (fs.existsSync(hashFile)) {
lastInfoHash = fs.readFileSync(hashFile, 'utf8');
console.log('Saved settings hash:', lastInfoHash);
}
2021-11-22 17:37:29 +00:00
// Fetch backends
2021-11-22 14:45:49 +00:00
const hostMap = [];
const baseMap = [];
const https = [];
const zoneMap = [];
2021-11-22 17:37:29 +00:00
const zones = new Set();
2021-11-22 14:45:49 +00:00
for (const domain in conf.domains) {
const domainConf = conf.domains[domain];
for (const service in domainConf)
addService(service, domainConf[service], domain);
}
function addService(service, serviceConf, mainDomain) {
let rules;
if (typeof serviceConf == 'string') {
rules = serviceConf;
serviceConf = undefined;
}
serviceConf = Object.assign({},
conf.defaults,
serviceConf
);
if (serviceConf.https)
https.push(service);
2021-11-22 17:37:29 +00:00
if (serviceConf.zone) {
2021-11-22 14:45:49 +00:00
zoneMap.push([service, serviceConf.zone]);
2021-11-22 17:37:29 +00:00
zones.add(serviceConf.zone);
}
2021-11-22 14:45:49 +00:00
rules = rules || serviceConf.rules;
if (!rules)
rules = service;
if (!Array.isArray(rules))
rules = [rules];
for (let rule of rules) {
if (typeof rule == 'string')
rule = {domain: rule};
let domains = rule.domain;
let paths = rule.path;
if (!Array.isArray(domains))
domains = [domains];
if (!Array.isArray(paths))
paths = [paths];
for (const domain of domains) {
for (const path of paths) {
const fullDomain = domain && domain !== '$'
? `${domain}.${mainDomain}`
: mainDomain;
if (!path)
hostMap.push([fullDomain, service]);
else
baseMap.push([fullDomain + path, service]);
}}
}
}
2021-11-22 17:37:29 +00:00
// Fetch ACLs
const aclMap = [];
2021-11-22 14:45:49 +00:00
const acls = [];
for (const acl in conf.acls) {
const aclConf = conf.acls[acl];
2021-11-22 17:37:29 +00:00
2021-11-22 14:45:49 +00:00
const ips = [];
for (const ip of aclConf.ips) {
aclMap.push([ip, acl]);
ips.push(parseNet(ip));
}
2021-11-22 17:37:29 +00:00
2021-11-22 14:45:49 +00:00
acls.push({
name: acl,
ips,
zones: aclConf.zones === 'all'
2021-11-22 17:37:29 +00:00
? new Set(zones)
: new Set(aclConf.zones)
2021-11-22 14:45:49 +00:00
});
}
function parseNet(net) {
const netSplit = net.split('/');
const mask = parseInt(netSplit[1]);
const ip = netSplit[0].split('.')
.reduce((ipInt, octet) => (ipInt<<8) + parseInt(octet, 10), 0) >>> 0;
return {ip, mask};
}
for (const aAcl of acls) {
for (const aNet of aAcl.ips) {
for (const bAcl of acls) {
if (aAcl === bAcl) continue;
let match = false;
for (const bNet of bAcl.ips) {
match = bNet.mask === 0;
2021-11-22 17:55:37 +00:00
if (bNet.mask > 0 && bNet.mask <= aNet.mask) {
2021-11-22 14:45:49 +00:00
const netMask = (~0) << (32 - bNet.mask);
const aSubnet = aNet.ip & netMask;
const bSubnet = bNet.ip & netMask;
match = aSubnet === bSubnet;
}
if (match) break;
}
if (match) {
for (const zone of bAcl.zones)
aAcl.zones.add(zone);
}
}}}
2021-11-22 17:37:29 +00:00
const accessMap = [];
2021-11-22 14:45:49 +00:00
for (const acl of acls)
for (const zone of acl.zones)
accessMap.push(`${acl.name}/${zone}`);
2021-11-22 17:37:29 +00:00
// Generate maps
2021-11-22 14:45:49 +00:00
const files = {
host: hostMap,
base: baseMap,
zone: zoneMap,
acl: aclMap,
access: accessMap,
https: https
};
function strSortFn(a, b) {
return a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0;
}
function netSortFn(a, b) {
const aMask = parseInt(a[0].split('/')[1], 10);
const bMask = parseInt(b[0].split('/')[1], 10);
return bMask - aMask;
}
2021-11-22 16:30:54 +00:00
const mapDir = `${tmpDir}/maps`;
2021-11-22 15:00:37 +00:00
if (await fs.pathExists(mapDir))
await fs.remove(mapDir, {recursive: true});
await fs.mkdir(mapDir);
2021-11-22 14:45:49 +00:00
for (const file in files) {
files[file].sort(file == 'acl'
? netSortFn
: strSortFn
);
const fd = await fs.open(`${mapDir}/${file}.map`, 'w+');
files[file].forEach(map => {
if (Array.isArray(map))
fs.write(fd, `${map[0]} ${map[1]}\n`);
else
fs.write(fd, `${map}\n`);
});
await fs.close(fd);
}
2021-11-22 16:33:52 +00:00
2021-11-22 17:37:29 +00:00
// Initalize
2021-11-22 16:02:55 +00:00
await updateProxy(true);
2020-01-27 16:25:39 +00:00
2020-01-29 16:01:39 +00:00
console.log('Listening for events.')
2020-01-27 16:25:39 +00:00
docker.getEvents({}, (err, stream) => {
if (err || !stream) {
console.error('Failed to monitor docker host', err);
return;
}
stream.on('data', event => {
event = JSON.parse(event);
if (conf.events && conf.events.indexOf(event.Type) == -1) return;
console.log(`Event: ${event.Type}: ${event.Action}`);
if (timeoutId) return;
2020-01-28 13:57:24 +00:00
timeoutId = setTimeout(async () => {
2020-01-27 16:25:39 +00:00
timeoutId = null;
2020-01-28 13:57:24 +00:00
await updateProxy();
2020-01-27 16:25:39 +00:00
}, conf.delay * 1000);
})
});
})();