Browse Source

Initial commit

develop
William Petit 4 years ago
parent
commit
bacacc8dee
13 changed files with 550 additions and 0 deletions
  1. 7
    0
      .editorconfig
  2. 4
    0
      .gitignore
  3. 8
    0
      app.js
  4. 102
    0
      client/index.html
  5. 93
    0
      client/js/app.js
  6. 4
    0
      handlers/build-package-from-git
  7. 2
    0
      jobs/.gitignore
  8. 20
    0
      lib/config.js
  9. 54
    0
      lib/db-rest.js
  10. 5
    0
      lib/db.js
  11. 126
    0
      lib/hook-server.js
  12. 102
    0
      lib/web-app.js
  13. 23
    0
      package.json

+ 7
- 0
.editorconfig View File

@@ -0,0 +1,7 @@
1
+root = true
2
+
3
+[*]
4
+charset = utf-8
5
+indent_style = space
6
+indent_size = 2
7
+trim_trailing_whitespace = true

+ 4
- 0
.gitignore View File

@@ -0,0 +1,4 @@
1
+*.log
2
+/data
3
+*~
4
+node_modules

+ 8
- 0
app.js View File

@@ -0,0 +1,8 @@
1
+var HookServer = require('./lib/hook-server');
2
+var WebApp = require('./lib/web-app');
3
+
4
+var hs = new HookServer();
5
+hs.start();
6
+
7
+var app = new WebApp();
8
+app.start();

+ 102
- 0
client/index.html View File

