Compare commits

...
45 Commits
Author SHA1 Message Date
ElevenNotes 2e3aa6ea57 exception handling 2024-01-25 14:54:14 +01:00
ElevenNotes 962cd22f26 0.2.0 > latest 2024-01-25 09:19:13 +01:00
ElevenNotes 9f24499f6f verbosity 2024-01-25 09:18:26 +01:00
ElevenNotes 33cf74e52a verbosity 2024-01-24 23:36:32 +01:00
ElevenNotes 543a19f5f0 js-yaml 2024-01-24 22:52:48 +01:00
ElevenNotes 5ad1a0d355 client > server 2024-01-24 22:28:19 +01:00
ElevenNotes 307e7101f8 add TLS defaults 2024-01-23 22:52:36 +01:00
ElevenNotes 6752ebee1a 0.1.10 2024-01-23 22:08:09 +01:00
ElevenNotes 3c2ab08ba9 0.1.9 2024-01-23 22:07:09 +01:00
ElevenNotes 498c0988e5 0.1.10 2024-01-23 22:01:53 +01:00
ElevenNotes 49801cd521 0.1.10 2024-01-23 22:01:10 +01:00
ElevenNotes 66b5bab4ae add TLS auth 2024-01-23 21:58:09 +01:00
ElevenNotes 070f45fae5 add TLS authentication 2024-01-23 15:34:43 +01:00
ElevenNotes 085de10ca1 add debug info 2024-01-23 09:14:35 +01:00
ElevenNotes 85575ce073 add skip info 2024-01-22 17:09:00 +01:00
ElevenNotes b754494a98 debug 2024-01-22 17:05:18 +01:00
ElevenNotes 6ac866ef5e match all TXT 2024-01-22 16:53:33 +01:00
ElevenNotes 426826532d obfuscate key in logs 2024-01-22 16:50:56 +01:00
ElevenNotes 82b132c68e add debug 2024-01-22 16:42:00 +01:00
ElevenNotes 3f89d3ddd9 add debug 2024-01-22 16:33:25 +01:00
ElevenNotes ff8e1d9cd9 0.1.9 > latest 2024-01-22 15:35:11 +01:00
ElevenNotes 2d5ef49071 add dig 2024-01-22 15:33:28 +01:00
ElevenNotes 76a9a7261f log container layout 2024-01-22 15:06:34 +01:00
ElevenNotes 7e67e971ae add dig for on change checking rfc2136 2024-01-22 15:01:37 +01:00
ElevenNotes fad60b7019 latest 2024-01-21 09:08:56 +01:00
ElevenNotes 2eadac07bb add container info 2024-01-21 09:03:17 +01:00
ElevenNotes e129945fde true JSON with elevenLogJSON 2024-01-18 11:42:57 +01:00
ElevenNotes 2d9bfafffa add debug option 2024-01-18 09:37:47 +01:00
ElevenNotes 823151dc98 fix ? on object 2024-01-18 09:28:42 +01:00
ElevenNotes 7bd574c3a3 latest 2024-01-18 09:12:29 +01:00
ElevenNotes e7c3a83ae2 add correct arch and build 2024-01-18 09:09:18 +01:00
ElevenNotes 24a2e20641 fix URL 2024-01-18 08:56:56 +01:00
ElevenNotes 7404380279 typo 😅 2024-01-16 08:35:51 +01:00
ElevenNotes d0b98b201d add rfc2136 update capabilities 2024-01-16 07:33:49 +01:00
ElevenNotes 33ac5c9e4c multi arch 2024-01-02 01:48:55 +01:00
ElevenNotes 0cb3f5d165 add aarch64 2024-01-02 01:41:21 +01:00
ElevenNotes b551e02001 defaults 2023-12-22 23:40:52 +01:00
ElevenNotes fc94b61f62 latest 2023-12-22 07:30:31 +01:00
ElevenNotes 426c69011b refactor 2023-12-22 07:28:22 +01:00
ElevenNotes 064fbaeac6 refactor 2023-12-22 07:12:34 +01:00
ElevenNotes d978cd42c8 0.1.6 2023-12-19 13:56:06 +01:00
ElevenNotes af8343f89a 0.1.6 simplify webhook 2023-12-19 13:55:52 +01:00
ElevenNotes f60f0ad124 0.1.5, remove container restart on kill 2023-12-19 11:44:50 +01:00
ElevenNotes e7202481b3 0.1.4 2023-12-05 08:58:34 +01:00
ElevenNotes ccdf29b7ae add webhook 2023-12-05 08:57:00 +01:00
12 changed files with 503 additions and 147 deletions
+1 -2
View File
@@ -1,2 +1 @@
maintain/
/build
maintain/
+77 -11
View File
@@ -1,14 +1,19 @@
# Alpine :: Traefik Labels
![size](https://img.shields.io/docker/image-size/11notes/traefik-labels/0.1.3?color=0eb305) ![version](https://img.shields.io/docker/v/11notes/traefik-labels?color=eb7a09) ![pulls](https://img.shields.io/docker/pulls/11notes/traefik-labels?color=2b75d6) ![activity](https://img.shields.io/github/commit-activity/m/11notes/docker-traefik-labels?color=c91cb8) ![commit-last](https://img.shields.io/github/last-commit/11notes/docker-traefik-labels?color=c91cb8)
![size](https://img.shields.io/docker/image-size/11notes/traefik-labels/amd64-0.1.10?color=0eb305) ![version](https://img.shields.io/docker/v/11notes/traefik-labels/amd64-0.1.10?color=eb7a09) ![pulls](https://img.shields.io/docker/pulls/11notes/traefik-labels?color=2b75d6) ![activity](https://img.shields.io/github/commit-activity/m/11notes/docker-traefik-labels?color=c91cb8) ![commit-last](https://img.shields.io/github/last-commit/11notes/docker-traefik-labels?color=c91cb8)
Run Traefik Labels based on Alpine Linux. Small, lightweight, secure and fast 🏔️
What can I do with this? Simply put: It will export any traefik labels on a container on the same host as this image runs to a Redis instance. This makes it possible for a centralized Traefik load balancer to update endpoints dynamically by utilizing the docker labels, just like you would on a local installation of Traefik with labels. It is meant as an alternative and simple way to proxy requests from a Traefik load balanacer to multiple docker nodes running in different networks and locations.
## Description
What can I do with this? Simply put: It will export any traefik labels on a container on the same host as this image runs to a Redis instance. This makes it possible for a centralized Traefik load balancer to update endpoints dynamically by utilizing the docker labels, just like you would on a local installation of Traefik with labels. It is meant as an alternative and simple way to proxy requests from a Traefik load balancer to multiple docker nodes running in different networks and locations. The image has the ability to use nsupdate with labels too, so that not only you can add a dynamic proxy to your Traefik, but add DNS entries to your internal or external NS servers. It uses split DNS as WAN and LAN in your labels, so you can update external as well as internal DNS entries on the fly. If a container is removed the image will automatically reverse any `nsupdate update add` to `nsupdate update delete` so entrie are removed too.
In order to use this image, you need to setup Traefik with a Redis provider and then point this image via REDIS_URL to the same Redis instance. Each entry will have an expire timer set in Redis, so that if a container is removed by a server crashing, Redis will automatically remove stale entries as well. Entries are refreshed every 60 seconds or on all docker container events (create, run, kill, stop, restart, ...).
In order to use this image, you need to setup Traefik with a Redis provider and then point this image via REDIS_URL to the same Redis instance. Each entry will have an expire timer set in Redis, so that if a container is removed by a server crashing, Redis will automatically remove stale entries as well. Entries are refreshed every 300 seconds or on all docker container events (create, run, kill, stop, restart, ...). As for nsupdate, you need to setup tsig authentication in your NS servers and add the keys to the zones you want to be able to update, you can restrict the keys by using update-policy if you use BIND.
This image provides the ability to call a webhook for each container for each event or poll after the data was updates in Redis and or nsupdate.
## Volumes
* **/labels/ssl** - Directory of ssl certificates for TLS<sup>1</sup>
## Run
This will export all labels from all containers to the Redis instance specified in LABELS_REDIS_URL from the same host this container is running on.
```shell
docker run --name traefik-labels \
-v /run/docker.sock:/run/docker.sock \
@@ -16,9 +21,27 @@ docker run --name traefik-labels \
-d 11notes/traefik-labels:[tag]
```
This is a demo webserver that will start on :8080, all the traefik labels will be exportet to the Redis instance. They follow the exact same [syntax](https://doc.traefik.io/traefik/routing/providers/kv/) as for normal Redis and Traefik, just as labels.
```shell
docker run --name demo \
docker run --name traefik-labels \
-v /run/docker.sock:/run/docker.sock \
-e LABELS_REDIS_URL="rediss://foo:bar@10.127.198.254:6379/0" \
-e LABELS_WEBHOOK="https://domain.com/traefik/labels" \
-e LABELS_WEBHOOK_AUTH_BASIC="foo:bar" \
-d 11notes/traefik-labels:[tag]
```
```shell
docker run --name traefik-labels \
-v .../ssl:/labels/ssl \
-e LABELS_REDIS_URL="rediss://foo:bar@10.127.198.254:6379/0" \
-e LABELS_WEBHOOK="https://domain.com/traefik/labels" \
-e LABELS_WEBHOOK_AUTH_BASIC="foo:bar" \
-d 11notes/traefik-labels:[tag]
```
## Examples
```shell
docker run --name traefik-demo \
-p 8080:8080 \
-l "traefik/http/routers/demo.domain.com/service=demo.domain.com" \
-l "traefik/http/routers/demo.domain.com/rule=Host(`demo.domain.com`)" \
@@ -28,6 +51,24 @@ docker run --name demo \
-d 11notes/nginx:stable
```
```shell
docker run --name traefik-rfc2136-demo \
-p 8080:8080 \
-l "traefik/http/routers/demo.domain.com/service=demo.domain.com" \
-l "traefik/http/routers/demo.domain.com/rule=Host(`demo.domain.com`)" \
-l "traefik/http/routers/demo.domain.com/tls=true" \
-l "traefik/http/routers/demo.domain.com/entrypoints=https" \
-l "traefik/http/services/demo.domain.com/loadbalancer/servers/0/url=http://fqdn-of-docker-node:8080" \
-l "rfc2136/WAN/server=ns.domain.com" \
-l "rfc2136/WAN/key=algo:name:secret" \
-l "rfc2136/WAN/nsupdate=update add foo.domain.com 300 A 175.12.41.11" \
-l "rfc2136/WAN/nsupdate=update add foo.domain.com 300 TXT \"hello from traefik-labels!\"" \
-l "rfc2136/LAN/server=ns.domain.local" \
-l "rfc2136/LAN/key=algo:name:secret" \
-l "rfc2136/LAN/nsupdate=update add foo.domain.local 300 A 192.168.12.54" \
-d 11notes/nginx:stable
```
## Defaults
| Parameter | Value | Description |
| --- | --- | --- |
@@ -35,18 +76,29 @@ docker run --name demo \
| `uid` | 1000 | user id 1000 |
| `gid` | 1000 | group id 1000 |
| `home` | /labels | home directory of user docker |
| `api` | https://${IP}:5000 | HTTPS endpoint of Docker registry |
| `ca.crt` | /labels/ssl/ca.crt | Certificate of CA for TLS<sup>1</sup> |
| `labels.crt` | /labels/ssl/labels.crt | Certificate of client for TLS<sup>1</sup> |
| `labels.key` | /labels/ssl/labels.key | Private key of client for TLS<sup>1</sup> |
## Environment
| Parameter | Value | Default |
| --- | --- | --- |
| `TZ` | [Time Zone](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) | |
| `DEBUG` | Show debug information | |
| `LABELS_REDIS_URL` | the redis URL to connect, use rediss:// for SSL | rediss://localhost:6379/0 |
| `LABELS_INTERVAL` | in what interval container information is pulled | 300 |
| `LABELS_TIMEOUT` | how many seconds after an interval the keys should stay till they expire | 30 |
| `LABELS_INTERVAL` | in what interval container information is pulled in seconds | 300 |
| `LABELS_TIMEOUT` | how many seconds after an interval the keys should stay till they expire in seconds | 30 |
| `LABELS_WEBHOOK` | URL to call on each event or poll for each container | |
| `LABELS_WEBHOOK_AUTH_BASIC` | Basic authentication to use in the form of "username:password" for the webhook | |
| `LABELS_RFC2136_ONLY_UPDATE_ON_CHANGE` | Only update DNS entries if they are new or changed (will use dig on each call to the set server) | |
| `LABELS_DOCKER_IP` | Used for TLS<sup>1</sup> authentication against dockerd | localhost |
| `LABELS_DOCKER_PORT` | Used for TLS<sup>1</sup> authentication against dockerd | 2376 |
## Parent image
* [11notes/node:stable](https://github.com/11notes/docker-node)
* [11notes/node:stable](https://hub.docker.com/r/11notes/node)
## Built with and thanks to
## Built with (thanks to)
* [npm::redis](https://www.npmjs.com/package/redis)
* [npm::dockerode](https://www.npmjs.com/package/dockerode)
* [nodejs](https://nodejs.org/en)
@@ -55,4 +107,18 @@ docker run --name demo \
## Tips
* Only use rootless container runtime (podman, rootless docker)
* Don't bind to ports < 1024 (requires root), use NAT/reverse proxy (haproxy, traefik, nginx)
* Do not access docker.sock as root
* Do not access docker.sock as root
* Use TLS if you can't access docker.sock as non-root
## Disclaimers
* <sup>1</sup> For TLS to work you need proper certificates in place for your dockerd and your clients. The CN in the certificate needs to match the FQDN or IP you have set on the docker node, you can set multiple by using SAN. See an example of a daemon.json configuration to enable TLS.
```json
{
"tls": true,
"tlsverify": true,
"tlscacert": "/etc/docker/ca.crt",
"tlscert": "/etc/docker/server.crt",
"tlskey": "/etc/docker/server.key",
"hosts": ["unix:///var/run/docker.sock", "tcp://0.0.0.0:2376"]
}
```
+15 -1
View File
@@ -1,3 +1,11 @@
# :: Util
FROM alpine as util
RUN set -ex; \
apk add --no-cache \
git; \
git clone https://github.com/11notes/util.git;
# :: Header
FROM 11notes/node:stable
ENV APP_ROOT=/labels
@@ -7,13 +15,19 @@
# :: prepare image
RUN set -ex; \
mkdir -p ${APP_ROOT};
mkdir -p ${APP_ROOT}/lib; \
apk --no-cache add \
bind-tools; \
apk --no-cache upgrade;
COPY --from=util /util/node/util.js /labels/lib
# :: install
RUN set -ex; \
cd ${APP_ROOT}; \
npm --save install \
redis@4.6.11 \
js-yaml@4.1.0 \
dockerode@4.0.0;
# :: update image
+52
View File
@@ -0,0 +1,52 @@
# :: Util
FROM alpine as util
RUN set -ex; \
apk add --no-cache \
git; \
git clone https://github.com/11notes/util.git;
# :: Builder
FROM alpine AS qemu
ENV QEMU_URL https://github.com/balena-io/qemu/releases/download/v3.0.0%2Bresin/qemu-3.0.0+resin-aarch64.tar.gz
RUN apk add curl && curl -L ${QEMU_URL} | tar zxvf - -C . && mv qemu-3.0.0+resin-aarch64/qemu-aarch64-static .
# :: Header
FROM arm64v8/node:20.10.0-alpine3.19
COPY --from=qemu qemu-aarch64-static /usr/bin
ENV APP_ROOT=/labels
# :: Run
USER root
# :: prepare image
RUN set -ex; \
mkdir -p ${APP_ROOT}/lib; \
apk --no-cache add \
bind-tools; \
apk --no-cache upgrade;
COPY --from=util /util/node/util.js /labels/lib
# :: install
RUN set -ex; \
cd ${APP_ROOT}; \
npm --save install \
redis@4.6.11 \
js-yaml@4.1.0 \
dockerode@4.0.0;
# :: update image
RUN set -ex; \
apk --no-cache upgrade;
# :: copy root filesystem changes and set correct permissions
COPY ./rootfs /
RUN set -ex; \
chmod +x -R /usr/local/bin; \
chown -R 1000:1000 \
${APP_ROOT};
# :: Start
USER docker
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
+4
View File
@@ -0,0 +1,4 @@
#!/bin/bash
curl -Lo manifest-tool https://github.com/estesp/manifest-tool/releases/download/v0.9.0/manifest-tool-linux-amd64
chmod +x manifest-tool
./manifest-tool push from-spec multi-arch-manifest.yaml
+2
View File
@@ -0,0 +1,2 @@
#!/bin/bash
docker run --rm --privileged multiarch/qemu-user-static:register --reset
+11
View File
@@ -0,0 +1,11 @@
image: 11notes/traefik-labels:latest
manifests:
- image: 11notes/traefik-labels:amd64-0.2.0
platform:
architecture: amd64
os: linux
- image: 11notes/traefik-labels:arm64v8-0.2.0
platform:
architecture: arm64
os: linux
variant: v8
-116
View File
@@ -1,116 +0,0 @@
const Docker = require('dockerode');
const redis = require('redis');
const ERROR = 1;
class Labels{
#docker;
#redis;
#poll = false;
constructor(){
this.#docker = new Docker({socketPath: '/run/docker.sock'});
}
#log(message, error){
console.log(JSON.stringify({
time:new Date().toISOString(),
type:(error === ERROR) ? 'ERROR' : 'INFO',
message:message,
}));
}
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', ()=>{
this.#log('successfully connected to redis database');
(async() => {
await this.dockerPoll();
})();
this.dockerEvents();
});
this.#redis.on('error', error =>{
this.#log(error, ERROR);
});
setInterval(async() => {
await this.dockerPoll();
}, parseInt(process.env.LABELS_INTERVAL)*1000);
}
dockerEvents(){
this.#docker.getEvents({}, (error, data) => {
if(error){
this.#log(error, ERROR);
}else{
data.on('data', async(chunk) => {
const event = JSON.parse(chunk.toString('utf8'));
if(/Container/i.test(event?.Type) && /^(start|stop|restart|kill|die|destroy)$/i.test(event?.status)){
await this.dockerInspect(event.id, event.status);
}
});
}
});
}
async dockerPoll(){
if(!this.#poll){
try{
this.#poll = true;
this.#log(`poll start (interval:${process.env.LABELS_INTERVAL})}`);
this.#docker.listContainers((error, containers) => {
containers.forEach(async(container) => {
await this.dockerInspect(container.Id);
});
});
}catch(e){
this.#log(e, ERROR);
}finally{
this.#log('poll end');
this.#poll = false;
}
}
}
dockerInspect(id, status = null){
return(new Promise((resolve, reject) => {
let log = false;
const container = this.#docker.getContainer(id);
container.inspect(async(error, data) => {
if(error){
reject(error);
}
for(const label in data?.Config?.Labels){
if(/traefik\//i.test(label)){
if(!log){
this.#log(`inspect container ${data.Name.replace(/^\//i, '')}${(
(null === status) ? '' : ` event[${status}]`
)}`);
log = true;
}
switch(true){
case /start|restart/i.test(status):
await this.#redis.set(label, data?.Config?.Labels[label], {EX:parseInt(process.env.LABELS_INTERVAL) + parseInt(process.env.LABELS_TIMEOUT)});
break;
case /stop|kill|die|destroy/i.test(status):
await this.#redis.del(label);
break;
}
}
}
resolve(true);
});
}));
}
}
new Labels().watch();
+27
View File
@@ -0,0 +1,27 @@
const { spawn } = require('node:child_process');
const { elevenLogJSON } = require('/labels/lib/util.js');
exports.dig = async(resolver, type, record) => {
return(new Promise((resolve, reject) => {
const dig = spawn('/usr/bin/dig', ['+short', '+answer', type, record, `@${resolver}`]);
const io = {stdout:'', stderr:''};
dig.stderr.on('data', data => {io.stderr += data.toString()});
dig.stdout.on('data', data => {io.stdout += (data.toString()).replace(/[\r\n]*$/ig, '')});
dig.on('error', error => {reject(error)});
dig.on('close', code =>{
elevenLogJSON('debug', {method:'dig()', params:{resolver:resolver, type:type, record:record}, io:io});
switch(true){
case /no servers could be reached/ig.test(io.stdout): io.stderr += io.stdout; break;
}
if(code === 0){
if(io.stderr.length > 0){
reject(io.stderr);
}else{
resolve(io.stdout);
}
}else{
reject(io.stderr);
}
});
}));
}
+20
View File
@@ -0,0 +1,20 @@
labels:
redis:
url: rediss://foo:bar@10.127.198.254:6379/0
webhook:
url: https://my.cool.webhook/v1
auth:
basic: labels:*****
nodes:
- 192.168.18.12
- 10.14.120.1
rfc2136:
update-only: true
poll:
interval: 300
ping:
interval: 2.5
tls:
ca: /labels/ssl/ca.crt
crt: /labels/ssl/server.crt
key: /labels/ssl/server.key
+261 -17
View File
@@ -1,18 +1,262 @@
process.once('SIGTERM', () => process.exit(0));
process.once('SIGINT', () => process.exit(0));
const { fork } = require('node:child_process');
const child = fork(`${__dirname}/app.js`, [], {
env:{
LABELS_REDIS_URL:process.env?.LABELS_REDIS_URL || 'rediss://localhost:6379/0',
LABELS_INTERVAL:parseInt(process.env?.LABELS_INTERVAL || 300),
LABELS_TIMEOUT:parseInt(process.env?.LABELS_TIMEOUT|| 30),
const fs = require('fs');
const util = require('util');
const Docker = require('dockerode');
const yaml = require('js-yaml');
const redis = require('redis');
const { nsupdate } = require('./nsupdate');
const { dig } = require('./dig');
const { elevenLogJSON } = require('/labels/lib/util.js');
class Labels{
#config = yaml.load(fs.readFileSync(`${process.env.APP_ROOT}/etc/config.yaml`, 'utf8'))?.labels;
#defaults = {
tls:{
ca:`${process.env.APP_ROOT}/ssl/ca.crt`,
crt:`${process.env.APP_ROOT}/ssl/labels.crt`,
key:`${process.env.APP_ROOT}/ssl/labels.key`,
port:2376
},poll:{interval:300}, ping:{interval:2.5}, redis:{url:'rediss://localhost:6379/0'}, rfc2136:{'update-only':false}};
#intervals = {ping:false, poll:false};
#loops = {ping:false, poll:false};
#redis;
#nodes = {};
constructor(){
for(const node of this.#config?.nodes){
this.#nodes[node] = new Docker({
protocol:'https',
host:node,
port:this.#config?.tls?.port || this.#defaults.tls.port,
ca:fs.readFileSync(this.#config?.tls?.ca || this.#defaults.tls.ca),
cert:fs.readFileSync(this.#config?.tls?.crt || this.#defaults.tls.crt),
key:fs.readFileSync(this.#config?.tls?.key || this.#defaults.tls.key),
});
this.#nodes[node].labels = {ping:false, firstConnect:true};
}
if(this.#config?.webhook?.url){
this.#config.webhook.headers = {'Content-Type':'application/json'};
switch(true){
case this.#config?.webhook?.auth?.basic:
this.#config.webhook.headers['Authorization'] = 'Basic ' + Buffer.from(this.#config.webhook.auth.basic).toString('base64');
break;
}
}
}
});
child.on('error', (error) =>{
console.error(error);
process.exit(1);
});
child.on('close', (code) =>{
console.warn(`child process closed with exit code ${code}`);
process.exit(code);
});
async watch(){
this.#redis = await redis.createClient({
url:this.#config?.redis?.url || this.#defaults.redis.url,
pingInterval:30000,
socket:{
rejectUnauthorized: false,
}
});
this.#redis.on('ready', async()=>{
elevenLogJSON('info', `connected to redis`);
await this.#ping();
await this.#poll();
});
this.#redis.on('error', error =>{
elevenLogJSON('error', JSON.stringify({redis:{exception:e.toString()}}));
});
this.#redis.connect();
}
async #ping(){
if(!this.#intervals.ping){
this.#intervals.ping = true;
setInterval(async() => {
if(!this.#loops.ping){
this.#loops.ping = true;
try{
await this.#ping()
}catch(e){
elevenLogJSON('error', JSON.stringify({ping:{exception:e.toString()}}));
}finally{
this.#loops.ping = false;
}
}
}, (this.#config?.ping?.interval || this.#defaults.ping.interval)*1000);
}
for(const node in this.#nodes){
try{
await this.#nodes[node].ping();
if(!this.#nodes[node].labels.ping){
elevenLogJSON('info', `connected to node [${node}]`);
this.#nodes[node].getEvents({}, (error, data) => {
if(!error){
data.on('data', async(chunk) =>{
const event = JSON.parse(chunk.toString('utf8'));
if(/Container/i.test(event?.Type) && /^(start|die)$/i.test(event?.status)){
await this.#inspect(node, event.id, event.status);
}
});
}else{
elevenLogJSON('error', JSON.stringify({getEvents:{exception:error.toString()}}));
}
});
}
this.#nodes[node].labels.ping = true;
}catch(e){
elevenLogJSON('error', JSON.stringify({ping:{exception:e.toString()}}));
if(this.#nodes[node].labels.ping){
elevenLogJSON('warning', `connection to node [${node}] lost!`);
}else if(this.#nodes[node].labels.firstConnect){
this.#nodes[node].labels.firstConnect = false;
elevenLogJSON('warning', `connection to node [${node}] failed!`);
}
this.#nodes[node].labels.ping = false;
}
}
}
async #poll(){
if(!this.#intervals.poll){
this.#intervals.poll = true;
setInterval(async() => {
if(!this.#loops.poll){
this.#loops.poll = true;
try{
await this.#poll()
}catch(e){
elevenLogJSON('error', JSON.stringify({poll:{exception:e.toString()}}));
}finally{
this.#loops.poll = false;
}
}
}, (this.#config?.poll?.interval || this.#defaults.poll.interval)*1000);
}
for(const node in this.#nodes){
try{
await this.#nodes[node].listContainers((error, containers) => {
if(!error){
containers.forEach(async(container) => {
await this.#inspect(node, container.Id, 'poll');
});
}
});
}catch(e){
elevenLogJSON('error', JSON.stringify({listContainers:{exception:e.toString()}}));
}
}
}
async #inspect(node, id, event){
return(new Promise((resolve, reject) => {
const container = this.#nodes[node].getContainer(id);
container.inspect(async(error, data) => {
if(!error){
const container = {
name:(data?.Name || data?.id).replace(/^\//i, ''),
event:event,
run:(/start|poll/i.test(event)) ? true : false,
labels:{
traefik:[],
rfc2136:[],
},
};
elevenLogJSON('info', `[${node}] container [${container.name}] event [${container.event}]`);
const rfc2136 = {
WAN:{server:'', key:'', commands:[]},
LAN:{server:'', key:'', commands:[]},
}
for(const label in data?.Config?.Labels){
switch(true){
case /traefik\//i.test(label):
if(container.run){
await this.#redis.set(label, data.Config.Labels[label], {EX:(this.#config?.poll?.interval || this.#defaults.poll.interval) + 30});
}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(!container.run){
data.Config.Labels[label] = data.Config.Labels[label].replace(/update add/i, 'update delete');
}
rfc2136[type].commands.push(data.Config.Labels[label]);
}
break;
}
}
if(rfc2136.LAN.commands.length > 0 || rfc2136.WAN.commands.length){
await this.#rfc2136(rfc2136);
}
if(this.#config?.webhook?.url){
await this.#webhook(container);
}
}
})
resolve(true);
}));
}
async #rfc2136(rfc2136){
for(const type in rfc2136){
if(rfc2136[type].commands.length > 0 && rfc2136[type].server && rfc2136[type].key){
if(this.#config?.rfc2136?.['update-only'] || this.#defaults?.rfc2136?.['update-only']){
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){
await nsupdate(rfc2136[type].server, rfc2136[type].key, rfc2136[type].commands);
}
}catch(e){
elevenLogJSON('error', JSON.stringify({nsupdate:{exception:e.toString()}}));
}
}
}
}
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]);
return((record.match(new RegExp(matches[3], 'ig'))));
}catch(e){
return(false);
}
}
}
async #webhook(container){
try{
await fetch(this.#config.webhook.url, {method:(
(container.run) ? 'PUT' : 'DELETE'
), body:JSON.stringify(container), headers:this.#config.webhook.headers, signal:AbortSignal.timeout(2500)});
}catch(e){
elevenLogJSON('error', JSON.stringify({webhook:{exception:e.toString()}}));
}
}
}
new Labels().watch();
+33
View File
@@ -0,0 +1,33 @@
const { spawn } = require('node:child_process');
const { elevenLogJSON } = require('/labels/lib/util.js');
exports.nsupdate = async(server, key, commands) => {
commands.unshift(`server ${server}`);
commands.push('send');
commands.push('quit');
return(new Promise((resolve, reject) => {
elevenLogJSON('debug', {method:'nsupdate()', params:{server:server, key:'******', commands:commands}});
const nsupdate = spawn('/usr/bin/nsupdate', ['-y', key]);
const io = {stdout:'', stderr:''};
nsupdate.stderr.on('data', data => {io.stderr += data.toString()});
nsupdate.stdout.on('data', data => {io.stdout += (data.toString()).replace(/[\r\n]*$/ig, '')});
nsupdate.on('error', error => {reject(error)});
nsupdate.on('close', code =>{
if(code === 0){
if(io.stderr.length > 0){
reject(io.stderr);
}else{
resolve(io.stdout);
}
}else{
reject(io.stderr);
}
});
for(const command of commands){
nsupdate.stdin.write(`${command}\n`);
}
nsupdate.stdin.end();
}));
}