eavesdocker
Advanced tools
Comparing version 0.1.2 to 0.1.3
@@ -8,2 +8,10 @@ # Eavesdocker Changelog | ||
## [v0.1.3] - 2021-07-14 | ||
### Added | ||
- endpoint to list all active containers | ||
- SSE endpoint to check container events and log entries in real-time | ||
- logic to handle swarm events via redis pubsub and cache | ||
- monitoring UI | ||
## [v0.1.2] - 2021-04-01 | ||
@@ -10,0 +18,0 @@ |
@@ -0,4 +1,15 @@ | ||
const { v4: uuidv4 } = require('uuid'); | ||
const fs = require('fs').promises; | ||
module.exports = function({ post }){ | ||
const { sanitizeContainer } = require('./transport'); | ||
async function readFile(name, type, { res }){ | ||
const data = await fs.readFile(__dirname + '/inspector/' + name, 'utf8'); | ||
res.type(type).end(data); | ||
} | ||
module.exports = function({ post, get }){ | ||
get('/', readFile.bind(null, 'page.html', 'text/html')); | ||
post('/transport/:name/message', function({ transports, res, params, body }){ | ||
@@ -8,10 +19,11 @@ | ||
let json; | ||
try{ | ||
var json = typeof body == 'object' ? body : JSON.parse(body); | ||
json = typeof body == 'object' ? body : JSON.parse(body); | ||
} | ||
catch(err){ | ||
catch(_err){ | ||
json = { message: body }; | ||
} | ||
let t = transports[params.name]; | ||
const t = transports[params.name]; | ||
t.push(t.json ? json : body); | ||
@@ -22,2 +34,65 @@ | ||
get('/containers', async function({ cookies, node, redis, cluster, swarm, containers, res, conf }){ | ||
const dash = conf.eavesdocker.dashboard || {}; | ||
const secret = dash.secret || ''; | ||
res.unauthorized(cookies.eavesdocker != secret); | ||
const cs = containers; | ||
Object.keys(cs).forEach(c => cs[c].node = node); | ||
if(conf.eavesdocker.swarm){ | ||
const prefix = 'eavesdocker:' + cluster + ':'; | ||
for(const nid in swarm){ | ||
const all = await redis.hgetall(prefix + nid + ':containers'); | ||
Object.keys(all).forEach(c => all[c].node = swarm[nid]); | ||
containers = { ...containers, all }; | ||
} | ||
} | ||
res.json(Object.values(cs).map(sanitizeContainer)); | ||
}); | ||
post('/auth', function({ body, res, conf }){ | ||
const dash = conf.eavesdocker.dashboard || {}; | ||
const secret = dash.secret || ''; | ||
res.unauthorized(body != secret); | ||
res.cookie('eavesdocker', secret, { | ||
maxAge: 60 * 60 * 24 * 7 * 1000, | ||
secure: true, | ||
httpOnly: true, | ||
sameSite: 'Strict' | ||
}).end(); | ||
}), | ||
get('/live', function({ req, res, sseClients, log, cookies, conf }){ | ||
const dash = conf.eavesdocker.dashboard || {}; | ||
const secret = dash.secret || ''; | ||
res.unauthorized(cookies.eavesdocker != secret); | ||
res.type('text/event-stream'); | ||
res.set('Connection', 'keep-alive'); | ||
res.set('Cache-Control', 'no-cache'); | ||
res.write(`data: online\n\n`); | ||
const id = uuidv4(); | ||
sseClients[id] = res; | ||
log.debug(`SSE Client ${id} is online`); | ||
req.on('close', () => { | ||
delete sseClients[id]; | ||
log.debug(`SSE Client ${id} is gone`); | ||
}); | ||
}); | ||
get('/stylesheet.css', readFile.bind(null, 'stylesheet.css', 'text/css')); | ||
get('/scripts.js', readFile.bind(null, 'scripts.js', 'application/javascript')); | ||
get('/favicon.png', readFile.bind(null, 'favicon.png', 'image/png')); | ||
} |
@@ -11,14 +11,17 @@ const { routeMessage } = require('./transport'); | ||
let c = await this.global.docker.call('/containers/' + id + '/json'); | ||
const c = { id }; | ||
c.stack = labels['com.docker.stack.namespace'] || labels['com.docker.compose.project'] || ''; | ||
c.service = labels['com.docker.swarm.service.name'] || labels['com.docker.compose.service'] || ''; | ||
c.service = c.service.replace(c.stack + '_', ''); | ||
c.node = this.global.node; | ||
c.tasks = matchTasks.call(this, c, labels); | ||
c.number = (labels['com.docker.swarm.task.name'] || | ||
labels['com.docker.compose.container-number'] || '1').replace(/.*?\./, '') | ||
.replace(/\..*$/, ''); | ||
c.tasks = matchTasks.call(this, c); | ||
if(c.tasks == 0) | ||
return; | ||
c.id = id; | ||
c.cid = '\'' + c.stack + '\' > ' + c.service + ' (' + id.substr(0, 8) + ')'; | ||
// : id.substr(0, 32); | ||
@@ -28,5 +31,8 @@ try{ | ||
this.global.containers[id] = c; | ||
this.global.emitter.emit('start', c); | ||
this.log.debug('Attached to %s with %s tasks', c.cid, c.tasks.length); | ||
} | ||
catch(err){ | ||
catch(_err){ | ||
// console.log(err); | ||
@@ -41,2 +47,5 @@ this.log.warn('Failed to attach to %s', c.cid); | ||
return; | ||
this.global.emitter.emit('die', { id }); | ||
this.global.containers[id].stream.destroy(); | ||
@@ -48,4 +57,4 @@ this.log.debug('Removed container %s', this.global.containers[id].cid); | ||
function parseEvent(data){ | ||
let event = JSON.parse(data); | ||
let actionType = event.Action + ' ' + event.Type; | ||
const event = JSON.parse(data); | ||
const actionType = event.Action + ' ' + event.Type; | ||
if(actionType == 'start container') | ||
@@ -58,3 +67,3 @@ addContainer.call(this, { Labels: event.Actor.Attributes, Id: event.id }); | ||
function destroyLoggers(){ | ||
for(let id in this.global.containers) | ||
for(const id in this.global.containers) | ||
this.global.containers[id].stream.destroy(); | ||
@@ -61,0 +70,0 @@ } |
108
lib/main.js
@@ -0,2 +1,4 @@ | ||
const { EventEmitter } = require('events'); | ||
const Nodecaf = require('nodecaf'); | ||
const redis = require('nodecaf-redis'); | ||
@@ -8,6 +10,77 @@ const { addContainer, parseEvent, destroyLoggers } = require('./container'); | ||
const api = require('./api'); | ||
async function connectToSwarm(global, conf, log){ | ||
const info = await global.docker.call('/info'); | ||
global.node = { name: info.Name, id: info.Swarm.NodeID }; | ||
global.cluster = info.Swarm.Cluster ? info.Swarm.Cluster.ID : 'local'; | ||
global.swarm = {}; | ||
try{ | ||
global.redis = await redis(conf.redis, 'eavesdocker:' + global.cluster, function(_c, msg){ | ||
msg = JSON.parse(msg); | ||
if(msg.node.id == global.node.id) | ||
return; | ||
else if(msg.type == 'node-ping' && msg.node.id in global.swarm){ | ||
clearTimeout(global.swarm[msg.node.id].tto); | ||
global.swarm[msg.node.id].tto = setTimeout(() => | ||
delete global.swarm[msg.node.id], 10e3); | ||
} | ||
else if(msg.type == 'node-ping') | ||
global.swarm[msg.node.id] = { | ||
...msg.node, | ||
ttl: setTimeout(() => delete global.swarm[msg.node.id], 10e3) | ||
}; | ||
else if(msg.type in { start: 1, die: 1, log: 1 }) | ||
global.emitter.emit('swarm-' + msg.type, msg); | ||
}); | ||
} | ||
catch(err){ | ||
log.warn({ err }); | ||
return setTimeout(() => connectToSwarm(global, conf, log), 3000); | ||
} | ||
const sendSwarmEvent = function(msg){ | ||
const ev = `event: ${msg.type}\ndata: ${JSON.stringify({ | ||
...msg.data, node: msg.node })}\n\n`; | ||
Object.values(global.sseClients).forEach(res => res.write(ev)); | ||
}; | ||
global.emitter.on('swarm-log', sendSwarmEvent); | ||
global.emitter.on('swarm-start', sendSwarmEvent); | ||
global.emitter.on('swarm-die', sendSwarmEvent); | ||
const pub = function(type, data){ | ||
global.redis.publish('eavesdocker:' + global.cluster, | ||
JSON.stringify({ data, node: global.node, type })); | ||
}; | ||
global.pingger = setInterval(() => pub('node-ping'), 9e3); | ||
const containersKey = 'eavesdocker:' + global.cluster + ':' + global.node.id + ':containers'; | ||
global.emitter.on('start', function(c){ | ||
pub('start', c); | ||
global.redis.hset(containersKey, c.id, JSON.stringify(c)); | ||
}); | ||
global.emitter.on('die', function(id){ | ||
pub('die', id); | ||
global.redis.hdel(containersKey, id); | ||
}); | ||
global.emitter.on('log', data => pub('log', data)); | ||
global.redis.del(containersKey); | ||
} | ||
module.exports = () => new Nodecaf({ | ||
conf: __dirname + '/default.toml', | ||
async startup({ global, conf }){ | ||
api, | ||
async startup({ global, conf, log }){ | ||
global.containers = {}; | ||
@@ -26,9 +99,32 @@ global.transports = {}; | ||
await createTasks.call(this); | ||
let containers = await global.docker.call('/containers/json'); | ||
for(let c of containers) | ||
global.emitter = new EventEmitter(); | ||
global.sseClients = {}; | ||
const sendEvent = function(type, data){ | ||
const ev = `event: ${type}\ndata: ${JSON.stringify({ ...data, node: global.node })}\n\n`; | ||
Object.values(global.sseClients).forEach(res => res.write(ev)); | ||
}; | ||
global.emitter.on('start', sendEvent.bind(null, 'start')); | ||
global.emitter.on('die', sendEvent.bind(null, 'die')); | ||
global.emitter.on('log', sendEvent.bind(null, 'log')); | ||
if(conf.eavesdocker.swarm && conf.redis) | ||
await connectToSwarm(global, conf, log); | ||
const containers = await global.docker.call('/containers/json'); | ||
for(const c of containers) | ||
await addContainer.call(this, c); | ||
}, | ||
async shutdown({ global }){ | ||
global.events.off('data', parseEvent.bind(this)); | ||
async shutdown({ global, conf }){ | ||
if(conf.eavesdocker.swarm){ | ||
clearInterval(global.pingger); | ||
await global.redis.close(); | ||
} | ||
global.emitter.removeAllListeners(); | ||
global.events.removeAllListeners(); | ||
global.events.destroy(); | ||
@@ -35,0 +131,0 @@ destroyLoggers.call(this); |
@@ -16,3 +16,3 @@ | ||
let queue = []; | ||
let it = setInterval(async function(){ | ||
const it = setInterval(async function(){ | ||
queue.length > 0 && await coll.insertMany(queue); | ||
@@ -59,4 +59,10 @@ queue = []; | ||
function sanitizeContainer(c){ | ||
return { id: c.id, stack: c.stack, service: c.service, number: c.number, | ||
node: c.node }; | ||
} | ||
module.exports = { | ||
sanitizeContainer, | ||
createTransport, | ||
@@ -67,20 +73,26 @@ | ||
let json; | ||
try{ | ||
var json = JSON.parse(entry); | ||
json = JSON.parse(entry); | ||
assert(typeof json == 'object'); | ||
} | ||
catch(err){ | ||
catch(_err){ | ||
json = { message: String(entry).replace(/\r?\n$/, ''), time: new Date() }; | ||
} | ||
for(let task of container.tasks){ | ||
let input = task.transform(container, json); | ||
for(let transport of task.transports) | ||
for(const task of container.tasks){ | ||
const input = task.transform(container, json); | ||
for(const transport of task.transports) | ||
transport.push(input); | ||
} | ||
this.global.emitter.emit('log', { | ||
source: sanitizeContainer(container), | ||
entry: json | ||
}); | ||
}, | ||
async createTransports(){ | ||
for(let name in this.conf.eavesdocker.transports){ | ||
let t = this.conf.eavesdocker.transports[name]; | ||
for(const name in this.conf.eavesdocker.transports){ | ||
const t = this.conf.eavesdocker.transports[name]; | ||
await createTransport.call(this, name, t); | ||
@@ -91,3 +103,3 @@ } | ||
async closeTransports(){ | ||
for(let name in this.global.transports) | ||
for(const name in this.global.transports) | ||
await this.global.transports[name].close(); | ||
@@ -94,0 +106,0 @@ } |
{ | ||
"name": "eavesdocker", | ||
"version": "0.1.2", | ||
"version": "0.1.3", | ||
"main": "lib/main.js", | ||
@@ -12,4 +12,5 @@ "scripts": { | ||
"nodecaf": "^0.11.5", | ||
"nodecaf-redis": "0.0.1", | ||
"toml": "^3.0.0" | ||
"nodecaf-redis": "0.0.3", | ||
"toml": "^3.0.0", | ||
"uuid": "^8.3.2" | ||
}, | ||
@@ -16,0 +17,0 @@ "devDependencies": { |
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
62332
18
1515
5
6
2
+ Addeduuid@^8.3.2
+ Addednodecaf-redis@0.0.3(transitive)
+ Addeduuid@8.3.2(transitive)
- Removednodecaf-redis@0.0.1(transitive)
Updatednodecaf-redis@0.0.3