diff --git a/.DS_Store b/.DS_Store index de51a46..91b761c 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..cb21727 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "editor.fontSize": 16, + "terminal.integrated.fontSize": 16 +} \ No newline at end of file diff --git a/bo.yml b/bo.yml deleted file mode 100644 index 80e5aa5..0000000 --- a/bo.yml +++ /dev/null @@ -1,21 +0,0 @@ ---- - apiVersion: extensions/v1beta1 - kind: Deployment - metadata: - name: kube-monkey - namespace: kube-system - spec: - replicas: 1 - template: - metadata: - labels: - app: kube-monkey - spec: - containers: - - name: kube-monkey - command: - - "/kube-monkey" - args: ["-v=5", "-log_dir=/var/log/kube-monkey"] - image: ayushsobti/kube-monkey:v0.2.3 - - \ No newline at end of file diff --git a/chaos-control/chaos-control.js b/chaos-control/chaos-control.js index 13585f6..ec188c9 100644 --- a/chaos-control/chaos-control.js +++ b/chaos-control/chaos-control.js @@ -2,245 +2,56 @@ const bodyParser = require("body-parser"); const express = require("express"); class ChaosControl { - constructor(expressApplication, configuration) { - console.log(`Chaos monkey is set to active`); - this.configuration = configuration; + constructor(expressApplication) { + console.log(`Chaos control is now starting`); + + //TODO: should we really rely on Express (which also demand the user to pass in params)? + //maybe we can intercept on the HTTP layer this.app = expressApplication; } start() { - console.log(`Opening the zoo now`); + console.log(`Opening the chaos zoo now`); if (this.configuration.startMode === "config") { this.startAllConfigurationPranks([this.app]); } - if (this.app) { - this.registerAPI(); - } } - registerAPI() { - this.app.use( - bodyParser.urlencoded({ - extended: true - }) - ); - this.app.use(bodyParser.json()); + + getPrankSchedule(prankDefinition) { + const ScheduleClass = require(`../schedules/${prankDefinition.schedule.type}`); - const router = express.Router(); - router.post("/chaos/pranks", (req, res) => { - try { - console.log( - `Chaos gate was asked to start a new prank ${JSON.stringify( - req.body - )}` - ); - this.startPrankActivity(req.body, [this.app]); - res.status(200).json({ - status: "OK" - }); - } catch (e) { - console.log(e); - res.status(500).json(e); - } - }); - this.app.use(router); + return new ScheduleClass(prankDefinition.schedule); } - getPrankSchedule(prankConfig) { - const ScheduleClass = require(`../schedules/${prankConfig.schedule.type}`); - - return new ScheduleClass(prankConfig.schedule); - } + startPrankExecution(prankDefinition, prankParams=[]) { + if (prankDefinition.active === false) { + console.info(`Prank ${prankDefinition.name} is not active so not starting`); + return; + } - getPranksActivity() { - //temporarily hard-coded, to be fixed in few days - const result = { + console.info( + `Chaos control is about to start the Prank ${prankDefinition.name}` + ); - }; + const PrankExecutor = require(`../pranks/${prankDefinition.file}`); + const PrankSchedule = this.getPrankSchedule(prankDefinition); - return result; + const prankToStart = new PrankExecutor( + prankDefinition, + PrankSchedule, + [...prankParams, this.app] + ); + PrankSchedule.start(); } - getPranksPool() { - //temporarily hard-coded, to be fixed in few days - const result = [{ - name: "500-error-on-route", - friendlyName: "API route returns 500 error", - file: "500-error-on-route", - active: true, - properties: { - urls: ["/api/products", "/anyurl"] - }, - schedule: { - type: "immediate-schedule", - fadeOutInMS: 10000 - }, - description: 'Our monkey intercepts HTTP routes and return errors on your behalf', - lastHappened: new Date(), - expectations: 'Your monitoring system should notify, error should appear in log', - reality: 'This feature is not implemented yet', - success: 'Yes' - }, - { - name: "Process-exit", - friendlyName: "Process exit", - file: "process-exit", - active: true, - properties: { - exitCode: 1 - }, - schedule: { - type: "immediate-schedule", - fadeOutInMS: 10000 - }, - description: 'Our monkey kills the process', - lastHappened: new Date(), - expectations: 'Your monitoring system should notify, a new process should be instantiated', - reality: 'This feature is not implemented yet', - success: 'No' - }, - { - name: "uncaught-exception", - friendlyName: "Uncaught exception", - file: "uncaught-exception", - active: true, - properties: { - message: "Uncaught exception was thrown by the chaos monkey" - }, - schedule: { - type: "one-time-schedule", - delay: 12000 - }, - description: 'Our monkey Throws an uncaught exception into the void', - lastHappened: new Date(), - expectations: 'Your monitoring system should notify, process should stay alive or new one should get instantiated', - reality: 'This feature is not implemented yet', - success: 'No' - }, - { - name: "unhandled-rejection", - friendlyName: "Unhandled promise rejection", - file: "unhandled-rejection", - active: true, - properties: { - message: "Uncaught rejection was thrown by the chaos monkey" - }, - schedule: { - type: "one-time-schedule", - delay: 10000 - }, - description: 'Our monkey Throws an uncaught promise rejection into the void', - lastHappened: new Date(), - expectations: 'Your monitoring system should notify, process should stay alive or new one should get instantiated', - reality: 'This feature is not implemented yet', - success: 'No' - }, - { - name: "memory-load", - friendlyName: "Memory is overloaded", - file: "memory-load", - active: true, - properties: { - maxMemorySizeInMB: 10 - }, - schedule: { - type: "one-time-schedule", - delay: 1000, - fadeOutInMS: 30000 - }, - description: 'Our monkey constantly increases the RAM, if no limit was specified the entire allowed 1.7GB will be used', - lastHappened: new Date(), - expectations: 'Your monitoring system should notify', - reality: 'This feature is not implemented yet', - success: 'No' - }, - { - name: "cpu-load", - friendlyName: "Busy CPU", - file: "cpu-load", - active: true, - properties: {}, - schedule: { - type: "peaks", - sleepTimeBetweenPeaksInMS: 3000, - pickLengthInMS: 10000, - forHowLong: 8000 - }, - description: 'Our monkey constantly keeps the CPU busy (without blocking the event loop)', - lastHappened: new Date(), - expectations: 'Your monitoring system should notify', - reality: 'This feature is not implemented yet', - success: 'No' - }, - { - name: "blocked-event-loop", - friendlyName: "Blocked event loop", - file: "blocked-event-loop", - active: true, - properties: {}, - schedule: { - type: "peaks", - sleepTimeBetweenPeaksInMS: 3000, - pickLengthInMS: 10000, - forHowLong: 8000 - }, - description: 'Our monkey constantly blocks the event loop', - lastHappened: new Date(), - expectations: 'Your monitoring system should notify', - reality: 'This feature is not implemented yet', - success: 'No' - }, - { - name: "broken-http-integration", - friendlyName: "Broken Integration", - file: "broken-http-integration", - active: true, - properties: {}, - schedule: { - type: "peaks", - sleepTimeBetweenPeaksInMS: 3000, - pickLengthInMS: 10000, - forHowLong: 8000 - }, - description: 'Our monkey intercepts HTTP calls and return random HTTP erros on their behalf', - lastHappened: new Date(), - expectations: 'Your monitoring system should notify, code should find alternate routes to achieve a decent UX', - reality: 'This feature is not implemented yet', - success: 'No' - } - ]; - - return result; -} - -startPrankActivity(prankConfig, prankParams) { - if (prankConfig.active === false) { - console.info(`Prank ${prankConfig.name} is not active so not starting`); - return; + startAllConfigurationPranks(prankParams) { + this.configuration.pranks + .filter(prank => prank.active === true) + .forEach(prankConfiguration => { + this.startPrankExecution(prankConfiguration, prankParams); + }); } - - console.info( - `Chaos control is about to start the Prank ${prankConfig.name}` - ); - - const PrankClass = require(`../pranks/${prankConfig.file}`); - console.log(PrankClass); - const PrankSchedule = this.getPrankSchedule(prankConfig); - const prankToStart = new PrankClass( - prankConfig, - PrankSchedule, - ...prankParams - ); - PrankSchedule.start(); -} - -startAllConfigurationPranks(prankParams) { - this.configuration.pranks - .filter(prank => prank.active === true) - .forEach(prankConfiguration => { - this.startPrankActivity(prankConfiguration, prankParams); - }); -} } module.exports = ChaosControl; \ No newline at end of file diff --git a/misc/chaos-monkey.png b/docs/chaos-monkey.png similarity index 100% rename from misc/chaos-monkey.png rename to docs/chaos-monkey.png diff --git a/entry-points/api.js b/entry-points/api.js new file mode 100644 index 0000000..fa25bb0 --- /dev/null +++ b/entry-points/api.js @@ -0,0 +1,95 @@ +const express = require('express'); +const ChaosControl = require("../chaos-control"); +const PranksDefinition = require('../pranks/pranks-definition'); +const bodyParser = require('body-parser') +const util = require('util'); +var cors = require('cors'); +const path = require('path'); +const publicPath = path.join('ui', 'build'); + +class API { + constructor(expressApp) { + console.log('Chaos API is about to register routes, websockets and listen') + const router = express.Router(); + this.registerMiddlewares(expressApp); + expressApp.use('/chaos', router); + this.listenToRoutes(router); + this.registerWebSockets(); + this.ChaosControl = new ChaosControl(expressApp); + } + + registerMiddlewares(expressApp) { + expressApp.use( + bodyParser.urlencoded({ + extended: true + }) + ); + expressApp.use(cors()); + expressApp.options(cors()); + expressApp.use(express.static(publicPath)); + expressApp.use(bodyParser.json()) + } + + listenToRoutes(router) { + router.post("/pranks/execute", (req, res) => { + try { + console.log(`Chaos gate was asked to start a new prank activity ${util.inspect(req.body)}`); + this.ChaosControl.startPrankExecution(req.body); + res.status(200).json({ + status: "OK" + }); + } catch (e) { + console.log(e); + res.status(500).json(e); + } + }); + + router.get("/pranks/definition", (req, res) => { + try { + console.log(`Chaos gate was asked to get all pranks definition`); + const result = new PranksDefinition().getAll + res.status(200).json(result); + } catch (e) { + res.status(500).json(e); + } + }); + + + } + + registerWebSockets() { + const http = require('http'); + + const server = http.createServer((req, res) => { + console.log('Web socket connection was called with a plain HTTP request, hmmm') + }); + + const webSocketConnection = require('socket.io').listen(server); + //TODO: allow the user to determine the websocket port + server.listen(9090); + + webSocketConnection.on('error', (error) => { + console.log(error); + }); + webSocketConnection.on('connection', (socket) => { + console.log('a user connected'); + + const prankActivity = new PranksDefinition().getAll(); + + setInterval(() => { + console.log('Emitting prank activity'); + const prankToEmit = prankActivity[Math.ceil(Math.random() * prankActivity.length - 1)]; + if (prankToEmit.name === "blocked-event-loop" || prankToEmit.name === "memory-load" || prankToEmit.name === "uncaught-exception" || prankToEmit.name === "cpu-load") { + socket.broadcast.emit('new-prank-activity', prankToEmit); + } + }, 1000); + + + socket.on('disconnect', function () { + console.log('user disconnected'); + }); + }); + } +} + +module.exports = API; \ No newline at end of file diff --git a/entry-points/command-line.js b/entry-points/command-line.js new file mode 100644 index 0000000..69625c0 --- /dev/null +++ b/entry-points/command-line.js @@ -0,0 +1 @@ +//Not implemented yet, would you like to make it happen? \ No newline at end of file diff --git a/gates/webapp.js b/gates/webapp.js deleted file mode 100644 index 5ca14cb..0000000 --- a/gates/webapp.js +++ /dev/null @@ -1,86 +0,0 @@ -const express = require('express'); -const expressApp = express(); -const http = require('http').Server(expressApp); -const io = require('socket.io'); -const ChaosControl = require("../chaos-control"); -const bodyParser = require('body-parser') -const util = require('util'); -var cors = require('cors'); -const path = require('path'); -const publicPath = path.join('ui', 'build'); - - -const router = express.Router(); -expressApp.use( - bodyParser.urlencoded({ - extended: true - }) -); -expressApp.use(cors()); -expressApp.options(cors()); -expressApp.use(express.static(publicPath)); - -expressApp.use(bodyParser.json()); - -http.listen(8081, function () { - console.log('listening on *:8081') -}); - -const webSocketConnection = io.listen(http); - -router.post("/chaos/pranks-activity", (req, res) => { - try { - console.log(`Chaos gate was asked to start a new prank activity ${util.inspect(req.body)}`); - new ChaosControl().startPrankActivity(req.body, [expressApp]); - res.status(200).json({ - status: "OK" - }); - } catch (e) { - console.log(e); - res.status(500).json(e); - } -}); - -router.get("/chaos/pranks-pool", (req, res) => { - try { - console.log(`Chaos gate was asked to get all pranks`); - const result = new ChaosControl().getPranksPool(); - res.status(200).json(result); - } catch (e) { - res.status(500).json(e); - } -}); - -router.get("/api/product/:id", (req, res) => { - try { - console.log(`Chaos example route was called`); - setTimeout(() => { - res.status(200).json({}); - }, 50); - - } catch (e) { - res.status(500).json(e); - } -}); - - -expressApp.use(router); - -webSocketConnection.on('error', (error) => { - console.log(error); -}); -webSocketConnection.on('connection', (socket) => { - console.log('a user connected'); - - const prankActivity = new ChaosControl().getPranksPool(); - - setInterval(() => { - console.log('Emitting prank activity'); - socket.broadcast.emit('new-prank-activity', prankActivity[Math.ceil(Math.random() * prankActivity.length - 1)]); - }, 7000); - - - socket.on('disconnect', function () { - console.log('user disconnected'); - }); -}); \ No newline at end of file diff --git a/index.js b/index.js index c822f47..d05f0d9 100644 --- a/index.js +++ b/index.js @@ -1,3 +1,11 @@ -//initialize all gates (triggers to start a chaos) +const API = require('./entry-points/api'); +const ChaosControl = require('./chaos-control'); -require('./gates/webapp'); \ No newline at end of file +module.exports.initialize = (expressApp) => { + return { + api: new API(expressApp), + chaosControl: new ChaosControl(expressApp) + }; +}; + +//TODO: add command-line entry point as well \ No newline at end of file diff --git a/package.json b/package.json index 44cf987..4b5e7ba 100644 --- a/package.json +++ b/package.json @@ -5,8 +5,9 @@ "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", + "test:manual": "npm run build:ui && nodemon ./tests/manual-testing-with-simple-web-app/index.js", "start": "node ./index.js", - "start:dev": "nodemon ./index.js", + "start:dev": "npm run build:ui && nodemon ./index.js", "build:ui": "npm install --prefix ui && npm run build --prefix ui", "serve": "concurrently \"npm run build:ui\" \"npm run start\"", "start:app": "npm run build:ui && npm run start" diff --git a/pranks/500-error-on-route.js b/pranks/500-error-on-route.js index aed6da9..6b68db6 100644 --- a/pranks/500-error-on-route.js +++ b/pranks/500-error-on-route.js @@ -1,6 +1,6 @@ -const PrankBase = require("./prank-base"); +const PrankExecutorBase = require("./prank-executor-base"); const removeRoute = require('express-remove-route'); -class Route500ErrorSin extends PrankBase { +class Route500ErrorSin extends PrankExecutorBase { constructor() { super(...arguments); } diff --git a/pranks/cpu-load.js b/pranks/cpu-load.js index a088389..6f6f16b 100644 --- a/pranks/cpu-load.js +++ b/pranks/cpu-load.js @@ -1,11 +1,11 @@ -const PrankBase = require("./prank-base"); +const PrankExecutorBase = require("./prank-executor-base"); const sizeOf = require('object-sizeof'); const fs = require('fs'); const util = require('util'); const readFilePromise = util.promisify(fs.readFile); const path = require('path'); -class OverloadCPU extends PrankBase { +class OverloadCPU extends PrankExecutorBase { constructor(expressApp) { super(...arguments); } diff --git a/pranks/memory-load.js b/pranks/memory-load.js index 0f75620..05dd761 100644 --- a/pranks/memory-load.js +++ b/pranks/memory-load.js @@ -1,11 +1,11 @@ -const PrankBase = require("./prank-base"); +const PrankExecutorBase = require("./prank-executor-base"); const sizeOf = require('object-sizeof'); const fs = require('fs'); const util = require('util'); const readFilePromise = util.promisify(fs.readFile); const path = require('path'); -class overloadMemory extends PrankBase { +class overloadMemory extends PrankExecutorBase { constructor(expressApp) { super(...arguments); } diff --git a/pranks/prank-base.js b/pranks/prank-executor-base.js similarity index 89% rename from pranks/prank-base.js rename to pranks/prank-executor-base.js index 20e4a28..42962b3 100644 --- a/pranks/prank-base.js +++ b/pranks/prank-executor-base.js @@ -1,6 +1,6 @@ const EventEmitter = require("events"); -class PrankBase extends EventEmitter { +class PrankExecutorBase extends EventEmitter { constructor(configuration, schedule, expressApp) { super(); console.log(`Sin ${configuration.name} is now initialized with the the @@ -21,4 +21,4 @@ class PrankBase extends EventEmitter { } } -module.exports = PrankBase; \ No newline at end of file +module.exports = PrankExecutorBase; \ No newline at end of file diff --git a/pranks/pranks-definition.js b/pranks/pranks-definition.js new file mode 100644 index 0000000..d24156a --- /dev/null +++ b/pranks/pranks-definition.js @@ -0,0 +1,155 @@ +class PranksDefinition{ + getAll() { + const result = [{ + name: "500-error-on-route", + friendlyName: "API route returns 500 error", + file: "500-error-on-route", + active: true, + properties: { + urls: ["/api/products", "/anyurl"] + }, + schedule: { + type: "immediate-schedule", + fadeOutInMS: 10000 + }, + description: 'Our monkey intercepts HTTP routes and return errors on your behalf', + lastHappened: new Date(), + expectations: 'Your monitoring system should notify, error should appear in log', + reality: 'This feature is not implemented yet', + success: 'Yes' + }, + { + name: "Process-exit", + friendlyName: "Process exit", + file: "process-exit", + active: true, + properties: { + exitCode: 1 + }, + schedule: { + type: "immediate-schedule", + fadeOutInMS: 10000 + }, + description: 'Our monkey kills the process', + lastHappened: new Date(), + expectations: 'Your monitoring system should notify, a new process should be instantiated', + reality: 'This feature is not implemented yet', + success: 'No' + }, + { + name: "uncaught-exception", + friendlyName: "Uncaught exception", + file: "uncaught-exception", + active: true, + properties: { + message: "Uncaught exception was thrown by the chaos monkey" + }, + schedule: { + type: "one-time-schedule", + delay: 12000 + }, + description: 'Our monkey Throws an uncaught exception into the void', + lastHappened: new Date(), + expectations: 'Your monitoring system should notify, process should stay alive or new one should get instantiated', + reality: 'This feature is not implemented yet', + success: 'No' + }, + { + name: "unhandled-rejection", + friendlyName: "Unhandled promise rejection", + file: "unhandled-rejection", + active: true, + properties: { + message: "Uncaught rejection was thrown by the chaos monkey" + }, + schedule: { + type: "one-time-schedule", + delay: 10000 + }, + description: 'Our monkey Throws an uncaught promise rejection into the void', + lastHappened: new Date(), + expectations: 'Your monitoring system should notify, process should stay alive or new one should get instantiated', + reality: 'This feature is not implemented yet', + success: 'No' + }, + { + name: "memory-load", + friendlyName: "Memory is overloaded", + file: "memory-load", + active: true, + properties: { + maxMemorySizeInMB: 10 + }, + schedule: { + type: "one-time-schedule", + delay: 1000, + fadeOutInMS: 30000 + }, + description: 'Our monkey constantly increases the RAM, if no limit was specified the entire allowed 1.7GB will be used', + lastHappened: new Date(), + expectations: 'Your monitoring system should notify', + reality: 'This feature is not implemented yet', + success: 'No' + }, + { + name: "cpu-load", + friendlyName: "Busy CPU", + file: "cpu-load", + active: true, + properties: {}, + schedule: { + type: "peaks", + sleepTimeBetweenPeaksInMS: 3000, + pickLengthInMS: 10000, + forHowLong: 8000 + }, + description: 'Our monkey constantly keeps the CPU busy (without blocking the event loop)', + lastHappened: new Date(), + expectations: 'Your monitoring system should notify', + reality: 'This feature is not implemented yet', + success: 'No' + }, + { + name: "blocked-event-loop", + friendlyName: "Blocked event loop", + file: "blocked-event-loop", + active: true, + properties: {}, + schedule: { + type: "peaks", + sleepTimeBetweenPeaksInMS: 3000, + pickLengthInMS: 10000, + forHowLong: 8000 + }, + description: 'Our monkey constantly blocks the event loop', + lastHappened: new Date(), + expectations: 'Your monitoring system should notify', + reality: 'This feature is not implemented yet', + success: 'No' + }, + { + name: "broken-http-integration", + friendlyName: "Broken Integration", + file: "broken-http-integration", + active: true, + properties: {}, + schedule: { + type: "peaks", + sleepTimeBetweenPeaksInMS: 3000, + pickLengthInMS: 10000, + forHowLong: 8000 + }, + description: 'Our monkey intercepts HTTP calls and return random HTTP erros on their behalf', + lastHappened: new Date(), + expectations: 'Your monitoring system should notify, code should find alternate routes to achieve a decent UX', + reality: 'This feature is not implemented yet', + success: 'No' + } + ]; + + return result; + } + +} + +module.exports = PranksDefinition; \ No newline at end of file diff --git a/pranks/process-exit.js b/pranks/process-exit.js index 807d3f7..c7e518e 100644 --- a/pranks/process-exit.js +++ b/pranks/process-exit.js @@ -1,5 +1,5 @@ -const PrankBase = require("./prank-base"); -class ProcessExitSin extends PrankBase { +const PrankExecutorBase = require("./prank-executor-base"); +class ProcessExitSin extends PrankExecutorBase { constructor(expressApp) { super(...arguments); } diff --git a/pranks/uncaught-exception.js b/pranks/uncaught-exception.js index 519eecc..12ce7bf 100644 --- a/pranks/uncaught-exception.js +++ b/pranks/uncaught-exception.js @@ -1,6 +1,6 @@ -const PrankBase = require("./prank-base"); +const PrankExecutorBase = require("./prank-executor-base"); -class Route500ErrorSin extends PrankBase { +class Route500ErrorSin extends PrankExecutorBase { constructor(expressApp) { super(...arguments); diff --git a/pranks/unhandled-rejection.js b/pranks/unhandled-rejection.js index 335ab8c..b336d7e 100644 --- a/pranks/unhandled-rejection.js +++ b/pranks/unhandled-rejection.js @@ -1,6 +1,6 @@ -const PrankBase = require("./prank-base"); +const PrankExecutorBase = require("./prank-executor-base"); -class UnhandledRejection extends PrankBase { +class UnhandledRejection extends PrankExecutorBase { constructor(expressApp) { super(...arguments); diff --git a/readme.md b/readme.md index 63c4c72..aabf2ce 100644 --- a/readme.md +++ b/readme.md @@ -1,5 +1,5 @@