Compare commits

..
9 Commits
Author SHA1 Message Date
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
6 changed files with 184 additions and 158 deletions
+1 -2
View File
@@ -1,2 +1 @@
maintain/
/build
maintain/
+2 -2
View File
@@ -1,11 +1,11 @@
# 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/latest?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)
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.
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, ...).
This image provides the ability to call a webhook for each container for each event or poll after the data was updates in Redis.
+38
View File
@@ -0,0 +1,38 @@
# :: 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};
# :: install
RUN set -ex; \
cd ${APP_ROOT}; \
npm --save install \
redis@4.6.11 \
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"]
+2
View File
@@ -0,0 +1,2 @@
#!/bin/bash
docker run --rm --privileged multiarch/qemu-user-static:register --reset
-137
View File
@@ -1,137 +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(){
if('' !== process.env.LABELS_WEBHOOK){
this.#log(`using webhook ${process.env.LABELS_WEBHOOK}`);
}
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) => {
const container = this.#docker.getContainer(id);
container.inspect(async(error, data) => {
let update = false;
const webHook = {event:status, labels:{}};
const headers = {'Content-Type':'application/json'};
if('' !== process.env.LABELS_WEBHOOK_AUTH_BASIC){
headers['Authorization'] = 'Basic ' + Buffer.from(process.env.LABELS_WEBHOOK_AUTH_BASIC).toString('base64')
}
if(error){
reject(error);
}
this.#log(`inspect container ${data.Name.replace(/^\//i, '')}${(
(null === status) ? '' : ` event[${status}]`
)}`);
if(/start|restart/i.test(status)){
update = true;
}
for(const label in data?.Config?.Labels){
if(/traefik\//i.test(label)){
webHook.labels[label] = data?.Config?.Labels[label];
if(update){
await this.#redis.set(label, data?.Config?.Labels[label], {EX:parseInt(process.env.LABELS_INTERVAL) + parseInt(process.env.LABELS_TIMEOUT)});
}else{
await this.#redis.del(label);
}
}
}
if('' !== process.env.LABELS_WEBHOOK){
try{
const webHookCall = await fetch(process.env.LABELS_WEBHOOK, {method:(
(update) ? 'PUT' : 'DELETE'
), body:JSON.stringify(webHook), headers:headers});
}catch(e){
this.#log(e, ERROR);
}
}
resolve(true);
});
}));
}
}
new Labels().watch();
+141 -17
View File
@@ -1,20 +1,144 @@
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),
LABELS_WEBHOOK:process.env?.LABELS_WEBHOOK || '',
LABELS_WEBHOOK_AUTH_BASIC:process.env?.LABELS_WEBHOOK_AUTH_BASIC || '',
const Docker = require('dockerode');
const redis = require('redis');
const ERROR = 1;
const POLL_INTERVAL = parseInt(process.env?.LABELS_INTERVAL || 300);
const POLL_TIMEOUT = parseInt(process.env?.LABELS_TIMEOUT|| 30);
class Labels{
#docker;
#redis;
#poll = false;
#webhook = {
headers:{'Content-Type':'application/json'}
};
constructor(){
this.#docker = new Docker({socketPath:'/run/docker.sock'});
if(undefined !== process.env.LABELS_WEBHOOK_AUTH_BASIC){
this.#webhook.headers['Authorization'] = 'Basic ' + Buffer.from(process.env.LABELS_WEBHOOK_AUTH_BASIC).toString('base64')
}
}
});
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);
});
#log(message, type){
console.log(JSON.stringify({
time:new Date().toISOString(),
type:(type === ERROR) ? 'ERROR' : 'INFO',
message:message,
}));
}
async watch(){
if(undefined !== process.env.LABELS_WEBHOOK){
this.#log(`using webhook ${process.env.LABELS_WEBHOOK}`);
}
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');
(async() => {
await this.dockerPoll();
})();
this.dockerEvents();
});
this.#redis.on('error', error =>{
this.#log(error, ERROR);
});
setInterval(async() => {
await this.dockerPoll();
}, POLL_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|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){
this.#log(e, ERROR);
}finally{
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){
const update = (/start|poll/i.test(status)) ? true : false;
const webHook = {event:status, labels:{}};
let log = false;
for(const label in data?.Config?.Labels){
if(/traefik\//i.test(label)){
if(!log){
this.#log(`inspect container {${(data?.Name || data?.id).replace(/^\//i, '')}}${(
(null === status) ? '' : ` event[${status}]`
)} traefik labels`);
log = true;
}
if(undefined !== process.env.LABELS_WEBHOOK){
webHook.labels[label] = data?.Config?.Labels[label];
}
if(update){
await this.#redis.set(label, data?.Config?.Labels[label], {EX:POLL_INTERVAL + POLL_TIMEOUT});
}else{
await this.#redis.del(label);
}
}
}
if(undefined !== process.env.LABELS_WEBHOOK){
try{
await fetch(process.env.LABELS_WEBHOOK, {method:(
(update) ? 'PUT' : 'DELETE'
), body:JSON.stringify(webHook), headers:this.#webhook.headers, signal:AbortSignal.timeout(2500)});
}catch(e){
this.#log(e, ERROR);
}
}
resolve(true);
}
});
}));
}
}
new Labels().watch();