1
0
mirror of https://github.com/woodpecker-ci/woodpecker.git synced 2024-11-24 08:02:18 +02:00

updated vendor files and paths

This commit is contained in:
Brad Rydzewski 2015-09-29 18:21:17 -07:00
parent 155576fb03
commit dfea14c7e5
719 changed files with 128749 additions and 34774 deletions

View File

@ -1,13 +0,0 @@
bin/
cmd/drone-server/drone_bindata.go
dist/
doc/
.git/
.dockerignore
.drone.yml
.gitignore
drone.sqlite
Dockerfile
LICENSE
README.md

View File

@ -8,14 +8,12 @@ env:
- PATH=$PATH:$GOROOT/bin:$GOPATH/bin
script:
- go run make.go deps
- go run make.go bindata
- go run make.go vet
- go run make.go fmt
- go run make.go build
- go run make.go test
- make dist
- apt-get -y -qq update
- apt-get -y -qq install libsqlite3-dev
- make deps
- make
- make test
- make deb
notify:
email:
@ -29,34 +27,26 @@ publish:
bucket: downloads.drone.io
access_key: $$AWS_KEY
secret_key: $$AWS_SECRET
source: dist/drone.deb
source: contrib/debian/drone.deb
target: $DRONE_BRANCH/
when:
owner: drone
---
clone:
path: github.com/drone/drone
build:
image: golang:1.5.0
image: golang:1.5
commands:
- export GOPATH=/drone
- export PATH=$PATH:$GOPATH/bin
- go run make.go deps
- go run make.go bindata
- go run make.go vet
- go run make.go fmt
- go run make.go build
- go run make.go test
- make dist
compose:
database:
image: mysql:5.5
environment:
- MYSQL_ALLOW_EMPTY_PASSWORD=yes
- MYSQL_DATABASE=test
- apt-get -y -qq update
- apt-get -y -qq install libsqlite3-dev
- make deps
- make gen
- make test
- make build
- make build_static
- make deb

41
.gitignore vendored
View File

@ -1,34 +1,13 @@
drone.sublime-project
drone.sublime-workspace
.vagrant
*~
~*
drone
drone_*
*.sqlite
*.sqlite3
*.deb
*.deb.*
*.rpm
*.out
*.prof
*.rice-box.go
*.db
*_gen.go
*.html
*.css
*.txt
*.min.css
*.zip
*.gz
*.out
*.min.js
*_bindata.go
*.toml
# generate binaries
cmd/drone-agent/drone-agent
cmd/drone-build/drone-build
cmd/drone-agent/drone-server
# generated binaries in ./bin
bin/drone
bin/drone-agent
bin/drone-build
bin/drone-server
# generated binaries in dpkg
dist/drone/usr/local/bin/drone
*.deb
temp/

View File

