Compare commits
61
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2b98936893 | ||
|
|
16ceb5a17a | ||
|
|
b18b3f67be | ||
|
|
68d4b66972 | ||
|
|
3c0c04abe5 | ||
|
|
53d0fb843d | ||
|
|
b09f41e04f | ||
|
|
c9ca96a547 | ||
|
|
71f5dce8f7 | ||
|
|
9fabbcd86c | ||
|
|
2e3aa6ea57 | ||
|
|
962cd22f26 | ||
|
|
9f24499f6f | ||
|
|
33cf74e52a | ||
|
|
543a19f5f0 | ||
|
|
5ad1a0d355 | ||
|
|
307e7101f8 | ||
|
|
6752ebee1a | ||
|
|
3c2ab08ba9 | ||
|
|
498c0988e5 | ||
|
|
49801cd521 | ||
|
|
66b5bab4ae | ||
|
|
070f45fae5 | ||
|
|
085de10ca1 | ||
|
|
85575ce073 | ||
|
|
b754494a98 | ||
|
|
6ac866ef5e | ||
|
|
426826532d | ||
|
|
82b132c68e | ||
|
|
3f89d3ddd9 | ||
|
|
ff8e1d9cd9 | ||
|
|
2d5ef49071 | ||
|
|
76a9a7261f | ||
|
|
7e67e971ae | ||
|
|
fad60b7019 | ||
|
|
2eadac07bb | ||
|
|
e129945fde | ||
|
|
2d9bfafffa | ||
|
|
823151dc98 | ||
|
|
7bd574c3a3 | ||
|
|
e7c3a83ae2 | ||
|
|
24a2e20641 | ||
|
|
7404380279 | ||
|
|
d0b98b201d | ||
|
|
33ac5c9e4c | ||
|
|
0cb3f5d165 | ||
|
|
b551e02001 | ||
|
|
fc94b61f62 | ||
|
|
426c69011b | ||
|
|
064fbaeac6 | ||
|
|
d978cd42c8 | ||
|
|
af8343f89a | ||
|
|
f60f0ad124 | ||
|
|
e7202481b3 | ||
|
|
ccdf29b7ae | ||
|
|
69f14549c6 | ||
|
|
6ab10f6b2a | ||
|
|
35fee8e82b | ||
|
|
8686ba58c0 | ||
|
|
437a3e92d3 | ||
|
|
4796cc1aa1 |
+1
-1
@@ -1,2 +1,2 @@
|
||||
maintain/
|
||||
/build
|
||||
project*
|
||||
@@ -1,58 +1,108 @@
|
||||
# Alpine :: Traefik Labels
|
||||
    
|
||||

|
||||
|
||||
Run Traefik Labels based on Alpine Linux. Small, lightweight, secure and fast 🏔️
|
||||
# 🏔️ Alpine - Traefik Labels
|
||||
     
