github hooks phase 1

This commit is contained in:
Kasra Bigdeli
2017-12-17 14:04:44 -08:00
parent ab608fe7e5
commit 50865fa9ec
10 changed files with 383 additions and 130 deletions
+2
View File
@@ -173,6 +173,8 @@ app.use(function (err, req, res, next) {
res.locals.message = err.message;
res.locals.error = req.app.get('env') === 'development' ? err : {};
Logger.e(err);
// render the error page
res.status(err.status || 500);
res.render('error');
+124 -119
View File
@@ -464,6 +464,7 @@ angular
var volumes = appDefinition.volumes;
var ports = appDefinition.ports;
var nodeId = appDefinition.nodeId;
var appPushWebhook = appDefinition.appPushWebhook;
$http
.post(BASE_API + 'user/appDefinitions/update', {
@@ -472,6 +473,7 @@ angular
notExposeAsWebApp: notExposeAsWebApp,
volumes: volumes,
ports: ports,
appPushWebhook: appPushWebhook,
nodeId: nodeId,
envVars: envVars
}, createConfig())
@@ -650,6 +652,124 @@ angular.module('RDash')
}
});
/**
* File Upload Read Directive
*/
angular
.module('RDash')
.directive("fileread", [function () {
return {
scope: {
fileread: "="
},
link: function (scope, element, attributes) {
element.bind("change", function (changeEvent) {
scope.$apply(function () {
scope.fileread = changeEvent.target.files[0];
// or all selected files:
// scope.fileread = changeEvent.target.files;
});
});
}
}
}]);
/**
* Loading Directive
* @see http://tobiasahlin.com/spinkit/
*/
angular
.module('RDash')
.directive('rdLoading', rdLoading);
function rdLoading() {
var directive = {
restrict: 'AE',
template: '<div class="loading"><div class="double-bounce1"></div><div class="double-bounce2"></div></div>'
};
return directive;
};
/**
* Widget Body Directive
*/
angular
.module('RDash')
.directive('rdWidgetBody', rdWidgetBody);
function rdWidgetBody() {
var directive = {
requires: '^rdWidget',
scope: {
loading: '=?',
classes: '@?'
},
transclude: true,
template: '<div class="widget-body" ng-class="classes"><rd-loading ng-show="loading"></rd-loading><div ng-hide="loading" class="widget-content" ng-transclude></div></div>',
restrict: 'E'
};
return directive;
};
/**
* Widget Footer Directive
*/
angular
.module('RDash')
.directive('rdWidgetFooter', rdWidgetFooter);
function rdWidgetFooter() {
var directive = {
requires: '^rdWidget',
transclude: true,
template: '<div class="widget-footer" ng-transclude></div>',
restrict: 'E'
};
return directive;
};
/**
* Widget Header Directive
*/
angular
.module('RDash')
.directive('rdWidgetHeader', rdWidgetTitle);
function rdWidgetTitle() {
var directive = {
requires: '^rdWidget',
scope: {
title: '@',
icon: '@'
},
transclude: true,
template: '<div class="widget-header"><div class="row"><div class="pull-left"><i class="fa" ng-class="icon"></i> {{title}} </div><div class="pull-right col-xs-6 col-sm-4" ng-transclude></div></div></div>',
restrict: 'E'
};
return directive;
};
/**
* Widget Directive
*/
angular
.module('RDash')
.directive('rdWidget', rdWidget);
function rdWidget() {
var directive = {
transclude: true,
template: '<div class="widget" ng-transclude></div>',
restrict: 'EA'
};
return directive;
function link(scope, element, attrs) {
/* */
}
};
/**
* Alerts Controller
*/
@@ -685,6 +805,7 @@ angular.module('RDash')
function AppDefinitionCtrl($scope, $cookieStore, $rootScope, pageDefinitions,
apiManager, captainToast, $uibModal, $state, $location) {
$scope.rootDomainWithProtocol = window.location.protocol;
$scope.loadingState = {};
$scope.loadingState.enabled = true;
$scope.search = {};
@@ -715,6 +836,7 @@ function AppDefinitionCtrl($scope, $cookieStore, $rootScope, pageDefinitions,
}
$scope.rootDomain = data.rootDomain;
$scope.rootDomainWithProtocol = window.location.protocol + '//' + data.rootDomain;
});
@@ -936,6 +1058,7 @@ function AppDefinitionCtrl($scope, $cookieStore, $rootScope, pageDefinitions,
ports: app.ports,
nodeId: app.nodeId,
notExposeAsWebApp: app.notExposeAsWebApp,
appPushWebhook: app.appPushWebhook,
volumes: app.volumes
}
@@ -1526,122 +1649,4 @@ function SettingsCtrl($scope, $cookieStore, $rootScope, pageDefinitions,
}());
}
/**
* File Upload Read Directive
*/
angular
.module('RDash')
.directive("fileread", [function () {
return {
scope: {
fileread: "="
},
link: function (scope, element, attributes) {
element.bind("change", function (changeEvent) {
scope.$apply(function () {
scope.fileread = changeEvent.target.files[0];
// or all selected files:
// scope.fileread = changeEvent.target.files;
});
});
}
}
}]);
/**
* Loading Directive
* @see http://tobiasahlin.com/spinkit/
*/
angular
.module('RDash')
.directive('rdLoading', rdLoading);
function rdLoading() {
var directive = {
restrict: 'AE',
template: '<div class="loading"><div class="double-bounce1"></div><div class="double-bounce2"></div></div>'
};
return directive;
};
/**
* Widget Body Directive
*/
angular
.module('RDash')
.directive('rdWidgetBody', rdWidgetBody);
function rdWidgetBody() {
var directive = {
requires: '^rdWidget',
scope: {
loading: '=?',
classes: '@?'
},
transclude: true,
template: '<div class="widget-body" ng-class="classes"><rd-loading ng-show="loading"></rd-loading><div ng-hide="loading" class="widget-content" ng-transclude></div></div>',
restrict: 'E'
};
return directive;
};
/**
* Widget Footer Directive
*/
angular
.module('RDash')
.directive('rdWidgetFooter', rdWidgetFooter);
function rdWidgetFooter() {
var directive = {
requires: '^rdWidget',
transclude: true,
template: '<div class="widget-footer" ng-transclude></div>',
restrict: 'E'
};
return directive;
};
/**
* Widget Header Directive
*/
angular
.module('RDash')
.directive('rdWidgetHeader', rdWidgetTitle);
function rdWidgetTitle() {
var directive = {
requires: '^rdWidget',
scope: {
title: '@',
icon: '@'
},
transclude: true,
template: '<div class="widget-header"><div class="row"><div class="pull-left"><i class="fa" ng-class="icon"></i> {{title}} </div><div class="pull-right col-xs-6 col-sm-4" ng-transclude></div></div></div>',
restrict: 'E'
};
return directive;
};
/**
* Widget Directive
*/
angular
.module('RDash')
.directive('rdWidget', rdWidget);
function rdWidget() {
var directive = {
transclude: true,
template: '<div class="widget" ng-transclude></div>',
restrict: 'EA'
};
return directive;
function link(scope, element, attrs) {
/* */
}
};
}
+42
View File
@@ -235,6 +235,48 @@
<div class="col-lg-3">
</div>
</div>
<div class="row">
<h4>
Deploy from Github/Bitbucket
</h4>
<p>
POST Webhook:
<br>
<pre>{{app.appPushWebhook.pushWebhookToken?(rootDomainWithProtocol+'/api/v1/webhooks/triggerbuild?token='+app.appPushWebhook.pushWebhookToken):
'** Add repo info and save for this webhook to appear **'}}</pre>
</p>
</div>
<br>
<div class="row">
<div class="form-group col-sm-6">
<div class="input-group">
<span class="input-group-addon">Username:</span>
<input type="text" class="form-control" ng-model="app.appPushWebhook.repoInfo.user" placeholder="githubusername">
</div>
</div>
<div class="col-sm-6 form-group">
<div class="input-group">
<span class="input-group-addon">Password:</span>
<input type="text" class="form-control" ng-model="app.appPushWebhook.repoInfo.password" placeholder="githubpassword">
</div>
</div>
</div>
<div class="row">
<div class="form-group col-sm-6">
<div class="input-group">
<span class="input-group-addon">Repository:</span>
<input type="text" class="form-control" ng-model="app.appPushWebhook.repoInfo.repo" placeholder="githubusername">
</div>
</div>
<div class="col-sm-6 form-group">
<div class="input-group">
<span class="input-group-addon">Branch:</span>
<input type="text" class="form-control" ng-model="app.appPushWebhook.repoInfo.branch" placeholder="master">
</div>
</div>
</div>
<div class="row" ng-show="app.isAppBuilding">
<div class="pull-left">
<div class="inline-loader"></div>
+64 -3
View File
@@ -2,6 +2,7 @@
* Created by kasra on 27/06/17.
*/
const Configstore = require('configstore');
const uuid = require('uuid/v4');
const isValidPath = require('is-valid-path');
const fs = require('fs-extra');
const ApiStatusCodes = require('../api/ApiStatusCodes');
@@ -392,11 +393,39 @@ class DataStore {
});
}
updateAppDefinitionInDb(appName, instanceCount, envVars, volumes, nodeId, notExposeAsWebApp, ports) {
updateAppDefinitionInDb(appName, instanceCount, envVars, volumes, nodeId, notExposeAsWebApp, ports, appPushWebhook, authenticator) {
const self = this;
return this.getAppDefinition(appName)
.then(function (app) {
let app;
return Promise.resolve()
.then(function () {
return self.getAppDefinition(appName);
})
.then(function (appObj) {
app = appObj;
})
.then(function () {
if (appPushWebhook.repoInfo && appPushWebhook.repoInfo.repo && appPushWebhook.repoInfo.branch
&& appPushWebhook.repoInfo.user && appPushWebhook.repoInfo.password) {
return authenticator
.getAppPushWebhookDatastore({
repo: appPushWebhook.repoInfo.repo,
branch: appPushWebhook.repoInfo.branch,
user: appPushWebhook.repoInfo.user,
password: appPushWebhook.repoInfo.password
})
}
return null;
})
.then(function (appPushWebhookRepoInfo) {
instanceCount = Number(instanceCount);
@@ -404,6 +433,22 @@ class DataStore {
app.notExposeAsWebApp = !!notExposeAsWebApp;
app.nodeId = nodeId;
if (appPushWebhookRepoInfo) {
app.appPushWebhook = app.appPushWebhook || {};
if (!app.appPushWebhook.tokenVersion) {
app.appPushWebhook.tokenVersion = uuid();
}
app.appPushWebhook.repoInfo = appPushWebhookRepoInfo;
}
else {
app.appPushWebhook = {};
}
if (ports) {
function isPortValid(portNumber) {
@@ -471,6 +516,21 @@ class DataStore {
}
}
})
.then(function () {
if (app.appPushWebhook.repoInfo) {
return authenticator
.getAppPushWebhookToken(appName, app.appPushWebhook.tokenVersion)
}
})
.then(function (pushWebhookToken) {
if (pushWebhookToken) {
app.appPushWebhook.pushWebhookToken = pushWebhookToken;
}
self.data.set(APP_DEFINITIONS + '.' + appName, app);
});
@@ -687,6 +747,7 @@ class DataStore {
envVars: [],
volumes: [],
ports: [],
appPushWebhook: {}, // tokenVersion, repoInfo, pushWebhookToken
versions: []
};
+24 -2
View File
@@ -4,6 +4,7 @@ const router = express.Router();
const BaseApi = require('../api/BaseApi');
const ApiStatusCodes = require('../api/ApiStatusCodes');
const Logger = require('../utils/Logger');
const Authenticator = require('../user/Authenticator');
// Get a list of oneclickspps
router.get('/oneclickapps', function (req, res, next) {
@@ -36,19 +37,39 @@ router.get('/', function (req, res, next) {
let dataStore = res.locals.user.dataStore;
let serviceManager = res.locals.user.serviceManager;
let appsArray = [];
dataStore.getAppDefinitions()
.then(function (apps) {
let appsArray = [];
let promises = [];
Object.keys(apps).forEach(function (key, index) {
let app = apps[key];
app.appName = key;
app.isAppBuilding = serviceManager.isAppBuilding(key);
app.appPushWebhook = app.appPushWebhook || {};
let repoInfoEncrypted = app.appPushWebhook ? app.appPushWebhook.repoInfo : null;
if (repoInfoEncrypted) {
promises.push(
Authenticator.get(dataStore.getNameSpace())
.decodeAppPushWebhookDatastore(repoInfoEncrypted)
.then(function (decryptedData) {
app.appPushWebhook.repoInfo = decryptedData;
}));
}
else {
app.appPushWebhook.repoInfo = {};
}
appsArray.push(app);
});
return Promise.all(promises);
})
.then(function () {
let baseApi = new BaseApi(ApiStatusCodes.STATUS_OK, "App definitions are retrieved.");
baseApi.data = appsArray;
baseApi.rootDomain = dataStore.getRootDomain();
@@ -299,6 +320,7 @@ router.post('/update/', function (req, res, next) {
let appName = req.body.appName;
let nodeId = req.body.nodeId;
let notExposeAsWebApp = req.body.notExposeAsWebApp;
let appPushWebhook = req.body.appPushWebhook || {};
let envVars = req.body.envVars || [];
let volumes = req.body.volumes || [];
let ports = req.body.ports || [];
@@ -306,7 +328,7 @@ router.post('/update/', function (req, res, next) {
Logger.d('Updating app started: ' + appName);
serviceManager.updateAppDefinition(appName, Number(instanceCount), envVars, volumes, nodeId, notExposeAsWebApp, ports)
serviceManager.updateAppDefinition(appName, Number(instanceCount), envVars, volumes, nodeId, notExposeAsWebApp, ports, appPushWebhook)
.then(function () {
Logger.d('AppName is updated: ' + appName);
+76 -4
View File
@@ -10,8 +10,9 @@ const DataStoreProvider = require('../datastore/DataStoreProvider');
const captainDefaultPassword = EnvVar.DEFAULT_PASSWORD || 'captain42';
const COOKIE_AUTH_PREFIX = 'cookie-';
const COOKIE_AUTH_SUFFIX = 'cookie-';
const WEBHOOK_APP_PUSH_SUFFIX = '-webhook-app-push';
const WEBHOOK_APP_PUSH_DATASTORE_SUFFIX = "-webhook-app-datastore";
class Authenticator {
@@ -72,7 +73,7 @@ class Authenticator {
}
getAuthTokenForCookies(password) {
return this.getAuthToken(password, COOKIE_AUTH_PREFIX);
return this.getAuthToken(password, COOKIE_AUTH_SUFFIX);
}
getAuthToken(password, keySuffix) {
@@ -101,7 +102,7 @@ class Authenticator {
}
decodeAuthTokenFromCookies(token) {
return this.decodeAuthToken(token, COOKIE_AUTH_PREFIX);
return this.decodeAuthToken(token, COOKIE_AUTH_SUFFIX);
}
decodeAuthToken(token, keySuffix) {
@@ -135,6 +136,77 @@ class Authenticator {
});
}
getAppPushWebhookDatastore(dataToSave) {
const self = this;
return self.getGenericToken(dataToSave, WEBHOOK_APP_PUSH_DATASTORE_SUFFIX)
}
decodeAppPushWebhookDatastore(token) {
const self = this;
return self.decodeGenericToken(token, WEBHOOK_APP_PUSH_DATASTORE_SUFFIX)
}
getAppPushWebhookToken(appName, tokenVersion) {
const self = this;
if (!appName) {
throw ApiStatusCodes.createError(ApiStatusCodes.STATUS_ERROR_GENERIC, 'App name are required for webhook token..');
}
return self.getGenericToken({
tokenVersion: tokenVersion,
appName: appName
}, WEBHOOK_APP_PUSH_SUFFIX)
}
decodeAppPushWebhookToken(token) {
const self = this;
return self.decodeGenericToken(token, WEBHOOK_APP_PUSH_SUFFIX)
}
getGenericToken(obj, keySuffix) {
const self = this;
obj.namespace = self.namespace;
return Promise.resolve()
.then(function () {
return jwt.sign({
data: obj
}, self.encryptionKey + (keySuffix ? keySuffix : ''));
})
}
decodeGenericToken(token, keySuffix) {
const self = this;
return new Promise(function (resolve, reject) {
jwt.verify(token, self.encryptionKey + (keySuffix ? keySuffix : ''), function (err, rawDecoded) {
if (err) {
Logger.d(err);
reject(ApiStatusCodes.createError(ApiStatusCodes.STATUS_AUTH_TOKEN_INVALID, 'Token corrupted'));
return;
}
let decodedData = rawDecoded.data;
if (decodedData.namespace !== self.namespace) {
reject(ApiStatusCodes.createError(ApiStatusCodes.STATUS_AUTH_TOKEN_INVALID,
'token does not match the namespace'));
return;
}
resolve(decodedData);
});
});
}
}
const authenticatorCache = {};
+4 -2
View File
@@ -6,6 +6,7 @@ const path = require('path');
const CaptainManager = require('./CaptainManager');
const ApiStatusCodes = require('../api/ApiStatusCodes');
const TemplateHelper = require('./TemplateHelper');
const Authenticator = require('./Authenticator');
const uuid = require('uuid/v4');
const SOURCE_FOLDER_NAME = 'src';
@@ -630,7 +631,7 @@ class ServiceManager {
});
}
updateAppDefinition(appName, instanceCount, envVars, volumes, nodeId, notExposeAsWebApp, ports) {
updateAppDefinition(appName, instanceCount, envVars, volumes, nodeId, notExposeAsWebApp, ports, appPushWebhook) {
const self = this;
const dataStore = this.dataStore;
@@ -721,7 +722,8 @@ class ServiceManager {
})
.then(function () {
return dataStore.updateAppDefinitionInDb(appName, instanceCount, envVars, volumes, nodeId, notExposeAsWebApp, ports);
return dataStore.updateAppDefinitionInDb(appName, instanceCount, envVars, volumes, nodeId,
notExposeAsWebApp, ports, appPushWebhook, Authenticator.get(dataStore.getNameSpace()));
})
.then(function () {
@@ -382,6 +382,7 @@
var volumes = appDefinition.volumes;
var ports = appDefinition.ports;
var nodeId = appDefinition.nodeId;
var appPushWebhook = appDefinition.appPushWebhook;
$http
.post(BASE_API + 'user/appDefinitions/update', {
@@ -390,6 +391,7 @@
notExposeAsWebApp: notExposeAsWebApp,
volumes: volumes,
ports: ports,
appPushWebhook: appPushWebhook,
nodeId: nodeId,
envVars: envVars
}, createConfig())
@@ -6,6 +6,7 @@ angular.module('RDash')
function AppDefinitionCtrl($scope, $cookieStore, $rootScope, pageDefinitions,
apiManager, captainToast, $uibModal, $state, $location) {
$scope.rootDomainWithProtocol = window.location.protocol;
$scope.loadingState = {};
$scope.loadingState.enabled = true;
$scope.search = {};
@@ -36,6 +37,7 @@ function AppDefinitionCtrl($scope, $cookieStore, $rootScope, pageDefinitions,
}
$scope.rootDomain = data.rootDomain;
$scope.rootDomainWithProtocol = window.location.protocol + '//' + data.rootDomain;
});
@@ -257,6 +259,7 @@ function AppDefinitionCtrl($scope, $cookieStore, $rootScope, pageDefinitions,
ports: app.ports,
nodeId: app.nodeId,
notExposeAsWebApp: app.notExposeAsWebApp,
appPushWebhook: app.appPushWebhook,
volumes: app.volumes
}
+42
View File
@@ -236,6 +236,48 @@
<div class="col-lg-3">
</div>
</div>
<div class="row">
<h4>
Deploy from Github/Bitbucket
</h4>
<p>
POST Webhook:
<br>
<pre>{{app.appPushWebhook.pushWebhookToken?(rootDomainWithProtocol+'/api/v1/webhooks/triggerbuild?token='+app.appPushWebhook.pushWebhookToken):
'** Add repo info and save for this webhook to appear **'}}</pre>
</p>
</div>
<br/>
<div class="row">
<div class="form-group col-sm-6">
<div class="input-group">
<span class="input-group-addon">Username:</span>
<input type="text" class="form-control" ng-model="app.appPushWebhook.repoInfo.user" placeholder="githubusername">
</div>
</div>
<div class="col-sm-6 form-group">
<div class="input-group">
<span class="input-group-addon">Password:</span>
<input type="text" class="form-control" ng-model="app.appPushWebhook.repoInfo.password" placeholder="githubpassword">
</div>
</div>
</div>
<div class="row">
<div class="form-group col-sm-6">
<div class="input-group">
<span class="input-group-addon">Repository:</span>
<input type="text" class="form-control" ng-model="app.appPushWebhook.repoInfo.repo" placeholder="githubusername">
</div>
</div>
<div class="col-sm-6 form-group">
<div class="input-group">
<span class="input-group-addon">Branch:</span>
<input type="text" class="form-control" ng-model="app.appPushWebhook.repoInfo.branch" placeholder="master">
</div>
</div>
</div>
<div class="row" ng-show="app.isAppBuilding">
<div class="pull-left">
<div class="inline-loader"></div>