@ -1,22 +1,20 @@
FROM golang:1.4.2
# Build the drone executable on a x64 Linux host:
#
# go build --ldflags '-extldflags "-static"' -o drone_static
#
#
# Alternate command for Go 1.4 and older:
#
# go build -a -tags netgo --ldflags '-extldflags "-static"' -o drone_static
#
#
# Build the docker image:
#
# docker build --rm=true -t drone/drone .
ENV DRONE_SERVER_PORT :80
WORKDIR $GOPATH/src/github.com/drone/drone
FROM centurylink/ca-certs
EXPOSE 8080
EXPOSE 80
ADD drone_static /drone_static
ENTRYPOINT ["/usr/local/bin/drone"]
CMD ["-config", "/tmp/drone.toml"]
RUN apt-get update \
&& apt-get install -y libsqlite3-dev \
&& git clone git://github.com/gin-gonic/gin.git $GOPATH/src/github.com/gin-gonic/gin \
&& go get -u github.com/jteeuwen/go-bindata/...
RUN touch /tmp/drone.toml
ADD . .
RUN make bindata deps \
&& make build \
&& mv bin/* /usr/local/bin/ \
&& rm -rf bin cmd/drone-server/drone_bindata.go
ENTRYPOINT ["/drone_static"]

View File

@ -1,26 +0,0 @@
# Docker image for the Drone build runner
#
# docker build --file=Dockerfile.alpine --rm=true -t drone/drone-alpine .
FROM alpine:3.2
EXPOSE 8080
ENV GOROOT=/usr/lib/go \
GOPATH=/gopath \
GOBIN=/gopath/bin \
PATH=$PATH:$GOROOT/bin:$GOPATH/bin
WORKDIR /gopath/src/github.com/drone/drone
ADD . /gopath/src/github.com/drone/drone
RUN apk add -U go ca-certificates libc-dev gcc git sqlite-libs && \
go get github.com/jteeuwen/go-bindata/... && \
/gopath/bin/go-bindata -o="cmd/drone-server/drone_bindata.go" cmd/drone-server/static/... && \
go run make.go build && \
apk del git go gcc libc-dev && \
mv bin/drone /bin/drone && \
rm -rf /gopath && \
rm -rf /var/cache/apk/*
ENTRYPOINT ["/bin/drone"]

View File

@ -1,39 +1,35 @@
.PHONY: dist
.PHONY: vendor
SHA := $(shell git rev-parse --short HEAD)
VERSION := 0.4.0-alpha
PACKAGES = $(shell go list ./... | grep -v /vendor/)
all: build
all: gen build
deps:
go get golang.org/x/tools/cmd/cover
go get golang.org/x/tools/cmd/vet
go get -u github.com/kr/vexp
go get -u github.com/eknkc/amber/amberc
go get -u github.com/jteeuwen/go-bindata/...
go get -u github.com/elazarl/go-bindata-assetfs/...
gen:
go generate $(go list ./... | grep -v /vendor/)
build:
go run make.go bindata build
GO15VENDOREXPERIMENT=1 go build
build_static:
GO15VENDOREXPERIMENT=1 go build --ldflags '-extldflags "-static"' -o drone_static
# Execute the database test suite against mysql 5.5
#
# You can launch a mysql container locally for testing:
# docker run -rm -e MYSQL_ALLOW_EMPTY_PASSWORD=yes -e MYSQL_DATABASE=test -p 3306:3306 mysql:5.5
test_mysql:
mysql -P 3306 --protocol=tcp -u root -e 'create database if not exists test;'
TEST_DRIVER="mysql" TEST_DATASOURCE="root@tcp(127.0.0.1:3306)/test" go test -short github.com/drone/drone/pkg/store/builtin
mysql -P 3306 --protocol=tcp -u root -e 'drop database test;'
test:
go test -cover $(PACKAGES)
run:
bin/drone --debug
deb:
mkdir -p contrib/debian/drone/usr/local/bin
mkdir -p contrib/debian/drone/var/lib/drone
mkdir -p contrib/debian/drone/var/cache/drone
cp drone contrib/debian/drone/usr/local/bin
-dpkg-deb --build contrib/debian/drone
# installs the drone binaries into bin
install:
install -t /usr/local/bin bin/drone
install -t /usr/local/bin bin/drone-agent
docker:
docker build --file=cmd/drone-build/Dockerfile.alpine --rm=true -t drone/drone-build .
# creates a debian package for drone
# to install `sudo dpkg -i drone.deb`
dist:
mkdir -p dist/drone/usr/local/bin
mkdir -p dist/drone/var/lib/drone
mkdir -p dist/drone/var/cache/drone
cp bin/drone dist/drone/usr/local/bin
-dpkg-deb --build dist/drone
vendor:
vexp

View File

@ -1 +0,0 @@
This is where Drone packages go after running "make dist" in the root directory.

View File

@ -0,0 +1,6 @@
// +build ignore
// This program converts amber templates to standard
// Go template files.
package main

60
contrib/generate-js.go Normal file
View File

@ -0,0 +1,60 @@
// +build ignore
// This program minifies JavaScript files
// $ go run generate-js.go -dir scripts/ -out scripts/drone.min.js
package main
import (
"bytes"
"flag"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"github.com/dchest/jsmin"
)
var (
dir = flag.String("dir", "scripts/", "")
out = flag.String("o", "scripts/drone.min.js", "")
)
func main() {
flag.Parse()
var buf bytes.Buffer
// walk the directory tree and write all
// javascript files to the buffer.
filepath.Walk(*dir, func(path string, info os.FileInfo, err error) error {
if filepath.Ext(path) != ".js" {
return nil
}
f, err := os.Open(path)
if err != nil {
return nil
}
defer f.Close()
// write the file name to the minified output
fmt.Fprintf(&buf, "// %s\n", path)
// copy the file to the buffer
_, err = io.Copy(&buf, f)
return err
})
// minifies the javascript
data, err := jsmin.Minify(buf.Bytes())
if err != nil {
fmt.Println(err)
os.Exit(1)
}
// write the minified output
ioutil.WriteFile(*out, data, 0700)
}

11
contrib/setup-sqlite.sh Normal file
View File

@ -0,0 +1,11 @@
#!/bin/bash
cd /tmp
curl -O https://www.sqlite.org/2015/sqlite-autoconf-3081101.tar.gz
tar xzf sqlite-autoconf-3081101.tar.gz
cd sqlite-autoconf-3081101
cd sqlite-3.6.421
./configure -prefix=/scratch/usr/local
make
make install

View File

@ -1,32 +1,29 @@
package server
package controller
import (
"github.com/drone/drone/Godeps/_workspace/src/github.com/gin-gonic/gin"
"github.com/gin-gonic/gin"
"github.com/drone/drone/pkg/ccmenu"
common "github.com/drone/drone/pkg/types"
"github.com/drone/drone/model"
"github.com/drone/drone/router/middleware/context"
)
var (
badgeSuccess = []byte(`<svg xmlns="http://www.w3.org/2000/svg" width="91" height="20"><linearGradient id="a" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><rect rx="3" width="91" height="20" fill="#555"/><rect rx="3" x="37" width="54" height="20" fill="#4c1"/><path fill="#4c1" d="M37 0h4v20h-4z"/><rect rx="3" width="91" height="20" fill="url(#a)"/><g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11"><text x="19.5" y="15" fill="#010101" fill-opacity=".3">build</text><text x="19.5" y="14">build</text><text x="63" y="15" fill="#010101" fill-opacity=".3">success</text><text x="63" y="14">success</text></g></svg>`)
badgeFailure = []byte(`<svg xmlns="http://www.w3.org/2000/svg" width="83" height="20"><linearGradient id="a" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><rect rx="3" width="83" height="20" fill="#555"/><rect rx="3" x="37" width="46" height="20" fill="#e05d44"/><path fill="#e05d44" d="M37 0h4v20h-4z"/><rect rx="3" width="83" height="20" fill="url(#a)"/><g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11"><text x="19.5" y="15" fill="#010101" fill-opacity=".3">build</text><text x="19.5" y="14">build</text><text x="59" y="15" fill="#010101" fill-opacity=".3">failure</text><text x="59" y="14">failure</text></g></svg>`)
badgeStarted = []byte(`<svg xmlns="http://www.w3.org/2000/svg" width="87" height="20"><linearGradient id="a" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><rect rx="3" width="87" height="20" fill="#555"/><rect rx="3" x="37" width="50" height="20" fill="#dfb317"/><path fill="#dfb317" d="M37 0h4v20h-4z"/><rect rx="3" width="87" height="20" fill="url(#a)"/><g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11"><text x="19.5" y="15" fill="#010101" fill-opacity=".3">build</text><text x="19.5" y="14">build</text><text x="61" y="15" fill="#010101" fill-opacity=".3">started</text><text x="61" y="14">started</text></g></svg>`)
badgeError = []byte(`<svg xmlns="http://www.w3.org/2000/svg" width="76" height="20"><linearGradient id="a" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><rect rx="3" width="76" height="20" fill="#555"/><rect rx="3" x="37" width="39" height="20" fill="#9f9f9f"/><path fill="#9f9f9f" d="M37 0h4v20h-4z"/><rect rx="3" width="76" height="20" fill="url(#a)"/><g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11"><text x="19.5" y="15" fill="#010101" fill-opacity=".3">build</text><text x="19.5" y="14">build</text><text x="55.5" y="15" fill="#010101" fill-opacity=".3">error</text><text x="55.5" y="14">error</text></g></svg>`)
badgeNone = []byte(`<svg xmlns="http://www.w3.org/2000/svg" width="75" height="20"><linearGradient id="a" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><rect rx="3" width="75" height="20" fill="#555"/><rect rx="3" x="37" width="38" height="20" fill="#9f9f9f"/><path fill="#9f9f9f" d="M37 0h4v20h-4z"/><rect rx="3" width="75" height="20" fill="url(#a)"/><g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11"><text x="19.5" y="15" fill="#010101" fill-opacity=".3">build</text><text x="19.5" y="14">build</text><text x="55" y="15" fill="#010101" fill-opacity=".3">none</text><text x="55" y="14">none</text></g></svg>`)
badgeSuccess = `<svg xmlns="http://www.w3.org/2000/svg" width="91" height="20"><linearGradient id="a" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><rect rx="3" width="91" height="20" fill="#555"/><rect rx="3" x="37" width="54" height="20" fill="#4c1"/><path fill="#4c1" d="M37 0h4v20h-4z"/><rect rx="3" width="91" height="20" fill="url(#a)"/><g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11"><text x="19.5" y="15" fill="#010101" fill-opacity=".3">build</text><text x="19.5" y="14">build</text><text x="63" y="15" fill="#010101" fill-opacity=".3">success</text><text x="63" y="14">success</text></g></svg>`
badgeFailure = `<svg xmlns="http://www.w3.org/2000/svg" width="83" height="20"><linearGradient id="a" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><rect rx="3" width="83" height="20" fill="#555"/><rect rx="3" x="37" width="46" height="20" fill="#e05d44"/><path fill="#e05d44" d="M37 0h4v20h-4z"/><rect rx="3" width="83" height="20" fill="url(#a)"/><g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11"><text x="19.5" y="15" fill="#010101" fill-opacity=".3">build</text><text x="19.5" y="14">build</text><text x="59" y="15" fill="#010101" fill-opacity=".3">failure</text><text x="59" y="14">failure</text></g></svg>`
badgeStarted = `<svg xmlns="http://www.w3.org/2000/svg" width="87" height="20"><linearGradient id="a" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><rect rx="3" width="87" height="20" fill="#555"/><rect rx="3" x="37" width="50" height="20" fill="#dfb317"/><path fill="#dfb317" d="M37 0h4v20h-4z"/><rect rx="3" width="87" height="20" fill="url(#a)"/><g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11"><text x="19.5" y="15" fill="#010101" fill-opacity=".3">build</text><text x="19.5" y="14">build</text><text x="61" y="15" fill="#010101" fill-opacity=".3">started</text><text x="61" y="14">started</text></g></svg>`
badgeError = `<svg xmlns="http://www.w3.org/2000/svg" width="76" height="20"><linearGradient id="a" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><rect rx="3" width="76" height="20" fill="#555"/><rect rx="3" x="37" width="39" height="20" fill="#9f9f9f"/><path fill="#9f9f9f" d="M37 0h4v20h-4z"/><rect rx="3" width="76" height="20" fill="url(#a)"/><g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11"><text x="19.5" y="15" fill="#010101" fill-opacity=".3">build</text><text x="19.5" y="14">build</text><text x="55.5" y="15" fill="#010101" fill-opacity=".3">error</text><text x="55.5" y="14">error</text></g></svg>`
badgeNone = `<svg xmlns="http://www.w3.org/2000/svg" width="75" height="20"><linearGradient id="a" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><rect rx="3" width="75" height="20" fill="#555"/><rect rx="3" x="37" width="38" height="20" fill="#9f9f9f"/><path fill="#9f9f9f" d="M37 0h4v20h-4z"/><rect rx="3" width="75" height="20" fill="url(#a)"/><g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11"><text x="19.5" y="15" fill="#010101" fill-opacity=".3">build</text><text x="19.5" y="14">build</text><text x="55" y="15" fill="#010101" fill-opacity=".3">none</text><text x="55" y="14">none</text></g></svg>`
)
// GetBadge accepts a request to retrieve the named
// repo and branhes latest build details from the datastore
// and return an SVG badges representing the build results.
//
// GET /api/badge/:owner/:name/status.svg
//
func GetBadge(c *gin.Context) {
var repo = ToRepo(c)
var store = ToDatastore(c)
var branch = c.Request.FormValue("branch")
if len(branch) == 0 {
branch = repo.Branch
db := context.Database(c)
repo, err := model.GetRepoName(db,
c.Param("owner"),
c.Param("name"),
)
if err != nil {
c.AbortWithStatus(404)
return
}
// an SVG response is always served, even when error, so
@ -36,44 +33,48 @@ func GetBadge(c *gin.Context) {
// if no commit was found then display
// the 'none' badge, instead of throwing
// an error response
build, err := store.BuildLast(repo, branch)
branch := c.Query("branch")
if len(branch) == 0 {
branch = repo.Branch
}
build, err := model.GetBuildLast(db, repo, branch)
if err != nil {
c.Writer.Write(badgeNone)
c.String(404, badgeNone)
return
}
switch build.Status {
case common.StateSuccess:
c.Writer.Write(badgeSuccess)
case common.StateFailure:
c.Writer.Write(badgeFailure)
case common.StateError, common.StateKilled:
c.Writer.Write(badgeError)
case common.StatePending, common.StateRunning:
c.Writer.Write(badgeStarted)
case model.StatusSuccess:
c.String(200, badgeSuccess)
case model.StatusFailure:
c.String(200, badgeFailure)
case model.StatusError, model.StatusKilled:
c.String(200, badgeError)
case model.StatusPending, model.StatusRunning:
c.String(200, badgeStarted)
default:
c.Writer.Write(badgeNone)
c.String(404, badgeNone)
}
}
// GetCC accepts a request to retrieve the latest build
// status for the given repository from the datastore and
// in CCTray XML format.
//
// GET /api/badge/:host/:owner/:name/cc.xml
//
// TODO(bradrydzewski) this will not return in-progress builds, which it should
func GetCC(c *gin.Context) {
store := ToDatastore(c)
repo := ToRepo(c)
list, err := store.BuildList(repo, 1, 0)
if err != nil || len(list) == 0 {
db := context.Database(c)
repo, err := model.GetRepoName(db,
c.Param("owner"),
c.Param("name"),
)
if err != nil {
c.AbortWithStatus(404)
return
}
cc := ccmenu.NewCC(repo, list[0])
builds, err := model.GetBuildList(db, repo)
if err != nil || len(builds) == 0 {
c.AbortWithStatus(404)
return
}
c.Writer.Header().Set("Content-Type", "application/xml")
cc := model.NewCC(repo, builds[0], "")
c.XML(200, cc)
}

View File

@ -1,104 +0,0 @@
package server
import (
"database/sql"
"encoding/xml"
"net/http"
"net/url"
"testing"
"github.com/drone/drone/pkg/ccmenu"
"github.com/drone/drone/pkg/server/recorder"
"github.com/drone/drone/pkg/store/mock"
common "github.com/drone/drone/pkg/types"
. "github.com/drone/drone/Godeps/_workspace/src/github.com/franela/goblin"
"github.com/drone/drone/Godeps/_workspace/src/github.com/gin-gonic/gin"
"github.com/drone/drone/Godeps/_workspace/src/github.com/stretchr/testify/mock"
)
var badgeTests = []struct {
branch string
badge []byte
state string
activity string
status string
err error
}{
{"", badgeSuccess, common.StateSuccess, "Sleeping", "Success", nil},
{"master", badgeSuccess, common.StateSuccess, "Sleeping", "Success", nil},
{"", badgeStarted, common.StateRunning, "Building", "Unknown", nil},
{"", badgeError, common.StateError, "Sleeping", "Exception", nil},
{"", badgeError, common.StateKilled, "Sleeping", "Exception", nil},
{"", badgeFailure, common.StateFailure, "Sleeping", "Failure", nil},
{"", badgeNone, "", "", "", sql.ErrNoRows},
}
func TestBadges(t *testing.T) {
store := new(mocks.Store)
url_, _ := url.Parse("http://localhost:8080")
g := Goblin(t)
g.Describe("Badges", func() {
g.It("should serve svg badges", func() {
for _, test := range badgeTests {
rw := recorder.New()
ctx := &gin.Context{Engine: gin.Default(), Writer: rw}
ctx.Request = &http.Request{
Form: url.Values{},
}
if len(test.branch) != 0 {
ctx.Request.Form.Set("branch", test.branch)
}
repo := &common.Repo{FullName: "foo/bar"}
ctx.Set("datastore", store)
ctx.Set("repo", repo)
commit := &common.Build{Status: test.state}
store.On("BuildLast", repo, test.branch).Return(commit, test.err).Once()
GetBadge(ctx)
g.Assert(rw.Code).Equal(200)
g.Assert(rw.Body.Bytes()).Equal(test.badge)
g.Assert(rw.HeaderMap.Get("Content-Type")).Equal("image/svg+xml")
}
})
g.It("should serve ccmenu xml", func() {
for _, test := range badgeTests {
rw := recorder.New()
ctx := &gin.Context{Engine: gin.Default(), Writer: rw}
ctx.Request = &http.Request{URL: url_}
repo := &common.Repo{FullName: "foo/bar"}
ctx.Set("datastore", store)
ctx.Set("repo", repo)
commits := []*common.Build{
&common.Build{Status: test.state},
}
store.On("BuildList", repo, mock.AnythingOfType("int"), mock.AnythingOfType("int")).Return(commits, test.err).Once()
GetCC(ctx)
// in an error scenario (ie no build exists) we should
// return a 404 not found error.
if test.err != nil {
g.Assert(rw.Status()).Equal(404)
continue
}
// else parse the CCMenu xml output and verify
// it matches the expected values.
cc := &ccmenu.CCProjects{}
xml.Unmarshal(rw.Body.Bytes(), cc)
g.Assert(rw.Code).Equal(200)
g.Assert(cc.Project.Activity).Equal(test.activity)
g.Assert(cc.Project.LastBuildStatus).Equal(test.status)
g.Assert(rw.HeaderMap.Get("Content-Type")).Equal("application/xml; charset=utf-8")
}
})
})
}

199
controller/build.go Normal file
View File

@ -0,0 +1,199 @@
package controller
import (
"io"
"net/http"
"os"
"strconv"
"strings"
log "github.com/Sirupsen/logrus"
"github.com/drone/drone/engine"
"github.com/drone/drone/shared/httputil"
"github.com/gin-gonic/gin"
"github.com/drone/drone/model"
"github.com/drone/drone/router/middleware/context"
"github.com/drone/drone/router/middleware/session"
)
func GetBuilds(c *gin.Context) {
repo := session.Repo(c)
db := context.Database(c)
builds, err := model.GetBuildList(db, repo)
if err != nil {
c.AbortWithStatus(http.StatusInternalServerError)
return
}
c.IndentedJSON(http.StatusOK, builds)
}
func GetBuild(c *gin.Context) {
repo := session.Repo(c)
db := context.Database(c)
num, err := strconv.Atoi(c.Param("number"))
if err != nil {
c.AbortWithError(http.StatusBadRequest, err)
return
}
build, err := model.GetBuildNumber(db, repo, num)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
jobs, _ := model.GetJobList(db, build)
out := struct {
*model.Build
Jobs []*model.Job `json:"jobs"`
}{build, jobs}
c.IndentedJSON(http.StatusOK, &out)
}
func GetBuildLogs(c *gin.Context) {
repo := session.Repo(c)
db := context.Database(c)
// the user may specify to stream the full logs,
// or partial logs, capped at 2MB.
full, _ := strconv.ParseBool(c.Params.ByName("full"))
// parse the build number and job sequence number from
// the repquest parameter.
num, _ := strconv.Atoi(c.Params.ByName("number"))
seq, _ := strconv.Atoi(c.Params.ByName("job"))
build, err := model.GetBuildNumber(db, repo, num)
if err != nil {
c.AbortWithError(404, err)
return
}
job, err := model.GetJobNumber(db, build, seq)
if err != nil {
c.AbortWithError(404, err)
return
}
r, err := model.GetLog(db, job)
if err != nil {
c.AbortWithError(404, err)
return
}
defer r.Close()
if full {
io.Copy(c.Writer, r)
} else {
io.Copy(c.Writer, io.LimitReader(r, 2000000))
}
}
func DeleteBuild(c *gin.Context) {
c.String(http.StatusOK, "DeleteBuild")
}
func PostBuild(c *gin.Context) {
remote := context.Remote(c)
repo := session.Repo(c)
db := context.Database(c)
num, err := strconv.Atoi(c.Param("number"))
if err != nil {
c.AbortWithError(http.StatusBadRequest, err)
return
}
user, err := model.GetUser(db, repo.UserID)
if err != nil {
log.Errorf("failure to find repo owner %s. %s", repo.FullName, err)
c.AbortWithError(500, err)
return
}
build, err := model.GetBuildNumber(db, repo, num)
if err != nil {
log.Errorf("failure to get build %d. %s", num, err)
c.AbortWithError(404, err)
return
}
// fetch the .drone.yml file from the database
raw, sec, err := remote.Script(user, repo, build)
if err != nil {
log.Errorf("failure to get .drone.yml for %s. %s", repo.FullName, err)
c.AbortWithError(404, err)
return
}
key, _ := model.GetKey(db, repo)
netrc, err := remote.Netrc(user, repo)
if err != nil {
log.Errorf("failure to generate netrc for %s. %s", repo.FullName, err)
c.AbortWithError(500, err)
return
}
jobs, err := model.GetJobList(db, build)
if err != nil {
log.Errorf("failure to get build %d jobs. %s", build.Number, err)
c.AbortWithError(404, err)
return
}
// must not restart a running build
if build.Status == model.StatusPending || build.Status == model.StatusRunning {
c.AbortWithStatus(409)
return
}
tx, err := db.Begin()
if err != nil {
c.AbortWithStatus(500)
return
}
defer tx.Rollback()
build.Status = model.StatusPending
build.Started = 0
build.Finished = 0
for _, job := range jobs {
job.Status = model.StatusPending
job.Started = 0
job.Finished = 0
job.ExitCode = 0
model.UpdateJob(db, job)
}
err = model.UpdateBuild(db, build)
if err != nil {
c.AbortWithStatus(500)
return
}
tx.Commit()
c.JSON(202, build)
engine_ := context.Engine(c)
go engine_.Schedule(&engine.Task{
User: user,
Repo: repo,
Build: build,
Jobs: jobs,
Keys: key,
Netrc: netrc,
Config: string(raw),
Secret: string(sec),
System: &model.System{
Link: httputil.GetURL(c.Request),
Plugins: strings.Split(os.Getenv("PLUGIN_FILTER"), " "),
Globals: strings.Split(os.Getenv("PLUGIN_PARAMS"), " "),
},
})
}

View File

@ -1,285 +0,0 @@
package server
import (
"fmt"
"io"
"os"
"strconv"
"strings"
"time"
"github.com/drone/drone/Godeps/_workspace/src/github.com/gin-gonic/gin"
"github.com/drone/drone/pkg/queue"
common "github.com/drone/drone/pkg/types"
"github.com/drone/drone/pkg/utils/httputil"
)
// GetCommit accepts a request to retrieve a commit
// from the datastore for the given repository and
// commit sequence.
//
// GET /api/repos/:owner/:name/:number
//
func GetBuild(c *gin.Context) {
store := ToDatastore(c)
repo := ToRepo(c)
num, err := strconv.Atoi(c.Params.ByName("number"))
if err != nil {
c.Fail(400, err)
return
}
build, err := store.BuildNumber(repo, num)
if err != nil {
c.Fail(404, err)
return
}
build.Jobs, err = store.JobList(build)
if err != nil {
c.Fail(404, err)
} else {
c.JSON(200, build)
}
}
// GetCommits accepts a request to retrieve a list
// of commits from the datastore for the given repository.
//
// GET /api/repos/:owner/:name/builds
//
func GetBuilds(c *gin.Context) {
store := ToDatastore(c)
repo := ToRepo(c)
builds, err := store.BuildList(repo, 20, 0)
if err != nil {
c.Fail(404, err)
} else {
c.JSON(200, builds)
}
}
// GetLogs accepts a request to retrieve logs from the
// datastore for the given repository, build and task
// number.
//
// GET /api/repos/:owner/:name/logs/:number/:task
//
func GetLogs(c *gin.Context) {
store := ToDatastore(c)
repo := ToRepo(c)
full, _ := strconv.ParseBool(c.Params.ByName("full"))
build, _ := strconv.Atoi(c.Params.ByName("number"))
job, _ := strconv.Atoi(c.Params.ByName("task"))
path := fmt.Sprintf("/logs/%s/%v/%v", repo.FullName, build, job)
r, err := store.GetBlobReader(path)
if err != nil {
c.Fail(404, err)
return
}
defer r.Close()
if full {
io.Copy(c.Writer, r)
} else {
io.Copy(c.Writer, io.LimitReader(r, 2000000))
}
}
// // PostBuildStatus accepts a request to create a new build
// // status. The created user status is returned in JSON
// // format if successful.
// //
// // POST /api/repos/:owner/:name/status/:number
// //
// func PostBuildStatus(c *gin.Context) {
// store := ToDatastore(c)
// repo := ToRepo(c)
// num, err := strconv.Atoi(c.Params.ByName("number"))
// if err != nil {
// c.Fail(400, err)
// return
// }
// in := &common.Status{}
// if !c.BindWith(in, binding.JSON) {
// c.AbortWithStatus(400)
// return
// }
// if err := store.SetBuildStatus(repo.Name, num, in); err != nil {
// c.Fail(400, err)
// } else {
// c.JSON(201, in)
// }
// }
// RunBuild accepts a request to restart an existing build.
//
// POST /api/builds/:owner/:name/builds/:number
//
func RunBuild(c *gin.Context) {
remote := ToRemote(c)
store := ToDatastore(c)
queue_ := ToQueue(c)
repo := ToRepo(c)
num, err := strconv.Atoi(c.Params.ByName("number"))
if err != nil {
c.Fail(400, err)
return
}
build, err := store.BuildNumber(repo, num)
if err != nil {
c.Fail(404, err)
return
}
build.Jobs, err = store.JobList(build)
if err != nil {
c.Fail(404, err)
return
}
user, err := store.User(repo.UserID)
if err != nil {
c.Fail(404, err)
return
}
// must not restart a running build
if build.Status == common.StatePending || build.Status == common.StateRunning {
c.AbortWithStatus(409)
return
}
build.Status = common.StatePending
build.Started = 0
build.Finished = 0
for _, job := range build.Jobs {
job.Status = common.StatePending
job.Started = 0
job.Finished = 0
job.ExitCode = 0
}
err = store.SetBuild(build)
if err != nil {
c.Fail(500, err)
return
}
netrc, err := remote.Netrc(user, repo)
if err != nil {
c.Fail(500, err)
return
}
// featch the .drone.yml file from the database
raw, sec, err := remote.Script(user, repo, build)
if err != nil {
c.Fail(404, err)
return
}
// get the previous build so taht we can send
// on status change notifications
last, _ := store.BuildLast(repo, build.Commit.Branch)
c.JSON(202, build)
queue_.Publish(&queue.Work{
User: user,
Repo: repo,
Build: build,
BuildPrev: last,
Keys: repo.Keys,
Netrc: netrc,
Config: raw,
Secret: sec,
System: &common.System{
Link: httputil.GetURL(c.Request),
Plugins: strings.Split(os.Getenv("PLUGIN_FILTER"), " "),
Globals: strings.Split(os.Getenv("PLUGIN_PARAMS"), " "),
},
})
}
// KillBuild accepts a request to kill a running build.
//
// DELETE /api/builds/:owner/:name/builds/:number
//
func KillBuild(c *gin.Context) {
runner := ToRunner(c)
queue := ToQueue(c)
store := ToDatastore(c)
repo := ToRepo(c)
num, err := strconv.Atoi(c.Params.ByName("number"))
if err != nil {
c.Fail(400, err)
return
}
build, err := store.BuildNumber(repo, num)
if err != nil {
c.Fail(404, err)
return
}
build.Jobs, err = store.JobList(build)
if err != nil {
c.Fail(404, err)
return
}
// must not restart a running build
if build.Status != common.StatePending && build.Status != common.StateRunning {
c.Fail(409, err)
return
}
// remove from the queue if exists
//
// TODO(bradrydzewski) this could yield a race condition
// because other threads may also be accessing these items.
for _, item := range queue.Items() {
if item.Repo.FullName == repo.FullName && item.Build.Number == build.Number {
queue.Remove(item)
break
}
}
build.Status = common.StateKilled
build.Finished = time.Now().Unix()
if build.Started == 0 {
build.Started = build.Finished
}
for _, job := range build.Jobs {
if job.Status != common.StatePending && job.Status != common.StateRunning {
continue
}
job.Status = common.StateKilled
job.Started = build.Started
job.Finished = build.Finished
}
err = store.SetBuild(build)
if err != nil {
c.Fail(500, err)
return
}
for _, job := range build.Jobs {
runner.Cancel(job)
}
// // get the agent from the repository so we can
// // notify the agent to kill the build.
// agent, err := store.BuildAgent(repo.FullName, build.Number)
// if err != nil {
// c.JSON(200, build)
// return
// }
// url_, _ := url.Parse("http://" + agent.Addr)
// url_.Path = fmt.Sprintf("/cancel/%s/%v", repo.FullName, build.Number)
// resp, err := http.Post(url_.String(), "application/json", nil)
// if err != nil {
// c.Fail(500, err)
// return
// }
// defer resp.Body.Close()
c.JSON(200, build)
}

View File

@ -1,158 +1,105 @@
package server
package controller
import (
"fmt"
"strconv"
"net/http"
"github.com/drone/drone/Godeps/_workspace/src/github.com/gin-gonic/gin"
"github.com/gin-gonic/gin"
"github.com/drone/drone/pkg/token"
"github.com/drone/drone/model"
"github.com/drone/drone/router/middleware/context"
"github.com/drone/drone/router/middleware/session"
"github.com/drone/drone/shared/token"
)
// RedirectSha accepts a request to retvie a redirect
// to job from the datastore for the given repository
// and commit sha
//
// GET /gitlab/:owner/:name/redirect/commits/:sha
//
// REASON: It required by GitLab, becuase we get only
// sha and ref name, but drone uses build numbers
func RedirectSha(c *gin.Context) {
var branch string
store := ToDatastore(c)
repo := ToRepo(c)
sha := c.Params.ByName("sha")
branch = c.Request.FormValue("branch")
if branch == "" {
branch = repo.Branch
}
build, err := store.BuildSha(repo, sha, branch)
if err != nil {
c.Redirect(301, "/")
return
}
c.Redirect(301, fmt.Sprintf("/%s/%s/%d", repo.Owner, repo.Name, build.Number))
return
}
// RedirectPullRequest accepts a request to retvie a redirect
// to job from the datastore for the given repository
// and pull request number
//
// GET /gitlab/:owner/:name/redirect/pulls/:number
//
// REASON: It required by GitLab, because we get only
// internal merge request id/ref/sha, but drone uses
// build numbers
func RedirectPullRequest(c *gin.Context) {
store := ToDatastore(c)
repo := ToRepo(c)
num, err := strconv.Atoi(c.Params.ByName("number"))
if err != nil {
c.Redirect(301, "/")
return
}
build, err := store.BuildPullRequestNumber(repo, num)
if err != nil {
c.Redirect(301, "/")
return
}
c.Redirect(301, fmt.Sprintf("/%s/%s/%d", repo.Owner, repo.Name, build.Number))
return
}
// GetPullRequest accepts a requests to retvie a pull request
// from the datastore for the given repository and
// pull request number
//
// GET /gitlab/:owner/:name/pulls/:number
//
// REASON: It required by GitLab, becuase we get only
// sha and ref name, but drone uses build numbers
func GetPullRequest(c *gin.Context) {
store := ToDatastore(c)
repo := ToRepo(c)
parsed, err := token.ParseRequest(c.Request, func(t *token.Token) (string, error) {
return repo.Hash, nil
})
if err != nil {
c.Fail(400, err)
return
}
if parsed.Text != repo.FullName {
c.AbortWithStatus(403)
return
}
num, err := strconv.Atoi(c.Params.ByName("number"))
if err != nil {
c.Fail(400, err)
return
}
build, err := store.BuildPullRequestNumber(repo, num)
if err != nil {
c.Fail(404, err)
return
}
build.Jobs, err = store.JobList(build)
if err != nil {
c.Fail(404, err)
} else {
c.JSON(200, build)
}
}
// GetCommit accepts a requests to retvie a sha and branch
// from the datastore for the given repository and
// pull request number
//
// GET /gitlab/:owner/:name/commits/:sha
//
// REASON: It required by GitLab, becuase we get only
// sha and ref name, but drone uses build numbers
func GetCommit(c *gin.Context) {
var branch string
store := ToDatastore(c)
repo := ToRepo(c)
sha := c.Params.ByName("sha")
db := context.Database(c)
repo := session.Repo(c)
parsed, err := token.ParseRequest(c.Request, func(t *token.Token) (string, error) {
return repo.Hash, nil
})
if err != nil {
c.Fail(400, err)
c.AbortWithError(http.StatusBadRequest, err)
return
}
if parsed.Text != repo.FullName {
c.AbortWithStatus(403)
c.AbortWithStatus(http.StatusUnauthorized)
return
}
branch = c.Request.FormValue("branch")
if branch == "" {
commit := c.Param("sha")
branch := c.Query("branch")
if len(branch) == 0 {
branch = repo.Branch
}
build, err := store.BuildSha(repo, sha, branch)
build, err := model.GetBuildCommit(db, repo, commit, branch)
if err != nil {
c.Fail(404, err)
c.AbortWithError(http.StatusNotFound, err)
return
}
build.Jobs, err = store.JobList(build)
c.JSON(http.StatusOK, build)
}
func GetPullRequest(c *gin.Context) {
db := context.Database(c)
repo := session.Repo(c)
refs := fmt.Sprintf("refs/pull/%s/head", c.Param("number"))
parsed, err := token.ParseRequest(c.Request, func(t *token.Token) (string, error) {
return repo.Hash, nil
})
if err != nil {
c.Fail(404, err)
} else {
c.JSON(200, build)
c.AbortWithError(http.StatusBadRequest, err)
return
}
if parsed.Text != repo.FullName {
c.AbortWithStatus(http.StatusUnauthorized)
return
}
return
build, err := model.GetBuildRef(db, repo, refs)
if err != nil {
c.AbortWithError(http.StatusNotFound, err)
return
}
c.JSON(http.StatusOK, build)
}
func RedirectSha(c *gin.Context) {
db := context.Database(c)
repo := session.Repo(c)
commit := c.Param("sha")
branch := c.Query("branch")
if len(branch) == 0 {
branch = repo.Branch
}
build, err := model.GetBuildCommit(db, repo, commit, branch)
if err != nil {
c.AbortWithError(http.StatusNotFound, err)
return
}
path := fmt.Sprintf("/%s/%s/%d", repo.Owner, repo.Name, build.Number)
c.Redirect(http.StatusSeeOther, path)
}
func RedirectPullRequest(c *gin.Context) {
db := context.Database(c)
repo := session.Repo(c)
refs := fmt.Sprintf("refs/pull/%s/head", c.Param("number"))
build, err := model.GetBuildRef(db, repo, refs)
if err != nil {
c.AbortWithError(http.StatusNotFound, err)
return
}
path := fmt.Sprintf("/%s/%s/%d", repo.Owner, repo.Name, build.Number)
c.Redirect(http.StatusSeeOther, path)
}

View File

@ -1,40 +1,37 @@
package server
package controller
import (
"fmt"
"github.com/gin-gonic/gin"
"os"
"path/filepath"
"strings"
log "github.com/drone/drone/Godeps/_workspace/src/github.com/Sirupsen/logrus"
"github.com/drone/drone/Godeps/_workspace/src/github.com/gin-gonic/gin"
"github.com/drone/drone/pkg/queue"
"github.com/drone/drone/pkg/token"
common "github.com/drone/drone/pkg/types"
"github.com/drone/drone/pkg/utils/httputil"
"github.com/drone/drone/pkg/yaml"
"github.com/drone/drone/pkg/yaml/matrix"
log "github.com/Sirupsen/logrus"
"github.com/drone/drone/engine"
"github.com/drone/drone/model"
"github.com/drone/drone/router/middleware/context"
"github.com/drone/drone/shared/httputil"
"github.com/drone/drone/shared/token"
"github.com/drone/drone/yaml"
"github.com/drone/drone/yaml/matrix"
)
// PostHook accepts a post-commit hook and parses the payload
// in order to trigger a build.
//
// GET /api/hook
//
func PostHook(c *gin.Context) {
remote := ToRemote(c)
store := ToDatastore(c)
queue_ := ToQueue(c)
remote := context.Remote(c)
db := context.Database(c)
hook, err := remote.Hook(c.Request)
tmprepo, build, err := remote.Hook(c.Request)
if err != nil {
log.Errorf("failure to parse hook. %s", err)
c.Fail(400, err)
c.AbortWithError(400, err)
return
}
if hook == nil {
if build == nil {
c.Writer.WriteHeader(200)
return
}
if hook.Repo == nil {
if tmprepo == nil {
log.Errorf("failure to ascertain repo from hook.")
c.Writer.WriteHeader(400)
return
@ -42,16 +39,16 @@ func PostHook(c *gin.Context) {
// a build may be skipped if the text [CI SKIP]
// is found inside the commit message
if hook.Commit != nil && strings.Contains(hook.Commit.Message, "[CI SKIP]") {
if strings.Contains(build.Message, "[CI SKIP]") {
log.Infof("ignoring hook. [ci skip] found for %s")
c.Writer.WriteHeader(204)
return
}
repo, err := store.RepoName(hook.Repo.Owner, hook.Repo.Name)
repo, err := model.GetRepoName(db, tmprepo.Owner, tmprepo.Name)
if err != nil {
log.Errorf("failure to find repo %s/%s from hook. %s", hook.Repo.Owner, hook.Repo.Name, err)
c.Fail(404, err)
log.Errorf("failure to find repo %s/%s from hook. %s", tmprepo.Owner, tmprepo.Name, err)
c.AbortWithError(404, err)
return
}
@ -61,7 +58,7 @@ func PostHook(c *gin.Context) {
})
if err != nil {
log.Errorf("failure to parse token from hook for %s. %s", repo.FullName, err)
c.Fail(400, err)
c.AbortWithError(400, err)
return
}
if parsed.Text != repo.FullName {
@ -70,106 +67,134 @@ func PostHook(c *gin.Context) {
return
}
switch {
case repo.UserID == 0:
if repo.UserID == 0 {
log.Warnf("ignoring hook. repo %s has no owner.", repo.FullName)
c.Writer.WriteHeader(204)
return
case !repo.Hooks.Push && hook.Commit != nil:
log.Infof("ignoring hook. repo %s is disabled.", repo.FullName)
c.Writer.WriteHeader(204)
return
case !repo.Hooks.PullRequest && hook.PullRequest != nil:
log.Warnf("ignoring hook. repo %s is disabled for pull requests.", repo.FullName)
}
var skipped = true
if (build.Event == model.EventPush && repo.AllowPush) ||
(build.Event == model.EventPull && repo.AllowPull) ||
(build.Event == model.EventDeploy && repo.AllowDeploy) ||
(build.Event == model.EventTag && repo.AllowTag) {
skipped = false
}
if skipped {
log.Infof("ignoring hook. repo %s is disabled for %s events.", repo.FullName, build.Event)
c.Writer.WriteHeader(204)
return
}
user, err := store.User(repo.UserID)
user, err := model.GetUser(db, repo.UserID)
if err != nil {
log.Errorf("failure to find repo owner %s. %s", repo.FullName, err)
c.Fail(500, err)
c.AbortWithError(500, err)
return
}
build := &common.Build{}
build.Commit = hook.Commit
build.PullRequest = hook.PullRequest
build.Status = common.StatePending
build.RepoID = repo.ID
// fetch the .drone.yml file from the database
raw, sec, err := remote.Script(user, repo, build)
if err != nil {
log.Errorf("failure to get .drone.yml for %s. %s", repo.FullName, err)
c.Fail(404, err)
c.AbortWithError(404, err)
return
}
axes, err := matrix.Parse(string(raw))
if err != nil {
log.Errorf("failure to calculate matrix for %s. %s", repo.FullName, err)
c.Fail(400, err)
c.AbortWithError(400, err)
return
}
if len(axes) == 0 {
axes = append(axes, matrix.Axis{})
}
for num, axis := range axes {
build.Jobs = append(build.Jobs, &common.Job{
BuildID: build.ID,
Number: num + 1,
Status: common.StatePending,
Environment: axis,
})
}
netrc, err := remote.Netrc(user, repo)
if err != nil {
log.Errorf("failure to generate netrc for %s. %s", repo.FullName, err)
c.Fail(500, err)
c.AbortWithError(500, err)
return
}
key, _ := model.GetKey(db, repo)
// verify the branches can be built vs skipped
when, _ := parser.ParseCondition(string(raw))
if build.PullRequest != nil && when != nil && !when.MatchBranch(build.Commit.Branch) {
log.Infof("ignoring hook. yaml file excludes repo and branch %s %s", repo.FullName, build.Commit.Branch)
yconfig, _ := yaml.Parse(string(raw))
var match = false
for _, branch := range yconfig.Branches {
if branch == build.Branch {
match = true
break
}
match, _ = filepath.Match(branch, build.Branch)
if match {
break
}
}
if !match && len(yconfig.Branches) != 0 {
log.Infof("ignoring hook. yaml file excludes repo and branch %s %s", repo.FullName, build.Branch)
c.AbortWithStatus(200)
return
}
err = store.AddBuild(build)
tx, err := db.Begin()
if err != nil {
log.Errorf("failure to save commit for %s. %s", repo.FullName, err)
c.Fail(500, err)
log.Errorf("failure to begin database transaction", err)
c.AbortWithError(500, err)
return
}
defer tx.Rollback()
// update some build fields
build.Status = model.StatusPending
build.RepoID = repo.ID
var jobs []*model.Job
for num, axis := range axes {
jobs = append(jobs, &model.Job{
BuildID: build.ID,
Number: num + 1,
Status: model.StatusPending,
Environment: axis,
})
}
err = model.CreateBuild(tx, build, jobs...)
if err != nil {
log.Errorf("failure to save commit for %s. %s", repo.FullName, err)
c.AbortWithError(500, err)
return
}
tx.Commit()
c.JSON(200, build)
err = remote.Status(user, repo, build)
url := fmt.Sprintf("%s/%s/%d", httputil.GetURL(c.Request), repo.FullName, build.Number)
err = remote.Status(user, repo, build, url)
if err != nil {
log.Errorf("error setting commit status for %s/%d", repo.FullName, build.Number)
}
// get the previous build so taht we can send
// on status change notifications
last, _ := store.BuildLast(repo, build.Commit.Branch)
last, _ := model.GetBuildLast(db, repo, build.Branch)
queue_.Publish(&queue.Work{
engine_ := context.Engine(c)
go engine_.Schedule(&engine.Task{
User: user,
Repo: repo,
Build: build,
BuildPrev: last,
Keys: repo.Keys,
Jobs: jobs,
Keys: key,
Netrc: netrc,
Config: raw,
Secret: sec,
System: &common.System{
Config: string(raw),
Secret: string(sec),
System: &model.System{
Link: httputil.GetURL(c.Request),
Plugins: strings.Split(os.Getenv("PLUGIN_FILTER"), " "),
Globals: strings.Split(os.Getenv("PLUGIN_PARAMS"), " "),
},
})
}

199
controller/index.go Normal file
View File

@ -0,0 +1,199 @@
package controller
import (
"net/http"
"strconv"
"time"
"github.com/gin-gonic/gin"
"github.com/drone/drone/model"
"github.com/drone/drone/router/middleware/context"
"github.com/drone/drone/router/middleware/session"
"github.com/drone/drone/shared/httputil"
"github.com/drone/drone/shared/token"
)
func ShowIndex(c *gin.Context) {
// remote := context.Remote(c)
user := session.User(c)
if user == nil {
c.HTML(200, "login.html", gin.H{})
return
}
// attempt to get the repository list from the
// cache since the operation is expensive
// v, ok := cache.Get(user.Login)
// if ok {
// c.HTML(200, "repos.html", gin.H{
// "User": user,
// "Repos": v,
// })
// return
// }
// fetch the repmote repos
// repos, err := remote.Repos(user)
// if err != nil {
// c.AbortWithStatus(http.StatusInternalServerError)
// return
// }
// cache.Add(user.Login, repos)
c.HTML(200, "repos.html", gin.H{
"User": user,
// "Repos": repos,
})
}
func ShowLogin(c *gin.Context) {
c.HTML(200, "login.html", gin.H{"Error": c.Query("error")})
}
func ShowUser(c *gin.Context) {
user := session.User(c)
token, _ := token.New(
token.CsrfToken,
user.Login,
).Sign(user.Hash)
c.HTML(200, "user.html", gin.H{
"User": user,
"Csrf": token,
})
}
func ShowUsers(c *gin.Context) {
db := context.Database(c)
user := session.User(c)
if !user.Admin {
c.AbortWithStatus(http.StatusForbidden)
return
}
users, _ := model.GetUserList(db)
token, _ := token.New(
token.CsrfToken,
user.Login,
).Sign(user.Hash)
c.HTML(200, "users.html", gin.H{
"User": user,
"Users": users,
"Csrf": token,
})
}
func ShowRepo(c *gin.Context) {
db := context.Database(c)
user := session.User(c)
repo := session.Repo(c)
if !user.Admin {
c.AbortWithStatus(http.StatusForbidden)
return
}
builds, _ := model.GetBuildList(db, repo)
groups := []*model.BuildGroup{}
var curr *model.BuildGroup
for _, build := range builds {
date := time.Unix(build.Created, 0).Format("Jan 2 2006")
if curr == nil || curr.Date != date {
curr = &model.BuildGroup{}
curr.Date = date
groups = append(groups, curr)
}
curr.Builds = append(curr.Builds, build)
}
httputil.SetCookie(c.Writer, c.Request, "user_last", repo.FullName)
c.HTML(200, "repo.html", gin.H{
"User": user,
"Repo": repo,
"Builds": builds,
"Groups": groups,
})
}
func ShowRepoConf(c *gin.Context) {
db := context.Database(c)
user := session.User(c)
repo := session.Repo(c)
key, _ := model.GetKey(db, repo)
if !user.Admin {
c.AbortWithStatus(http.StatusForbidden)
return
}
var view = "repo_config.html"
switch c.Param("action") {
case "delete":
view = "repo_delete.html"
case "encrypt":
view = "repo_secret.html"
case "badges":
view = "repo_badge.html"
}
token, _ := token.New(
token.CsrfToken,
user.Login,
).Sign(user.Hash)
c.HTML(200, view, gin.H{
"User": user,
"Repo": repo,
"Key": key,
"Csrf": token,
"Link": httputil.GetURL(c.Request),
})
}
func ShowBuild(c *gin.Context) {
db := context.Database(c)
user := session.User(c)
repo := session.Repo(c)
num, _ := strconv.Atoi(c.Param("number"))
seq, _ := strconv.Atoi(c.Param("job"))
if seq == 0 {
seq = 1
}
build, err := model.GetBuildNumber(db, repo, num)
if err != nil {
c.AbortWithError(404, err)
return
}
jobs, err := model.GetJobList(db, build)
if err != nil {
c.AbortWithError(404, err)
return
}
var job *model.Job
for _, j := range jobs {
if j.Number == seq {
job = j
break
}
}
httputil.SetCookie(c.Writer, c.Request, "user_last", repo.FullName)
token, _ := token.New(
token.CsrfToken,
user.Login,
).Sign(user.Hash)
c.HTML(200, "build.html", gin.H{
"User": user,
"Repo": repo,
"Build": build,
"Jobs": jobs,
"Job": job,
"Csrf": token,
})
}

View File

@ -1,82 +1,72 @@
package server
package controller
import (
"net/http"
"time"
"github.com/drone/drone/Godeps/_workspace/src/github.com/gin-gonic/gin"
"github.com/drone/drone/Godeps/_workspace/src/github.com/ungerik/go-gravatar"
"github.com/gin-gonic/gin"
log "github.com/drone/drone/Godeps/_workspace/src/github.com/Sirupsen/logrus"
"github.com/drone/drone/pkg/token"
"github.com/drone/drone/pkg/types"
log "github.com/Sirupsen/logrus"
"github.com/drone/drone/model"
"github.com/drone/drone/router/middleware/context"
"github.com/drone/drone/shared/crypto"
"github.com/drone/drone/shared/httputil"
"github.com/drone/drone/shared/token"
)
// GetLogin accepts a request to authorize the user and to
// return a valid OAuth2 access token. The access token is
// returned as url segment #access_token
//
// GET /authorize
//
func GetLogin(c *gin.Context) {
remote := ToRemote(c)
store := ToDatastore(c)
db := context.Database(c)
remote := context.Remote(c)
// when dealing with redirects we may need
// to adjust the content type. I cannot, however,
// rememver why, so need to revisit this line.
c.Writer.Header().Del("Content-Type")
// TODO: move this back to the remote section
getLoginOauth2(c)
// exit if authorization fails
if c.Writer.Status() != 200 {
tmpuser, open, err := remote.Login(c.Writer, c.Request)
if err != nil {
log.Errorf("cannot authenticate user. %s", err)
c.Redirect(303, "/login?error=oauth_error")
return
}
// this will happen when the user is redirected by
// the remote provide as part of the oauth dance.
if tmpuser == nil {
return
}
login := ToUser(c)
// check organization membership, if applicable
if len(remote.GetOrgs()) != 0 {
orgs, _ := remote.Orgs(login)
if !checkMembership(orgs, remote.GetOrgs()) {
c.Redirect(303, "/login#error=access_denied_org")
return
}
}
// get the user from the database
u, err := store.UserLogin(login.Login)
u, err := model.GetUserLogin(db, tmpuser.Login)
if err != nil {
count, err := store.UserCount()
count, err := model.GetUserCount(db)
if err != nil {
log.Errorf("cannot register %s. %s", login.Login, err)
c.Redirect(303, "/login#error=internal_error")
log.Errorf("cannot register %s. %s", tmpuser.Login, err)
c.Redirect(303, "/login?error=internal_error")
return
}
// if self-registration is disabled we should
// return a notAuthorized error. the only exception
// is if no users exist yet in the system we'll proceed.
if !remote.GetOpen() && count != 0 {
log.Errorf("cannot register %s. registration closed", login.Login)
c.Redirect(303, "/login#error=access_denied")
if !open && count != 0 {
log.Errorf("cannot register %s. registration closed", tmpuser.Login)
c.Redirect(303, "/login?error=access_denied")
return
}
// create the user account
u = &types.User{}
u.Login = login.Login
u.Token = login.Token
u.Secret = login.Secret
u.Email = login.Email
u.Avatar = login.Avatar
u.Hash = types.GenerateToken()
u = &model.User{}
u.Login = tmpuser.Login
u.Token = tmpuser.Token
u.Secret = tmpuser.Secret
u.Email = tmpuser.Email
u.Avatar = tmpuser.Avatar
u.Hash = crypto.Rand()
// insert the user into the database
if err := store.AddUser(u); err != nil {
log.Errorf("cannot insert %s. %s", login.Login, err)
c.Redirect(303, "/login#error=internal_error")
if err := model.CreateUser(db, u); err != nil {
log.Errorf("cannot insert %s. %s", u.Login, err)
c.Redirect(303, "/login?error=internal_error")
return
}
@ -89,20 +79,14 @@ func GetLogin(c *gin.Context) {
// update the user meta data and authorization
// data and cache in the datastore.
u.Token = login.Token
u.Secret = login.Secret
u.Email = login.Email
u.Avatar = login.Avatar
u.Token = tmpuser.Token
u.Secret = tmpuser.Secret
u.Email = tmpuser.Email
u.Avatar = tmpuser.Avatar
// TODO: remove this once gitlab implements setting
// avatar in the remote package, similar to github
if len(u.Avatar) == 0 {
u.Avatar = gravatar.Hash(u.Email)
}
if err := store.SetUser(u); err != nil {
if err := model.UpdateUser(db, u); err != nil {
log.Errorf("cannot update %s. %s", u.Login, err)
c.Redirect(303, "/login#error=internal_error")
c.Redirect(303, "/login?error=internal_error")
return
}
@ -111,93 +95,65 @@ func GetLogin(c *gin.Context) {
tokenstr, err := token.SignExpires(u.Hash, exp)
if err != nil {
log.Errorf("cannot create token for %s. %s", u.Login, err)
c.Redirect(303, "/login#error=internal_error")
c.Redirect(303, "/login?error=internal_error")
return
}
c.Redirect(303, "/#access_token="+tokenstr)
httputil.SetCookie(c.Writer, c.Request, "user_sess", tokenstr)
redirect := httputil.GetCookie(c.Request, "user_last")
if len(redirect) == 0 {
redirect = "/"
}
c.Redirect(303, redirect)
}
// getLoginOauth2 is the default authorization implementation
// using the oauth2 protocol.
func getLoginOauth2(c *gin.Context) {
var remote = ToRemote(c)
func GetLogout(c *gin.Context) {
// Bugagazavr: I think this must be moved to remote config
//var scope = strings.Join(settings.Auth.Scope, ",")
//if scope == "" {
// scope = remote.Scope()
//}
var transport = remote.Oauth2Transport(c.Request)
httputil.DelCookie(c.Writer, c.Request, "user_sess")
httputil.DelCookie(c.Writer, c.Request, "user_last")
c.Redirect(303, "/login")
}
// get the OAuth code
var code = c.Request.FormValue("code")
//var state = c.Request.FormValue("state")
if len(code) == 0 {
// TODO this should be a random number, verified by a cookie
c.Redirect(303, transport.AuthCodeURL("random"))
return
}
func GetLoginToken(c *gin.Context) {
db := context.Database(c)
remote := context.Remote(c)
// exhange for a token
var token, err = transport.Exchange(code)
in := &tokenPayload{}
err := c.Bind(in)
if err != nil {
log.Errorf("cannot get access_token. %s", err)
c.Redirect(303, "/login#error=token_exchange")
c.AbortWithError(http.StatusBadRequest, err)
return
}
// get user account
user, err := remote.Login(token.AccessToken, token.RefreshToken)
login, err := remote.Auth(in.Access, in.Refresh)
if err != nil {
log.Errorf("cannot get user with access_token. %s", err)
c.Redirect(303, "/login#error=user_not_found")
c.AbortWithError(http.StatusUnauthorized, err)
return
}
// add the user to the request
c.Set("user", user)
}
// getLoginOauth1 is able to authorize a user with the oauth1
// authentication protocol. This is used primarily with Bitbucket
// and Stash only, and one day I hope can be removed.
func getLoginOauth1(c *gin.Context) {
}
// getLoginBasic is able to authorize a user with a username and
// password. This can be used for systems that do not support oauth.
func getLoginBasic(c *gin.Context) {
var (
remote = ToRemote(c)
username = c.Request.FormValue("username")
password = c.Request.FormValue("password")
)
// get user account
user, err := remote.Login(username, password)
user, err := model.GetUserLogin(db, login)
if err != nil {
log.Errorf("invalid username or password for %s. %s", username, err)
c.Redirect(303, "/login#error=invalid_credentials")
c.AbortWithError(http.StatusNotFound, err)
return
}
// add the user to the request
c.Set("user", user)
exp := time.Now().Add(time.Hour * 72).Unix()
token := token.New(token.SessToken, user.Login)
tokenstr, err := token.SignExpires(user.Hash, exp)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
c.IndentedJSON(http.StatusOK, &tokenPayload{
Access: tokenstr,
Expires: exp - time.Now().Unix(),
})
}
// checkMembership is a helper function that compares the user's
// organization list to a whitelist of organizations that are
// approved to use the system.
func checkMembership(orgs, whitelist []string) bool {
orgs_ := make(map[string]struct{}, len(orgs))
for _, org := range orgs {
orgs_[org] = struct{}{}
}
for _, org := range whitelist {
if _, ok := orgs_[org]; ok {
return true
}
}
return false
type tokenPayload struct {
Access string `json:"access_token,omitempty"`
Refresh string `json:"refresh_token,omitempty"`
Expires int64 `json:"expires_in,omitempty"`
}

80
controller/node.go Normal file
View File

@ -0,0 +1,80 @@
package controller
import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"github.com/drone/drone/model"
"github.com/drone/drone/router/middleware/context"
"github.com/drone/drone/router/middleware/session"
"github.com/drone/drone/shared/token"
)
func GetNodes(c *gin.Context) {
db := context.Database(c)
nodes, err := model.GetNodeList(db)
if err != nil {
c.AbortWithStatus(http.StatusInternalServerError)
} else {
c.IndentedJSON(http.StatusOK, nodes)
}
}
func ShowNodes(c *gin.Context) {
db := context.Database(c)
user := session.User(c)
nodes, _ := model.GetNodeList(db)
token, _ := token.New(token.CsrfToken, user.Login).Sign(user.Hash)
c.HTML(http.StatusOK, "nodes.html", gin.H{"User": user, "Nodes": nodes, "Csrf": token})
}
func GetNode(c *gin.Context) {
}
func PostNode(c *gin.Context) {
db := context.Database(c)
engine := context.Engine(c)
node := &model.Node{}
err := c.Bind(node)
if err != nil {
c.AbortWithStatus(http.StatusBadRequest)
return
}
node.Arch = "linux_amd64"
err = model.InsertNode(db, node)
if err != nil {
c.AbortWithStatus(http.StatusInternalServerError)
return
}
ok := engine.Allocate(node)
if !ok {
c.AbortWithStatus(http.StatusInternalServerError)
} else {
c.IndentedJSON(http.StatusOK, node)
}
}
func DeleteNode(c *gin.Context) {
db := context.Database(c)
engine := context.Engine(c)
id, _ := strconv.Atoi(c.Param("node"))
node, err := model.GetNode(db, int64(id))
if err != nil {
c.AbortWithStatus(http.StatusNotFound)
return
}
err = model.DeleteNode(db, node)
if err != nil {
c.AbortWithStatus(http.StatusInternalServerError)
return
}
engine.Deallocate(node)
}

View File

@ -1,11 +0,0 @@
package server
import (
"github.com/drone/drone/Godeps/_workspace/src/github.com/gin-gonic/gin"
)
func GetQueue(c *gin.Context) {
queue := ToQueue(c)
items := queue.Items()
c.JSON(200, items)
}

View File

@ -1,33 +0,0 @@
package recorder
import (
"bufio"
"net"
"net/http"
"net/http/httptest"
)
type ResponseRecorder struct {
*httptest.ResponseRecorder
}
func New() *ResponseRecorder {
return &ResponseRecorder{httptest.NewRecorder()}
}
func (rr *ResponseRecorder) reset() {
rr.ResponseRecorder = httptest.NewRecorder()
}
func (rr *ResponseRecorder) CloseNotify() <-chan bool {
return http.ResponseWriter(rr).(http.CloseNotifier).CloseNotify()
}
func (rr *ResponseRecorder) Hijack() (net.Conn, *bufio.ReadWriter, error) {
return http.ResponseWriter(rr).(http.Hijacker).Hijack()
}
func (rr *ResponseRecorder) Size() int { return rr.Body.Len() }
func (rr *ResponseRecorder) Status() int { return rr.Code }
func (rr *ResponseRecorder) WriteHeaderNow() {}
func (rr *ResponseRecorder) Written() bool { return rr.Code != 0 }

274
controller/repo.go Normal file
View File

@ -0,0 +1,274 @@
package controller
import (
"bytes"
"fmt"
"io/ioutil"
"net/http"
"github.com/gin-gonic/gin"
"gopkg.in/yaml.v2"
"github.com/drone/drone/model"
"github.com/drone/drone/router/middleware/context"
"github.com/drone/drone/router/middleware/session"
"github.com/drone/drone/shared/crypto"
"github.com/drone/drone/shared/httputil"
"github.com/drone/drone/shared/token"
)
func PostRepo(c *gin.Context) {
db := context.Database(c)
remote := context.Remote(c)
user := session.User(c)
owner := c.Param("owner")
name := c.Param("name")
if user == nil {
c.AbortWithStatus(403)
return
}
r, err := remote.Repo(user, owner, name)
if err != nil {
c.String(404, err.Error())
return
}
m, err := remote.Perm(user, owner, name)
if err != nil {
c.String(404, err.Error())
return
}
if !m.Admin {
c.String(403, "Administrative access is required.")
return
}
// error if the repository already exists
_, err = model.GetRepoName(db, owner, name)
if err == nil {
c.String(409, "Repository already exists.")
return
}
// set the repository owner to the
// currently authenticated user.
r.UserID = user.ID
r.AllowPush = true
r.AllowPull = true
r.Timeout = 60 // 1 hour default build time
r.Hash = crypto.Rand()
// crates the jwt token used to verify the repository
t := token.New(token.HookToken, r.FullName)
sig, err := t.Sign(r.Hash)
if err != nil {
c.AbortWithError(500, err)
return
}
link := fmt.Sprintf(
"%s/hook?access_token=%s",
httputil.GetURL(c.Request),
sig,
)
// generate an RSA key and add to the repo
key, err := crypto.GeneratePrivateKey()
if err != nil {
c.AbortWithError(500, err)
return
}
keys := new(model.Key)
keys.Public = string(crypto.MarshalPublicKey(&key.PublicKey))
keys.Private = string(crypto.MarshalPrivateKey(key))
// activate the repository before we make any
// local changes to the database.
err = remote.Activate(user, r, keys, link)
if err != nil {
c.AbortWithError(500, err)
return
}
tx, err := db.Begin()
if err != nil {
c.AbortWithError(500, err)
return
}
defer tx.Rollback()
// persist the repository
err = model.CreateRepo(tx, r)
if err != nil {
c.AbortWithError(500, err)
return
}
keys.RepoID = r.ID
err = model.CreateKey(tx, keys)
if err != nil {
c.AbortWithError(500, err)
return
}
err = model.CreateStar(tx, user, r)
if err != nil {
c.AbortWithError(500, err)
return
}
tx.Commit()
c.JSON(200, r)
}
func PatchRepo(c *gin.Context) {
db := context.Database(c)
repo := session.Repo(c)
user := session.User(c)
in := &struct {
IsTrusted *bool `json:"trusted,omitempty"`
Timeout *int64 `json:"timeout,omitempty"`
AllowPull *bool `json:"allow_pr,omitempty"`
AllowPush *bool `json:"allow_push,omitempty"`
AllowDeploy *bool `json:"allow_deploy,omitempty"`
AllowTag *bool `json:"allow_tag,omitempty"`
}{}
if err := c.Bind(in); err != nil {
c.AbortWithError(http.StatusBadRequest, err)
return
}
if in.AllowPush != nil {
repo.AllowPush = *in.AllowPush
}
if in.AllowPull != nil {
repo.AllowPull = *in.AllowPull
}
if in.AllowDeploy != nil {
repo.AllowDeploy = *in.AllowDeploy
}
if in.AllowTag != nil {
repo.AllowTag = *in.AllowTag
}
if in.IsTrusted != nil && user.Admin {
repo.IsTrusted = *in.IsTrusted
}
if in.Timeout != nil && user.Admin {
repo.Timeout = *in.Timeout
}
err := model.UpdateRepo(db, repo)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
// if the user is authenticated we should
// check to see if they've starred the repository
repo.IsStarred, _ = model.GetStar(db, user, repo)
c.IndentedJSON(http.StatusOK, repo)
}
func GetRepo(c *gin.Context) {
db := context.Database(c)
repo := session.Repo(c)
user := session.User(c)
if user == nil {
c.IndentedJSON(http.StatusOK, repo)
return
}
// if the user is authenticated we should
// check to see if they've starred the repository
repo.IsStarred, _ = model.GetStar(db, user, repo)
c.IndentedJSON(http.StatusOK, repo)
}
func GetRepoKey(c *gin.Context) {
db := context.Database(c)
repo := session.Repo(c)
keys, err := model.GetKey(db, repo)
if err != nil {
c.AbortWithError(http.StatusNotFound, err)
} else {
c.String(http.StatusOK, keys.Public)
}
}
func DeleteRepo(c *gin.Context) {
db := context.Database(c)
repo := session.Repo(c)
err := model.DeleteRepo(db, repo)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
} else {
c.Writer.WriteHeader(http.StatusOK)
}
}
func PostSecure(c *gin.Context) {
db := context.Database(c)
repo := session.Repo(c)
in, err := ioutil.ReadAll(c.Request.Body)
if err != nil {
c.AbortWithError(http.StatusBadRequest, err)
return
}
// we found some strange characters included in
// the yaml file when entered into a browser textarea.
// these need to be removed
in = bytes.Replace(in, []byte{'\xA0'}, []byte{' '}, -1)
// make sure the Yaml is valid format to prevent
// a malformed value from being used in the build
err = yaml.Unmarshal(in, &yaml.MapSlice{})
if err != nil {
c.String(http.StatusBadRequest, err.Error())
return
}
key, err := model.GetKey(db, repo)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
// encrypts using go-jose
out, err := crypto.Encrypt(string(in), key.Private)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
c.String(http.StatusOK, out)
}
func PostStar(c *gin.Context) {
db := context.Database(c)
repo := session.Repo(c)
user := session.User(c)
err := model.CreateStar(db, user, repo)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
} else {
c.Writer.WriteHeader(http.StatusOK)
}
}
func DeleteStar(c *gin.Context) {
db := context.Database(c)
repo := session.Repo(c)
user := session.User(c)
err := model.DeleteStar(db, user, repo)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
} else {
c.Writer.WriteHeader(http.StatusOK)
}
}

View File

@ -1,349 +0,0 @@
package server
import (
"bytes"
"encoding/base64"
"fmt"
"io/ioutil"
"github.com/drone/drone/Godeps/_workspace/src/github.com/gin-gonic/gin"
"github.com/drone/drone/Godeps/_workspace/src/github.com/gin-gonic/gin/binding"
"github.com/drone/drone/Godeps/_workspace/src/gopkg.in/yaml.v2"
"github.com/drone/drone/pkg/remote"
"github.com/drone/drone/pkg/token"
common "github.com/drone/drone/pkg/types"
"github.com/drone/drone/pkg/utils/httputil"
"github.com/drone/drone/pkg/utils/sshutil"
"github.com/drone/drone/pkg/yaml/secure"
)
// repoResp is a data structure used for sending
// repository data to the client, augmented with
// additional repository meta-data.
type repoResp struct {
*common.Repo
Perms *common.Perm `json:"permissions,omitempty"`
Keypair *common.Keypair `json:"keypair,omitempty"`
Params map[string]string `json:"params,omitempty"`
Starred bool `json:"starred,omitempty"`
}
// repoReq is a data structure used for receiving
// repository data from the client to modify the
// attributes of an existing repository.
//
// note that attributes are pointers so that we can
// accept null values, effectively patching an existing
// repository object with only the supplied fields.
type repoReq struct {
Trusted *bool `json:"trusted"`
Timeout *int64 `json:"timeout"`
Hooks struct {
PullReqeust *bool `json:"pull_request"`
Push *bool `json:"push"`
}
// optional private parameters can only be
// supplied by the repository admin.
Params *map[string]string `json:"params"`
}
// GetRepo accepts a request to retrieve a commit
// from the datastore for the given repository, branch and
// commit hash.
//
// GET /api/repos/:owner/:name
//
func GetRepo(c *gin.Context) {
store := ToDatastore(c)
repo := ToRepo(c)
user := ToUser(c)
perm := ToPerm(c)
data := repoResp{repo, perm, nil, nil, false}
// if the user is authenticated, we should display
// if she is watching the current repository.
if user == nil {
c.JSON(200, data)
return
}
// if the user is an administrator of the project
// we should display the private parameter data
// and keypair data.
if perm.Push {
data.Params = repo.Params
data.Keypair = repo.Keys
}
// check to see if the user is subscribing to the repo
data.Starred, _ = store.Starred(user, repo)
c.JSON(200, data)
}
// PutRepo accepts a request to update the named repository
// in the datastore. It expects a JSON input and returns the
// updated repository in JSON format if successful.
//
// PUT /api/repos/:owner/:name
//
func PutRepo(c *gin.Context) {
store := ToDatastore(c)
perm := ToPerm(c)
user := ToUser(c)
repo := ToRepo(c)
in := &repoReq{}
if !c.BindWith(in, binding.JSON) {
return
}
if in.Params != nil {
repo.Params = *in.Params
}
if in.Hooks.Push != nil {
repo.Hooks.Push = *in.Hooks.Push
}
if in.Hooks.PullReqeust != nil {
repo.Hooks.PullRequest = *in.Hooks.PullReqeust
}
if in.Trusted != nil && user.Admin {
repo.Trusted = *in.Trusted
}
if in.Timeout != nil && user.Admin {
repo.Timeout = *in.Timeout
}
err := store.SetRepo(repo)
if err != nil {
c.Fail(400, err)
return
}
data := repoResp{repo, perm, nil, nil, false}
data.Params = repo.Params
data.Keypair = repo.Keys
data.Starred, _ = store.Starred(user, repo)
c.JSON(200, data)
}
// DeleteRepo accepts a request to delete the named
// repository.
//
// DEL /api/repos/:owner/:name
//
func DeleteRepo(c *gin.Context) {
ds := ToDatastore(c)
u := ToUser(c)
r := ToRepo(c)
link := fmt.Sprintf(
"%s/api/hook",
httputil.GetURL(c.Request),
)
remote := ToRemote(c)
err := remote.Deactivate(u, r, link)
if err != nil {
c.Fail(400, err)
}
err = ds.DelRepo(r)
if err != nil {
c.Fail(500, err)
}
c.Writer.WriteHeader(200)
}
// PostRepo accapets a request to activate the named repository
// in the datastore. It returns a 201 status created if successful
//
// POST /api/repos/:owner/:name
//
func PostRepo(c *gin.Context) {
user := ToUser(c)
store := ToDatastore(c)
owner := c.Params.ByName("owner")
name := c.Params.ByName("name")
// get the repository and user permissions
// from the remote system.
remote := ToRemote(c)
r, err := remote.Repo(user, owner, name)
if err != nil {
c.Fail(404, err)
}
m, err := remote.Perm(user, owner, name)
if err != nil {
c.Fail(404, err)
return
}
if !m.Admin {
c.Fail(403, fmt.Errorf("must be repository admin"))
return
}
// error if the repository already exists
_, err = store.RepoName(owner, name)
if err == nil {
c.String(409, "Repository already exists")
return
}
// set the repository owner to the
// currently authenticated user.
r.UserID = user.ID
r.Hooks = new(common.Hooks)
r.Hooks.Push = true
r.Hooks.PullRequest = true
r.Timeout = 60 // 1 hour default build time
r.Hash = common.GenerateToken()
r.Self = fmt.Sprintf(
"%s/%s",
httputil.GetURL(c.Request),
r.FullName,
)
// crates the jwt token used to verify the repository
t := token.New(token.HookToken, r.FullName)
sig, err := t.Sign(r.Hash)
if err != nil {
c.Fail(500, err)
return
}
link := fmt.Sprintf(
"%s/api/hook?access_token=%s",
httputil.GetURL(c.Request),
sig,
)
// generate an RSA key and add to the repo
key, err := sshutil.GeneratePrivateKey()
if err != nil {
c.Fail(500, err)
return
}
r.Keys = new(common.Keypair)
r.Keys.Public = string(sshutil.MarshalPublicKey(&key.PublicKey))
r.Keys.Private = string(sshutil.MarshalPrivateKey(key))
// activate the repository before we make any
// local changes to the database.
err = remote.Activate(user, r, r.Keys, link)
if err != nil {
c.Fail(500, err)
return
}
// persist the repository
err = store.AddRepo(r)
if err != nil {
c.Fail(500, err)
return
}
store.AddStar(user, r)
c.JSON(200, r)
}
// Encrypt accapets a request to encrypt the
// body of the request using the repository secret
// key.
//
// POST /api/repos/:owner/:name/encrypt
//
func Encrypt(c *gin.Context) {
repo := ToRepo(c)
in, err := ioutil.ReadAll(c.Request.Body)
if err != nil {
c.Fail(400, err)
return
}
in, err = base64.StdEncoding.DecodeString(string(in))
if err != nil {
c.Fail(500, err)
return
}
// we found some strange characters included in
// the yaml file when entered into a browser textarea.
// these need to be removed
in = bytes.Replace(in, []byte{'\xA0'}, []byte{' '}, -1)
// make sure the Yaml is valid format to prevent
// a malformed value from being used in the build
err = yaml.Unmarshal(in, &yaml.MapSlice{})
if err != nil {
c.Fail(500, err)
return
}
// encrypts using go-jose
out, err := secure.Encrypt(string(in), repo.Keys.Private)
if err != nil {
c.Fail(500, err)
return
}
c.Writer.Write([]byte(out))
}
// Unsubscribe accapets a request to unsubscribe the
// currently authenticated user to the repository.
//
// DEL /api/subscribers/:owner/:name
//
func Unsubscribe(c *gin.Context) {
store := ToDatastore(c)
repo := ToRepo(c)
user := ToUser(c)
err := store.DelStar(user, repo)
if err != nil {
c.Fail(400, err)
} else {
c.Writer.WriteHeader(200)
}
}
// Subscribe accapets a request to subscribe the
// currently authenticated user to the repository.
//
// POST /api/subscriber/:owner/:name
//
func Subscribe(c *gin.Context) {
store := ToDatastore(c)
repo := ToRepo(c)
user := ToUser(c)
err := store.AddStar(user, repo)
if err != nil {
c.Fail(400, err)
} else {
c.Writer.WriteHeader(200)
}
}
// perms is a helper function that returns user permissions
// for a particular repository.
func perms(remote remote.Remote, u *common.User, r *common.Repo) *common.Perm {
switch {
case u == nil && r.Private:
return &common.Perm{}
case u == nil && r.Private == false:
return &common.Perm{Pull: true}
case u.Admin:
return &common.Perm{Pull: true, Push: true, Admin: true}
}
p, err := remote.Perm(u, r.Owner, r.Name)
if err != nil {
return &common.Perm{}
}
return p
}

View File

@ -1,261 +0,0 @@
package server
import (
"net/http"
"time"
"github.com/drone/drone/Godeps/_workspace/src/github.com/gin-gonic/gin"
"github.com/drone/drone/pkg/bus"
"github.com/drone/drone/pkg/queue"
"github.com/drone/drone/pkg/remote"
"github.com/drone/drone/pkg/runner"
"github.com/drone/drone/pkg/store"
"github.com/drone/drone/pkg/token"
common "github.com/drone/drone/pkg/types"
)
func SetQueue(q queue.Queue) gin.HandlerFunc {
return func(c *gin.Context) {
c.Set("queue", q)
c.Next()
}
}
func ToQueue(c *gin.Context) queue.Queue {
v, ok := c.Get("queue")
if !ok {
return nil
}
return v.(queue.Queue)
}
func SetBus(r bus.Bus) gin.HandlerFunc {
return func(c *gin.Context) {
c.Set("bus", r)
c.Next()
}
}
func ToBus(c *gin.Context) bus.Bus {
v, ok := c.Get("bus")
if !ok {
return nil
}
return v.(bus.Bus)
}
func ToRemote(c *gin.Context) remote.Remote {
v, ok := c.Get("remote")
if !ok {
return nil
}
return v.(remote.Remote)
}
func SetRemote(r remote.Remote) gin.HandlerFunc {
return func(c *gin.Context) {
c.Set("remote", r)
c.Next()
}
}
func ToRunner(c *gin.Context) runner.Runner {
v, ok := c.Get("runner")
if !ok {
return nil
}
return v.(runner.Runner)
}
func SetRunner(r runner.Runner) gin.HandlerFunc {
return func(c *gin.Context) {
c.Set("runner", r)
c.Next()
}
}
func ToPerm(c *gin.Context) *common.Perm {
v, ok := c.Get("perm")
if !ok {
return nil
}
return v.(*common.Perm)
}
func ToUser(c *gin.Context) *common.User {
v, ok := c.Get("user")
if !ok {
return nil
}
return v.(*common.User)
}
func ToRepo(c *gin.Context) *common.Repo {
v, ok := c.Get("repo")
if !ok {
return nil
}
return v.(*common.Repo)
}
func ToDatastore(c *gin.Context) store.Store {
return c.MustGet("datastore").(store.Store)
}
func SetDatastore(ds store.Store) gin.HandlerFunc {
return func(c *gin.Context) {
c.Set("datastore", ds)
c.Next()
}
}
func SetUser() gin.HandlerFunc {
return func(c *gin.Context) {
var store = ToDatastore(c)
var user *common.User
_, err := token.ParseRequest(c.Request, func(t *token.Token) (string, error) {
var err error
user, err = store.UserLogin(t.Text)
if err != nil {
return "", err
}
return user.Hash, nil
})
if err == nil && user != nil && user.ID != 0 {
c.Set("user", user)
}
c.Next()
}
}
func SetRepo() gin.HandlerFunc {
return func(c *gin.Context) {
ds := ToDatastore(c)
u := ToUser(c)
owner := c.Params.ByName("owner")
name := c.Params.ByName("name")
r, err := ds.RepoName(owner, name)
switch {
case err != nil && u != nil:
c.Fail(404, err)
return
case err != nil && u == nil:
c.Fail(401, err)
return
}
c.Set("repo", r)
c.Next()
}
}
func SetPerm() gin.HandlerFunc {
return func(c *gin.Context) {
remote := ToRemote(c)
user := ToUser(c)
repo := ToRepo(c)
perm := perms(remote, user, repo)
c.Set("perm", perm)
c.Next()
}
}
func MustUser() gin.HandlerFunc {
return func(c *gin.Context) {
u := ToUser(c)
if u == nil {
c.AbortWithStatus(401)
} else {
c.Set("user", u)
c.Next()
}
}
}
func MustAdmin() gin.HandlerFunc {
return func(c *gin.Context) {
u := ToUser(c)
if u == nil {
c.AbortWithStatus(401)
} else if !u.Admin {
c.AbortWithStatus(403)
} else {
c.Set("user", u)
c.Next()
}
}
}
func CheckPull() gin.HandlerFunc {
return func(c *gin.Context) {
u := ToUser(c)
m := ToPerm(c)
switch {
case u == nil && m == nil:
c.AbortWithStatus(401)
case u == nil && m.Pull == false:
c.AbortWithStatus(401)
case u != nil && m.Pull == false:
c.AbortWithStatus(404)
default:
c.Next()
}
}
}
func CheckPush() gin.HandlerFunc {
return func(c *gin.Context) {
switch c.Request.Method {
case "GET", "OPTIONS":
c.Next()
return
}
u := ToUser(c)
m := ToPerm(c)
switch {
case u == nil && m.Push == false:
c.AbortWithStatus(401)
case u != nil && m.Push == false:
c.AbortWithStatus(404)
default:
c.Next()
}
}
}
func SetHeaders() gin.HandlerFunc {
return func(c *gin.Context) {
c.Writer.Header().Add("Access-Control-Allow-Origin", "*")
c.Writer.Header().Add("X-Frame-Options", "DENY")
c.Writer.Header().Add("X-Content-Type-Options", "nosniff")
c.Writer.Header().Add("X-XSS-Protection", "1; mode=block")
c.Writer.Header().Add("Cache-Control", "no-cache")
c.Writer.Header().Add("Cache-Control", "no-store")
c.Writer.Header().Add("Cache-Control", "max-age=0")
c.Writer.Header().Add("Cache-Control", "must-revalidate")
c.Writer.Header().Add("Cache-Control", "value")
c.Writer.Header().Set("Last-Modified", time.Now().UTC().Format(http.TimeFormat))
c.Writer.Header().Set("Expires", "Thu, 01 Jan 1970 00:00:00 GMT")
if c.Request.TLS != nil {
c.Writer.Header().Add("Strict-Transport-Security", "max-age=31536000")
}
if c.Request.Method == "OPTIONS" {
c.Writer.Header().Set("Access-Control-Allow-Methods", "GET,POST,PUT,PATCH,DELETE,OPTIONS")
c.Writer.Header().Set("Access-Control-Allow-Headers", "Authorization")
c.Writer.Header().Set("Allow", "HEAD,GET,POST,PUT,PATCH,DELETE,OPTIONS")
c.Writer.Header().Set("Content-Type", "application/json")
c.Writer.WriteHeader(200)
return
}
c.Next()
}
}

123
controller/stream.go Normal file
View File

@ -0,0 +1,123 @@
package controller
/*
stream.Get("/:owner/:name", controller.GetRepoEvents)
stream.Get("/:owner/:name/:build/:number", controller.GetStream)
*/
import (
"io"
"strconv"
"github.com/gin-gonic/gin"
"github.com/docker/docker/pkg/stdcopy"
"github.com/drone/drone/engine"
"github.com/drone/drone/model"
"github.com/drone/drone/router/middleware/context"
"github.com/drone/drone/router/middleware/session"
log "github.com/Sirupsen/logrus"
"github.com/manucorporat/sse"
)
// GetRepoEvents will upgrade the connection to a Websocket and will stream
// event updates to the browser.
func GetRepoEvents(c *gin.Context) {
engine_ := context.Engine(c)
repo := session.Repo(c)
c.Writer.Header().Set("Content-Type", "text/event-stream")
eventc := make(chan *engine.Event, 1)
engine_.Subscribe(eventc)
defer func() {
engine_.Unsubscribe(eventc)
close(eventc)
log.Infof("closed event stream")
}()
c.Stream(func(w io.Writer) bool {
select {
case event := <-eventc:
if event == nil {
log.Infof("nil event received")
return false
}
if event.Name == repo.FullName {
log.Debugf("received message %s", event.Name)
sse.Encode(w, sse.Event{
Event: "message",
Data: string(event.Msg),
})
}
case <-c.Writer.CloseNotify():
return false
}
return true
})
}
func GetStream(c *gin.Context) {
db := context.Database(c)
engine_ := context.Engine(c)
repo := session.Repo(c)
buildn, _ := strconv.Atoi(c.Param("build"))
jobn, _ := strconv.Atoi(c.Param("number"))
c.Writer.Header().Set("Content-Type", "text/event-stream")
build, err := model.GetBuildNumber(db, repo, buildn)
if err != nil {
log.Debugln("stream cannot get build number.", err)
c.AbortWithError(404, err)
return
}
job, err := model.GetJobNumber(db, build, jobn)
if err != nil {
log.Debugln("stream cannot get job number.", err)
c.AbortWithError(404, err)
return
}
node, err := model.GetNode(db, job.NodeID)
if err != nil {
log.Debugln("stream cannot get node.", err)
c.AbortWithError(404, err)
return
}
rc, err := engine_.Stream(build.ID, job.ID, node)
if err != nil {
c.AbortWithError(404, err)
return
}
defer func() {
rc.Close()
}()
go func() {
<-c.Writer.CloseNotify()
rc.Close()
}()
rw := &StreamWriter{c.Writer, 0}
stdcopy.StdCopy(rw, rw, rc)
}
type StreamWriter struct {
writer gin.ResponseWriter
count int
}
func (w *StreamWriter) Write(data []byte) (int, error) {
var err = sse.Encode(w.writer, sse.Event{
Id: strconv.Itoa(w.count),
Event: "message",
Data: string(data),
})
w.writer.Flush()
w.count += len(data)
return len(data), err
}