@@ -0,0 +1,102 @@
1
+<html>
2
+  <head>
3
+    <meta charset="utf-8">
4
+    <title>Marang</title>
5
+    <link rel="stylesheet" href="vendor/bootstrap/dist/css/bootstrap.min.css">
6
+  </head>
7
+  <body>
8
+    <div class="container">
9
+      <div class="row">
10
+        <div class="col-md-12">
11
+          <h1>Marang</h1>
12
+
13
+          <ul class="nav nav-tabs" role="tablist">
14
+            <li role="presentation" class="active"><a href="#jobs" aria-controls="jobs" role="tab" data-toggle="tab">Jobs</a></li>
15
+            <li role="presentation"><a href="#slots" aria-controls="slots" role="tab" data-toggle="tab">Slots</a></li>
16
+          </ul>
17
+
18
+          <div class="tab-content">
19
+            <div role="tabpanel" class="tab-pane active" id="jobs">
20
+              <ul class="list-group" style="margin-top:15px;" id="jobs-list"></ul>
21
+            </div>
22
+            <div role="tabpanel" class="tab-pane" id="slots">
23
+              <div class="clearfix" style="margin-top:15px;">
24
+                <button type="button" class="btn btn-success pull-right"
25
+                  data-toggle="modal" data-target="#addSlotModal">
26
+                  Add slot  <span class="glyphicon glyphicon-plus"></span>
27
+                </button>
28
+              </div>
29
+              <ul class="list-group" style="margin-top:15px;" id="slots-list"></ul>
30
+            </div>
31
+          </div>
32
+      </div>
33
+    </div>
34
+
35
+    <div class="modal fade" id="addSlotModal">
36
+      <div class="modal-dialog">
37
+        <div class="modal-content">
38
+          <div class="modal-header">
39
+            <button type="button" class="close" data-dismiss="modal" aria-label="Close">
40
+              <span aria-hidden="true">&times;</span>
41
+            </button>
42
+            <h4 class="modal-title">Add slot</h4>
43
+          </div>
44
+          <div class="modal-body">
45
+            <form class="form" id="addSlotForm">
46
+              <div class="form-group">
47
+                <label for="slotLabelInput">Label</label>
48
+                <input name="label" required type="text" class="form-control" id="slotLabelInput" placeholder="Slot's label">
49
+              </div>
50
+              <div class="form-group">
51
+                <label for="slotHandlerInput">Handler</label>
52
+                <select name="handler" class="form-control" required id="slotHandlerInput"></select>
53
+              </div>
54
+              <div class="form-group">
55
+                <label for="slotTokenInput">Token</label>
56
+                <input name="token" type="text" class="form-control" readonly required id="slotTokenInput">
57
+              </div>
58
+            </form>
59
+          </div>
60
+          <div class="modal-footer">
61
+            <button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
62
+            <button type="button" class="btn btn-primary" id="addSlotCreateButton">Create</button>
63
+          </div>
64
+        </div>
65
+      </div>
66
+    </div>
67
+
68
+    <script type="text/x-template-handlebars" id="slotItemTmpl">
69
+      <li class="list-group-item">
70
+        <button data-slot-token="{{ token }}"
71
+          disabled
72
+          class="btn btn-danger pull-right">
73
+          Delete
74
+        </button>
75
+        <h4 class="list-group-item-heading">{{ label }}</h4>
76
+        <p class="list-group-item-text">
77
+          <strong>Token</strong> {{ token }}<br />
78
+          <strong>Handler</strong> {{ handler }}
79
+        </p>
80
+      </li>
81
+    </script>
82
+
83
+    <script type="text/x-template-handlebars" id="jobItemTmpl">
84
+      <li class="list-group-item">
85
+        <h4 class="list-group-item-heading">Job {{ slotLabel }}/{{timestamp}}</h4>
86
+        {{#if hasTerminated }}
87
+
88
+        {{/if}}
89
+        <p class="list-group-item-text">
90
+        <strong>Slot</strong> {{ slotLabel }}<br />
91
+        <strong>Handler</strong> {{ handler }}<br />
92
+        <a href="#" data-job="{{timestamp}}-{{slotToken}}">Show job output<a>
93
+        </p>
94
+      </li>
95
+    </script>
96
+
97
+    <script src="vendor/handlebars/dist/handlebars.min.js"></script>
98
+    <script src="vendor/jquery/dist/jquery.min.js"></script>
99
+    <script src="vendor/bootstrap/dist/js/bootstrap.min.js"></script>
100
+    <script src="js/app.js"></script>
101
+  </body>
102
+</html>

+ 93
- 0
client/js/app.js View File

@@ -0,0 +1,93 @@
1
+$(function() {
2
+
3
+  var $slotsList = $('#slots-list');
4
+  var $jobsList = $('#jobs-list');
5
+  var $slotHandlerInput = $('#slotHandlerInput');
6
+  var $slotTokenInput = $('#slotTokenInput');
7
+  var $addSlotForm = $('#addSlotForm');
8
+  var $addSlotModal = $('#addSlotModal');
9
+
10
+  var slotItemTmpl = Handlebars.compile($('#slotItemTmpl').text());
11
+  var jobItemTmpl  = Handlebars.compile($('#jobItemTmpl').text());
12
+
13
+  refreshSlots();
14
+  refreshJobs();
15
+
16
+  setInterval(function() {
17
+    refreshJobs();
18
+  }, 2 * 60 * 1000);
19
+
20
+  $addSlotModal.on('show.bs.modal', function (e) {
21
+    refreshHandlers();
22
+    generateSlotToken();
23
+  });
24
+
25
+  $('#addSlotCreateButton').on('click', function() {
26
+
27
+    var formData = $addSlotForm.serializeArray();
28
+    var data = {};
29
+
30
+    formData.forEach(function(field) {
31
+      data[field.name] = field.value;
32
+    });
33
+
34
+    $.ajax({
35
+      url:'/api/slots/'+data.token,
36
+      type:"POST",
37
+      data: JSON.stringify(data),
38
+      contentType:"application/json; charset=utf-8",
39
+      success: function(){
40
+        refreshSlots();
41
+        $addSlotModal.modal('hide');
42
+      }
43
+    });
44
+  });
45
+
46
+  function refreshSlots() {
47
+    $.getJSON('/api/slots', function(data) {
48
+      var $items = Object.keys(data).map(function(slotKey) {
49
+        var slot = data[slotKey];
50
+        return slotItemTmpl(slot);
51
+      });
52
+      $slotsList.empty().append($items);
53
+    });
54
+  }
55
+
56
+  function refreshJobs() {
57
+    $.getJSON('/api/jobs', function(data) {
58
+      var $items = Object.keys(data).map(function(jobKey) {
59
+        var job = data[jobKey];
60
+        job.hasTerminated = job.exitCode !== undefined;
61
+        return jobItemTmpl(job);
62
+      });
63
+      $jobsList.empty().append($items);
64
+    });
65
+  }
66
+
67
+  function refreshHandlers() {
68
+    $.getJSON('/api/handlers', function(data) {
69
+      var options = data.map(function(handlerKey) {
70
+        return '<option value="'+handlerKey+'">'+handlerKey+'</option>';
71
+      });
72
+      $slotHandlerInput.html(options.join(''));
73
+    });
74
+  }
75
+
76
+  function generateSlotToken() {
77
+    var uuid = generateUUID();
78
+    $slotTokenInput.val(uuid);
79
+  }
80
+
81
+
82
+  function generateUUID(){
83
+    var d = new Date().getTime();
84
+    var uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
85
+      var r = (d + Math.random()*16)%16 | 0;
86
+      d = Math.floor(d/16);
87
+      return (c=='x' ? r : (r&0x3|0x8)).toString(16);
88
+    });
89
+    return uuid;
90
+  }
91
+
92
+
93
+});

