eavesdocker
Advanced tools
Comparing version 0.2.0 to 0.3.0
@@ -8,2 +8,13 @@ # Eavesdocker Changelog | ||
## [v0.3.0] - 2021-08-20 | ||
### Added | ||
- twilio transport to send SMS | ||
- support to yml config files | ||
- templating to GitLab `title` and `labels` settings | ||
- templating to email `subject` setting | ||
### Changed | ||
- entire configuration schema from `tasks` to `pipeline` key | ||
## [v0.2.0] - 2021-08-18 | ||
@@ -143,1 +154,2 @@ | ||
[v0.2.0]: https://gitlab.com/GCSBOSS/eavesdocker/-/tags/v0.2.0 | ||
[v0.3.0]: https://gitlab.com/GCSBOSS/eavesdocker/-/tags/v0.3.0 |
const { routeMessage } = require('./transport'); | ||
const { matchTasks } = require('./task'); | ||
const { shouldAttachContainer } = require('./pipeline'); | ||
@@ -26,4 +26,3 @@ const os = require('os'); | ||
c.tasks = matchTasks.call(this, c); | ||
if(c.tasks == 0) | ||
if(!shouldAttachContainer.call(this, c)) | ||
return; | ||
@@ -37,3 +36,3 @@ | ||
this.log.debug('Attached to %s with %s tasks', c.cid, c.tasks.length); | ||
this.log.debug('Attached to %s', c.cid); | ||
@@ -45,3 +44,2 @@ if(this.global.healthRedis) | ||
catch(_err){ | ||
// console.log(err); | ||
this.log.warn('Failed to attach to %s', c.cid); | ||
@@ -57,3 +55,3 @@ } | ||
this.global.containers[id].stream.destroy(); | ||
this.log.debug('Removed container %s', this.global.containers[id].cid); | ||
this.log.debug('Dettached from %s', this.global.containers[id].cid); | ||
delete this.global.containers[id]; | ||
@@ -60,0 +58,0 @@ |
@@ -5,6 +5,8 @@ const Nodecaf = require('nodecaf'); | ||
const { addContainer, parseEvent, destroyLoggers } = require('./container'); | ||
const { closeTransports } = require('./transport'); | ||
const { createTasks } = require('./task'); | ||
const { closeTransports, TRANSPORTS } = require('./transport'); | ||
const { buildPipelineFunction } = require('./pipeline'); | ||
const docker = require('./docker'); | ||
const MATCHERS = { stack: 1, services: 1 }; | ||
module.exports = () => new Nodecaf({ | ||
@@ -15,4 +17,13 @@ conf: __dirname + '/default.toml', | ||
global.transports = {}; | ||
global.tasks = {}; | ||
global.matcher = {}; | ||
for(const job of conf.pipeline){ | ||
const type = Object.keys(job)[0]; | ||
if(type in MATCHERS) | ||
global.matcher[type] = job[type]; | ||
else if(type in TRANSPORTS) | ||
break; | ||
} | ||
global.levels = { trace: 0, debug: 1, info: 2, warn: 3, error: 4, fatal: 5 }; | ||
global.docker = docker(conf.docker); | ||
@@ -22,3 +33,3 @@ global.events = await global.docker.events(parseEvent.bind(this)); | ||
await createTasks.call(this); | ||
global.processEntry = await buildPipelineFunction.call(this); | ||
@@ -28,7 +39,11 @@ const info = await global.docker.call('/info'); | ||
if(conf.healthRedis){ | ||
if(typeof conf.health == 'object'){ | ||
global.healthRedis = await redis(typeof conf.healthRedis == 'string' | ||
? conf[conf.healthRedis] : conf.healthRedis); | ||
if(typeof conf.health.$inherit == 'string'){ | ||
conf.health = { ...conf[conf.health.$inherit], ...conf.health }; | ||
delete conf.health.$inherit; | ||
} | ||
global.healthRedis = await redis(conf.health); | ||
global.healthRedis.set('eavesdocker:instance:' + global.node.id, | ||
@@ -35,0 +50,0 @@ global.node.name, 'EX', '60'); |
const assert = require('assert'); | ||
function buildTemplater(template){ | ||
const parts = template.split(/{[a-zA-Z0-9_\-\.]+}/g); | ||
const vars = (template.match(/(?<={)[a-zA-Z0-9_\-\.]+(?=})/g) || []).map(v => | ||
v.split('.').map(ident => `['${ident}']`).join()); | ||
let fn = 'return '; | ||
for(let i = 0; i < vars.length; i++) | ||
fn += `'${parts.shift()}' + d${vars.shift()} + `; | ||
fn += `'${parts.shift()}'`; | ||
return new Function('d', fn); | ||
} | ||
const TRANSPORTS = { | ||
@@ -67,3 +81,2 @@ | ||
const lp = conf.labelPrefix || ''; | ||
const url = conf.url || 'https://gitlab.com'; | ||
@@ -93,2 +106,5 @@ const path = url + '/api/v4/projects/' + encodeURIComponent(conf.project) + '/issues'; | ||
const labelers = conf.labels.map(buildTemplater); | ||
const titler = buildTemplater(conf.title || '{level} - {message}'); | ||
return { | ||
@@ -100,14 +116,6 @@ close: Function.prototype, | ||
const labels = []; | ||
for(const f of conf.labelFields || []) | ||
if(typeof data[f] == 'string' && data[f].length < 32) | ||
labels.push(lp + data[f]); | ||
const title = | ||
(data.level ? data.level.toUpperCase() + ': ' : '') + | ||
data.message || data.msg || 'Important Event' | ||
const { status } = await pool.post(path, headers, { | ||
// assignee_id: | ||
title, labels, 'created_at': new Date(), | ||
title: titler(data), | ||
labels: labelers.map(l => l(data)), | ||
'created_at': new Date(), | ||
description: markdownObject.call(ctx, data) | ||
@@ -126,4 +134,2 @@ }); | ||
const defaultSubject = 'Important log entry'; | ||
if(!conf.to) | ||
@@ -151,2 +157,4 @@ throw new Error('Recipient not defined'); | ||
const subjecter = buildTemplater(conf.subject || '{level} - {message}'); | ||
return { | ||
@@ -157,3 +165,3 @@ close: () => transporter.close(), | ||
to: conf.to, | ||
subject: conf.subject || data[conf.subjectField] || defaultSubject, | ||
subject: subjecter(data), | ||
text: JSON.stringify(data, null, 4), | ||
@@ -163,37 +171,63 @@ html: htmlObject(data), | ||
}; | ||
} | ||
}, | ||
}; | ||
twilio(conf){ | ||
const { Pool } = require('muhb'); | ||
const pool = new Pool({ size: 100, timeout: 4000 }); | ||
const url = conf.fakeHost | ||
? `http://${conf.sid}:${conf.token}@${conf.fakeHost}` | ||
: `https://${conf.sid}:${conf.token}@api.twilio.com/2010-04-01/Accounts/${conf.sid}/Messages`; | ||
const headers = { 'Content-Type': 'application/x-www-form-urlencoded' }; | ||
async function createTransport(name, spec, type){ | ||
this.global.transports[name] = await TRANSPORTS[type].call(this, spec); | ||
this.log.debug('Installed transport \'%s\' (%s)', name, type); | ||
} | ||
function textObject(obj){ | ||
let data = ''; | ||
for(const key in obj){ | ||
data += ' '.repeat(this.indentation) + key + ': '; | ||
function transform(container, task, input){ | ||
let output = { ...input }; | ||
if(obj[key] instanceof Date) | ||
data += obj[key].toISOString(); | ||
if(typeof obj[key] == 'object'){ | ||
this.indentation += 2; | ||
data += '\n' + textObject.call(this, obj[key]); | ||
this.indentation -= 2; | ||
} | ||
else | ||
data += String(obj[key]); | ||
if(typeof task.time == 'string'){ | ||
const date = new Date(input[task.time]); | ||
output[task.time] = isNaN(date) ? new Date() : date; | ||
} | ||
data += '\n'; | ||
} | ||
return data; | ||
} | ||
if(task.includeSource) | ||
output.source = { | ||
stack: container.stack, | ||
service: container.service, | ||
id: container.id | ||
return { | ||
close: Function.prototype, | ||
push: async data => { | ||
const ctx = { indentation: 0 }; | ||
for(const to of conf.to){ | ||
const body = textObject.call(ctx, data).substr(0, 1200); | ||
pool.post(url, headers, | ||
`From=${conf.from}&To=${encodeURIComponent(to)}&Body=${encodeURIComponent(body)}`); | ||
} | ||
await pool.done(); | ||
} | ||
} | ||
} | ||
if(typeof task.envelope == 'string') | ||
output = { [task.envelope]: output }; | ||
}; | ||
if(typeof task.apply == 'object') | ||
Object.assign(output, task.apply); | ||
async function createTransport(spec, type){ | ||
const name = type + Object.keys(this.global.transports).length; | ||
return output; | ||
if(typeof spec.$inherit == 'string'){ | ||
spec = { ...this.conf[spec.$inherit], ...spec }; | ||
delete spec.$inherit; | ||
} | ||
this.global.transports[name] = await TRANSPORTS[type].call(this, spec); | ||
this.log.debug('Installed transport \'%s\' (%s)', name, type); | ||
return name; | ||
} | ||
const LEVELS = { trace: 0, debug: 1, info: 2, warn: 3, error: 4, fatal: 5, unlimited: 50 }; | ||
module.exports = { | ||
@@ -219,14 +253,4 @@ | ||
for(const task of container.tasks){ | ||
if(task.level && json.level | ||
&& LEVELS[json.level] >= LEVELS[task.level.from] | ||
&& LEVELS[json.level] <= LEVELS[task.level.to]) | ||
continue; | ||
const input = transform(container, task, json); | ||
for(const transport of task.transports) | ||
Promise.resolve(transport.push(input)).catch(err => | ||
this.log.error({ err })); | ||
} | ||
Promise.resolve(this.global.processEntry(json, container)).catch(err => | ||
this.log.error({ err })); | ||
}, | ||
@@ -233,0 +257,0 @@ |
{ | ||
"name": "eavesdocker", | ||
"version": "0.2.0", | ||
"version": "0.3.0", | ||
"main": "lib/main.js", | ||
@@ -11,3 +11,3 @@ "scripts": { | ||
"mongodb": "^3.6.11", | ||
"muhb": "^3.0.4", | ||
"muhb": "^3.0.5", | ||
"nodecaf": "^0.11.9", | ||
@@ -17,3 +17,4 @@ "nodecaf-redis": "0.0.3", | ||
"toml": "^3.0.0", | ||
"uuid": "^8.3.2" | ||
"uuid": "^8.3.2", | ||
"yaml": "^1.10.2" | ||
}, | ||
@@ -20,0 +21,0 @@ "devDependencies": { |
251
test/spec.js
@@ -34,3 +34,4 @@ // var wtf = require('wtfnode'); | ||
token: 'bar', | ||
url: 'http://localhost:12345' | ||
url: 'http://localhost:12345', | ||
labels: [ 'level::{level}', 'barbar' ] | ||
}, | ||
@@ -42,8 +43,11 @@ emit: { event: 'foobar' } | ||
async function sleep(ms){ | ||
await new Promise(done => setTimeout(done, ms)); | ||
} | ||
before(async function(){ | ||
this.timeout(10e3); | ||
docker = new Docker(DEFAULT_CONF.docker); | ||
await docker.pull('mhart/alpine-node:slim-13'); | ||
await docker.pull('hello-world'); | ||
await docker.pull('alpine'); | ||
await docker.pull('mhart/alpine-node:slim-14'); | ||
await sleep(4000); | ||
}); | ||
@@ -60,17 +64,11 @@ | ||
async function sleep(ms){ | ||
await new Promise(done => setTimeout(done, ms)); | ||
} | ||
const debugLabels = { | ||
'com.docker.compose.project': 'foobar', | ||
'com.docker.compose.service': 'bazbaz' | ||
}; | ||
async function startBlab(){ | ||
const container = await docker.createContainer({ | ||
Image: 'mhart/alpine-node:slim-13', | ||
Image: 'mhart/alpine-node:slim-14', | ||
Tty: false, | ||
Init: true, | ||
Labels: debugLabels, | ||
Labels: { | ||
'com.docker.compose.project': 'foobar', | ||
'com.docker.compose.service': 'bazbaz' | ||
}, | ||
Cmd: [ 'node', '-e', 'setInterval(() => console.log(Date.now()), 600)' ] | ||
@@ -84,6 +82,9 @@ }); | ||
const container = await docker.createContainer({ | ||
Image: 'mhart/alpine-node:slim-13', | ||
Image: 'mhart/alpine-node:slim-14', | ||
Tty: false, | ||
Init: true, | ||
Labels: debugLabels, | ||
Labels: { | ||
'com.docker.compose.project': 'foobar', | ||
'com.docker.compose.service': 'bazbaz' | ||
}, | ||
Cmd: [ 'node', '-e', 'setTimeout(() => console.log(\'28234768\'), 2000);' + | ||
@@ -111,55 +112,12 @@ 'setTimeout(Function.prototype, 10000)'] | ||
describe('Task Definition', function(){ | ||
describe('Containers', function(){ | ||
it('Should not create tasks without a transport', async function(){ | ||
await app.setup({ tasks: { bar: {} } }); | ||
await app.start(); | ||
assert(!app.global.tasks.emit0); | ||
await app.stop(); | ||
}); | ||
it('Should skip transport with a bad reference', async function(){ | ||
await app.setup({ tasks: { bar: { emit: 'none' } } }); | ||
await app.start(); | ||
assert(!app.global.tasks.bar); | ||
await app.stop(); | ||
}); | ||
it('Should create defined tasks', async function(){ | ||
await app.setup({ tasks: { | ||
bar: { emit: 'emit' }, | ||
foo: { emit: { event: 'bazbah' } }, | ||
baz: { emit: 'emit' } | ||
} }); | ||
await app.start(); | ||
assert.strictEqual(Object.keys(app.global.transports).length, 2); | ||
assert(app.global.tasks.bar); | ||
assert(app.global.tasks.foo); | ||
assert(app.global.tasks.baz); | ||
await app.stop(); | ||
}); | ||
}); | ||
describe('Containers & Tasks', function(){ | ||
it('Should not attach containers that don\'t match any task', async function(){ | ||
this.timeout(8000); | ||
await app.start(); | ||
const [ , { id } ] = await docker.run('hello-world'); | ||
await sleep(2000); | ||
assert(!(id in app.global.containers)); | ||
await app.stop(); | ||
}); | ||
it('Should attach containers that match at least 1 task', async function(){ | ||
it('Should attach containers', async function(){ | ||
this.timeout(5000); | ||
await app.setup({ tasks: { bar: { emit: 'emit' } } }); | ||
await app.start(); | ||
const [ , { id } ] = await docker.run('hello-world', null, null, { | ||
Labels: debugLabels | ||
}); | ||
await sleep(300); | ||
assert(id in app.global.containers); | ||
const c = await startWaitForIt(); | ||
await sleep(500); | ||
assert(c.id in app.global.containers); | ||
await app.stop(); | ||
await c.kill(); | ||
}); | ||
@@ -170,3 +128,2 @@ | ||
const c = await startBlab(); | ||
await app.setup({ tasks: { bar: { emit: 'emit' } } }); | ||
await app.start(); | ||
@@ -179,24 +136,21 @@ await sleep(500); | ||
it('Should not fail when can\'t attach to some container', async function(){ | ||
await app.setup({ tasks: { bar: { emit: 'emit' } } }); | ||
it('Should forget containers after they die', async function(){ | ||
this.timeout(3000); | ||
await app.start(); | ||
const [ , { id } ] = await docker.run('hello-world', null, null, { | ||
Labels: debugLabels, | ||
HostConfig: { LogConfig: { type: 'none' } } | ||
}); | ||
await sleep(400); | ||
assert(!(id in app.global.containers)); | ||
const c = await startWaitForIt(); | ||
await c.kill(); | ||
await sleep(1000); | ||
assert(!(c.id in app.global.containers)); | ||
await app.stop(); | ||
}); | ||
it('Should forget containers after they die', async function(){ | ||
it('Should not attach containers filtered out in the global line', async function(){ | ||
this.timeout(8000); | ||
await app.setup({ tasks: { bar: { emit: 'emit' } } }); | ||
app.setup({ pipeline: [ { stack: 'none' } ] }); | ||
await app.start(); | ||
const [ , { id } ] = await docker.run('hello-world', null, null, { | ||
Labels: debugLabels | ||
}); | ||
const c = await startWaitForIt(); | ||
await sleep(2000); | ||
assert(!(id in app.global.containers)); | ||
assert(!(c.id in app.global.containers)); | ||
await app.stop(); | ||
await c.kill(); | ||
}); | ||
@@ -215,21 +169,19 @@ | ||
it('Should apply keys to log entry', function(done){ | ||
this.timeout(9000); | ||
// it('Should apply keys to log entry', function(done){ | ||
// this.timeout(9000); | ||
process.once('foobar', function(msg){ | ||
assert.strictEqual(msg.test, 'foo'); | ||
done(); | ||
}); | ||
// process.once('foobar', function(msg){ | ||
// assert.strictEqual(msg.test, 'foo'); | ||
// done(); | ||
// }); | ||
(async function(){ | ||
await app.setup({ tasks: { | ||
foobar: { | ||
emit: 'emit', | ||
apply: { test: 'foo' } | ||
} | ||
} }); | ||
await app.start(); | ||
c = await startWaitForIt(); | ||
})(); | ||
}); | ||
// (async function(){ | ||
// app.setup({ pipeline: [ | ||
// { emit: 'foobar' }, | ||
// { apply: { test: 'foo' } } | ||
// ] }); | ||
// await app.start(); | ||
// c = await startWaitForIt(); | ||
// })(); | ||
// }); | ||
@@ -244,8 +196,6 @@ it('Should ensure given key is a datetime', function(done){ | ||
(async function(){ | ||
await app.setup({ tasks: { | ||
foobar: { | ||
emit: 'emit', | ||
time: 'foo' | ||
} | ||
} }); | ||
app.setup({ pipeline: [ | ||
{ time: 'foo' }, | ||
{ emit: { event: 'foobar' } } | ||
] }); | ||
await app.start(); | ||
@@ -265,8 +215,6 @@ c = await startWaitForIt(); | ||
(async function(){ | ||
await app.setup({ tasks: { | ||
foobar: { | ||
emit: 'emit', | ||
includeSource: true | ||
} | ||
} }); | ||
app.setup({ pipeline: [ | ||
'includeSource', | ||
{ emit: { event: 'foobar' } } | ||
] }); | ||
await app.start(); | ||
@@ -286,8 +234,6 @@ c = await startWaitForIt(); | ||
(async function(){ | ||
await app.setup({ tasks: { | ||
foobar: { | ||
emit: 'emit', | ||
envelope: 'final' | ||
} | ||
} }); | ||
app.setup({ pipeline: [ | ||
{ envelope: 'final' }, | ||
{ emit: { event: 'foobar' } } | ||
] }); | ||
await app.start(); | ||
@@ -304,5 +250,3 @@ c = await startWaitForIt(); | ||
this.timeout(8000); | ||
await app.setup({ tasks: { | ||
foobar: { mongo: { url: MONGO_URL }, } | ||
} }); | ||
app.setup({ pipeline: [ { mongo: { url: MONGO_URL } } ] }); | ||
await app.start(); | ||
@@ -328,3 +272,6 @@ const c = await startWaitForIt(); | ||
(async function(){ | ||
await app.setup({ tasks: { foobar: { redispub: 'redis', stack: 'foobar' } } }); | ||
app.setup({ pipeline: [ | ||
{ stack: 'foobar' }, | ||
{ redispub: { $inherit: 'redis' } } | ||
] }); | ||
await app.start(); | ||
@@ -349,3 +296,6 @@ | ||
await muhb.delete(MC_HOST + '/messages'); | ||
await app.setup({ tasks: { foobar: { email: 'smtp', stack: 'foobar' } } }); | ||
app.setup({ pipeline: [ | ||
{ stack: 'foobar' }, | ||
{ email: { $inherit: 'smtp' } } | ||
] }); | ||
await app.start(); | ||
@@ -360,2 +310,3 @@ const c = await startWaitForIt(); | ||
assert.strictEqual(obj.length, 1); | ||
assert.strictEqual(obj[0].subject.indexOf('debug -'), 0); | ||
}); | ||
@@ -376,3 +327,5 @@ | ||
await app.setup({ tasks: { foobar: { webhook: { url: 'http://localhost:8765/test' } } } }); | ||
app.setup({ pipeline: [ | ||
{ webhook: { url: 'http://localhost:8765/test' } } | ||
] }); | ||
await app.start(); | ||
@@ -389,3 +342,3 @@ const c = await startWaitForIt(); | ||
it('Should crate a gitlab issue', async function(){ | ||
it('Should create a gitlab issue', async function(){ | ||
this.timeout(8000); | ||
@@ -402,3 +355,7 @@ | ||
assert.strictEqual(params.id, '23489'); | ||
JSON.parse(body.toString()); | ||
body = JSON.parse(body.toString()); | ||
assert.strictEqual(body.labels[0], 'level::debug'); | ||
assert.strictEqual(body.labels[1], 'barbar'); | ||
assert.strictEqual(body.title.indexOf('debug - '), 0); | ||
gotIt = true; | ||
@@ -412,3 +369,3 @@ res.end(); | ||
await app.setup({ tasks: { foobar: { gitlab: 'gitlab' } } }); | ||
app.setup({ pipeline: [ { gitlab: { $inherit: 'gitlab' } } ] }); | ||
@@ -425,2 +382,40 @@ await app.start(); | ||
it('Should send an SMS via Twilio', async function(){ | ||
this.timeout(8000); | ||
let gotIt; | ||
const Nodecaf = require('nodecaf'); | ||
const s = new Nodecaf({ conf: { port: 12345 }, api: function({ post }){ | ||
post('/', ({ headers, body, res }) => { | ||
const [ user, pass ] = Buffer.from(headers.authorization.split(' ')[1], 'base64').toString('ascii').split(':'); | ||
assert.strictEqual(user, 'foo'); | ||
assert.strictEqual(pass, 'bar'); | ||
assert.strictEqual(body.From, '123'); | ||
assert.strictEqual(body.To, '123'); | ||
assert.strictEqual(body.Body.indexOf('message: '), 0); | ||
gotIt = true; | ||
res.end(); | ||
}); | ||
} }); | ||
await s.start(); | ||
app.setup({ pipeline: [ { twilio: { | ||
sid: 'foo', token: 'bar', from: '123', fakeHost: 'localhost:12345', | ||
to: [ '123' ] | ||
} } ] }); | ||
await app.start(); | ||
const c = await startWaitForIt(); | ||
await sleep(5000); | ||
await c.kill(); | ||
await app.stop(); | ||
await s.stop(); | ||
assert(gotIt); | ||
}); | ||
}); | ||
@@ -435,5 +430,5 @@ | ||
await app.setup({ | ||
tasks: { foobar: { emit: 'emit' } }, | ||
healthRedis: 'redis' | ||
app.setup({ | ||
health: { $inherit: 'redis' }, | ||
pipeline: [ { emit: { event: 'emit' } } ] | ||
}); | ||
@@ -440,0 +435,0 @@ await app.start(); |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Uses eval
Supply chain riskPackage uses dynamic code execution (e.g., eval()), which is a dangerous practice. This can prevent the code from running in certain environments and increases the risk that the code may contain exploits or malicious behavior.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
39503
787
8
4
+ Addedyaml@^1.10.2
+ Addedyaml@1.10.2(transitive)
Updatedmuhb@^3.0.5