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:
Brad Rydzewski 2014-06-21 14:22:38 -07:00
parent 06dfc269f7
commit 83577a7d5d
138 changed files with 3133 additions and 2760 deletions

3
.gitignore vendored
View file

@ -12,5 +12,4 @@ drone.sublime-workspace
client/client client/client
server/server server/server
debian/drone/usr/local/bin/drone debian/drone/usr
debian/drone/usr/local/bin/droned

View file

@ -1,9 +1,10 @@
SHA := $(shell git rev-parse --short HEAD) SHA := $(shell git rev-parse --short HEAD)
all: rice amberc lessc build all: build
deps: 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 get github.com/GeertJohan/go.rice/rice
go list github.com/drone/drone/... | xargs go get -t -v go list github.com/drone/drone/... | xargs go get -t -v
@ -15,43 +16,29 @@ test:
go vet ./... go vet ./...
go test -cover -short ./... go test -cover -short ./...
run:
@cd server && go run main.go
clean: clean:
@find ./ -name '*.out' | xargs rm # remove go coverage output @find . -name "*.out" -delete # remove go coverage output
@find ./ -name '*.sqlite' | xargs rm # remove sqlite databases @find . -name "*.sqlite" -delete # remove sqlite databases
@find ./ -name '*.rice-box.go' | xargs rm @find . -name '*.rice-box.go' -delete # remove go rice files & embedded content
rm -rf debian/drone/usr/local/bin/drone #@find . -name '*.css' -delete
rm -rf debian/drone/usr/local/bin/droned @rm -r debian/drone/usr/local/bin debian/drone.deb server/server client/client server/template/html
rm -rf debian/drone.deb
rm server/server
rm client/client
#cd cmd/droned/static && rice clean dpkg: lessc rice build deb
#cd cmd/droned/template && rice clean
# embeds content in go source code so that it is compiled
# and packaged inside the go binary file.
rice: rice:
cd server && rice embed 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
lessc: 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: # creates a debian package for drone to install
yui-compressor --type='css' -o 'server/static/styles/drone.min.css' server/static/styles/drone.css # `sudo dpkg -i drone.deb`
deb:
# 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:
mkdir -p debian/drone/usr/local/bin mkdir -p debian/drone/usr/local/bin
-dpkg-deb --build debian/drone dpkg-deb --build debian/drone
run:
@cd server && go run main.go amber.go

View file

@ -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)
}

View file

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

Before

Width:  |  Height:  |  Size: 442 B

After

Width:  |  Height:  |  Size: 442 B

39
server/app/index.html Normal file
View 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
View file

@ -0,0 +1,3 @@
# robotstxt.org
User-agent: *

395
server/app/scripts/app.js Normal file
View 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";
}
}

View 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);
};
})();

View file

@ -0,0 +1 @@
'use strict';

View file

@ -0,0 +1 @@
'use strict';

View 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);
});
});

View file

@ -0,0 +1 @@
'use strict';

View 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);
});
};
});

View 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
};
};
});

View 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);
});
});

View 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>'
}
});

View 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;
}
}
});

View 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;
}
}
});

View 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;
}
}
});

View 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;
}
}
});

View 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, "&lt;");
s = s.replace(/>/g, "&gt;");
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;
}
};
})();

View 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>

View 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>

View 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>

View 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>

View file

@ -0,0 +1 @@
dashboard.html

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View file

@ -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()
}

View file

@ -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()
}

View file

@ -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
View 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
}

View 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)
}
}

View file