+ 4
- 0
handlers/build-package-from-git View File

@@ -0,0 +1,4 @@
1
+#!/usr/bin/env bash
2
+
3
+echo "Hello world !"
4
+echo $MARANG_PAYLOAD

+ 2
- 0
jobs/.gitignore View File

@@ -0,0 +1,2 @@
1
+*
2
+!.gitignore

+ 20
- 0
lib/config.js View File

@@ -0,0 +1,20 @@
1
+module.exports = require('rc')('marang', {
2
+
3
+  webApp: {
4
+    host: 'localhost',
5
+    port: 3000,
6
+  },
7
+
8
+  hookServer: {
9
+    host: 'localhost',
10
+    port: 3001,
11
+  },
12
+
13
+  // Chemin d'accès à la base de données
14
+  dbPath: __dirname + '/../data',
15
+
16
+  // Chemin d'accès vers les scripts "handlers"
17
+  handlersPath: __dirname + '/../handlers',
18
+  jobsLogPath: __dirname + '/../jobs'
19
+
20
+});

+ 54
- 0
lib/db-rest.js View File

@@ -0,0 +1,54 @@
1
+var express = require('express');
2
+var db = require('./db');
3
+var bodyParser = require('body-parser');
4
+
5
+module.exports = function DbRest(namespace) {
6
+
7
+  var subdb = db.sublevel(namespace);
8
+  var app = express();
9
+
10
+  app.use(bodyParser.json());
11
+
12
+  app.get('/', function(req, res) {
13
+    var entities = {};
14
+    subdb.createReadStream()
15
+      .on('data', function(entry) {
16
+        entities[entry.key] = entry.value;
17
+      })
18
+      .once('close', function() {
19
+        res.status(200).send(entities);
20
+      })
21
+      .once('error', function(err) {
22
+        res.status(500).send(err);
23
+      })
24
+    ;
25
+  });
26
+
27
+  app.get('/:key', function(req, res) {
28
+    var key = req.params.key;
29
+    subdb.get(key, function(err, data) {
30
+      if(err) return res.statuts(500).send(err);
31
+      res.status(200).send(data);
32
+    });
33
+  });
34
+
35
+  app.post('/:key', function(req, res) {
36
+    var key = req.params.key;
37
+    var data = req.body;
38
+    subdb.put(key, data, function(err, data) {
39
+      if(err) return res.statuts(500).send(err);
40
+      res.status(201).send(data);
41
+    });
42
+  });
43
+
44
+  app.delete('/:key', function(req, res) {
45
+    var key = req.params.key;
46
+    subdb.del(key, function(err, data) {
47
+      if(err) return res.statuts(500).send(err);
48
+      res.status(204).end();
49
+    });
50
+  });
51
+
52
+  return app;
53
+
54
+};