|
||||
|
||||
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.
|
||||
# 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 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.
|
||||
|
||||
## 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.
|
||||
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/etc** - Directory of config.yaml
|
||||
* **/labels/ssl** - Directory of ssl certificates for TLS<sup>1</sup>
|
||||
|
||||
# EXAMPLES
|
||||
## config /labels/etc/config.yaml
|
||||
```yaml
|
||||
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 (remove existing entry)
|
||||
update-only: true
|
||||
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
|
||||
```
|
||||
|
||||
# RUN
|
||||
```shell
|
||||
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" \
|
||||
-v .../etc:/labels/etc \
|
||||
-v .../ssl:/labels/ssl \
|
||||
-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 \
|
||||
-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" \
|
||||
-d 11notes/nginx:stable
|
||||
```
|
||||
|
||||
## Defaults
|
||||
# DEFAULT SETTINGS
|
||||
| Parameter | Value | Description |
|
||||
| --- | --- | --- |
|
||||
| `user` | docker | user docker |
|
||||
| `uid` | 1000 | user id 1000 |
|
||||
| `gid` | 1000 | group id 1000 |
|
||||
| `home` | /labels | home directory of user docker |
|
||||
| `config` | /labels/etc/config.yaml | Static config |
|
||||
| `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 |
|
||||
| --- | --- | --- |
|
||||
| `LABELS_REDIS_URL` | the redis URL to connect, use rediss:// for SSL | redis:://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 |
|
||||
| `TZ` | [Time Zone](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) | |
|
||||
| `DEBUG` | Show debug information | |
|
||||
|
||||
## Parent image
|
||||
* [11notes/node:stable](https://github.com/11notes/docker-node)
|
||||
# PARENT IMAGE
|
||||
* [11notes/node:stable](https://hub.docker.com/r/11notes/node)
|
||||
|
||||
## Built with and thanks to
|
||||
# BUILT WITH
|
||||
* [npm::redis](https://www.npmjs.com/package/redis)
|
||||
* [npm::dockerode](https://www.npmjs.com/package/dockerode)
|
||||
* [nodejs](https://nodejs.org/en)
|
||||
* [Alpine Linux](https://alpinelinux.org)
|
||||
* [alpine](https://alpinelinux.org)
|
||||
|
||||
## Tips
|
||||
# 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
|
||||
* Allow non-root ports < 1024 via `echo "net.ipv4.ip_unprivileged_port_start=53" > /etc/sysctl.d/ports.conf`
|
||||
* Use a reverse proxy like Traefik, Nginx to terminate TLS with a valid certificate
|
||||
|
||||
# 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.
|
||||
|
||||
+15
-2
@@ -1,6 +1,13 @@
|
||||
# :: 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_VERSION=0.1.1
|
||||
ENV APP_ROOT=/labels
|
||||
|
||||
# :: Run
|
||||
@@ -8,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
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
# :: Util
|
||||
FROM alpine as util
|
||||
|
||||
RUN set -ex; \
|
||||
apk add --no-cache \
|
||||
git; \
|
||||
git clone https://github.com/11notes/util.git;
|
||||
|
||||
# :: Arch
|
||||
FROM multiarch/qemu-user-static:x86_64-aarch64 as qemu
|
||||
|
||||
# :: Header
|
||||
FROM 11notes/node:arm64v8-stable
|
||||
COPY --from=qemu /usr/bin/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"]
|
||||
@@ -0,0 +1,5 @@
|
||||
#!/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-version.yaml
|
||||
./manifest-tool push from-spec multi-arch-manifest-latest.yaml
|
||||
@@ -0,0 +1,2 @@
|
||||
#!/bin/bash
|
||||
docker run --rm --privileged multiarch/qemu-user-static:register --reset
|
||||
@@ -0,0 +1,11 @@
|
||||
image: 11notes/traefik-labels:latest
|
||||
manifests:
|
||||
- image: 11notes/traefik-labels:amd64-0.2.1
|
||||
platform:
|
||||
architecture: amd64
|
||||
os: linux
|
||||
- image: 11notes/traefik-labels:arm64v8-0.2.1
|
||||
platform:
|
||||
architecture: arm64
|
||||
os: linux
|
||||
variant: v8
|
||||
@@ -0,0 +1,11 @@
|
||||
image: 11notes/traefik-labels:0.2.1
|
||||
manifests:
|
||||
- image: 11notes/traefik-labels:amd64-0.2.1
|
||||
platform:
|
||||
architecture: amd64
|
||||
os: linux
|
||||
- image: 11notes/traefik-labels:arm64v8-0.2.1
|
||||
platform:
|
||||
architecture: arm64
|
||||
os: linux
|
||||
variant: v8
|
||||
@@ -1,106 +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('connected to Redis');
|
||||
(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) => {
|
||||
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)){
|
||||
this.#log(`new docker event for container ${event.id}`);
|
||||
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, 'start');
|
||||
});
|
||||
});
|
||||
}catch(e){
|
||||
this.#log(e, ERROR);
|
||||
}finally{
|
||||
this.#log('poll end');
|
||||
this.#poll = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dockerInspect(id, status = null){
|
||||
return(new Promise((resolve, reject) => {
|
||||
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)){
|
||||
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();
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
}));
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
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 (remove existing entry)
|
||||
update-only: true
|
||||
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
|
||||
+319
-17
@@ -1,18 +1,320 @@
|
||||
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 || 'redis:://localhost:6379/0',
|
||||
LABELS_INTERVAL:parseInt(process.env?.LABELS_INTERVAL || 300),
|
||||
LABELS_TIMEOUT:parseInt(process.env?.LABELS_TIMEOUT|| 30),
|
||||
const fs = require('fs');
|
||||
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');
|
||||
|
||||
process
|
||||
.on('unhandledRejection', (e, p) => {
|
||||
elevenLogJSON('error', JSON.stringify({rejection:{exception:e.toString()}}));
|
||||
})
|
||||
.on('uncaughtException', e => {
|
||||
elevenLogJSON('error', JSON.stringify({exception:{exception:e.toString()}}));
|
||||
});
|
||||
|
||||
class Labels{
|
||||
#config = yaml.load(fs.readFileSync(`${process.env.APP_ROOT}/etc/config.yaml`, 'utf8'))?.labels;
|
||||
#defaults = {
|
||||
interval:0,
|
||||
timeout:2.5,
|
||||
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, nodes:false};
|
||||
#loops = {ping:false, poll:false, nodes:false};
|
||||
#redis;
|
||||
#nodes = {};
|
||||
#tls = {ca:'', crt:'', key:''};
|
||||
|
||||
constructor(){
|
||||
this.#tls.ca = fs.readFileSync(this.#config?.tls?.ca || this.#defaults.tls.ca);
|
||||
this.#tls.crt = fs.readFileSync(this.#config?.tls?.crt || this.#defaults.tls.crt);
|
||||
this.#tls.key = fs.readFileSync(this.#config?.tls?.key || this.#defaults.tls.key);
|
||||
|
||||
this.#loadNodes(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 #loadNodes(init){
|
||||
if(!this.#intervals.nodes && (this.#config?.interval || this.#defaults.interval) > 0){
|
||||
this.#intervals.nodes = true;
|
||||
setInterval(async() => {
|
||||
if(!this.#loops.nodes){
|
||||
this.#loops.nodes = true;
|
||||
try{
|
||||
elevenLogJSON('info', `reload nodes from config.yaml`);
|
||||
this.#config = yaml.load(fs.readFileSync(`${process.env.APP_ROOT}/etc/config.yaml`, 'utf8'))?.labels;
|
||||
await this.#loadNodes(false);
|
||||
}catch(e){
|
||||
elevenLogJSON('error', JSON.stringify({nodes:{exception:e.toString()}}));
|
||||
}finally{
|
||||
this.#loops.nodes = false;
|
||||
}
|
||||
}
|
||||
}, (this.#config?.interval || this.#defaults.interval)*1000);
|
||||
}
|
||||
|
||||
for(const node of this.#config?.nodes){
|
||||
if(!this.#nodes[node]){
|
||||
this.#nodes[node] = new Docker({
|
||||
protocol:'https',
|
||||
host:node,
|
||||
port:this.#config?.tls?.port || this.#defaults.tls.port,
|
||||
ca:this.#tls.ca,
|
||||
cert:this.#tls.crt,
|
||||
key:this.#tls.key,
|
||||
timeout:(this.#config?.timeout || this.#defaults.timeout)*1000
|
||||
});
|
||||
this.#nodes[node].labels = {ping:false, firstConnect:init};
|
||||
}
|
||||
}
|
||||
|
||||
for(const pnode in this.#nodes){
|
||||
let valid = false;
|
||||
for(const node of this.#config?.nodes){
|
||||
if(pnode === node){
|
||||
valid = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(!valid){
|
||||
elevenLogJSON('info', `removed node [${pnode}] from configuration`);
|
||||
delete(this.#nodes[pnode]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async watch(){
|
||||
this.#redis = await redis.createClient({
|
||||
url:this.#config?.redis?.url || this.#defaults.redis.url,
|
||||
pingInterval:30000,
|
||||
socket:{
|
||||
rejectUnauthorized:false,
|
||||
},
|
||||
disableOfflineQueue:false,
|
||||
commandsQueueMaxLength:1024
|
||||
});
|
||||
|
||||
this.#redis.on('ready', async()=>{
|
||||
elevenLogJSON('info', `connected to redis`);
|
||||
await this.#ping();
|
||||
elevenLogJSON('info', `start initial poll after container start`);
|
||||
await this.#poll();
|
||||
});
|
||||
|
||||
this.#redis.on('error', error =>{
|
||||
elevenLogJSON('error', JSON.stringify({redis:{exception:error.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){
|
||||
if(this.#nodes[node].labels.ping && !this.#nodes[node].labels.firstConnect){
|
||||
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){
|
||||
if(this.#nodes[node].labels.ping){
|
||||
try{
|
||||
await this.#nodes[node].listContainers((error, containers) => {
|
||||
elevenLogJSON('info', `poll started on node [${node}]`);
|
||||
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();
|
||||
@@ -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();
|
||||
}));
|
||||
}
|
||||
Reference in New Issue
Block a user