@ -85,6 +85,35 @@ var stmts = []string{`
,commit_id INTEGER ,commit_id INTEGER
,output_raw BLOB ,output_raw BLOB
,UNIQUE(commit_id) ,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
View 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
}

View 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)
}
}

View file

@ -34,6 +34,14 @@ var stmts = []string{
// insert commit console output // insert commit console output
"insert into output values (1, 1, 'sample console output');", "insert into output values (1, 1, 'sample console output');",
"insert into output values (2, 2, '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 // Load will populate the database with a fixed dataset for

View file

@ -29,6 +29,9 @@ type UserManager interface {
// Delete removes the User from the datastore. // Delete removes the User from the datastore.
Delete(user *model.User) error 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. // userManager manages a list of users in a SQL database.
@ -65,6 +68,11 @@ const deleteUserStmt = `
DELETE FROM users WHERE user_id=? 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 // NewUserManager initiales a new UserManager intended to
// manage and persist commits. // manage and persist commits.
func NewUserManager(db *sql.DB) UserManager { func NewUserManager(db *sql.DB) UserManager {
@ -110,3 +118,10 @@ func (db *userManager) Delete(user *model.User) error {
_, err := db.Exec(deleteUserStmt, user.ID) _, err := db.Exec(deleteUserStmt, user.ID)
return err return err
} }
func (db *userManager) Exist() bool {
row := db.QueryRow(confirmUserStmt)
var result int
row.Scan(&result)
return result == 1
}

View file

@ -5,8 +5,8 @@ import (
"net/http" "net/http"
"github.com/drone/drone/server/database" "github.com/drone/drone/server/database"
"github.com/drone/drone/server/queue"
"github.com/drone/drone/server/session" "github.com/drone/drone/server/session"
"github.com/drone/drone/server/worker"
"github.com/drone/drone/shared/model" "github.com/drone/drone/shared/model"
"github.com/gorilla/pat" "github.com/gorilla/pat"
) )
@ -16,10 +16,10 @@ type CommitHandler struct {
repos database.RepoManager repos database.RepoManager
commits database.CommitManager commits database.CommitManager
sess session.Session 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} 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 // 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 return nil
} }

View file

@ -4,7 +4,7 @@ import (
"net/http" "net/http"
"github.com/drone/drone/server/database" "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/drone/drone/shared/model"
"github.com/gorilla/pat" "github.com/gorilla/pat"
) )
@ -14,10 +14,10 @@ type HookHandler struct {
repos database.RepoManager repos database.RepoManager
commits database.CommitManager commits database.CommitManager
conf database.ConfigManager 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} 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) //fmt.Printf("%s", yml)
// drop the items on the queue // 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 return nil
} }
func (h *HookHandler) Register(r *pat.Router) { func (h *HookHandler) Register(r *pat.Router) {
r.Post("/hook/{host}", errorHandler(h.PostHook)) r.Post("/v1/hook/{host}", errorHandler(h.PostHook))
r.Put("/hook/{host}", errorHandler(h.PostHook)) r.Put("/v1/hook/{host}", errorHandler(h.PostHook))
} }

View file

@ -48,8 +48,9 @@ func (h *LoginHandler) GetLogin(w http.ResponseWriter, r *http.Request) error {
u, err := h.users.FindLogin(host, login.Login) u, err := h.users.FindLogin(host, login.Login)
if err != nil { if err != nil {
// if self-registration is disabled we should // if self-registration is disabled we should
// return a notAuthorized error // return a notAuthorized error. the only exception
if !h.conf.Find().Registration { // is if no users exist yet in the system we'll proceed.
if h.conf.Find().Registration == false && h.users.Exist() {
return notAuthorized{} return notAuthorized{}
} }

108
server/handler/remote.go Normal file
View 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))
}

View file

@ -92,7 +92,7 @@ func (h *RepoHandler) PostRepo(w http.ResponseWriter, r *http.Request) error {
} }
// post commit hook url // 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 // activate the repository in the remote system
client := remote.GetClient(user.Access, user.Secret) client := remote.GetClient(user.Access, user.Secret)

88
server/handler/server.go Normal file
View 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))
}

View file

@ -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
View 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))
}

View file