+ 5
- 0
lib/db.js View File

@@ -0,0 +1,5 @@
1
+var level = require('level-party');
2
+var sublevel = require('level-sublevel');
3
+var config = require('./config');
4
+
5
+module.exports = sublevel(level(config.dbPath, {encoding: 'json'}));

+ 126
- 0
lib/hook-server.js View File

@@ -0,0 +1,126 @@
1
+var express = require('express');
2
+var config = require('./config');
3
+var db = require('./db');
4
+var debug = require('debug')('marang:hook-server');
5
+var bodyParser = require('body-parser');
6
+var spawn = require('child_process').spawn;
7
+var path = require('path');
8
+var fs = require('fs');
9
+
10
+var slotsDb = db.sublevel('slots');
11
+var jobsDb = db.sublevel('jobs');
12
+
13
+function HookServer() {
14
+  this._app = express();
15
+  this._configureRoutes();
16
+}
17
+
18
+var p = HookServer.prototype;
19
+
20
+p.start = function() {
21
+  this._app.listen(config.hookServer.port, config.hookServer.host);
22
+};
23
+
24
+p._configureRoutes = function() {
25
+
26
+  var app = this._app;
27
+
28
+  app.use(bodyParser.json());
29
+  app.get('/hooks/:token', this._handleWebHook.bind(this));
30
+
31
+};
32
+
33
+p._handleWebHook = function(req, res) {
34
+
35
+  var self = this;
36
+  var token = req.params.token;
37
+  var payload = req.body || {};
38
+
39
+  if(!token) return res.status(400).end();
40
+
41
+  slotsDb.get(token, function(err, slot) {
42
+
43
+    if(err) return res.status(err.notFound ? 404 : 500).end();
44
+
45
+    var payload = res.body;
46
+
47
+    var job = {
48
+      handler: slot.handler,
49
+      timestamp: Date.now(),
50
+      slotLabel: slot.label,
51
+      slotToken: token,
52
+      payload: payload
53
+    };
54
+
55
+    var jobKey = job.timestamp + '-' + token;
56
+
57
+    jobsDb.put(jobKey, job, function(err) {
58
+
59
+      if(err) {
60
+        return res.status(500).end();
61
+      }
62
+
63
+      self._execJobHandler(jobKey, slot.handler, payload);
64
+
65
+      return res.status(204).end();
66
+
67
+    });
68
+
69
+  });
70
+
71
+};
72
+
73
+p._execJobHandler = function(jobKey, handlerKey, payload) {
74
+
75
+  payload = payload || {};
76
+
77
+  debug('Starting job %s with handler %s', jobKey, handlerKey);
78
+
79
+  var handlerPath = path.join(config.handlersPath, handlerKey);
80
+
81
+  // Dirty trick to copy env variables
82
+  var handlerEnv = JSON.parse(JSON.stringify(process.env));
83
+  handlerEnv.MARANG_PAYLOAD = JSON.stringify(payload);
84
+
85
+  var handler = spawn(handlerPath, [], {
86
+    cwd: config.handlersPath,
87
+    env: handlerEnv
88
+  });
89
+
90
+  var jobLog = path.join(config.jobsLogPath, jobKey+'.log');
91
+  var logStream = fs.createWriteStream(jobLog, {flags: 'w+'});
92
+
93
+  handler.stdout.pipe(logStream);
94
+  handler.stderr.pipe(logStream);
95
+
96
+  handler.once('error', function(err) {
97
+    debug(err);
98
+  });
99
+
100
+  handler.once('close', function(exitCode) {
101
+
102
+    debug('Job %s terminated with exit code %s.', jobKey, exitCode);
103
+
104
+    jobsDb.get(jobKey, function(err, job) {
105
+
106
+      if(err) {
107
+        debug(err);
108
+        return;
109
+      }
110
+
111
+      job.exitCode = exitCode;
112
+
113
+      jobsDb.put(jobKey, job, function(err) {
114
+        if(err) {
115
+          debug(err);
116
+          return;
117
+        }
118
+      });
119
+
120
+    });
121
+
122
+  });
123
+
124
+};
125
+
126
+module.exports = HookServer;

