mirror of
https://github.com/11notes/docker-traefik-labels.git
synced 2026-11-13 06:17:40 -06:00
Compare commits
26
Commits
dev
...
amd64-0.1.10
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
@@ -1,5 +1,5 @@
|
||||
# Alpine :: Traefik Labels
|
||||
    
|
||||
    
|
||||
|
||||
Run Traefik Labels based on Alpine Linux. Small, lightweight, secure and fast 🏔️
|
||||
|
||||
@@ -10,6 +10,9 @@ In order to use this image, you need to setup Traefik with a Redis provider and
|
||||
|
||||
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
|
||||
```shell
|
||||
docker run --name traefik-labels \
|
||||
@@ -27,6 +30,15 @@ docker run --name traefik-labels \
|
||||
-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 \
|
||||
@@ -49,11 +61,11 @@ docker run --name traefik-rfc2136-demo \
|
||||
-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 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 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 A 192.168.12.54" \
|
||||
-l "rfc2136/LAN/nsupdate=update add foo.domain.local 300 A 192.168.12.54" \
|
||||
-d 11notes/nginx:stable
|
||||
```
|
||||
|
||||
@@ -65,16 +77,23 @@ docker run --name traefik-rfc2136-demo \
|
||||
| `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) | null |
|
||||
| `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 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://hub.docker.com/r/11notes/node)
|
||||
@@ -88,4 +107,18 @@ docker run --name traefik-rfc2136-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"]
|
||||
}
|
||||
```
|
||||
+3
-2
@@ -8,7 +8,6 @@
|
||||
|
||||
# :: Header
|
||||
FROM 11notes/node:stable
|
||||
COPY --from=util /util/node/util.js /labels/lib
|
||||
ENV APP_ROOT=/labels
|
||||
|
||||
# :: Run
|
||||
@@ -16,11 +15,13 @@
|
||||
|
||||
# :: 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}; \
|
||||
|
||||
+3
-2
@@ -13,7 +13,6 @@
|
||||
|
||||
# :: Header
|
||||
FROM arm64v8/node:20.10.0-alpine3.19
|
||||
COPY --from=util /util/node/util.js /labels/lib
|
||||
COPY --from=qemu qemu-aarch64-static /usr/bin
|
||||
ENV APP_ROOT=/labels
|
||||
|
||||
@@ -22,11 +21,13 @@
|
||||
|
||||
# :: 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}; \
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1,11 @@
|
||||
image: 11notes/traefik-labels:latest
|
||||
manifests:
|
||||
- image: 11notes/traefik-labels:amd64-0.1.10
|
||||
platform:
|
||||
architecture: amd64
|
||||
os: linux
|
||||
- image: 11notes/traefik-labels:arm64v8-0.1.10
|
||||
platform:
|
||||
architecture: arm64
|
||||
os: linux
|
||||
variant: v8
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
}));
|
||||
}
|
||||
+73
-21
@@ -1,15 +1,18 @@
|
||||
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 } = requore('./nsupdate');
|
||||
const { logJSON } = require('/labels/lib/util.js');
|
||||
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;
|
||||
|
||||
class Labels{
|
||||
#docker;
|
||||
@@ -20,17 +23,33 @@ class Labels{
|
||||
};
|
||||
|
||||
constructor(){
|
||||
this.#docker = new Docker({socketPath:'/run/docker.sock'});
|
||||
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 || 'localhost',
|
||||
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/labels.crt`),
|
||||
key:fs.readFileSync(`${process.env?.APP_ROOT}/ssl/labels.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(){
|
||||
if(ENV_LABELS_WEBHOOK){
|
||||
logJSON('info', `using webhook ${ENV_LABELS_WEBHOOK}`);
|
||||
}
|
||||
|
||||
this.#redis = await redis.createClient({
|
||||
url:process.env.LABELS_REDIS_URL,
|
||||
pingInterval:30000,
|
||||
@@ -41,7 +60,6 @@ class Labels{
|
||||
|
||||
this.#redis.connect();
|
||||
this.#redis.on('ready', ()=>{
|
||||
logJSON('info', 'successfully connected to redis');
|
||||
(async() => {
|
||||
await this.dockerPoll();
|
||||
})();
|
||||
@@ -49,7 +67,7 @@ class Labels{
|
||||
});
|
||||
|
||||
this.#redis.on('error', error =>{
|
||||
logJSON('error', error);
|
||||
elevenLogJSON('error', error);
|
||||
});
|
||||
|
||||
setInterval(async() => {
|
||||
@@ -60,7 +78,7 @@ class Labels{
|
||||
dockerEvents(){
|
||||
this.#docker.getEvents({}, (error, data) => {
|
||||
if(error){
|
||||
logJSON('error', error);
|
||||
elevenLogJSON('error', error);
|
||||
}else{
|
||||
data.on('data', async(chunk) => {
|
||||
const event = JSON.parse(chunk.toString('utf8'));
|
||||
@@ -84,7 +102,7 @@ class Labels{
|
||||
}
|
||||
});
|
||||
}catch(e){
|
||||
logJSON('error', e);
|
||||
elevenLogJSON('error', e);
|
||||
}finally{
|
||||
this.#poll = false;
|
||||
}
|
||||
@@ -98,6 +116,7 @@ class Labels{
|
||||
if(!error){
|
||||
const update = (/start|poll/i.test(status)) ? true : false;
|
||||
const container = {
|
||||
name:(data?.Name || data?.id).replace(/^\//i, ''),
|
||||
event:status,
|
||||
labels:{
|
||||
traefik:[],
|
||||
@@ -109,35 +128,37 @@ class Labels{
|
||||
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});
|
||||
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];
|
||||
container.labels.traefik[label] = data.Config.Labels[label];
|
||||
break;
|
||||
|
||||
case /rfc2136\//i.test(label):
|
||||
container.labels.rfc2136[label] = data?.Config?.Labels[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];
|
||||
rfc2136[type].server = data.Config.Labels[label];
|
||||
break;
|
||||
|
||||
case /rfc2136\/\S+\/key/i.test(label):
|
||||
rfc2136[type].key = data?.Config?.Labels[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');
|
||||
data.Config.Labels[label] = data.Config.Labels[label].replace(/update add/i, 'update delete');
|
||||
}
|
||||
rfc2136[type].commands.push(data?.Config?.Labels[label]);
|
||||
rfc2136[type].commands.push(data.Config.Labels[label]);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -145,10 +166,24 @@ class Labels{
|
||||
|
||||
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{
|
||||
await nsupdate(rfc2136[type].server, rfc2136[type].key, rfc2136[type].commands);
|
||||
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){
|
||||
logJSON('error', e);
|
||||
elevenLogJSON('error', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -159,7 +194,7 @@ class Labels{
|
||||
(update) ? 'PUT' : 'DELETE'
|
||||
), body:JSON.stringify(container), headers:this.#webhook.headers, signal:AbortSignal.timeout(2500)});
|
||||
}catch(e){
|
||||
logJSON('error', e);
|
||||
elevenLogJSON('error', {method:'fetch(webhook)', error:e});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,6 +203,23 @@ class Labels{
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
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();
|
||||
@@ -1,22 +1,28 @@
|
||||
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]);
|
||||
let errors = '';
|
||||
nsupdate.stderr.on('data', data =>{errors += data.toString()});
|
||||
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('exit', code =>{
|
||||
nsupdate.on('close', code =>{
|
||||
if(code === 0){
|
||||
if(errors.length > 0){
|
||||
reject(errors);
|
||||
if(io.stderr.length > 0){
|
||||
reject(io.stderr);
|
||||
}else{
|
||||
resolve(true);
|
||||
resolve(io.stdout);
|
||||
}
|
||||
}else{
|
||||
reject(errors);
|
||||
reject(io.stderr);
|
||||
}
|
||||
});
|
||||
for(const command of commands){
|
||||
|
||||
Reference in New Issue
Block a user