forked from github-starred/docker-traefik-labels
233 lines
7.8 KiB
JavaScript
233 lines
7.8 KiB
JavaScript
process.once('SIGTERM', () => process.exit(0));
|
|
process.once('SIGINT', () => process.exit(0));
|
|
|
|
const fs = require('fs');
|
|
const Docker = require('dockerode');
|
|
const redis = require('redis');
|
|
const { nsupdate } = require('./nsupdate');
|
|
const { dig } = require('./dig');
|
|
const { elevenLogJSON } = require('/labels/lib/util.js');
|
|
|
|
const ENV_REDIS_INTERVAL = parseInt(process.env?.LABELS_INTERVAL || 300);
|
|
const ENV_REDIS_TIMEOUT = parseInt(process.env?.LABELS_TIMEOUT|| 30);
|
|
const ENV_LABELS_WEBHOOK = process.env?.LABELS_WEBHOOK;
|
|
const ENV_LABELS_WEBHOOK_AUTH_BASIC = process.env?.LABELS_WEBHOOK_AUTH_BASIC;
|
|
const ENV_LABELS_RFC2136_ONLY_UPDATE_ON_CHANGE = process.env?.LABELS_RFC2136_ONLY_UPDATE_ON_CHANGE || false;
|
|
|
|
elevenLogJSON('info', {config:{
|
|
LABELS_INTERVAL :ENV_REDIS_INTERVAL,
|
|
LABELS_TIMEOUT:ENV_REDIS_TIMEOUT,
|
|
LABELS_WEBHOOK:ENV_LABELS_WEBHOOK,
|
|
LABELS_WEBHOOK_AUTH_BASIC:((ENV_LABELS_WEBHOOK_AUTH_BASIC) ? true: false),
|
|
LABELS_RFC2136_ONLY_UPDATE_ON_CHANGE:ENV_LABELS_RFC2136_ONLY_UPDATE_ON_CHANGE
|
|
}});
|
|
|
|
class Labels{
|
|
#docker;
|
|
#redis;
|
|
#poll = false;
|
|
#webhook = {
|
|
headers:{'Content-Type':'application/json'}
|
|
};
|
|
|
|
constructor(){
|
|
switch(true){
|
|
case fs.existsSync('/run/docker.sock'):
|
|
elevenLogJSON('info', 'connect to Docker socket');
|
|
this.#docker = new Docker({socketPath:'/run/docker.sock'});
|
|
break;
|
|
|
|
case fs.existsSync(`${process.env?.APP_ROOT}/ssl/ca.crt`):
|
|
elevenLogJSON('info', 'connect to Docker API via TLS verify');
|
|
this.#docker = new Docker({
|
|
protocol:'https',
|
|
host:process.env?.LABELS_DOCKER_IP,
|
|
port: process.env.LABELS_DOCKER_PORT || 2376,
|
|
ca:fs.readFileSync(`${process.env?.APP_ROOT}/ssl/ca.crt`),
|
|
cert:fs.readFileSync(`${process.env?.APP_ROOT}/ssl/client.crt`),
|
|
key:fs.readFileSync(`${process.env?.APP_ROOT}/ssl/client.key`)
|
|
});
|
|
break;
|
|
|
|
default:
|
|
elevenLogJSON('error', 'No docker API available, add /run/docker.sock (non-root) or use TLS verify authentication!');
|
|
}
|
|
if(ENV_LABELS_WEBHOOK_AUTH_BASIC){
|
|
this.#webhook.headers['Authorization'] = 'Basic ' + Buffer.from(ENV_LABELS_WEBHOOK_AUTH_BASIC).toString('base64')
|
|
}
|
|
}
|
|
|
|
async watch(){
|
|
this.#redis = await redis.createClient({
|
|
url:process.env.LABELS_REDIS_URL,
|
|
pingInterval:30000,
|
|
socket:{
|
|
rejectUnauthorized: false,
|
|
}
|
|
});
|
|
|
|
this.#redis.connect();
|
|
this.#redis.on('ready', ()=>{
|
|
(async() => {
|
|
await this.dockerPoll();
|
|
})();
|
|
this.dockerEvents();
|
|
});
|
|
|
|
this.#redis.on('error', error =>{
|
|
elevenLogJSON('error', error);
|
|
});
|
|
|
|
setInterval(async() => {
|
|
await this.dockerPoll();
|
|
}, ENV_REDIS_INTERVAL*1000);
|
|
}
|
|
|
|
dockerEvents(){
|
|
this.#docker.getEvents({}, (error, data) => {
|
|
if(error){
|
|
elevenLogJSON('error', error);
|
|
}else{
|
|
data.on('data', async(chunk) => {
|
|
const event = JSON.parse(chunk.toString('utf8'));
|
|
if(/Container/i.test(event?.Type) && /^(start|kill)$/i.test(event?.status)){
|
|
await this.dockerInspect(event.id, event.status);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
async dockerPoll(){
|
|
if(!this.#poll){
|
|
try{
|
|
this.#poll = true;
|
|
this.#docker.listContainers((error, containers) => {
|
|
if(!error){
|
|
containers.forEach(async(container) => {
|
|
await this.dockerInspect(container.Id, 'poll');
|
|
});
|
|
}
|
|
});
|
|
}catch(e){
|
|
elevenLogJSON('error', e);
|
|
}finally{
|
|
this.#poll = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
async dockerInspect(id, status = null){
|
|
return(new Promise((resolve, reject) => {
|
|
const container = this.#docker.getContainer(id);
|
|
container.inspect(async(error, data) => {
|
|
if(!error){
|
|
const update = (/start|poll/i.test(status)) ? true : false;
|
|
const container = {
|
|
name:(data?.Name || data?.id).replace(/^\//i, ''),
|
|
event:status,
|
|
labels:{
|
|
traefik:[],
|
|
rfc2136:[],
|
|
},
|
|
};
|
|
|
|
const rfc2136 = {
|
|
WAN:{server:'', key:'', commands:[]},
|
|
LAN:{server:'', key:'', commands:[]},
|
|
}
|
|
|
|
elevenLogJSON('info', {container:container.name, event:status, method:'inspect'});
|
|
|
|
for(const label in data?.Config?.Labels){
|
|
switch(true){
|
|
case /traefik\//i.test(label):
|
|
if(update){
|
|
await this.#redis.set(label, data.Config.Labels[label], {EX:ENV_REDIS_INTERVAL + ENV_REDIS_TIMEOUT});
|
|
}else{
|
|
await this.#redis.del(label);
|
|
}
|
|
container.labels.traefik[label] = data.Config.Labels[label];
|
|
break;
|
|
|
|
case /rfc2136\//i.test(label):
|
|
container.labels.rfc2136[label] = data.Config.Labels[label];
|
|
const type = ((label.match(/rfc2136\/WAN\//i)) ? 'WAN' : 'LAN');
|
|
switch(true){
|
|
case /rfc2136\/\S+\/server/i.test(label):
|
|
rfc2136[type].server = data.Config.Labels[label];
|
|
break;
|
|
|
|
case /rfc2136\/\S+\/key/i.test(label):
|
|
rfc2136[type].key = data.Config.Labels[label];
|
|
break;
|
|
|
|
default:
|
|
if(!update){
|
|
data.Config.Labels[label] = data.Config.Labels[label].replace(/update add/i, 'update delete');
|
|
}
|
|
rfc2136[type].commands.push(data.Config.Labels[label]);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
for(const type in rfc2136){
|
|
if(rfc2136[type].commands.length > 0 && rfc2136[type].server && rfc2136[type].key){
|
|
if(ENV_LABELS_RFC2136_ONLY_UPDATE_ON_CHANGE){
|
|
for(let i=0; i<rfc2136[type].commands.length; i++){
|
|
if(await this.rfc2136KnownRecord(rfc2136[type].server, rfc2136[type].commands[i])){
|
|
rfc2136[type].commands.splice(i, 1);
|
|
}
|
|
}
|
|
}
|
|
try{
|
|
if(rfc2136[type].commands.length > 0){
|
|
elevenLogJSON('info', {container:container.name, event:status, method:`nsupdate ${rfc2136[type].server}`});
|
|
await nsupdate(rfc2136[type].server, rfc2136[type].key, rfc2136[type].commands);
|
|
}else{
|
|
if(ENV_LABELS_RFC2136_ONLY_UPDATE_ON_CHANGE){
|
|
elevenLogJSON('info', {container:container.name, event:status, method:`nsupdate ${rfc2136[type].server} skipped due to same record data`});
|
|
}
|
|
}
|
|
}catch(e){
|
|
elevenLogJSON('error', e);
|
|
}
|
|
}
|
|
}
|
|
|
|
if(ENV_LABELS_WEBHOOK){
|
|
try{
|
|
await fetch(ENV_LABELS_WEBHOOK, {method:(
|
|
(update) ? 'PUT' : 'DELETE'
|
|
), body:JSON.stringify(container), headers:this.#webhook.headers, signal:AbortSignal.timeout(2500)});
|
|
}catch(e){
|
|
elevenLogJSON('error', {method:'fetch(webhook)', error:e});
|
|
}
|
|
}
|
|
|
|
resolve(true);
|
|
}
|
|
});
|
|
}));
|
|
}
|
|
|
|
async rfc2136KnownRecord(server, nsupdate){
|
|
const matches = nsupdate.match(/update add (\S+) \d+ (\S+) (\S+)/i);
|
|
if(matches && matches.length >= 4){
|
|
try{
|
|
const record = await dig(server, matches[2], matches[1]);
|
|
const match = (
|
|
(record.match(new RegExp(matches[3], 'ig'))) ? true : false
|
|
);
|
|
elevenLogJSON('debug', {method:'rfc2136KnownRecord()', params:{server:server, nsupdate:nsupdate}, match:{A:record, B:matches[3], match:match}});
|
|
return(match);
|
|
}catch(e){
|
|
elevenLogJSON('error', e);
|
|
return(false);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
new Labels().watch(); |