+ 102
- 0
lib/web-app.js View File

@@ -0,0 +1,102 @@
1
+var express = require('express');
2
+var path = require('path');
3
+var fs = require('fs');
4
+var dbRest = require('./db-rest');
5
+var config = require('./config');
6
+var db = require('./db');
7
+
8
+function WebApp() {
9
+  this._app = express();
10
+  this._configureRoutes();
11
+}
12
+
13
+var p = WebApp.prototype;
14
+
15
+p.start = function() {
16
+  this._app.listen(config.webApp.port, config.webApp.host);
17
+};
18
+
19
+p._configureRoutes = function() {
20
+
21
+  var app = this._app;
22
+
23
+  app.use('/api/slots', dbRest('slots'));
24
+  app.use('/api/jobs', dbRest('jobs'));
25
+  app.get('/api/handlers', this._serveHandlers.bind(this));
26
+  app.get('/vendor/*', this._serveVendor.bind(this));
27
+  app.get('/*', this._serveApp.bind(this));
28
+
29
+};
30
+
31
+p._serveApp = function(req, res) {
32
+  return res.sendFile(
33
+    req.params[0] || 'index.html',
34
+    { root: path.join(__dirname, '../', 'client') }
35
+  );
36
+};
37
+
38
+p._serveVendor = function(req, res) {
39
+  return res.sendFile(
40
+    req.params[0],
41
+    { root: path.join(__dirname, '../', 'node_modules') },
42
+    function(err) {
43
+      if(err) {
44
+        res.status(404).end();
45
+      }
46
+      res.end();
47
+    }
48
+  );
49
+};
50
+
51
+p._serveHandlers = function(req, res) {
52
+
53
+  this._findHandlers()
54
+    .then(function(handlers) {
55
+      res.status(200).send(handlers);
56
+    })
57
+    .catch(function(err) {
58
+      res.status(500).send(err);
59
+    })
60
+  ;
61
+
62
+};
63
+
64
+p._findHandlers = function() {
65
+
66
+  return new Promise(function(resolve, reject) {
67
+
68
+    fs.readdir(config.handlersPath, function(err, files) {
69
+
70
+      if(err) return reject(err);
71
+
72
+      var promises = files.map(function(file) {
73
+        return new Promise(function(resolve, reject) {
74
+
75
+          var p = path.join(config.handlersPath, file);
76
+
77
+          fs.stat(p, function(err, stats) {
78
+            if(err) return reject(err);
79
+            return resolve(stats.isFile() ? file : null);
80
+
81
+          });
82
+
83
+        });
84
+      });
85
+
86
+      Promise.all(promises)
87
+        .then(function(results) {
88
+          return results.filter(function(file) {
89
+            return file !== null;
90
+          });
91
+        })
92
+        .then(resolve)
93
+        .catch(reject)
94
+      ;
95
+
96
+    });
97
+
98
+  });
99
+
100
+};
101
+
102
+module.exports = WebApp;

+ 23
- 0
package.json View File

@@ -0,0 +1,23 @@
1
+{
2
+  "name": "marang",
3
+  "version": "0.0.0",
4
+  "description": "Webhook server",
5
+  "main": "app.js",
6
+  "scripts": {
7
+    "test": "echo \"Error: no test specified\" && exit 1",
8
+    "start": "node app.js"
9
+  },
10
+  "author": "William Petit <wpetit@cadoles.com>",
11
+  "license": "AGPL",
12
+  "dependencies": {
13
+    "body-parser": "^1.13.1",
14
+    "bootstrap": "^3.3.5",
15
+    "debug": "^2.2.0",
16
+    "express": "^4.13.0",
17
+    "handlebars": "^3.0.3",
18
+    "jquery": "^2.1.4",
19
+    "level-party": "^2.1.2",
20
+    "level-sublevel": "^6.4.6",
21
+    "rc": "^1.0.3"
22
+  }
23
+}

Loading…
Cancel
Save