@ -3,24 +3,22 @@ package main
import ( import (
"database/sql" "database/sql"
"flag" "flag"
"html/template"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"os" "os"
"os/user" "os/user"
"path/filepath" "path/filepath"
"runtime" "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"
"github.com/drone/drone/server/database/schema" "github.com/drone/drone/server/database/schema"
"github.com/drone/drone/server/handler" "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/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/build/log"
"github.com/drone/drone/shared/model"
"github.com/gorilla/pat" "github.com/gorilla/pat"
//"github.com/justinas/nosurf" //"github.com/justinas/nosurf"
@ -51,10 +49,6 @@ var (
version string = "0.2-dev" version string = "0.2-dev"
revision string 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 // Number of concurrent build workers to run
// default to number of CPUs on machine // default to number of CPUs on machine
workers int workers int
@ -70,25 +64,9 @@ func main() {
flag.StringVar(&datasource, "datasource", "drone.sqlite", "") flag.StringVar(&datasource, "datasource", "drone.sqlite", "")
flag.StringVar(&sslcert, "sslcert", "", "") flag.StringVar(&sslcert, "sslcert", "", "")
flag.StringVar(&sslkey, "sslkey", "", "") flag.StringVar(&sslkey, "sslkey", "", "")
flag.DurationVar(&timeout, "timeout", 300*time.Minute, "")
flag.IntVar(&workers, "workers", runtime.NumCPU(), "") flag.IntVar(&workers, "workers", runtime.NumCPU(), "")
flag.Parse() 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 // setup the database
meddler.Default = meddler.SQLite meddler.Default = meddler.SQLite
db, _ := sql.Open(driver, datasource) db, _ := sql.Open(driver, datasource)
@ -99,14 +77,24 @@ func main() {
users := database.NewUserManager(db) users := database.NewUserManager(db)
perms := database.NewPermManager(db) perms := database.NewPermManager(db)
commits := database.NewCommitManager(db) commits := database.NewCommitManager(db)
servers := database.NewServerManager(db)
remotes := database.NewRemoteManager(db)
configs := database.NewConfigManager(filepath.Join(home, "config.toml")) configs := database.NewConfigManager(filepath.Join(home, "config.toml"))
// message broker
pubsub := pubsub.NewPubSub()
// cancel all previously running builds // cancel all previously running builds
go commits.CancelAll() go commits.CancelAll()
// setup the build queue // setup the build queue
queueRunner := queue.NewBuildRunner(docker.New(), timeout) //queueRunner := queue.NewBuildRunner(docker.New(), timeout)
queue := queue.Start(workers, commits, queueRunner) //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 // setup the session managers
sess := session.NewSession(users) sess := session.NewSession(users)
@ -122,28 +110,51 @@ func main() {
handler.NewRepoHandler(repos, commits, perms, sess, configs).Register(router) handler.NewRepoHandler(repos, commits, perms, sess, configs).Register(router)
handler.NewBadgeHandler(repos, commits).Register(router) handler.NewBadgeHandler(repos, commits).Register(router)
handler.NewConfigHandler(configs, sess).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 box := rice.MustFindBox("app/")
// TODO we need to replace this with go.rice fserver := http.FileServer(box.HTTPBox())
http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(rice.MustFindBox("static/").HTTPBox()))) index, _ := box.Bytes("index.html")
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
// server websocket data switch {
http.Handle("/feed", websocket.Handler(channel.Read)) case strings.HasPrefix(r.URL.Path, "/favicon.ico"),
strings.HasPrefix(r.URL.Path, "/scripts/"),
// register the router strings.HasPrefix(r.URL.Path, "/styles/"),
// TODO we disabled nosurf because it was impacting API calls. strings.HasPrefix(r.URL.Path, "/views/"):
// we need to disable nosurf for api calls (ie not coming from website). fserver.ServeHTTP(w, r)
http.Handle("/", router) 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 // start webserver using HTTPS or HTTP
if len(sslcert) != 0 && len(sslkey) != 0 { if len(sslcert) != 0 {
panic(http.ListenAndServeTLS(port, sslcert, sslkey, nil)) panic(http.ListenAndServeTLS(port, sslcert, sslkey, nil))
} else { } else {
panic(http.ListenAndServe(port, nil)) panic(http.ListenAndServe(port, nil))
} }
} }
func setupDatabase() {
}
func setupQueue() {
}
func setupHandlers() {
}
// initialize the .drone directory and create a skeleton config // initialize the .drone directory and create a skeleton config
// file if one does not already exist. // file if one does not already exist.
func init() { func init() {

30
server/pubsub/buffer.go Normal file
View 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
View 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
View 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
View 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
}

View 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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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 := &notify.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
}

View file

@ -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