View File

@ -1,103 +1,82 @@
package server
package controller
import (
// "crypto/sha1"
"net/http"
"github.com/drone/drone/Godeps/_workspace/src/github.com/gin-gonic/gin"
"github.com/drone/drone/Godeps/_workspace/src/github.com/gin-gonic/gin/binding"
"github.com/drone/drone/Godeps/_workspace/src/github.com/ungerik/go-gravatar"
"github.com/gin-gonic/gin"
"github.com/drone/drone/pkg/token"
"github.com/drone/drone/pkg/types"
"github.com/drone/drone/model"
"github.com/drone/drone/router/middleware/context"
"github.com/drone/drone/router/middleware/session"
"github.com/drone/drone/shared/token"
"github.com/hashicorp/golang-lru"
)
// GetUserCurr accepts a request to retrieve the
// currently authenticated user from the datastore
// and return in JSON format.
//
// GET /api/user
//
func GetUserCurr(c *gin.Context) {
u := ToUser(c)
// f := fmt.Printf("% x", sha1.Sum(u.Hash))
var cache *lru.Cache
// v := struct {
// *types.User
// // token fingerprint
// Token string `json:"token"`
// }{u, f}
c.JSON(200, u)
func init() {
var err error
cache, err = lru.New(1028)
if err != nil {
panic(err)
}
}
// PutUserCurr accepts a request to update the currently
// authenticated User profile.
//
// PUT /api/user
//
func PutUserCurr(c *gin.Context) {
store := ToDatastore(c)
user := ToUser(c)
func GetSelf(c *gin.Context) {
c.IndentedJSON(200, session.User(c))
}
in := &types.User{}
if !c.BindWith(in, binding.JSON) {
func GetFeed(c *gin.Context) {
user := session.User(c)
db := context.Database(c)
feed, err := model.GetUserFeed(db, user, 25, 0)
if err != nil {
c.AbortWithStatus(http.StatusInternalServerError)
return
}
// TODO: we are no longer auto-generating avatar
user.Email = in.Email
user.Avatar = gravatar.Hash(in.Email)
err := store.SetUser(user)
if err != nil {
c.Fail(400, err)
} else {
c.JSON(200, user)
}
c.IndentedJSON(http.StatusOK, feed)
}
// GetUserRepos accepts a request to get the currently
// authenticated user's repository list from the datastore,
// encoded and returned in JSON format.
//
// GET /api/user/repos
//
func GetUserRepos(c *gin.Context) {
store := ToDatastore(c)
user := ToUser(c)
repos, err := store.RepoList(user)
func GetRepos(c *gin.Context) {
user := session.User(c)
db := context.Database(c)
repos, err := model.GetRepoList(db, user)
if err != nil {
c.Fail(400, err)
} else {
c.JSON(200, &repos)
c.AbortWithStatus(http.StatusInternalServerError)
return
}
c.IndentedJSON(http.StatusOK, repos)
}
// GetUserFeed accepts a request to get the currently
// authenticated user's build feed from the datastore,
// encoded and returned in JSON format.
//
// GET /api/user/feed
//
func GetUserFeed(c *gin.Context) {
store := ToDatastore(c)
user := ToUser(c)
feed, err := store.UserFeed(user, 25, 0)
if err != nil {
c.Fail(400, err)
} else {
c.JSON(200, &feed)
func GetRemoteRepos(c *gin.Context) {
user := session.User(c)
remote := context.Remote(c)
// attempt to get the repository list from the
// cache since the operation is expensive
v, ok := cache.Get(user.Login)
if ok {
c.IndentedJSON(http.StatusOK, v)
return
}
repos, err := remote.Repos(user)
if err != nil {
c.AbortWithStatus(http.StatusInternalServerError)
return
}
cache.Add(user.Login, repos)
c.IndentedJSON(http.StatusOK, repos)
}
// POST /api/user/token
func PostUserToken(c *gin.Context) {
user := ToUser(c)
func PostToken(c *gin.Context) {
user := session.User(c)
token := token.New(token.UserToken, user.Login)
tokenstr, err := token.Sign(user.Hash)
if err != nil {
c.Fail(500, err)
c.AbortWithError(http.StatusInternalServerError, err)
} else {
c.String(200, tokenstr)
c.String(http.StatusOK, tokenstr)
}
}

View File

@ -1,88 +0,0 @@
package server
import (
"bytes"
"encoding/json"
"errors"
"io/ioutil"
"net/http"
"testing"
. "github.com/drone/drone/Godeps/_workspace/src/github.com/franela/goblin"
"github.com/drone/drone/Godeps/_workspace/src/github.com/gin-gonic/gin"
"github.com/drone/drone/pkg/server/recorder"
"github.com/drone/drone/pkg/store/mock"
common "github.com/drone/drone/pkg/types"
)
func TestUser(t *testing.T) {
store := new(mocks.Store)
g := Goblin(t)
g.Describe("User", func() {
g.It("should get", func() {
rw := recorder.New()
ctx := &gin.Context{Engine: gin.Default(), Writer: rw}
user := &common.User{Login: "octocat"}
ctx.Set("user", user)
GetUserCurr(ctx)
out := &common.User{}
json.NewDecoder(rw.Body).Decode(out)
g.Assert(rw.Code).Equal(200)
g.Assert(out).Equal(user)
})
g.It("should put", func() {
var buf bytes.Buffer
in := &common.User{Email: "octocat@github.com"}
json.NewEncoder(&buf).Encode(in)
rw := recorder.New()
ctx := &gin.Context{Engine: gin.Default(), Writer: rw}
ctx.Request = &http.Request{Body: ioutil.NopCloser(&buf)}
ctx.Request.Header = http.Header{}
ctx.Request.Header.Set("Content-Type", "application/json")
user := &common.User{Login: "octocat"}
ctx.Set("user", user)
ctx.Set("datastore", store)
store.On("SetUser", user).Return(nil).Once()
PutUserCurr(ctx)
out := &common.User{}
json.NewDecoder(rw.Body).Decode(out)
g.Assert(rw.Code).Equal(200)
g.Assert(out.Login).Equal(user.Login)
g.Assert(out.Email).Equal(in.Email)
g.Assert(out.Avatar).Equal("7194e8d48fa1d2b689f99443b767316c")
})
g.It("should put, error", func() {
var buf bytes.Buffer
in := &common.User{Email: "octocat@github.com"}
json.NewEncoder(&buf).Encode(in)
rw := recorder.New()
ctx := &gin.Context{Engine: gin.Default(), Writer: rw}
ctx.Request = &http.Request{Body: ioutil.NopCloser(&buf)}
ctx.Request.Header = http.Header{}
ctx.Request.Header.Set("Content-Type", "application/json")
user := &common.User{Login: "octocat"}
ctx.Set("user", user)
ctx.Set("datastore", store)
store.On("SetUser", user).Return(errors.New("error")).Once()
PutUserCurr(ctx)
out := &common.User{}
json.NewDecoder(rw.Body).Decode(out)
g.Assert(rw.Code).Equal(400)
})
})
}

View File

@ -1,131 +1,118 @@
package server
package controller
import (
"github.com/drone/drone/Godeps/_workspace/src/github.com/gin-gonic/gin"
"github.com/drone/drone/Godeps/_workspace/src/github.com/gin-gonic/gin/binding"
"github.com/drone/drone/Godeps/_workspace/src/github.com/ungerik/go-gravatar"
"net/http"
"github.com/drone/drone/pkg/types"
"github.com/gin-gonic/gin"
"github.com/drone/drone/model"
"github.com/drone/drone/router/middleware/context"
"github.com/drone/drone/router/middleware/session"
"github.com/drone/drone/shared/crypto"
)
// GetUsers accepts a request to retrieve all users
// from the datastore and return encoded in JSON format.
//
// GET /api/users
//
func GetUsers(c *gin.Context) {
store := ToDatastore(c)
users, err := store.UserList()
db := context.Database(c)
users, err := model.GetUserList(db)
if err != nil {
c.Fail(400, err)
} else {
c.JSON(200, users)
c.AbortWithStatus(http.StatusInternalServerError)
return
}
c.IndentedJSON(http.StatusOK, users)
}
// PostUser accepts a request to create a new user in the
// system. The created user account is returned in JSON
// format if successful.
//
// POST /api/users
//
func PostUser(c *gin.Context) {
store := ToDatastore(c)
name := c.Params.ByName("name")
user := &types.User{Login: name}
user.Token = c.Request.FormValue("token")
user.Secret = c.Request.FormValue("secret")
user.Hash = c.Request.FormValue("hash")
if len(user.Hash) == 0 {
user.Hash = types.GenerateToken()
}
if err := store.AddUser(user); err != nil {
c.Fail(400, err)
} else {
c.JSON(201, user)
}
}
// GetUser accepts a request to retrieve a user by hostname
// and login from the datastore and return encoded in JSON
// format.
//
// GET /api/users/:name
//
func GetUser(c *gin.Context) {
store := ToDatastore(c)
name := c.Params.ByName("name")
user, err := store.UserLogin(name)
db := context.Database(c)
user, err := model.GetUserLogin(db, c.Param("login"))
if err != nil {
c.Fail(404, err)
} else {
c.JSON(200, user)
c.AbortWithStatus(http.StatusNotFound)
return
}
c.IndentedJSON(http.StatusOK, user)
}
// PutUser accepts a request to update an existing user in
// the system. The modified user account is returned in JSON
// format if successful.
//
// PUT /api/users/:name
//
func PutUser(c *gin.Context) {
store := ToDatastore(c)
me := ToUser(c)
name := c.Params.ByName("name")
user, err := store.UserLogin(name)
func PatchUser(c *gin.Context) {
me := session.User(c)
db := context.Database(c)
in := &model.User{}
err := c.Bind(in)
if err != nil {
c.Fail(404, err)
c.AbortWithStatus(http.StatusBadRequest)
return
}
in := &types.User{}
if !c.BindWith(in, binding.JSON) {
user, err := model.GetUserLogin(db, c.Param("login"))
if err != nil {
c.AbortWithStatus(http.StatusNotFound)
return
}
user.Admin = in.Admin
user.Active = in.Active
// cannot update self
if me.ID == user.ID {
c.AbortWithStatus(http.StatusForbidden)
return
}
err = model.UpdateUser(db, user)
if err != nil {
c.AbortWithStatus(http.StatusConflict)
return
}
c.IndentedJSON(http.StatusOK, user)
}
func PostUser(c *gin.Context) {
db := context.Database(c)
in := &model.User{}
err := c.Bind(in)
if err != nil {
c.String(http.StatusBadRequest, err.Error())
return
}
user := &model.User{}
user.Login = in.Login
user.Email = in.Email
user.Avatar = gravatar.Hash(user.Email)
user.Admin = in.Admin
user.Avatar = in.Avatar
user.Active = true
user.Hash = crypto.Rand()
// an administrator must not be able to
// downgrade her own account.
if me.Login != user.Login {
user.Admin = in.Admin
}
err = store.SetUser(user)
err = model.CreateUser(db, user)
if err != nil {
c.Fail(400, err)
} else {
c.JSON(200, user)
c.String(http.StatusInternalServerError, err.Error())
return
}
c.IndentedJSON(http.StatusOK, user)
}
// DeleteUser accepts a request to delete the specified
// user account from the system. A successful request will
// respond with an OK 200 status.
//
// DELETE /api/users/:name
//
func DeleteUser(c *gin.Context) {
store := ToDatastore(c)
me := ToUser(c)
name := c.Params.ByName("name")
user, err := store.UserLogin(name)
me := session.User(c)
db := context.Database(c)
user, err := model.GetUserLogin(db, c.Param("login"))
if err != nil {
c.Fail(404, err)
c.AbortWithStatus(http.StatusNotFound)
return
}
// an administrator must not be able to
// delete her own account.
if user.Login == me.Login {
c.Writer.WriteHeader(403)
// cannot delete self
if me.ID == user.ID {
c.AbortWithStatus(http.StatusForbidden)
return
}
if err := store.DelUser(user); err != nil {
c.Fail(400, err)
} else {
c.Writer.WriteHeader(204)
err = model.DeleteUser(db, user)
if err != nil {
c.AbortWithStatus(http.StatusInternalServerError)
return
}
c.Writer.WriteHeader(http.StatusNoContent)
}

View File

@ -1,105 +0,0 @@
package server
import (
"io"
"strconv"
"github.com/drone/drone/pkg/bus"
log "github.com/drone/drone/Godeps/_workspace/src/github.com/Sirupsen/logrus"
"github.com/drone/drone/Godeps/_workspace/src/github.com/gin-gonic/gin"
"github.com/drone/drone/Godeps/_workspace/src/github.com/manucorporat/sse"
"github.com/drone/drone/pkg/docker"
)
// GetRepoEvents will upgrade the connection to a Websocket and will stream
// event updates to the browser.
func GetRepoEvents(c *gin.Context) {
bus_ := ToBus(c)
repo := ToRepo(c)
c.Writer.Header().Set("Content-Type", "text/event-stream")
eventc := make(chan *bus.Event, 1)
bus_.Subscribe(eventc)
defer func() {
bus_.Unsubscribe(eventc)
close(eventc)
log.Infof("closed event stream")
}()
c.Stream(func(w io.Writer) bool {
select {
case event := <-eventc:
if event == nil {
log.Infof("nil event received")
return false
}
if event.Kind == bus.EventRepo &&
event.Name == repo.FullName {
sse.Encode(w, sse.Event{
Event: "message",
Data: string(event.Msg),
})
}
case <-c.Writer.CloseNotify():
return false
}
return true
})
}
func GetStream(c *gin.Context) {
store := ToDatastore(c)
repo := ToRepo(c)
runner := ToRunner(c)
commitseq, _ := strconv.Atoi(c.Params.ByName("build"))
jobnum, _ := strconv.Atoi(c.Params.ByName("number"))
c.Writer.Header().Set("Content-Type", "text/event-stream")
build, err := store.BuildNumber(repo, commitseq)
if err != nil {
c.Fail(404, err)
return
}
job, err := store.JobNumber(build, jobnum)
if err != nil {
c.Fail(404, err)
return
}
rc, err := runner.Logs(job)
if err != nil {
c.Fail(404, err)
return
}
defer func() {
rc.Close()
}()
go func() {
<-c.Writer.CloseNotify()
rc.Close()
}()
rw := &StreamWriter{c.Writer, 0}
docker.StdCopy(rw, rw, rc)
}
type StreamWriter struct {
writer gin.ResponseWriter
count int
}
func (w *StreamWriter) Write(data []byte) (int, error) {
var err = sse.Encode(w.writer, sse.Event{
Id: strconv.Itoa(w.count),
Event: "message",
Data: string(data),
})
w.writer.Flush()
w.count += len(data)
return len(data), err
}

51
drone.go Normal file
View File

@ -0,0 +1,51 @@
package main
import (
"flag"
"github.com/drone/drone/engine"
"github.com/drone/drone/remote"
"github.com/drone/drone/router"
"github.com/drone/drone/router/middleware/context"
"github.com/drone/drone/shared/database"
"github.com/drone/drone/shared/envconfig"
"github.com/drone/drone/shared/server"
"github.com/Sirupsen/logrus"
)
var (
dotenv = flag.String("config", ".env", "")
debug = flag.Bool("debug", true, "")
)
func main() {
flag.Parse()
// debug level if requested by user
if *debug {
logrus.SetLevel(logrus.DebugLevel)
}
// Load the configuration from env file
env := envconfig.Load(*dotenv)
// Setup the database driver
database_ := database.Load(env)
// setup the remote driver
remote_ := remote.Load(env)
// setup the runner
engine_ := engine.Load(database_, remote_)
// setup the server and start the listener
server_ := server.Load(env)
server_.Run(
router.Load(
context.SetDatabase(database_),
context.SetRemote(remote_),
context.SetEngine(engine_),
),
)
}

View File

@ -1,28 +1,26 @@
package builtin
package engine
import (
"sync"
"github.com/drone/drone/pkg/bus"
)
type Bus struct {
type eventbus struct {
sync.Mutex
subs map[chan *bus.Event]bool
subs map[chan *Event]bool
}
// New creates a new Bus that manages a list of
// New creates a new eventbus that manages a list of
// subscribers to which events are published.
func New() *Bus {
return &Bus{
subs: make(map[chan *bus.Event]bool),
func newEventbus() *eventbus {
return &eventbus{
subs: make(map[chan *Event]bool),
}
}
// Subscribe adds the channel to the list of
// subscribers. Each subscriber in the list will
// receive broadcast events.
func (b *Bus) Subscribe(c chan *bus.Event) {
func (b *eventbus) subscribe(c chan *Event) {
b.Lock()
b.subs[c] = true
b.Unlock()
@ -30,19 +28,19 @@ func (b *Bus) Subscribe(c chan *bus.Event) {
// Unsubscribe removes the channel from the
// list of subscribers.
func (b *Bus) Unsubscribe(c chan *bus.Event) {
func (b *eventbus) unsubscribe(c chan *Event) {
b.Lock()
delete(b.subs, c)
b.Unlock()
}
// Send dispatches a message to all subscribers.
func (b *Bus) Send(event *bus.Event) {
func (b *eventbus) send(event *Event) {
b.Lock()
defer b.Unlock()
for s := range b.subs {
go func(c chan *bus.Event) {
go func(c chan *Event) {
defer recover()
c <- event
}(s)

View File

@ -1,46 +1,45 @@
package builtin
package engine
import (
"testing"
. "github.com/drone/drone/Godeps/_workspace/src/github.com/franela/goblin"
"github.com/drone/drone/pkg/bus"
. "github.com/franela/goblin"
)
func TestBuild(t *testing.T) {
func TestBus(t *testing.T) {
g := Goblin(t)
g.Describe("Bus", func() {
g.Describe("Event bus", func() {
g.It("Should unsubscribe", func() {
c1 := make(chan *bus.Event)
c2 := make(chan *bus.Event)
b := New()
b.Subscribe(c1)
b.Subscribe(c2)
c1 := make(chan *Event)
c2 := make(chan *Event)
b := newEventbus()
b.subscribe(c1)
b.subscribe(c2)
g.Assert(len(b.subs)).Equal(2)
})
g.It("Should subscribe", func() {
c1 := make(chan *bus.Event)
c2 := make(chan *bus.Event)
b := New()
b.Subscribe(c1)
b.Subscribe(c2)
c1 := make(chan *Event)
c2 := make(chan *Event)
b := newEventbus()
b.subscribe(c1)
b.subscribe(c2)
g.Assert(len(b.subs)).Equal(2)
b.Unsubscribe(c1)
b.Unsubscribe(c2)
b.unsubscribe(c1)
b.unsubscribe(c2)
g.Assert(len(b.subs)).Equal(0)
})
g.It("Should send", func() {
em := map[string]bool{"foo": true, "bar": true}
e1 := &bus.Event{Name: "foo"}
e2 := &bus.Event{Name: "bar"}
c := make(chan *bus.Event)
b := New()
b.Subscribe(c)
b.Send(e1)
b.Send(e2)
e1 := &Event{Name: "foo"}
e2 := &Event{Name: "bar"}
c := make(chan *Event)
b := newEventbus()
b.subscribe(c)
b.send(e1)
b.send(e2)
r1 := <-c
r2 := <-c
g.Assert(em[r1.Name]).Equal(true)

392
engine/engine.go Normal file
View File

@ -0,0 +1,392 @@
package engine
import (
"bytes"
"crypto/tls"
"crypto/x509"
"database/sql"
"errors"
"fmt"
"io"
"io/ioutil"
"runtime"
"time"
log "github.com/Sirupsen/logrus"
"github.com/docker/docker/pkg/stdcopy"
"github.com/drone/drone/model"
"github.com/drone/drone/remote"
"github.com/drone/drone/shared/docker"
"github.com/samalba/dockerclient"
)
type Engine interface {
Schedule(*Task)
Cancel(int64, int64, *model.Node) error
Stream(int64, int64, *model.Node) (io.ReadCloser, error)
Deallocate(*model.Node)
Allocate(*model.Node) bool
Subscribe(chan *Event)
Unsubscribe(chan *Event)
}
var (
// options to fetch the stdout and stderr logs
logOpts = &dockerclient.LogOptions{
Stdout: true,
Stderr: true,
}
// options to fetch the stdout and stderr logs
// by tailing the output.
logOptsTail = &dockerclient.LogOptions{
Follow: true,
Stdout: true,
Stderr: true,
}
// error when the system cannot find logs
errLogging = errors.New("Logs not available")
)
type engine struct {
db *sql.DB
bus *eventbus
updater *updater
pool *pool
}
// Load creates a new build engine, loaded with registered nodes from the
// database. The registered nodes are added to the pool of nodes to immediately
// start accepting workloads.
func Load(db *sql.DB, remote remote.Remote) Engine {
engine := &engine{}
engine.bus = newEventbus()
engine.pool = newPool()
engine.db = db
engine.updater = &updater{engine.bus, db, remote}
nodes, err := model.GetNodeList(db)
if err != nil {
log.Fatalf("failed to get nodes from database. %s", err)
}
for _, node := range nodes {
engine.pool.allocate(node)
log.Infof("registered docker daemon %s", node.Addr)
}
return engine
}
// Cancel cancels the job running on the specified Node.
func (e *engine) Cancel(build, job int64, node *model.Node) error {
client, err := dockerclient.NewDockerClient(node.Addr, nil)
if err != nil {
return err
}
id := fmt.Sprintf("drone_build_%d_job_%d", build, job)
return client.StopContainer(id, 30)
}
// Stream streams the job output from the specified Node.
func (e *engine) Stream(build, job int64, node *model.Node) (io.ReadCloser, error) {
client, err := dockerclient.NewDockerClient(node.Addr, nil)
if err != nil {
log.Errorf("cannot create Docker client for node %s", node.Addr)
return nil, err
}
id := fmt.Sprintf("drone_build_%d_job_%d", build, job)
log.Debugf("streaming container logs %s", id)
return client.ContainerLogs(id, logOptsTail)
}
// Subscribe subscribes the channel to all build events.
func (e *engine) Subscribe(c chan *Event) {
e.bus.subscribe(c)
}
// Unsubscribe unsubscribes the channel from all build events.
func (e *engine) Unsubscribe(c chan *Event) {
e.bus.unsubscribe(c)
}
func (e *engine) Allocate(node *model.Node) bool {
log.Infof("registered docker daemon %s", node.Addr)
return e.pool.allocate(node)
}
func (e *engine) Deallocate(n *model.Node) {
nodes := e.pool.list()
for _, node := range nodes {
if node.ID == n.ID {
log.Infof("un-registered docker daemon %s", node.Addr)
e.pool.deallocate(node)
break
}
}
}
func (e *engine) Schedule(req *Task) {
node := <-e.pool.reserve()
// since we are probably running in a go-routine
// make sure we recover from any panics so that
// a bug doesn't crash the whole system.
defer func() {
if err := recover(); err != nil {
const size = 64 << 10
buf := make([]byte, size)
buf = buf[:runtime.Stack(buf, false)]
log.Printf("panic running build: %v\n%s", err, buf)
}
e.pool.release(node)
}()
// update the node that was allocated to each job
func(id int64) {
tx, err := e.db.Begin()
if err != nil {
log.Errorf("error updating job to persist node. %s", err)
return
}
defer tx.Commit()
for _, job := range req.Jobs {
job.NodeID = id
model.UpdateJob(e.db, job)
}
}(node.ID)
// run the full build!
client, err := newDockerClient(node.Addr, node.Cert, node.Key, node.CA)
if err != nil {
log.Errorln("error creating docker client", err)
}
// update the build state if any of the sub-tasks
// had a non-success status
req.Build.Started = time.Now().UTC().Unix()
req.Build.Status = model.StatusRunning
e.updater.SetBuild(req)
// run all bulid jobs
for _, job := range req.Jobs {
req.Job = job
runJob(req, e.updater, client)
}
// TODO
req.Build.Status = model.StatusSuccess
for _, job := range req.Jobs {
if job.Status != model.StatusSuccess {
req.Build.Status = job.Status
break
}
}
req.Build.Finished = time.Now().UTC().Unix()
err = e.updater.SetBuild(req)
if err != nil {
log.Errorf("error updating build completion status. %s", err)
}
// run notifications!!!
// for _ = range req.Jobs {
// err := runJobNotify(req, client)
// if err != nil {
// log.Errorf("error executing notification step. %s", err)
// }
// break
// }
}
func newDockerClient(addr, cert, key, ca string) (dockerclient.Client, error) {
var tlc *tls.Config
// create the Docket client TLS config
if len(cert) != 0 {
cert_, err := tls.LoadX509KeyPair(cert, key)
if err != nil {
return dockerclient.NewDockerClient(addr, nil)
}
// create the TLS configuration for secure
// docker communications.
tlc = &tls.Config{
Certificates: []tls.Certificate{cert_},
}
// use the certificate authority if provided.
// else don't use a certificate authority and set
// skip verify to true
if len(ca) != 0 {
pool := x509.NewCertPool()
pool.AppendCertsFromPEM([]byte(ca))
tlc.RootCAs = pool
} else {
tlc.InsecureSkipVerify = true
}
}
// create the Docker client. In this version of Drone (alpha)
// we do not spread builds across clients, but this can and
// (probably) will change in the future.
return dockerclient.NewDockerClient(addr, tlc)
}
func runJob(r *Task, updater *updater, client dockerclient.Client) error {
name := fmt.Sprintf("drone_build_%d_job_%d", r.Build.ID, r.Job.ID)
defer func() {
if r.Job.Status == model.StatusRunning {
r.Job.Status = model.StatusError
r.Job.Finished = time.Now().UTC().Unix()
r.Job.ExitCode = 255
}
if r.Job.Status == model.StatusPending {
r.Job.Status = model.StatusError
r.Job.Started = time.Now().UTC().Unix()
r.Job.Finished = time.Now().UTC().Unix()
r.Job.ExitCode = 255
}
updater.SetJob(r)
client.KillContainer(name, "9")
client.RemoveContainer(name, true, true)
}()
// marks the task as running
r.Job.Status = model.StatusRunning
r.Job.Started = time.Now().UTC().Unix()
// encode the build payload to write to stdin
// when launching the build container
in, err := encodeToLegacyFormat(r)
if err != nil {
log.Errorf("failure to marshal work. %s", err)
return err
}
// CREATE AND START BUILD
args := DefaultBuildArgs
if r.Build.Event == model.EventPull {
args = DefaultPullRequestArgs
}
args = append(args, "--")
args = append(args, string(in))
conf := &dockerclient.ContainerConfig{
Image: DefaultAgent,
Entrypoint: DefaultEntrypoint,
Cmd: args,
HostConfig: dockerclient.HostConfig{
Binds: []string{"/var/run/docker.sock:/var/run/docker.sock"},
},
Volumes: map[string]struct{}{
"/var/run/docker.sock": struct{}{},
},
}
// w.client.PullImage(conf.Image, nil)
_, err = docker.RunDaemon(client, conf, name)
if err != nil {
log.Errorf("error starting build container. %s", err)
return err
}
// UPDATE STATUS
err = updater.SetJob(r)
if err != nil {
log.Errorf("error updating job status as running. %s", err)
return err
}
// WAIT FOR OUTPUT
info, builderr := docker.Wait(client, name)
switch {
case info.State.ExitCode == 128:
r.Job.ExitCode = info.State.ExitCode
r.Job.Status = model.StatusKilled
case info.State.ExitCode == 130:
r.Job.ExitCode = info.State.ExitCode
r.Job.Status = model.StatusKilled
case builderr != nil:
r.Job.Status = model.StatusError
case info.State.ExitCode != 0:
r.Job.ExitCode = info.State.ExitCode
r.Job.Status = model.StatusFailure
default:
r.Job.Status = model.StatusSuccess
}
// send the logs to the datastore
var buf bytes.Buffer
rc, err := client.ContainerLogs(name, docker.LogOpts)
if err != nil && builderr != nil {
buf.WriteString("Error launching build")
buf.WriteString(builderr.Error())
} else if err != nil {
buf.WriteString("Error launching build")
buf.WriteString(err.Error())
log.Errorf("error opening connection to logs. %s", err)
return err
} else {
defer rc.Close()
stdcopy.StdCopy(&buf, &buf, io.LimitReader(rc, 5000000))
}
// update the task in the datastore
r.Job.Finished = time.Now().UTC().Unix()
err = updater.SetJob(r)
if err != nil {
log.Errorf("error updating job after completion. %s", err)
return err
}
err = updater.SetLogs(r, ioutil.NopCloser(&buf))
if err != nil {
log.Errorf("error updating logs. %s", err)
return err
}
log.Debugf("completed job %d with status %s.", r.Job.ID, r.Job.Status)
return nil
}
func runJobNotify(r *Task, client dockerclient.Client) error {
name := fmt.Sprintf("drone_build_%d_notify", r.Build.ID)
defer func() {
client.KillContainer(name, "9")
client.RemoveContainer(name, true, true)
}()
// encode the build payload to write to stdin
// when launching the build container
in, err := encodeToLegacyFormat(r)
if err != nil {
log.Errorf("failure to marshal work. %s", err)
return err
}
args := DefaultNotifyArgs
args = append(args, "--")
args = append(args, string(in))
conf := &dockerclient.ContainerConfig{
Image: DefaultAgent,
Entrypoint: DefaultEntrypoint,
Cmd: args,
HostConfig: dockerclient.HostConfig{},
}
_, err = docker.Run(client, conf, name)
return err
}

86
engine/pool.go Normal file
View File

@ -0,0 +1,86 @@
package engine
import (
"sync"
"github.com/drone/drone/model"
)
type pool struct {
sync.Mutex
nodes map[*model.Node]bool
nodec chan *model.Node
}
func newPool() *pool {
return &pool{
nodes: make(map[*model.Node]bool),
nodec: make(chan *model.Node, 999),
}
}
// Allocate allocates a node to the pool to
// be available to accept work.
func (p *pool) allocate(n *model.Node) bool {
if p.isAllocated(n) {
return false
}
p.Lock()
p.nodes[n] = true
p.Unlock()
p.nodec <- n
return true
}
// IsAllocated is a helper function that returns
// true if the node is currently allocated to
// the pool.
func (p *pool) isAllocated(n *model.Node) bool {
p.Lock()
defer p.Unlock()
_, ok := p.nodes[n]
return ok
}
// Deallocate removes the node from the pool of
// available nodes. If the node is currently
// reserved and performing work it will finish,
// but no longer be given new work.
func (p *pool) deallocate(n *model.Node) {
p.Lock()
defer p.Unlock()
delete(p.nodes, n)
}
// List returns a list of all model.Nodes currently
// allocated to the pool.
func (p *pool) list() []*model.Node {
p.Lock()
defer p.Unlock()
var nodes []*model.Node
for n := range p.nodes {
nodes = append(nodes, n)
}
return nodes
}
// Reserve reserves the next available node to
// start doing work. Once work is complete, the
// node should be released back to the pool.
func (p *pool) reserve() <-chan *model.Node {
return p.nodec
}
// Release releases the node back to the pool
// of available nodes.
func (p *pool) release(n *model.Node) bool {
if !p.isAllocated(n) {
return false
}
p.nodec <- n
return true
}

89
engine/pool_test.go Normal file
View File

@ -0,0 +1,89 @@
package engine
import (
"testing"
"github.com/drone/drone/model"
"github.com/franela/goblin"
)
func TestPool(t *testing.T) {
g := goblin.Goblin(t)
g.Describe("Pool", func() {
g.It("Should allocate nodes", func() {
n := &model.Node{Addr: "unix:///var/run/docker.sock"}
pool := newPool()
pool.allocate(n)
g.Assert(len(pool.nodes)).Equal(1)
g.Assert(len(pool.nodec)).Equal(1)
g.Assert(pool.nodes[n]).Equal(true)
})
g.It("Should not re-allocate an allocated node", func() {
n := &model.Node{Addr: "unix:///var/run/docker.sock"}
pool := newPool()
g.Assert(pool.allocate(n)).Equal(true)
g.Assert(pool.allocate(n)).Equal(false)
})
g.It("Should reserve a node", func() {
n := &model.Node{Addr: "unix:///var/run/docker.sock"}
pool := newPool()
pool.allocate(n)
g.Assert(<-pool.reserve()).Equal(n)
})
g.It("Should release a node", func() {
n := &model.Node{Addr: "unix:///var/run/docker.sock"}
pool := newPool()
pool.allocate(n)
g.Assert(len(pool.nodec)).Equal(1)
g.Assert(<-pool.reserve()).Equal(n)
g.Assert(len(pool.nodec)).Equal(0)
pool.release(n)
g.Assert(len(pool.nodec)).Equal(1)
g.Assert(<-pool.reserve()).Equal(n)
g.Assert(len(pool.nodec)).Equal(0)
})
g.It("Should not release an unallocated node", func() {
n := &model.Node{Addr: "unix:///var/run/docker.sock"}
pool := newPool()
g.Assert(len(pool.nodes)).Equal(0)
g.Assert(len(pool.nodec)).Equal(0)
pool.release(n)
g.Assert(len(pool.nodes)).Equal(0)
g.Assert(len(pool.nodec)).Equal(0)
pool.release(nil)
g.Assert(len(pool.nodes)).Equal(0)
g.Assert(len(pool.nodec)).Equal(0)
})
g.It("Should list all allocated nodes", func() {
n1 := &model.Node{Addr: "unix:///var/run/docker.sock"}
n2 := &model.Node{Addr: "unix:///var/run/docker.sock"}
pool := newPool()
pool.allocate(n1)
pool.allocate(n2)
g.Assert(len(pool.nodes)).Equal(2)
g.Assert(len(pool.nodec)).Equal(2)
g.Assert(len(pool.list())).Equal(2)
})
g.It("Should remove a node", func() {
n1 := &model.Node{Addr: "unix:///var/run/docker.sock"}
n2 := &model.Node{Addr: "unix:///var/run/docker.sock"}
pool := newPool()
pool.allocate(n1)
pool.allocate(n2)
g.Assert(len(pool.nodes)).Equal(2)
pool.deallocate(n1)
pool.deallocate(n2)
g.Assert(len(pool.nodes)).Equal(0)
g.Assert(len(pool.list())).Equal(0)
})
})
}

View File

@ -1,123 +0,0 @@
package builtin
import (
"errors"
"sync"
"github.com/drone/drone/pkg/queue"
)
var ErrNotFound = errors.New("work item not found")
type Queue struct {
sync.Mutex
acks map[*queue.Work]struct{}
items map[*queue.Work]struct{}
itemc chan *queue.Work
}
func New() *Queue {
return &Queue{
acks: make(map[*queue.Work]struct{}),
items: make(map[*queue.Work]struct{}),
itemc: make(chan *queue.Work, 999),
}
}
// Publish inserts work at the tail of this queue, waiting for
// space to become available if the queue is full.
func (q *Queue) Publish(work *queue.Work) error {
q.Lock()
q.items[work] = struct{}{}
q.Unlock()
q.itemc <- work
return nil
}
// Remove removes the specified work item from this queue,
// if it is present.
func (q *Queue) Remove(work *queue.Work) error {
q.Lock()
defer q.Unlock()
_, ok := q.items[work]
if !ok {
return ErrNotFound
}
var items []*queue.Work
// loop through and drain all items
// from the queue.
drain:
for {
select {
case item := <-q.itemc:
items = append(items, item)
default:
break drain
}
}
// re-add all items to the queue except
// the item we're trying to remove
for _, item := range items {
if item == work {
delete(q.items, work)
delete(q.acks, work)
continue
}
q.itemc <- item
}
return nil
}
// Pull retrieves and removes the head of this queue, waiting
// if necessary until work becomes available.
func (q *Queue) Pull() *queue.Work {
work := <-q.itemc
q.Lock()
delete(q.items, work)
q.acks[work] = struct{}{}
q.Unlock()
return work
}
// PullClose retrieves and removes the head of this queue,
// waiting if necessary until work becomes available. The
// CloseNotifier should be provided to clone the channel
// if the subscribing client terminates its connection.
func (q *Queue) PullClose(cn queue.CloseNotifier) *queue.Work {
for {
select {
case <-cn.CloseNotify():
return nil
case work := <-q.itemc:
q.Lock()
delete(q.items, work)
q.acks[work] = struct{}{}
q.Unlock()
return work
}
}
}
// Ack acknowledges an item in the queue was processed.
func (q *Queue) Ack(work *queue.Work) error {
q.Lock()
delete(q.acks, work)
q.Unlock()
return nil
}
// Items returns a slice containing all of the work in this
// queue, in proper sequence.
func (q *Queue) Items() []*queue.Work {
q.Lock()
defer q.Unlock()
items := []*queue.Work{}
for work := range q.items {
items = append(items, work)
}
return items
}

View File

@ -1,98 +0,0 @@
package builtin
import (
"sync"
"testing"
. "github.com/drone/drone/Godeps/_workspace/src/github.com/franela/goblin"
"github.com/drone/drone/pkg/queue"
)
func TestBuild(t *testing.T) {
g := Goblin(t)
g.Describe("Queue", func() {
g.It("Should publish item", func() {
w1 := &queue.Work{}
w2 := &queue.Work{}
q := New()
q.Publish(w1)
q.Publish(w2)
g.Assert(len(q.items)).Equal(2)
g.Assert(len(q.itemc)).Equal(2)
})
g.It("Should remove item", func() {
w1 := &queue.Work{}
w2 := &queue.Work{}
w3 := &queue.Work{}
q := New()
q.Publish(w1)
q.Publish(w2)
q.Publish(w3)
q.Remove(w2)
g.Assert(len(q.items)).Equal(2)
g.Assert(len(q.itemc)).Equal(2)
g.Assert(q.Pull()).Equal(w1)
g.Assert(q.Pull()).Equal(w3)
g.Assert(q.Remove(w2)).Equal(ErrNotFound)
})
g.It("Should pull item", func() {
w1 := &queue.Work{}
w2 := &queue.Work{}
q := New()
c := new(closeNotifier)
q.Publish(w1)
q.Publish(w2)
g.Assert(q.Pull()).Equal(w1)
g.Assert(q.PullClose(c)).Equal(w2)
g.Assert(q.acks[w1]).Equal(struct{}{})
g.Assert(q.acks[w2]).Equal(struct{}{})
g.Assert(len(q.acks)).Equal(2)
})
g.It("Should cancel pulling item", func() {
q := New()
c := new(closeNotifier)
c.closec = make(chan bool, 1)
var wg sync.WaitGroup
go func() {
wg.Add(1)
g.Assert(q.PullClose(c) == nil).IsTrue()
wg.Done()
}()
go func() {
c.closec <- true
}()
wg.Wait()
})
g.It("Should ack item", func() {
w := &queue.Work{}
c := new(closeNotifier)
q := New()
q.Publish(w)
g.Assert(q.PullClose(c)).Equal(w)
g.Assert(len(q.acks)).Equal(1)
g.Assert(q.Ack(w)).Equal(nil)
g.Assert(len(q.acks)).Equal(0)
})
g.It("Should get all items", func() {
q := New()
q.Publish(&queue.Work{})
q.Publish(&queue.Work{})
q.Publish(&queue.Work{})
g.Assert(len(q.Items())).Equal(3)
})
})
}
type closeNotifier struct {
closec chan bool
}
func (c *closeNotifier) CloseNotify() <-chan bool {
return c.closec
}

View File

@ -1,318 +0,0 @@
package builtin
import (
"bytes"
"crypto/tls"
"crypto/x509"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"os"
"time"
"github.com/drone/drone/Godeps/_workspace/src/github.com/samalba/dockerclient"
"github.com/drone/drone/pkg/docker"
"github.com/drone/drone/pkg/queue"
"github.com/drone/drone/pkg/types"
log "github.com/drone/drone/Godeps/_workspace/src/github.com/Sirupsen/logrus"
)
var (
// Defult docker host address
DefaultHost = "unix:///var/run/docker.sock"
// Docker host address from environment variable
DockerHost = os.Getenv("DOCKER_HOST")
// Docker TLS variables
DockerHostCa = os.Getenv("DOCKER_CA")
DockerHostKey = os.Getenv("DOCKER_KEY")
DockerHostCert = os.Getenv("DOCKER_CERT")
)
func init() {
// if the environment doesn't specify a DOCKER_HOST
// we should use the default Docker socket.
if len(DockerHost) == 0 {
DockerHost = DefaultHost
}
}
type Runner struct {
Updater
}
func newDockerClient() (dockerclient.Client, error) {
var tlc *tls.Config
// create the Docket client TLS config
if len(DockerHostCert) > 0 && len(DockerHostKey) > 0 && len(DockerHostCa) > 0 {
cert, err := tls.LoadX509KeyPair(DockerHostCert, DockerHostKey)
if err != nil {
log.Errorf("failure to load SSL cert and key. %s", err)
return dockerclient.NewDockerClient(DockerHost, nil)
}
caCert, err := ioutil.ReadFile(DockerHostCa)
if err != nil {
log.Errorf("failure to load SSL CA cert. %s", err)
return dockerclient.NewDockerClient(DockerHost, nil)
}
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCert)
tlc = &tls.Config{
Certificates: []tls.Certificate{cert},
RootCAs: caCertPool,
}
}
// create the Docker client. In this version of Drone (alpha)
// we do not spread builds across clients, but this can and
// (probably) will change in the future.
return dockerclient.NewDockerClient(DockerHost, tlc)
}
func (r *Runner) Run(w *queue.Work) error {
var workers []*worker
var client dockerclient.Client
defer func() {
recover()
// ensures that all containers have been removed
// from the host machine.
for _, worker := range workers {
worker.Remove()
}
// if any part of the commit fails and leaves
// behind orphan sub-builds we need to cleanup
// after ourselves.
if w.Build.Status == types.StateRunning {
// if any tasks are running or pending
// we should mark them as complete.
for _, b := range w.Build.Jobs {
if b.Status == types.StateRunning {
b.Status = types.StateError
b.Finished = time.Now().UTC().Unix()
b.ExitCode = 255
}
if b.Status == types.StatePending {
b.Status = types.StateError
b.Started = time.Now().UTC().Unix()
b.Finished = time.Now().UTC().Unix()
b.ExitCode = 255
}
r.SetJob(w.Repo, w.Build, b)
}
// must populate build start
if w.Build.Started == 0 {
w.Build.Started = time.Now().UTC().Unix()
}
// mark the build as complete (with error)
w.Build.Status = types.StateError
w.Build.Finished = time.Now().UTC().Unix()
r.SetBuild(w.User, w.Repo, w.Build)
}
}()
// marks the build as running
w.Build.Started = time.Now().UTC().Unix()
w.Build.Status = types.StateRunning
err := r.SetBuild(w.User, w.Repo, w.Build)
if err != nil {
log.Errorf("failure to set build. %s", err)
return err
}
// create the Docker client. In this version of Drone (alpha)
// we do not spread builds across clients, but this can and
// (probably) will change in the future.
client, err = newDockerClient()
if err != nil {
log.Errorf("failure to connect to docker. %s", err)
return err
}
// loop through and execute the build and
// clone steps for each build job.
for _, job := range w.Build.Jobs {
// marks the task as running
job.Status = types.StateRunning
job.Started = time.Now().UTC().Unix()
err = r.SetJob(w.Repo, w.Build, job)
if err != nil {
log.Errorf("failure to set job. %s", err)
return err
}
work := &work{
System: w.System,
Workspace: &types.Workspace{Netrc: w.Netrc, Keys: w.Keys},
Repo: w.Repo,
Build: w.Build,
Job: job,
Secret: string(w.Secret),
Config: string(w.Config),
}
in, err := json.Marshal(work)
if err != nil {
log.Errorf("failure to marshalise work. %s", err)
return err
}
worker := newWorker(client)
workers = append(workers, worker)
cname := cname(job)
pullrequest := (w.Build.PullRequest != nil && w.Build.PullRequest.Number != 0)
state, builderr := worker.Build(cname, in, pullrequest)
switch {
case state == 128:
job.ExitCode = state
job.Status = types.StateKilled
case state == 130:
job.ExitCode = state
job.Status = types.StateKilled
case builderr != nil:
job.Status = types.StateError
case state != 0:
job.ExitCode = state
job.Status = types.StateFailure
default:
job.Status = types.StateSuccess
}
// send the logs to the datastore
var buf bytes.Buffer
rc, err := worker.Logs()
if err != nil && builderr != nil {
buf.WriteString("001 Error launching build")
buf.WriteString(builderr.Error())
} else if err != nil {
buf.WriteString("002 Error launching build")
buf.WriteString(err.Error())
return err
} else {
defer rc.Close()
docker.StdCopy(&buf, &buf, rc)
}
err = r.SetLogs(w.Repo, w.Build, job, ioutil.NopCloser(&buf))
if err != nil {
return err
}
// update the task in the datastore
job.Finished = time.Now().UTC().Unix()
err = r.SetJob(w.Repo, w.Build, job)
if err != nil {
return err
}
}
// update the build state if any of the sub-tasks
// had a non-success status
w.Build.Status = types.StateSuccess
for _, job := range w.Build.Jobs {
if job.Status != types.StateSuccess {
w.Build.Status = job.Status
break
}
}
err = r.SetBuild(w.User, w.Repo, w.Build)
if err != nil {
return err
}
// loop through and execute the notifications and
// the destroy all containers afterward.
for i, job := range w.Build.Jobs {
work := &work{
System: w.System,
Workspace: &types.Workspace{Netrc: w.Netrc, Keys: w.Keys},
Repo: w.Repo,
Build: w.Build,
Job: job,
Secret: string(w.Secret),
Config: string(w.Config),
}
in, err := json.Marshal(work)
if err != nil {
return err
}
workers[i].Notify(in)
break
}
return nil
}
func (r *Runner) Cancel(job *types.Job) error {
client, err := newDockerClient()
if err != nil {
return err
}
return client.StopContainer(cname(job), 30)
}
func (r *Runner) Logs(job *types.Job) (io.ReadCloser, error) {
client, err := newDockerClient()
if err != nil {
return nil, err
}
// make sure this container actually exists
info, err := client.InspectContainer(cname(job))
if err != nil {
// add a small exponential backoff since there
// is a small window when the container hasn't
// been created yet, but the build is about to start
for i := 0; ; i++ {
time.Sleep(1 * time.Second)
info, err = client.InspectContainer(cname(job))
if err != nil && i == 5 {
return nil, err
}
if err == nil {
break
}
}
}
// verify the container is running. if not we'll
// do an exponential backoff and attempt to wait
if !info.State.Running {
for i := 0; ; i++ {
time.Sleep(1 * time.Second)
info, err = client.InspectContainer(info.Id)
if err != nil {
return nil, err
}
if info.State.Running {
break
}
if i == 5 {
return nil, dockerclient.ErrNotFound
}
}
}
return client.ContainerLogs(info.Id, logOptsTail)
}
func cname(job *types.Job) string {
return fmt.Sprintf("drone-%d", job.ID)
}
func (r *Runner) Poll(q queue.Queue) {
for {
w := q.Pull()
q.Ack(w)
err := r.Run(w)
if err != nil {
log.Error(err)
}
}
}

24
engine/types.go Normal file
View File

@ -0,0 +1,24 @@
package engine
import (
"github.com/drone/drone/model"
)
type Event struct {
Name string
Msg []byte
}
type Task struct {
User *model.User `json:"-"`
Repo *model.Repo `json:"repo"`
Build *model.Build `json:"build"`
BuildPrev *model.Build `json:"build_last"`
Jobs []*model.Job `json:"jobs"`
Job *model.Job `json:"job"`
Keys *model.Key `json:"keys"`
Netrc *model.Netrc `json:"netrc"`
Config string `json:"config"`
Secret string `json:"secret"`
System *model.System `json:"system"`
}

View File

@ -1,94 +1,67 @@
package builtin
package engine
import (
"database/sql"
"encoding/json"
"fmt"
"io"
"github.com/drone/drone/pkg/bus"
"github.com/drone/drone/pkg/remote"
"github.com/drone/drone/pkg/store"
"github.com/drone/drone/pkg/types"
"github.com/drone/drone/model"
"github.com/drone/drone/remote"
)
type Updater interface {
SetBuild(*types.User, *types.Repo, *types.Build) error
SetJob(*types.Repo, *types.Build, *types.Job) error
SetLogs(*types.Repo, *types.Build, *types.Job, io.ReadCloser) error
}
// NewUpdater returns an implementation of the Updater interface
// that directly modifies the database and sends messages to the bus.
func NewUpdater(bus bus.Bus, store store.Store, rem remote.Remote) Updater {
return &updater{bus, store, rem}
}
type updater struct {
bus bus.Bus
store store.Store
bus *eventbus
db *sql.DB
remote remote.Remote
}
func (u *updater) SetBuild(user *types.User, r *types.Repo, c *types.Build) error {
err := u.store.SetBuild(c)
func (u *updater) SetBuild(r *Task) error {
err := model.UpdateBuild(u.db, r.Build)
if err != nil {
return err
}
err = u.remote.Status(user, r, c)
err = u.remote.Status(r.User, r.Repo, r.Build, fmt.Sprintf("%s/%s/%d", r.System.Link, r.Repo.FullName, r.Build.Number))
if err != nil {
// log err
}
// we need this because builds coming from
// a remote agent won't have the embedded
// build list. we should probably just rethink
// the messaging instead of this hack.
if c.Jobs == nil || len(c.Jobs) == 0 {
c.Jobs, _ = u.store.JobList(c)
}
msg, err := json.Marshal(c)
msg, err := json.Marshal(&payload{r.Build, r.Jobs})
if err != nil {
return err
}
u.bus.Send(&bus.Event{
Name: r.FullName,
Kind: bus.EventRepo,
u.bus.send(&Event{
Name: r.Repo.FullName,
Msg: msg,
})
return nil
}
func (u *updater) SetJob(r *types.Repo, c *types.Build, j *types.Job) error {
err := u.store.SetJob(j)
func (u *updater) SetJob(r *Task) error {
err := model.UpdateJob(u.db, r.Job)
if err != nil {
return err
}
// we need this because builds coming from
// a remote agent won't have the embedded
// build list. we should probably just rethink
// the messaging instead of this hack.
if c.Jobs == nil || len(c.Jobs) == 0 {
c.Jobs, _ = u.store.JobList(c)
}
msg, err := json.Marshal(c)
msg, err := json.Marshal(&payload{r.Build, r.Jobs})
if err != nil {
return err
}
u.bus.Send(&bus.Event{
Name: r.FullName,
Kind: bus.EventRepo,
u.bus.send(&Event{
Name: r.Repo.FullName,
Msg: msg,
})
return nil
}
func (u *updater) SetLogs(r *types.Repo, c *types.Build, j *types.Job, rc io.ReadCloser) error {
path := fmt.Sprintf("/logs/%s/%v/%v", r.FullName, c.Number, j.Number)
return u.store.SetBlobReader(path, rc)
func (u *updater) SetLogs(r *Task, rc io.ReadCloser) error {
return model.SetLog(u.db, r.Job, rc)
}
type payload struct {
*model.Build
Jobs []*model.Job `json:"jobs"`
}

35
engine/util.go Normal file
View File

@ -0,0 +1,35 @@
package engine
import (
"encoding/json"
)
func encodeToLegacyFormat(t *Task) ([]byte, error) {
t.System.Plugins = append(t.System.Plugins, "plugins/*")
s := map[string]interface{}{}
s["repo"] = t.Repo
s["config"] = t.Config
s["secret"] = t.Secret
s["job"] = t.Job
s["system"] = t.System
s["workspace"] = map[string]interface{}{
"netrc": t.Netrc,
"keys": t.Keys,
}
s["build"] = map[string]interface{}{
"number": t.Build.Number,
"status": t.Build.Status,
"head_commit": map[string]interface{}{
"sha": t.Build.Commit,
"ref": t.Build.Ref,
"branch": t.Build.Branch,
"message": t.Build.Message,
"author": map[string]interface{}{
"login": t.Build.Author,
"email": t.Build.Email,
},
},
}
return json.Marshal(&s)
}

View File

@ -1,30 +1,10 @@
package builtin
package engine
import (
"errors"
"io"
"io/ioutil"
"github.com/drone/drone/Godeps/_workspace/src/github.com/samalba/dockerclient"
"github.com/drone/drone/pkg/types"
)
var ErrLogging = errors.New("Logs not available")
var (
// options to fetch the stdout and stderr logs
logOpts = &dockerclient.LogOptions{
Stdout: true,
Stderr: true,
}
// options to fetch the stdout and stderr logs
// by tailing the output.
logOptsTail = &dockerclient.LogOptions{
Follow: true,
Stdout: true,
Stderr: true,
}
"github.com/drone/drone/shared/docker"
"github.com/samalba/dockerclient"
)
var (
@ -35,26 +15,15 @@ var (
DefaultEntrypoint = []string{"/bin/drone-exec"}
// default argument to invoke build steps
DefaultBuildArgs = []string{"--pull", "--cache", "--clone", "--build", "--deploy"}
DefaultBuildArgs = []string{"--cache", "--debug", "--clone", "--build", "--deploy"}
// default argument to invoke build steps
DefaultPullRequestArgs = []string{"--pull", "--cache", "--clone", "--build"}
DefaultPullRequestArgs = []string{"--cache", "--clone", "--build"}
// default arguments to invoke notify steps
DefaultNotifyArgs = []string{"--pull", "--notify"}
DefaultNotifyArgs = []string{"--notify"}
)
type work struct {
Repo *types.Repo `json:"repo"`
Build *types.Build `json:"build"`
BuildLast *types.Build `json:"build_last"`
Job *types.Job `json:"job"`
System *types.System `json:"system"`
Workspace *types.Workspace `json:"workspace"`
Secret string `json:"secret"`
Config string `json:"config"`
}
type worker struct {
client dockerclient.Client
build *dockerclient.ContainerInfo
@ -84,7 +53,6 @@ func (w *worker) Build(name string, stdin []byte, pr bool) (_ int, err error) {
Binds: []string{"/var/run/docker.sock:/var/run/docker.sock"},
},
Volumes: map[string]struct{}{
"/drone": struct{}{},
"/var/run/docker.sock": struct{}{},
},
}
@ -92,9 +60,9 @@ func (w *worker) Build(name string, stdin []byte, pr bool) (_ int, err error) {
// TEMPORARY: always try to pull the new image for now
// since we'll be frequently updating the build image
// for the next few weeks
w.client.PullImage(conf.Image, nil)
// w.client.PullImage(conf.Image, nil)
w.build, err = run(w.client, conf, name)
w.build, err = docker.Run(w.client, conf, name)
if err != nil {
return 1, err
}
@ -103,16 +71,7 @@ func (w *worker) Build(name string, stdin []byte, pr bool) (_ int, err error) {
// Notify executes the notification steps.
func (w *worker) Notify(stdin []byte) error {
// use the affinity parameter in case we are
// using Docker swarm as a backend.
environment := []string{"affinity:container==" + w.build.Id}
// the build container is acting as an ambassador container
// with a shared filesystem .
volume := []string{w.build.Id}
// the command line arguments passed into the
// build agent container.
args := DefaultNotifyArgs
args = append(args, "--")
args = append(args, string(stdin))
@ -121,14 +80,11 @@ func (w *worker) Notify(stdin []byte) error {
Image: DefaultAgent,
Entrypoint: DefaultEntrypoint,
Cmd: args,
Env: environment,
HostConfig: dockerclient.HostConfig{
VolumesFrom: volume,
},
HostConfig: dockerclient.HostConfig{},
}
var err error
w.notify, err = run(w.client, conf, "")
w.notify, err = docker.Run(w.client, conf, "")
return err
}
@ -136,7 +92,7 @@ func (w *worker) Notify(stdin []byte) error {
// from the build and deploy agents.
func (w *worker) Logs() (io.ReadCloser, error) {
if w.build == nil {
return nil, ErrLogging
return nil, errLogging
}
return w.client.ContainerLogs(w.build.Id, logOpts)
}
@ -153,77 +109,3 @@ func (w *worker) Remove() {
w.client.RemoveContainer(w.build.Id, true, true)
}
}
// run is a helper function that creates and starts a container,
// blocking until either complete.
func run(client dockerclient.Client, conf *dockerclient.ContainerConfig, name string) (*dockerclient.ContainerInfo, error) {
// attempts to create the contianer
id, err := client.CreateContainer(conf, name)
if err != nil {
// and pull the image and re-create if that fails
client.PullImage(conf.Image, nil)
id, err = client.CreateContainer(conf, name)
// make sure the container is removed in
// the event of a creation error.
if err != nil && len(id) != 0 {
client.RemoveContainer(id, true, true)
}
if err != nil {
return nil, err
}
}
// ensures the container is always stopped
// and ready to be removed.
defer func() {
client.StopContainer(id, 5)
client.KillContainer(id, "9")
}()
// fetches the container information.
info, err := client.InspectContainer(id)
if err != nil {
return nil, err
}
// channel listening for errors while the
// container is running async.
errc := make(chan error, 1)
infoc := make(chan *dockerclient.ContainerInfo, 1)
go func() {
// starts the container
err := client.StartContainer(id, &conf.HostConfig)
if err != nil {
errc <- err
return
}
// blocks and waits for the container to finish
// by streaming the logs (to /dev/null). Ideally
// we could use the `wait` function instead
rc, err := client.ContainerLogs(id, logOptsTail)
if err != nil {
errc <- err
return
}
io.Copy(ioutil.Discard, rc)
rc.Close()
// fetches the container information
info, err := client.InspectContainer(id)
if err != nil {
errc <- err
return
}
infoc <- info
}()
select {
case info := <-infoc:
return info, nil
case err := <-errc:
return info, err
}
}

426
make.go
View File

@ -1,426 +0,0 @@
// +build ignore
// This program builds Drone.
// $ go run make.go deps bindata build test
package main
import (
"fmt"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"strings"
)
var (
version = "0.4"
sha = rev()
)
// list of all posible steps that can be executed
// as part of the build process.
var steps = map[string]step{
"deps": executeDeps,
"json": executeJson,
"embed": executeEmbed,
"scripts": executeScripts,
"styles": executeStyles,
"vet": executeVet,
"fmt": executeFmt,
"test": executeTest,
"build": executeBuild,
"install": executeInstall,
"image": executeImage,
"bindata": executeBindata,
"clean": executeClean,
}
func main() {
for _, arg := range os.Args[1:] {
step, ok := steps[arg]
if !ok {
fmt.Println("Error: Invalid step", arg)
os.Exit(1)
}
err := step()
if err != nil {
fmt.Println("Error: Failed step", arg)
os.Exit(1)
}
}
}
type step func() error
func executeDeps() error {
deps := []string{
"github.com/jteeuwen/go-bindata/...",
"golang.org/x/tools/cmd/cover",
}
for _, dep := range deps {
err := run(
"go",
"get",
"-u",
dep)
if err != nil {
return err
}
}
return nil
}
// json step generates optimized json marshal and
// unmarshal functions to override defaults.
func executeJson() error {
return nil
}
// embed step embeds static files in .go files.
func executeEmbed() error {
// embed drone.{revision}.css
// embed drone.{revision}.js
return nil
}
// scripts step concatinates all javascript files.
func executeScripts() error {
files := []string{
"cmd/drone-server/static/scripts/term.js",
"cmd/drone-server/static/scripts/drone.js",
"cmd/drone-server/static/scripts/controllers/repos.js",
"cmd/drone-server/static/scripts/controllers/builds.js",
"cmd/drone-server/static/scripts/controllers/users.js",
"cmd/drone-server/static/scripts/services/repos.js",
"cmd/drone-server/static/scripts/services/builds.js",
"cmd/drone-server/static/scripts/services/users.js",
"cmd/drone-server/static/scripts/services/logs.js",
"cmd/drone-server/static/scripts/services/tokens.js",
"cmd/drone-server/static/scripts/services/feed.js",
"cmd/drone-server/static/scripts/filters/filter.js",
"cmd/drone-server/static/scripts/filters/gravatar.js",
"cmd/drone-server/static/scripts/filters/time.js",
}
f, err := os.OpenFile(
"cmd/drone-server/static/scripts/drone.min.js",
os.O_CREATE|os.O_RDWR|os.O_TRUNC,
0660)
defer f.Close()
if err != nil {
fmt.Println("Failed to open output file")
return err
}
for _, input := range files {
content, err := ioutil.ReadFile(input)
if err != nil {
return err
}
f.Write(content)
}
return nil
}
// styles step concatinates the stylesheet files.
func executeStyles() error {
files := []string{
"cmd/drone-server/static/styles/reset.css",
"cmd/drone-server/static/styles/fonts.css",
"cmd/drone-server/static/styles/alert.css",
"cmd/drone-server/static/styles/blankslate.css",
"cmd/drone-server/static/styles/list.css",
"cmd/drone-server/static/styles/label.css",
"cmd/drone-server/static/styles/range.css",
"cmd/drone-server/static/styles/switch.css",
"cmd/drone-server/static/styles/main.css",
}
f, err := os.OpenFile(
"cmd/drone-server/static/styles/drone.min.css",
os.O_CREATE|os.O_RDWR|os.O_TRUNC,
0660)
defer f.Close()
if err != nil {
fmt.Println("Failed to open output file")
return err
}
for _, input := range files {
content, err := ioutil.ReadFile(input)
if err != nil {
return err
}
f.Write(content)
}
return nil
}
// vet step executes the `go vet` command
func executeVet() error {
return run(
"go",
"vet",
"github.com/drone/drone/pkg/...",
"github.com/drone/drone/cmd/...")
}
// fmt step executes the `go fmt` command
func executeFmt() error {
return run(
"go",
"fmt",
"github.com/drone/drone/pkg/...",
"github.com/drone/drone/cmd/...")
}
// test step executes unit tests and coverage.
func executeTest() error {
ldf := fmt.Sprintf(
"-X main.revision=%s -X main.version=%s",
sha,
version)
return run(
"go",
"test",
"-cover",
"-ldflags",
ldf,
"github.com/drone/drone/pkg/...",
"github.com/drone/drone/cmd/...")
}
// install step installs the application binaries.
func executeInstall() error {
var bins = []struct {
input string
}{
{
"github.com/drone/drone/cmd/drone-server",
},
}
for _, bin := range bins {
ldf := fmt.Sprintf(
"-X main.revision=%s -X main.version=%s",
sha,
version)
err := run(
"go",
"install",
"-ldflags",
ldf,
bin.input)
if err != nil {
return err
}
}
return nil
}
// build step creates the application binaries.
func executeBuild() error {
var bins = []struct {
input string
output string
}{
{
"github.com/drone/drone/cmd/drone-server",
"bin/drone",
},
}
for _, bin := range bins {
ldf := fmt.Sprintf(
"-X main.revision=%s -X main.version=%s",
sha,
version)
err := run(
"go",
"build",
"-o",
bin.output,
"-ldflags",
ldf,
bin.input)
if err != nil {
return err
}
}
return nil
}
// image step builds docker images.
func executeImage() error {
var images = []struct {
dir string
name string
}{
{
"bin/drone-server",
"drone/drone",
},
}
for _, image := range images {
path := filepath.Join(
image.dir,
"Dockerfile")
name := fmt.Sprintf("%s:%s",
image.name,
version)
err := run(
"docker",
"build",
"-rm",
path,
name)
if err != nil {
return err
}
}
return nil
}
// bindata step generates go-bindata package.
func executeBindata() error {
var paths = []struct {
input string
output string
pkg string
}{
{
"cmd/drone-server/static/...",
"cmd/drone-server/drone_bindata.go",
"main",
},
}
for _, path := range paths {
binErr := run(
"go-bindata",
fmt.Sprintf("-o=%s", path.output),
fmt.Sprintf("-pkg=%s", path.pkg),
path.input)
if binErr != nil {
return binErr
}
fmtErr := run(
"go",
"fmt",
path.output)
if fmtErr != nil {
return fmtErr
}
}
return nil
}
// clean step removes all generated files.
func executeClean() error {
err := filepath.Walk(".", func(path string, f os.FileInfo, err error) error {
suffixes := []string{
".out",
"_bindata.go",
}
for _, suffix := range suffixes {
if strings.HasSuffix(path, suffix) {
if err := os.Remove(path); err != nil {
return err
}
}
}
return nil
})
if err != nil {
return err
}
files := []string{
"bin/drone",
}
for _, file := range files {
if _, err := os.Stat(file); err != nil {
continue
}
if err := os.Remove(file); err != nil {
return err
}
}
return nil
}
// run is a helper function that executes commands
// and assigns stdout and stderr targets
func run(command string, args ...string) error {
cmd := exec.Command(command, args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
trace(cmd.Args)
return cmd.Run()
}
// helper function to parse the git revision
func rev() string {
cmd := exec.Command(
"git",
"rev-parse",
"--short",
"HEAD")
raw, err := cmd.CombinedOutput()
if err != nil {
return "HEAD"
}
return strings.Trim(string(raw), "\n")
}
// trace is a helper function that writes a command
// to stdout similar to bash +x
func trace(args []string) {
print("+ ")
println(strings.Join(args, " "))
}

View File

@ -1,48 +1,146 @@
package types
package model
const (
StatePending = "pending"
StateRunning = "running"
StateSuccess = "success"
StateFailure = "failure"
StateKilled = "killed"
StateError = "error"
import (
"time"
"github.com/drone/drone/shared/database"
"github.com/russross/meddler"
)
type Build struct {
ID int64 `json:"id"`
RepoID int64 `json:"-" sql:"unique:ux_build_number,index:ix_build_repo_id"`
Number int `json:"number" sql:"unique:ux_build_number"`
Event string `json:"event"`
Status string `json:"status"`
Started int64 `json:"started_at"`
Finished int64 `json:"finished_at"`
Commit *Commit `json:"head_commit"`
PullRequest *PullRequest `json:"pull_request,omitempty"`
Jobs []*Job `json:"jobs,omitempty" sql:"-"`
ID int64 `json:"id" meddler:"build_id,pk"`
RepoID int64 `json:"-" meddler:"build_repo_id"`
Number int `json:"number" meddler:"build_number"`
Event string `json:"event" meddler:"build_event"`
Status string `json:"status" meddler:"build_status"`
Created int64 `json:"created_at" meddler:"build_created"`
Started int64 `json:"started_at" meddler:"build_started"`
Finished int64 `json:"finished_at" meddler:"build_finished"`
Commit string `json:"commit" meddler:"build_commit"`
Branch string `json:"branch" meddler:"build_branch"`
Ref string `json:"ref" meddler:"build_ref"`
Refspec string `json:"refspec" meddler:"build_refspec"`
Remote string `json:"remote" meddler:"build_remote"`
Title string `json:"title" meddler:"build_title"`
Message string `json:"message" meddler:"build_message"`
Timestamp string `json:"timestamp" meddler:"build_timestamp"`
Author string `json:"author" meddler:"build_author"`
Avatar string `json:"author_avatar" meddler:"build_avatar"`
Email string `json:"author_email" meddler:"build_email"`
Link string `json:"link_url" meddler:"build_link"`
}
type PullRequest struct {
Number int `json:"number,omitempty"`
Title string `json:"title,omitempty"`
Link string `json:"link_url,omitempty"`
Base *Commit `json:"base_commit,omitempty"`
type BuildGroup struct {
Date string
Builds []*Build
}
type Commit struct {
Sha string `json:"sha"`
Ref string `json:"ref"`
Link string `json:"link_url,omitempty"`
Branch string `json:"branch" sql:"index:ix_commit_branch"`
Message string `json:"message"`
Timestamp string `json:"timestamp,omitempty"`
Remote string `json:"remote,omitempty"`
Author *Author `json:"author,omitempty"`
func GetBuild(db meddler.DB, id int64) (*Build, error) {
var build = new(Build)
var err = meddler.Load(db, buildTable, build, id)
return build, err
}
type Author struct {
Login string `json:"login,omitempty"`
Email string `json:"email,omitempty"`
func GetBuildNumber(db meddler.DB, repo *Repo, number int) (*Build, error) {
var build = new(Build)
var err = meddler.QueryRow(db, build, database.Rebind(buildNumberQuery), repo.ID, number)
return build, err
}
func GetBuildRef(db meddler.DB, repo *Repo, ref string) (*Build, error) {
var build = new(Build)
var err = meddler.QueryRow(db, build, database.Rebind(buildRefQuery), repo.ID, ref)
return build, err
}
func GetBuildCommit(db meddler.DB, repo *Repo, sha, branch string) (*Build, error) {
var build = new(Build)
var err = meddler.QueryRow(db, build, database.Rebind(buildCommitQuery), repo.ID, sha, branch)
return build, err
}
func GetBuildLast(db meddler.DB, repo *Repo, branch string) (*Build, error) {
var build = new(Build)
var err = meddler.QueryRow(db, build, database.Rebind(buildLastQuery), repo.ID, branch)
return build, err
}
func GetBuildList(db meddler.DB, repo *Repo) ([]*Build, error) {
var builds = []*Build{}
var err = meddler.QueryAll(db, &builds, database.Rebind(buildListQuery), repo.ID)
return builds, err
}
func CreateBuild(db meddler.DB, build *Build, jobs ...*Job) error {
var number int
db.QueryRow(buildNumberLast, build.RepoID).Scan(&number)
build.Number = number + 1
build.Created = time.Now().UTC().Unix()
err := meddler.Insert(db, buildTable, build)
if err != nil {
return err
}
for i, job := range jobs {
job.BuildID = build.ID
job.Number = i + 1
err = InsertJob(db, job)
if err != nil {
return err
}
}
return nil
}
func UpdateBuild(db meddler.DB, build *Build) error {
return meddler.Update(db, buildTable, build)
}
const buildTable = "builds"
const buildListQuery = `
SELECT *
FROM builds
WHERE build_repo_id = ?
ORDER BY build_number DESC
LIMIT 50
`
const buildNumberQuery = `
SELECT *
FROM builds
WHERE build_repo_id = ?
AND build_number = ?
LIMIT 1;
`
const buildLastQuery = `
SELECT *
FROM builds
WHERE build_repo_id = ?
AND build_branch = ?
ORDER BY build_number DESC
LIMIT 1
`
const buildCommitQuery = `
SELECT *
FROM builds
WHERE build_repo_id = ?
AND build_commit = ?
AND build_branch = ?
LIMIT 1
`
const buildRefQuery = `
SELECT *
FROM builds
WHERE build_repo_id = ?
AND build_ref = ?
LIMIT 1
`
const buildNumberLast = `
SELECT MAX(build_number)
FROM builds
WHERE build_repo_id = ?
`

207
model/build_test.go Normal file
View File

@ -0,0 +1,207 @@
package model
import (
"testing"
"github.com/drone/drone/shared/database"
"github.com/franela/goblin"
)
func TestBuild(t *testing.T) {
db := database.Open("sqlite3", ":memory:")
defer db.Close()
g := goblin.Goblin(t)
g.Describe("Builds", func() {
// before each test be sure to purge the package
// table data from the database.
g.BeforeEach(func() {
db.Exec("DELETE FROM builds")
db.Exec("DELETE FROM jobs")
})
g.It("Should Post a Build", func() {
build := Build{
RepoID: 1,
Status: StatusSuccess,
Commit: "85f8c029b902ed9400bc600bac301a0aadb144ac",
}
err := CreateBuild(db, &build, []*Job{}...)
g.Assert(err == nil).IsTrue()
g.Assert(build.ID != 0).IsTrue()
g.Assert(build.Number).Equal(1)
g.Assert(build.Commit).Equal("85f8c029b902ed9400bc600bac301a0aadb144ac")
})
g.It("Should Put a Build", func() {
build := Build{
RepoID: 1,
Number: 5,
Status: StatusSuccess,
Commit: "85f8c029b902ed9400bc600bac301a0aadb144ac",
}
CreateBuild(db, &build, []*Job{}...)
build.Status = StatusRunning
err1 := UpdateBuild(db, &build)
getbuild, err2 := GetBuild(db, build.ID)
g.Assert(err1 == nil).IsTrue()
g.Assert(err2 == nil).IsTrue()
g.Assert(build.ID).Equal(getbuild.ID)
g.Assert(build.RepoID).Equal(getbuild.RepoID)
g.Assert(build.Status).Equal(getbuild.Status)
g.Assert(build.Number).Equal(getbuild.Number)
})
g.It("Should Get a Build", func() {
build := Build{
RepoID: 1,
Status: StatusSuccess,
}
CreateBuild(db, &build, []*Job{}...)
getbuild, err := GetBuild(db, build.ID)
g.Assert(err == nil).IsTrue()
g.Assert(build.ID).Equal(getbuild.ID)
g.Assert(build.RepoID).Equal(getbuild.RepoID)
g.Assert(build.Status).Equal(getbuild.Status)
})
g.It("Should Get a Build by Number", func() {
build1 := &Build{
RepoID: 1,
Status: StatusPending,
}
build2 := &Build{
RepoID: 1,
Status: StatusPending,
}
err1 := CreateBuild(db, build1, []*Job{}...)
err2 := CreateBuild(db, build2, []*Job{}...)
getbuild, err3 := GetBuildNumber(db, &Repo{ID: 1}, build2.Number)
g.Assert(err1 == nil).IsTrue()
g.Assert(err2 == nil).IsTrue()
g.Assert(err3 == nil).IsTrue()
g.Assert(build2.ID).Equal(getbuild.ID)
g.Assert(build2.RepoID).Equal(getbuild.RepoID)
g.Assert(build2.Number).Equal(getbuild.Number)
})
g.It("Should Get a Build by Ref", func() {
build1 := &Build{
RepoID: 1,
Status: StatusPending,
Ref: "refs/pull/5",
}
build2 := &Build{
RepoID: 1,
Status: StatusPending,
Ref: "refs/pull/6",
}
err1 := CreateBuild(db, build1, []*Job{}...)
err2 := CreateBuild(db, build2, []*Job{}...)
getbuild, err3 := GetBuildRef(db, &Repo{ID: 1}, "refs/pull/6")
g.Assert(err1 == nil).IsTrue()
g.Assert(err2 == nil).IsTrue()
g.Assert(err3 == nil).IsTrue()
g.Assert(build2.ID).Equal(getbuild.ID)
g.Assert(build2.RepoID).Equal(getbuild.RepoID)
g.Assert(build2.Number).Equal(getbuild.Number)
g.Assert(build2.Ref).Equal(getbuild.Ref)
})
g.It("Should Get a Build by Ref", func() {
build1 := &Build{
RepoID: 1,
Status: StatusPending,
Ref: "refs/pull/5",
}
build2 := &Build{
RepoID: 1,
Status: StatusPending,
Ref: "refs/pull/6",
}
err1 := CreateBuild(db, build1, []*Job{}...)
err2 := CreateBuild(db, build2, []*Job{}...)
getbuild, err3 := GetBuildRef(db, &Repo{ID: 1}, "refs/pull/6")
g.Assert(err1 == nil).IsTrue()
g.Assert(err2 == nil).IsTrue()
g.Assert(err3 == nil).IsTrue()
g.Assert(build2.ID).Equal(getbuild.ID)
g.Assert(build2.RepoID).Equal(getbuild.RepoID)
g.Assert(build2.Number).Equal(getbuild.Number)
g.Assert(build2.Ref).Equal(getbuild.Ref)
})
g.It("Should Get a Build by Commit", func() {
build1 := &Build{
RepoID: 1,
Status: StatusPending,
Branch: "master",
Commit: "85f8c029b902ed9400bc600bac301a0aadb144ac",
}
build2 := &Build{
RepoID: 1,
Status: StatusPending,
Branch: "dev",
Commit: "85f8c029b902ed9400bc600bac301a0aadb144aa",
}
err1 := CreateBuild(db, build1, []*Job{}...)
err2 := CreateBuild(db, build2, []*Job{}...)
getbuild, err3 := GetBuildCommit(db, &Repo{ID: 1}, build2.Commit, build2.Branch)
g.Assert(err1 == nil).IsTrue()
g.Assert(err2 == nil).IsTrue()
g.Assert(err3 == nil).IsTrue()
g.Assert(build2.ID).Equal(getbuild.ID)
g.Assert(build2.RepoID).Equal(getbuild.RepoID)
g.Assert(build2.Number).Equal(getbuild.Number)
g.Assert(build2.Commit).Equal(getbuild.Commit)
g.Assert(build2.Branch).Equal(getbuild.Branch)
})
g.It("Should Get a Build by Commit", func() {
build1 := &Build{
RepoID: 1,
Status: StatusFailure,
Branch: "master",
Commit: "85f8c029b902ed9400bc600bac301a0aadb144ac",
}
build2 := &Build{
RepoID: 1,
Status: StatusSuccess,
Branch: "master",
Commit: "85f8c029b902ed9400bc600bac301a0aadb144aa",
}
err1 := CreateBuild(db, build1, []*Job{}...)
err2 := CreateBuild(db, build2, []*Job{}...)
getbuild, err3 := GetBuildLast(db, &Repo{ID: 1}, build2.Branch)
g.Assert(err1 == nil).IsTrue()
g.Assert(err2 == nil).IsTrue()
g.Assert(err3 == nil).IsTrue()
g.Assert(build2.ID).Equal(getbuild.ID)
g.Assert(build2.RepoID).Equal(getbuild.RepoID)
g.Assert(build2.Number).Equal(getbuild.Number)
g.Assert(build2.Status).Equal(getbuild.Status)
g.Assert(build2.Branch).Equal(getbuild.Branch)
g.Assert(build2.Commit).Equal(getbuild.Commit)
})
g.It("Should get recent Builds", func() {
build1 := &Build{
RepoID: 1,
Status: StatusFailure,
}
build2 := &Build{
RepoID: 1,
Status: StatusSuccess,
}
CreateBuild(db, build1, []*Job{}...)
CreateBuild(db, build2, []*Job{}...)
builds, err := GetBuildList(db, &Repo{ID: 1})
g.Assert(err == nil).IsTrue()
g.Assert(len(builds)).Equal(2)
g.Assert(builds[0].ID).Equal(build2.ID)
g.Assert(builds[0].RepoID).Equal(build2.RepoID)
g.Assert(builds[0].Status).Equal(build2.Status)
})
})
}

View File

@ -1,11 +1,9 @@
package ccmenu
package model
import (
"encoding/xml"
"strconv"
"time"
"github.com/drone/drone/pkg/types"
)
type CCProjects struct {
@ -23,10 +21,10 @@ type CCProject struct {
WebURL string `xml:"webUrl,attr"`
}
func NewCC(r *types.Repo, b *types.Build) *CCProjects {
func NewCC(r *Repo, b *Build, link string) *CCProjects {
proj := &CCProject{
Name: r.Owner + "/" + r.Name,
WebURL: r.Self,
Name: r.FullName,
WebURL: link,
Activity: "Building",
LastBuildStatus: "Unknown",
LastBuildLabel: "Unknown",
@ -34,24 +32,22 @@ func NewCC(r *types.Repo, b *types.Build) *CCProjects {
// if the build is not currently running then
// we can return the latest build status.
if b.Status != types.StatePending &&
b.Status != types.StateRunning {
if b.Status != StatusPending &&
b.Status != StatusRunning {
proj.Activity = "Sleeping"
proj.LastBuildTime = time.Unix(b.Started, 0).Format(time.RFC3339)
proj.LastBuildLabel = strconv.Itoa(b.Number)
}
// ensure the last build state accepts a valid
// ensure the last build Status accepts a valid
// ccmenu enumeration
switch b.Status {
case types.StateError, types.StateKilled:
case StatusError, StatusKilled:
proj.LastBuildStatus = "Exception"
case types.StateSuccess:
case StatusSuccess:
proj.LastBuildStatus = "Success"
case types.StateFailure:
case StatusFailure:
proj.LastBuildStatus = "Failure"
default:
proj.LastBuildStatus = "Unknown"
}
return &CCProjects{Project: proj}

83
model/cc_test.go Normal file
View File

@ -0,0 +1,83 @@
package model
import (
"testing"
"github.com/franela/goblin"
)
func TestCC(t *testing.T) {
g := goblin.Goblin(t)
g.Describe("CC", func() {
g.It("Should create a project", func() {
r := &Repo{
FullName: "foo/bar",
}
b := &Build{
Status: StatusSuccess,
Number: 1,
Started: 1442872675,
}
cc := NewCC(r, b, "http://localhost/foo/bar/1")
g.Assert(cc.Project.Name).Equal("foo/bar")
g.Assert(cc.Project.Activity).Equal("Sleeping")
g.Assert(cc.Project.LastBuildStatus).Equal("Success")
g.Assert(cc.Project.LastBuildLabel).Equal("1")
g.Assert(cc.Project.LastBuildTime).Equal("2015-09-21T14:57:55-07:00")
g.Assert(cc.Project.WebURL).Equal("http://localhost/foo/bar/1")
})
g.It("Should properly label exceptions", func() {
r := &Repo{FullName: "foo/bar"}
b := &Build{
Status: StatusError,
Number: 1,
Started: 1257894000,
}
cc := NewCC(r, b, "http://localhost/foo/bar/1")
g.Assert(cc.Project.LastBuildStatus).Equal("Exception")
g.Assert(cc.Project.Activity).Equal("Sleeping")
})
g.It("Should properly label success", func() {
r := &Repo{FullName: "foo/bar"}
b := &Build{
Status: StatusSuccess,
Number: 1,
Started: 1257894000,
}
cc := NewCC(r, b, "http://localhost/foo/bar/1")
g.Assert(cc.Project.LastBuildStatus).Equal("Success")
g.Assert(cc.Project.Activity).Equal("Sleeping")
})
g.It("Should properly label failure", func() {
r := &Repo{FullName: "foo/bar"}
b := &Build{
Status: StatusFailure,
Number: 1,
Started: 1257894000,
}
cc := NewCC(r, b, "http://localhost/foo/bar/1")
g.Assert(cc.Project.LastBuildStatus).Equal("Failure")
g.Assert(cc.Project.Activity).Equal("Sleeping")
})
g.It("Should properly label running", func() {
r := &Repo{FullName: "foo/bar"}
b := &Build{
Status: StatusRunning,
Number: 1,
Started: 1257894000,
}
cc := NewCC(r, b, "http://localhost/foo/bar/1")
g.Assert(cc.Project.Activity).Equal("Building")
g.Assert(cc.Project.LastBuildStatus).Equal("Unknown")
g.Assert(cc.Project.LastBuildLabel).Equal("Unknown")
})
})
}

View File

@ -1,126 +0,0 @@
package types
import (
"path/filepath"
"strings"
)
// Config represents a repository build configuration.
type Config struct {
Cache *Step
Setup *Step
Clone *Step
Build *Step
Compose map[string]*Step
Publish map[string]*Step
Deploy map[string]*Step
Notify map[string]*Step
Matrix Matrix
Axis Axis
}
// Matrix represents the build matrix.
type Matrix map[string][]string
// Axis represents a single permutation of entries
// from the build matrix.
type Axis map[string]string
// String returns a string representation of an Axis as
// a comma-separated list of environment variables.
func (a Axis) String() string {
var envs []string
for k, v := range a {
envs = append(envs, k+"="+v)
}
return strings.Join(envs, " ")
}
// Step represents a step in the build process, including
// the execution environment and parameters.
type Step struct {
Image string
Pull bool
Privileged bool
Environment []string
Entrypoint []string
Command []string
Volumes []string
Cache []string
WorkingDir string `yaml:"working_dir"`
NetworkMode string `yaml:"net"`
// Condition represents a set of conditions that must
// be met in order to execute this step.
Condition *Condition `yaml:"when"`
// Config represents the unique configuration details
// for each plugin.
Config map[string]interface{} `yaml:"config,inline"`
}
// Condition represents a set of conditions that must
// be met in order to proceed with a build or build step.
type Condition struct {
Owner string // Indicates the step should run only for this repo (useful for forks)
Branch string // Indicates the step should run only for this branch
Event string
Success string
Failure string
// Indicates the step should only run when the following
// matrix values are present for the sub-build.
Matrix map[string]string
}
// MatchBranch is a helper function that returns true
// if all_branches is true. Else it returns false if a
// branch condition is specified, and the branch does
// not match.
func (c *Condition) MatchBranch(branch string) bool {
if len(c.Branch) == 0 {
return true
}
if strings.HasPrefix(branch, "refs/heads/") {
branch = branch[11:]
}
match, _ := filepath.Match(c.Branch, branch)
return match
}
// MatchOwner is a helper function that returns false
// if an owner condition is specified and the repository
// owner does not match.
//
// This is useful when you want to prevent forks from
// executing deployment, publish or notification steps.
func (c *Condition) MatchOwner(owner string) bool {
if len(c.Owner) == 0 {
return true
}
parts := strings.Split(owner, "/")
switch len(parts) {
case 2:
return c.Owner == parts[0]
case 3:
return c.Owner == parts[1]
default:
return c.Owner == owner
}
}
// MatchMatrix is a helper function that returns false
// to limit steps to only certain matrix axis.
func (c *Condition) MatchMatrix(matrix map[string]string) bool {
if len(c.Matrix) == 0 {
return true
}
for k, v := range c.Matrix {
if matrix[k] != v {
return false
}
}
return true
}

18
model/const.go Normal file
View File

@ -0,0 +1,18 @@
package model
const (
EventPush = "push"
EventPull = "pull_request"
EventTag = "tag"
EventDeploy = "deploy"
)
const (
StatusSkipped = "skipped"
StatusPending = "pending"
StatusRunning = "running"
StatusSuccess = "success"
StatusFailure = "failure"
StatusKilled = "killed"
StatusError = "error"
)

23
model/feed.go Normal file
View File

@ -0,0 +1,23 @@
package model
type Feed struct {
Owner string `json:"owner" meddler:"repo_owner"`
Name string `json:"name" meddler:"repo_name"`
FullName string `json:"full_name" meddler:"repo_full_name"`
Avatar string `json:"avatar_url" meddler:"repo_avatar"`
Number int `json:"number" meddler:"build_number"`
Event string `json:"event" meddler:"build_event"`
Status string `json:"status" meddler:"build_status"`
Started int64 `json:"started_at" meddler:"build_started"`
Finished int64 `json:"finished_at" meddler:"build_finished"`
Commit string `json:"commit" meddler:"build_commit"`
Branch string `json:"branch" meddler:"build_branch"`
Ref string `json:"ref" meddler:"build_ref"`
Refspec string `json:"refspec" meddler:"build_refspec"`
Remote string `json:"remote" meddler:"build_remote"`
Title string `json:"title" meddler:"build_title"`
Message string `json:"message" meddler:"build_message"`
Author string `json:"author" meddler:"build_author"`
Email string `json:"author_email" meddler:"build_email"`
}

View File

@ -1,8 +0,0 @@
package types
type Hook struct {
Event string
Repo *Repo
Commit *Commit
PullRequest *PullRequest
}

View File

@ -1,13 +1,62 @@
package types
package model
import (
"github.com/drone/drone/shared/database"
"github.com/russross/meddler"
)
type Job struct {
ID int64 `json:"id"`
BuildID int64 `json:"-" sql:"unique:ux_build_number,index:ix_job_build_id"`
Number int `json:"number" sql:"unique:ux_build_number"`
Status string `json:"status"`
ExitCode int `json:"exit_code"`
Started int64 `json:"started_at"`
Finished int64 `json:"finished_at"`
ID int64 `json:"id" meddler:"job_id,pk"`
BuildID int64 `json:"-" meddler:"job_build_id"`
NodeID int64 `json:"-" meddler:"job_node_id"`
Number int `json:"number" meddler:"job_number"`
Status string `json:"status" meddler:"job_status"`
ExitCode int `json:"exit_code" meddler:"job_exit_code"`
Started int64 `json:"started_at" meddler:"job_started"`
Finished int64 `json:"finished_at" meddler:"job_finished"`
Environment map[string]string `json:"environment" sql:"type:varchar,size:2048"`
Environment map[string]string `json:"environment" meddler:"job_environment,json"`
}
func GetJob(db meddler.DB, id int64) (*Job, error) {
var job = new(Job)
var err = meddler.Load(db, jobTable, job, id)
return job, err
}
func GetJobNumber(db meddler.DB, build *Build, number int) (*Job, error) {
var job = new(Job)
var err = meddler.QueryRow(db, job, database.Rebind(jobNumberQuery), build.ID, number)
return job, err
}
func GetJobList(db meddler.DB, build *Build) ([]*Job, error) {
var jobs = []*Job{}
var err = meddler.QueryAll(db, &jobs, database.Rebind(jobListQuery), build.ID)
return jobs, err
}
func InsertJob(db meddler.DB, job *Job) error {
return meddler.Insert(db, jobTable, job)
}
func UpdateJob(db meddler.DB, job *Job) error {
return meddler.Update(db, jobTable, job)
}
const jobTable = "jobs"
const jobListQuery = `
SELECT *
FROM jobs
WHERE job_build_id = ?
ORDER BY job_number ASC
`
const jobNumberQuery = `
SELECT *
FROM jobs
WHERE job_build_id = ?
AND job_number = ?
LIMIT 1
`

117
model/job_test.go Normal file
View File

@ -0,0 +1,117 @@
package model
import (
"testing"
"github.com/drone/drone/shared/database"
"github.com/franela/goblin"
)
func TestJob(t *testing.T) {
db := database.Open("sqlite3", ":memory:")
defer db.Close()
g := goblin.Goblin(t)
g.Describe("Job", func() {
// before each test we purge the package table data from the database.
g.BeforeEach(func() {
db.Exec("DELETE FROM jobs")
db.Exec("DELETE FROM builds")
})
g.It("Should Set a job", func() {
job := &Job{
BuildID: 1,
Status: "pending",
ExitCode: 0,
Number: 1,
}
err1 := InsertJob(db, job)
g.Assert(err1 == nil).IsTrue()
g.Assert(job.ID != 0).IsTrue()
job.Status = "started"
err2 := UpdateJob(db, job)
g.Assert(err2 == nil).IsTrue()
getjob, err3 := GetJob(db, job.ID)
g.Assert(err3 == nil).IsTrue()
g.Assert(getjob.Status).Equal(job.Status)
})
g.It("Should Get a Job by ID", func() {
job := &Job{
BuildID: 1,
Status: "pending",
ExitCode: 1,
Number: 1,
Environment: map[string]string{"foo": "bar"},
}
err1 := InsertJob(db, job)
g.Assert(err1 == nil).IsTrue()
g.Assert(job.ID != 0).IsTrue()
getjob, err2 := GetJob(db, job.ID)
g.Assert(err2 == nil).IsTrue()
g.Assert(getjob.ID).Equal(job.ID)
g.Assert(getjob.Status).Equal(job.Status)
g.Assert(getjob.ExitCode).Equal(job.ExitCode)
g.Assert(getjob.Environment).Equal(job.Environment)
g.Assert(getjob.Environment["foo"]).Equal("bar")
})
g.It("Should Get a Job by Number", func() {
job := &Job{
BuildID: 1,
Status: "pending",
ExitCode: 1,
Number: 1,
}
err1 := InsertJob(db, job)
g.Assert(err1 == nil).IsTrue()
g.Assert(job.ID != 0).IsTrue()
getjob, err2 := GetJobNumber(db, &Build{ID: 1}, 1)
g.Assert(err2 == nil).IsTrue()
g.Assert(getjob.ID).Equal(job.ID)
g.Assert(getjob.Status).Equal(job.Status)
})
g.It("Should Get a List of Jobs by Commit", func() {
build := Build{
RepoID: 1,
Status: StatusSuccess,
}
jobs := []*Job{
&Job{
BuildID: 1,
Status: "success",
ExitCode: 0,
Number: 1,
},
&Job{
BuildID: 3,
Status: "error",
ExitCode: 1,
Number: 2,
},
&Job{
BuildID: 5,
Status: "pending",
ExitCode: 0,
Number: 3,
},
}
//
err1 := CreateBuild(db, &build, jobs...)
g.Assert(err1 == nil).IsTrue()
getjobs, err2 := GetJobList(db, &build)
g.Assert(err2 == nil).IsTrue()
g.Assert(len(getjobs)).Equal(3)
g.Assert(getjobs[0].Number).Equal(1)
g.Assert(getjobs[0].Status).Equal(StatusSuccess)
})
})
}

46
model/key.go Normal file
View File

@ -0,0 +1,46 @@
package model
import (
"github.com/drone/drone/shared/database"
"github.com/russross/meddler"
)
type Key struct {
ID int64 `json:"-" meddler:"key_id,pk"`
RepoID int64 `json:"-" meddler:"key_repo_id"`
Public string `json:"public" meddler:"key_public"`
Private string `json:"private" meddler:"key_private"`
}
func GetKey(db meddler.DB, repo *Repo) (*Key, error) {
var key = new(Key)
var err = meddler.QueryRow(db, key, database.Rebind(keyQuery), repo.ID)
return key, err
}
func CreateKey(db meddler.DB, key *Key) error {
return meddler.Save(db, keyTable, key)
}
func UpdateKey(db meddler.DB, key *Key) error {
return meddler.Save(db, keyTable, key)
}
func DeleteKey(db meddler.DB, repo *Repo) error {
var _, err = db.Exec(database.Rebind(keyDeleteStmt), repo.ID)
return err
}
const keyTable = "keys"
const keyQuery = `
SELECT *
FROM keys
WHERE key_repo_id=?
LIMIT 1
`
const keyDeleteStmt = `
DELETE FROM keys
WHERE key_repo_id=?
`

113
model/key_test.go Normal file
View File

@ -0,0 +1,113 @@
package model
import (
"testing"
"github.com/drone/drone/shared/database"
"github.com/franela/goblin"
)
func TestKey(t *testing.T) {
db := database.Open("sqlite3", ":memory:")
defer db.Close()
g := goblin.Goblin(t)
g.Describe("Keys", func() {
// before each test be sure to purge the package
// table data from the database.
g.BeforeEach(func() {
db.Exec("DELETE FROM keys")
})
g.It("Should create a key", func() {
key := Key{
RepoID: 1,
Public: fakePublicKey,
Private: fakePrivateKey,
}
err := CreateKey(db, &key)
g.Assert(err == nil).IsTrue()
g.Assert(key.ID != 0).IsTrue()
})
g.It("Should update a key", func() {
key := Key{
RepoID: 1,
Public: fakePublicKey,
Private: fakePrivateKey,
}
err := CreateKey(db, &key)
g.Assert(err == nil).IsTrue()
g.Assert(key.ID != 0).IsTrue()
key.Private = ""
key.Public = ""
err1 := UpdateKey(db, &key)
getkey, err2 := GetKey(db, &Repo{ID: 1})
g.Assert(err1 == nil).IsTrue()
g.Assert(err2 == nil).IsTrue()
g.Assert(key.ID).Equal(getkey.ID)
g.Assert(key.Public).Equal(getkey.Public)
g.Assert(key.Private).Equal(getkey.Private)
})
g.It("Should get a key", func() {
key := Key{
RepoID: 1,
Public: fakePublicKey,
Private: fakePrivateKey,
}
err := CreateKey(db, &key)
g.Assert(err == nil).IsTrue()
g.Assert(key.ID != 0).IsTrue()
getkey, err := GetKey(db, &Repo{ID: 1})
g.Assert(err == nil).IsTrue()
g.Assert(key.ID).Equal(getkey.ID)
g.Assert(key.Public).Equal(getkey.Public)
g.Assert(key.Private).Equal(getkey.Private)
})
g.It("Should delete a key", func() {
key := Key{
RepoID: 1,
Public: fakePublicKey,
Private: fakePrivateKey,
}
err1 := CreateKey(db, &key)
err2 := DeleteKey(db, &Repo{ID: 1})
g.Assert(err1 == nil).IsTrue()
g.Assert(err2 == nil).IsTrue()
_, err := GetKey(db, &Repo{ID: 1})
g.Assert(err == nil).IsFalse()
})
})
}
var fakePublicKey = `
-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCqGKukO1De7zhZj6+H0qtjTkVxwTCpvKe4eCZ0
FPqri0cb2JZfXJ/DgYSF6vUpwmJG8wVQZKjeGcjDOL5UlsuusFncCzWBQ7RKNUSesmQRMSGkVb1/
3j+skZ6UtW+5u09lHNsj6tQ51s1SPrCBkedbNf0Tp0GbMJDyR4e9T04ZZwIDAQAB
-----END PUBLIC KEY-----
`
var fakePrivateKey = `
-----BEGIN RSA PRIVATE KEY-----
MIICXAIBAAKBgQCqGKukO1De7zhZj6+H0qtjTkVxwTCpvKe4eCZ0FPqri0cb2JZfXJ/DgYSF6vUp
wmJG8wVQZKjeGcjDOL5UlsuusFncCzWBQ7RKNUSesmQRMSGkVb1/3j+skZ6UtW+5u09lHNsj6tQ5
1s1SPrCBkedbNf0Tp0GbMJDyR4e9T04ZZwIDAQABAoGAFijko56+qGyN8M0RVyaRAXz++xTqHBLh
3tx4VgMtrQ+WEgCjhoTwo23KMBAuJGSYnRmoBZM3lMfTKevIkAidPExvYCdm5dYq3XToLkkLv5L2
pIIVOFMDG+KESnAFV7l2c+cnzRMW0+b6f8mR1CJzZuxVLL6Q02fvLi55/mbSYxECQQDeAw6fiIQX
GukBI4eMZZt4nscy2o12KyYner3VpoeE+Np2q+Z3pvAMd/aNzQ/W9WaI+NRfcxUJrmfPwIGm63il
AkEAxCL5HQb2bQr4ByorcMWm/hEP2MZzROV73yF41hPsRC9m66KrheO9HPTJuo3/9s5p+sqGxOlF
L0NDt4SkosjgGwJAFklyR1uZ/wPJjj611cdBcztlPdqoxssQGnh85BzCj/u3WqBpE2vjvyyvyI5k
X6zk7S0ljKtt2jny2+00VsBerQJBAJGC1Mg5Oydo5NwD6BiROrPxGo2bpTbu/fhrT8ebHkTz2epl
U9VQQSQzY1oZMVX8i1m5WUTLPz2yLJIBQVdXqhMCQBGoiuSoSjafUhV7i1cEGpb88h5NBYZzWXGZ
37sJ5QsW+sJyoNde3xH8vdXhzU7eT82D6X/scw9RZz+/6rCJ4p0=
-----END RSA PRIVATE KEY-----
`

42
model/log.go Normal file
View File

@ -0,0 +1,42 @@
package model
import (
"bytes"
"io"
"io/ioutil"
"github.com/drone/drone/shared/database"
"github.com/russross/meddler"
)
type Log struct {
ID int64 `meddler:"log_id,pk"`
JobID int64 `meddler:"log_job_id"`
Data []byte `meddler:"log_data"`
}
func GetLog(db meddler.DB, job *Job) (io.ReadCloser, error) {
var log = new(Log)
var err = meddler.QueryRow(db, log, database.Rebind(logQuery), job.ID)
var buf = bytes.NewBuffer(log.Data)
return ioutil.NopCloser(buf), err
}
func SetLog(db meddler.DB, job *Job, r io.Reader) error {
var log = new(Log)
var err = meddler.QueryRow(db, log, database.Rebind(logQuery), job.ID)
if err != nil {
log = &Log{JobID: job.ID}
}
log.Data, _ = ioutil.ReadAll(r)
return meddler.Save(db, logTable, log)
}
const logTable = "logs"
const logQuery = `
SELECT *
FROM logs
WHERE log_job_id=?
LIMIT 1
`

59
model/log_test.go Normal file
View File

@ -0,0 +1,59 @@
package model
import (
"bytes"
"io/ioutil"
"testing"
"github.com/drone/drone/shared/database"
"github.com/franela/goblin"
)
func TestLog(t *testing.T) {
db := database.Open("sqlite3", ":memory:")
defer db.Close()
g := goblin.Goblin(t)
g.Describe("Logs", func() {
// before each test be sure to purge the package
// table data from the database.
g.BeforeEach(func() {
db.Exec("DELETE FROM logs")
})
g.It("Should create a log", func() {
job := Job{
ID: 1,
}
buf := bytes.NewBufferString("echo hi")
err := SetLog(db, &job, buf)
g.Assert(err == nil).IsTrue()
rc, err := GetLog(db, &job)
g.Assert(err == nil).IsTrue()
defer rc.Close()
out, _ := ioutil.ReadAll(rc)
g.Assert(string(out)).Equal("echo hi")
})
g.It("Should update a log", func() {
job := Job{
ID: 1,
}
buf1 := bytes.NewBufferString("echo hi")
buf2 := bytes.NewBufferString("echo allo?")
err1 := SetLog(db, &job, buf1)
err2 := SetLog(db, &job, buf2)
g.Assert(err1 == nil).IsTrue()
g.Assert(err2 == nil).IsTrue()
rc, err := GetLog(db, &job)
g.Assert(err == nil).IsTrue()
defer rc.Close()
out, _ := ioutil.ReadAll(rc)
g.Assert(string(out)).Equal("echo allo?")
})
})
}

7
model/netrc.go Normal file
View File

@ -0,0 +1,7 @@
package model
type Netrc struct {
Machine string `json:"machine"`
Login string `json:"login"`
Password string `json:"user"`
}

79
model/node.go Normal file
View File

@ -0,0 +1,79 @@
package model
import (
"github.com/drone/drone/shared/database"
"github.com/russross/meddler"
)
type Node struct {
ID int64 `meddler:"node_id,pk" json:"id"`
Addr string `meddler:"node_addr" json:"address"`
Arch string `meddler:"node_arch" json:"architecture"`
Cert string `meddler:"node_cert" json:"-"`
Key string `meddler:"node_key" json:"-"`
CA string `meddler:"node_ca" json:"-"`
}
func GetNode(db meddler.DB, id int64) (*Node, error) {
var node = new(Node)
var err = meddler.Load(db, nodeTable, node, id)
return node, err
}
func GetNodeList(db meddler.DB) ([]*Node, error) {
var nodes = []*Node{}
var err = meddler.QueryAll(db, &nodes, database.Rebind(nodeListQuery))
return nodes, err
}
func InsertNode(db meddler.DB, node *Node) error {
return meddler.Insert(db, nodeTable, node)
}
func UpdateNode(db meddler.DB, node *Node) error {
return meddler.Update(db, nodeTable, node)
}
func DeleteNode(db meddler.DB, node *Node) error {
var _, err = db.Exec(database.Rebind(nodeDeleteStmt), node.ID)
return err
}
const nodeTable = "nodes"
const nodeListQuery = `
SELECT *
FROM nodes
ORDER BY node_addr
`
const nodeDeleteStmt = `
DELETE FROM nodes
WHERE node_id=?
`
const (
Freebsd_386 uint = iota
Freebsd_amd64
Freebsd_arm
Linux_386
Linux_amd64
Linux_arm
Linux_arm64
Solaris_amd64
Windows_386
Windows_amd64
)
var Archs = map[string]uint{
"freebsd_386": Freebsd_386,
"freebsd_amd64": Freebsd_amd64,
"freebsd_arm": Freebsd_arm,
"linux_386": Linux_386,
"linux_amd64": Linux_amd64,
"linux_arm": Linux_arm,
"linux_arm64": Linux_arm64,
"solaris_amd64": Solaris_amd64,
"windows_386": Windows_386,
"windows_amd64": Windows_amd64,
}

100
model/node_test.go Normal file
View File

@ -0,0 +1,100 @@
package model
import (
"testing"
"github.com/drone/drone/shared/database"
"github.com/franela/goblin"
)
func TestNode(t *testing.T) {
db := database.Open("sqlite3", ":memory:")
defer db.Close()
g := goblin.Goblin(t)
g.Describe("Nodes", func() {
// before each test be sure to purge the package
// table data from the database.
g.BeforeEach(func() {
db.Exec("DELETE FROM nodes")
})
g.It("Should create a node", func() {
node := Node{
Addr: "unix:///var/run/docker/docker.sock",
Arch: "linux_amd64",
}
err := InsertNode(db, &node)
g.Assert(err == nil).IsTrue()
g.Assert(node.ID != 0).IsTrue()
})
g.It("Should update a node", func() {
node := Node{
Addr: "unix:///var/run/docker/docker.sock",
Arch: "linux_amd64",
}
err := InsertNode(db, &node)
g.Assert(err == nil).IsTrue()
g.Assert(node.ID != 0).IsTrue()
node.Addr = "unix:///var/run/docker.sock"
err1 := UpdateNode(db, &node)
getnode, err2 := GetNode(db, node.ID)
g.Assert(err1 == nil).IsTrue()
g.Assert(err2 == nil).IsTrue()
g.Assert(node.ID).Equal(getnode.ID)
g.Assert(node.Addr).Equal(getnode.Addr)
g.Assert(node.Arch).Equal(getnode.Arch)
})
g.It("Should get a node", func() {
node := Node{
Addr: "unix:///var/run/docker/docker.sock",
Arch: "linux_amd64",
}
err := InsertNode(db, &node)
g.Assert(err == nil).IsTrue()
g.Assert(node.ID != 0).IsTrue()
getnode, err := GetNode(db, node.ID)
g.Assert(err == nil).IsTrue()
g.Assert(node.ID).Equal(getnode.ID)
g.Assert(node.Addr).Equal(getnode.Addr)
g.Assert(node.Arch).Equal(getnode.Arch)
})
g.It("Should get a node list", func() {
node1 := Node{
Addr: "unix:///var/run/docker/docker.sock",
Arch: "linux_amd64",
}
node2 := Node{
Addr: "unix:///var/run/docker.sock",
Arch: "linux_386",
}
InsertNode(db, &node1)
InsertNode(db, &node2)
nodes, err := GetNodeList(db)
g.Assert(err == nil).IsTrue()
g.Assert(len(nodes)).Equal(2)
})
g.It("Should delete a node", func() {
node := Node{
Addr: "unix:///var/run/docker/docker.sock",
Arch: "linux_amd64",
}
err1 := InsertNode(db, &node)
err2 := DeleteNode(db, &node)
g.Assert(err1 == nil).IsTrue()
g.Assert(err2 == nil).IsTrue()
_, err := GetNode(db, node.ID)
g.Assert(err == nil).IsFalse()
})
})
}

7
model/perm.go Normal file
View File

@ -0,0 +1,7 @@
package model
type Perm struct {
Pull bool `json:"pull"`
Push bool `json:"push"`
Admin bool `json:"admin"`
}

View File

@ -1,77 +1,89 @@
package types
package model
type Repo struct {
ID int64 `json:"id"`
UserID int64 `json:"-" sql:"index:ix_repo_user_id"`
Owner string `json:"owner" sql:"unique:ux_repo_owner_name"`
Name string `json:"name" sql:"unique:ux_repo_owner_name"`
FullName string `json:"full_name" sql:"unique:ux_repo_full_name"`
Avatar string `json:"avatar_url"`
Self string `json:"self_url"`
Link string `json:"link_url"`
Clone string `json:"clone_url"`
Branch string `json:"default_branch"`
Private bool `json:"private"`
Trusted bool `json:"trusted"`
Timeout int64 `json:"timeout"`
Keys *Keypair `json:"-"`
Hooks *Hooks `json:"hooks"`
// Perms are the current user's permissions to push,
// pull, and administer this repository. The permissions
// are sourced from the version control system (ie GitHub)
Perms *Perm `json:"perms,omitempty" sql:"-"`
// Params are private environment parameters that are
// considered secret and are therefore stored external
// to the source code repository inside Drone.
Params map[string]string `json:"-"`
// randomly generated hash used to sign repository
// tokens and encrypt and decrypt private variables.
Hash string `json:"-"`
}
import (
"github.com/drone/drone/shared/database"
"github.com/russross/meddler"
)
type RepoLite struct {
ID int64 `json:"id"`
UserID int64 `json:"-"`
Owner string `json:"owner"`
Name string `json:"name"`
FullName string `json:"full_name"`
Language string `json:"language"`
Private bool `json:"private"`
Created int64 `json:"created_at"`
Updated int64 `json:"updated_at"`
Avatar string `json:"avatar_url"`
}
type RepoCommit struct {
ID int64 `json:"id"`
Owner string `json:"owner"`
Name string `json:"name"`
FullName string `json:"full_name"`
Number int `json:"number"`
Status string `json:"status"`
Started int64 `json:"started_at"`
Finished int64 `json:"finished_at"`
type Repo struct {
ID int64 `json:"id" meddler:"repo_id,pk"`
UserID int64 `json:"-" meddler:"repo_user_id"`
Owner string `json:"owner" meddler:"repo_owner"`
Name string `json:"name" meddler:"repo_name"`
FullName string `json:"full_name" meddler:"repo_full_name"`
Avatar string `json:"avatar_url" meddler:"repo_avatar"`
Link string `json:"link_url" meddler:"repo_link"`
Clone string `json:"clone_url" meddler:"repo_clone"`
Branch string `json:"default_branch" meddler:"repo_branch"`
Timeout int64 `json:"timeout" meddler:"repo_timeout"`
IsPrivate bool `json:"private" meddler:"repo_private"`
IsTrusted bool `json:"trusted" meddler:"repo_trusted"`
IsStarred bool `json:"starred,omitempty" meddler:"-"`
AllowPull bool `json:"allow_pr" meddler:"repo_allow_pr"`
AllowPush bool `json:"allow_push" meddler:"repo_allow_push"`
AllowDeploy bool `json:"allow_deploys" meddler:"repo_allow_deploys"`
AllowTag bool `json:"allow_tags" meddler:"repo_allow_tags"`
Hash string `json:"-" meddler:"repo_hash"`
}
type Perm struct {
Pull bool `json:"pull" sql:"-"`
Push bool `json:"push" sql:"-"`
Admin bool `json:"admin" sql:"-"`
func GetRepo(db meddler.DB, id int64) (*Repo, error) {
var repo = new(Repo)
var err = meddler.Load(db, repoTable, repo, id)
return repo, err
}
type Hooks struct {
PullRequest bool `json:"pull_request"`
Push bool `json:"push"`
Tags bool `json:"tags"`
func GetRepoName(db meddler.DB, owner, name string) (*Repo, error) {
var repo = new(Repo)
var err = meddler.QueryRow(db, repo, database.Rebind(repoNameQuery), owner, name)
return repo, err
}
// Keypair represents an RSA public and private key
// assigned to a repository. It may be used to clone
// private repositories, or as a deployment key.
type Keypair struct {
Public string `json:"public,omitempty"`
Private string `json:"private,omitempty"`
func GetRepoList(db meddler.DB, user *User) ([]*Repo, error) {
var repos = []*Repo{}
var err = meddler.QueryAll(db, &repos, database.Rebind(repoListQuery), user.ID)
return repos, err
}
func CreateRepo(db meddler.DB, repo *Repo) error {
return meddler.Insert(db, repoTable, repo)
}
func UpdateRepo(db meddler.DB, repo *Repo) error {
return meddler.Update(db, repoTable, repo)
}
func DeleteRepo(db meddler.DB, repo *Repo) error {
var _, err = db.Exec(database.Rebind(repoDeleteStmt), repo.ID)
return err
}
const repoTable = "repos"
const repoNameQuery = `
SELECT *
FROM repos
WHERE repo_owner = ?
AND repo_name = ?
LIMIT 1;
`
const repoListQuery = `
SELECT r.*
FROM
repos r
,stars s
WHERE r.repo_id = s.star_repo_id
AND s.star_user_id = ?
`
const repoDeleteStmt = `
DELETE FROM repos
WHERE repo_id = ?
`

148
model/repo_test.go Normal file
View File

@ -0,0 +1,148 @@
package model
import (
"testing"
"github.com/drone/drone/shared/database"
"github.com/franela/goblin"
)
func TestRepostore(t *testing.T) {
db := database.Open("sqlite3", ":memory:")
defer db.Close()
g := goblin.Goblin(t)
g.Describe("Repo", func() {
// before each test be sure to purge the package
// table data from the database.
g.BeforeEach(func() {
db.Exec("DELETE FROM stars")
db.Exec("DELETE FROM repos")
db.Exec("DELETE FROM users")
})
g.It("Should Set a Repo", func() {
repo := Repo{
UserID: 1,
FullName: "bradrydzewski/drone",
Owner: "bradrydzewski",
Name: "drone",
}
err1 := CreateRepo(db, &repo)
err2 := UpdateRepo(db, &repo)
getrepo, err3 := GetRepo(db, repo.ID)
if err3 != nil {
println("Get Repo Error")
println(err3.Error())
}
g.Assert(err1 == nil).IsTrue()
g.Assert(err2 == nil).IsTrue()
g.Assert(err3 == nil).IsTrue()
g.Assert(repo.ID).Equal(getrepo.ID)
})
g.It("Should Add a Repo", func() {
repo := Repo{
UserID: 1,
FullName: "bradrydzewski/drone",
Owner: "bradrydzewski",
Name: "drone",
}
err := CreateRepo(db, &repo)
g.Assert(err == nil).IsTrue()
g.Assert(repo.ID != 0).IsTrue()
})
g.It("Should Get a Repo by ID", func() {
repo := Repo{
UserID: 1,
FullName: "bradrydzewski/drone",
Owner: "bradrydzewski",
Name: "drone",
}
CreateRepo(db, &repo)
getrepo, err := GetRepo(db, repo.ID)
g.Assert(err == nil).IsTrue()
g.Assert(repo.ID).Equal(getrepo.ID)
g.Assert(repo.UserID).Equal(getrepo.UserID)
g.Assert(repo.Owner).Equal(getrepo.Owner)
g.Assert(repo.Name).Equal(getrepo.Name)
})
g.It("Should Get a Repo by Name", func() {
repo := Repo{
UserID: 1,
FullName: "bradrydzewski/drone",
Owner: "bradrydzewski",
Name: "drone",
}
CreateRepo(db, &repo)
getrepo, err := GetRepoName(db, repo.Owner, repo.Name)
g.Assert(err == nil).IsTrue()
g.Assert(repo.ID).Equal(getrepo.ID)
g.Assert(repo.UserID).Equal(getrepo.UserID)
g.Assert(repo.Owner).Equal(getrepo.Owner)
g.Assert(repo.Name).Equal(getrepo.Name)
})
g.It("Should Get a Repo List by User", func() {
repo1 := Repo{
UserID: 1,
FullName: "bradrydzewski/drone",
Owner: "bradrydzewski",
Name: "drone",
}
repo2 := Repo{
UserID: 1,
FullName: "bradrydzewski/drone-dart",
Owner: "bradrydzewski",
Name: "drone-dart",
}
CreateRepo(db, &repo1)
CreateRepo(db, &repo2)
CreateStar(db, &User{ID: 1}, &repo1)
repos, err := GetRepoList(db, &User{ID: 1})
g.Assert(err == nil).IsTrue()
g.Assert(len(repos)).Equal(1)
g.Assert(repos[0].UserID).Equal(repo1.UserID)
g.Assert(repos[0].Owner).Equal(repo1.Owner)
g.Assert(repos[0].Name).Equal(repo1.Name)
})
g.It("Should Delete a Repo", func() {
repo := Repo{
UserID: 1,
FullName: "bradrydzewski/drone",
Owner: "bradrydzewski",
Name: "drone",
}
CreateRepo(db, &repo)
_, err1 := GetRepo(db, repo.ID)
err2 := DeleteRepo(db, &repo)
_, err3 := GetRepo(db, repo.ID)
g.Assert(err1 == nil).IsTrue()
g.Assert(err2 == nil).IsTrue()
g.Assert(err3 == nil).IsFalse()
})
g.It("Should Enforce Unique Repo Name", func() {
repo1 := Repo{
UserID: 1,
FullName: "bradrydzewski/drone",
Owner: "bradrydzewski",
Name: "drone",
}
repo2 := Repo{
UserID: 2,
FullName: "bradrydzewski/drone",
Owner: "bradrydzewski",
Name: "drone",
}
err1 := CreateRepo(db, &repo1)
err2 := CreateRepo(db, &repo2)
g.Assert(err1 == nil).IsTrue()
g.Assert(err2 == nil).IsFalse()
})
})
}

44
model/star.go Normal file
View File

@ -0,0 +1,44 @@
package model
import (
"github.com/drone/drone/shared/database"
"github.com/russross/meddler"
)
type Star struct {
ID int64 `meddler:"star_id,pk"`
RepoID int64 `meddler:"star_repo_id"`
UserID int64 `meddler:"star_user_id"`
}
func GetStar(db meddler.DB, user *User, repo *Repo) (bool, error) {
var star = new(Star)
err := meddler.QueryRow(db, star, database.Rebind(starQuery), user.ID, repo.ID)
return (err == nil), err
}
func CreateStar(db meddler.DB, user *User, repo *Repo) error {
var star = &Star{UserID: user.ID, RepoID: repo.ID}
return meddler.Insert(db, starTable, star)
}
func DeleteStar(db meddler.DB, user *User, repo *Repo) error {
var _, err = db.Exec(database.Rebind(starDeleteStmt), user.ID, repo.ID)
return err
}
const starTable = "stars"
const starQuery = `
SELECT *
FROM stars
WHERE star_user_id=?
AND star_repo_id=?
LIMIT 1
`
const starDeleteStmt = `
DELETE FROM stars
WHERE star_user_id=?
AND star_repo_id=?
`

59
model/star_test.go Normal file
View File

@ -0,0 +1,59 @@
package model
import (
"testing"
"github.com/drone/drone/shared/database"
"github.com/franela/goblin"
)
func TestStarstore(t *testing.T) {
db := database.Open("sqlite3", ":memory:")
defer db.Close()
g := goblin.Goblin(t)
g.Describe("Stars", func() {
// before each test be sure to purge the package
// table data from the database.
g.BeforeEach(func() {
db.Exec("DELETE FROM stars")
})
g.It("Should Add a Star", func() {
user := User{ID: 1}
repo := Repo{ID: 2}
err := CreateStar(db, &user, &repo)
g.Assert(err == nil).IsTrue()
})
g.It("Should Get Starred", func() {
user := User{ID: 1}
repo := Repo{ID: 2}
CreateStar(db, &user, &repo)
ok, err := GetStar(db, &user, &repo)
g.Assert(err == nil).IsTrue()
g.Assert(ok).IsTrue()
})
g.It("Should Not Get Starred", func() {
user := User{ID: 1}
repo := Repo{ID: 2}
ok, err := GetStar(db, &user, &repo)
g.Assert(err != nil).IsTrue()
g.Assert(ok).IsFalse()
})
g.It("Should Del a Star", func() {
user := User{ID: 1}
repo := Repo{ID: 2}
CreateStar(db, &user, &repo)
_, err1 := GetStar(db, &user, &repo)
err2 := DeleteStar(db, &user, &repo)
_, err3 := GetStar(db, &user, &repo)
g.Assert(err1 == nil).IsTrue()
g.Assert(err2 == nil).IsTrue()
g.Assert(err3 == nil).IsFalse()
})
})
}

View File

@ -1,10 +0,0 @@
package types
type Status struct {
ID int64 `json:"-"`
CommitID int64 `json:"-"`
State string `json:"status"`
Link string `json:"target_url"`
Desc string `json:"description"`
Context string `json:"context"`
}

8
model/sys.go Normal file
View File

@ -0,0 +1,8 @@
package model
type System struct {
Version string `json:"version"`
Link string `json:"link_url"`
Plugins []string `json:"plugins"`
Globals []string `json:"globals"`
}

View File

@ -1,27 +0,0 @@
package types
// System provides important information about the Drone
// server to the plugin.
type System struct {
Version string `json:"version"`
Link string `json:"link_url"`
Plugins []string `json:"plugins"`
Globals []string `json:"globals"`
}
// Workspace defines the build's workspace inside the
// container. This helps the plugin locate the source
// code directory.
type Workspace struct {
Root string `json:"root"`
Path string `json:"path"`
Netrc *Netrc `json:"netrc"`
Keys *Keypair `json:"keys"`
}
type Netrc struct {
Machine string `json:"machine"`
Login string `json:"login"`
Password string `json:"user"`
}

View File

@ -1,16 +1,117 @@
package types
package model
import (
"github.com/drone/drone/shared/database"
"github.com/russross/meddler"
)
type User struct {
ID int64 `json:"id"`
Login string `json:"login,omitempty" sql:"unique:ux_user_login"`
Token string `json:"-"`
Secret string `json:"-"`
Email string `json:"email,omitempty"`
Avatar string `json:"avatar_url,omitempty"`
Active bool `json:"active,omitempty"`
Admin bool `json:"admin,omitempty"`
// randomly generated hash used to sign user
// session and application tokens.
Hash string `json:"-"`
ID int64 `json:"id" meddler:"user_id,pk"`
Login string `json:"login" meddler:"user_login"`
Token string `json:"-" meddler:"user_token"`
Secret string `json:"-" meddler:"user_secret"`
Email string `json:"email" meddler:"user_email"`
Avatar string `json:"avatar_url" meddler:"user_avatar"`
Active bool `json:"active," meddler:"user_active"`
Admin bool `json:"admin," meddler:"user_admin"`
Hash string `json:"-" meddler:"user_hash"`
}
func GetUser(db meddler.DB, id int64) (*User, error) {
var usr = new(User)
var err = meddler.Load(db, userTable, usr, id)
return usr, err
}
func GetUserLogin(db meddler.DB, login string) (*User, error) {
var usr = new(User)
var err = meddler.QueryRow(db, usr, database.Rebind(userLoginQuery), login)
return usr, err
}
func GetUserList(db meddler.DB) ([]*User, error) {
var users = []*User{}
var err = meddler.QueryAll(db, &users, database.Rebind(userListQuery))
return users, err
}
func GetUserFeed(db meddler.DB, user *User, limit, offset int) ([]*Feed, error) {
var feed = []*Feed{}
var err = meddler.QueryAll(db, &feed, database.Rebind(userFeedQuery), user.ID, limit, offset)
return feed, err
}
func GetUserCount(db meddler.DB) (int, error) {
var count int
var err = db.QueryRow(database.Rebind(userCountQuery)).Scan(&count)
return count, err
}
func CreateUser(db meddler.DB, user *User) error {
return meddler.Insert(db, userTable, user)
}
func UpdateUser(db meddler.DB, user *User) error {
return meddler.Update(db, userTable, user)
}
func DeleteUser(db meddler.DB, user *User) error {
var _, err = db.Exec(database.Rebind(userDeleteStmt), user.ID)
return err
}
const userTable = "users"
const userLoginQuery = `
SELECT *
FROM users
WHERE user_login=?
LIMIT 1
`
const userListQuery = `
SELECT *
FROM users
ORDER BY user_login ASC
`
const userCountQuery = `
SELECT count(1)
FROM users
`
const userDeleteStmt = `
DELETE FROM users
WHERE user_id=?
`
const userFeedQuery = `
SELECT
repo_owner
,repo_name
,repo_full_name
,repo_avatar
,build_number
,build_event
,build_status
,build_started
,build_finished
,build_commit
,build_branch
,build_ref
,build_refspec
,build_remote
,build_title
,build_message
,build_author
,build_email
FROM
builds b
,repos r
,stars s
WHERE b.build_repo_id = r.repo_id
AND r.repo_id = s.star_repo_id
AND s.star_user_id = ?
ORDER BY b.build_number DESC
LIMIT ? OFFSET ?
`

207
model/user_test.go Normal file
View File

@ -0,0 +1,207 @@
package model
import (
"testing"
"github.com/drone/drone/shared/database"
"github.com/franela/goblin"
)
func TestUserstore(t *testing.T) {
db := database.Open("sqlite3", ":memory:")
defer db.Close()
g := goblin.Goblin(t)
g.Describe("User", func() {
// before each test be sure to purge the package
// table data from the database.
g.BeforeEach(func() {
db.Exec("DELETE FROM users")
db.Exec("DELETE FROM stars")
db.Exec("DELETE FROM repos")
db.Exec("DELETE FROM builds")
db.Exec("DELETE FROM jobs")
})
g.It("Should Update a User", func() {
user := User{
Login: "joe",
Email: "foo@bar.com",
Token: "e42080dddf012c718e476da161d21ad5",
}
err1 := CreateUser(db, &user)
err2 := UpdateUser(db, &user)
getuser, err3 := GetUser(db, user.ID)
g.Assert(err1 == nil).IsTrue()
g.Assert(err2 == nil).IsTrue()
g.Assert(err3 == nil).IsTrue()
g.Assert(user.ID).Equal(getuser.ID)
})
g.It("Should Add a new User", func() {
user := User{
Login: "joe",
Email: "foo@bar.com",
Token: "e42080dddf012c718e476da161d21ad5",
}
err := CreateUser(db, &user)
g.Assert(err == nil).IsTrue()
g.Assert(user.ID != 0).IsTrue()
})
g.It("Should Get a User", func() {
user := User{
Login: "joe",
Token: "f0b461ca586c27872b43a0685cbc2847",
Secret: "976f22a5eef7caacb7e678d6c52f49b1",
Email: "foo@bar.com",
Avatar: "b9015b0857e16ac4d94a0ffd9a0b79c8",
Active: true,
Admin: true,
}
CreateUser(db, &user)
getuser, err := GetUser(db, user.ID)
g.Assert(err == nil).IsTrue()
g.Assert(user.ID).Equal(getuser.ID)
g.Assert(user.Login).Equal(getuser.Login)
g.Assert(user.Token).Equal(getuser.Token)
g.Assert(user.Secret).Equal(getuser.Secret)
g.Assert(user.Email).Equal(getuser.Email)
g.Assert(user.Avatar).Equal(getuser.Avatar)
g.Assert(user.Active).Equal(getuser.Active)
g.Assert(user.Admin).Equal(getuser.Admin)
})
g.It("Should Get a User By Login", func() {
user := User{
Login: "joe",
Email: "foo@bar.com",
Token: "e42080dddf012c718e476da161d21ad5",
}
CreateUser(db, &user)
getuser, err := GetUserLogin(db, user.Login)
g.Assert(err == nil).IsTrue()
g.Assert(user.ID).Equal(getuser.ID)
g.Assert(user.Login).Equal(getuser.Login)
})
g.It("Should Enforce Unique User Login", func() {
user1 := User{
Login: "joe",
Email: "foo@bar.com",
Token: "e42080dddf012c718e476da161d21ad5",
}
user2 := User{
Login: "joe",
Email: "foo@bar.com",
Token: "ab20g0ddaf012c744e136da16aa21ad9",
}
err1 := CreateUser(db, &user1)
err2 := CreateUser(db, &user2)
g.Assert(err1 == nil).IsTrue()
g.Assert(err2 == nil).IsFalse()
})
g.It("Should Get a User List", func() {
user1 := User{
Login: "jane",
Email: "foo@bar.com",
Token: "ab20g0ddaf012c744e136da16aa21ad9",
}
user2 := User{
Login: "joe",
Email: "foo@bar.com",
Token: "e42080dddf012c718e476da161d21ad5",
}
CreateUser(db, &user1)
CreateUser(db, &user2)
users, err := GetUserList(db)
g.Assert(err == nil).IsTrue()
g.Assert(len(users)).Equal(2)
g.Assert(users[0].Login).Equal(user1.Login)
g.Assert(users[0].Email).Equal(user1.Email)
g.Assert(users[0].Token).Equal(user1.Token)
})
g.It("Should Get a User Count", func() {
user1 := User{
Login: "jane",
Email: "foo@bar.com",
Token: "ab20g0ddaf012c744e136da16aa21ad9",
}
user2 := User{
Login: "joe",
Email: "foo@bar.com",
Token: "e42080dddf012c718e476da161d21ad5",
}
CreateUser(db, &user1)
CreateUser(db, &user2)
count, err := GetUserCount(db)
g.Assert(err == nil).IsTrue()
g.Assert(count).Equal(2)
})
g.It("Should Get a User Count Zero", func() {
count, err := GetUserCount(db)
g.Assert(err == nil).IsTrue()
g.Assert(count).Equal(0)
})
g.It("Should Del a User", func() {
user := User{
Login: "joe",
Email: "foo@bar.com",
Token: "e42080dddf012c718e476da161d21ad5",
}
CreateUser(db, &user)
_, err1 := GetUser(db, user.ID)
err2 := DeleteUser(db, &user)
_, err3 := GetUser(db, user.ID)
g.Assert(err1 == nil).IsTrue()
g.Assert(err2 == nil).IsTrue()
g.Assert(err3 == nil).IsFalse()
})
g.It("Should get the Build feed for a User", func() {
repo1 := &Repo{
UserID: 1,
Owner: "bradrydzewski",
Name: "drone",
FullName: "bradrydzewski/drone",
}
repo2 := &Repo{
UserID: 2,
Owner: "drone",
Name: "drone",
FullName: "drone/drone",
}
CreateRepo(db, repo1)
CreateRepo(db, repo2)
CreateStar(db, &User{ID: 1}, repo1)
build1 := &Build{
RepoID: repo1.ID,
Status: StatusFailure,
}
build2 := &Build{
RepoID: repo1.ID,
Status: StatusSuccess,
}
build3 := &Build{
RepoID: repo2.ID,
Status: StatusSuccess,
}
CreateBuild(db, build1)
CreateBuild(db, build2)
CreateBuild(db, build3)
builds, err := GetUserFeed(db, &User{ID: 1}, 20, 0)
g.Assert(err == nil).IsTrue()
g.Assert(len(builds)).Equal(2)
g.Assert(builds[0].Owner).Equal("bradrydzewski")
g.Assert(builds[0].Name).Equal("drone")
})
})
}

View File

@ -1,36 +0,0 @@
package types
import (
"crypto/rand"
"io"
)
// standard characters allowed in token string.
var chars = []byte("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789")
// default token length
var length = 32
// GenerateToken generates random strings good for use in URIs to
// identify unique objects.
func GenerateToken() string {
b := make([]byte, length)
r := make([]byte, length+(length/4)) // storage for random bytes.
clen := byte(len(chars))
maxrb := byte(256 - (256 % len(chars)))
i := 0
for {
io.ReadFull(rand.Reader, r)
for _, c := range r {
if c >= maxrb {
// Skip this number to avoid modulo bias.
continue
}
b[i] = chars[c%clen]
i++
if i == length {
return string(b)
}
}
}
}

View File

@ -1,12 +0,0 @@
package types
import (
"testing"
)
func Test_GenerateToken(t *testing.T) {
token := GenerateToken()
if len(token) != length {
t.Errorf("Want token length %d, got %d", length, len(token))
}
}

436
remote/github/github.go Normal file
View File

@ -0,0 +1,436 @@
package github
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"strconv"
"strings"
"github.com/drone/drone/model"
"github.com/drone/drone/shared/envconfig"
"github.com/drone/drone/shared/httputil"
"github.com/drone/drone/shared/oauth2"
log "github.com/Sirupsen/logrus"
"github.com/google/go-github/github"
)
const (
DefaultURL = "https://github.com"
DefaultAPI = "https://api.github.com"
DefaultScope = "repo,repo:status,user:email"
)
type Github struct {
URL string
API string
Client string
Secret string
Orgs []string
Open bool
PrivateMode bool
SkipVerify bool
}
func Load(env envconfig.Env) *Github {
config := env.String("REMOTE_CONFIG", "")
// parse the remote DSN configuration string
url_, err := url.Parse(config)
if err != nil {
log.Fatalln("unable to parse remote dsn. %s", err)
}
params := url_.Query()
url_.Path = ""
url_.RawQuery = ""
// create the Githbub remote using parameters from
// the parsed DSN configuration string.
github := Github{}
github.URL = url_.String()
github.Client = params.Get("client_id")
github.Secret = params.Get("client_secret")
github.Orgs = params["orgs"]
github.PrivateMode, _ = strconv.ParseBool(params.Get("private_mode"))
github.SkipVerify, _ = strconv.ParseBool(params.Get("skip_verify"))
github.Open, _ = strconv.ParseBool(params.Get("open"))
if github.URL == DefaultURL {
github.API = DefaultAPI
} else {
github.API = github.URL + "/api/v3/"
}
return &github
}
// Login authenticates the session and returns the
// remote user details.
func (g *Github) Login(res http.ResponseWriter, req *http.Request) (*model.User, bool, error) {
var config = &oauth2.Config{
ClientId: g.Client,
ClientSecret: g.Secret,
Scope: DefaultScope,
AuthURL: fmt.Sprintf("%s/login/oauth/authorize", g.URL),
TokenURL: fmt.Sprintf("%s/login/oauth/access_token", g.URL),
RedirectURL: fmt.Sprintf("%s/authorize", httputil.GetURL(req)),
}
// get the OAuth code
var code = req.FormValue("code")
if len(code) == 0 {
var random = GetRandom()
http.Redirect(res, req, config.AuthCodeURL(random), http.StatusSeeOther)
return nil, false, nil
}
var trans = &oauth2.Transport{Config: config}
var token, err = trans.Exchange(code)
if err != nil {
return nil, false, fmt.Errorf("Error exchanging token. %s", err)
}
var client = NewClient(g.API, token.AccessToken, g.SkipVerify)
var useremail, errr = GetUserEmail(client)
if errr != nil {
return nil, false, fmt.Errorf("Error retrieving user or verified email. %s", errr)
}
if len(g.Orgs) > 0 {
allowedOrg, err := UserBelongsToOrg(client, g.Orgs)
if err != nil {
return nil, false, fmt.Errorf("Could not check org membership. %s", err)
}
if !allowedOrg {
return nil, false, fmt.Errorf("User does not belong to correct org. Must belong to %v", g.Orgs)
}
}
user := model.User{}
user.Login = *useremail.Login
user.Email = *useremail.Email
user.Token = token.AccessToken
user.Avatar = *useremail.AvatarURL
return &user, g.Open, nil
}
// Auth authenticates the session and returns the remote user
// login for the given token and secret
func (g *Github) Auth(token, secret string) (string, error) {
client := NewClient(g.API, token, g.SkipVerify)
user, _, err := client.Users.Get("")
if err != nil {
return "", err
}
return *user.Login, nil
}
// Repo fetches the named repository from the remote system.
func (g *Github) Repo(u *model.User, owner, name string) (*model.Repo, error) {
client := NewClient(g.API, u.Token, g.SkipVerify)
repo_, err := GetRepo(client, owner, name)
if err != nil {
return nil, err
}
repo := &model.Repo{}
repo.Owner = owner
repo.Name = name
repo.FullName = *repo_.FullName
repo.Link = *repo_.HTMLURL
repo.IsPrivate = *repo_.Private
repo.Clone = *repo_.CloneURL
repo.Branch = "master"
repo.Avatar = *repo_.Owner.AvatarURL
if repo_.DefaultBranch != nil {
repo.Branch = *repo_.DefaultBranch
}
if g.PrivateMode {
repo.IsPrivate = true
}
return repo, err
}
// Repos fetches a list of repos from the remote system.
func (g *Github) Repos(u *model.User) ([]*model.RepoLite, error) {
client := NewClient(g.API, u.Token, g.SkipVerify)
all, err := GetAllRepos(client)
if err != nil {
return nil, err
}
var repos = []*model.RepoLite{}
for _, repo := range all {
repos = append(repos, &model.RepoLite{
Owner: *repo.Owner.Login,
Name: *repo.Name,
FullName: *repo.FullName,
Avatar: *repo.Owner.AvatarURL,
})
}
return repos, err
}
// Perm fetches the named repository permissions from
// the remote system for the specified user.
func (g *Github) Perm(u *model.User, owner, name string) (*model.Perm, error) {
client := NewClient(g.API, u.Token, g.SkipVerify)
repo, err := GetRepo(client, owner, name)
if err != nil {
return nil, err
}
m := &model.Perm{}
m.Admin = (*repo.Permissions)["admin"]
m.Push = (*repo.Permissions)["push"]
m.Pull = (*repo.Permissions)["pull"]
return m, nil
}
// Script fetches the build script (.drone.yml) from the remote
// repository and returns in string format.
func (g *Github) Script(u *model.User, r *model.Repo, b *model.Build) ([]byte, []byte, error) {
client := NewClient(g.API, u.Token, g.SkipVerify)
cfg, err := GetFile(client, r.Owner, r.Name, ".drone.yml", b.Commit)
sec, _ := GetFile(client, r.Owner, r.Name, ".drone.sec", b.Commit)
return cfg, sec, err
}
// Status sends the commit status to the remote system.
// An example would be the GitHub pull request status.
func (g *Github) Status(u *model.User, r *model.Repo, b *model.Build, link string) error {
client := NewClient(g.API, u.Token, g.SkipVerify)
status := getStatus(b.Status)
desc := getDesc(b.Status)
data := github.RepoStatus{
Context: github.String("Drone"),
State: github.String(status),
Description: github.String(desc),
TargetURL: github.String(link),
}
_, _, err := client.Repositories.CreateStatus(r.Owner, r.Name, b.Commit, &data)
return err
}
// Netrc returns a .netrc file that can be used to clone
// private repositories from a remote system.
func (g *Github) Netrc(u *model.User, r *model.Repo) (*model.Netrc, error) {
url_, err := url.Parse(g.URL)
if err != nil {
return nil, err
}
netrc := &model.Netrc{}
netrc.Login = u.Token
netrc.Password = "x-oauth-basic"
netrc.Machine = url_.Host
return netrc, nil
}
// Activate activates a repository by creating the post-commit hook and
// adding the SSH deploy key, if applicable.
func (g *Github) Activate(u *model.User, r *model.Repo, k *model.Key, link string) error {
client := NewClient(g.API, u.Token, g.SkipVerify)
title, err := GetKeyTitle(link)
if err != nil {
return err
}
// if the CloneURL is using the SSHURL then we know that
// we need to add an SSH key to GitHub.
if r.IsPrivate || g.PrivateMode {
_, err = CreateUpdateKey(client, r.Owner, r.Name, title, k.Public)
if err != nil {
return err
}
}
_, err = CreateUpdateHook(client, r.Owner, r.Name, link)
return err
}
// Deactivate removes a repository by removing all the post-commit hooks
// which are equal to link and removing the SSH deploy key.
func (g *Github) Deactivate(u *model.User, r *model.Repo, link string) error {
client := NewClient(g.API, u.Token, g.SkipVerify)
title, err := GetKeyTitle(link)
if err != nil {
return err
}
// remove the deploy-key if it is installed remote.
if r.IsPrivate || g.PrivateMode {
if err := DeleteKey(client, r.Owner, r.Name, title); err != nil {
return err
}
}
return DeleteHook(client, r.Owner, r.Name, link)
}
// Hook parses the post-commit hook from the Request body
// and returns the required data in a standard format.
func (g *Github) Hook(r *http.Request) (*model.Repo, *model.Build, error) {
switch r.Header.Get("X-Github-Event") {
case "pull_request":
return g.pullRequest(r)
case "push":
return g.push(r)
default:
return nil, nil, nil
}
}
// push parses a hook with event type `push` and returns
// the commit data.
func (g *Github) push(r *http.Request) (*model.Repo, *model.Build, error) {
payload := GetPayload(r)
hook := &pushHook{}
err := json.Unmarshal(payload, hook)
if err != nil {
return nil, nil, err
}
if hook.Deleted {
return nil, nil, err
}
repo := &model.Repo{}
repo.Owner = hook.Repo.Owner.Login
if len(repo.Owner) == 0 {
repo.Owner = hook.Repo.Owner.Name
}
repo.Name = hook.Repo.Name
// Generating rather than using hook.Repo.FullName as it's
// not always present
repo.FullName = fmt.Sprintf("%s/%s", repo.Owner, repo.Name)
repo.Link = hook.Repo.HTMLURL
repo.IsPrivate = hook.Repo.Private
repo.Clone = hook.Repo.CloneURL
repo.Branch = hook.Repo.DefaultBranch
build := &model.Build{}
build.Event = model.EventPush
build.Commit = hook.Head.ID
build.Ref = hook.Ref
build.Link = hook.Head.URL
build.Branch = strings.Replace(build.Ref, "refs/heads/", "", -1)
build.Message = hook.Head.Message
// build.Timestamp = hook.Head.Timestamp
// build.Email = hook.Head.Author.Email
build.Avatar = hook.Sender.Avatar
build.Author = hook.Sender.Login
build.Remote = hook.Repo.CloneURL
// we should ignore github pages
if build.Ref == "refs/heads/gh-pages" {
return nil, nil, nil
}
return repo, build, nil
}
// pullRequest parses a hook with event type `pullRequest`
// and returns the commit data.
func (g *Github) pullRequest(r *http.Request) (*model.Repo, *model.Build, error) {
payload := GetPayload(r)
hook := &struct {
Action string `json:"action"`
PullRequest *github.PullRequest `json:"pull_request"`
Repo *github.Repository `json:"repository"`
}{}
err := json.Unmarshal(payload, hook)
if err != nil {
return nil, nil, err
}
// ignore these
if hook.Action != "opened" && hook.Action != "synchronize" {
return nil, nil, nil
}
if *hook.PullRequest.State != "open" {
return nil, nil, nil
}
repo := &model.Repo{}
repo.Owner = *hook.Repo.Owner.Login
repo.Name = *hook.Repo.Name
repo.FullName = *hook.Repo.FullName
repo.Link = *hook.Repo.HTMLURL
repo.IsPrivate = *hook.Repo.Private
repo.Clone = *hook.Repo.CloneURL
repo.Branch = "master"
if hook.Repo.DefaultBranch != nil {
repo.Branch = *hook.Repo.DefaultBranch
}
build := &model.Build{}
build.Event = model.EventPull
build.Commit = *hook.PullRequest.Head.SHA
build.Ref = fmt.Sprintf("refs/pull/%d/merge", *hook.PullRequest.Number)
build.Link = *hook.PullRequest.HTMLURL
build.Branch = *hook.PullRequest.Head.Ref
build.Message = *hook.PullRequest.Title
build.Author = *hook.PullRequest.Head.User.Login
build.Avatar = *hook.PullRequest.Head.User.AvatarURL
build.Remote = *hook.PullRequest.Base.Repo.CloneURL
build.Title = *hook.PullRequest.Title
// build.Timestamp = time.Now().UTC().Format("2006-01-02 15:04:05.000000000 +0000 MST")
return repo, build, nil
}
const (
StatusPending = "pending"
StatusSuccess = "success"
StatusFailure = "failure"
StatusError = "error"
)
const (
DescPending = "this build is pending"
DescSuccess = "the build was successful"
DescFailure = "the build failed"
DescError = "oops, something went wrong"
)
// getStatus is a helper functin that converts a Drone
// status to a GitHub status.
func getStatus(status string) string {
switch status {
case model.StatusPending, model.StatusRunning:
return StatusPending
case model.StatusSuccess:
return StatusSuccess
case model.StatusFailure:
return StatusFailure
case model.StatusError, model.StatusKilled:
return StatusError
default:
return StatusError
}
}
// getDesc is a helper function that generates a description
// message for the build based on the status.
func getDesc(status string) string {
switch status {
case model.StatusPending, model.StatusRunning:
return DescPending
case model.StatusSuccess:
return DescSuccess
case model.StatusFailure:
return DescFailure
case model.StatusError, model.StatusKilled:
return DescError
default:
return DescError
}
}

View File

@ -1,478 +0,0 @@
package github
import (
"crypto/tls"
"encoding/json"
"fmt"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"github.com/drone/drone/Godeps/_workspace/src/github.com/hashicorp/golang-lru"
"github.com/drone/drone/pkg/oauth2"
"github.com/drone/drone/pkg/remote"
common "github.com/drone/drone/pkg/types"
"github.com/drone/drone/pkg/utils/httputil"
"github.com/drone/drone/Godeps/_workspace/src/github.com/google/go-github/github"
)
const (
DefaultURL = "https://github.com"
DefaultAPI = "https://api.github.com"
DefaultScope = "repo,repo:status,user:email"
)
type GitHub struct {
URL string
API string
Client string
Secret string
AllowedOrgs []string
Open bool
PrivateMode bool
SkipVerify bool
cache *lru.Cache
}
func init() {
remote.Register("github", NewDriver)
}
func NewDriver(config string) (remote.Remote, error) {
url_, err := url.Parse(config)
if err != nil {
return nil, err
}
params := url_.Query()
url_.Path = ""
url_.RawQuery = ""
github := GitHub{}
github.URL = url_.String()
github.Client = params.Get("client_id")
github.Secret = params.Get("client_secret")
github.AllowedOrgs = params["orgs"]
github.PrivateMode, _ = strconv.ParseBool(params.Get("private_mode"))
github.SkipVerify, _ = strconv.ParseBool(params.Get("skip_verify"))
github.Open, _ = strconv.ParseBool(params.Get("open"))
if github.URL == DefaultURL {
github.API = DefaultAPI
} else {
github.API = github.URL + "/api/v3/"
}
// here we cache permissions to avoid too many api
// calls. this should really be moved outise the
// remote plugin into the app
github.cache, err = lru.New(1028)
return &github, err
}
func (g *GitHub) Login(token, secret string) (*common.User, error) {
client := NewClient(g.API, token, g.SkipVerify)
login, err := GetUserEmail(client)
if err != nil {
return nil, err
}
user := common.User{}
user.Login = *login.Login
user.Email = *login.Email
user.Token = token
user.Secret = secret
user.Avatar = *login.AvatarURL
return &user, nil
}
// Orgs fetches the organizations for the given user.
func (g *GitHub) Orgs(u *common.User) ([]string, error) {
client := NewClient(g.API, u.Token, g.SkipVerify)
orgs_ := []string{}
orgs, err := GetOrgs(client)
if err != nil {
return orgs_, err
}
for _, org := range orgs {
orgs_ = append(orgs_, *org.Login)
}
return orgs_, nil
}
// Accessor method, to allowed remote organizations field.
func (g *GitHub) GetOrgs() []string {
return g.AllowedOrgs
}
// Accessor method, to open field.
func (g *GitHub) GetOpen() bool {
return g.Open
}
// Repo fetches the named repository from the remote system.
func (g *GitHub) Repo(u *common.User, owner, name string) (*common.Repo, error) {
client := NewClient(g.API, u.Token, g.SkipVerify)
repo_, err := GetRepo(client, owner, name)
if err != nil {
return nil, err
}
repo := &common.Repo{}
repo.Owner = owner
repo.Name = name
repo.FullName = *repo_.FullName
repo.Link = *repo_.HTMLURL
repo.Private = *repo_.Private
repo.Clone = *repo_.CloneURL
repo.Branch = "master"
repo.Avatar = *repo_.Owner.AvatarURL
if repo_.DefaultBranch != nil {
repo.Branch = *repo_.DefaultBranch
}
if g.PrivateMode {
repo.Private = true
}
return repo, err
}
// Perm fetches the named repository from the remote system.
func (g *GitHub) Perm(u *common.User, owner, name string) (*common.Perm, error) {
key := fmt.Sprintf("%s/%s/%s", u.Login, owner, name)
val, ok := g.cache.Get(key)
if ok {
return val.(*common.Perm), nil
}
client := NewClient(g.API, u.Token, g.SkipVerify)
repo, err := GetRepo(client, owner, name)
if err != nil {
return nil, err
}
m := &common.Perm{}
m.Admin = (*repo.Permissions)["admin"]
m.Push = (*repo.Permissions)["push"]
m.Pull = (*repo.Permissions)["pull"]
g.cache.Add(key, m)
return m, nil
}
// Script fetches the build script (.drone.yml) from the remote
// repository and returns in string format.
func (g *GitHub) Script(u *common.User, r *common.Repo, b *common.Build) ([]byte, []byte, error) {
client := NewClient(g.API, u.Token, g.SkipVerify)
cfg, err := GetFile(client, r.Owner, r.Name, ".drone.yml", b.Commit.Sha)
sec, _ := GetFile(client, r.Owner, r.Name, ".drone.sec", b.Commit.Sha)
return cfg, sec, err
}
// Netrc returns a .netrc file that can be used to clone
// private repositories from a remote system.
func (g *GitHub) Netrc(u *common.User, r *common.Repo) (*common.Netrc, error) {
url_, err := url.Parse(g.URL)
if err != nil {
return nil, err
}
netrc := &common.Netrc{}
netrc.Login = u.Token
netrc.Password = "x-oauth-basic"
netrc.Machine = url_.Host
return netrc, nil
}
// Activate activates a repository by creating the post-commit hook and
// adding the SSH deploy key, if applicable.
func (g *GitHub) Activate(u *common.User, r *common.Repo, k *common.Keypair, link string) error {
client := NewClient(g.API, u.Token, g.SkipVerify)
title, err := GetKeyTitle(link)
if err != nil {
return err
}
// if the CloneURL is using the SSHURL then we know that
// we need to add an SSH key to GitHub.
if r.Private || g.PrivateMode {
_, err = CreateUpdateKey(client, r.Owner, r.Name, title, k.Public)
if err != nil {
return err
}
}
_, err = CreateUpdateHook(client, r.Owner, r.Name, link)
return err
}
// Deactivate removes a repository by removing all the post-commit hooks
// which are equal to link and removing the SSH deploy key.
func (g *GitHub) Deactivate(u *common.User, r *common.Repo, link string) error {
client := NewClient(g.API, u.Token, g.SkipVerify)
title, err := GetKeyTitle(link)
if err != nil {
return err
}
// remove the deploy-key if it is installed remote.
if r.Private || g.PrivateMode {
if err := DeleteKey(client, r.Owner, r.Name, title); err != nil {
return err
}
}
return DeleteHook(client, r.Owner, r.Name, link)
}
func (g *GitHub) Status(u *common.User, r *common.Repo, b *common.Build) error {
client := NewClient(g.API, u.Token, g.SkipVerify)
link := fmt.Sprintf("%s/%v", r.Self, b.Number)
status := getStatus(b.Status)
desc := getDesc(b.Status)
data := github.RepoStatus{
Context: github.String("Drone"),
State: github.String(status),
Description: github.String(desc),
TargetURL: github.String(link),
}
_, _, err := client.Repositories.CreateStatus(r.Owner, r.Name, b.Commit.Sha, &data)
return err
}
// Hook parses the post-commit hook from the Request body
// and returns the required data in a standard format.
func (g *GitHub) Hook(r *http.Request) (*common.Hook, error) {
switch r.Header.Get("X-Github-Event") {
case "pull_request":
return g.pullRequest(r)
case "push":
return g.push(r)
default:
return nil, nil
}
}
// return default scope for GitHub
func (g *GitHub) Scope() string {
return DefaultScope
}
// push parses a hook with event type `push` and returns
// the commit data.
func (g *GitHub) push(r *http.Request) (*common.Hook, error) {
payload := GetPayload(r)
hook := &pushHook{}
err := json.Unmarshal(payload, hook)
if err != nil {
return nil, err
}
if hook.Deleted {
return nil, nil
}
repo := &common.Repo{}
repo.Owner = hook.Repo.Owner.Login
if len(repo.Owner) == 0 {
repo.Owner = hook.Repo.Owner.Name
}
repo.Name = hook.Repo.Name
// Generating rather than using hook.Repo.FullName as it's
// not always present
repo.FullName = fmt.Sprintf("%s/%s", repo.Owner, repo.Name)
repo.Link = hook.Repo.HTMLURL
repo.Private = hook.Repo.Private
repo.Clone = hook.Repo.CloneURL
repo.Branch = hook.Repo.DefaultBranch
commit := &common.Commit{}
commit.Sha = hook.Head.ID
commit.Ref = hook.Ref
commit.Link = hook.Head.URL
commit.Branch = strings.Replace(commit.Ref, "refs/heads/", "", -1)
commit.Message = hook.Head.Message
commit.Timestamp = hook.Head.Timestamp
commit.Author = &common.Author{}
commit.Author.Email = hook.Head.Author.Email
commit.Author.Login = hook.Head.Author.Username
commit.Remote = hook.Repo.CloneURL
// we should ignore github pages
if commit.Ref == "refs/heads/gh-pages" {
return nil, nil
}
return &common.Hook{Event: "push", Repo: repo, Commit: commit}, nil
}
// ¯\_(ツ)_/¯
func (g *GitHub) Oauth2Transport(r *http.Request) *oauth2.Transport {
return &oauth2.Transport{
Config: &oauth2.Config{
ClientId: g.Client,
ClientSecret: g.Secret,
Scope: DefaultScope,
AuthURL: fmt.Sprintf("%s/login/oauth/authorize", g.URL),
TokenURL: fmt.Sprintf("%s/login/oauth/access_token", g.URL),
RedirectURL: fmt.Sprintf("%s/authorize", httputil.GetURL(r)),
//settings.Server.Scheme, settings.Server.Hostname),
},
Transport: &http.Transport{
Proxy: http.ProxyFromEnvironment,
TLSClientConfig: &tls.Config{InsecureSkipVerify: g.SkipVerify},
},
}
}
// pullRequest parses a hook with event type `pullRequest`
// and returns the commit data.
func (g *GitHub) pullRequest(r *http.Request) (*common.Hook, error) {
payload := GetPayload(r)
hook := &struct {
Action string `json:"action"`
PullRequest *github.PullRequest `json:"pull_request"`
Repo *github.Repository `json:"repository"`
}{}
err := json.Unmarshal(payload, hook)
if err != nil {
return nil, err
}
// ignore these
if hook.Action != "opened" && hook.Action != "synchronize" {
return nil, nil
}
if *hook.PullRequest.State != "open" {
return nil, nil
}
repo := &common.Repo{}
repo.Owner = *hook.Repo.Owner.Login
repo.Name = *hook.Repo.Name
repo.FullName = *hook.Repo.FullName
repo.Link = *hook.Repo.HTMLURL
repo.Private = *hook.Repo.Private
repo.Clone = *hook.Repo.CloneURL
repo.Branch = "master"
if hook.Repo.DefaultBranch != nil {
repo.Branch = *hook.Repo.DefaultBranch
}
c := &common.Commit{}
c.Sha = *hook.PullRequest.Head.SHA
c.Ref = *hook.PullRequest.Head.Ref
c.Ref = fmt.Sprintf("refs/pull/%d/merge", *hook.PullRequest.Number)
c.Branch = *hook.PullRequest.Head.Ref
c.Timestamp = time.Now().UTC().Format("2006-01-02 15:04:05.000000000 +0000 MST")
c.Remote = *hook.PullRequest.Head.Repo.CloneURL
c.Author = &common.Author{}
c.Author.Login = *hook.PullRequest.Head.User.Login
// Author.Email
// Message
pr := &common.PullRequest{}
pr.Number = *hook.PullRequest.Number
pr.Title = *hook.PullRequest.Title
pr.Base = &common.Commit{}
pr.Base.Sha = *hook.PullRequest.Base.SHA
pr.Base.Ref = *hook.PullRequest.Base.Ref
pr.Base.Remote = *hook.PullRequest.Base.Repo.CloneURL
pr.Link = *hook.PullRequest.HTMLURL
// Branch
// Message
// Timestamp
// Author.Login
// Author.Email
return &common.Hook{Event: "pull_request", Repo: repo, Commit: c, PullRequest: pr}, nil
}
type pushHook struct {
Ref string `json:"ref"`
Deleted bool `json:"deleted"`
Head struct {
ID string `json:"id"`
URL string `json:"url"`
Message string `json:"message"`
Timestamp string `json:"timestamp"`
Author struct {
Name string `json:"name"`
Email string `json:"email"`
Username string `json:"username"`
} `json:"author"`
Committer struct {
Name string `json:"name"`
Email string `json:"email"`
Username string `json:"username"`
} `json:"committer"`
} `json:"head_commit"`
Repo struct {
Owner struct {
Login string `json:"login"`
Name string `json:"name"`
} `json:"owner"`
Name string `json:"name"`
FullName string `json:"full_name"`
Language string `json:"language"`
Private bool `json:"private"`
HTMLURL string `json:"html_url"`
CloneURL string `json:"clone_url"`
DefaultBranch string `json:"default_branch"`
} `json:"repository"`
}
const (
StatusPending = "pending"
StatusSuccess = "success"
StatusFailure = "failure"
StatusError = "error"
)
const (
DescPending = "this build is pending"
DescSuccess = "the build was successful"
DescFailure = "the build failed"
DescError = "oops, something went wrong"
)
// getStatus is a helper functin that converts a Drone
// status to a GitHub status.
func getStatus(status string) string {
switch status {
case common.StatePending, common.StateRunning:
return StatusPending
case common.StateSuccess:
return StatusSuccess
case common.StateFailure:
return StatusFailure
case common.StateError, common.StateKilled:
return StatusError
default:
return StatusError
}
}
// getDesc is a helper function that generates a description
// message for the build based on the status.
func getDesc(status string) string {
switch status {
case common.StatePending, common.StateRunning:
return DescPending
case common.StateSuccess:
return DescSuccess
case common.StateFailure:
return DescFailure
case common.StateError, common.StateKilled:
return DescError
default:
return DescError
}
}

View File

@ -9,9 +9,9 @@ import (
"net/url"
"strings"
"github.com/drone/drone/Godeps/_workspace/src/github.com/google/go-github/github"
"github.com/drone/drone/Godeps/_workspace/src/github.com/gorilla/securecookie"
"github.com/drone/drone/pkg/oauth2"
"github.com/drone/drone/shared/oauth2"
"github.com/google/go-github/github"
"github.com/gorilla/securecookie"
)
// NewClient is a helper function that returns a new GitHub

48
remote/github/types.go Normal file
View File

@ -0,0 +1,48 @@
package github
type postHook struct {
}
type pushHook struct {
Ref string `json:"ref"`
Deleted bool `json:"deleted"`
Head struct {
ID string `json:"id"`
URL string `json:"url"`
Message string `json:"message"`
Timestamp string `json:"timestamp"`
Author struct {
Name string `json:"name"`
Email string `json:"name"`
Username string `json:"username"`
} `json:"author"`
Committer struct {
Name string `json:"name"`
Email string `json:"name"`
Username string `json:"username"`
} `json:"committer"`
} `json:"head_commit"`
Sender struct {
Login string `json:"login"`
Avatar string `json:"avatar_url"`
}
Repo struct {
Owner struct {
Login string `json:"login"`
Name string `json:"name"`
} `json:"owner"`
Name string `json:"name"`
FullName string `json:"full_name"`
Language string `json:"language"`
Private bool `json:"private"`
HTMLURL string `json:"html_url"`
CloneURL string `json:"clone_url"`
DefaultBranch string `json:"default_branch"`
} `json:"repository"`
}

View File

@ -9,13 +9,13 @@ import (
"strconv"
"strings"
"github.com/drone/drone/Godeps/_workspace/src/github.com/Bugagazavr/go-gitlab-client"
"github.com/drone/drone/Godeps/_workspace/src/github.com/hashicorp/golang-lru"
"github.com/drone/drone/pkg/oauth2"
"github.com/drone/drone/pkg/remote"
"github.com/drone/drone/pkg/token"
common "github.com/drone/drone/pkg/types"
"github.com/drone/drone/pkg/utils/httputil"
"github.com/drone/drone/model"
"github.com/drone/drone/shared/envconfig"
"github.com/drone/drone/shared/httputil"
"github.com/drone/drone/shared/oauth2"
"github.com/drone/drone/shared/token"
"github.com/Bugagazavr/go-gitlab-client"
)
const (
@ -32,18 +32,14 @@ type Gitlab struct {
PrivateMode bool
SkipVerify bool
Search bool
cache *lru.Cache
}
func init() {
remote.Register("gitlab", NewDriver)
}
func Load(env envconfig.Env) *Gitlab {
config := env.String("REMOTE_CONFIG", "")
func NewDriver(config string) (remote.Remote, error) {
url_, err := url.Parse(config)
if err != nil {
return nil, err
panic(err)
}
params := url_.Query()
url_.RawQuery = ""
@ -66,40 +62,71 @@ func NewDriver(config string) (remote.Remote, error) {
// this is a temp workaround
gitlab.Search, _ = strconv.ParseBool(params.Get("search"))
// here we cache permissions to avoid too many api
// calls. this should really be moved outise the
// remote plugin into the app
gitlab.cache, err = lru.New(1028)
return &gitlab, err
return &gitlab
}
func (g *Gitlab) Login(token, secret string) (*common.User, error) {
client := NewClient(g.URL, token, g.SkipVerify)
var login, err = client.CurrentUser()
if err != nil {
return nil, err
// Login authenticates the session and returns the
// remote user details.
func (g *Gitlab) Login(res http.ResponseWriter, req *http.Request) (*model.User, bool, error) {
var config = &oauth2.Config{
ClientId: g.Client,
ClientSecret: g.Secret,
Scope: DefaultScope,
AuthURL: fmt.Sprintf("%s/oauth/authorize", g.URL),
TokenURL: fmt.Sprintf("%s/oauth/token", g.URL),
RedirectURL: fmt.Sprintf("%s/authorize", httputil.GetURL(req)),
}
user := common.User{}
trans_ := &http.Transport{
Proxy: http.ProxyFromEnvironment,
TLSClientConfig: &tls.Config{InsecureSkipVerify: g.SkipVerify},
}
// get the OAuth code
var code = req.FormValue("code")
if len(code) == 0 {
http.Redirect(res, req, config.AuthCodeURL("drone"), http.StatusSeeOther)
return nil, false, nil
}
var trans = &oauth2.Transport{Config: config, Transport: trans_}
var token_, err = trans.Exchange(code)
if err != nil {
return nil, false, fmt.Errorf("Error exchanging token. %s", err)
}
client := NewClient(g.URL, token_.AccessToken, g.SkipVerify)
login, err := client.CurrentUser()
if err != nil {
return nil, false, err
}
user := &model.User{}
user.Login = login.Username
user.Email = login.Email
user.Token = token
user.Secret = secret
user.Token = token_.AccessToken
user.Secret = token_.RefreshToken
if strings.HasPrefix(login.AvatarUrl, "http") {
user.Avatar = login.AvatarUrl
} else {
user.Avatar = g.URL + "/" + login.AvatarUrl
}
return &user, nil
return user, true, nil
}
// Orgs fetches the organizations for the given user.
func (g *Gitlab) Orgs(u *common.User) ([]string, error) {
return nil, nil
func (g *Gitlab) Auth(token, secret string) (string, error) {
client := NewClient(g.URL, token, g.SkipVerify)
login, err := client.CurrentUser()
if err != nil {
return "", err
}
return login.Username, nil
}
// Repo fetches the named repository from the remote system.
func (g *Gitlab) Repo(u *common.User, owner, name string) (*common.Repo, error) {
func (g *Gitlab) Repo(u *model.User, owner, name string) (*model.Repo, error) {
client := NewClient(g.URL, u.Token, g.SkipVerify)
id, err := GetProjectId(g, client, owner, name)
if err != nil {
@ -110,7 +137,7 @@ func (g *Gitlab) Repo(u *common.User, owner, name string) (*common.Repo, error)
return nil, err
}
repo := &common.Repo{}
repo := &model.Repo{}
repo.Owner = owner
repo.Name = name
repo.FullName = repo_.PathWithNamespace
@ -123,22 +150,44 @@ func (g *Gitlab) Repo(u *common.User, owner, name string) (*common.Repo, error)
}
if g.PrivateMode {
repo.Private = true
repo.IsPrivate = true
} else {
repo.Private = !repo_.Public
repo.IsPrivate = !repo_.Public
}
return repo, err
}
// Perm fetches the named repository from the remote system.
func (g *Gitlab) Perm(u *common.User, owner, name string) (*common.Perm, error) {
key := fmt.Sprintf("%s/%s/%s", u.Login, owner, name)
val, ok := g.cache.Get(key)
if ok {
return val.(*common.Perm), nil
// Repos fetches a list of repos from the remote system.
func (g *Gitlab) Repos(u *model.User) ([]*model.RepoLite, error) {
client := NewClient(g.URL, u.Token, g.SkipVerify)
var repos = []*model.RepoLite{}
all, err := client.AllProjects()
if err != nil {
return repos, err
}
for _, repo := range all {
var parts = strings.Split(repo.PathWithNamespace, "/")
var owner = parts[0]
var name = parts[1]
repos = append(repos, &model.RepoLite{
Owner: owner,
Name: name,
FullName: repo.PathWithNamespace,
})
// TODO: add repo.AvatarUrl
}
return repos, err
}
// Perm fetches the named repository from the remote system.
func (g *Gitlab) Perm(u *model.User, owner, name string) (*model.Perm, error) {
client := NewClient(g.URL, u.Token, g.SkipVerify)
id, err := GetProjectId(g, client, owner, name)
if err != nil {
@ -149,43 +198,42 @@ func (g *Gitlab) Perm(u *common.User, owner, name string) (*common.Perm, error)
if err != nil {
return nil, err
}
m := &common.Perm{}
m := &model.Perm{}
m.Admin = IsAdmin(repo)
m.Pull = IsRead(repo)
m.Push = IsWrite(repo)
g.cache.Add(key, m)
return m, nil
}
// GetScript fetches the build script (.drone.yml) from the remote
// repository and returns in string format.
func (g *Gitlab) Script(user *common.User, repo *common.Repo, build *common.Build) ([]byte, []byte, error) {
func (g *Gitlab) Script(user *model.User, repo *model.Repo, build *model.Build) ([]byte, []byte, error) {
var client = NewClient(g.URL, user.Token, g.SkipVerify)
id, err := GetProjectId(g, client, repo.Owner, repo.Name)
if err != nil {
return nil, nil, err
}
cfg, err := client.RepoRawFile(id, build.Commit.Sha, ".drone.yml")
enc, _ := client.RepoRawFile(id, build.Commit.Sha, ".drone.sec")
cfg, err := client.RepoRawFile(id, build.Commit, ".drone.yml")
enc, _ := client.RepoRawFile(id, build.Commit, ".drone.sec")
return cfg, enc, err
}
// NOTE Currently gitlab doesn't support status for commits and events,
// also if we want get MR status in gitlab we need implement a special plugin for gitlab,
// gitlab uses API to fetch build status on client side. But for now we skip this.
func (g *Gitlab) Status(u *common.User, repo *common.Repo, b *common.Build) error {
func (g *Gitlab) Status(u *model.User, repo *model.Repo, b *model.Build, link string) error {
return nil
}
// Netrc returns a .netrc file that can be used to clone
// private repositories from a remote system.
func (g *Gitlab) Netrc(u *common.User, r *common.Repo) (*common.Netrc, error) {
func (g *Gitlab) Netrc(u *model.User, r *model.Repo) (*model.Netrc, error) {
url_, err := url.Parse(g.URL)
if err != nil {
return nil, err
}
netrc := &common.Netrc{}
netrc := &model.Netrc{}
netrc.Machine = url_.Host
switch g.CloneMode {
@ -202,7 +250,7 @@ func (g *Gitlab) Netrc(u *common.User, r *common.Repo) (*common.Netrc, error) {
// Activate activates a repository by adding a Post-commit hook and
// a Public Deploy key, if applicable.
func (g *Gitlab) Activate(user *common.User, repo *common.Repo, k *common.Keypair, link string) error {
func (g *Gitlab) Activate(user *model.User, repo *model.Repo, k *model.Key, link string) error {
var client = NewClient(g.URL, user.Token, g.SkipVerify)
id, err := GetProjectId(g, client, repo.Owner, repo.Name)
if err != nil {
@ -227,7 +275,7 @@ func (g *Gitlab) Activate(user *common.User, repo *common.Repo, k *common.Keypai
// Deactivate removes a repository by removing all the post-commit hooks
// which are equal to link and removing the SSH deploy key.
func (g *Gitlab) Deactivate(user *common.User, repo *common.Repo, link string) error {
func (g *Gitlab) Deactivate(user *model.User, repo *model.Repo, link string) error {
var client = NewClient(g.URL, user.Token, g.SkipVerify)
id, err := GetProjectId(g, client, repo.Owner, repo.Name)
if err != nil {
@ -239,12 +287,12 @@ func (g *Gitlab) Deactivate(user *common.User, repo *common.Repo, link string) e
// ParseHook parses the post-commit hook from the Request body
// and returns the required data in a standard format.
func (g *Gitlab) Hook(req *http.Request) (*common.Hook, error) {
func (g *Gitlab) Hook(req *http.Request) (*model.Repo, *model.Build, error) {
defer req.Body.Close()
var payload, _ = ioutil.ReadAll(req.Body)
var parsed, err = gogitlab.ParseHook(payload)
if err != nil {
return nil, err
return nil, nil, err
}
switch parsed.ObjectKind {
@ -253,92 +301,87 @@ func (g *Gitlab) Hook(req *http.Request) (*common.Hook, error) {
case "tag_push", "push":
return push(parsed, req)
default:
return nil, nil
return nil, nil, nil
}
}
func mergeRequest(parsed *gogitlab.HookPayload, req *http.Request) (*common.Hook, error) {
var hook = new(common.Hook)
hook.Event = "pull_request"
hook.Repo = &common.Repo{}
hook.Repo.Owner = req.FormValue("owner")
hook.Repo.Name = req.FormValue("name")
hook.Repo.FullName = fmt.Sprintf("%s/%s", hook.Repo.Owner, hook.Repo.Name)
hook.Repo.Link = parsed.ObjectAttributes.Target.WebUrl
hook.Repo.Clone = parsed.ObjectAttributes.Target.HttpUrl
hook.Repo.Branch = "master"
func mergeRequest(parsed *gogitlab.HookPayload, req *http.Request) (*model.Repo, *model.Build, error) {
hook.Commit = &common.Commit{}
hook.Commit.Message = parsed.ObjectAttributes.LastCommit.Message
hook.Commit.Sha = parsed.ObjectAttributes.LastCommit.Id
hook.Commit.Remote = parsed.ObjectAttributes.Source.HttpUrl
repo := &model.Repo{}
repo.Owner = req.FormValue("owner")
repo.Name = req.FormValue("name")
repo.FullName = fmt.Sprintf("%s/%s", repo.Owner, repo.Name)
repo.Link = parsed.ObjectAttributes.Target.WebUrl
repo.Clone = parsed.ObjectAttributes.Target.HttpUrl
repo.Branch = "master"
build := &model.Build{}
build.Event = "pull_request"
build.Message = parsed.ObjectAttributes.LastCommit.Message
build.Commit = parsed.ObjectAttributes.LastCommit.Id
//build.Remote = parsed.ObjectAttributes.Source.HttpUrl
if parsed.ObjectAttributes.SourceProjectId == parsed.ObjectAttributes.TargetProjectId {
hook.Commit.Ref = fmt.Sprintf("refs/heads/%s", parsed.ObjectAttributes.SourceBranch)
build.Ref = fmt.Sprintf("refs/heads/%s", parsed.ObjectAttributes.SourceBranch)
} else {
hook.Commit.Ref = fmt.Sprintf("refs/merge-requests/%d/head", parsed.ObjectAttributes.IId)
build.Ref = fmt.Sprintf("refs/merge-requests/%d/head", parsed.ObjectAttributes.IId)
}
hook.Commit.Branch = parsed.ObjectAttributes.SourceBranch
hook.Commit.Timestamp = parsed.ObjectAttributes.LastCommit.Timestamp
build.Branch = parsed.ObjectAttributes.SourceBranch
// build.Timestamp = parsed.ObjectAttributes.LastCommit.Timestamp
hook.Commit.Author = &common.Author{}
hook.Commit.Author.Login = parsed.ObjectAttributes.LastCommit.Author.Name
hook.Commit.Author.Email = parsed.ObjectAttributes.LastCommit.Author.Email
build.Author = parsed.ObjectAttributes.LastCommit.Author.Name
build.Email = parsed.ObjectAttributes.LastCommit.Author.Email
build.Title = parsed.ObjectAttributes.Title
build.Link = parsed.ObjectAttributes.Url
hook.PullRequest = &common.PullRequest{}
hook.PullRequest.Number = parsed.ObjectAttributes.IId
hook.PullRequest.Title = parsed.ObjectAttributes.Title
hook.PullRequest.Link = parsed.ObjectAttributes.Url
return hook, nil
return repo, build, nil
}
func push(parsed *gogitlab.HookPayload, req *http.Request) (*common.Hook, error) {
func push(parsed *gogitlab.HookPayload, req *http.Request) (*model.Repo, *model.Build, error) {
var cloneUrl = parsed.Repository.GitHttpUrl
var hook = new(common.Hook)
hook.Event = "push"
hook.Repo = &common.Repo{}
hook.Repo.Owner = req.FormValue("owner")
hook.Repo.Name = req.FormValue("name")
hook.Repo.Link = parsed.Repository.URL
hook.Repo.Clone = cloneUrl
hook.Repo.Branch = "master"
repo := &model.Repo{}
repo.Owner = req.FormValue("owner")
repo.Name = req.FormValue("name")
repo.FullName = fmt.Sprintf("%s/%s", repo.Owner, repo.Name)
repo.Link = parsed.Repository.URL
repo.Clone = cloneUrl
repo.Branch = "master"
switch parsed.Repository.VisibilityLevel {
case 0:
hook.Repo.Private = true
repo.IsPrivate = true
case 10:
hook.Repo.Private = true
repo.IsPrivate = true
case 20:
hook.Repo.Private = false
repo.IsPrivate = false
}
hook.Repo.FullName = fmt.Sprintf("%s/%s", req.FormValue("owner"), req.FormValue("name"))
repo.FullName = fmt.Sprintf("%s/%s", req.FormValue("owner"), req.FormValue("name"))
hook.Commit = &common.Commit{}
hook.Commit.Sha = parsed.After
hook.Commit.Branch = parsed.Branch()
hook.Commit.Ref = parsed.Ref
hook.Commit.Remote = cloneUrl
build := &model.Build{}
build.Event = model.EventPush
build.Commit = parsed.After
build.Branch = parsed.Branch()
build.Ref = parsed.Ref
// hook.Commit.Remote = cloneUrl
var head = parsed.Head()
hook.Commit.Message = head.Message
hook.Commit.Timestamp = head.Timestamp
hook.Commit.Author = &common.Author{}
build.Message = head.Message
// build.Timestamp = head.Timestamp
// extracts the commit author (ideally email)
// from the post-commit hook
switch {
case head.Author != nil:
hook.Commit.Author.Email = head.Author.Email
hook.Commit.Author.Login = parsed.UserName
build.Email = head.Author.Email
build.Author = parsed.UserName
case head.Author == nil:
hook.Commit.Author.Login = parsed.UserName
build.Author = parsed.UserName
}
return hook, nil
return repo, build, nil
}
// ¯\_(ツ)_/¯

View File

@ -2,13 +2,12 @@ package gitlab
import (
"bytes"
"fmt"
"net/http"
"testing"
"github.com/drone/drone/Godeps/_workspace/src/github.com/franela/goblin"
"github.com/drone/drone/pkg/remote/builtin/gitlab/testdata"
"github.com/drone/drone/pkg/types"
"github.com/drone/drone/model"
"github.com/drone/drone/remote/gitlab/testdata"
"github.com/franela/goblin"
)
func Test_Gitlab(t *testing.T) {
@ -16,17 +15,17 @@ func Test_Gitlab(t *testing.T) {
var server = testdata.NewServer()
defer server.Close()
var gitlab, err = NewDriver(server.URL + "?client_id=test&client_secret=test")
if err != nil {
panic(err)
}
env := map[string]string{}
env["REMOTE_CONFIG"] = server.URL + "?client_id=test&client_secret=test"
var user = types.User{
gitlab := Load(env)
var user = model.User{
Login: "test_user",
Token: "e3b0c44298fc1c149afbf4c8996fb",
}
var repo = types.Repo{
var repo = model.Repo{
Name: "diaspora-client",
Owner: "diaspora",
}
@ -56,7 +55,6 @@ func Test_Gitlab(t *testing.T) {
g.It("Should return repo permissions", func() {
perm, err := gitlab.Perm(&user, "diaspora", "diaspora-client")
fmt.Println(gitlab.(*Gitlab), err)
g.Assert(err == nil).IsTrue()
g.Assert(perm.Admin).Equal(true)
g.Assert(perm.Pull).Equal(true)
@ -73,13 +71,13 @@ func Test_Gitlab(t *testing.T) {
// Test activate method
g.Describe("Activate", func() {
g.It("Should be success", func() {
err := gitlab.Activate(&user, &repo, &types.Keypair{}, "http://example.com/api/hook/test/test?access_token=token")
err := gitlab.Activate(&user, &repo, &model.Key{}, "http://example.com/api/hook/test/test?access_token=token")
g.Assert(err == nil).IsTrue()
})
g.It("Should be failed, when token not given", func() {
err := gitlab.Activate(&user, &repo, &types.Keypair{}, "http://example.com/api/hook/test/test")
err := gitlab.Activate(&user, &repo, &model.Key{}, "http://example.com/api/hook/test/test")
g.Assert(err != nil).IsTrue()
})
@ -95,20 +93,20 @@ func Test_Gitlab(t *testing.T) {
})
// Test login method
g.Describe("Login", func() {
g.It("Should return user", func() {
user, err := gitlab.Login("valid_token", "")
// g.Describe("Login", func() {
// g.It("Should return user", func() {
// user, err := gitlab.Login("valid_token", "")
g.Assert(err == nil).IsTrue()
g.Assert(user == nil).IsFalse()
})
// g.Assert(err == nil).IsTrue()
// g.Assert(user == nil).IsFalse()
// })
g.It("Should return error, when token is invalid", func() {
_, err := gitlab.Login("invalid_token", "")
// g.It("Should return error, when token is invalid", func() {
// _, err := gitlab.Login("invalid_token", "")
g.Assert(err != nil).IsTrue()
})
})
// g.Assert(err != nil).IsTrue()
// })
// })
// Test hook method
g.Describe("Hook", func() {
@ -119,14 +117,13 @@ func Test_Gitlab(t *testing.T) {
bytes.NewReader(testdata.PushHook),
)
hook, err := gitlab.Hook(req)
repo, build, err := gitlab.Hook(req)
g.Assert(err == nil).IsTrue()
g.Assert(hook.Repo.Owner).Equal("diaspora")
g.Assert(hook.Repo.Name).Equal("diaspora-client")
g.Assert(hook.Commit.Ref).Equal("refs/heads/master")
g.Assert(repo.Owner).Equal("diaspora")
g.Assert(repo.Name).Equal("diaspora-client")
g.Assert(build.Ref).Equal("refs/heads/master")
g.Assert(hook.PullRequest == nil).IsTrue()
})
g.It("Should parse tag push hook", func() {
@ -136,14 +133,13 @@ func Test_Gitlab(t *testing.T) {
bytes.NewReader(testdata.TagHook),
)
hook, err := gitlab.Hook(req)
repo, build, err := gitlab.Hook(req)
g.Assert(err == nil).IsTrue()
g.Assert(hook.Repo.Owner).Equal("diaspora")
g.Assert(hook.Repo.Name).Equal("diaspora-client")
g.Assert(hook.Commit.Ref).Equal("refs/tags/v1.0.0")
g.Assert(repo.Owner).Equal("diaspora")
g.Assert(repo.Name).Equal("diaspora-client")
g.Assert(build.Ref).Equal("refs/tags/v1.0.0")
g.Assert(hook.PullRequest == nil).IsTrue()
})
g.It("Should parse merge request hook", func() {
@ -153,14 +149,13 @@ func Test_Gitlab(t *testing.T) {
bytes.NewReader(testdata.MergeRequestHook),
)
hook, err := gitlab.Hook(req)
repo, build, err := gitlab.Hook(req)
g.Assert(err == nil).IsTrue()
g.Assert(hook.Repo.Owner).Equal("diaspora")
g.Assert(hook.Repo.Name).Equal("diaspora-client")
g.Assert(repo.Owner).Equal("diaspora")
g.Assert(repo.Name).Equal("diaspora-client")
g.Assert(hook.PullRequest.Number).Equal(1)
g.Assert(hook.PullRequest.Title).Equal("MS-Viewport")
g.Assert(build.Title).Equal("MS-Viewport")
})
})
})

View File

@ -5,7 +5,7 @@ import (
"net/url"
"strconv"
"github.com/drone/drone/Godeps/_workspace/src/github.com/Bugagazavr/go-gitlab-client"
"github.com/Bugagazavr/go-gitlab-client"
)
// NewClient is a helper function that returns a new GitHub

View File

@ -3,91 +3,70 @@ package remote
import (
"net/http"
"github.com/drone/drone/pkg/oauth2"
"github.com/drone/drone/pkg/types"
"github.com/drone/drone/model"
"github.com/drone/drone/remote/github"
"github.com/drone/drone/remote/gitlab"
"github.com/drone/drone/shared/envconfig"
log "github.com/drone/drone/Godeps/_workspace/src/github.com/Sirupsen/logrus"
log "github.com/Sirupsen/logrus"
)
var drivers = make(map[string]DriverFunc)
func Load(env envconfig.Env) Remote {
driver := env.Get("REMOTE_DRIVER")
// Register makes a remote driver available by the provided name.
// If Register is called twice with the same name or if driver is nil,
// it panics.
func Register(name string, driver DriverFunc) {
if driver == nil {
panic("remote: Register driver is nil")
}
if _, dup := drivers[name]; dup {
panic("remote: Register called twice for driver " + name)
}
drivers[name] = driver
}
switch driver {
case "github":
return github.Load(env)
case "gitlab":
return gitlab.Load(env)
// DriverFunc returns a new connection to the remote.
// Config is a struct, with base remote configuration.
type DriverFunc func(config string) (Remote, error)
// New creates a new remote connection.
func New(driver, config string) (Remote, error) {
fn, ok := drivers[driver]
if !ok {
log.Fatalf("remote: unknown driver %q", driver)
default:
log.Fatalf("unknown remote driver %s", driver)
}
log.Infof("remote: loading driver %s", driver)
log.Infof("remote: loading config %s", config)
return fn(config)
return nil
}
type Remote interface {
// Login authenticates the session and returns the
// remote user details.
Login(token, secret string) (*types.User, error)
Login(w http.ResponseWriter, r *http.Request) (*model.User, bool, error)
// Orgs fetches the organizations for the given user.
Orgs(u *types.User) ([]string, error)
// Auth authenticates the session and returns the remote user
// login for the given token and secret
Auth(token, secret string) (string, error)
// Repo fetches the named repository from the remote system.
Repo(u *types.User, owner, repo string) (*types.Repo, error)
Repo(u *model.User, owner, repo string) (*model.Repo, error)
// Repos fetches a list of repos from the remote system.
Repos(u *model.User) ([]*model.RepoLite, error)
// Perm fetches the named repository permissions from
// the remote system for the specified user.
Perm(u *types.User, owner, repo string) (*types.Perm, error)
Perm(u *model.User, owner, repo string) (*model.Perm, error)
// Script fetches the build script (.drone.yml) from the remote
// repository and returns in string format.
Script(u *types.User, r *types.Repo, b *types.Build) ([]byte, []byte, error)
Script(u *model.User, r *model.Repo, b *model.Build) ([]byte, []byte, error)
// Status sends the commit status to the remote system.
// An example would be the GitHub pull request status.
Status(u *types.User, r *types.Repo, b *types.Build) error
Status(u *model.User, r *model.Repo, b *model.Build, link string) error
// Netrc returns a .netrc file that can be used to clone
// private repositories from a remote system.
Netrc(u *types.User, r *types.Repo) (*types.Netrc, error)
Netrc(u *model.User, r *model.Repo) (*model.Netrc, error)
// Activate activates a repository by creating the post-commit hook and
// adding the SSH deploy key, if applicable.
Activate(u *types.User, r *types.Repo, k *types.Keypair, link string) error
Activate(u *model.User, r *model.Repo, k *model.Key, link string) error
// Deactivate removes a repository by removing all the post-commit hooks
// which are equal to link and removing the SSH deploy key.
Deactivate(u *types.User, r *types.Repo, link string) error
Deactivate(u *model.User, r *model.Repo, link string) error
// Hook parses the post-commit hook from the Request body
// and returns the required data in a standard format.
Hook(r *http.Request) (*types.Hook, error)
// Oauth2Transport
Oauth2Transport(r *http.Request) *oauth2.Transport
// GetOrgs returns all allowed organizations for remote.
GetOrgs() []string
// GetOpen returns boolean field with enabled or disabled
// registration.
GetOpen() bool
// Default scope for remote
Scope() string
Hook(r *http.Request) (*model.Repo, *model.Build, error)
}

View File

@ -0,0 +1,42 @@
package context
import (
"database/sql"
"github.com/drone/drone/engine"
"github.com/drone/drone/remote"
"github.com/gin-gonic/gin"
)
func SetDatabase(db *sql.DB) gin.HandlerFunc {
return func(c *gin.Context) {
c.Set("database", db)
c.Next()
}
}
func Database(c *gin.Context) *sql.DB {
return c.MustGet("database").(*sql.DB)
}
func SetRemote(remote remote.Remote) gin.HandlerFunc {
return func(c *gin.Context) {
c.Set("remote", remote)
c.Next()
}
}
func Remote(c *gin.Context) remote.Remote {
return c.MustGet("remote").(remote.Remote)
}
func SetEngine(engine engine.Engine) gin.HandlerFunc {
return func(c *gin.Context) {
c.Set("engine", engine)
c.Next()
}
}
func Engine(c *gin.Context) engine.Engine {
return c.MustGet("engine").(engine.Engine)
}

View File

@ -0,0 +1,40 @@
package header
import (
"net/http"
"time"
"github.com/gin-gonic/gin"
)
func SetHeaders() gin.HandlerFunc {
return func(c *gin.Context) {
c.Writer.Header().Add("Access-Control-Allow-Origin", "*")
c.Writer.Header().Add("X-Frame-Options", "DENY")
c.Writer.Header().Add("X-Content-Type-Options", "nosniff")
c.Writer.Header().Add("X-XSS-Protection", "1; mode=block")
c.Writer.Header().Add("Cache-Control", "no-cache")
c.Writer.Header().Add("Cache-Control", "no-store")
c.Writer.Header().Add("Cache-Control", "max-age=0")
c.Writer.Header().Add("Cache-Control", "must-revalidate")
c.Writer.Header().Add("Cache-Control", "value")
c.Writer.Header().Set("Last-Modified", time.Now().UTC().Format(http.TimeFormat))
c.Writer.Header().Set("Expires", "Thu, 01 Jan 1970 00:00:00 GMT")
//c.Writer.Header().Set("Content-Security-Policy", "script-src 'self' https://cdnjs.cloudflare.com")
if c.Request.TLS != nil {
c.Writer.Header().Add("Strict-Transport-Security", "max-age=31536000")
}
if c.Request.Method == "OPTIONS" {
c.Writer.Header().Set("Access-Control-Allow-Methods", "GET,POST,PUT,PATCH,DELETE,OPTIONS")
c.Writer.Header().Set("Access-Control-Allow-Headers", "Authorization")
c.Writer.Header().Set("Allow", "HEAD,GET,POST,PUT,PATCH,DELETE,OPTIONS")
c.Writer.Header().Set("Content-Type", "application/json")
c.Writer.WriteHeader(200)
return
}
c.Next()
}
}

View File

@ -0,0 +1,236 @@
package session
import (
"fmt"
"net/http"
"github.com/drone/drone/model"
"github.com/drone/drone/router/middleware/context"
log "github.com/Sirupsen/logrus"
"github.com/gin-gonic/gin"
"github.com/hashicorp/golang-lru"
)
var cache *lru.Cache
func init() {
var err error
cache, err = lru.New(1028)
if err != nil {
panic(err)
}
}
func Repo(c *gin.Context) *model.Repo {
v, ok := c.Get("repo")
if !ok {
return nil
}
u, ok := v.(*model.Repo)
if !ok {
return nil
}
return u
}
func SetRepo() gin.HandlerFunc {
return func(c *gin.Context) {
var (
owner = c.Param("owner")
name = c.Param("name")
)
db := context.Database(c)
user := User(c)
repo, err := model.GetRepoName(db, owner, name)
if err == nil {
c.Set("repo", repo)
c.Next()
return
}
// if the user is not nil, check the remote system
// to see if the repository actually exists. If yes,
// we can prompt the user to add.
if user != nil {
remote := context.Remote(c)
repo, _ = remote.Repo(user, owner, name)
}
data := gin.H{
"User": user,
"Repo": repo,
}
// if we found a repository, we should display a page
// to the user allowing them to activate.
if repo != nil && len(repo.FullName) != 0 {
c.HTML(http.StatusNotFound, "repo_activate.html", data)
} else {
c.HTML(http.StatusNotFound, "404.html", data)
}
c.Abort()
}
}
func Perm(c *gin.Context) *model.Perm {
v, ok := c.Get("perm")
if !ok {
return nil
}
u, ok := v.(*model.Perm)
if !ok {
return nil
}
return u
}
func SetPerm() gin.HandlerFunc {
return func(c *gin.Context) {
user := User(c)
repo := Repo(c)
remote := context.Remote(c)
perm := &model.Perm{}
if user != nil {
// attempt to get the permissions from a local cache
// just to avoid excess API calls to GitHub
key := fmt.Sprintf("%d.%d", user.ID, repo.ID)
val, ok := cache.Get(key)
if ok {
c.Set("perm", val.(*model.Perm))
c.Next()
log.Debugf("%s using cached %+v permission to %s",
user.Login, val, repo.FullName)
return
}
}
switch {
// if the user is not authenticated, and the
// repository is private, the user has NO permission
// to view the repository.
case user == nil && repo.IsPrivate == true:
perm.Pull = false
perm.Push = false
perm.Admin = false
// if the user is not authenticated, but the repository
// is public, the user has pull-rights only.
case user == nil && repo.IsPrivate == false:
perm.Pull = true
perm.Push = false
perm.Admin = false
case user.Admin:
perm.Pull = true
perm.Push = true
perm.Admin = true
// otherwise if the user is authenticated we should
// check the remote system to get the users permissiosn.
default:
var err error
perm, err = remote.Perm(user, repo.Owner, repo.Name)
if err != nil {
perm.Pull = false
perm.Push = false
perm.Admin = false
// debug
log.Errorf("Error fetching permission for %s %s",
user.Login, repo.FullName)
}
// if we couldn't fetch permissions, but the repository
// is public, we should grant the user pull access.
if err != nil && repo.IsPrivate == false {
perm.Pull = true
}
}
if user != nil {
// cache the updated repository permissions to
// prevent un-necessary GitHub API requests.
key := fmt.Sprintf("%d.%d", user.ID, repo.ID)
cache.Add(key, perm)
// debug
log.Debugf("%s granted %+v permission to %s",
user.Login, perm, repo.FullName)
} else {
log.Debugf("Guest granted %+v to %s", perm, repo.FullName)
}
c.Set("perm", perm)
c.Next()
}
}
func MustPull(c *gin.Context) {
user := User(c)
repo := Repo(c)
perm := Perm(c)
if perm.Pull {
c.Next()
return
}
// if the user doesn't have pull permission to the
// repository we display a 404 error to avoid leaking
// repository information.
c.HTML(http.StatusNotFound, "404.html", gin.H{
"User": user,
"Repo": repo,
"Perm": perm,
})
c.Abort()
}
func MustPush(c *gin.Context) {
user := User(c)
repo := Repo(c)
perm := Perm(c)
// if the user has push access, immediately proceed
// the middleware execution chain.
if perm.Push {
c.Next()
return
}
data := gin.H{
"User": user,
"Repo": repo,
"Perm": perm,
}
// if the user has pull access we should tell them
// the operation is not authorized. Otherwise we should
// give a 404 to avoid leaking information.
if !perm.Pull {
c.HTML(http.StatusNotFound, "404.html", data)
} else {
c.HTML(http.StatusUnauthorized, "401.html", data)
}
// debugging
if user != nil {
log.Debugf("%s denied write access to %s",
user.Login, c.Request.URL.Path)
} else {
log.Debugf("Guest denied write access to %s %s",
c.Request.Method,
c.Request.URL.Path,
)
}
c.Abort()
}

View File

@ -0,0 +1,98 @@
package session
import (
"net/http"
"github.com/drone/drone/model"
"github.com/drone/drone/router/middleware/context"
"github.com/drone/drone/shared/token"
"github.com/gin-gonic/gin"
)
func User(c *gin.Context) *model.User {
v, ok := c.Get("user")
if !ok {
return nil
}
u, ok := v.(*model.User)
if !ok {
return nil
}
return u
}
func Token(c *gin.Context) *token.Token {
v, ok := c.Get("token")
if !ok {
return nil
}
u, ok := v.(*token.Token)
if !ok {
return nil
}
return u
}
func SetUser() gin.HandlerFunc {
return func(c *gin.Context) {
var user *model.User
t, err := token.ParseRequest(c.Request, func(t *token.Token) (string, error) {
var db = context.Database(c)
var err error
user, err = model.GetUserLogin(db, t.Text)
return user.Hash, err
})
if err == nil {
c.Set("user", user)
// if this is a session token (ie not the API token)
// this means the user is accessing with a web browser,
// so we should implement CSRF protection measures.
if t.Kind == token.SessToken {
err = token.CheckCsrf(c.Request, func(t *token.Token) (string, error) {
return user.Hash, nil
})
// if csrf token validation fails, exit immediately
// with a not authorized error.
if err != nil {
c.AbortWithStatus(http.StatusUnauthorized)
return
}
}
}
c.Next()
}
}
func MustAdmin() gin.HandlerFunc {
return func(c *gin.Context) {
user := User(c)
switch {
case user == nil:
c.AbortWithStatus(http.StatusUnauthorized)
// c.HTML(http.StatusUnauthorized, "401.html", gin.H{})
case user.Admin == false:
c.AbortWithStatus(http.StatusForbidden)
// c.HTML(http.StatusForbidden, "401.html", gin.H{})
default:
c.Next()
}
}
}
func MustUser() gin.HandlerFunc {
return func(c *gin.Context) {
user := User(c)
switch {
case user == nil:
c.AbortWithStatus(http.StatusUnauthorized)
// c.HTML(http.StatusUnauthorized, "401.html", gin.H{})
default:
c.Next()
}
}
}

179
router/router.go Normal file
View File

@ -0,0 +1,179 @@
package router
import (
"net/http"
"strings"
"github.com/gin-gonic/gin"
"github.com/drone/drone/controller"
"github.com/drone/drone/router/middleware/header"
"github.com/drone/drone/router/middleware/session"
"github.com/drone/drone/static"
"github.com/drone/drone/template"
)
func Load(middleware ...gin.HandlerFunc) http.Handler {
e := gin.Default()
e.SetHTMLTemplate(template.Load())
e.StaticFS("/static", static.FileSystem())
e.Use(header.SetHeaders())
e.Use(middleware...)
e.Use(session.SetUser())
e.GET("/", controller.ShowIndex)
e.GET("/login", controller.ShowLogin)
e.GET("/logout", controller.GetLogout)
settings := e.Group("/settings")
{
settings.Use(session.MustUser())
settings.GET("/profile", controller.ShowUser)
settings.GET("/people", session.MustAdmin(), controller.ShowUsers)
settings.GET("/nodes", session.MustAdmin(), controller.ShowNodes)
}
repo := e.Group("/repos/:owner/:name")
{
repo.Use(session.SetRepo())
repo.Use(session.SetPerm())
repo.Use(session.MustPull)
repo.GET("", controller.ShowRepo)
repo.GET("/builds/:number", controller.ShowBuild)
repo.GET("/builds/:number/:job", controller.ShowBuild)
repo_settings := repo.Group("/settings")
{
repo_settings.Use(session.MustPush)
repo_settings.GET("", controller.ShowRepoConf)
repo_settings.GET("/:action", controller.ShowRepoConf)
}
}
user := e.Group("/api/user")
{
user.Use(session.MustUser())
user.GET("", controller.GetSelf)
user.GET("/feed", controller.GetFeed)
user.GET("/repos", controller.GetRepos)
user.POST("/token", controller.PostToken)
user.GET("/repos/remote", controller.GetRemoteRepos)
}
users := e.Group("/api/users")
{
users.Use(session.MustAdmin())
users.GET("", controller.GetUsers)
users.POST("", controller.PostUser)
users.GET("/:login", controller.GetUser)
users.PATCH("/:login", controller.PatchUser)
users.DELETE("/:login", controller.DeleteUser)
}
nodes := e.Group("/api/nodes")
{
nodes.Use(session.MustAdmin())
nodes.GET("", controller.GetNodes)
nodes.POST("", controller.PostNode)
nodes.DELETE("/:node", controller.DeleteNode)
}
repos := e.Group("/api/repos/:owner/:name")
{
repos.POST("", controller.PostRepo)
repo := repos.Group("")
{
repo.Use(session.SetRepo())
repo.Use(session.SetPerm())
repo.Use(session.MustPull)
repo.GET("", controller.GetRepo)
repo.GET("/key", controller.GetRepoKey)
repo.GET("/builds", controller.GetBuilds)
repo.GET("/builds/:number", controller.GetBuild)
repo.GET("/logs/:number/:job", controller.GetBuildLogs)
// requires authenticated user
repo.POST("/starred", session.MustUser(), controller.PostStar)
repo.DELETE("/starred", session.MustUser(), controller.DeleteStar)
repo.POST("/encrypt", session.MustUser(), controller.PostSecure)
// requires push permissions
repo.PATCH("", session.MustPush, controller.PatchRepo)
repo.DELETE("", session.MustPush, controller.DeleteRepo)
repo.POST("/builds/:number", session.MustPush, controller.PostBuild)
// repo.DELETE("/builds/:number", MustPush(), controller.DeleteBuild)
}
}
badges := e.Group("/api/badges/:owner/:name")
{
badges.GET("/status.svg", controller.GetBadge)
badges.GET("/cc.xml", controller.GetCC)
}
hook := e.Group("/hook")
{
hook.POST("", controller.PostHook)
}
stream := e.Group("/api/stream")
{
stream.Use(session.SetRepo())
stream.Use(session.SetPerm())
stream.Use(session.MustPull)
stream.GET("/:owner/:name", controller.GetRepoEvents)
stream.GET("/:owner/:name/:build/:number", controller.GetStream)
}
auth := e.Group("/authorize")
{
auth.GET("", controller.GetLogin)
auth.POST("", controller.GetLogin)
auth.POST("/token", controller.GetLoginToken)
}
gitlab := e.Group("/api/gitlab/:owner/:name")
{
gitlab.Use(session.SetRepo())
gitlab.GET("/commits/:sha", controller.GetCommit)
gitlab.GET("/pulls/:number", controller.GetPullRequest)
redirects := gitlab.Group("/redirect")
{
redirects.GET("/commits/:sha", controller.RedirectSha)
redirects.GET("/pulls/:number", controller.RedirectPullRequest)
}
}
return normalize(e)
}
// normalize is a helper function to work around the following
// issue with gin. https://github.com/gin-gonic/gin/issues/388
func normalize(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
parts := strings.Split(r.URL.Path, "/")[1:]
switch parts[0] {
case "settings", "api", "login", "logout", "", "authorize", "hook", "static":
// no-op
default:
if len(parts) > 2 && parts[2] != "settings" {
parts = append(parts[:2], append([]string{"builds"}, parts[2:]...)...)
}
// prefix the URL with /repo so that it
// can be effectively routed.
parts = append([]string{"", "repos"}, parts...)
// reconstruct the path
r.URL.Path = strings.Join(parts, "/")
}
h.ServeHTTP(w, r)
})
}

118
shared/crypto/crypto.go Normal file
View File

@ -0,0 +1,118 @@
package crypto
import (
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"io"
"code.google.com/p/go.crypto/ssh"
"github.com/square/go-jose"
)
const (
RSA_BITS = 2048 // Default number of bits in an RSA key
RSA_BITS_MIN = 768 // Minimum number of bits in an RSA key
)
// standard characters allowed in token string.
var chars = []byte("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789")
// default token length
var length = 32
// Rand generates a 32-bit random string.
func Rand() string {
b := make([]byte, length)
r := make([]byte, length+(length/4)) // storage for random bytes.
clen := byte(len(chars))
maxrb := byte(256 - (256 % len(chars)))
i := 0
for {
io.ReadFull(rand.Reader, r)
for _, c := range r {
if c >= maxrb {
// Skip this number to avoid modulo bias.
continue
}
b[i] = chars[c%clen]
i++
if i == length {
return string(b)
}
}
}
}
// helper function to generate an RSA Private Key.
func GeneratePrivateKey() (*rsa.PrivateKey, error) {
return rsa.GenerateKey(rand.Reader, RSA_BITS)
}
// helper function that marshalls an RSA Public Key to an SSH
// .authorized_keys format
func MarshalPublicKey(public *rsa.PublicKey) []byte {
private, err := ssh.NewPublicKey(public)
if err != nil {
return []byte{}
}
return ssh.MarshalAuthorizedKey(private)
}
// helper function that marshalls an RSA Private Key to
// a PEM encoded file.
func MarshalPrivateKey(private *rsa.PrivateKey) []byte {
marshaled := x509.MarshalPKCS1PrivateKey(private)
encoded := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Headers: nil, Bytes: marshaled})
return encoded
}
// UnmarshalPrivateKey is a helper function that unmarshals a PEM
// bytes to an RSA Private Key
func UnmarshalPrivateKey(private []byte) *rsa.PrivateKey {
decoded, _ := pem.Decode(private)
parsed, err := x509.ParsePKCS1PrivateKey(decoded.Bytes)
if err != nil {
return nil
}
return parsed
}
// Encrypt encrypts a secret string.
func Encrypt(in, privKey string) (string, error) {
rsaPrivKey, err := decodePrivateKey(privKey)
if err != nil {
return "", err
}
return encrypt(in, &rsaPrivKey.PublicKey)
}
// decodePrivateKey is a helper function that unmarshals a PEM
// bytes to an RSA Private Key
func decodePrivateKey(privateKey string) (*rsa.PrivateKey, error) {
derBlock, _ := pem.Decode([]byte(privateKey))
return x509.ParsePKCS1PrivateKey(derBlock.Bytes)
}
// encrypt encrypts a plaintext variable using JOSE with
// RSA_OAEP and A128GCM algorithms.
func encrypt(text string, pubKey *rsa.PublicKey) (string, error) {
var encrypted string
var plaintext = []byte(text)
// Creates a new encrypter using defaults
encrypter, err := jose.NewEncrypter(jose.RSA_OAEP, jose.A128GCM, pubKey)
if err != nil {
return encrypted, err
}
// Encrypts the plaintext value and serializes
// as a JOSE string.
object, err := encrypter.Encrypt(plaintext)
if err != nil {
return encrypted, err
}
return object.CompactSerialize()
}

View File

@ -1,13 +1,25 @@
package secure
package crypto
import (
"testing"
"github.com/drone/drone/Godeps/_workspace/src/github.com/franela/goblin"
"github.com/drone/drone/Godeps/_workspace/src/github.com/square/go-jose"
"github.com/franela/goblin"
"github.com/square/go-jose"
)
func Test_Secure(t *testing.T) {
func TestKeys(t *testing.T) {
g := goblin.Goblin(t)
g.Describe("Generate Key", func() {
g.It("Generates a private key", func() {
_, err := GeneratePrivateKey()
g.Assert(err == nil).IsTrue()
})
})
}
func Test_Encrypt(t *testing.T) {
g := goblin.Goblin(t)
g.Describe("Secure", func() {

View File

@ -1,72 +0,0 @@
package sshutil
import (
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"hash"
"github.com/drone/drone/Godeps/_workspace/src/code.google.com/p/go.crypto/ssh"
)
const (
RSA_BITS = 2048 // Default number of bits in an RSA key
RSA_BITS_MIN = 768 // Minimum number of bits in an RSA key
)
// helper function to generate an RSA Private Key.
func GeneratePrivateKey() (*rsa.PrivateKey, error) {
return rsa.GenerateKey(rand.Reader, RSA_BITS)
}
// helper function that marshalls an RSA Public Key to an SSH
// .authorized_keys format
func MarshalPublicKey(pubkey *rsa.PublicKey) []byte {
pk, err := ssh.NewPublicKey(pubkey)
if err != nil {
return []byte{}
}
return ssh.MarshalAuthorizedKey(pk)
}
// helper function that marshalls an RSA Private Key to
// a PEM encoded file.
func MarshalPrivateKey(privkey *rsa.PrivateKey) []byte {
privateKeyMarshaled := x509.MarshalPKCS1PrivateKey(privkey)
privateKeyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Headers: nil, Bytes: privateKeyMarshaled})
return privateKeyPEM
}
// UnMarshalPrivateKey is a helper function that unmarshals a PEM
// bytes to an RSA Private Key
func UnMarshalPrivateKey(privateKeyPEM []byte) *rsa.PrivateKey {
derBlock, _ := pem.Decode(privateKeyPEM)
privateKey, err := x509.ParsePKCS1PrivateKey(derBlock.Bytes)
if err != nil {
return nil
}
return privateKey
}
// Encrypt is helper function to encrypt a plain-text string using
// an RSA public key.
func Encrypt(hash hash.Hash, pubkey *rsa.PublicKey, msg string) (string, error) {
src, err := rsa.EncryptOAEP(hash, rand.Reader, pubkey, []byte(msg), nil)
return base64.RawURLEncoding.EncodeToString(src), err
}
// Decrypt is helper function to encrypt a plain-text string using
// an RSA public key.
func Decrypt(hash hash.Hash, privkey *rsa.PrivateKey, secret string) (string, error) {
decoded, err := base64.RawURLEncoding.DecodeString(secret)
if err != nil {
return "", err
}
out, err := rsa.DecryptOAEP(hash, rand.Reader, privkey, decoded, nil)
return string(out), err
}

View File

@ -1,40 +0,0 @@
package sshutil
import (
"crypto/sha256"
"testing"
"github.com/drone/drone/Godeps/_workspace/src/github.com/franela/goblin"
)
func TestSSHUtil(t *testing.T) {
g := goblin.Goblin(t)
g.Describe("sshutil", func() {
var encrypted, testMsg string
privkey, err := GeneratePrivateKey()
g.Assert(err == nil).IsTrue()
pubkey := privkey.PublicKey
sha256 := sha256.New()
testMsg = "foo=bar"
g.Before(func() {
encrypted, err = Encrypt(sha256, &pubkey, testMsg)
g.Assert(err == nil).IsTrue()
})
g.It("Can decrypt encrypted msg", func() {
decrypted, err := Decrypt(sha256, privkey, encrypted)
g.Assert(err == nil).IsTrue()
g.Assert(decrypted == testMsg).IsTrue()
})
g.It("Unmarshals private key from PEM block", func() {
privateKeyPEM := MarshalPrivateKey(privkey)
privateKey := UnMarshalPrivateKey(privateKeyPEM)
g.Assert(privateKey.PublicKey.E == pubkey.E).IsTrue()
})
})
}

View File

@ -0,0 +1,51 @@
package database
//go:generate go-bindata -pkg database -o database_gen.go sqlite3/ mysql/ postgres/
import (
"database/sql"
"github.com/drone/drone/shared/envconfig"
log "github.com/Sirupsen/logrus"
_ "github.com/go-sql-driver/mysql"
_ "github.com/lib/pq"
_ "github.com/mattn/go-sqlite3"
"github.com/rubenv/sql-migrate"
)
func Load(env envconfig.Env) *sql.DB {
var (
driver = env.String("DATABASE_DRIVER", "sqlite3")
config = env.String("DATABASE_CONFIG", "drone.sqlite")
)
log.Infof("using database driver %s", driver)
log.Infof("using database config %s", config)
return Open(driver, config)
}
// Open opens a database connection, runs the database migrations, and returns
// the database connection. Any errors connecting to the database or executing
// migrations will cause the application to exit.
func Open(driver, config string) *sql.DB {
var db, err = sql.Open(driver, config)
if err != nil {
log.Errorln(err)
log.Fatalln("database connection failed")
}
var migrations = &migrate.AssetMigrationSource{
Asset: Asset,
AssetDir: AssetDir,
Dir: driver,
}
_, err = migrate.Exec(db, driver, migrations, migrate.Up)
if err != nil {
log.Errorln(err)
log.Fatalln("migration failed")
}
return db
}

View File

@ -0,0 +1,132 @@
-- +migrate Up
CREATE TABLE users (
user_id INTEGER PRIMARY KEY AUTO_INCREMENT
,user_login VARCHAR(500)
,user_token VARCHAR(500)
,user_secret VARCHAR(500)
,user_email VARCHAR(500)
,user_avatar VARCHAR(500)
,user_active BOOLEAN
,user_admin BOOLEAN
,user_hash VARCHAR(500)
,UNIQUE(user_login)
);
CREATE TABLE repos (
repo_id INTEGER PRIMARY KEY AUTO_INCREMENT
,repo_user_id INTEGER
,repo_owner VARCHAR(500)
,repo_name VARCHAR(500)
,repo_full_name VARCHAR(1000)
,repo_avatar VARCHAR(500)
,repo_link VARCHAR(1000)
,repo_clone VARCHAR(1000)
,repo_branch VARCHAR(500)
,repo_timeout INTEGER
,repo_private BOOLEAN
,repo_trusted BOOLEAN
,repo_allow_pr BOOLEAN
,repo_allow_push BOOLEAN
,repo_allow_deploys BOOLEAN
,repo_allow_tags BOOLEAN
,repo_hash VARCHAR(500)
,UNIQUE(repo_owner, repo_name)
);
CREATE TABLE stars (
star_id INTEGER PRIMARY KEY AUTO_INCREMENT
,star_repo_id INTEGER
,star_user_id INTEGER
,UNIQUE(star_repo_id, star_user_id)
);
CREATE INDEX ix_star_user ON builds (star_user_id);
CREATE TABLE keys (
key_id INTEGER PRIMARY KEY AUTO_INCREMENT
,key_repo_id INTEGER
,key_public MEDIUMBLOB
,key_private MEDIUMBLOB
,UNIQUE(key_repo_id)
);
CREATE TABLE builds (
build_id INTEGER PRIMARY KEY AUTO_INCREMENT
,build_repo_id INTEGER
,build_number INTEGER
,build_event VARCHAR(500)
,build_status VARCHAR(500)
,build_created INTEGER
,build_started INTEGER
,build_finished INTEGER
,build_commit VARCHAR(500)
,build_branch VARCHAR(500)
,build_ref VARCHAR(500)
,build_refspec VARCHAR(1000)
,build_remote VARCHAR(500)
,build_title VARCHAR(1000)
,build_message VARCHAR(2000)
,build_timestamp INTEGER
,build_author VARCHAR(500)
,build_avatar VARCHAR(1000)
,build_email VARCHAR(500)
,build_link VARCHAR(1000)
,UNIQUE(build_number, build_repo_id)
);
CREATE INDEX ix_build_repo ON builds (build_repo_id);
CREATE TABLE jobs (
job_id INTEGER PRIMARY KEY AUTO_INCREMENT
,job_node_id INTEGER
,job_build_id INTEGER
,job_number INTEGER
,job_status VARCHAR(500)
,job_exit_code INTEGER
,job_started INTEGER
,job_finished INTEGER
,job_environment VARCHAR(2000)
,UNIQUE(job_build_id, job_number)
);
CREATE INDEX ix_job_build ON jobs (job_build_id);
CREATE INDEX ix_job_node ON jobs (job_node_id);
CREATE TABLE IF NOT EXISTS logs (
log_id INTEGER PRIMARY KEY AUTO_INCREMENT
,log_job_id INTEGER
,log_data MEDIUMBLOB
,UNIQUE(log_job_id)
);
CREATE TABLE IF NOT EXISTS nodes (
node_id INTEGER PRIMARY KEY AUTOINCREMENT
,node_addr VARCHAR(1024)
,node_arch VARCHAR(50)
,node_cert MEDIUMBLOB
,node_key MEDIUMBLOB
,node_ca MEDIUMBLOB
);
INSERT INTO nodes VALUES(null, 'unix:///var/run/docker.sock', 'linux_amd64', '', '', '');
INSERT INTO nodes VALUES(null, 'unix:///var/run/docker.sock', 'linux_amd64', '', '', '');
-- +migrate Down
DROP TABLE nodes;
DROP TABLE logs;
DROP TABLE jobs;
DROP TABLE builds;
DROP TABLE keys;
DROP TABLE stars;
DROP TABLE repos;
DROP TABLE users;

View File

@ -0,0 +1,132 @@
-- +migrate Up
CREATE TABLE users (
user_id SERIAL PRIMARY KEY
,user_login VARCHAR(500)
,user_token VARCHAR(500)
,user_secret VARCHAR(500)
,user_email VARCHAR(500)
,user_avatar VARCHAR(500)
,user_active BOOLEAN
,user_admin BOOLEAN
,user_hash VARCHAR(500)
,UNIQUE(user_login)
);
CREATE TABLE repos (
repo_id SERIAL PRIMARY KEY
,repo_user_id INTEGER
,repo_owner VARCHAR(500)
,repo_name VARCHAR(500)
,repo_full_name VARCHAR(1000)
,repo_avatar VARCHAR(500)
,repo_link VARCHAR(1000)
,repo_clone VARCHAR(1000)
,repo_branch VARCHAR(500)
,repo_timeout INTEGER
,repo_private BOOLEAN
,repo_trusted BOOLEAN
,repo_allow_pr BOOLEAN
,repo_allow_push BOOLEAN
,repo_allow_deploys BOOLEAN
,repo_allow_tags BOOLEAN
,repo_hash VARCHAR(500)
,UNIQUE(repo_owner, repo_name)
);
CREATE TABLE stars (
star_id SERIAL PRIMARY KEY
,star_repo_id INTEGER
,star_user_id INTEGER
,UNIQUE(star_repo_id, star_user_id)
);
CREATE INDEX ix_star_user ON builds (star_user_id);
CREATE TABLE keys (
key_id SERIAL PRIMARY KEY
,key_repo_id INTEGER
,key_public BYTEA
,key_private BYTEA
,UNIQUE(key_repo_id)
);
CREATE TABLE builds (
build_id SERIAL PRIMARY KEY
,build_repo_id INTEGER
,build_number INTEGER
,build_event VARCHAR(500)
,build_status VARCHAR(500)
,build_created INTEGER
,build_started INTEGER
,build_finished INTEGER
,build_commit VARCHAR(500)
,build_branch VARCHAR(500)
,build_ref VARCHAR(500)
,build_refspec VARCHAR(1000)
,build_remote VARCHAR(500)
,build_title VARCHAR(1000)
,build_message VARCHAR(2000)
,build_timestamp INTEGER
,build_author VARCHAR(500)
,build_avatar VARCHAR(1000)
,build_email VARCHAR(500)
,build_link VARCHAR(1000)
,UNIQUE(build_number, build_repo_id)
);
CREATE INDEX ix_build_repo ON builds (build_repo_id);
CREATE TABLE jobs (
job_id SERIAL PRIMARY KEY
,job_node_id INTEGER
,job_build_id INTEGER
,job_number INTEGER
,job_status VARCHAR(500)
,job_exit_code INTEGER
,job_started INTEGER
,job_finished INTEGER
,job_environment VARCHAR(2000)
,UNIQUE(job_build_id, job_number)
);
CREATE INDEX ix_job_build ON jobs (job_build_id);
CREATE INDEX ix_job_node ON jobs (job_node_id);
CREATE TABLE IF NOT EXISTS logs (
log_id SERIAL PRIMARY KEY
,log_job_id INTEGER
,log_data BYTEA
,UNIQUE(log_job_id)
);
CREATE TABLE IF NOT EXISTS nodes (
node_id INTEGER PRIMARY KEY AUTOINCREMENT
,node_addr VARCHAR(1024)
,node_arch VARCHAR(50)
,node_cert BYTEA
,node_key BYTEA
,node_ca BYTEA
);
INSERT INTO nodes VALUES(null, 'unix:///var/run/docker.sock', 'linux_amd64', '', '', '');
INSERT INTO nodes VALUES(null, 'unix:///var/run/docker.sock', 'linux_amd64', '', '', '');
-- +migrate Down
DROP TABLE nodes;
DROP TABLE logs;
DROP TABLE jobs;
DROP TABLE builds;
DROP TABLE keys;
DROP TABLE stars;
DROP TABLE repos;
DROP TABLE users;

32
shared/database/rebind.go Normal file
View File

@ -0,0 +1,32 @@
package database
import (
"strconv"
"github.com/russross/meddler"
)
// Rebind is a helper function that changes the sql
// bind type from ? to $ for postgres queries.
func Rebind(query string) string {
if meddler.Default != meddler.PostgreSQL {
return query
}
qb := []byte(query)
// Add space enough for 5 params before we have to allocate
rqb := make([]byte, 0, len(qb)+5)
j := 1
for _, b := range qb {
if b == '?' {
rqb = append(rqb, '$')
for _, b := range strconv.Itoa(j) {
rqb = append(rqb, byte(b))
}
j++
} else {
rqb = append(rqb, b)
}
}
return string(rqb)
}

View File

@ -0,0 +1,131 @@
-- +migrate Up
CREATE TABLE users (
user_id INTEGER PRIMARY KEY AUTOINCREMENT
,user_login TEXT
,user_token TEXT
,user_secret TEXT
,user_email TEXT
,user_avatar TEXT
,user_active BOOLEAN
,user_admin BOOLEAN
,user_hash TEXT
,UNIQUE(user_login)
);
CREATE TABLE repos (
repo_id INTEGER PRIMARY KEY AUTOINCREMENT
,repo_user_id INTEGER
,repo_owner TEXT
,repo_name TEXT
,repo_full_name TEXT
,repo_avatar TEXT
,repo_link TEXT
,repo_clone TEXT
,repo_branch TEXT
,repo_timeout INTEGER
,repo_private BOOLEAN
,repo_trusted BOOLEAN
,repo_allow_pr BOOLEAN
,repo_allow_push BOOLEAN
,repo_allow_deploys BOOLEAN
,repo_allow_tags BOOLEAN
,repo_hash TEXT
,UNIQUE(repo_owner, repo_name)
);
CREATE TABLE stars (
star_id INTEGER PRIMARY KEY AUTOINCREMENT
,star_repo_id INTEGER
,star_user_id INTEGER
,UNIQUE(star_repo_id, star_user_id)
);
CREATE INDEX ix_star_user ON stars (star_user_id);
CREATE TABLE keys (
key_id INTEGER PRIMARY KEY AUTOINCREMENT
,key_repo_id INTEGER
,key_public BLOB
,key_private BLOB
,UNIQUE(key_repo_id)
);
CREATE TABLE builds (
build_id INTEGER PRIMARY KEY AUTOINCREMENT
,build_repo_id INTEGER
,build_number INTEGER
,build_event TEXT
,build_status TEXT
,build_created INTEGER
,build_started INTEGER
,build_finished INTEGER
,build_commit TEXT
,build_branch TEXT
,build_ref TEXT
,build_refspec TEXT
,build_remote TEXT
,build_title TEXT
,build_message TEXT
,build_timestamp INTEGER
,build_author TEXT
,build_avatar TEXT
,build_email TEXT
,build_link TEXT
,UNIQUE(build_number, build_repo_id)
);
CREATE INDEX ix_build_repo ON builds (build_repo_id);
CREATE TABLE jobs (
job_id INTEGER PRIMARY KEY AUTOINCREMENT
,job_node_id INTEGER
,job_build_id INTEGER
,job_number INTEGER
,job_status TEXT
,job_exit_code INTEGER
,job_started INTEGER
,job_finished INTEGER
,job_environment TEXT
,UNIQUE(job_build_id, job_number)
);
CREATE INDEX ix_job_build ON jobs (job_build_id);
CREATE INDEX ix_job_node ON jobs (job_node_id);
CREATE TABLE IF NOT EXISTS logs (
log_id INTEGER PRIMARY KEY AUTOINCREMENT
,log_job_id INTEGER
,log_data BLOB
,UNIQUE(log_job_id)
);
CREATE TABLE IF NOT EXISTS nodes (
node_id INTEGER PRIMARY KEY AUTOINCREMENT
,node_addr TEXT
,node_arch TEXT
,node_cert BLOB
,node_key BLOB
,node_ca BLOB
);
INSERT INTO nodes VALUES(null, 'unix:///var/run/docker.sock', 'linux_amd64', '', '', '');
INSERT INTO nodes VALUES(null, 'unix:///var/run/docker.sock', 'linux_amd64', '', '', '');
-- +migrate Down
DROP TABLE nodes;
DROP TABLE logs;
DROP TABLE jobs;
DROP TABLE builds;
DROP TABLE keys;
DROP TABLE stars;
DROP TABLE repos;
DROP TABLE users;

109
shared/docker/docker.go Normal file
View File

@ -0,0 +1,109 @@
package docker
import (
"io"
"io/ioutil"
"github.com/samalba/dockerclient"
)
var (
LogOpts = &dockerclient.LogOptions{
Stdout: true,
Stderr: true,
}
LogOptsTail = &dockerclient.LogOptions{
Follow: true,
Stdout: true,
Stderr: true,
}
)
// Run creates the docker container, pulling images if necessary, starts
// the container and blocks until the container exits, returning the exit
// information.
func Run(client dockerclient.Client, conf *dockerclient.ContainerConfig, name string) (*dockerclient.ContainerInfo, error) {
info, err := RunDaemon(client, conf, name)
if err != nil {
return nil, err
}
return Wait(client, info.Id)
}
// RunDaemon creates the docker container, pulling images if necessary, starts
// the container and returns the container information. It does not wait for
// the container to exit.
func RunDaemon(client dockerclient.Client, conf *dockerclient.ContainerConfig, name string) (*dockerclient.ContainerInfo, error) {
// attempts to create the contianer
id, err := client.CreateContainer(conf, name)
if err != nil {
// and pull the image and re-create if that fails
err = client.PullImage(conf.Image, nil)
if err != nil {
return nil, err
}
id, err = client.CreateContainer(conf, name)
if err != nil {
client.RemoveContainer(id, true, true)
return nil, err
}
}
// fetches the container information
info, err := client.InspectContainer(id)
if err != nil {
client.RemoveContainer(id, true, true)
return nil, err
}
// starts the container
err = client.StartContainer(id, &conf.HostConfig)
if err != nil {
client.RemoveContainer(id, true, true)
return nil, err
}
return info, err
}
// Wait blocks until the named container exits, returning the exit information.
func Wait(client dockerclient.Client, name string) (*dockerclient.ContainerInfo, error) {
defer func() {
client.StopContainer(name, 5)
client.KillContainer(name, "9")
}()
errc := make(chan error, 1)
infoc := make(chan *dockerclient.ContainerInfo, 1)
go func() {
// blocks and waits for the container to finish
// by streaming the logs (to /dev/null). Ideally
// we could use the `wait` function instead
rc, err := client.ContainerLogs(name, LogOptsTail)
if err != nil {
errc <- err
return
}
io.Copy(ioutil.Discard, rc)
rc.Close()
info, err := client.InspectContainer(name)
if err != nil {
errc <- err
return
}
infoc <- info
}()
select {
case info := <-infoc:
return info, nil
case err := <-errc:
return nil, err
}
}

Some files were not shown because too many files have changed in this diff Show More