mirror of
https://github.com/woodpecker-ci/woodpecker.git
synced 2024-11-26 03:41:01 +00:00
removed css files. designers will be providing
removed amber files. replacing with angular removed queue package in favor or worker package removed channel package in favor of pubsub package
This commit is contained in:
parent
06dfc269f7
commit
83577a7d5d
138 changed files with 3133 additions and 2760 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -12,5 +12,4 @@ drone.sublime-workspace
|
|||
|
||||
client/client
|
||||
server/server
|
||||
debian/drone/usr/local/bin/drone
|
||||
debian/drone/usr/local/bin/droned
|
||||
debian/drone/usr
|
||||
|
|
55
Makefile
55
Makefile
|
@ -1,9 +1,10 @@
|
|||
SHA := $(shell git rev-parse --short HEAD)
|
||||
|
||||
all: rice amberc lessc build
|
||||
all: build
|
||||
|
||||
deps:
|
||||
go get github.com/eknkc/amber/amberc
|
||||
# npm install -g uglify-js
|
||||
# npm install -g less
|
||||
go get github.com/GeertJohan/go.rice/rice
|
||||
go list github.com/drone/drone/... | xargs go get -t -v
|
||||
|
||||
|
@ -15,43 +16,29 @@ test:
|
|||
go vet ./...
|
||||
go test -cover -short ./...
|
||||
|
||||
run:
|
||||
@cd server && go run main.go
|
||||
|
||||
clean:
|
||||
@find ./ -name '*.out' | xargs rm # remove go coverage output
|
||||
@find ./ -name '*.sqlite' | xargs rm # remove sqlite databases
|
||||
@find ./ -name '*.rice-box.go' | xargs rm
|
||||
rm -rf debian/drone/usr/local/bin/drone
|
||||
rm -rf debian/drone/usr/local/bin/droned
|
||||
rm -rf debian/drone.deb
|
||||
rm server/server
|
||||
rm client/client
|
||||
@find . -name "*.out" -delete # remove go coverage output
|
||||
@find . -name "*.sqlite" -delete # remove sqlite databases
|
||||
@find . -name '*.rice-box.go' -delete # remove go rice files & embedded content
|
||||
#@find . -name '*.css' -delete
|
||||
@rm -r debian/drone/usr/local/bin debian/drone.deb server/server client/client server/template/html
|
||||
|
||||
#cd cmd/droned/static && rice clean
|
||||
#cd cmd/droned/template && rice clean
|
||||
dpkg: lessc rice build deb
|
||||
|
||||
# embeds content in go source code so that it is compiled
|
||||
# and packaged inside the go binary file.
|
||||
rice:
|
||||
cd server && rice embed
|
||||
#cd server/template/html && rice embed
|
||||
|
||||
amberc:
|
||||
@for f in server/template/*.amber; do $$GOPATH/bin/amberc -pp=true "$$f" > "$${f%.amber}.html"; done
|
||||
@mkdir -p server/template/html
|
||||
@mv server/template/*.html server/template/html
|
||||
cd server && rice embed
|
||||
|
||||
lessc:
|
||||
@lessc server/static/styles/drone.less > server/static/styles/drone.css
|
||||
lessc server/app/styles/drone.less server/app/styles/drone.css
|
||||
lessc --clean-css server/app/styles/drone.less server/app/styles/drone.min.css
|
||||
|
||||
uglify:
|
||||
yui-compressor --type='css' -o 'server/static/styles/drone.min.css' server/static/styles/drone.css
|
||||
|
||||
# npm install -g uglifycss
|
||||
# npm install -g uglify-js
|
||||
# npm install -g less
|
||||
|
||||
# creates a debian package for drone
|
||||
# to install `sudo dpkg -i drone.deb`
|
||||
dpkg:
|
||||
# creates a debian package for drone to install
|
||||
# `sudo dpkg -i drone.deb`
|
||||
deb:
|
||||
mkdir -p debian/drone/usr/local/bin
|
||||
-dpkg-deb --build debian/drone
|
||||
|
||||
run:
|
||||
@cd server && go run main.go amber.go
|
||||
dpkg-deb --build debian/drone
|
||||
|
|
287
server/amber.go
287
server/amber.go
|
@ -1,287 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
var funcMap = template.FuncMap{
|
||||
"__amber_add": runtime_add,
|
||||
"__amber_sub": runtime_sub,
|
||||
"__amber_mul": runtime_mul,
|
||||
"__amber_quo": runtime_quo,
|
||||
"__amber_rem": runtime_rem,
|
||||
"__amber_minus": runtime_minus,
|
||||
"__amber_plus": runtime_plus,
|
||||
"__amber_eql": runtime_eql,
|
||||
"__amber_gtr": runtime_gtr,
|
||||
"__amber_lss": runtime_lss,
|
||||
|
||||
"json": runtime_json,
|
||||
"unescaped": runtime_unescaped,
|
||||
}
|
||||
|
||||
func runtime_add(x, y interface{}) interface{} {
|
||||
vx, vy := reflect.ValueOf(x), reflect.ValueOf(y)
|
||||
switch vx.Kind() {
|
||||
case reflect.Int, reflect.Int32, reflect.Int64, reflect.Int16, reflect.Int8:
|
||||
{
|
||||
switch vy.Kind() {
|
||||
case reflect.Int, reflect.Int32, reflect.Int64, reflect.Int16, reflect.Int8:
|
||||
return vx.Int() + vy.Int()
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return float64(vx.Int()) + vy.Float()
|
||||
case reflect.String:
|
||||
return fmt.Sprintf("%d%s", vx.Int(), vy.String())
|
||||
}
|
||||
}
|
||||
case reflect.Float32, reflect.Float64:
|
||||
{
|
||||
switch vy.Kind() {
|
||||
case reflect.Int, reflect.Int32, reflect.Int64, reflect.Int16, reflect.Int8:
|
||||
return vx.Float() + float64(vy.Int())
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return vx.Float() + vy.Float()
|
||||
case reflect.String:
|
||||
return fmt.Sprintf("%f%s", vx.Float(), vy.String())
|
||||
}
|
||||
}
|
||||
case reflect.String:
|
||||
{
|
||||
switch vy.Kind() {
|
||||
case reflect.Int, reflect.Int32, reflect.Int64, reflect.Int16, reflect.Int8:
|
||||
return fmt.Sprintf("%s%d", vx.String(), vy.Int())
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return fmt.Sprintf("%s%f", vx.String(), vy.Float())
|
||||
case reflect.String:
|
||||
return fmt.Sprintf("%s%s", vx.String(), vy.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "<nil>"
|
||||
}
|
||||
|
||||
func runtime_sub(x, y interface{}) interface{} {
|
||||
vx, vy := reflect.ValueOf(x), reflect.ValueOf(y)
|
||||
switch vx.Kind() {
|
||||
case reflect.Int, reflect.Int32, reflect.Int64, reflect.Int16, reflect.Int8:
|
||||
{
|
||||
switch vy.Kind() {
|
||||
case reflect.Int, reflect.Int32, reflect.Int64, reflect.Int16, reflect.Int8:
|
||||
return vx.Int() - vy.Int()
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return float64(vx.Int()) - vy.Float()
|
||||
}
|
||||
}
|
||||
case reflect.Float32, reflect.Float64:
|
||||
{
|
||||
switch vy.Kind() {
|
||||
case reflect.Int, reflect.Int32, reflect.Int64, reflect.Int16, reflect.Int8:
|
||||
return vx.Float() - float64(vy.Int())
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return vx.Float() - vy.Float()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "<nil>"
|
||||
}
|
||||
|
||||
func runtime_mul(x, y interface{}) interface{} {
|
||||
vx, vy := reflect.ValueOf(x), reflect.ValueOf(y)
|
||||
switch vx.Kind() {
|
||||
case reflect.Int, reflect.Int32, reflect.Int64, reflect.Int16, reflect.Int8:
|
||||
{
|
||||
switch vy.Kind() {
|
||||
case reflect.Int, reflect.Int32, reflect.Int64, reflect.Int16, reflect.Int8:
|
||||
return vx.Int() * vy.Int()
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return float64(vx.Int()) * vy.Float()
|
||||
}
|
||||
}
|
||||
case reflect.Float32, reflect.Float64:
|
||||
{
|
||||
switch vy.Kind() {
|
||||
case reflect.Int, reflect.Int32, reflect.Int64, reflect.Int16, reflect.Int8:
|
||||
return vx.Float() * float64(vy.Int())
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return vx.Float() * vy.Float()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "<nil>"
|
||||
}
|
||||
|
||||
func runtime_quo(x, y interface{}) interface{} {
|
||||
vx, vy := reflect.ValueOf(x), reflect.ValueOf(y)
|
||||
switch vx.Kind() {
|
||||
case reflect.Int, reflect.Int32, reflect.Int64, reflect.Int16, reflect.Int8:
|
||||
{
|
||||
switch vy.Kind() {
|
||||
case reflect.Int, reflect.Int32, reflect.Int64, reflect.Int16, reflect.Int8:
|
||||
return vx.Int() / vy.Int()
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return float64(vx.Int()) / vy.Float()
|
||||
}
|
||||
}
|
||||
case reflect.Float32, reflect.Float64:
|
||||
{
|
||||
switch vy.Kind() {
|
||||
case reflect.Int, reflect.Int32, reflect.Int64, reflect.Int16, reflect.Int8:
|
||||
return vx.Float() / float64(vy.Int())
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return vx.Float() / vy.Float()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "<nil>"
|
||||
}
|
||||
|
||||
func runtime_rem(x, y interface{}) interface{} {
|
||||
vx, vy := reflect.ValueOf(x), reflect.ValueOf(y)
|
||||
switch vx.Kind() {
|
||||
case reflect.Int, reflect.Int32, reflect.Int64, reflect.Int16, reflect.Int8:
|
||||
{
|
||||
switch vy.Kind() {
|
||||
case reflect.Int, reflect.Int32, reflect.Int64, reflect.Int16, reflect.Int8:
|
||||
return vx.Int() % vy.Int()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "<nil>"
|
||||
}
|
||||
|
||||
func runtime_minus(x interface{}) interface{} {
|
||||
vx := reflect.ValueOf(x)
|
||||
switch vx.Kind() {
|
||||
case reflect.Int, reflect.Int32, reflect.Int64, reflect.Int16, reflect.Int8:
|
||||
return -vx.Int()
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return -vx.Float()
|
||||
}
|
||||
|
||||
return "<nil>"
|
||||
}
|
||||
|
||||
func runtime_plus(x interface{}) interface{} {
|
||||
vx := reflect.ValueOf(x)
|
||||
switch vx.Kind() {
|
||||
case reflect.Int, reflect.Int32, reflect.Int64, reflect.Int16, reflect.Int8:
|
||||
return +vx.Int()
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return +vx.Float()
|
||||
}
|
||||
|
||||
return "<nil>"
|
||||
}
|
||||
|
||||
func runtime_eql(x, y interface{}) bool {
|
||||
vx, vy := reflect.ValueOf(x), reflect.ValueOf(y)
|
||||
switch vx.Kind() {
|
||||
case reflect.Int, reflect.Int32, reflect.Int64, reflect.Int16, reflect.Int8:
|
||||
{
|
||||
switch vy.Kind() {
|
||||
case reflect.Int, reflect.Int32, reflect.Int64, reflect.Int16, reflect.Int8:
|
||||
return vx.Int() == vy.Int()
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return float64(vx.Int()) == vy.Float()
|
||||
case reflect.String:
|
||||
return fmt.Sprintf("%d", vx.Int()) == vy.String()
|
||||
}
|
||||
}
|
||||
case reflect.Float32, reflect.Float64:
|
||||
{
|
||||
switch vy.Kind() {
|
||||
case reflect.Int, reflect.Int32, reflect.Int64, reflect.Int16, reflect.Int8:
|
||||
return vx.Float() == float64(vy.Int())
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return vx.Float() == vy.Float()
|
||||
case reflect.String:
|
||||
return fmt.Sprintf("%f", vx.Float()) == vy.String()
|
||||
}
|
||||
}
|
||||
case reflect.String:
|
||||
{
|
||||
switch vy.Kind() {
|
||||
case reflect.Int, reflect.Int32, reflect.Int64, reflect.Int16, reflect.Int8:
|
||||
return vx.String() == fmt.Sprintf("%d", vy.Int())
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return vx.String() == fmt.Sprintf("%f", vy.Float())
|
||||
case reflect.String:
|
||||
return vx.String() == fmt.Sprintf("%s", vy.String())
|
||||
}
|
||||
}
|
||||
case reflect.Bool:
|
||||
{
|
||||
switch vy.Kind() {
|
||||
case reflect.Int, reflect.Int32, reflect.Int64, reflect.Int16, reflect.Int8:
|
||||
return vx.Bool() && vy.Int() != 0
|
||||
case reflect.Bool:
|
||||
return vx.Bool() == vy.Bool()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func runtime_lss(x, y interface{}) bool {
|
||||
vx, vy := reflect.ValueOf(x), reflect.ValueOf(y)
|
||||
switch vx.Kind() {
|
||||
case reflect.Int, reflect.Int32, reflect.Int64, reflect.Int16, reflect.Int8:
|
||||
{
|
||||
switch vy.Kind() {
|
||||
case reflect.Int, reflect.Int32, reflect.Int64, reflect.Int16, reflect.Int8:
|
||||
return vx.Int() < vy.Int()
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return float64(vx.Int()) < vy.Float()
|
||||
case reflect.String:
|
||||
return fmt.Sprintf("%d", vx.Int()) < vy.String()
|
||||
}
|
||||
}
|
||||
case reflect.Float32, reflect.Float64:
|
||||
{
|
||||
switch vy.Kind() {
|
||||
case reflect.Int, reflect.Int32, reflect.Int64, reflect.Int16, reflect.Int8:
|
||||
return vx.Float() < float64(vy.Int())
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return vx.Float() < vy.Float()
|
||||
case reflect.String:
|
||||
return fmt.Sprintf("%f", vx.Float()) < vy.String()
|
||||
}
|
||||
}
|
||||
case reflect.String:
|
||||
{
|
||||
switch vy.Kind() {
|
||||
case reflect.Int, reflect.Int32, reflect.Int64, reflect.Int16, reflect.Int8:
|
||||
return vx.String() < fmt.Sprintf("%d", vy.Int())
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return vx.String() < fmt.Sprintf("%f", vy.Float())
|
||||
case reflect.String:
|
||||
return vx.String() < vy.String()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func runtime_gtr(x, y interface{}) bool {
|
||||
return !runtime_lss(x, y) && !runtime_eql(x, y)
|
||||
}
|
||||
|
||||
func runtime_json(x interface{}) (res string, err error) {
|
||||
bres, err := json.Marshal(x)
|
||||
res = string(bres)
|
||||
return
|
||||
}
|
||||
|
||||
func runtime_unescaped(x string) interface{} {
|
||||
return template.HTML(x)
|
||||
}
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 442 B After Width: | Height: | Size: 442 B |
39
server/app/index.html
Normal file
39
server/app/index.html
Normal file
|
@ -0,0 +1,39 @@
|
|||
<!doctype html>
|
||||
<html ng-app="app">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="author" content="Brad Rydzewski" />
|
||||
<title></title>
|
||||
|
||||
<link rel="shortcut icon" href="/favicon.png" />
|
||||
<link rel="stylesheet" href="//yui.yahooapis.com/pure/0.4.2/pure-min.css" />
|
||||
<link rel="stylesheet" href="//fonts.googleapis.com/css?family=Open+Sans" />
|
||||
<link rel="stylesheet" href="//fonts.googleapis.com/css?family=Orbitron" />
|
||||
<link rel="stylesheet" href="//fonts.googleapis.com/css?family=Droid+Sans+Mono" />
|
||||
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div ng-view></div>
|
||||
|
||||
<script src="//cdnjs.cloudflare.com/ajax/libs/angular.js/1.2.8/angular.min.js"></script>
|
||||
<script src="//cdnjs.cloudflare.com/ajax/libs/angular.js/1.2.8/angular-route.min.js"></script>
|
||||
<script src="//cdnjs.cloudflare.com/ajax/libs/angular.js/1.2.8/angular-resource.min.js"></script>
|
||||
<script src="//cdnjs.cloudflare.com/ajax/libs/angular-ui/0.4.0/angular-ui.min.js"></script>
|
||||
<script src="//cdnjs.cloudflare.com/ajax/libs/moment.js/2.6.0/moment.min.js"></script>
|
||||
|
||||
<!-- main javascript application -->
|
||||
<script src="/scripts/app.js"></script>
|
||||
<script src="/scripts/controllers/conf.js"></script>
|
||||
<script src="/scripts/controllers/home.js"></script>
|
||||
<script src="/scripts/controllers/repo.js"></script>
|
||||
<script src="/scripts/controllers/user.js"></script>
|
||||
<script src="/scripts/controllers/users.js"></script>
|
||||
<script src="/scripts/controllers/setup.js"></script>
|
||||
<script src="/scripts/services/auth.js"></script>
|
||||
<script src="/scripts/services/conf.js"></script>
|
||||
<script src="/scripts/services/repo.js"></script>
|
||||
<script src="/scripts/services/user.js"></script>
|
||||
<script src="/scripts/filters/filters.js"></script>
|
||||
</body>
|
||||
</html>
|
3
server/app/robots.txt
Normal file
3
server/app/robots.txt
Normal file
|
@ -0,0 +1,3 @@
|
|||
# robotstxt.org
|
||||
|
||||
User-agent: *
|
395
server/app/scripts/app.js
Normal file
395
server/app/scripts/app.js
Normal file
|
@ -0,0 +1,395 @@
|
|||
'use strict';
|
||||
|
||||
var app = angular.module('app', [
|
||||
'ngRoute',
|
||||
'ui.filters'
|
||||
]);
|
||||
|
||||
app.config(['$routeProvider', '$locationProvider', function($routeProvider, $locationProvider) {
|
||||
$routeProvider.when('/', {
|
||||
templateUrl: '/views/index.html',
|
||||
controller: 'HomeController',
|
||||
title: 'Dashboard',
|
||||
resolve: {
|
||||
user: function(authService) {
|
||||
return authService.getUser();
|
||||
}
|
||||
}
|
||||
})
|
||||
.when('/login', {
|
||||
templateUrl: '/views/login.html',
|
||||
title: 'Login',
|
||||
})
|
||||
.when('/setup', {
|
||||
templateUrl: '/views/setup.html',
|
||||
controller: 'SetupController',
|
||||
title: 'Setup'
|
||||
})
|
||||
.when('/setup/:remote', {
|
||||
templateUrl: '/views/setup.html',
|
||||
controller: 'SetupController',
|
||||
title: 'Setup'
|
||||
})
|
||||
.when('/account/profile', {
|
||||
templateUrl: '/views/account.html',
|
||||
controller: 'UserController',
|
||||
title: 'Profile',
|
||||
resolve: {
|
||||
user: function(authService) {
|
||||
return authService.getUser();
|
||||
}
|
||||
}
|
||||
})
|
||||
.when('/account/repos', {
|
||||
templateUrl: '/views/repo_list.html',
|
||||
controller: 'AccountReposController',
|
||||
title: 'Repositories',
|
||||
resolve: {
|
||||
user: function(authService) {
|
||||
return authService.getUser();
|
||||
}
|
||||
}
|
||||
})
|
||||
.when('/admin/users', {
|
||||
templateUrl: '/views/sys_users.html',
|
||||
controller: 'UsersController',
|
||||
title: 'System Users',
|
||||
resolve: {
|
||||
user: function(authService) {
|
||||
return authService.getUser();
|
||||
}
|
||||
}
|
||||
})
|
||||
.when('/admin/settings', {
|
||||
templateUrl: '/views/sys_config.html',
|
||||
controller: 'ConfigController',
|
||||
title: 'System Settings',
|
||||
resolve: {
|
||||
user: function(authService) {
|
||||
return authService.getUser();
|
||||
},
|
||||
conf: function(confService) {
|
||||
return confService.getConfig();
|
||||
}
|
||||
}
|
||||
})
|
||||
.when('/:remote/:owner/:name/settings', {
|
||||
templateUrl: '/views/repo_conf.html',
|
||||
controller: 'RepoConfigController',
|
||||
title: 'Repository Settings',
|
||||
resolve: {
|
||||
user: function(authService) {
|
||||
return authService.getUser();
|
||||
}
|
||||
}
|
||||
})
|
||||
.when('/:remote/:owner/:name/:branch/:commit', {
|
||||
templateUrl: '/views/commit.html',
|
||||
controller: 'CommitController',
|
||||
title: 'Recent Commits',
|
||||
resolve: {
|
||||
user: function(authService) {
|
||||
return authService.getUser();
|
||||
}
|
||||
}
|
||||
})
|
||||
.when('/:remote/:owner/:name/:branch', {
|
||||
templateUrl: '/views/branch.html',
|
||||
controller: 'BranchController',
|
||||
title: 'Recent Commits',
|
||||
resolve: {
|
||||
user: function(authService) {
|
||||
return authService.getUser();
|
||||
}
|
||||
}
|
||||
})
|
||||
.when('/:remote/:owner/:name', {
|
||||
templateUrl: '/views/repo.html',
|
||||
controller: 'RepoController',
|
||||
title: 'Recent Commits',
|
||||
resolve: {
|
||||
user: function(authService) {
|
||||
return authService.getUser();
|
||||
},
|
||||
repo: function($route, repoService) {
|
||||
var remote = $route.current.params.remote;
|
||||
var owner = $route.current.params.owner;
|
||||
var name = $route.current.params.name;
|
||||
return repoService.getRepo(remote, owner, name);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// use the HTML5 History API
|
||||
$locationProvider.html5Mode(true);
|
||||
}]);
|
||||
|
||||
/* Directives */
|
||||
|
||||
/* also see https://coderwall.com/p/vcfo4q */
|
||||
app.run(['$location', '$rootScope', '$routeParams', function($location, $rootScope, $routeParams) {
|
||||
$rootScope.$on('$routeChangeSuccess', function (event, current, previous) {
|
||||
document.title = current.$$route.title + ' · drone.io';
|
||||
});
|
||||
}]);
|
||||
|
||||
|
||||
|
||||
|
||||
/* Controllers */
|
||||
|
||||
|
||||
|
||||
|
||||
app.controller("AccountReposController", function($scope, $http, user) {
|
||||
|
||||
$scope.user = user;
|
||||
|
||||
// get the user details
|
||||
$http({method: 'GET', url: '/v1/user/repos'}).
|
||||
success(function(data, status, headers, config) {
|
||||
$scope.repos = (typeof data==='string')?[]:data;
|
||||
}).
|
||||
error(function(data, status, headers, config) {
|
||||
console.log(data);
|
||||
});
|
||||
|
||||
$scope.active="";
|
||||
$scope.remote="";
|
||||
$scope.byActive = function(entry){
|
||||
switch (true) {
|
||||
case $scope.active == "true" && !entry.active: return false;
|
||||
case $scope.active == "false" && entry.active: return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
$scope.byRemote = function(entry){
|
||||
return $scope.remote == "" || $scope.remote == entry.remote;
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
|
||||
app.controller("ConfigController", function($scope, $http, user) {
|
||||
|
||||
$scope.user = user;
|
||||
|
||||
$http({method: 'GET', url: '/v1/config'}).
|
||||
success(function(data, status, headers, config) {
|
||||
$scope.config = data;
|
||||
}).
|
||||
error(function(data, status, headers, config) {
|
||||
console.log(data);
|
||||
});
|
||||
});
|
||||
|
||||
app.controller("RepoConfigController", function($scope, $http, $routeParams, user) {
|
||||
|
||||
$scope.user = user;
|
||||
|
||||
var remote = $routeParams.remote;
|
||||
var owner = $routeParams.owner;
|
||||
var name = $routeParams.name;
|
||||
|
||||
|
||||
|
||||
// load the repo meta-data
|
||||
$http({method: 'GET', url: '/v1/repos/'+remote+'/'+owner+"/"+name}).
|
||||
success(function(data, status, headers, config) {
|
||||
$scope.repo = data;
|
||||
$scope.repoTemp = {
|
||||
pull_requests : $scope.repo.pull_requests,
|
||||
post_commits : $scope.repo.post_commits,
|
||||
params : $scope.repo.params,
|
||||
timeout : $scope.repo.timeout,
|
||||
privileged : $scope.repo.privileged
|
||||
};
|
||||
|
||||
$scope.badgeMarkdown = badgeMarkdown(data.remote+"/"+data.owner+"/"+data.name)
|
||||
$scope.badgeMarkup = badgeMarkup(data.remote+"/"+data.owner+"/"+data.name)
|
||||
}).
|
||||
error(function(data, status, headers, config) {
|
||||
console.log(data);
|
||||
});
|
||||
|
||||
$scope.save = function() {
|
||||
// request to create a new repository
|
||||
$http({method: 'PUT', url: '/v1/repos/'+remote+'/'+owner+"/"+name, data: $scope.repoTemp }).
|
||||
success(function(data, status, headers, config) {
|
||||
delete $scope.failure;
|
||||
$scope.repo = data;
|
||||
}).
|
||||
error(function(data, status, headers, config) {
|
||||
$scope.failure = data;
|
||||
});
|
||||
};
|
||||
$scope.cancel = function() {
|
||||
delete $scope.failure;
|
||||
$scope.repoTemp = {
|
||||
pull_requests : $scope.repo.pull_requests,
|
||||
post_commits : $scope.repo.post_commits,
|
||||
params : $scope.repo.params,
|
||||
timeout : $scope.repo.timeout,
|
||||
privileged : $scope.repo.privileged
|
||||
};
|
||||
};
|
||||
});
|
||||
|
||||
function badgeMarkdown(repo) {
|
||||
var scheme = window.location.protocol;
|
||||
var host = window.location.host;
|
||||
return '[![Build Status]('+scheme+'//'+host+'/v1/badge/'+repo+'/status.svg?branch=master)]('+scheme+'//'+host+'/'+repo+')'
|
||||
}
|
||||
|
||||
function badgeMarkup(repo) {
|
||||
var scheme = window.location.protocol;
|
||||
var host = window.location.host;
|
||||
return '<a href="'+scheme+'//'+host+'/'+repo+'"><img src="'+scheme+'//'+host+'/v1/badge/'+repo+'/status.svg?branch=master" /></a>'
|
||||
}
|
||||
|
||||
app.controller("RepoController", function($scope, $http, $routeParams, user, repo) {
|
||||
$scope.user = user;
|
||||
$scope.repo = repo;
|
||||
|
||||
// load the repo branch list
|
||||
$http({method: 'GET', url: '/v1/repos/'+repo.host+'/'+repo.owner+"/"+repo.name+"/branches"}).
|
||||
success(function(data, status, headers, config) {
|
||||
$scope.branches = (typeof data==='string')?[]:data;
|
||||
}).
|
||||
error(function(data, status, headers, config) {
|
||||
console.log(data);
|
||||
});
|
||||
|
||||
// load the repo commit feed
|
||||
$http({method: 'GET', url: '/v1/repos/'+repo.host+'/'+repo.owner+"/"+repo.name+"/feed"}).
|
||||
success(function(data, status, headers, config) {
|
||||
$scope.commits = (typeof data==='string')?[]:data;
|
||||
}).
|
||||
error(function(data, status, headers, config) {
|
||||
console.log(data);
|
||||
});
|
||||
|
||||
$scope.activate = function() {
|
||||
// request to create a new repository
|
||||
$http({method: 'POST', url: '/v1/repos/'+repo.host+'/'+repo.owner+"/"+repo.name }).
|
||||
success(function(data, status, headers, config) {
|
||||
$scope.repo = data;
|
||||
}).
|
||||
error(function(data, status, headers, config) {
|
||||
$scope.failure = data;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.options={
|
||||
barColor:"#40C598",
|
||||
trackColor:'#EEEEEE',
|
||||
scaleColor:false,
|
||||
lineWidth:10,
|
||||
lineCap:'butt',
|
||||
size:130
|
||||
};
|
||||
});
|
||||
|
||||
app.controller("BranchController", function($scope, $http, $routeParams, user) {
|
||||
|
||||
$scope.user = user;
|
||||
var remote = $routeParams.remote;
|
||||
var owner = $routeParams.owner;
|
||||
var name = $routeParams.name;
|
||||
var branch = $routeParams.branch;
|
||||
$scope.branch = branch;
|
||||
|
||||
// load the repo meta-data
|
||||
$http({method: 'GET', url: '/v1/repos/'+remote+'/'+owner+"/"+name}).
|
||||
success(function(data, status, headers, config) {
|
||||
$scope.repo = data;
|
||||
}).
|
||||
error(function(data, status, headers, config) {
|
||||
console.log(data);
|
||||
});
|
||||
|
||||
// load the repo branch list
|
||||
$http({method: 'GET', url: '/v1/repos/'+remote+'/'+owner+"/"+name+"/branches"}).
|
||||
success(function(data, status, headers, config) {
|
||||
$scope.branches = (typeof data==='string')?[]:data;
|
||||
}).
|
||||
error(function(data, status, headers, config) {
|
||||
console.log(data);
|
||||
});
|
||||
|
||||
// load the repo commit feed
|
||||
$http({method: 'GET', url: '/v1/repos/'+remote+'/'+owner+"/"+name+"/branches/"+branch+"/commits"}).
|
||||
success(function(data, status, headers, config) {
|
||||
$scope.commits = data;
|
||||
}).
|
||||
error(function(data, status, headers, config) {
|
||||
console.log(data);
|
||||
});
|
||||
});
|
||||
|
||||
app.controller("CommitController", function($scope, $http, $routeParams, user) {
|
||||
|
||||
$scope.user = user;
|
||||
var remote = $routeParams.remote;
|
||||
var owner = $routeParams.owner;
|
||||
var name = $routeParams.name;
|
||||
var branch = $routeParams.branch;
|
||||
var commit = $routeParams.commit;
|
||||
|
||||
// load the repo meta-data
|
||||
$http({method: 'GET', url: '/v1/repos/'+remote+'/'+owner+"/"+name}).
|
||||
success(function(data, status, headers, config) {
|
||||
$scope.repo = data;
|
||||
}).
|
||||
error(function(data, status, headers, config) {
|
||||
console.log(data);
|
||||
});
|
||||
|
||||
// load the repo commit data
|
||||
$http({method: 'GET', url: '/v1/repos/'+remote+'/'+owner+"/"+name+"/branches/"+branch+"/commits/"+commit}).
|
||||
success(function(data, status, headers, config) {
|
||||
$scope.commit = data;
|
||||
$scope.coverage=45;
|
||||
$scope.passing=100;
|
||||
}).
|
||||
error(function(data, status, headers, config) {
|
||||
console.log(data);
|
||||
});
|
||||
|
||||
// load the repo build data
|
||||
$http({method: 'GET', url: '/v1/repos/'+remote+'/'+owner+"/"+name+"/branches/"+branch+"/commits/"+commit+"/builds/1"}).
|
||||
success(function(data, status, headers, config) {
|
||||
$scope.build = data;
|
||||
}).
|
||||
error(function(data, status, headers, config) {
|
||||
console.log(data);
|
||||
});
|
||||
|
||||
// load the repo build stdout
|
||||
$http({method: 'GET', url: '/v1/repos/'+remote+'/'+owner+"/"+name+"/branches/"+branch+"/commits/"+commit+"/builds/1/console"}).
|
||||
success(function(data, status, headers, config) {
|
||||
$scope.console = data;
|
||||
}).
|
||||
error(function(data, status, headers, config) {
|
||||
console.log(data);
|
||||
});
|
||||
|
||||
|
||||
$scope.options={
|
||||
barColor:"#40C598",
|
||||
trackColor:'#EEEEEE',
|
||||
scaleColor:false,
|
||||
lineWidth:10,
|
||||
lineCap:'butt',
|
||||
size:130
|
||||
};
|
||||
|
||||
});
|
||||
|
||||
function barColor(percent) {
|
||||
switch(true) {
|
||||
case percent > 80: return "#40C598";
|
||||
case percent < 50: return "rgba(189, 54, 47, 0.8)";
|
||||
default: return "#f0ad4e";
|
||||
}
|
||||
}
|
98
server/app/scripts/commit_updates.js
Normal file
98
server/app/scripts/commit_updates.js
Normal file
|
@ -0,0 +1,98 @@
|
|||
;// Live commit updates
|
||||
|
||||
if(typeof(Drone) === 'undefined') { Drone = {}; }
|
||||
|
||||
(function () {
|
||||
Drone.CommitUpdates = function(socket) {
|
||||
if(typeof(socket) === "string") {
|
||||
var url = [(window.location.protocol == 'https:' ? 'wss' : 'ws'),
|
||||
'://',
|
||||
window.location.host,
|
||||
socket].join('')
|
||||
this.socket = new WebSocket(url);
|
||||
} else {
|
||||
this.socket = socket;
|
||||
}
|
||||
|
||||
this.lineFormatter = new Drone.LineFormatter();
|
||||
this.attach();
|
||||
}
|
||||
|
||||
Drone.CommitUpdates.prototype = {
|
||||
lineBuffer: "",
|
||||
autoFollow: false,
|
||||
|
||||
startOutput: function(el) {
|
||||
if(typeof(el) === 'string') {
|
||||
this.el = document.getElementById(el);
|
||||
} else {
|
||||
this.el = el;
|
||||
}
|
||||
|
||||
if(!this.reqId) {
|
||||
this.updateScreen();
|
||||
}
|
||||
},
|
||||
|
||||
stopOutput: function() {
|
||||
this.stoppingRefresh = true;
|
||||
},
|
||||
|
||||
attach: function() {
|
||||
this.socket.onopen = this.onOpen;
|
||||
this.socket.onerror = this.onError;
|
||||
this.socket.onmessage = this.onMessage.bind(this);
|
||||
this.socket.onclose = this.onClose;
|
||||
},
|
||||
|
||||
updateScreen: function() {
|
||||
if(this.lineBuffer.length > 0) {
|
||||
this.el.innerHTML += this.lineBuffer;
|
||||
this.lineBuffer = '';
|
||||
|
||||
if (this.autoFollow) {
|
||||
window.scrollTo(0, document.body.scrollHeight);
|
||||
}
|
||||
}
|
||||
|
||||
if(this.stoppingRefresh) {
|
||||
this.stoppingRefresh = false;
|
||||
} else {
|
||||
window.requestAnimationFrame(this.updateScreen.bind(this));
|
||||
}
|
||||
},
|
||||
|
||||
onOpen: function() {
|
||||
console.log('output websocket open');
|
||||
},
|
||||
|
||||
onError: function(e) {
|
||||
console.log('websocket error: ' + e);
|
||||
},
|
||||
|
||||
onMessage: function(e) {
|
||||
this.lineBuffer += this.lineFormatter.format(e.data);
|
||||
},
|
||||
|
||||
onClose: function(e) {
|
||||
console.log('output websocket closed: ' + JSON.stringify(e));
|
||||
window.location.reload();
|
||||
}
|
||||
};
|
||||
|
||||
// Polyfill rAF for older browsers
|
||||
window.requestAnimationFrame = window.requestAnimationFrame ||
|
||||
window.webkitRequestAnimationFrame ||
|
||||
function(callback, element) {
|
||||
return window.setTimeout(function() {
|
||||
callback(+new Date());
|
||||
}, 1000 / 60);
|
||||
};
|
||||
|
||||
window.cancelRequestAnimationFrame = window.cancelRequestAnimationFrame ||
|
||||
window.cancelWebkitRequestAnimationFrame ||
|
||||
function(fn) {
|
||||
window.clearTimeout(fn);
|
||||
};
|
||||
|
||||
})();
|
1
server/app/scripts/controllers/build.js
Normal file
1
server/app/scripts/controllers/build.js
Normal file
|
@ -0,0 +1 @@
|
|||
'use strict';
|
1
server/app/scripts/controllers/conf.js
Normal file
1
server/app/scripts/controllers/conf.js
Normal file
|
@ -0,0 +1 @@
|
|||
'use strict';
|
14
server/app/scripts/controllers/home.js
Normal file
14
server/app/scripts/controllers/home.js
Normal file
|
@ -0,0 +1,14 @@
|
|||
'use strict';
|
||||
|
||||
angular.module('app').controller("HomeController", function($scope, $http, user) {
|
||||
|
||||
$scope.user = user;
|
||||
|
||||
$http({method: 'GET', url: '/v1/user/feed'}).
|
||||
success(function(data, status, headers, config) {
|
||||
$scope.feed = (typeof data==='string')?[]:data;
|
||||
}).
|
||||
error(function(data, status, headers, config) {
|
||||
console.log(data);
|
||||
});
|
||||
});
|
1
server/app/scripts/controllers/repo.js
Normal file
1
server/app/scripts/controllers/repo.js
Normal file
|
@ -0,0 +1 @@
|
|||
'use strict';
|
36
server/app/scripts/controllers/setup.js
Normal file
36
server/app/scripts/controllers/setup.js
Normal file
|
@ -0,0 +1,36 @@
|
|||
'use strict';
|
||||
|
||||
angular.module('app').controller("SetupController", function($scope, $http, $routeParams) {
|
||||
|
||||
// create a remote that will be populated
|
||||
// and persisted to the database.
|
||||
$scope.remote = {};
|
||||
$scope.remote.type = $routeParams.remote;
|
||||
$scope.remote.register = true;
|
||||
switch($scope.remote.type) {
|
||||
case undefined:
|
||||
case 'github.com':
|
||||
$scope.remote.type = "github.com"
|
||||
$scope.remote.url = "https://github.com";
|
||||
$scope.remote.api = "https://api.github.com";
|
||||
break;
|
||||
case 'bitbucket.org':
|
||||
$scope.remote.url = "https://bitbucket.org";
|
||||
$scope.remote.api = "https://bitbucket.org";
|
||||
break;
|
||||
}
|
||||
|
||||
$scope.save = function() {
|
||||
// request to create a new repository
|
||||
$http({method: 'POST', url: '/v1/remotes', data: $scope.remote }).
|
||||
success(function(data, status, headers, config) {
|
||||
delete $scope.failure;
|
||||
$scope.remote = data;
|
||||
console.log('success', $scope.remote);
|
||||
}).
|
||||
error(function(data, status, headers, config) {
|
||||
$scope.failure = data;
|
||||
console.log('failure', $scope.failure);
|
||||
});
|
||||
};
|
||||
});
|
38
server/app/scripts/controllers/user.js
Normal file
38
server/app/scripts/controllers/user.js
Normal file
|
@ -0,0 +1,38 @@
|
|||
'use strict';
|
||||
|
||||
angular.module('app').controller("UserController", function($scope, $http, user) {
|
||||
|
||||
$scope.user = user;
|
||||
|
||||
// get the user details
|
||||
$http({method: 'GET', url: '/v1/user'}).
|
||||
success(function(data, status, headers, config) {
|
||||
$scope.user = data;
|
||||
$scope.userTemp = {
|
||||
email : $scope.user.email,
|
||||
name : $scope.user.name
|
||||
};
|
||||
}).
|
||||
error(function(data, status, headers, config) {
|
||||
console.log(data);
|
||||
});
|
||||
|
||||
$scope.save = function() {
|
||||
// request to create a new repository
|
||||
$http({method: 'PUT', url: '/v1/user', data: $scope.userTemp }).
|
||||
success(function(data, status, headers, config) {
|
||||
delete $scope.failure;
|
||||
$scope.user = data;
|
||||
}).
|
||||
error(function(data, status, headers, config) {
|
||||
$scope.failure = data;
|
||||
});
|
||||
};
|
||||
$scope.cancel = function() {
|
||||
delete $scope.failure;
|
||||
$scope.userTemp = {
|
||||
email : $scope.user.email,
|
||||
name : $scope.user.name
|
||||
};
|
||||
};
|
||||
});
|
14
server/app/scripts/controllers/users.js
Normal file
14
server/app/scripts/controllers/users.js
Normal file
|
@ -0,0 +1,14 @@
|
|||
'use strict';
|
||||
|
||||
angular.module('app').controller("UsersController", function($scope, $http, user) {
|
||||
|
||||
$scope.user = user;
|
||||
|
||||
$http({method: 'GET', url: '/v1/users'}).
|
||||
success(function(data, status, headers, config) {
|
||||
$scope.users = data;
|
||||
}).
|
||||
error(function(data, status, headers, config) {
|
||||
console.log(data);
|
||||
});
|
||||
});
|
62
server/app/scripts/filters/filters.js
Normal file
62
server/app/scripts/filters/filters.js
Normal file
|
@ -0,0 +1,62 @@
|
|||
'use strict';
|
||||
|
||||
angular.module('app').filter('gravatar', function() {
|
||||
return function(gravatar) {
|
||||
return "https://secure.gravatar.com/avatar/"+gravatar+"?s=32&d=mm"
|
||||
}
|
||||
});
|
||||
|
||||
angular.module('app').filter('fromNow', function() {
|
||||
return function(date) {
|
||||
return moment(new Date(date*1000)).fromNow();
|
||||
}
|
||||
});
|
||||
|
||||
angular.module('app').filter('toDuration', function() {
|
||||
return function(seconds) {
|
||||
return moment.duration(seconds, "seconds").humanize();
|
||||
}
|
||||
});
|
||||
|
||||
angular.module('app').filter('toDate', function() {
|
||||
return function(date) {
|
||||
return moment(new Date(date*1000)).format('ll');
|
||||
}
|
||||
});
|
||||
|
||||
angular.module('app').filter('fullName', function() {
|
||||
return function(repo) {
|
||||
return repo.owner+"/"+repo.name;
|
||||
}
|
||||
});
|
||||
|
||||
angular.module('app').filter('fullPath', function() {
|
||||
return function(repo) {
|
||||
return repo.remote+"/"+repo.owner+"/"+repo.name;
|
||||
}
|
||||
});
|
||||
|
||||
angular.module('app').filter('shortHash', function() {
|
||||
return function(sha) {
|
||||
if (!sha) { return ""; }
|
||||
return sha.substr(0,10)
|
||||
}
|
||||
});
|
||||
|
||||
angular.module('app').filter('badgeMarkdown', function() {
|
||||
return function(repo) {
|
||||
var scheme = window.location.protocol;
|
||||
var host = window.location.host;
|
||||
var path = repo.host+'/'+repo.owner+'/'+repo.name;
|
||||
return '[![Build Status]('+scheme+'//'+host+'/v1/badge/'+path+'/status.svg?branch=master)]('+scheme+'//'+host+'/'+path+')'
|
||||
}
|
||||
});
|
||||
|
||||
angular.module('app').filter('badgeMarkup', function() {
|
||||
return function(repo) {
|
||||
var scheme = window.location.protocol;
|
||||
var host = window.location.host;
|
||||
var path = repo.host+'/'+repo.owner+'/'+repo.name;
|
||||
return '<a href="'+scheme+'//'+host+'/'+path+'"><img src="'+scheme+'//'+host+'/v1/badge/'+path+'/status.svg?branch=master" /></a>'
|
||||
}
|
||||
});
|
34
server/app/scripts/services/auth.js
Normal file
34
server/app/scripts/services/auth.js
Normal file
|
@ -0,0 +1,34 @@
|
|||
'use strict;'
|
||||
|
||||
angular.module('app').service('authService', function($q, $http) {
|
||||
return{
|
||||
user : null,
|
||||
// getUser will retrieve the currently authenticated
|
||||
// user from the session. If no user is found a 401
|
||||
// Not Authorized status will be returned.
|
||||
getUser : function() {
|
||||
var _this = this;
|
||||
var defer = $q.defer();
|
||||
|
||||
// if the user is already authenticated
|
||||
if (_this.user != null) {
|
||||
defer.resolve(_this.user);
|
||||
}
|
||||
|
||||
// else we need to fetch from the server
|
||||
$http({method: 'GET', url: '/v1/user'}).
|
||||
success(function(data) {
|
||||
_this.user=data;
|
||||
defer.resolve(_this.user);
|
||||
}).
|
||||
error(function(data, status) {
|
||||
_this.user=null;
|
||||
defer.resolve();
|
||||
});
|
||||
|
||||
// returns a promise that this will complete
|
||||
// at some future time.
|
||||
return defer.promise;
|
||||
}
|
||||
}
|
||||
});
|
14
server/app/scripts/services/conf.js
Normal file
14
server/app/scripts/services/conf.js
Normal file
|
@ -0,0 +1,14 @@
|
|||
'use strict';
|
||||
|
||||
angular.module('app').service('confService', function($q, $http) {
|
||||
return{
|
||||
getConfig : function() {
|
||||
var defer = $q.defer();
|
||||
var route = '/v1/config';
|
||||
$http.get(route).success(function(data){
|
||||
defer.resolve(data);
|
||||
});
|
||||
return defer.promise;
|
||||
}
|
||||
}
|
||||
});
|
14
server/app/scripts/services/repo.js
Normal file
14
server/app/scripts/services/repo.js
Normal file
|
@ -0,0 +1,14 @@
|
|||
'use strict';
|
||||
|
||||
angular.module('app').service('repoService', function($q, $http) {
|
||||
return{
|
||||
getRepo : function(host, owner, name) {
|
||||
var defer = $q.defer();
|
||||
var route = '/v1/repos/'+host+'/'+owner+'/'+name;
|
||||
$http.get(route).success(function(data){
|
||||
defer.resolve(data);
|
||||
});
|
||||
return defer.promise;
|
||||
}
|
||||
}
|
||||
});
|
31
server/app/scripts/services/user.js
Normal file
31
server/app/scripts/services/user.js
Normal file
|
@ -0,0 +1,31 @@
|
|||
'use strict';
|
||||
|
||||
angular.module('app').service('userService', function($q, $http, $timeout) {
|
||||
return{
|
||||
user : null,
|
||||
getCurrent : function() {
|
||||
var _this = this;
|
||||
var defer = $q.defer();
|
||||
|
||||
// if the user is already authenticated
|
||||
if (_this.user != null) {
|
||||
defer.resolve(_this.user);
|
||||
}
|
||||
|
||||
// else we need to fetch from the server
|
||||
$http({method: 'GET', url: '/v1/user'}).
|
||||
then(function(data) {
|
||||
_this.user=data;
|
||||
defer.resolve(_this.user);
|
||||
}).
|
||||
error(function(data, status) {
|
||||
_this.user=null;
|
||||
defer.resolve();
|
||||
});
|
||||
|
||||
// returns a promise that this will complete
|
||||
// at some future time.
|
||||
return defer.promise;
|
||||
}
|
||||
}
|
||||
});
|
66
server/app/scripts/util/line_formatter.js
Normal file
66
server/app/scripts/util/line_formatter.js
Normal file
|
@ -0,0 +1,66 @@
|
|||
;// Format ANSI to HTML
|
||||
|
||||
if(typeof(Drone) === 'undefined') { Drone = {}; }
|
||||
|
||||
(function() {
|
||||
Drone.LineFormatter = function() {};
|
||||
|
||||
Drone.LineFormatter.prototype = {
|
||||
regex: /\u001B\[([0-9]+;?)*[Km]/g,
|
||||
styles: [],
|
||||
|
||||
format: function(s) {
|
||||
// Check for newline and early exit?
|
||||
s = s.replace(/</g, "<");
|
||||
s = s.replace(/>/g, ">");
|
||||
|
||||
var output = "";
|
||||
var current = 0;
|
||||
while (m = this.regex.exec(s)) {
|
||||
var part = s.substring(current, m.index);
|
||||
current = this.regex.lastIndex;
|
||||
|
||||
var token = s.substr(m.index, this.regex.lastIndex - m.index);
|
||||
var code = token.substr(2, token.length-2);
|
||||
|
||||
var pre = "";
|
||||
var post = "";
|
||||
|
||||
switch (code) {
|
||||
case 'm':
|
||||
case '0m':
|
||||
var len = this.styles.length;
|
||||
for (var i=0; i < len; i++) {
|
||||
this.styles.pop();
|
||||
post += "</span>"
|
||||
}
|
||||
break;
|
||||
case '30;42m': pre = '<span style="color:black;background:lime">'; break;
|
||||
case '36m':
|
||||
case '36;1m': pre = '<span style="color:cyan;">'; break;
|
||||
case '31m':
|
||||
case '31;31m': pre = '<span style="color:red;">'; break;
|
||||
case '33m':
|
||||
case '33;33m': pre = '<span style="color:yellow;">'; break;
|
||||
case '32m':
|
||||
case '0;32m': pre = '<span style="color:lime;">'; break;
|
||||
case '90m': pre = '<span style="color:gray;">'; break;
|
||||
case 'K':
|
||||
case '0K':
|
||||
case '1K':
|
||||
case '2K': break;
|
||||
}
|
||||
|
||||
if (pre !== "") {
|
||||
this.styles.push(pre);
|
||||
}
|
||||
|
||||
output += part + pre + post;
|
||||
}
|
||||
|
||||
var part = s.substring(current, s.length);
|
||||
output += part;
|
||||
return output;
|
||||
}
|
||||
};
|
||||
})();
|
64
server/app/views/account.html
Normal file
64
server/app/views/account.html
Normal file
|
@ -0,0 +1,64 @@
|
|||
<div ng-include src=" '/views/header.html' "></div>
|
||||
|
||||
<h1>User Account</h1>
|
||||
|
||||
<div class="row">
|
||||
|
||||
<!-- summary column -->
|
||||
<div class="col-xs-6 col-sm-3">
|
||||
What to show here?
|
||||
</div>
|
||||
|
||||
<!-- primary column -->
|
||||
<div class="col-xs-12 col-sm-9">
|
||||
|
||||
<div>
|
||||
<label>Remote</label>
|
||||
<span>{{ user.remote }}</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label>Login</label>
|
||||
<span>{{ user.login }}</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label>Gravatar</label>
|
||||
<img class="gravatar" ng-src="https://secure.gravatar.com/avatar/{{ user.gravatar }}?s=32&d=mm" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label>Email</label>
|
||||
<input type="text" ng-model="userTemp.email" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label>Full Name</label>
|
||||
<input type="text" ng-model="userTemp.name" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label>Created</label>
|
||||
<span>{{ user.created_at | fromNow }}</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label>Updated</label>
|
||||
<span>{{ user.updated_at | fromNow }}</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label>Synced</label>
|
||||
<span>{{ user.synced_at | fromNow }}</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{{failure}}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button ng-click="save()">Save</button>
|
||||
<button ng-click="cancel()">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
51
server/app/views/branch.html
Normal file
51
server/app/views/branch.html
Normal file
|
@ -0,0 +1,51 @@
|
|||
<div ng-include src=" '/views/header.html' "></div>
|
||||
|
||||
|
||||
<h1>{{ repo.name }}</h1>
|
||||
<a href="/{{ repo.remote }}/{{ repo.owner}}/{{ repo.name }}">Back</a>
|
||||
<a href="/{{ repo.remote }}/{{ repo.owner}}/{{ repo.name }}/settings">Settings</a>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xs-6 col-sm-3">
|
||||
What to show here?
|
||||
</div>
|
||||
|
||||
<!-- primary column -->
|
||||
<div class="col-xs-12 col-sm-9">
|
||||
<table class="table table-bordered">
|
||||
<tr>
|
||||
<th>Status</th>
|
||||
<th>Branch</th>
|
||||
<th>Sha</th>
|
||||
<th>PR</th>
|
||||
<th colspan=2>Author</th>
|
||||
<th>Message</th>
|
||||
<th>Started</th>
|
||||
<th>Finished</th>
|
||||
</tr>
|
||||
<tr ng-repeat="commit in commits">
|
||||
<td>
|
||||
<a href="/{{ $parent.repo | fullPath }}/{{ commit.branch }}/{{ commit.sha }}">
|
||||
<span>{{ commit.status }}</span>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="/{{ $parent.repo | fullPath }}/{{ commit.branch }}">
|
||||
<span>{{ commit.branch }}</span>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="/{{ $parent.repo | fullPath }}/{{ commit.branch }}/{{ commit.sha }}">
|
||||
<span>{{ commit.sha | shortHash }}</span>
|
||||
</a>
|
||||
</td>
|
||||
<td>{{ commit.pull_request }}</td>
|
||||
<td>{{ commit.author }}</td>
|
||||
<td><img class="gravatar" ng-src="https://secure.gravatar.com/avatar/{{ commit.gravatar }}?s=32&d=mm" /></td>
|
||||
<td>{{ commit.message }}</td>
|
||||
<td>{{ commit.started_at | fromNow }}</td>
|
||||
<td>{{ commit.finished_at | fromNow }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
18
server/app/views/commit.html
Normal file
18
server/app/views/commit.html
Normal file
|
@ -0,0 +1,18 @@
|
|||
<div ng-include src=" '/views/header.html' "></div>
|
||||
|
||||
|
||||
<h1>{{ repo.name }}</h1>
|
||||
<a href="/{{ repo.remote }}/{{ repo.owner}}/{{ repo.name }}">Back</a>
|
||||
<a href="/{{ repo.remote }}/{{ repo.owner}}/{{ repo.name }}/settings">Settings</a>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xs-6 col-sm-3">
|
||||
<span>Commit</span>
|
||||
<span>{{ commit.sha }}</span>
|
||||
</div>
|
||||
|
||||
<!-- primary column -->
|
||||
<div class="col-xs-12 col-sm-9">
|
||||
<pre>{{ console }}</pre>
|
||||
</div>
|
||||
</div>
|
12
server/app/views/header.html
Normal file
12
server/app/views/header.html
Normal file
|
@ -0,0 +1,12 @@
|
|||
<table border="1">
|
||||
<tr ng-if="user != null">
|
||||
<td><a href="/">Home</a></td>
|
||||
<td><a href="/account/profile">{{ user.login }}</a></td>
|
||||
<td><img ng-src="{{ user.gravatar | gravatar }}" /></td>
|
||||
</tr>
|
||||
<tr ng-if="user == null">
|
||||
<td>
|
||||
<a href="/login">Login</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
1
server/app/views/home.html
Normal file
1
server/app/views/home.html
Normal file
|
@ -0,0 +1 @@
|
|||
dashboard.html
|
53
server/app/views/index.html
Normal file
53
server/app/views/index.html
Normal file
|
@ -0,0 +1,53 @@
|
|||
<div ng-include src=" '/views/header.html' "></div>
|
||||
|
||||
<h1>Dashboard</h1>
|
||||
|
||||
<div class="row">
|
||||
|
||||
<!-- primary column -->
|
||||
<div class="col-xs-12 col-sm-9">
|
||||
<table class="table table-bordered">
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Status</th>
|
||||
<th>Branch</th>
|
||||
<th>Sha</th>
|
||||
<th>PR</th>
|
||||
<th colspan=2>Author</th>
|
||||
<th>Message</th>
|
||||
<th>Started</th>
|
||||
<th>Finished</th>
|
||||
</tr>
|
||||
<tr ng-repeat="commit in feed">
|
||||
<td>
|
||||
<a href="/{{ commit | fullPath }}">
|
||||
<span>{{ commit.owner }}</span>
|
||||
<span>/</span>
|
||||
<span>{{ commit.name }}</span>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="/{{ commit | fullPath }}/{{ commit.branch }}/{{ commit.sha }}">
|
||||
<span>{{ commit.status }}</span>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="/{{ commit | fullPath }}/{{ commit.branch }}">
|
||||
<span>{{ commit.branch }}</span>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="/{{ commit | fullPath }}/{{ commit.branch }}/{{ commit.sha }}">
|
||||
<span>{{ commit.sha | shortHash }}</span>
|
||||
</a>
|
||||
</td>
|
||||
<td>{{ commit.pull_request }}</td>
|
||||
<td>{{ commit.author }}</td>
|
||||
<td><img class="gravatar" ng-src="{{ commit.gravatar | gravatar }}" /></td>
|
||||
<td>{{ commit.message }}</td>
|
||||
<td>{{ commit.started_at | fromNow }}</td>
|
||||
<td>{{ commit.finished_at | fromNow }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
19
server/app/views/login.html
Normal file
19
server/app/views/login.html
Normal file
|
@ -0,0 +1,19 @@
|
|||
<div ng-include src=" '/views/header.html' "></div>
|
||||
|
||||
<h1>Login</h1>
|
||||
|
||||
<div class="pure-u-1">
|
||||
<a href="/login/github.com" target="_self">GitHub</a>
|
||||
</div>
|
||||
<div class="pure-u-1">
|
||||
<a href="/login/enterprise.github.com" target="_self">GitHub Enterprise</a>
|
||||
</div>
|
||||
<div class="pure-u-1">
|
||||
<a href="/login/bitbucket.org" target="_self">Bitbucket</a>
|
||||
</div>
|
||||
<div class="pure-u-1">
|
||||
<a href="/login/stash.atlassian.com" target="_self">Stash</a>
|
||||
</div>
|
||||
<div class="pure-u-1">
|
||||
<a href="/login/gitlab.com" target="_self">GitLab</a>
|
||||
</div>
|
71
server/app/views/repo.html
Normal file
71
server/app/views/repo.html
Normal file
|
@ -0,0 +1,71 @@
|
|||
<div ng-include src=" '/views/header.html' "></div>
|
||||
|
||||
|
||||
<h1>{{ repo.name }}</h1>
|
||||
|
||||
|
||||
<div class="row" ng-if="repo.active == false">
|
||||
This repo is not active
|
||||
<div>URL: {{ repo.url }}</div>
|
||||
<div>Private: {{ repo.private }}</div>
|
||||
<div>remote: {{ repo.remote }}</div>
|
||||
<div>owner: {{ repo.owner }}</div>
|
||||
<div>name: {{ repo.name }}</div>
|
||||
|
||||
<div>
|
||||
<button ng-click="activate()">Activate</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-if="repo.active == true">
|
||||
<div class="col-xs-6 col-sm-3">
|
||||
<a href="/{{ repo.remote }}/{{ repo.owner}}/{{ repo.name }}/settings">Settings</a><br/>
|
||||
Recently Updated Branches
|
||||
<ul ng-repeat="commit in branches">
|
||||
<li>
|
||||
<a href="/{{ $parent.repo | fullPath }}/{{ commit.branch }}">
|
||||
<span>{{ commit.branch }}</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- primary column -->
|
||||
<div class="col-xs-12 col-sm-9">
|
||||
<table class="table table-bordered">
|
||||
<tr>
|
||||
<th>Status</th>
|
||||
<th>Branch</th>
|
||||
<th>Sha</th>
|
||||
<th>PR</th>
|
||||
<th colspan=2>Author</th>
|
||||
<th>Message</th>
|
||||
<th>Started</th>
|
||||
<th>Finished</th>
|
||||
</tr>
|
||||
<tr ng-repeat="commit in commits">
|
||||
<td>
|
||||
<a href="/{{ $parent.repo | fullPath }}/{{ commit.branch }}/{{ commit.sha }}">
|
||||
<span>{{ commit.status }}</span>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="/{{ $parent.repo | fullPath }}/{{ commit.branch }}">
|
||||
<span>{{ commit.branch }}</span>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="/{{ $parent.repo | fullPath }}/{{ commit.branch }}/{{ commit.sha }}">
|
||||
<span>{{ commit.sha | shortHash }}</span>
|
||||
</a>
|
||||
</td>
|
||||
<td>{{ commit.pull_request }}</td>
|
||||
<td>{{ commit.author }}</td>
|
||||
<td><img class="gravatar" ng-src="https://secure.gravatar.com/avatar/{{ commit.gravatar }}?s=32&d=mm" /></td>
|
||||
<td>{{ commit.message }}</td>
|
||||
<td>{{ commit.started_at | fromNow }}</td>
|
||||
<td>{{ commit.finished_at | fromNow }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
71
server/app/views/repo_conf.html
Normal file
71
server/app/views/repo_conf.html
Normal file
|
@ -0,0 +1,71 @@
|
|||
<div ng-include src=" '/views/header.html' "></div>
|
||||
|
||||
|
||||
<h1>{{ repo.name }}</h1>
|
||||
<h2>Config</h2>
|
||||
|
||||
<a href="/{{ repo.remote }}/{{ repo.owner}}/{{ repo.name }}">Back</a>
|
||||
|
||||
<div class="row">
|
||||
|
||||
<!-- summary column -->
|
||||
<div class="col-xs-6 col-sm-3">
|
||||
<!-- badge -->
|
||||
<a href="/{{ repo.remote }}/{{ repo.owner}}/{{ repo.name }}">
|
||||
<img ng-src="/v1/badge/{{ repo.remote }}/{{ repo.owner}}/{{ repo.name }}/status.svg" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- primary column -->
|
||||
<div class="col-xs-12 col-sm-9">
|
||||
|
||||
<div>
|
||||
<label>Post Commit Hook</label>
|
||||
<input type="checkbox" ng-model="repoTemp.post_commits" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label>Pull Reqest Hook</label>
|
||||
<input type="checkbox" ng-model="repoTemp.pull_requests" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label>Private Variables</label>
|
||||
<div>--</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label>Privileged</label>
|
||||
<input type="checkbox" ng-model="repoTemp.privileged" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label>Timeout</label>
|
||||
<input type="number" ng-model="repoTemp.timeout" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label>URL</label>
|
||||
<span>{{ repo.url }}</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label>Created</label>
|
||||
<span>{{ repo.created_at | fromNow }}</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label>Updated</label>
|
||||
<span>{{ repo.updated_at | fromNow }}</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{{failure}}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button ng-click="save()">Save</button>
|
||||
<button ng-click="cancel()">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
57
server/app/views/setup.html
Normal file
57
server/app/views/setup.html
Normal file
|
@ -0,0 +1,57 @@
|
|||
<h1>First Time Setup</h1>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xs-6 col-sm-3">
|
||||
<table border="1">
|
||||
<tr>
|
||||
<td><a href="/setup/github.com">GitHub</a></td>
|
||||
<td><a href="/setup/enterprise.github.com">GitHub Enterprise</a></td>
|
||||
<td><a href="/setup/gitlab.com">Gitlab</a></td>
|
||||
<td><a href="/setup/bitbucket.org">Bitbucket</a></td>
|
||||
<td><a href="/setup/stash.atlassian.com">Stash</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- primary column -->
|
||||
<div class="col-xs-12 col-sm-9">
|
||||
|
||||
<div>
|
||||
<label>Registration</label>
|
||||
<input type="radio" ng-model="remote.register" ng-value="true" />
|
||||
<input type="radio" ng-model="remote.register" ng-value="false" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label>URL</label>
|
||||
<div ng-switch="remote.type">
|
||||
<input ng-switch-when="github.com" ng-model="remote.url" type="text" readonly />
|
||||
<input ng-switch-when="bitbucket.org" ng-model="remote.url" type="text" readonly />
|
||||
<input ng-switch-default ng-model="remote.url" type="text" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-if="remote.type != 'gitlab.com'">
|
||||
<label>API</label>
|
||||
<div ng-switch="remote.type">
|
||||
<input ng-switch-when="github.com" ng-model="remote.api" type="text" readonly />
|
||||
<input ng-switch-when="bitbucket.org" ng-model="remote.api" type="text" readonly />
|
||||
<input ng-switch-default ng-model="remote.api" type="text" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-if="remote.type != 'gitlab.com'">
|
||||
<label>Client</label>
|
||||
<input type="text" ng-model="remote.client" />
|
||||
</div>
|
||||
|
||||
<div ng-if="remote.type != 'gitlab.com'">
|
||||
<label>Secret</label>
|
||||
<input type="text" ng-model="remote.secret" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button ng-click="save()">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
115
server/app/views/sys_config.html
Normal file
115
server/app/views/sys_config.html
Normal file
|
@ -0,0 +1,115 @@
|
|||
<div ng-include src=" '/views/header.html' "></div>
|
||||
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xs-6 col-sm-3">
|
||||
<ul>
|
||||
<li><a href="/admin/settings">Config</a></li>
|
||||
<li><a href="/admin/users">Users</a></li>
|
||||
<li><a href="/admin/workers">Workers</a></li>
|
||||
<li><a href="/admin/queue">Queue</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- primary column -->
|
||||
<div class="col-xs-12 col-sm-9">
|
||||
<table class="table table-bordered">
|
||||
<tr>
|
||||
<th>Host</th>
|
||||
<td>{{ config.host }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Scheme</th>
|
||||
<td>{{ config.scheme }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Registration</th>
|
||||
<td>{{ config.registration }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<h2>GitHub</h2>
|
||||
<table class="table table-bordered">
|
||||
<tr>
|
||||
<th>Enabled</th>
|
||||
<td>{{ config.github.enabled }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>URL</th>
|
||||
<td>{{ config.github.url }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>API</th>
|
||||
<td>{{ config.github.api }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Client</th>
|
||||
<td>{{ config.github.client }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Secret</th>
|
||||
<td>{{ config.github.secret }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<h2>GitHub Enterprise</h2>
|
||||
<table class="table table-bordered">
|
||||
<tr>
|
||||
<th>Enabled</th>
|
||||
<td>{{ config.githubEnterprise.enabled }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>URL</th>
|
||||
<td>{{ config.githubEnterprise.url }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>API</th>
|
||||
<td>{{ config.githubEnterprise.api }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Client</th>
|
||||
<td>{{ config.githubEnterprise.client }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Secret</th>
|
||||
<td>{{ config.githubEnterprise.secret }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<h2>Bitbucket</h2>
|
||||
<table class="table table-bordered">
|
||||
<tr>
|
||||
<th>Enabled</th>
|
||||
<td>{{ config.bitbucket.enabled }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>URL</th>
|
||||
<td>{{ config.bitbucket.url }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>API</th>
|
||||
<td>{{ config.bitbucket.api }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Client</th>
|
||||
<td>{{ config.bitbucket.client }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Secret</th>
|
||||
<td>{{ config.bitbucket.secret }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<h2>Gitlab</h2>
|
||||
<table class="table table-bordered">
|
||||
<tr>
|
||||
<th>Enabled</th>
|
||||
<td>{{ config.bitbucket.enabled }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>URL</th>
|
||||
<td>{{ config.bitbucket.url }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
34
server/app/views/sys_users.html
Normal file
34
server/app/views/sys_users.html
Normal file
|
@ -0,0 +1,34 @@
|
|||
<div ng-include src=" '/views/header.html' "></div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xs-6 col-sm-3">
|
||||
<ul>
|
||||
<li><a href="/admin/settings">Config</a></li>
|
||||
<li><a href="/admin/users">Users</a></li>
|
||||
<li><a href="/admin/workers">Workers</a></li>
|
||||
<li><a href="/admin/queue">Queue</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- primary column -->
|
||||
<div class="col-xs-12 col-sm-9">
|
||||
<table class="table table-bordered" border="1">
|
||||
<tr>
|
||||
<th>Remote</th>
|
||||
<th>Login</th>
|
||||
<th>Gravatar</th>
|
||||
<th>Email</th>
|
||||
<th>Created</th>
|
||||
<th>Updated</th>
|
||||
</tr>
|
||||
<tr ng-repeat="user in users">
|
||||
<td>{{ user.remote }}</td>
|
||||
<td>{{ user.login }}</td>
|
||||
<td><img class="gravatar" ng-src="{{ user.gravatar | gravatar }}" /></td>
|
||||
<td>{{ user.email }}</td>
|
||||
<td>{{ user.created_at | toDate}}</td>
|
||||
<td>{{ user.updated_at | toDate}}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
|
@ -1,157 +0,0 @@
|
|||
package channel
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"code.google.com/p/go.net/websocket"
|
||||
"github.com/dchest/authcookie"
|
||||
)
|
||||
|
||||
// secret key used to generate tokens
|
||||
var secret = make([]byte, 32)
|
||||
|
||||
func init() {
|
||||
// generate the secret key by reading
|
||||
// from crypto/random
|
||||
if _, err := io.ReadFull(rand.Reader, secret); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Create will generate a token and create a new
|
||||
// channel over which messages will be sent.
|
||||
func Create(name string) string {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
|
||||
if _, ok := hubs[name]; !ok {
|
||||
hub := newHub(false, true)
|
||||
hubs[name] = hub
|
||||
go hub.run()
|
||||
}
|
||||
return authcookie.NewSinceNow(name, 24*time.Hour, secret)
|
||||
}
|
||||
|
||||
// CreateStream will generate a token and create a new
|
||||
// channel over which messages streams (ie build output)
|
||||
// are sent.
|
||||
func CreateStream(name string) string {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
|
||||
if _, ok := hubs[name]; !ok {
|
||||
hub := newHub(true, false)
|
||||
hubs[name] = hub
|
||||
go hub.run()
|
||||
}
|
||||
return authcookie.NewSinceNow(name, 24*time.Hour, secret)
|
||||
}
|
||||
|
||||
// Token will generate a token, but will not create
|
||||
// a new channel.
|
||||
func Token(name string) string {
|
||||
return authcookie.NewSinceNow(name, 24*time.Hour, secret)
|
||||
}
|
||||
|
||||
// Send sends a message on the named channel.
|
||||
func Send(name string, message string) error {
|
||||
return SendBytes(name, []byte(message))
|
||||
}
|
||||
|
||||
// SendJSON sends a JSON-encoded value on
|
||||
// the named channel.
|
||||
func SendJSON(name string, value interface{}) error {
|
||||
m, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return SendBytes(name, m)
|
||||
}
|
||||
|
||||
// SendBytes send a message in byte format on
|
||||
// the named channel.
|
||||
func SendBytes(name string, value []byte) error {
|
||||
// get the hub for the specified channel name
|
||||
mu.RLock()
|
||||
hub, ok := hubs[name]
|
||||
mu.RUnlock()
|
||||
|
||||
if !ok {
|
||||
return fmt.Errorf("channel does not exist")
|
||||
}
|
||||
|
||||
go hub.Write(value)
|
||||
return nil
|
||||
}
|
||||
|
||||
func Read(ws *websocket.Conn) {
|
||||
|
||||
// get the name from the request
|
||||
hash := ws.Request().FormValue("token")
|
||||
|
||||
// get the hash of the token
|
||||
name := authcookie.Login(hash, secret)
|
||||
|
||||
// get the hub for the specified channel name
|
||||
mu.RLock()
|
||||
hub, ok := hubs[name]
|
||||
mu.RUnlock()
|
||||
|
||||
// if hub not found, exit
|
||||
if !ok {
|
||||
ws.Close()
|
||||
return
|
||||
}
|
||||
|
||||
// internal representation of a connection
|
||||
// maximum queue of 100000 messages
|
||||
conn := &connection{
|
||||
send: make(chan string, 100000),
|
||||
ws: ws,
|
||||
}
|
||||
|
||||
// register the connection with the hub
|
||||
hub.register <- conn
|
||||
|
||||
defer func() {
|
||||
go func() {
|
||||
hub.unregister <- conn
|
||||
}()
|
||||
closed := <-hub.closed
|
||||
|
||||
// this will remove the hub when the connection is
|
||||
// closed if the
|
||||
if hub.autoClose && closed {
|
||||
mu.Lock()
|
||||
delete(hubs, name)
|
||||
mu.Unlock()
|
||||
}
|
||||
}()
|
||||
|
||||
go conn.writer()
|
||||
conn.reader()
|
||||
}
|
||||
|
||||
func Close(name string) {
|
||||
// get the hub for the specified channel name
|
||||
mu.RLock()
|
||||
hub, ok := hubs[name]
|
||||
mu.RUnlock()
|
||||
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// close hub connections
|
||||
hub.Close()
|
||||
|
||||
// remove the hub
|
||||
mu.Lock()
|
||||
delete(hubs, name)
|
||||
mu.Unlock()
|
||||
}
|
|
@ -1,36 +0,0 @@
|
|||
package channel
|
||||
|
||||
import (
|
||||
"code.google.com/p/go.net/websocket"
|
||||
)
|
||||
|
||||
type connection struct {
|
||||
// The websocket connection.
|
||||
ws *websocket.Conn
|
||||
|
||||
// Buffered channel of outbound messages.
|
||||
send chan string
|
||||
}
|
||||
|
||||
func (c *connection) reader() {
|
||||
for {
|
||||
var message string
|
||||
err := websocket.Message.Receive(c.ws, &message)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
c.ws.Close()
|
||||
}
|
||||
|
||||
func (c *connection) writer() {
|
||||
for message := range c.send {
|
||||
err := websocket.Message.Send(c.ws, message)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
c.ws.Close()
|
||||
}
|
|
@ -1,133 +0,0 @@
|
|||
package channel
|
||||
|
||||
import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
// mutex to lock access to the
|
||||
// internal map of hubs.
|
||||
var mu sync.RWMutex
|
||||
|
||||
// a map of hubs. each hub represents a different
|
||||
// channel that a set of users can listen on. For
|
||||
// example, we may have a hub to stream build output
|
||||
// for github.com/foo/bar or a channel to post
|
||||
// updates for user octocat.
|
||||
var hubs = map[string]*hub{}
|
||||
|
||||
type hub struct {
|
||||
// Registered connections
|
||||
connections map[*connection]bool
|
||||
|
||||
// Inbound messages from the connections.
|
||||
broadcast chan string
|
||||
|
||||
// Register requests from the connections.
|
||||
register chan *connection
|
||||
|
||||
// Unregister requests from connections.
|
||||
unregister chan *connection
|
||||
|
||||
// Buffer of sent data. This is used mostly
|
||||
// for build output. A client may connect after
|
||||
// the build has already started, in which case
|
||||
// we need to stream them the build history.
|
||||
history []string
|
||||
|
||||
// Send a "shutdown" signal
|
||||
close chan bool
|
||||
|
||||
// Hub responds on this channel letting you know
|
||||
// if it's active
|
||||
closed chan bool
|
||||
|
||||
// Auto shutdown when last connection removed
|
||||
autoClose bool
|
||||
|
||||
// Send history
|
||||
sendHistory bool
|
||||
}
|
||||
|
||||
func newHub(sendHistory, autoClose bool) *hub {
|
||||
h := hub{
|
||||
broadcast: make(chan string),
|
||||
register: make(chan *connection),
|
||||
unregister: make(chan *connection),
|
||||
connections: make(map[*connection]bool),
|
||||
history: make([]string, 0), // This should be pre-allocated, but it's not
|
||||
close: make(chan bool),
|
||||
autoClose: autoClose,
|
||||
closed: make(chan bool),
|
||||
sendHistory: sendHistory,
|
||||
}
|
||||
|
||||
return &h
|
||||
}
|
||||
|
||||
func sendHistory(c *connection, history []string) {
|
||||
if len(history) > 0 {
|
||||
for i := range history {
|
||||
c.send <- history[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (h *hub) run() {
|
||||
// make sure we don't bring down the application
|
||||
// if somehow we encounter a nil pointer or some
|
||||
// other unexpected behavior.
|
||||
defer func() {
|
||||
recover()
|
||||
}()
|
||||
|
||||
for {
|
||||
select {
|
||||
case c := <-h.register:
|
||||
h.connections[c] = true
|
||||
if len(h.history) > 0 {
|
||||
b := make([]string, len(h.history))
|
||||
copy(b, h.history)
|
||||
go sendHistory(c, b)
|
||||
}
|
||||
case c := <-h.unregister:
|
||||
delete(h.connections, c)
|
||||
close(c.send)
|
||||
shutdown := h.autoClose && (len(h.connections) == 0)
|
||||
if shutdown {
|
||||
h.closed <- shutdown
|
||||
return
|
||||
}
|
||||
h.closed <- shutdown
|
||||
case m := <-h.broadcast:
|
||||
if h.sendHistory {
|
||||
h.history = append(h.history, m)
|
||||
}
|
||||
for c := range h.connections {
|
||||
select {
|
||||
case c.send <- m:
|
||||
// do nothing
|
||||
default:
|
||||
delete(h.connections, c)
|
||||
go c.ws.Close()
|
||||
}
|
||||
}
|
||||
case <-h.close:
|
||||
for c := range h.connections {
|
||||
delete(h.connections, c)
|
||||
close(c.send)
|
||||
}
|
||||
h.closed <- true
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func (h *hub) Close() {
|
||||
h.close <- true
|
||||
}
|
||||
|
||||
func (h *hub) Write(p []byte) (n int, err error) {
|
||||
h.broadcast <- string(p)
|
||||
return len(p), nil
|
||||
}
|
89
server/database/remote.go
Normal file
89
server/database/remote.go
Normal file
|
@ -0,0 +1,89 @@
|
|||
package database
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
"github.com/drone/drone/shared/model"
|
||||
"github.com/russross/meddler"
|
||||
)
|
||||
|
||||
type RemoteManager interface {
|
||||
// Find finds the Remote by ID.
|
||||
Find(id int64) (*model.Remote, error)
|
||||
|
||||
// FindHost finds the Remote by hostname.
|
||||
FindHost(name string) (*model.Remote, error)
|
||||
|
||||
// List finds all registered Remotes of the system.
|
||||
List() ([]*model.Remote, error)
|
||||
|
||||
// Insert persists the Remotes to the datastore.
|
||||
Insert(server *model.Remote) error
|
||||
|
||||
// Update persists changes to the Remotes to the datastore.
|
||||
Update(server *model.Remote) error
|
||||
|
||||
// Delete removes the Remotes from the datastore.
|
||||
Delete(server *model.Remote) error
|
||||
}
|
||||
|
||||
// remoteManager manages a list of remotes in a SQL database.
|
||||
type remoteManager struct {
|
||||
*sql.DB
|
||||
}
|
||||
|
||||
// SQL query to retrieve a Remote by remote login.
|
||||
const findRemoteQuery = `
|
||||
SELECT *
|
||||
FROM remotes
|
||||
WHERE remote_host=?
|
||||
LIMIT 1
|
||||
`
|
||||
|
||||
// SQL query to retrieve a list of all Remotes.
|
||||
const listRemoteQuery = `
|
||||
SELECT *
|
||||
FROM remotes
|
||||
`
|
||||
|
||||
// SQL statement to delete a Remote by ID.
|
||||
const deleteRemoteStmt = `
|
||||
DELETE FROM remotes WHERE remote_id=?
|
||||
`
|
||||
|
||||
// NewRemoteManager initiales a new RemoteManager intended to
|
||||
// manage and persist servers.
|
||||
func NewRemoteManager(db *sql.DB) RemoteManager {
|
||||
return &remoteManager{db}
|
||||
}
|
||||
|
||||
func (db *remoteManager) Find(id int64) (*model.Remote, error) {
|
||||
dst := model.Remote{}
|
||||
err := meddler.Load(db, "remotes", &dst, id)
|
||||
return &dst, err
|
||||
}
|
||||
|
||||
func (db *remoteManager) FindHost(host string) (*model.Remote, error) {
|
||||
dst := model.Remote{}
|
||||
err := meddler.QueryRow(db, &dst, findRemoteQuery, host)
|
||||
return &dst, err
|
||||
}
|
||||
|
||||
func (db *remoteManager) List() ([]*model.Remote, error) {
|
||||
var dst []*model.Remote
|
||||
err := meddler.QueryAll(db, &dst, listRemoteQuery)
|
||||
return dst, err
|
||||
}
|
||||
|
||||
func (db *remoteManager) Insert(remote *model.Remote) error {
|
||||
return meddler.Insert(db, "remotes", remote)
|
||||
}
|
||||
|
||||
func (db *remoteManager) Update(remote *model.Remote) error {
|
||||
return meddler.Update(db, "remotes", remote)
|
||||
}
|
||||
|
||||
func (db *remoteManager) Delete(remote *model.Remote) error {
|
||||
_, err := db.Exec(deleteRemoteStmt, remote.ID)
|
||||
return err
|
||||
}
|
196
server/database/remote_test.go
Normal file
196
server/database/remote_test.go
Normal file
|
@ -0,0 +1,196 @@
|
|||
package database
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"testing"
|
||||
|
||||
"github.com/drone/drone/shared/model"
|
||||
)
|
||||
|
||||
func TestRemoteFind(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
remotes := NewRemoteManager(db)
|
||||
remote, err := remotes.Find(1)
|
||||
if err != nil {
|
||||
t.Errorf("Want Remote from ID, got %s", err)
|
||||
}
|
||||
|
||||
testRemote(t, remote)
|
||||
}
|
||||
|
||||
func TestRemoteFindHost(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
remotes := NewRemoteManager(db)
|
||||
remote, err := remotes.FindHost("github.drone.io")
|
||||
if err != nil {
|
||||
t.Errorf("Want Remote from Host, got %s", err)
|
||||
}
|
||||
|
||||
testRemote(t, remote)
|
||||
}
|
||||
|
||||
func TestRemoteList(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
remotes := NewRemoteManager(db)
|
||||
all, err := remotes.List()
|
||||
if err != nil {
|
||||
t.Errorf("Want Remotes, got %s", err)
|
||||
}
|
||||
|
||||
var got, want = len(all), 2
|
||||
if got != want {
|
||||
t.Errorf("Want %v remotes, got %v", want, got)
|
||||
}
|
||||
|
||||
testRemote(t, all[0])
|
||||
}
|
||||
|
||||
func TestRemoteInsert(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
remote := &model.Remote{0, "bitbucket.org", "bitbucket.org", "https://bitbucket.org", "https://bitbucket.org", "abc", "123", false}
|
||||
remotes := NewRemoteManager(db)
|
||||
if err := remotes.Insert(remote); err != nil {
|
||||
t.Errorf("Want Remote created, got %s", err)
|
||||
}
|
||||
|
||||
var got, want = remote.ID, int64(3)
|
||||
if want != got {
|
||||
t.Errorf("Want Remote ID %v, got %v", want, got)
|
||||
}
|
||||
|
||||
// verify unique remote name constraint
|
||||
var err = remotes.Insert(&model.Remote{Type: "bitbucket.org", Host: "butbucket.com"})
|
||||
if err == nil {
|
||||
t.Error("Want Type unique constraint violated")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestRemoteUpdate(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
remotes := NewRemoteManager(db)
|
||||
remote, err := remotes.Find(1)
|
||||
if err != nil {
|
||||
t.Errorf("Want Remote from ID, got %s", err)
|
||||
}
|
||||
|
||||
// update the remote's address
|
||||
remote.Client = "abc"
|
||||
remote.Secret = "123"
|
||||
remote.Host = "git.drone.io"
|
||||
remote.URL = "https://git.drone.io"
|
||||
remote.API = "https://git.drone.io/v3/api"
|
||||
if err := remotes.Update(remote); err != nil {
|
||||
t.Errorf("Want Remote updated, got %s", err)
|
||||
}
|
||||
|
||||
updated, _ := remotes.Find(1)
|
||||
var got, want = remote.Host, remote.Host
|
||||
if got != want {
|
||||
t.Errorf("Want updated Host %s, got %s", want, got)
|
||||
}
|
||||
|
||||
got, want = updated.Client, remote.Client
|
||||
if got != want {
|
||||
t.Errorf("Want updated Client %s, got %s", want, got)
|
||||
}
|
||||
|
||||
got, want = updated.Secret, remote.Secret
|
||||
if got != want {
|
||||
t.Errorf("Want updated Secret %s, got %s", want, got)
|
||||
}
|
||||
|
||||
got, want = updated.Host, remote.Host
|
||||
if got != want {
|
||||
t.Errorf("Want updated Host %s, got %s", want, got)
|
||||
}
|
||||
|
||||
got, want = updated.Host, remote.Host
|
||||
if got != want {
|
||||
t.Errorf("Want updated Host %s, got %s", want, got)
|
||||
}
|
||||
|
||||
got, want = updated.URL, remote.URL
|
||||
if got != want {
|
||||
t.Errorf("Want updated URL %s, got %s", want, got)
|
||||
}
|
||||
|
||||
got, want = updated.API, remote.API
|
||||
if got != want {
|
||||
t.Errorf("Want updated API %s, got %s", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoteDelete(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
remotes := NewRemoteManager(db)
|
||||
remote, err := remotes.Find(1)
|
||||
if err != nil {
|
||||
t.Errorf("Want Remote from ID, got %s", err)
|
||||
}
|
||||
|
||||
// delete the remote
|
||||
if err := remotes.Delete(remote); err != nil {
|
||||
t.Errorf("Want Remote deleted, got %s", err)
|
||||
}
|
||||
|
||||
// check to see if the deleted remote is actually gone
|
||||
if _, err := remotes.Find(1); err != sql.ErrNoRows {
|
||||
t.Errorf("Want ErrNoRows, got %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func testRemote(t *testing.T, remote *model.Remote) {
|
||||
|
||||
var got, want = remote.Host, "github.drone.io"
|
||||
if got != want {
|
||||
t.Errorf("Want Host %v, got %v", want, got)
|
||||
}
|
||||
|
||||
got, want = remote.Type, "enterprise.github.com"
|
||||
if got != want {
|
||||
t.Errorf("Want Type %v, got %v", want, got)
|
||||
}
|
||||
|
||||
got, want = remote.URL, "https://github.drone.io"
|
||||
if got != want {
|
||||
t.Errorf("Want URL %v, got %v", want, got)
|
||||
}
|
||||
|
||||
got, want = remote.API, "https://github.drone.io/v3/api"
|
||||
if got != want {
|
||||
t.Errorf("Want API %v, got %v", want, got)
|
||||
}
|
||||
|
||||
got, want = remote.Client, "f0b461ca586c27872b43a0685cbc2847"
|
||||
if got != want {
|
||||
t.Errorf("Want Access Token %v, got %v", want, got)
|
||||
}
|
||||
|
||||
got, want = remote.Secret, "976f22a5eef7caacb7e678d6c52f49b1"
|
||||
if got != want {
|
||||
t.Errorf("Want Token Secret %v, got %v", want, got)
|
||||
}
|
||||
|
||||
var gotBool, wantBool = remote.Open, true
|
||||
if gotBool != wantBool {
|
||||
t.Errorf("Want Open %v, got %v", wantBool, gotBool)
|
||||
}
|
||||
|
||||
var gotInt64, wantInt64 = remote.ID, int64(1)
|
||||
if gotInt64 != wantInt64 {
|
||||
t.Errorf("Want ID %v, got %v", wantInt64, gotInt64)
|
||||
}
|
||||
}
|
|
@ -85,6 +85,35 @@ var stmts = []string{`
|
|||
,commit_id INTEGER
|
||||
,output_raw BLOB
|
||||
,UNIQUE(commit_id)
|
||||
);`, `
|
||||
CREATE TABLE IF NOT EXISTS remotes (
|
||||
remote_id INTEGER PRIMARY KEY AUTOINCREMENT
|
||||
,remote_type VARCHAR(255)
|
||||
,remote_host VARCHAR(255)
|
||||
,remote_url VARCHAR(255)
|
||||
,remote_api VARCHAR(255)
|
||||
,remote_client VARCHAR(255)
|
||||
,remote_secret VARCHAR(255)
|
||||
,remote_open BOOLEAN
|
||||
,UNIQUE(remote_host)
|
||||
,UNIQUE(remote_type)
|
||||
);`, `
|
||||
CREATE TABLE IF NOT EXISTS servers (
|
||||
server_id INTEGER PRIMARY KEY AUTOINCREMENT
|
||||
,server_name VARCHAR(255)
|
||||
,server_host VARCHAR(255)
|
||||
,server_user VARCHAR(255)
|
||||
,server_pass VARCHAR(255)
|
||||
,server_cert VARCHAR(4000)
|
||||
,UNIQUE(server_name)
|
||||
);`, `
|
||||
CREATE TABLE IF NOT EXISTS smtp (
|
||||
smtp_id INTEGER PRIMARY KEY AUTOINCREMENT
|
||||
,smtp_from VARCHAR(255)
|
||||
,smtp_host VARCHAR(255)
|
||||
,smtp_port VARCHAR(255)
|
||||
,smtp_user VARCHAR(255)
|
||||
,smtp_pass VARCHAR(255)
|
||||
);`,
|
||||
}
|
||||
|
||||
|
|
111
server/database/server.go
Normal file
111
server/database/server.go
Normal file
|
@ -0,0 +1,111 @@
|
|||
package database
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
"github.com/drone/drone/shared/model"
|
||||
"github.com/russross/meddler"
|
||||
)
|
||||
|
||||
type ServerManager interface {
|
||||
// Find finds the Server by ID.
|
||||
Find(id int64) (*model.Server, error)
|
||||
|
||||
// FindName finds the Server by name.
|
||||
FindName(name string) (*model.Server, error)
|
||||
|
||||
// FindName finds the Server by name.
|
||||
FindSMTP() (*model.SMTPServer, error)
|
||||
|
||||
// List finds all registered Servers of the system.
|
||||
List() ([]*model.Server, error)
|
||||
|
||||
// Insert persists the Server to the datastore.
|
||||
Insert(server *model.Server) error
|
||||
|
||||
// Update persists changes to the Server to the datastore.
|
||||
Update(server *model.Server) error
|
||||
|
||||
// UpdateSMTP persists changes to the SMTP Server to the datastore.
|
||||
UpdateSMTP(server *model.SMTPServer) error
|
||||
|
||||
// Delete removes the Server from the datastore.
|
||||
Delete(server *model.Server) error
|
||||
}
|
||||
|
||||
// serverManager manages a list of users in a SQL database.
|
||||
type serverManager struct {
|
||||
*sql.DB
|
||||
}
|
||||
|
||||
// SQL query to retrieve a Server by remote login.
|
||||
const findServerQuery = `
|
||||
SELECT *
|
||||
FROM servers
|
||||
WHERE server_name=?
|
||||
LIMIT 1
|
||||
`
|
||||
|
||||
// SQL query to retrieve a list of all Servers.
|
||||
const listServerQuery = `
|
||||
SELECT *
|
||||
FROM servers
|
||||
`
|
||||
|
||||
// SQL statement to delete a Server by ID.
|
||||
const deleteServerStmt = `
|
||||
DELETE FROM servers WHERE server_id=?
|
||||
`
|
||||
|
||||
// NewServerManager initiales a new ServerManager intended to
|
||||
// manage and persist servers.
|
||||
func NewServerManager(db *sql.DB) ServerManager {
|
||||
return &serverManager{db}
|
||||
}
|
||||
|
||||
func (db *serverManager) Find(id int64) (*model.Server, error) {
|
||||
dst := model.Server{}
|
||||
err := meddler.Load(db, "servers", &dst, id)
|
||||
return &dst, err
|
||||
}
|
||||
|
||||
func (db *serverManager) FindName(name string) (*model.Server, error) {
|
||||
dst := model.Server{}
|
||||
err := meddler.QueryRow(db, &dst, findServerQuery, name)
|
||||
return &dst, err
|
||||
}
|
||||
|
||||
func (db *serverManager) FindSMTP() (*model.SMTPServer, error) {
|
||||
dst := model.SMTPServer{}
|
||||
err := meddler.Load(db, "smtp", &dst, 1)
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
return &dst, err
|
||||
}
|
||||
return &dst, nil
|
||||
}
|
||||
|
||||
func (db *serverManager) List() ([]*model.Server, error) {
|
||||
var dst []*model.Server
|
||||
err := meddler.QueryAll(db, &dst, listServerQuery)
|
||||
return dst, err
|
||||
}
|
||||
|
||||
func (db *serverManager) Insert(server *model.Server) error {
|
||||
return meddler.Insert(db, "servers", server)
|
||||
}
|
||||
|
||||
func (db *serverManager) Update(server *model.Server) error {
|
||||
return meddler.Update(db, "servers", server)
|
||||
}
|
||||
|
||||
func (db *serverManager) UpdateSMTP(server *model.SMTPServer) error {
|
||||
server.ID = 0
|
||||
meddler.Insert(db, "smtp", server)
|
||||
server.ID = 1
|
||||
return meddler.Update(db, "smtp", server)
|
||||
}
|
||||
|
||||
func (db *serverManager) Delete(server *model.Server) error {
|
||||
_, err := db.Exec(deleteServerStmt, server.ID)
|
||||
return err
|
||||
}
|
201
server/database/server_test.go
Normal file
201
server/database/server_test.go
Normal file
|
@ -0,0 +1,201 @@
|
|||
package database
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"testing"
|
||||
|
||||
"github.com/drone/drone/shared/model"
|
||||
)
|
||||
|
||||
func TestServerFind(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
servers := NewServerManager(db)
|
||||
server, err := servers.Find(1)
|
||||
if err != nil {
|
||||
t.Errorf("Want Server from ID, got %s", err)
|
||||
}
|
||||
|
||||
testServer(t, server)
|
||||
}
|
||||
|
||||
func TestServerFindName(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
servers := NewServerManager(db)
|
||||
server, err := servers.FindName("docker1")
|
||||
if err != nil {
|
||||
t.Errorf("Want Server from Host, got %s", err)
|
||||
}
|
||||
|
||||
testServer(t, server)
|
||||
}
|
||||
|
||||
func TestServerFindSMTP(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
server := model.SMTPServer{
|
||||
From: "foo@bar.com",
|
||||
Host: "127.0.0.1",
|
||||
User: "foo"}
|
||||
|
||||
servers := NewServerManager(db)
|
||||
if err := servers.UpdateSMTP(&server); err != nil {
|
||||
t.Errorf("Want SMTP server inserted, got %s", err)
|
||||
}
|
||||
if inserted, err := servers.FindSMTP(); err != nil {
|
||||
t.Errorf("Want SMTP server, got %s", err)
|
||||
} else if inserted.ID == 0 {
|
||||
t.Errorf("Want SMTP server inserted")
|
||||
}
|
||||
|
||||
server.Host = "0.0.0.0"
|
||||
server.User = "bar"
|
||||
err := servers.UpdateSMTP(&server)
|
||||
if err := servers.UpdateSMTP(&server); err != nil {
|
||||
t.Errorf("Want SMTP server updated, got %s", err)
|
||||
}
|
||||
|
||||
updated, err := servers.FindSMTP()
|
||||
if err != nil {
|
||||
t.Errorf("Want SMTP server, got %s", err)
|
||||
}
|
||||
|
||||
var want, got = server.Host, updated.Host
|
||||
if want != got {
|
||||
t.Errorf("Want SMTP Host %v, got %v", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServerList(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
servers := NewServerManager(db)
|
||||
all, err := servers.List()
|
||||
if err != nil {
|
||||
t.Errorf("Want Servers, got %s", err)
|
||||
}
|
||||
|
||||
var got, want = len(all), 2
|
||||
if got != want {
|
||||
t.Errorf("Want %v Servers, got %v", want, got)
|
||||
}
|
||||
|
||||
testServer(t, all[0])
|
||||
}
|
||||
|
||||
func TestServerInsert(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
server := &model.Server{Host: "tcp://127.0.0.1:4243", Name: "docker3"}
|
||||
servers := NewServerManager(db)
|
||||
if err := servers.Insert(server); err != nil {
|
||||
t.Errorf("Want Server created, got %s", err)
|
||||
}
|
||||
|
||||
var got, want = server.ID, int64(3)
|
||||
if want != got {
|
||||
t.Errorf("Want Server ID %v, got %v", want, got)
|
||||
}
|
||||
|
||||
// verify unique server name constraint
|
||||
var err = servers.Insert(&model.Server{Host: "tcp://127.0.0.1:4243", Name: "docker3"})
|
||||
if err == nil {
|
||||
t.Error("Want Name unique constraint violated")
|
||||
}
|
||||
}
|
||||
|
||||
func TestServerUpdate(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
servers := NewServerManager(db)
|
||||
server, err := servers.Find(1)
|
||||
if err != nil {
|
||||
t.Errorf("Want Server from ID, got %s", err)
|
||||
}
|
||||
|
||||
// update the server's address
|
||||
server.Host = "tcp://1.2.3.4:4243"
|
||||
server.User = "docker"
|
||||
server.Pass = "123456"
|
||||
if err := servers.Update(server); err != nil {
|
||||
t.Errorf("Want Server updated, got %s", err)
|
||||
}
|
||||
|
||||
updated, _ := servers.Find(1)
|
||||
var got, want = server.Host, server.Host
|
||||
if got != want {
|
||||
t.Errorf("Want updated Host %s, got %s", want, got)
|
||||
}
|
||||
|
||||
got, want = updated.User, server.User
|
||||
if got != want {
|
||||
t.Errorf("Want updated User %s, got %s", want, got)
|
||||
}
|
||||
|
||||
got, want = updated.Pass, server.Pass
|
||||
if got != want {
|
||||
t.Errorf("Want updated Pass %s, got %s", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServerDelete(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
servers := NewServerManager(db)
|
||||
server, err := servers.Find(1)
|
||||
if err != nil {
|
||||
t.Errorf("Want Server from ID, got %s", err)
|
||||
}
|
||||
|
||||
// delete the server
|
||||
if err := servers.Delete(server); err != nil {
|
||||
t.Errorf("Want Server deleted, got %s", err)
|
||||
}
|
||||
|
||||
// check to see if the deleted server is actually gone
|
||||
if _, err := servers.Find(1); err != sql.ErrNoRows {
|
||||
t.Errorf("Want ErrNoRows, got %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
// testServer is a helper function that compares the server
|
||||
// to an expected set of fixed field values.
|
||||
func testServer(t *testing.T, server *model.Server) {
|
||||
|
||||
var got, want = server.Host, "tcp://127.0.0.1:4243"
|
||||
if got != want {
|
||||
t.Errorf("Want Host %v, got %v", want, got)
|
||||
}
|
||||
|
||||
got, want = server.Name, "docker1"
|
||||
if got != want {
|
||||
t.Errorf("Want Name %v, got %v", want, got)
|
||||
}
|
||||
|
||||
got, want = server.User, "root"
|
||||
if got != want {
|
||||
t.Errorf("Want User %v, got %v", want, got)
|
||||
}
|
||||
|
||||
got, want = server.Pass, "pa55word"
|
||||
if got != want {
|
||||
t.Errorf("Want Pass %v, got %v", want, got)
|
||||
}
|
||||
|
||||
got, want = server.Cert, "/path/to/cert.key"
|
||||
if got != want {
|
||||
t.Errorf("Want Cert %v, got %v", want, got)
|
||||
}
|
||||
|
||||
var gotInt64, wantInt64 = server.ID, int64(1)
|
||||
if gotInt64 != wantInt64 {
|
||||
t.Errorf("Want ID %v, got %v", wantInt64, gotInt64)
|
||||
}
|
||||
}
|
8
server/database/testdata/testdata.go
vendored
8
server/database/testdata/testdata.go
vendored
|
@ -34,6 +34,14 @@ var stmts = []string{
|
|||
// insert commit console output
|
||||
"insert into output values (1, 1, 'sample console output');",
|
||||
"insert into output values (2, 2, 'sample console output.....');",
|
||||
|
||||
// insert server entries
|
||||
"insert into servers values (1, 'docker1', 'tcp://127.0.0.1:4243', 'root', 'pa55word', '/path/to/cert.key');",
|
||||
"insert into servers values (2, 'docker2', 'tcp://127.0.0.1:4243', 'root', 'pa55word', '/path/to/cert.key');",
|
||||
|
||||
// insert remote entries
|
||||
"insert into remotes values (1, 'enterprise.github.com', 'github.drone.io', 'https://github.drone.io', 'https://github.drone.io/v3/api', 'f0b461ca586c27872b43a0685cbc2847', '976f22a5eef7caacb7e678d6c52f49b1', '1');",
|
||||
"insert into remotes values (2, 'github.com', 'github.com', 'https://github.io', 'https://api.github.com', 'a0b461ca586c27872b43a0685cbc2847', 'a76f22a5eef7caacb7e678d6c52f49b1', '0');",
|
||||
}
|
||||
|
||||
// Load will populate the database with a fixed dataset for
|
||||
|
|
|
@ -29,6 +29,9 @@ type UserManager interface {
|
|||
|
||||
// Delete removes the User from the datastore.
|
||||
Delete(user *model.User) error
|
||||
|
||||
// Exist returns true if Users exist in the system.
|
||||
Exist() bool
|
||||
}
|
||||
|
||||
// userManager manages a list of users in a SQL database.
|
||||
|
@ -65,6 +68,11 @@ const deleteUserStmt = `
|
|||
DELETE FROM users WHERE user_id=?
|
||||
`
|
||||
|
||||
// SQL statement to check if users exist.
|
||||
const confirmUserStmt = `
|
||||
select 0 from users limit 1
|
||||
`
|
||||
|
||||
// NewUserManager initiales a new UserManager intended to
|
||||
// manage and persist commits.
|
||||
func NewUserManager(db *sql.DB) UserManager {
|
||||
|
@ -110,3 +118,10 @@ func (db *userManager) Delete(user *model.User) error {
|
|||
_, err := db.Exec(deleteUserStmt, user.ID)
|
||||
return err
|
||||
}
|
||||
|
||||
func (db *userManager) Exist() bool {
|
||||
row := db.QueryRow(confirmUserStmt)
|
||||
var result int
|
||||
row.Scan(&result)
|
||||
return result == 1
|
||||
}
|
||||
|
|
|
@ -5,8 +5,8 @@ import (
|
|||
"net/http"
|
||||
|
||||
"github.com/drone/drone/server/database"
|
||||
"github.com/drone/drone/server/queue"
|
||||
"github.com/drone/drone/server/session"
|
||||
"github.com/drone/drone/server/worker"
|
||||
"github.com/drone/drone/shared/model"
|
||||
"github.com/gorilla/pat"
|
||||
)
|
||||
|
@ -16,10 +16,10 @@ type CommitHandler struct {
|
|||
repos database.RepoManager
|
||||
commits database.CommitManager
|
||||
sess session.Session
|
||||
queue *queue.Queue
|
||||
queue chan *worker.Request
|
||||
}
|
||||
|
||||
func NewCommitHandler(repos database.RepoManager, commits database.CommitManager, perms database.PermManager, sess session.Session, queue *queue.Queue) *CommitHandler {
|
||||
func NewCommitHandler(repos database.RepoManager, commits database.CommitManager, perms database.PermManager, sess session.Session, queue chan *worker.Request) *CommitHandler {
|
||||
return &CommitHandler{perms, repos, commits, sess, queue}
|
||||
}
|
||||
|
||||
|
@ -158,7 +158,13 @@ func (h *CommitHandler) PostCommit(w http.ResponseWriter, r *http.Request) error
|
|||
}
|
||||
|
||||
// drop the items on the queue
|
||||
h.queue.Add(&queue.BuildTask{Repo: repo, Commit: c})
|
||||
// drop the items on the queue
|
||||
go func() {
|
||||
h.queue <- &worker.Request{
|
||||
Repo: repo,
|
||||
Commit: c,
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ import (
|
|||
"net/http"
|
||||
|
||||
"github.com/drone/drone/server/database"
|
||||
"github.com/drone/drone/server/queue"
|
||||
"github.com/drone/drone/server/worker"
|
||||
"github.com/drone/drone/shared/model"
|
||||
"github.com/gorilla/pat"
|
||||
)
|
||||
|
@ -14,10 +14,10 @@ type HookHandler struct {
|
|||
repos database.RepoManager
|
||||
commits database.CommitManager
|
||||
conf database.ConfigManager
|
||||
queue *queue.Queue
|
||||
queue chan *worker.Request
|
||||
}
|
||||
|
||||
func NewHookHandler(users database.UserManager, repos database.RepoManager, commits database.CommitManager, conf database.ConfigManager, queue *queue.Queue) *HookHandler {
|
||||
func NewHookHandler(users database.UserManager, repos database.RepoManager, commits database.CommitManager, conf database.ConfigManager, queue chan *worker.Request) *HookHandler {
|
||||
return &HookHandler{users, repos, commits, conf, queue}
|
||||
}
|
||||
|
||||
|
@ -92,11 +92,16 @@ func (h *HookHandler) PostHook(w http.ResponseWriter, r *http.Request) error {
|
|||
//fmt.Printf("%s", yml)
|
||||
|
||||
// drop the items on the queue
|
||||
h.queue.Add(&queue.BuildTask{Repo: repo, Commit: &c})
|
||||
go func() {
|
||||
h.queue <- &worker.Request{
|
||||
Repo: repo,
|
||||
Commit: &c,
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *HookHandler) Register(r *pat.Router) {
|
||||
r.Post("/hook/{host}", errorHandler(h.PostHook))
|
||||
r.Put("/hook/{host}", errorHandler(h.PostHook))
|
||||
r.Post("/v1/hook/{host}", errorHandler(h.PostHook))
|
||||
r.Put("/v1/hook/{host}", errorHandler(h.PostHook))
|
||||
}
|
||||
|
|
|
@ -48,8 +48,9 @@ func (h *LoginHandler) GetLogin(w http.ResponseWriter, r *http.Request) error {
|
|||
u, err := h.users.FindLogin(host, login.Login)
|
||||
if err != nil {
|
||||
// if self-registration is disabled we should
|
||||
// return a notAuthorized error
|
||||
if !h.conf.Find().Registration {
|
||||
// return a notAuthorized error. the only exception
|
||||
// is if no users exist yet in the system we'll proceed.
|
||||
if h.conf.Find().Registration == false && h.users.Exist() {
|
||||
return notAuthorized{}
|
||||
}
|
||||
|
||||
|
|
108
server/handler/remote.go
Normal file
108
server/handler/remote.go
Normal file
|
@ -0,0 +1,108 @@
|
|||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/drone/drone/server/database"
|
||||
"github.com/drone/drone/server/session"
|
||||
"github.com/drone/drone/shared/model"
|
||||
"github.com/gorilla/pat"
|
||||
)
|
||||
|
||||
type RemoteHandler struct {
|
||||
users database.UserManager
|
||||
remotes database.RemoteManager
|
||||
sess session.Session
|
||||
}
|
||||
|
||||
func NewRemoteHandler(users database.UserManager, remotes database.RemoteManager, sess session.Session) *RemoteHandler {
|
||||
return &RemoteHandler{users, remotes, sess}
|
||||
}
|
||||
|
||||
// GetRemotes gets all remotes.
|
||||
// GET /api/remotes
|
||||
func (h *RemoteHandler) GetRemotes(w http.ResponseWriter, r *http.Request) error {
|
||||
// get the user form the session
|
||||
user := h.sess.User(r)
|
||||
if user == nil || !user.Admin {
|
||||
return notAuthorized{}
|
||||
}
|
||||
// get all remotes
|
||||
remotes, err := h.remotes.List()
|
||||
if err != nil {
|
||||
return internalServerError{err}
|
||||
}
|
||||
|
||||
return json.NewEncoder(w).Encode(remotes)
|
||||
}
|
||||
|
||||
// PostRemote creates a new remote.
|
||||
// POST /api/remotes
|
||||
func (h *RemoteHandler) PostRemote(w http.ResponseWriter, r *http.Request) error {
|
||||
// get the user form the session
|
||||
user := h.sess.User(r)
|
||||
if user == nil || !user.Admin {
|
||||
// if no users exist, this request is part of
|
||||
// the system installation process and can proceed.
|
||||
// else we should reject.
|
||||
if h.users.Exist() {
|
||||
return notAuthorized{}
|
||||
}
|
||||
}
|
||||
// unmarshal the remote from the payload
|
||||
defer r.Body.Close()
|
||||
in := model.Remote{}
|
||||
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
|
||||
return badRequest{err}
|
||||
}
|
||||
|
||||
uri, err := url.Parse(in.URL)
|
||||
if err != nil {
|
||||
return badRequest{err}
|
||||
}
|
||||
in.Host = uri.Host
|
||||
|
||||
// there is an edge case where, during installation, a user could attempt
|
||||
// to add the same result multiple times.
|
||||
if remote, err := h.remotes.FindHost(in.Host); err == nil && h.users.Exist() {
|
||||
h.remotes.Delete(remote)
|
||||
}
|
||||
|
||||
// insert the remote in the database
|
||||
if err := h.remotes.Insert(&in); err != nil {
|
||||
return internalServerError{err}
|
||||
}
|
||||
|
||||
return json.NewEncoder(w).Encode(&in)
|
||||
}
|
||||
|
||||
// DeleteRemote delete the remote.
|
||||
// GET /api/remotes/:name
|
||||
func (h *RemoteHandler) DeleteRemote(w http.ResponseWriter, r *http.Request) error {
|
||||
host := r.FormValue(":host")
|
||||
|
||||
// get the user form the session
|
||||
user := h.sess.User(r)
|
||||
if user == nil || !user.Admin {
|
||||
return notAuthorized{}
|
||||
}
|
||||
// get the remote
|
||||
remote, err := h.remotes.FindHost(host)
|
||||
if err != nil {
|
||||
return notFound{err}
|
||||
}
|
||||
if err := h.remotes.Delete(remote); err != nil {
|
||||
return internalServerError{err}
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *RemoteHandler) Register(r *pat.Router) {
|
||||
r.Delete("/v1/remotes/:name", errorHandler(h.DeleteRemote))
|
||||
r.Post("/v1/remotes", errorHandler(h.PostRemote))
|
||||
r.Get("/v1/remotes", errorHandler(h.GetRemotes))
|
||||
}
|
|
@ -92,7 +92,7 @@ func (h *RepoHandler) PostRepo(w http.ResponseWriter, r *http.Request) error {
|
|||
}
|
||||
|
||||
// post commit hook url
|
||||
hook := fmt.Sprintf("%s://%s/hook/%s", httputil.GetScheme(r), httputil.GetHost(r), remote.GetName())
|
||||
hook := fmt.Sprintf("%s://%s/v1/hook/%s", httputil.GetScheme(r), httputil.GetHost(r), remote.GetName())
|
||||
|
||||
// activate the repository in the remote system
|
||||
client := remote.GetClient(user.Access, user.Secret)
|
||||
|
|
88
server/handler/server.go
Normal file
88
server/handler/server.go
Normal file
|
@ -0,0 +1,88 @@
|
|||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/drone/drone/server/database"
|
||||
"github.com/drone/drone/server/session"
|
||||
"github.com/drone/drone/shared/model"
|
||||
"github.com/gorilla/pat"
|
||||
)
|
||||
|
||||
type ServerHandler struct {
|
||||
servers database.ServerManager
|
||||
sess session.Session
|
||||
}
|
||||
|
||||
func NewServerHandler(servers database.ServerManager, sess session.Session) *ServerHandler {
|
||||
return &ServerHandler{servers, sess}
|
||||
}
|
||||
|
||||
// GetServers gets all servers.
|
||||
// GET /api/servers
|
||||
func (h *ServerHandler) GetServers(w http.ResponseWriter, r *http.Request) error {
|
||||
// get the user form the session
|
||||
user := h.sess.User(r)
|
||||
if user == nil || !user.Admin {
|
||||
return notAuthorized{}
|
||||
}
|
||||
// get all servers
|
||||
servers, err := h.servers.List()
|
||||
if err != nil {
|
||||
return internalServerError{err}
|
||||
}
|
||||
|
||||
return json.NewEncoder(w).Encode(servers)
|
||||
}
|
||||
|
||||
// PostServer creates a new server.
|
||||
// POST /api/servers
|
||||
func (h *ServerHandler) PostServer(w http.ResponseWriter, r *http.Request) error {
|
||||
// get the user form the session
|
||||
user := h.sess.User(r)
|
||||
if user == nil || !user.Admin {
|
||||
return notAuthorized{}
|
||||
}
|
||||
// unmarshal the server from the payload
|
||||
defer r.Body.Close()
|
||||
in := model.Server{}
|
||||
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
|
||||
return badRequest{err}
|
||||
}
|
||||
// insert the server in the database
|
||||
if err := h.servers.Insert(&in); err != nil {
|
||||
return internalServerError{err}
|
||||
}
|
||||
|
||||
return json.NewEncoder(w).Encode(&in)
|
||||
}
|
||||
|
||||
// DeleteServers deletes the named server.
|
||||
// GET /api/servers/:name
|
||||
func (h *ServerHandler) DeleteServer(w http.ResponseWriter, r *http.Request) error {
|
||||
name := r.FormValue(":name")
|
||||
|
||||
// get the user form the session
|
||||
user := h.sess.User(r)
|
||||
if user == nil || !user.Admin {
|
||||
return notAuthorized{}
|
||||
}
|
||||
// get the server
|
||||
server, err := h.servers.FindName(name)
|
||||
if err != nil {
|
||||
return notFound{err}
|
||||
}
|
||||
if err := h.servers.Delete(server); err != nil {
|
||||
return internalServerError{err}
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *ServerHandler) Register(r *pat.Router) {
|
||||
r.Delete("/v1/servers/:name", errorHandler(h.DeleteServer))
|
||||
r.Post("/v1/servers", errorHandler(h.PostServer))
|
||||
r.Get("/v1/servers", errorHandler(h.GetServers))
|
||||
}
|
|
@ -1,186 +0,0 @@
|
|||
package handler
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/drone/drone/server/channel"
|
||||
"github.com/drone/drone/server/database"
|
||||
"github.com/drone/drone/server/session"
|
||||
"github.com/drone/drone/shared/httputil"
|
||||
"github.com/drone/drone/shared/model"
|
||||
"github.com/gorilla/pat"
|
||||
)
|
||||
|
||||
type Renderer func(wr io.Writer, name string, data interface{}) error
|
||||
|
||||
type SiteHandler struct {
|
||||
users database.UserManager
|
||||
repos database.RepoManager
|
||||
commits database.CommitManager
|
||||
perms database.PermManager
|
||||
sess session.Session
|
||||
render Renderer
|
||||
}
|
||||
|
||||
func NewSiteHandler(users database.UserManager, repos database.RepoManager, commits database.CommitManager, perms database.PermManager, sess session.Session, render Renderer) *SiteHandler {
|
||||
return &SiteHandler{users, repos, commits, perms, sess, render}
|
||||
}
|
||||
|
||||
// GetIndex serves the root domain request. This is forwarded to the dashboard
|
||||
// page iff the user is authenticated, else it is forwarded to the login page.
|
||||
func (s *SiteHandler) GetIndex(w http.ResponseWriter, r *http.Request) error {
|
||||
u := s.sess.User(r)
|
||||
if u == nil {
|
||||
http.Redirect(w, r, "/login", http.StatusSeeOther)
|
||||
return nil
|
||||
}
|
||||
feed, _ := s.commits.ListUser(u.ID)
|
||||
data := struct {
|
||||
User *model.User
|
||||
Feed []*model.CommitRepo
|
||||
}{u, feed}
|
||||
return s.render(w, "user_feed.html", &data)
|
||||
}
|
||||
|
||||
// GetLogin serves the account login page
|
||||
func (s *SiteHandler) GetLogin(w http.ResponseWriter, r *http.Request) error {
|
||||
return s.render(w, "login.html", struct{ User *model.User }{nil})
|
||||
}
|
||||
|
||||
// GetUser serves the account settings page.
|
||||
func (s *SiteHandler) GetUser(w http.ResponseWriter, r *http.Request) error {
|
||||
u := s.sess.User(r)
|
||||
if u == nil {
|
||||
return s.render(w, "404.html", nil)
|
||||
}
|
||||
return s.render(w, "user_conf.html", struct{ User *model.User }{u})
|
||||
}
|
||||
|
||||
func (s *SiteHandler) GetUsers(w http.ResponseWriter, r *http.Request) error {
|
||||
u := s.sess.User(r)
|
||||
if u == nil || u.Admin == false {
|
||||
return s.render(w, "404.html", nil)
|
||||
}
|
||||
return s.render(w, "admin_users.html", struct{ User *model.User }{u})
|
||||
}
|
||||
|
||||
func (s *SiteHandler) GetConfig(w http.ResponseWriter, r *http.Request) error {
|
||||
u := s.sess.User(r)
|
||||
if u == nil || u.Admin == false {
|
||||
return s.render(w, "404.html", nil)
|
||||
}
|
||||
return s.render(w, "admin_conf.html", struct{ User *model.User }{u})
|
||||
}
|
||||
|
||||
func (s *SiteHandler) GetRepo(w http.ResponseWriter, r *http.Request) error {
|
||||
host, owner, name := parseRepo(r)
|
||||
branch := parseBranch(r)
|
||||
sha := parseCommit(r)
|
||||
usr := s.sess.User(r)
|
||||
|
||||
arepo, err := s.repos.FindName(host, owner, name)
|
||||
if err != nil {
|
||||
return s.render(w, "404.html", nil)
|
||||
}
|
||||
if ok, _ := s.perms.Read(usr, arepo); !ok {
|
||||
return s.render(w, "404.html", nil)
|
||||
}
|
||||
data := struct {
|
||||
User *model.User
|
||||
Repo *model.Repo
|
||||
Branch string
|
||||
Channel string
|
||||
Stream string
|
||||
Branches []*model.Commit
|
||||
Commits []*model.Commit
|
||||
Commit *model.Commit
|
||||
}{User: usr, Repo: arepo}
|
||||
|
||||
// generate a token for connecting to the streaming server
|
||||
// to get notified of feed items.
|
||||
data.Channel = channel.Create(host + "/" + owner + "/" + name + "/")
|
||||
|
||||
// if commit details are provided we should retrieve the build details
|
||||
// and serve the build page.
|
||||
if len(sha) != 0 {
|
||||
data.Commit, err = s.commits.FindSha(data.Repo.ID, branch, sha)
|
||||
if err != nil {
|
||||
return s.render(w, "404.html", nil)
|
||||
}
|
||||
|
||||
// generate a token for connecting to the streaming server
|
||||
// to get notified of feed items.
|
||||
data.Stream = channel.Create(host + "/" + owner + "/" + name + "/" + branch + "/" + sha)
|
||||
|
||||
return s.render(w, "repo_commit.html", &data)
|
||||
}
|
||||
|
||||
// retrieve the list of recently built branches
|
||||
data.Branches, _ = s.commits.ListBranches(data.Repo.ID)
|
||||
|
||||
// if the branch parameter is provided we should retrieve the build
|
||||
// feed for this branch only.
|
||||
if len(branch) != 0 {
|
||||
data.Branch = branch
|
||||
data.Commits, err = s.commits.ListBranch(data.Repo.ID, branch)
|
||||
if err != nil {
|
||||
return s.render(w, "404.html", nil)
|
||||
}
|
||||
return s.render(w, "repo_branch.html", &data)
|
||||
}
|
||||
|
||||
// else we should serve the standard build feed
|
||||
data.Commits, _ = s.commits.List(data.Repo.ID)
|
||||
return s.render(w, "repo_feed.html", &data)
|
||||
}
|
||||
|
||||
func (s *SiteHandler) GetRepoAdmin(w http.ResponseWriter, r *http.Request) error {
|
||||
host, owner, name := parseRepo(r)
|
||||
arepo, err := s.repos.FindName(host, owner, name)
|
||||
u := s.sess.User(r)
|
||||
if err != nil {
|
||||
return s.render(w, "404.html", nil)
|
||||
}
|
||||
if ok, _ := s.perms.Admin(u, arepo); !ok {
|
||||
return s.render(w, "404.html", nil)
|
||||
}
|
||||
data := struct {
|
||||
User *model.User
|
||||
Repo *model.Repo
|
||||
Host string
|
||||
Scheme string
|
||||
}{u, arepo, httputil.GetHost(r), httputil.GetScheme(r)}
|
||||
return s.render(w, "repo_conf.html", &data)
|
||||
}
|
||||
|
||||
// GetRepos serves a page that lists all user repositories.
|
||||
func (s *SiteHandler) GetRepos(w http.ResponseWriter, r *http.Request) error {
|
||||
u := s.sess.User(r)
|
||||
if u == nil || u.Admin == false {
|
||||
return s.render(w, "404.html", nil)
|
||||
}
|
||||
repos, err := s.repos.List(u.ID)
|
||||
if err != nil {
|
||||
s.render(w, "500.html", nil)
|
||||
}
|
||||
data := struct {
|
||||
User *model.User
|
||||
Repos []*model.Repo
|
||||
}{u, repos}
|
||||
return s.render(w, "user_repos.html", &data)
|
||||
}
|
||||
|
||||
func (s *SiteHandler) Register(r *pat.Router) {
|
||||
|
||||
r.Get("/admin/users", errorHandler(s.GetUsers))
|
||||
r.Get("/admin/settings", errorHandler(s.GetConfig))
|
||||
r.Get("/account/profile", errorHandler(s.GetUser))
|
||||
r.Get("/account/repos", errorHandler(s.GetRepos))
|
||||
r.Get("/login", errorHandler(s.GetLogin))
|
||||
r.Get("/{host}/{owner}/{name}/settings", errorHandler(s.GetRepoAdmin))
|
||||
r.Get("/{host}/{owner}/{name}/branch/{branch}/commit/{commit}", errorHandler(s.GetRepo))
|
||||
r.Get("/{host}/{owner}/{name}/branch/{branch}", errorHandler(s.GetRepo))
|
||||
r.Get("/{host}/{owner}/{name}", errorHandler(s.GetRepo))
|
||||
r.Get("/", errorHandler(s.GetIndex))
|
||||
}
|
216
server/handler/ws.go
Normal file
216
server/handler/ws.go
Normal file
|
@ -0,0 +1,216 @@
|
|||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/drone/drone/server/database"
|
||||
"github.com/drone/drone/server/pubsub"
|
||||
"github.com/drone/drone/server/session"
|
||||
"github.com/drone/drone/server/worker"
|
||||
"github.com/drone/drone/shared/model"
|
||||
"github.com/gorilla/pat"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
const (
|
||||
// Time allowed to write the message to the client.
|
||||
writeWait = 10 * time.Second
|
||||
|
||||
// Time allowed to read the next pong message from the client.
|
||||
pongWait = 60 * time.Second
|
||||
|
||||
// Send pings to client with this period. Must be less than pongWait.
|
||||
pingPeriod = (pongWait * 9) / 10
|
||||
)
|
||||
|
||||
var upgrader = websocket.Upgrader{
|
||||
ReadBufferSize: 1024,
|
||||
WriteBufferSize: 1024,
|
||||
}
|
||||
|
||||
type WsHandler struct {
|
||||
pubsub *pubsub.PubSub
|
||||
commits database.CommitManager
|
||||
perms database.PermManager
|
||||
repos database.RepoManager
|
||||
sess session.Session
|
||||
}
|
||||
|
||||
func NewWsHandler(repos database.RepoManager, commits database.CommitManager, perms database.PermManager, sess session.Session, pubsub *pubsub.PubSub) *WsHandler {
|
||||
return &WsHandler{pubsub, commits, perms, repos, sess}
|
||||
}
|
||||
|
||||
// WsUser will upgrade the connection to a Websocket and will stream
|
||||
// all events to the browser pertinent to the authenticated user. If the user
|
||||
// is not authenticated, only public events are streamed.
|
||||
func (h *WsHandler) WsUser(w http.ResponseWriter, r *http.Request) error {
|
||||
// get the user form the session
|
||||
user := h.sess.UserCookie(r)
|
||||
|
||||
// upgrade the websocket
|
||||
ws, err := upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
return badRequest{err}
|
||||
}
|
||||
|
||||
// register a channel for global events
|
||||
channel := h.pubsub.Register("_global")
|
||||
sub := channel.Subscribe()
|
||||
|
||||
ticker := time.NewTicker(pingPeriod)
|
||||
defer func() {
|
||||
ticker.Stop()
|
||||
sub.Close()
|
||||
ws.Close()
|
||||
}()
|
||||
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case msg := <-sub.Read():
|
||||
work, ok := msg.(*worker.Request)
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
|
||||
// user must have read access to the repository
|
||||
// in order to pass this message along
|
||||
if ok, _ := h.perms.Read(user, work.Repo); !ok {
|
||||
break
|
||||
}
|
||||
|
||||
ws.SetWriteDeadline(time.Now().Add(writeWait))
|
||||
err := ws.WriteJSON(work)
|
||||
if err != nil {
|
||||
ws.Close()
|
||||
return
|
||||
}
|
||||
case <-sub.CloseNotify():
|
||||
ws.Close()
|
||||
return
|
||||
case <-ticker.C:
|
||||
ws.SetWriteDeadline(time.Now().Add(writeWait))
|
||||
err := ws.WriteMessage(websocket.PingMessage, []byte{})
|
||||
if err != nil {
|
||||
ws.Close()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
readWebsocket(ws)
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
// WsConsole will upgrade the connection to a Websocket and will stream
|
||||
// the build output to the browser.
|
||||
func (h *WsHandler) WsConsole(w http.ResponseWriter, r *http.Request) error {
|
||||
var host, owner, name = parseRepo(r)
|
||||
var branch = r.FormValue(":branch")
|
||||
var sha = r.FormValue(":commit")
|
||||
|
||||
repo, err := h.repos.FindName(host, owner, name)
|
||||
if err != nil {
|
||||
return notFound{err}
|
||||
}
|
||||
commit, err := h.commits.FindSha(repo.ID, branch, sha)
|
||||
if err != nil {
|
||||
return notFound{err}
|
||||
}
|
||||
user := h.sess.UserCookie(r)
|
||||
if ok, _ := h.perms.Read(user, repo); !ok {
|
||||
return notFound{err}
|
||||
}
|
||||
|
||||
// find a channel that we can subscribe to
|
||||
// and listen for stream updates.
|
||||
channel := h.pubsub.Lookup(commit.ID)
|
||||
if channel == nil {
|
||||
return notFound{}
|
||||
}
|
||||
sub := channel.Subscribe()
|
||||
defer sub.Close()
|
||||
|
||||
// upgrade the websocket
|
||||
ws, err := upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
return badRequest{err}
|
||||
}
|
||||
|
||||
ticker := time.NewTicker(pingPeriod)
|
||||
defer func() {
|
||||
ticker.Stop()
|
||||
ws.Close()
|
||||
}()
|
||||
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case msg := <-sub.Read():
|
||||
data, ok := msg.([]byte)
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
ws.SetWriteDeadline(time.Now().Add(writeWait))
|
||||
err := ws.WriteMessage(websocket.TextMessage, data)
|
||||
if err != nil {
|
||||
ws.Close()
|
||||
return
|
||||
}
|
||||
case <-sub.CloseNotify():
|
||||
ws.Close()
|
||||
return
|
||||
case <-ticker.C:
|
||||
ws.SetWriteDeadline(time.Now().Add(writeWait))
|
||||
err := ws.WriteMessage(websocket.PingMessage, []byte{})
|
||||
if err != nil {
|
||||
ws.Close()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
readWebsocket(ws)
|
||||
return nil
|
||||
}
|
||||
|
||||
// readWebsocket will block while reading the websocket data
|
||||
func readWebsocket(ws *websocket.Conn) {
|
||||
defer ws.Close()
|
||||
ws.SetReadLimit(512)
|
||||
ws.SetReadDeadline(time.Now().Add(pongWait))
|
||||
ws.SetPongHandler(func(string) error {
|
||||
ws.SetReadDeadline(time.Now().Add(pongWait))
|
||||
return nil
|
||||
})
|
||||
for {
|
||||
_, _, err := ws.ReadMessage()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ping is a method that is being used for internal testing and
|
||||
// will be removed prior to release
|
||||
func (h *WsHandler) Ping(w http.ResponseWriter, r *http.Request) error {
|
||||
channel := h.pubsub.Register("_global")
|
||||
msg := worker.Request{
|
||||
Repo: &model.Repo{ID: 1, Private: false, Host: "github.com", Owner: "ping", Name: "pong"},
|
||||
Commit: &model.Commit{ID: 1, Sha: "000000000000000000000", Message: "hello world"},
|
||||
}
|
||||
channel.Publish(&msg)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *WsHandler) Register(r *pat.Router) {
|
||||
r.Post("/ws/ping", errorHandler(h.Ping))
|
||||
r.Get("/ws/user", errorHandler(h.WsUser))
|
||||
r.Get("/ws/{host}/{owner}/{name}/branches/{branch}/commits/{commit}", errorHandler(h.WsConsole))
|
||||
}
|
|
@ -3,24 +3,22 @@ package main
|
|||
import (
|
||||
"database/sql"
|
||||
"flag"
|
||||
"html/template"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"time"
|
||||
"strings"
|
||||
|
||||
"code.google.com/p/go.net/websocket"
|
||||
"github.com/drone/drone/server/channel"
|
||||
"github.com/drone/drone/server/database"
|
||||
"github.com/drone/drone/server/database/schema"
|
||||
"github.com/drone/drone/server/handler"
|
||||
"github.com/drone/drone/server/queue"
|
||||
"github.com/drone/drone/server/pubsub"
|
||||
"github.com/drone/drone/server/session"
|
||||
"github.com/drone/drone/shared/build/docker"
|
||||
"github.com/drone/drone/server/worker"
|
||||
"github.com/drone/drone/shared/build/log"
|
||||
"github.com/drone/drone/shared/model"
|
||||
|
||||
"github.com/gorilla/pat"
|
||||
//"github.com/justinas/nosurf"
|
||||
|
@ -51,10 +49,6 @@ var (
|
|||
version string = "0.2-dev"
|
||||
revision string
|
||||
|
||||
// build will timeout after N milliseconds.
|
||||
// this will default to 500 minutes (6 hours)
|
||||
timeout time.Duration
|
||||
|
||||
// Number of concurrent build workers to run
|
||||
// default to number of CPUs on machine
|
||||
workers int
|
||||
|
@ -70,25 +64,9 @@ func main() {
|
|||
flag.StringVar(&datasource, "datasource", "drone.sqlite", "")
|
||||
flag.StringVar(&sslcert, "sslcert", "", "")
|
||||
flag.StringVar(&sslkey, "sslkey", "", "")
|
||||
flag.DurationVar(&timeout, "timeout", 300*time.Minute, "")
|
||||
flag.IntVar(&workers, "workers", runtime.NumCPU(), "")
|
||||
flag.Parse()
|
||||
|
||||
// parse the template files
|
||||
// TODO we need to retrieve these from go.rice
|
||||
//templ := template.Must(
|
||||
// template.New("_").Funcs(render.FuncMap).ParseGlob("template/html/*.html"),
|
||||
//).ExecuteTemplate
|
||||
|
||||
// load the html templates
|
||||
templateBox := rice.MustFindBox("template/html")
|
||||
templateFiles := []string{"login.html", "repo_branch.html", "repo_commit.html", "repo_conf.html", "repo_feed.html", "user_conf.html", "user_feed.html", "user_login.html", "user_repos.html", "404.html", "400.html"}
|
||||
templ := template.New("_").Funcs(funcMap)
|
||||
for _, file := range templateFiles {
|
||||
templateData, _ := templateBox.String(file)
|
||||
templ, _ = templ.New(file).Parse(templateData)
|
||||
}
|
||||
|
||||
// setup the database
|
||||
meddler.Default = meddler.SQLite
|
||||
db, _ := sql.Open(driver, datasource)
|
||||
|
@ -99,14 +77,24 @@ func main() {
|
|||
users := database.NewUserManager(db)
|
||||
perms := database.NewPermManager(db)
|
||||
commits := database.NewCommitManager(db)
|
||||
servers := database.NewServerManager(db)
|
||||
remotes := database.NewRemoteManager(db)
|
||||
configs := database.NewConfigManager(filepath.Join(home, "config.toml"))
|
||||
|
||||
// message broker
|
||||
pubsub := pubsub.NewPubSub()
|
||||
|
||||
// cancel all previously running builds
|
||||
go commits.CancelAll()
|
||||
|
||||
// setup the build queue
|
||||
queueRunner := queue.NewBuildRunner(docker.New(), timeout)
|
||||
queue := queue.Start(workers, commits, queueRunner)
|
||||
//queueRunner := queue.NewBuildRunner(docker.New(), timeout)
|
||||
//queue := queue.Start(work ers, commits, queueRunner)
|
||||
|
||||
queue := make(chan *worker.Request)
|
||||
workers := make(chan chan *worker.Request)
|
||||
worker.NewDispatch(queue, workers).Start()
|
||||
worker.NewWorker(workers, users, repos, commits, configs, pubsub, &model.Server{Host: "unix:///tmp/sock.sock"}).Start()
|
||||
|
||||
// setup the session managers
|
||||
sess := session.NewSession(users)
|
||||
|
@ -122,28 +110,51 @@ func main() {
|
|||
handler.NewRepoHandler(repos, commits, perms, sess, configs).Register(router)
|
||||
handler.NewBadgeHandler(repos, commits).Register(router)
|
||||
handler.NewConfigHandler(configs, sess).Register(router)
|
||||
handler.NewSiteHandler(users, repos, commits, perms, sess, templ.ExecuteTemplate).Register(router)
|
||||
handler.NewServerHandler(servers, sess).Register(router)
|
||||
handler.NewRemoteHandler(users, remotes, sess).Register(router)
|
||||
handler.NewWsHandler(repos, commits, perms, sess, pubsub).Register(router)
|
||||
//handler.NewSiteHandler(users, repos, commits, perms, sess, templ.ExecuteTemplate).Register(router)
|
||||
|
||||
// serve static assets
|
||||
// TODO we need to replace this with go.rice
|
||||
http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(rice.MustFindBox("static/").HTTPBox())))
|
||||
|
||||
// server websocket data
|
||||
http.Handle("/feed", websocket.Handler(channel.Read))
|
||||
|
||||
// register the router
|
||||
// TODO we disabled nosurf because it was impacting API calls.
|
||||
// we need to disable nosurf for api calls (ie not coming from website).
|
||||
http.Handle("/", router)
|
||||
box := rice.MustFindBox("app/")
|
||||
fserver := http.FileServer(box.HTTPBox())
|
||||
index, _ := box.Bytes("index.html")
|
||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case strings.HasPrefix(r.URL.Path, "/favicon.ico"),
|
||||
strings.HasPrefix(r.URL.Path, "/scripts/"),
|
||||
strings.HasPrefix(r.URL.Path, "/styles/"),
|
||||
strings.HasPrefix(r.URL.Path, "/views/"):
|
||||
fserver.ServeHTTP(w, r)
|
||||
case strings.HasPrefix(r.URL.Path, "/logout"),
|
||||
strings.HasPrefix(r.URL.Path, "/login/"),
|
||||
strings.HasPrefix(r.URL.Path, "/v1/"),
|
||||
strings.HasPrefix(r.URL.Path, "/ws/"):
|
||||
router.ServeHTTP(w, r)
|
||||
default:
|
||||
w.Write(index)
|
||||
}
|
||||
})
|
||||
|
||||
// start webserver using HTTPS or HTTP
|
||||
if len(sslcert) != 0 && len(sslkey) != 0 {
|
||||
if len(sslcert) != 0 {
|
||||
panic(http.ListenAndServeTLS(port, sslcert, sslkey, nil))
|
||||
} else {
|
||||
panic(http.ListenAndServe(port, nil))
|
||||
}
|
||||
}
|
||||
|
||||
func setupDatabase() {
|
||||
|
||||
}
|
||||
|
||||
func setupQueue() {
|
||||
|
||||
}
|
||||
|
||||
func setupHandlers() {
|
||||
|
||||
}
|
||||
|
||||
// initialize the .drone directory and create a skeleton config
|
||||
// file if one does not already exist.
|
||||
func init() {
|
||||
|
|
30
server/pubsub/buffer.go
Normal file
30
server/pubsub/buffer.go
Normal file
|
@ -0,0 +1,30 @@
|
|||
package pubsub
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
)
|
||||
|
||||
type Buffer struct {
|
||||
buf bytes.Buffer
|
||||
channel *Channel
|
||||
}
|
||||
|
||||
func NewBuffer(channel *Channel) *Buffer {
|
||||
return &Buffer{
|
||||
channel: channel,
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Buffer) Write(p []byte) (n int, err error) {
|
||||
n, err = b.buf.Write(p)
|
||||
b.channel.Publish(p)
|
||||
return
|
||||
}
|
||||
|
||||
func (b *Buffer) WriteString(s string) (n int, err error) {
|
||||
return b.Write([]byte(s))
|
||||
}
|
||||
|
||||
func (b *Buffer) Bytes() []byte {
|
||||
return b.buf.Bytes()
|
||||
}
|
118
server/pubsub/channel.go
Normal file
118
server/pubsub/channel.go
Normal file
|
@ -0,0 +1,118 @@
|
|||
package pubsub
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type Channel struct {
|
||||
record bool
|
||||
history []interface{}
|
||||
timeout time.Duration
|
||||
closed chan bool
|
||||
broadcast chan interface{}
|
||||
subscribe chan *Subscription
|
||||
unsubscribe chan *Subscription
|
||||
subscriptions map[*Subscription]bool
|
||||
}
|
||||
|
||||
func NewChannel(opts *Opts) *Channel {
|
||||
return &Channel{
|
||||
timeout: opts.Timeout,
|
||||
record: opts.Record,
|
||||
history: make([]interface{}, 0),
|
||||
closed: make(chan bool),
|
||||
broadcast: make(chan interface{}),
|
||||
subscribe: make(chan *Subscription),
|
||||
unsubscribe: make(chan *Subscription),
|
||||
subscriptions: make(map[*Subscription]bool),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Channel) Publish(data interface{}) {
|
||||
c.broadcast <- data
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Channel) Subscribe() *Subscription {
|
||||
s := NewSubscription(c)
|
||||
c.subscribe <- s
|
||||
return s
|
||||
}
|
||||
|
||||
func (c *Channel) Close() {
|
||||
go func() { c.closed <- true }()
|
||||
}
|
||||
|
||||
func (c *Channel) start() {
|
||||
// make sure we don't bring down the application
|
||||
// if somehow we encounter a nil pointer or some
|
||||
// other unexpected behavior.
|
||||
defer func() {
|
||||
recover()
|
||||
}()
|
||||
|
||||
// timeout the channel after N duration
|
||||
// ignore the timeout if set to 0
|
||||
var timeout <-chan time.Time
|
||||
if c.timeout > 0 {
|
||||
timeout = time.After(c.timeout)
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
|
||||
case sub := <-c.unsubscribe:
|
||||
delete(c.subscriptions, sub)
|
||||
close(sub.send)
|
||||
|
||||
case sub := <-c.subscribe:
|
||||
c.subscriptions[sub] = true
|
||||
|
||||
// if we are recording the output
|
||||
// we should send it to the subscriber
|
||||
// upon first connecting.
|
||||
if c.record && len(c.history) > 0 {
|
||||
history := make([]interface{}, len(c.history))
|
||||
copy(history, c.history)
|
||||
go replay(sub, history)
|
||||
}
|
||||
|
||||
case msg := <-c.broadcast:
|
||||
// if we are recording the output, append
|
||||
// the message to the history
|
||||
if c.record {
|
||||
c.history = append(c.history, msg)
|
||||
}
|
||||
|
||||
// loop through each subscription and
|
||||
// send the message.
|
||||
for sub := range c.subscriptions {
|
||||
select {
|
||||
case sub.send <- msg:
|
||||
// do nothing
|
||||
default:
|
||||
sub.Close()
|
||||
}
|
||||
}
|
||||
|
||||
case <-timeout:
|
||||
c.Close()
|
||||
|
||||
case <-c.closed:
|
||||
c.stop()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func replay(s *Subscription, history []interface{}) {
|
||||
for _, msg := range history {
|
||||
s.send <- msg
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Channel) stop() {
|
||||
for sub := range c.subscriptions {
|
||||
sub.Close()
|
||||
}
|
||||
}
|
21
server/pubsub/opts.go
Normal file
21
server/pubsub/opts.go
Normal file
|
@ -0,0 +1,21 @@
|
|||
package pubsub
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type Opts struct {
|
||||
// Timeout sets the expiration date for the channel,
|
||||
// at which time it will be closed and transmission will
|
||||
// stop. A zero value for means the channel will not timeout.
|
||||
Timeout time.Duration
|
||||
|
||||
// Record indicates the channel should record the channel
|
||||
// activity and playback the full history to subscribers.
|
||||
Record bool
|
||||
}
|
||||
|
||||
var DefaultOpts = &Opts{
|
||||
Timeout: 0,
|
||||
Record: false,
|
||||
}
|
75
server/pubsub/pubsub.go
Normal file
75
server/pubsub/pubsub.go
Normal file
|
@ -0,0 +1,75 @@
|
|||
package pubsub
|
||||
|
||||
import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
type PubSub struct {
|
||||
sync.Mutex
|
||||
|
||||
// In-memory list of all channels being managed by the broker.
|
||||
channels map[interface{}]*Channel
|
||||
}
|
||||
|
||||
// NewPubSub creates a new instance of the PubSub type
|
||||
// and returns a pointer.
|
||||
func NewPubSub() *PubSub {
|
||||
return &PubSub{
|
||||
channels: make(map[interface{}]*Channel),
|
||||
}
|
||||
}
|
||||
|
||||
// Lookup performs a thread safe operation to return a pointer
|
||||
// to an existing Channel object with the given key. If the
|
||||
// Channel does not exist a nil value is returned.
|
||||
func (b *PubSub) Lookup(key interface{}) *Channel {
|
||||
b.Lock()
|
||||
defer b.Unlock()
|
||||
|
||||
// find the channel in the existing list
|
||||
return b.channels[key]
|
||||
}
|
||||
|
||||
// Register performs a thread safe operation to return a pointer
|
||||
// to a Channel object for the given key. The Channel is created
|
||||
// if it does not yet exist.
|
||||
func (b *PubSub) Register(key interface{}) *Channel {
|
||||
return b.RegisterOpts(key, DefaultOpts)
|
||||
}
|
||||
|
||||
// Register performs a thread safe operation to return a pointer
|
||||
// to a Channel object for the given key. The Channel is created
|
||||
// if it does not yet exist using custom options.
|
||||
func (b *PubSub) RegisterOpts(key interface{}, opts *Opts) *Channel {
|
||||
b.Lock()
|
||||
defer b.Unlock()
|
||||
|
||||
// find the channel in the existing list
|
||||
c, ok := b.channels[key]
|
||||
if ok {
|
||||
return c
|
||||
}
|
||||
|
||||
// create the channel and register
|
||||
// with the pubsub server
|
||||
c = NewChannel(opts)
|
||||
b.channels[key] = c
|
||||
go c.start()
|
||||
return c
|
||||
}
|
||||
|
||||
// Unregister performs a thread safe operation to delete the
|
||||
// Channel with the given key.
|
||||
func (b *PubSub) Unregister(key interface{}) {
|
||||
b.Lock()
|
||||
defer b.Unlock()
|
||||
|
||||
// find the channel in the existing list
|
||||
c, ok := b.channels[key]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
c.Close()
|
||||
delete(b.channels, key)
|
||||
return
|
||||
}
|
28
server/pubsub/subscribe.go
Normal file
28
server/pubsub/subscribe.go
Normal file
|
@ -0,0 +1,28 @@
|
|||
package pubsub
|
||||
|
||||
type Subscription struct {
|
||||
channel *Channel
|
||||
closed chan bool
|
||||
send chan interface{}
|
||||
}
|
||||
|
||||
func NewSubscription(channel *Channel) *Subscription {
|
||||
return &Subscription{
|
||||
channel: channel,
|
||||
closed: make(chan bool),
|
||||
send: make(chan interface{}),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Subscription) Read() <-chan interface{} {
|
||||
return s.send
|
||||
}
|
||||
|
||||
func (s *Subscription) Close() {
|
||||
go func() { s.channel.unsubscribe <- s }()
|
||||
go func() { s.closed <- true }()
|
||||
}
|
||||
|
||||
func (s *Subscription) CloseNotify() <-chan bool {
|
||||
return s.closed
|
||||
}
|
|
@ -1,41 +0,0 @@
|
|||
package queue
|
||||
|
||||
import (
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/drone/drone/shared/build"
|
||||
"github.com/drone/drone/shared/build/docker"
|
||||
"github.com/drone/drone/shared/build/repo"
|
||||
"github.com/drone/drone/shared/build/script"
|
||||
)
|
||||
|
||||
type BuildRunner interface {
|
||||
Run(buildScript *script.Build, repo *repo.Repo, key []byte, privileged bool, buildOutput io.Writer) (success bool, err error)
|
||||
}
|
||||
|
||||
type buildRunner struct {
|
||||
dockerClient *docker.Client
|
||||
timeout time.Duration
|
||||
}
|
||||
|
||||
func NewBuildRunner(dockerClient *docker.Client, timeout time.Duration) BuildRunner {
|
||||
return &buildRunner{
|
||||
dockerClient: dockerClient,
|
||||
timeout: timeout,
|
||||
}
|
||||
}
|
||||
|
||||
func (runner *buildRunner) Run(buildScript *script.Build, repo *repo.Repo, key []byte, privileged bool, buildOutput io.Writer) (bool, error) {
|
||||
builder := build.New(runner.dockerClient)
|
||||
builder.Build = buildScript
|
||||
builder.Repo = repo
|
||||
builder.Key = key
|
||||
builder.Privileged = privileged
|
||||
builder.Stdout = buildOutput
|
||||
builder.Timeout = runner.timeout
|
||||
|
||||
err := builder.Run()
|
||||
|
||||
return builder.BuildState == nil || builder.BuildState.ExitCode != 0, err
|
||||
}
|
|
@ -1,47 +0,0 @@
|
|||
package queue
|
||||
|
||||
import (
|
||||
"github.com/drone/drone/server/database"
|
||||
"github.com/drone/drone/shared/model"
|
||||
|
||||
"github.com/drone/drone/shared/build/script"
|
||||
)
|
||||
|
||||
// A Queue dispatches tasks to workers.
|
||||
type Queue struct {
|
||||
tasks chan<- *BuildTask
|
||||
}
|
||||
|
||||
// BuildTasks represents a build that is pending
|
||||
// execution.
|
||||
type BuildTask struct {
|
||||
User *model.User
|
||||
Repo *model.Repo
|
||||
Commit *model.Commit
|
||||
|
||||
// Build instructions from the .drone.yml
|
||||
// file, unmarshalled.
|
||||
Script *script.Build
|
||||
}
|
||||
|
||||
// Start N workers with the given build runner.
|
||||
func Start(workers int, commits database.CommitManager, runner BuildRunner) *Queue {
|
||||
tasks := make(chan *BuildTask)
|
||||
queue := &Queue{tasks: tasks}
|
||||
|
||||
for i := 0; i < workers; i++ {
|
||||
worker := worker{
|
||||
runner: runner,
|
||||
commits: commits,
|
||||
}
|
||||
|
||||
go worker.work(tasks)
|
||||
}
|
||||
|
||||
return queue
|
||||
}
|
||||
|
||||
// Add adds the task to the build queue.
|
||||
func (q *Queue) Add(task *BuildTask) {
|
||||
q.tasks <- task
|
||||
}
|
|
@ -1,260 +0,0 @@
|
|||
package queue
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/drone/drone/server/channel"
|
||||
"github.com/drone/drone/server/database"
|
||||
"github.com/drone/drone/shared/build/git"
|
||||
"github.com/drone/drone/shared/build/repo"
|
||||
"github.com/drone/drone/shared/build/script"
|
||||
"github.com/drone/drone/shared/model"
|
||||
"io"
|
||||
"log"
|
||||
"path/filepath"
|
||||
"time"
|
||||
)
|
||||
|
||||
type worker struct {
|
||||
commits database.CommitManager
|
||||
|
||||
runner BuildRunner
|
||||
}
|
||||
|
||||
// work is a function that will infinitely
|
||||
// run in the background waiting for tasks that
|
||||
// it can pull off the queue and execute.
|
||||
func (w *worker) work(queue <-chan *BuildTask) {
|
||||
var task *BuildTask
|
||||
for {
|
||||
// get work item (pointer) from the queue
|
||||
task = <-queue
|
||||
if task == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// execute the task
|
||||
err := w.execute(task)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// execute will execute the build task and persist
|
||||
// the results to the datastore.
|
||||
func (w *worker) execute(task *BuildTask) error {
|
||||
// we need to be sure that we can recover
|
||||
// from any sort panic that could occur
|
||||
// to avoid brining down the entire application
|
||||
defer func() {
|
||||
if e := recover(); e != nil {
|
||||
task.Commit.Finished = time.Now().Unix()
|
||||
task.Commit.Duration = task.Commit.Finished - task.Commit.Started
|
||||
task.Commit.Status = model.StatusError
|
||||
w.commits.Update(task.Commit)
|
||||
}
|
||||
}()
|
||||
|
||||
// parse the build script
|
||||
params, err := task.Repo.ParamMap()
|
||||
task.Script, err = script.ParseBuild(task.Commit.Config, params)
|
||||
if err != nil {
|
||||
log.Printf("Error parsing repository params. %s\n", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// update commit and build status
|
||||
task.Commit.Status = model.StatusStarted
|
||||
task.Commit.Started = time.Now().Unix()
|
||||
|
||||
// persist the commit to the database
|
||||
if err := w.commits.Update(task.Commit); err != nil {
|
||||
log.Printf("Error updating commit. %s\n", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// get settings
|
||||
//settings, _ := database.GetSettings()
|
||||
|
||||
// notification context
|
||||
//context := ¬ify.Context{
|
||||
// Repo: task.Repo,
|
||||
// Commit: task.Commit,
|
||||
// Host: settings.URL().String(),
|
||||
//}
|
||||
|
||||
// send all "started" notifications
|
||||
//if task.Script.Notifications != nil {
|
||||
// task.Script.Notifications.Send(context)
|
||||
//}
|
||||
|
||||
// Send "started" notification to Github
|
||||
//if err := updateGitHubStatus(task.Repo, task.Commit); err != nil {
|
||||
// log.Printf("error updating github status: %s\n", err.Error())
|
||||
//}
|
||||
|
||||
// make sure a channel exists for the repository,
|
||||
// the commit, and the commit output (TODO)
|
||||
reposlug := fmt.Sprintf("%s/%s/%s", task.Repo.Host, task.Repo.Owner, task.Repo.Name)
|
||||
//commitslug := fmt.Sprintf("%s/%s/%s/%s/%s", task.Repo.Host, task.Repo.Owner, task.Repo.Name, task.Commit.Branch, task.Commit.Sha)
|
||||
consoleslug := fmt.Sprintf("%s/%s/%s/%s/%s", task.Repo.Host, task.Repo.Owner, task.Repo.Name, task.Commit.Branch, task.Commit.Sha)
|
||||
channel.Create(reposlug)
|
||||
//channel.Create(commitslug)
|
||||
channel.CreateStream(consoleslug)
|
||||
|
||||
// notify the channels that the commit and build started
|
||||
channel.SendJSON(reposlug, task.Commit)
|
||||
//channel.SendJSON(commitslug, task.Commit)
|
||||
|
||||
var buf = &bufferWrapper{channel: consoleslug}
|
||||
|
||||
// append private parameters to the environment
|
||||
// variable section of the .drone.yml file, iff
|
||||
// this is not a pull request (for security purposes)
|
||||
//if task.Repo.Params != nil && len(task.Commit.PullRequest) == 0 {
|
||||
// for k, v := range task.Repo.Params {
|
||||
// task.Script.Env = append(task.Script.Env, k+"="+v)
|
||||
// }
|
||||
//}
|
||||
|
||||
//defer func() {
|
||||
// // update the status of the commit using the
|
||||
// // GitHub status API.
|
||||
// if err := updateGitHubStatus(task.Repo, task.Commit); err != nil {
|
||||
// log.Printf("error updating github status: %s\n", err.Error())
|
||||
// }
|
||||
//}()
|
||||
|
||||
// execute the build
|
||||
passed, buildErr := w.runBuild(task, buf)
|
||||
|
||||
task.Commit.Finished = time.Now().Unix()
|
||||
task.Commit.Duration = task.Commit.Finished - task.Commit.Started
|
||||
task.Commit.Status = model.StatusSuccess
|
||||
|
||||
// capture build output
|
||||
stdout := buf.buf.String()
|
||||
|
||||
// if exit code != 0 set to failure
|
||||
if passed {
|
||||
task.Commit.Status = model.StatusFailure
|
||||
if buildErr != nil && len(stdout) == 0 {
|
||||
// TODO: If you wanted to have very friendly error messages, you could do that here
|
||||
stdout = fmt.Sprintf("%s\n", buildErr.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// persist the build output
|
||||
if err := w.commits.UpdateOutput(task.Commit, []byte(stdout)); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// persist the commit to the database
|
||||
if err := w.commits.Update(task.Commit); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// notify the channels that the commit and build finished
|
||||
channel.SendJSON(reposlug, task.Commit)
|
||||
//channel.SendJSON(commitslug, task.Commit)
|
||||
channel.Close(consoleslug)
|
||||
|
||||
// send all "finished" notifications
|
||||
//if task.Script.Notifications != nil {
|
||||
// task.Script.Notifications.Send(context)
|
||||
//}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *worker) runBuild(task *BuildTask, buf io.Writer) (bool, error) {
|
||||
var path = filepath.Join(task.Repo.Host, task.Repo.Owner, task.Repo.Name)
|
||||
path = git.GitPath(task.Script.Git, path)
|
||||
|
||||
repo := &repo.Repo{
|
||||
Name: task.Repo.Host + task.Repo.Owner + task.Repo.Name,
|
||||
Path: task.Repo.CloneURL,
|
||||
Branch: task.Commit.Branch,
|
||||
Commit: task.Commit.Sha,
|
||||
PR: task.Commit.PullRequest,
|
||||
//TODO the builder should handle this
|
||||
Dir: filepath.Join("/var/cache/drone/src", path),
|
||||
Depth: git.GitDepth(task.Script.Git),
|
||||
}
|
||||
|
||||
if task.Repo.Private {
|
||||
repo.Path = task.Repo.SSHURL
|
||||
}
|
||||
|
||||
return w.runner.Run(
|
||||
task.Script,
|
||||
repo,
|
||||
[]byte(task.Repo.PrivateKey),
|
||||
task.Repo.Privileged,
|
||||
buf,
|
||||
)
|
||||
}
|
||||
|
||||
// updateGitHubStatus is a helper function that will send
|
||||
// the build status to GitHub using the Status API.
|
||||
// see https://github.com/blog/1227-commit-status-api
|
||||
func updateGitHubStatus(repo *repo.Repo, commit *model.Commit) error {
|
||||
/*
|
||||
// convert from drone status to github status
|
||||
var message, status string
|
||||
switch commit.Status {
|
||||
case "Success":
|
||||
status = "success"
|
||||
message = "The build succeeded on drone.io"
|
||||
case "Failure":
|
||||
status = "failure"
|
||||
message = "The build failed on drone.io"
|
||||
case "Started":
|
||||
status = "pending"
|
||||
message = "The build is pending on drone.io"
|
||||
default:
|
||||
status = "error"
|
||||
message = "The build errored on drone.io"
|
||||
}
|
||||
|
||||
// get the system settings
|
||||
settings, _ := database.GetSettings()
|
||||
|
||||
// get the user from the database
|
||||
// since we need his / her GitHub token
|
||||
user, err := database.GetUser(repo.UserID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
client := github.New(user.GithubToken)
|
||||
client.ApiUrl = settings.GitHubApiUrl
|
||||
buildUrl := getBuildUrl(settings.URL().String(), repo, commit)
|
||||
|
||||
return client.Repos.CreateStatus(repo.Owner, repo.Name, status, buildUrl, message, commit.Hash)
|
||||
*/
|
||||
return nil
|
||||
}
|
||||
|
||||
/*
|
||||
func getBuildUrl(host string, repo *repo.Repo, commit *commit.Commit) string {
|
||||
branchQuery := url.Values{}
|
||||
branchQuery.Set("branch", commit.Branch)
|
||||
buildUrl := fmt.Sprintf("%s/%s/commit/%s?%s", host, repo.Slug, commit.Hash, branchQuery.Encode())
|
||||
return buildUrl
|
||||
}
|
||||
*/
|
||||
|
||||
type bufferWrapper struct {
|
||||
buf bytes.Buffer
|
||||
|
||||
// name of the channel
|
||||
channel string
|
||||
}
|
||||
|
||||
func (b *bufferWrapper) Write(p []byte) (n int, err error) {
|
||||
n, err = b.buf.Write(p)
|
||||
channel.SendBytes(b.channel, p)
|
||||
return
|
||||
}
|
|
@ -1,4 +0,0 @@
|
|||
extends base
|
||||
block content
|
||||
div
|
||||
| 400 BAD REQUEST!
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue