Compare commits
5
Commits
amd64-0.1.6
..
amd64
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0cb3f5d165 | ||
|
|
b551e02001 | ||
|
|
fc94b61f62 | ||
|
|
426c69011b | ||
|
|
064fbaeac6 |
@@ -1,11 +1,11 @@
|
||||
# Alpine :: Traefik Labels
|
||||
    
|
||||
    
|
||||
|
||||
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.
|
||||
|
||||
|
||||
@@ -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"]
|
||||
@@ -0,0 +1,2 @@
|
||||
#!/bin/bash
|
||||
docker run --rm --privileged multiarch/qemu-user-static:register --reset
|
||||
@@ -1,135 +0,0 @@
|
||||
const Docker = require('dockerode');
|
||||
const redis = require('redis');
|
||||
const ERROR = 1;
|
||||
|
||||
class Labels{
|
||||
#docker;
|
||||
#redis;
|
||||
#poll = false;
|
||||
#webhook = {
|
||||
headers:{'Content-Type':'application/json'}
|
||||
};
|
||||
|
||||
constructor(){
|
||||
this.#docker = new Docker({socketPath:'/run/docker.sock'});
|
||||
if('' !== process.env.LABELS_WEBHOOK_AUTH_BASIC){
|
||||
this.#webhook.headers['Authorization'] = 'Basic ' + Buffer.from(process.env.LABELS_WEBHOOK_AUTH_BASIC).toString('base64')
|
||||
}
|
||||
}
|
||||
|
||||
#log(message, type){
|
||||
console.log(JSON.stringify({
|
||||
time:new Date().toISOString(),
|
||||
type:(type === 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.#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){
|
||||
let update = false;
|
||||
const webHook = {event:status, labels:{}};
|
||||
if(/start|restart|poll/i.test(status)){
|
||||
update = true;
|
||||
}
|
||||
|
||||
this.#log(`inspect container {${(data?.Name || data?.id).replace(/^\//i, '')}}${(
|
||||
(null === status) ? '' : ` event[${status}]`
|
||||
)}`);
|
||||
|
||||
for(const label in data?.Config?.Labels){
|
||||
if(/traefik\//i.test(label)){
|
||||
webHook.labels[label] = data?.Config?.Labels[label];
|
||||
if(update){
|
||||
this.#log(`${label} = ${data?.Config?.Labels[label]}`);
|
||||
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{
|
||||
await fetch(process.env.LABELS_WEBHOOK, {method:(
|
||||
(update) ? 'PUT' : 'DELETE'
|
||||
), body:JSON.stringify(webHook), headers:this.#webhook.headers, signal:AbortSignal.timeout(5000)});
|
||||
}catch(e){
|
||||
this.#log(e, ERROR);
|
||||
}
|
||||
}
|
||||
resolve(true);
|
||||
}
|
||||
});
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
new Labels().watch();
|
||||
+141
-16
@@ -1,19 +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);
|
||||
});
|
||||
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();
|
||||
Reference in New Issue
Block a user