Compare commits

..
49 Commits
Author SHA1 Message Date
ElevenNotes 76df7f9c52 muli arch 2024-10-18 11:46:19 +02:00
ElevenNotes 13257cb2d4 multi arch 2024-10-18 11:45:47 +02:00
ElevenNotes 53c4b3b57c drop versions 2024-10-18 11:45:24 +02:00
ElevenNotes f383b448bd add switch for dropping dns records 2024-10-18 11:35:35 +02:00
ElevenNotes c2f6f641a4 add worker node info 2024-02-02 11:36:57 +01:00
ElevenNotes 1670f1d94f 0.3.0 2024-02-02 11:19:10 +01:00
ElevenNotes 2120d6b8b7 main/worker model for better scalability 2024-02-02 11:10:15 +01:00
ElevenNotes 2b98936893 add node timeout 2024-02-01 11:35:39 +01:00
ElevenNotes 16ceb5a17a remove stale nodes 2024-02-01 11:29:08 +01:00
ElevenNotes b18b3f67be interval: 60 2024-02-01 11:23:26 +01:00
ElevenNotes 68d4b66972 dynamic node reloading 2024-02-01 11:20:14 +01:00
ElevenNotes 3c0c04abe5 add yaml node reload 2024-02-01 11:14:33 +01:00
ElevenNotes 53d0fb843d add poll info 2024-02-01 11:02:49 +01:00
ElevenNotes b09f41e04f add initial poll 2024-02-01 10:52:09 +01:00
ElevenNotes c9ca96a547 switch qemu 2024-02-01 10:30:24 +01:00
ElevenNotes 71f5dce8f7 add version and latest 2024-02-01 10:26:28 +01:00
ElevenNotes 9fabbcd86c 0.2.1 2024-02-01 10:23:43 +01:00
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
12 changed files with 637 additions and 189 deletions
+2 -1
View File
@@ -1 +1,2 @@
maintain/ maintain/
project*
+124 -39
View File
@@ -1,33 +1,99 @@
# Alpine :: Traefik Labels ![Banner](https://github.com/11notes/defaults/blob/main/static/img/banner.png?raw=true)
![size](https://img.shields.io/docker/image-size/11notes/labels/latest?color=0eb305) ![version](https://img.shields.io/docker/v/11notes/labels?color=eb7a09) ![pulls](https://img.shields.io/docker/pulls/11notes/labels?color=2b75d6) ![activity](https://img.shields.io/github/commit-activity/m/11notes/docker-labels?color=c91cb8) ![commit-last](https://img.shields.io/github/last-commit/11notes/docker-labels?color=c91cb8)
Run Traefik Labels based on Alpine Linux. Small, lightweight, secure and fast 🏔️ # 🏔️ Alpine - Traefik Labels
![size](https://img.shields.io/docker/image-size/11notes/traefik-labels/latest?color=0eb305) ![version](https://img.shields.io/docker/v/11notes/traefik-labels/latest?color=eb7a09) ![pulls](https://img.shields.io/docker/pulls/11notes/traefik-labels?color=2b75d6)
## Description **Export Traefik labels from any Docker node to a central location**
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 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. # SYNOPSIS
What can I do with this? This image will connect to all your Docker nodes and read their labels. It will then use the labels to update your Traefik configuration in Redis automatically and dynamically on each container start, stop or timeout. It also supports updating your internal and external DNS servers too, so you can use labels for everything. If a container is removed the image will automatically reverse any `nsupdate update add` to `nsupdate update delete` so entries 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 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 from ISC.
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. 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.
## Run Each node gets its own worker thread for better scalability. If you have 10 nodes, you will have 10 worker processes inside the container forked from the main process. Forks die when their node disconnects and use the `ping.interval` to fork new processes indefinitely. As soon as the node is back online, it will always execute a poll on all containers of that node.
```shell
docker run --name traefik-labels \ # VOLUMES
-v /run/docker.sock:/run/docker.sock \ * **/labels/etc** - Directory of config.yaml
-e LABELS_REDIS_URL="rediss://foo:bar@10.127.198.254:6379/0" \ * **/labels/ssl** - Directory of ssl certificates for TLS<sup>1</sup>
-d 11notes/traefik-labels:[tag]
# COMPOSE
```yaml
name: "traefik-labels"
services:
redis:
image: "11notes/redis:7.2.5"
container_name: "redis"
environment:
REDIS_PASSWORD: "**************"
TZ: Europe/Zurich
volumes:
- "redis.etc:/redis/etc"
- "redis.var:/redis/var"
networks:
- redis
restart: always
labels:
image: "11notes/traefik-labels:latest"
container_name: "labels"
environment:
TZ: Europe/Zurich
volumes:
- "labels.etc:/labels/etc"
networks:
- redis
- nodes
restart: always
volumes:
redis.etc:
redis.var:
labels:etc:
networks:
redis:
internal: true
nodes:
``` ```
```shell # EXAMPLES
docker run --name traefik-labels \ ## config /labels/etc/config.yaml
-v /run/docker.sock:/run/docker.sock \ ```yaml
-e LABELS_REDIS_URL="rediss://foo:bar@10.127.198.254:6379/0" \ labels:
-e LABELS_WEBHOOK="https://domain.com/traefik/labels" \ redis:
-e LABELS_WEBHOOK_AUTH_BASIC="foo:bar" \ url: rediss://foo:bar@10.127.198.254:6379/0
-d 11notes/traefik-labels:[tag] webhook:
url: https://my.cool.webhook/v1
# optional
auth:
# supports basic authentication
basic: labels:*****
nodes:
# use FQDN and add the FQDN to your certificates SAN list (or IP)
- 192.168.18.12
- 10.14.120.1
# reload labels.nodes every {n} seconds (0 = disabled)
interval: 0
# timeout in seconds for the connection to a Docker node
timeout: 2.5
rfc2136:
# only nsupdate on entries which are different (do not update same data)
verify: false
# remove DNS entry if container is unreachable
remove: false
poll:
# polling all containers on a node every {n} seconds
interval: 300
ping:
# ping all nodes every {n} seconds to see if they are still online
interval: 10
tls:
# path for TLS certificates
ca: /labels/ssl/ca.crt
crt: /labels/ssl/server.crt
key: /labels/ssl/server.key
``` ```
## Examples ## container on a node with Traefik routes
```shell ```shell
docker run --name traefik-demo \ docker run --name traefik-demo \
-p 8080:8080 \ -p 8080:8080 \
@@ -39,6 +105,7 @@ docker run --name traefik-demo \
-d 11notes/nginx:stable -d 11notes/nginx:stable
``` ```
## container on a node with Traefik routes and rfc2136 DNS
```shell ```shell
docker run --name traefik-rfc2136-demo \ docker run --name traefik-rfc2136-demo \
-p 8080:8080 \ -p 8080:8080 \
@@ -49,43 +116,61 @@ docker run --name traefik-rfc2136-demo \
-l "traefik/http/services/demo.domain.com/loadbalancer/servers/0/url=http://fqdn-of-docker-node:8080" \ -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/server=ns.domain.com" \
-l "rfc2136/WAN/key=algo:name:secret" \ -l "rfc2136/WAN/key=algo:name:secret" \
-l "rfc2136/WAN/nsupdate=update add foo.domain.com A 175.12.41.11" \ -l "rfc2136/WAN/nsupdate=update add foo.domain.com 300 A 175.12.41.11" \
-l "rfc2136/WAN/nsupdate=update add foo.domain.com TXT \"hello from traefik-labels!\"" \ -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/server=ns.domain.local" \
-l "rfc2136/LAN/key=algo:name:secret" \ -l "rfc2136/LAN/key=algo:name:secret" \
-l "rfc2136/LAN/nsupdate=update add foo.domain.local A 192.168.12.54" \ -l "rfc2136/LAN/nsupdate=update add foo.domain.local 300 A 192.168.12.54" \
-d 11notes/nginx:stable -d 11notes/nginx:stable
``` ```
## Defaults # DEFAULT SETTINGS
| Parameter | Value | Description | | Parameter | Value | Description |
| --- | --- | --- | | --- | --- | --- |
| `user` | docker | user docker | | `user` | docker | user docker |
| `uid` | 1000 | user id 1000 | | `uid` | 1000 | user id 1000 |
| `gid` | 1000 | group id 1000 | | `gid` | 1000 | group id 1000 |
| `home` | /labels | home directory of user docker | | `home` | /labels | home directory of user docker |
| `api` | https://${IP}:5000 | HTTPS endpoint of Docker registry | | `config` | /labels/etc/config.yaml | config (nodes can be updated dynamically) |
| `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 # ENVIRONMENT
| Parameter | Value | Default | | Parameter | Value | Default |
| --- | --- | --- | | --- | --- | --- |
| `TZ` | [Time Zone](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) | null | | `TZ` | [Time Zone](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) | |
| `LABELS_REDIS_URL` | the redis URL to connect, use rediss:// for SSL | rediss://localhost:6379/0 | | `DEBUG` | Show debug information | |
| `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 | |
## Parent image # SOURCE
* [11notes/traefik-labels:latest](https://github.com/11notes/docker-traefik-labels/tree/latest)
# PARENT IMAGE
* [11notes/node:stable](https://hub.docker.com/r/11notes/node) * [11notes/node:stable](https://hub.docker.com/r/11notes/node)
## Built with (thanks to) # BUILT WITH
* [npm::redis](https://www.npmjs.com/package/redis) * [npm::redis](https://www.npmjs.com/package/redis)
* [npm::dockerode](https://www.npmjs.com/package/dockerode) * [npm::dockerode](https://www.npmjs.com/package/dockerode)
* [nodejs](https://nodejs.org/en) * [nodejs](https://nodejs.org/en)
* [Alpine Linux](https://alpinelinux.org) * [alpine](https://alpinelinux.org)
## Tips # TIPS
* Only use rootless container runtime (podman, rootless docker) * Use a reverse proxy like Traefik, Nginx to terminate TLS with a valid certificate
* Don't bind to ports < 1024 (requires root), use NAT/reverse proxy (haproxy, traefik, nginx) * Use Let’s Encrypt certificates to protect your SSL endpoints
* Do not access docker.sock as 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"]
}
```
# ElevenNotes<sup>™️</sup>
This image is provided to you at your own risk. Always make backups before updating an image to a new version. Check the changelog for breaking changes. You can find all my repositories on [github](https://github.com/11notes).
+4 -2
View File
@@ -8,7 +8,6 @@
# :: Header # :: Header
FROM 11notes/node:stable FROM 11notes/node:stable
COPY --from=util /util/node/util.js /labels/lib
ENV APP_ROOT=/labels ENV APP_ROOT=/labels
# :: Run # :: Run
@@ -16,16 +15,19 @@
# :: prepare image # :: prepare image
RUN set -ex; \ RUN set -ex; \
mkdir -p ${APP_ROOT}; \ mkdir -p ${APP_ROOT}/lib; \
apk --no-cache add \ apk --no-cache add \
bind-tools; \ bind-tools; \
apk --no-cache upgrade; apk --no-cache upgrade;
COPY --from=util /util/node/util.js /labels/lib
# :: install # :: install
RUN set -ex; \ RUN set -ex; \
cd ${APP_ROOT}; \ cd ${APP_ROOT}; \
npm --save install \ npm --save install \
redis@4.6.11 \ redis@4.6.11 \
js-yaml@4.1.0 \
dockerode@4.0.0; dockerode@4.0.0;
# :: update image # :: update image
+8 -8
View File
@@ -6,15 +6,12 @@
git; \ git; \
git clone https://github.com/11notes/util.git; git clone https://github.com/11notes/util.git;
# :: Builder # :: QEMU
FROM alpine AS qemu FROM multiarch/qemu-user-static:x86_64-aarch64 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 # :: Header
FROM arm64v8/node:20.10.0-alpine3.19 FROM --platform=linux/arm64 11notes/node:stable
COPY --from=util /util/node/util.js /labels/lib COPY --from=qemu /usr/bin/qemu-aarch64-static /usr/bin
COPY --from=qemu qemu-aarch64-static /usr/bin
ENV APP_ROOT=/labels ENV APP_ROOT=/labels
# :: Run # :: Run
@@ -22,16 +19,19 @@
# :: prepare image # :: prepare image
RUN set -ex; \ RUN set -ex; \
mkdir -p ${APP_ROOT}; \ mkdir -p ${APP_ROOT}/lib; \
apk --no-cache add \ apk --no-cache add \
bind-tools; \ bind-tools; \
apk --no-cache upgrade; apk --no-cache upgrade;
COPY --from=util /util/node/util.js /labels/lib
# :: install # :: install
RUN set -ex; \ RUN set -ex; \
cd ${APP_ROOT}; \ cd ${APP_ROOT}; \
npm --save install \ npm --save install \
redis@4.6.11 \ redis@4.6.11 \
js-yaml@4.1.0 \
dockerode@4.0.0; dockerode@4.0.0;
# :: update image # :: update image
+33
View File
@@ -0,0 +1,33 @@
name: "traefik-labels"
services:
redis:
image: "11notes/redis:7.2.5"
container_name: "redis"
environment:
REDIS_PASSWORD: "**************"
TZ: Europe/Zurich
volumes:
- "redis.etc:/redis/etc"
- "redis.var:/redis/var"
networks:
- redis
restart: always
labels:
image: "11notes/traefik-labels:latest"
container_name: "labels"
environment:
TZ: Europe/Zurich
volumes:
- "labels.etc:/labels/etc"
networks:
- redis
- nodes
restart: always
volumes:
redis.etc:
redis.var:
labels:etc:
networks:
redis:
internal: true
nodes:
-2
View File
@@ -1,2 +0,0 @@
#!/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-latest
platform:
architecture: amd64
os: linux
- image: 11notes/traefik-labels:arm64v8-latest
platform:
architecture: arm64
os: linux
variant: v8
+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);
}
});
}));
}
+33
View File
@@ -0,0 +1,33 @@
labels:
redis:
url: rediss://foo:bar@10.127.198.254:6379/0
webhook:
url: https://my.cool.webhook/v1
# optional
auth:
# supports basic authentication
basic: labels:*****
nodes:
# use FQDN and add the FQDN to your certificates SAN list (or IP)
- 192.168.18.12
- 10.14.120.1
# reload labels.nodes every {n} seconds (0 = disabled)
interval: 0
# timeout in seconds for the connection to a Docker node
timeout: 2.5
rfc2136:
# only nsupdate on entries which are different (do not update same data)
verify: false
# remove DNS entry if container is unreachable
remove: false
poll:
# polling all containers on a node every {n} seconds
interval: 300
ping:
# ping all nodes every {n} seconds to see if they are still online
interval: 10
tls:
# path for TLS certificates
ca: /labels/ssl/ca.crt
crt: /labels/ssl/server.crt
key: /labels/ssl/server.key
+233 -130
View File
@@ -1,173 +1,276 @@
process.once('SIGTERM', () => process.exit(0)); const { fork } = require('node:child_process');
process.once('SIGINT', () => process.exit(0)); const fs = require('fs');
const yaml = require('js-yaml');
const Docker = require('dockerode');
const redis = require('redis'); const redis = require('redis');
const { nsupdate } = requore('./nsupdate'); const { nsupdate } = require('./nsupdate');
const { logJSON } = require('/labels/lib/util.js'); const { dig } = require('./dig');
const { elevenLogJSON } = require('/labels/lib/util.js');
process
.on('unhandledRejection', (e, p) => {
elevenLogJSON('error', {unhandledRejection:e.toString()});
})
.on('uncaughtException', e => {
elevenLogJSON('error', {uncaughtException:e.toString()});
});
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;
class Labels{ class Labels{
#docker; #config = {webhook:{headers:{'Content-Type':'application/json'}}};
#workers = {};
#interval = {run:false, fok:false};
#redis; #redis;
#poll = false;
#webhook = {
headers:{'Content-Type':'application/json'}
};
constructor(){ constructor(){
this.#docker = new Docker({socketPath:'/run/docker.sock'}); const config = yaml.load(fs.readFileSync(`${process.env.APP_ROOT}/etc/config.yaml`, 'utf8'))?.labels;
if(ENV_LABELS_WEBHOOK_AUTH_BASIC){ this.#config.redis = {url:(config?.redis?.url || 'rediss://localhost:6379/0')};
this.#webhook.headers['Authorization'] = 'Basic ' + Buffer.from(ENV_LABELS_WEBHOOK_AUTH_BASIC).toString('base64') this.#config.webhook.url = (config?.webhook?.url || null);
if(this.#config.webhook.url && config?.webhook?.auth?.basic){
this.#config.webhook.headers['Authorization'] = 'Basic ' + Buffer.from(config.webhook.auth.basic).toString('base64');
elevenLogJSON('info', `using webhook ${this.#config.webhook.url} with basic authentication`);
}
this.#config.rfc2136 = {verify:(config?.rfc2136?.verify || false), remove:(config?.rfc2136?.remove || false)};
this.#config.poll = {interval:(config?.poll?.interval || 300)};
this.#config.ping = {interval:(config?.ping?.interval || 2.5)};
this.#config.port = (config?.port || 2376);
this.#config.timeout = (config?.timeout || 5);
this.#config.interval = (config?.interval || 0);
this.#config.tls = {
ca:(config?.tls?.ca || `${process.env.APP_ROOT}/ssl/ca.crt`),
crt:(config?.tls?.crt || `${process.env.APP_ROOT}/ssl/labels.crt`),
key:(config?.tls?.key || `${process.env.APP_ROOT}/ssl/labels.key`),
};
if(this.#config.interval > 0){
elevenLogJSON('info', `${process.env.APP_ROOT}/etc/config.yaml will reload labels.nodes every ${this.#config.interval}s`);
} }
} }
async watch(){ async run(){
if(ENV_LABELS_WEBHOOK){
logJSON('info', `using webhook ${ENV_LABELS_WEBHOOK}`);
}
this.#redis = await redis.createClient({ this.#redis = await redis.createClient({
url:process.env.LABELS_REDIS_URL, url:this.#config.redis.url,
pingInterval:30000, pingInterval:30000,
socket:{ socket:{
rejectUnauthorized: false, rejectUnauthorized:false,
} },
disableOfflineQueue:false,
commandsQueueMaxLength:1024
}); });
this.#redis.connect(); this.#redis.on('ready', async()=>{
this.#redis.on('ready', ()=>{ elevenLogJSON('info', `connected to redis`);
logJSON('info', 'successfully connected to redis');
(async() => { this.#run();
await this.dockerPoll();
})(); if(this.#config.interval > 0){
this.dockerEvents(); setInterval(async ()=>{
if(!this.#interval.run){
this.#interval.run = true;
try{
await this.#run();
}catch(e){
elevenLogJSON('error', {error:e});
}finally{
this.#interval.run = false;
}
}
}, this.#config.interval*1000);
}
setInterval(async()=>{
if(!this.#interval.fork){
this.#interval.fork = true;
try{
await this.#fork();
}catch(e){
elevenLogJSON('error', {error:e});
}finally{
this.#interval.fork = false;
}
}
}, this.#config.ping.interval*1000);
}); });
this.#redis.on('error', error =>{ this.#redis.on('error', error =>{
logJSON('error', error); elevenLogJSON('error', {redis:error.toString()});
}); });
setInterval(async() => { this.#redis.connect();
await this.dockerPoll();
}, ENV_REDIS_INTERVAL*1000);
} }
dockerEvents(){ #run(){
this.#docker.getEvents({}, (error, data) => { const nodes = yaml.load(fs.readFileSync(`${process.env.APP_ROOT}/etc/config.yaml`, 'utf8'))?.labels?.nodes;
if(error){ for(const node of nodes){
logJSON('error', error); if(!this.#workers[node]){
}else{ this.#workers[node] = new Worker(this.#config, node, this);
data.on('data', async(chunk) => { this.#workers[node].fork();
const event = JSON.parse(chunk.toString('utf8')); elevenLogJSON('info', `created new worker for node [${node}]`);
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){
logJSON('error', e);
}finally{
this.#poll = false;
} }
} }
} }
async dockerInspect(id, status = null){ async #fork(){
return(new Promise((resolve, reject) => { for(const node in this.#workers){
const container = this.#docker.getContainer(id); if(!this.#workers[node].run){
container.inspect(async(error, data) => { await this.#workers[node].fork();
if(!error){ if(!this.#workers[node].log.disconnect){
const update = (/start|poll/i.test(status)) ? true : false; this.#workers[node].log.disconnect = true;
const container = { elevenLogJSON('info', `trying to fork existing worker for node [${node}] after disconnect`);
event:status, }
labels:{ }
traefik:[], }
rfc2136:[], }
},
};
const rfc2136 = { async inspect(container){
WAN:{server:'', key:'', commands:[]}, try{
LAN:{server:'', key:'', commands:[]}, const counter = {
} add:0,
del:0,
};
const rfc2136 = {
WAN:{server:'', key:'', commands:[]},
LAN:{server:'', key:'', commands:[]},
}
for(const label in data?.Config?.Labels){ for(const label in container.labels){
switch(true){
case /traefik\//i.test(label):
if(container.start){
counter.add++;
await this.#redis.set(label, container.labels[label], {EX:this.#config.poll.interval + 30});
}else{
counter.del++;
await this.#redis.del(label);
}
break;
case /rfc2136\//i.test(label):
const type = ((label.match(/rfc2136\/WAN\//i)) ? 'WAN' : 'LAN');
switch(true){ switch(true){
case /traefik\//i.test(label): case /rfc2136\/\S+\/server/i.test(label):
if(update){ rfc2136[type].server = container.labels[label];
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; break;
case /rfc2136\//i.test(label): case /rfc2136\/\S+\/key/i.test(label):
container.labels.rfc2136[label] = data?.Config?.Labels[label]; rfc2136[type].key = container.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; break;
default:
if(!container.start && this.#config.rfc2136.remove){
container.labels[label] = container.labels[label].replace(/update add/i, 'update delete');
}
rfc2136[type].commands.push(container.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);
}
}catch(e){
elevenLogJSON('error', {inspect:e.toString(), exception:e});
}
}
async #rfc2136(rfc2136){
for(const type in rfc2136){
if(rfc2136[type].commands.length > 0 && rfc2136[type].server && rfc2136[type].key){
if(this.#config.rfc2136.verify){
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);
} }
} }
}
for(const type in rfc2136){ try{
if(rfc2136[type].commands.length > 0 && rfc2136[type].server && rfc2136[type].key){ if(rfc2136[type].commands.length > 0){
try{ await nsupdate(rfc2136[type].server, rfc2136[type].key, rfc2136[type].commands);
await nsupdate(rfc2136[type].server, rfc2136[type].key, rfc2136[type].commands);
}catch(e){
logJSON('error', e);
}
}
} }
}catch(e){
elevenLogJSON('error', {nsupdate:{exception:e.toString()}});
}
}
}
}
if(ENV_LABELS_WEBHOOK){ async #rfc2136knownRecord(server, nsupdate){
try{ const matches = nsupdate.match(/update add (\S+) \d+ (\S+) (\S+)/i);
await fetch(ENV_LABELS_WEBHOOK, {method:( if(matches && matches.length >= 4){
(update) ? 'PUT' : 'DELETE' try{
), body:JSON.stringify(container), headers:this.#webhook.headers, signal:AbortSignal.timeout(2500)}); const record = await dig(server, matches[2], matches[1]);
}catch(e){ return((record.match(new RegExp(matches[3], 'ig'))));
logJSON('error', e); }catch(e){
} return(false);
}
}
}
async #webhook(container){
try{
await fetch(this.#config.webhook.url, {method:(
(container.start) ? 'PUT' : 'DELETE'
), body:JSON.stringify(container), headers:this.#config.webhook.headers, signal:AbortSignal.timeout(2500)});
}catch(e){
elevenLogJSON('error', {webhook:{exception:e.toString()}});
}
}
}
class Worker{
run = false;
#fork;
#config;
#parent;
log = {
disconnect:false,
}
constructor(config, node, parent){
this.#config = {
tls:config.tls,
poll:config.poll.interval,
ping:config.ping.interval,
port:config.port,
node:node,
};
this.#parent = parent;
}
async fork(){
return(new Promise((resolve, reject) => {
this.#fork = fork(`${process.env.APP_ROOT}/worker.js`, [JSON.stringify(this.#config)], {stdio: 'inherit'});
this.#fork.on('spawn', () =>{
this.run = true;
resolve();
});
this.#fork.on('error', () =>{
this.run = false;
reject();
});
this.#fork.on('close', (code) =>{
this.run = false;
});
this.#fork.on('message', (message) =>{
if(message.error){
if(!this.log.disconnect){
elevenLogJSON('error', {fork:message.error});
}
}else{
this.run = true;
this.log.disconnect = false;
if(message?.labels){
this.#parent.inspect.apply(this.#parent, [message]);
} }
resolve(true);
} }
}); });
})); }));
} }
} }
new Labels().watch(); new Labels().run();
+13 -7
View File
@@ -1,22 +1,28 @@
const { spawn } = require('node:child_process'); const { spawn } = require('node:child_process');
const { elevenLogJSON } = require('/labels/lib/util.js');
exports.nsupdate = async(server, key, commands) => { exports.nsupdate = async(server, key, commands) => {
commands.unshift(`server ${server}`); commands.unshift(`server ${server}`);
commands.push('send'); commands.push('send');
commands.push('quit');
return(new Promise((resolve, reject) => { return(new Promise((resolve, reject) => {
elevenLogJSON('debug', {method:'nsupdate()', params:{server:server, key:'******', commands:commands}});
const nsupdate = spawn('/usr/bin/nsupdate', ['-y', key]); const nsupdate = spawn('/usr/bin/nsupdate', ['-y', key]);
let errors = ''; const io = {stdout:'', stderr:''};
nsupdate.stderr.on('data', data =>{errors += data.toString()}); 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('error', error => {reject(error)});
nsupdate.on('exit', code =>{ nsupdate.on('close', code =>{
if(code === 0){ if(code === 0){
if(errors.length > 0){ if(io.stderr.length > 0){
reject(errors); reject(io.stderr);
}else{ }else{
resolve(true); resolve(io.stdout);
} }
}else{ }else{
reject(errors); reject(io.stderr);
} }
}); });
for(const command of commands){ for(const command of commands){
+149
View File
@@ -0,0 +1,149 @@
const fs = require('fs');
const Docker = require('dockerode');
const args = process.argv.slice(2);
process
.on('unhandledRejection', (e, p) => {
console.error(e, p);
process.send({error:e.toString()});
})
.on('uncaughtException', e => {
console.error(e);
process.send({error:e.toString()});
});
class Worker{
#config;
#docker;
#interval = {poll:false, ping:false};
constructor(config){
this.#config = config;
this.#docker = new Docker({
protocol:'https',
host:this.#config.node,
port:this.#config.port,
ca:fs.readFileSync(this.#config.tls.ca),
cert:fs.readFileSync(this.#config.tls.crt),
key:fs.readFileSync(this.#config.tls.key),
timeout:parseInt(this.#config.timeout*1000)
});
}
async run(){
try{
this.#docker.getEvents({}, (error, data) => {
if(!error){
data.on('data', async(chunk) =>{
const response = chunk.toString('utf8').replace(',"ti"', '');
try{
const event = JSON.parse(response);
if(/Container/i.test(event?.Type) && /^(start|die)$/i.test(event?.status)){
this.#labels(event.id, event.status);
}
}catch(e){
//JSON parse error
}
});
}else{
process.send({error:error.toString()});
}
});
setInterval(async()=>{
if(!this.#interval.poll){
this.#interval.poll = true;
try{
await this.#poll();
}catch(e){
process.send({error:e.toString()});
}finally{
this.#interval.poll = false;
}
}
}, this.#config.poll*1000);
setInterval(async()=>{
if(!this.#interval.ping){
this.#interval.ping = true;
try{
await this.#ping();
}catch(e){
process.send({error:e.toString()});
}finally{
this.#interval.ping = false;
}
}
}, this.#config.ping*1000);
await this.#ping();
await this.#poll();
}catch(e){
process.send({error:e.toString()});
}
}
async #poll(){
try{
await this.#docker.listContainers((error, containers) => {
if(!error){
containers.forEach(async(container) => {
this.#labels(container.Id, 'poll');
});
}else{
process.send({error:e.toString()});
}
});
}catch(e){
process.send({error:e.toString()});
}
}
async #ping(){
try{
await this.#docker.ping();
}catch(e){
process.exit(1);
}
}
async #labels(id, event){
try{
const container = this.#docker.getContainer(id);
container.inspect(async(error, data) => {
if(!error){
const result = {
name:(data?.Name || data?.id).replace(/^\//i, ''),
event:event,
labels:{},
start:(/start|poll/i.test(event)) ? true : false,
worker:{
node:this.#config.node,
port:this.#config.port
},
};
for(const label in data?.Config?.Labels){
if(/traefik\/|rfc2136\//i.test(label)){
result.labels[label] = data.Config.Labels[label];
}
}
process.send(result);
}else{
throw(error);
}
});
}catch(e){
process.send({error:e.toString()});
}
}
}
if(Array.isArray(args) && args.length > 0){
try{
new Worker(JSON.parse(args)).run();
}catch(e){
process.send({error:e.toString()});
process.exit(1);
}
}