From 32522f39eb3551b1f023b75159454948c01096fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98ystein=20Bedin?= Date: Mon, 27 Aug 2018 18:24:39 -0400 Subject: [PATCH] Initial GitHub webhook bridge --- .../github-webhook/README.md | 46 ++++++++ ansible-tower-bridges/github-webhook/index.js | 46 ++++++++ .../github-webhook/lib/bridge.js | 105 ++++++++++++++++++ .../github-webhook/package.json | 18 +++ 4 files changed, 215 insertions(+) create mode 100644 ansible-tower-bridges/github-webhook/README.md create mode 100644 ansible-tower-bridges/github-webhook/index.js create mode 100644 ansible-tower-bridges/github-webhook/lib/bridge.js create mode 100644 ansible-tower-bridges/github-webhook/package.json diff --git a/ansible-tower-bridges/github-webhook/README.md b/ansible-tower-bridges/github-webhook/README.md new file mode 100644 index 0000000..cb2bf22 --- /dev/null +++ b/ansible-tower-bridges/github-webhook/README.md @@ -0,0 +1,46 @@ +ansible-tower-bridges/github-webhook +==================================== + +This implementation is used to bridge GitHub webhooks to Ansible Tower. Ansible Tower expects any payload/input parameters to be passed in the `extra_vars` dictionary. Of course GitHub does not do this, and hence this "bridge" to provide that mapping for webhooks request configured on github to be sent to/through this bridge. + +Requirements +------------ + +Although this can be run as a regular/standalone `node.js` app, it is recommended that this gets deployed on an OpenShift Container Platform for ease of maintaining it. + + +Running in OpenShift +-------------------- + +For example, use `oc new-app` to deploy the application: + +``` +> oc new-app openshift/nodejs:8~https://github.com/tool-integrations.git --context-dir=ansible-tower-bridges/github-webhook +``` + + +Configuration +------------- + +| Variable | Description | Required | Defaults | +|:---------|:------------|:---------|:---------| +| ANSIBLE_TOWER_URL | The fqdn portion of Ansible Tower (or IP) - do not include 'http://' or 'https://' | yes | | +| ANSIBLE_TOWER_TEMPLATE_ID | The numeric template id for the job template to run | yes | | +| ANSIBLE_TOWER_USERNAME | An Ansible Tower username with the correct permissions to run the above template | yes | | +| ANSIBLE_TOWER_PASSWORD | Password for the above Ansible Tower username | yes | | +| HTTP_PORT | The http port for the application to listen on | no | 8080 | +| HTTPS_PORT | The httpd (SSL) port for the application listen on (choose this or HTTP_PORT above) | no | 8443 | +| HTTPS_SSL_CERTIFICATE | When HTTPS_PORT above is used, specify the certificate here | no | | +| HTTPS_SSL_KEY | When the HTTPS_PORT above is used, specify the certificate key here | no | | + + +License +------- + +Apache License 2.0 + + +Author Information +------------------ + +Red Hat Community of Practice & staff of the Red Hat Open Innovation Labs. diff --git a/ansible-tower-bridges/github-webhook/index.js b/ansible-tower-bridges/github-webhook/index.js new file mode 100644 index 0000000..ac7cdea --- /dev/null +++ b/ansible-tower-bridges/github-webhook/index.js @@ -0,0 +1,46 @@ + +const http = require('http'); +const https = require('https'); +const fs = require('fs'); + +const httpPort = process.env.HTTP_PORT || 8080; +const httpsPort = process.env.HTTPS_PORT || 8443; +const httpsSSLKey = process.env.HTTPS_SSL_KEY || ''; +const httpsSSLCert = process.env.HTTPS_SSL_CERTIFICATE || ''; + +var express = require('express'); +var cors = require('cors'); +var app = express(); +var bodyParser = require('body-parser'); +var bridge = require('./lib/bridge'); + +app.use(cors()); +app.use(bodyParser()); + +app.post('/', function (request, res) { + bridge.processRequest(request.body, function(err, response) { + if (err){ + // TODO: make the return status code be more specific per the error + // - i.e.: not use "400 Bad Request" for all of it + res.status(400).send(err); + } else { + res.send(response); + } + }); +}); + +if (httpsSSLCert.trim() && httpsSSLKey.trim()) { // Secure + const options = { + key: fs.readFileSync(httpsSSLKey), + cert: fs.readFileSync(httpsSSLCert) + }; + + https.createServer(options, app).listen(httpsPort, function () { + console.log('Listening on https://localhost:' + httpsPort); + }); +} +else { // non-Secure + http.createServer(app).listen(httpPort, function() { + console.log('Listening on UNSECURE http://localhost:' + httpPort); + }); +} diff --git a/ansible-tower-bridges/github-webhook/lib/bridge.js b/ansible-tower-bridges/github-webhook/lib/bridge.js new file mode 100644 index 0000000..59b9895 --- /dev/null +++ b/ansible-tower-bridges/github-webhook/lib/bridge.js @@ -0,0 +1,105 @@ + +const request = require('request'); +const urlModule = require('url'); + +const AnsibleTowerUrl = process.env['ANSIBLE_TOWER_URL']; +const AnsibleTowerTemplateId = process.env['ANSIBLE_TOWER_TEMPLATE_ID']; +const AnsibleTowerUsername = process.env['ANSIBLE_TOWER_USERNAME'] || ''; +const AnsibleTowerPassword = process.env['ANSIBLE_TOWER_PASSWORD'] || ''; + +process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0" + + +function getTemplateLaunchUri(){ + return '/api/v1/job_templates/' + AnsibleTowerTemplateId + '/launch/'; +} + +function getRequestUrl(server, uri){ + return 'https://' + server + uri; +} + +function getAuth(){ + return 'Basic ' + new Buffer(AnsibleTowerUsername + ':' + AnsibleTowerPassword).toString('base64'); +} + + +function generateReturnJson(message, url) { + var returnjson = { + message: message, + url: '' + }; + + if (url) { + returnjson['url'] = url; + } + + return returnjson; +} + + +function sendResponse(cb, successMessage, errorMessage, url) { + if (errorMessage) { + console.log('ERROR: ' + errorMessage); + returnjson = generateReturnJson(errorMessage, url); + cb(returnjson, null); + } else if (successMessage) { + console.log('SUCCESS: ' + successMessage); + returnjson = generateReturnJson(successMessage, url); + cb(null, returnjson); + } else { + returnjson = generateReturnJson('Unknown Error'); + cb(returnjson, null); + } +} + + +function processRequest(params, cb) { + var uri = getTemplateLaunchUri(); + var request_url = getRequestUrl(AnsibleTowerUrl, uri); + var auth = getAuth(); + + var config_body = { + "extra_vars" : {} + }; + + // Wrap the params in the `extra_vars` dictionary + config_body['extra_vars'] = params; + + var options = { + url: request_url, + method: 'POST', + headers: { + 'Content-type': 'application/json', + 'Authorization': auth + }, + json: config_body + }; + + // options_string = JSON.stringify(options, null, 4); + // console.log('calling options: ' + options_string); + + request(options, function (error, response) { + //console.log('*********RESPONSE**********************'); + //console.log(response); + //console.log('**********END RESPONSE*****************'); + if (!error && response.statusCode >= 200 && response.statusCode < 300) { + sendResponse( + cb, + 'Successful Launch with Ansible Tower job ID: ' + response.body.job, + null, + 'https://' + AnsibleTowerUrl + '/#/jobs/' + response.body.job); + + } + else { + var errorMsg = 'Failed to launch Ansible Tower job. Please check your input and try again.'; + if (error) { + errorMsg += ' (' + error + ')'; + } + + sendResponse(cb, null, errorMsg, null); + } + }) +} + + +exports.processRequest = processRequest; diff --git a/ansible-tower-bridges/github-webhook/package.json b/ansible-tower-bridges/github-webhook/package.json new file mode 100644 index 0000000..a0ab55e --- /dev/null +++ b/ansible-tower-bridges/github-webhook/package.json @@ -0,0 +1,18 @@ +{ + "name": "github-webhook-bridge", + "version": "0.1.0", + "description": "GitHub Webhook Bridge for Ansible Tower", + "main": "index.js", + "scripts": { + "start": "node index.js" + }, + "author": "Øystein Bedin", + "license": "ISC", + "dependencies": { + "body-parser": "^1.15.2", + "cors": "^2.7.1", + "express": "^4.14.0", + "request": "^2.85.0", + "url": "^0.11.0" + } +}