initial public commit

This commit is contained in:
Brad Rydzewski 2014-02-07 03:10:01 -07:00
commit d5e5797934
183 changed files with 15701 additions and 0 deletions

24
.drone.yml Normal file
View file

@ -0,0 +1,24 @@
image: mischief/docker-golang
env:
- GOROOT=/usr/local/go
- GOPATH=/var/cache/drone
- PATH=$GOPATH/bin:$GOPATH/bin:$PATH
script:
- apt-get -y install libsqlite3-dev sqlite3 mercurial bzr 1> /dev/null 2> /dev/null
- make deps
- make
- make test
- make dpkg
notify:
email:
recipients:
- brad@drone.io
publish:
s3:
acl: public-read
region: us-east-1
bucket: downloads.drone.io
access_key: $AWS_KEY
secret_key: $AWS_SECRET
source: /tmp/drone.deb
target: latest/

14
.gitignore vendored Normal file
View file

@ -0,0 +1,14 @@
drone.sublime-project
drone.sublime-workspace
*~
~*
*.sqlite
*.deb
*.rice-box.go
bin/drone
bin/droned
cmd/drone/drone
cmd/droned/droned
deb/drone/usr/local/bin/drone
deb/drone/usr/local/bin/droned

5
AUTHORS Normal file
View file

@ -0,0 +1,5 @@
# This file lists all individuals having contributed content to the repository.
# If you're submitting a patch, please add your name here in alphabetical order as part of the patch.
Brad Rydzewski <brad@drone.io>
Thomas Burke <burke@drone.io>

202
LICENSE Normal file
View file

@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

78
Makefile Normal file
View file

@ -0,0 +1,78 @@
all: embed build
deps:
go get code.google.com/p/go.crypto/bcrypt
go get code.google.com/p/go.crypto/ssh
go get code.google.com/p/go.net/websocket
go get code.google.com/p/go.text/unicode/norm
go get launchpad.net/goyaml
go get github.com/andybons/hipchat
go get github.com/bmizerany/pat
go get github.com/dchest/authcookie
go get github.com/dchest/passwordreset
go get github.com/dchest/uniuri
go get github.com/dotcloud/docker/archive
go get github.com/dotcloud/docker/pkg/term
go get github.com/dotcloud/docker/utils
go get github.com/drone/go-github/github
go get github.com/drone/go-bitbucket/bitbucket
go get github.com/GeertJohan/go.rice
go get github.com/GeertJohan/go.rice/rice
go get github.com/mattn/go-sqlite3
go get github.com/russross/meddler
embed:
cd cmd/droned && rice embed
cd pkg/template && rice embed
build:
cd cmd/drone && go build -o ../../bin/drone
cd cmd/droned && go build -o ../../bin/droned
test:
go test -v github.com/drone/drone/pkg/build
go test -v github.com/drone/drone/pkg/build/buildfile
go test -v github.com/drone/drone/pkg/build/docker
go test -v github.com/drone/drone/pkg/build/dockerfile
go test -v github.com/drone/drone/pkg/build/proxy
go test -v github.com/drone/drone/pkg/build/repo
go test -v github.com/drone/drone/pkg/build/script
go test -v github.com/drone/drone/pkg/channel
go test -v github.com/drone/drone/pkg/database
go test -v github.com/drone/drone/pkg/database/encrypt
go test -v github.com/drone/drone/pkg/database/testing
go test -v github.com/drone/drone/pkg/mail
go test -v github.com/drone/drone/pkg/model
go test -v github.com/drone/drone/pkg/queue
install:
cp deb/drone/etc/init/drone.conf /etc/init/drone.conf
cd bin && install -t /usr/local/bin drone
cd bin && install -t /usr/local/bin droned
mkdir -p /var/lib/drone
clean:
cd cmd/droned && rice clean
cd pkg/template && rice clean
rm -rf cmd/drone/drone
rm -rf cmd/droned/droned
rm -rf cmd/droned/drone.sqlite
rm -rf bin/drone
rm -rf bin/droned
rm -rf deb/drone.deb
rm -rf usr/local/bin/drone
rm -rf usr/local/bin/droned
rm -rf drone.sqlite
# creates a debian package for drone
# to install `sudo dpkg -i drone.deb`
dpkg:
mkdir -p deb/drone/usr/local/bin
mkdir -p deb/drone/var/lib/drone
cp bin/drone deb/drone/usr/local/bin
cp bin/droned deb/drone/usr/local/bin
dpkg-deb --build deb/drone
run:
bin/droned --port=":8080" --datasource="/tmp/drone.sqlite"

128
README.md Normal file
View file

@ -0,0 +1,128 @@
Drone is a Continuous Integration platform built on Docker
### System
Drone is tested on the following versions of Ubuntu:
* Ubuntu Precise 12.04 (LTS) (64-bit)
* Ubuntu Raring 13.04 (64 bit)
Drone's only external dependency is the latest version of Docker (0.8)
### Setup
Drone is packaged and distributed as a debian file. You can download an install
using the following commands:
```sh
$ wget http://downloads.drone.io/latest/drone.deb
$ dpkg -i drone.deb
$ sudo start drone
```
Once Drone is running (by default on :80) navigate to **http://localhost:80/install**
and follow the steps in the setup wizard.
### Builds
Drone use a **.drone.yml** configuration file in the root of your
repository to run your build:
```
image: mischief/docker-golang
env:
- GOPATH=/var/cache/drone
script:
- go build
- go test -v
service:
- redis
notify:
email:
recipients:
- brad@drone.io
- burke@drone.io
```
### Environment
Drone clones your repository into a Docker container
at the following location:
```
/var/cache/drone/src/github.com/$owner/$name
```
Please take this into consideration when setting up your build image. For example,
you may need set the $GOAPTH or other environment variables appropriately.
### Databases
Drone can launch database containers for your build:
```
service:
- cassandra
- couchdb
- elasticsearch
- neo4j
- mongodb
- mysql
- postgres
- rabbitmq
- redis
- riak
- zookeeper
```
**NOTE:** database and service containers are exposed over TCP connections and
have their own local IP address. If the **socat** utility is installed inside your
Docker image, Drone will automatically proxy localhost connections to the correct
IP address.
### Deployments
Drone can trigger a deployment at the successful completion of your build:
```
deploy:
heroku:
app: safe-island-6261
publish:
s3:
acl: public-read
region: us-east-1
bucket: downloads.drone.io
access_key: C24526974F365C3B
secret_key: 2263c9751ed084a68df28fd2f658b127
source: /tmp/drone.deb
target: latest/
```
### Notifications
Drone can trigger email, hipchat and web hook notification at the completion
of your build:
```
notify:
email:
recipients:
- brad@drone.io
- burke@drone.io
urls:
- http://my-deploy-hook.com
hipchat:
room: support
token: 3028700e5466d375
```
### Docs
Coming Soon to [drone.readthedocs.org](http://drone.readthedocs.org/)

1
bin/README.md Normal file
View file

@ -0,0 +1 @@
This is where Drone binaries go after running `make` in the Drone root directory.

288
cmd/drone/drone.go Normal file
View file

@ -0,0 +1,288 @@
package main
import (
"bytes"
"flag"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"sync"
"time"
"github.com/drone/drone/pkg/build"
"github.com/drone/drone/pkg/build/log"
"github.com/drone/drone/pkg/build/repo"
"github.com/drone/drone/pkg/build/script"
"launchpad.net/goyaml"
)
var (
// identity file (id_rsa) that will be injected
// into the container if specified
identity = flag.String("identity", "", "")
// runs Drone in parallel mode if True
parallel = flag.Bool("parallel", false, "")
// build will timeout after N milliseconds.
// this will default to 500 minutes (6 hours)
timeout = flag.Duration("timeout", 300*time.Minute, "")
// runs Drone with verbose output if True
verbose = flag.Bool("v", false, "")
// displays the help / usage if True
help = flag.Bool("h", false, "")
)
func init() {
// default logging
log.SetPrefix("\033[2m[DRONE] ")
log.SetSuffix("\033[0m\n")
log.SetOutput(os.Stdout)
log.SetPriority(log.LOG_NOTICE)
}
func main() {
// Parse the input parameters
flag.Usage = usage
flag.Parse()
if *help {
flag.Usage()
os.Exit(0)
}
if *verbose {
log.SetPriority(log.LOG_DEBUG)
}
// Must speicify a command
args := flag.Args()
if len(args) == 0 {
flag.Usage()
os.Exit(0)
}
switch {
// run drone build assuming the current
// working directory contains the drone.yml
case args[0] == "build" && len(args) == 1:
path, _ := os.Getwd()
path = filepath.Join(path, ".drone.yml")
run(path)
// run drone build where the path to the
// source directory is provided
case args[0] == "build" && len(args) == 2:
path := args[1]
path = filepath.Clean(path)
path, _ = filepath.Abs(path)
path = filepath.Join(path, ".drone.yml")
run(path)
// run drone vet where the path to the
// source directory is provided
case args[0] == "vet" && len(args) == 2:
path := args[1]
path = filepath.Clean(path)
path, _ = filepath.Abs(path)
path = filepath.Join(path, ".drone.yml")
vet(path)
// run drone vet assuming the current
// working directory contains the drone.yml
case args[0] == "vet" && len(args) == 1:
path, _ := os.Getwd()
path = filepath.Join(path, ".drone.yml")
vet(path)
// print the help message
case args[0] == "help" && len(args) == 1:
flag.Usage()
}
os.Exit(0)
}
func vet(path string) {
// parse the Drone yml file
script, err := script.ParseBuildFile(path)
if err != nil {
log.Err(err.Error())
os.Exit(1)
return
}
// print the Drone yml as parsed
out, _ := goyaml.Marshal(script)
log.Noticef("parsed yaml:\n%s", string(out))
}
func run(path string) {
// parse the Drone yml file
s, err := script.ParseBuildFile(path)
if err != nil {
log.Err(err.Error())
os.Exit(1)
return
}
// get the repository root directory
dir := filepath.Dir(path)
code := repo.Repo{Path: dir}
// does the local repository match the
// $GOPATH/src/{package} pattern? This is
// important so we know the target location
// where the code should be copied inside
// the container.
if gopath, ok := getRepoPath(dir); ok {
code.Dir = gopath
} else if gopath, ok := getGoPath(dir); ok {
// in this case we found a GOPATH and
// reverse engineered the package path
code.Dir = gopath
} else {
// otherwise just use directory name
code.Dir = filepath.Base(dir)
}
// this is where the code gets uploaded to the container
// TODO move this code to the build package
code.Dir = filepath.Join("/var/cache/drone/src", filepath.Clean(code.Dir))
// track all build results
var builders []*build.Builder
// ssh key to import into container
var key []byte
if len(*identity) != 0 {
key, err = ioutil.ReadFile(*identity)
if err != nil {
fmt.Printf("[Error] Could not find or read identity file %s\n", identity)
os.Exit(1)
return
}
}
builds := []*script.Build{s}
// loop through and create builders
for _, b := range builds { //script.Builds {
builder := build.Builder{}
builder.Build = b
builder.Repo = &code
builder.Key = key
builder.Stdout = os.Stdout
builder.Timeout = *timeout
if *parallel == true {
var buf bytes.Buffer
builder.Stdout = &buf
}
builders = append(builders, &builder)
}
switch *parallel {
case false:
runSequential(builders)
case true:
runParallel(builders)
}
// if in parallel mode, print out the buffer
// if we had a failure
for _, builder := range builders {
if builder.BuildState.ExitCode == 0 {
continue
}
if buf, ok := builder.Stdout.(*bytes.Buffer); ok {
log.Noticef("printing stdout for failed build %s", builder.Build.Name)
println(buf.String())
}
}
// this exit code is initially 0 and will
// be set to an error code if any of the
// builds fail.
var exit int
fmt.Printf("\nDrone Build Results \033[90m(%v)\033[0m\n", len(builders))
// loop through and print results
for _, builder := range builders {
build := builder.Build
res := builder.BuildState
duration := time.Duration(res.Finished - res.Started)
switch {
case builder.BuildState.ExitCode == 0:
fmt.Printf(" \033[32m\u2713\033[0m %v \033[90m(%v)\033[0m\n", build.Name, humanizeDuration(duration*time.Second))
case builder.BuildState.ExitCode != 0:
fmt.Printf(" \033[31m\u2717\033[0m %v \033[90m(%v)\033[0m\n", build.Name, humanizeDuration(duration*time.Second))
exit = builder.BuildState.ExitCode
}
}
os.Exit(exit)
}
func runSequential(builders []*build.Builder) {
// loop through and execute each build
for _, builder := range builders {
if err := builder.Run(); err != nil {
log.Errf("Error executing build: %s", err.Error())
os.Exit(1)
}
}
}
func runParallel(builders []*build.Builder) {
// spawn four worker goroutines
var wg sync.WaitGroup
for _, builder := range builders {
// Increment the WaitGroup counter
wg.Add(1)
// Launch a goroutine to run the build
go func(builder *build.Builder) {
defer wg.Done()
builder.Run()
}(builder)
time.Sleep(500 * time.Millisecond) // get weird iptables failures unless we sleep.
}
// wait for the workers to finish
wg.Wait()
}
var usage = func() {
fmt.Println(`Drone is a tool for building and testing code in Docker containers.
Usage:
drone command [arguments]
The commands are:
build build and test the repository
version print the version number
vet validate the yaml configuration file
-v runs drone with verbose output
-h display this help and exit
--parallel runs drone build tasks in parallel
--timeout=300ms timeout build after 300 milliseconds
Examples:
drone build builds the source in the pwd
drone build /path/to/repo builds the source repository
Use "drone help [command]" for more information about a command.
`)
}

90
cmd/drone/util.go Normal file
View file

@ -0,0 +1,90 @@
package main
import (
"fmt"
"os"
"path/filepath"
"regexp"
"strings"
"time"
)
// getGoPath checks the source codes absolute path
// in reference to the host operating system's GOPATH
// to correctly determine the code's package path. This
// is Go-specific, since Go code must exist in
// $GOPATH/src/github.com/{owner}/{name}
func getGoPath(dir string) (string, bool) {
path := os.Getenv("GOPATH")
if len(path) == 0 {
return "", false
}
// append src to the GOPATH, since
// the code will be stored in the src dir
path = filepath.Join(path, "src")
if !filepath.HasPrefix(dir, path) {
return "", false
}
// remove the prefix from the directory
// this should leave us with the go package name
return dir[len(path):], true
}
var gopathExp = regexp.MustCompile("./src/(github.com/[^/]+/[^/]+|bitbucket.org/[^/]+/[^/]+|code.google.com/[^/]+/[^/]+)")
// getRepoPath checks the source codes absolute path
// on the host operating system in an attempt
// to correctly determine the code's package path. This
// is Go-specific, since Go code must exist in
// $GOPATH/src/github.com/{owner}/{name}
func getRepoPath(dir string) (path string, ok bool) {
// let's get the package directory based
// on the path in the host OS
indexes := gopathExp.FindStringIndex(dir)
if len(indexes) == 0 {
return
}
index := indexes[len(indexes)-1]
// if the dir is /home/ubuntu/go/src/github.com/foo/bar
// the index will start at /src/github.com/foo/bar.
// We'll need to strip "/src/" which is where the
// magic number 5 comes from.
index = strings.LastIndex(dir, "/src/")
return dir[index+5:], true
}
// getGitOrigin checks the .git origin in an attempt
// to correctly determine the code's package path. This
// is Go-specific, since Go code must exist in
// $GOPATH/src/github.com/{owner}/{name}
func getGitOrigin(dir string) (path string, ok bool) {
// TODO
return
}
// prints the time as a human readable string
func humanizeDuration(d time.Duration) string {
if seconds := int(d.Seconds()); seconds < 1 {
return "Less than a second"
} else if seconds < 60 {
return fmt.Sprintf("%d seconds", seconds)
} else if minutes := int(d.Minutes()); minutes == 1 {
return "About a minute"
} else if minutes < 60 {
return fmt.Sprintf("%d minutes", minutes)
} else if hours := int(d.Hours()); hours == 1 {
return "About an hour"
} else if hours < 48 {
return fmt.Sprintf("%d hours", hours)
} else if hours < 24*7*2 {
return fmt.Sprintf("%d days", hours/24)
} else if hours < 24*30*3 {
return fmt.Sprintf("%d weeks", hours/24/7)
} else if hours < 24*365*2 {
return fmt.Sprintf("%d months", hours/24/30)
}
return fmt.Sprintf("%f years", d.Hours()/24/365)
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

187
cmd/droned/drone.go Normal file
View file

@ -0,0 +1,187 @@
package main
import (
"database/sql"
"flag"
"log"
"net/http"
"strings"
"code.google.com/p/go.net/websocket"
"github.com/GeertJohan/go.rice"
"github.com/bmizerany/pat"
_ "github.com/mattn/go-sqlite3"
"github.com/russross/meddler"
"github.com/drone/drone/pkg/channel"
"github.com/drone/drone/pkg/database"
"github.com/drone/drone/pkg/handler"
)
var (
// local path where the SQLite database
// should be stored. By default this is
// in the current working directory.
path string
// port the server will run on
port string
// database driver used to connect to the database
driver string
// driver specific connection information. In this
// case, it should be the location of the SQLite file
datasource string
)
func main() {
// parse command line flags
flag.StringVar(&path, "path", "", "")
flag.StringVar(&port, "port", ":8080", "")
flag.StringVar(&driver, "driver", "sqlite3", "")
flag.StringVar(&datasource, "datasource", "drone.sqlite", "")
flag.Parse()
// setup database and handlers
setupDatabase()
setupStatic()
setupHandlers()
// start the webserver on the default port.
panic(http.ListenAndServe(port, nil))
}
// setup the database connection and register with the
// global database package.
func setupDatabase() {
// inform meddler we're using sqlite
meddler.Default = meddler.SQLite
// connect to the SQLite database
db, err := sql.Open(driver, datasource)
if err != nil {
log.Fatal(err)
}
database.Set(db)
}
// setup routes for static assets. These assets may
// be directly embedded inside the application using
// the `rice embed` command, else they are served from disk.
func setupStatic() {
box := rice.MustFindBox("assets")
http.Handle("/css/", http.FileServer(box.HTTPBox()))
http.Handle("/img/", http.FileServer(box.HTTPBox()))
}
// setup routes for serving dynamic content.
func setupHandlers() {
m := pat.New()
m.Get("/login", handler.ErrorHandler(handler.Login))
m.Post("/login", handler.ErrorHandler(handler.Authorize))
m.Get("/logout", handler.ErrorHandler(handler.Logout))
m.Get("/forgot", handler.ErrorHandler(handler.Forgot))
m.Post("/forgot", handler.ErrorHandler(handler.ForgotPost))
m.Get("/reset", handler.ErrorHandler(handler.Reset))
m.Post("/reset", handler.ErrorHandler(handler.ResetPost))
m.Get("/register", handler.ErrorHandler(handler.Register))
m.Post("/register", handler.ErrorHandler(handler.RegisterPost))
m.Get("/accept", handler.UserHandler(handler.TeamMemberAccept))
// handlers for setting up your GitHub repository
m.Post("/new/github.com", handler.UserHandler(handler.RepoCreateGithub))
m.Get("/new/github.com", handler.UserHandler(handler.RepoAdd))
// handlers for linking your GitHub account
m.Get("/auth/login/github", handler.UserHandler(handler.LinkGithub))
// handlers for dashboard pages
m.Get("/dashboard/team/:team", handler.UserHandler(handler.TeamShow))
m.Get("/dashboard", handler.UserHandler(handler.UserShow))
// handlers for user account management
m.Get("/account/user/profile", handler.UserHandler(handler.UserEdit))
m.Post("/account/user/profile", handler.UserHandler(handler.UserUpdate))
m.Get("/account/user/delete", handler.UserHandler(handler.UserDeleteConfirm))
m.Post("/account/user/delete", handler.UserHandler(handler.UserDelete))
m.Get("/account/user/password", handler.UserHandler(handler.UserPass))
m.Post("/account/user/password", handler.UserHandler(handler.UserPassUpdate))
m.Get("/account/user/teams/add", handler.UserHandler(handler.TeamAdd))
m.Post("/account/user/teams/add", handler.UserHandler(handler.TeamCreate))
m.Get("/account/user/teams", handler.UserHandler(handler.UserTeams))
// handlers for team managements
m.Get("/account/team/:team/profile", handler.UserHandler(handler.TeamEdit))
m.Post("/account/team/:team/profile", handler.UserHandler(handler.TeamUpdate))
m.Get("/account/team/:team/delete", handler.UserHandler(handler.TeamDeleteConfirm))
m.Post("/account/team/:team/delete", handler.UserHandler(handler.TeamDelete))
m.Get("/account/team/:team/members/add", handler.UserHandler(handler.TeamMemberAdd))
m.Post("/account/team/:team/members/add", handler.UserHandler(handler.TeamMemberInvite))
m.Get("/account/team/:team/members/edit", handler.UserHandler(handler.TeamMemberEdit))
m.Post("/account/team/:team/members/edit", handler.UserHandler(handler.TeamMemberUpdate))
m.Post("/account/team/:team/members/delete", handler.UserHandler(handler.TeamMemberDelete))
m.Get("/account/team/:team/members", handler.UserHandler(handler.TeamMembers))
// handlers for system administration
m.Get("/account/admin/settings", handler.AdminHandler(handler.AdminSettings))
m.Post("/account/admin/settings", handler.AdminHandler(handler.AdminSettingsUpdate))
m.Get("/account/admin/users/edit", handler.AdminHandler(handler.AdminUserEdit))
m.Post("/account/admin/users/edit", handler.AdminHandler(handler.AdminUserUpdate))
m.Post("/account/admin/users/delete", handler.AdminHandler(handler.AdminUserDelete))
m.Get("/account/admin/users/add", handler.AdminHandler(handler.AdminUserAdd))
m.Post("/account/admin/users", handler.AdminHandler(handler.AdminUserInvite))
m.Get("/account/admin/users", handler.AdminHandler(handler.AdminUserList))
// handlers for GitHub post-commit hooks
m.Post("/hook/github.com", handler.ErrorHandler(handler.Hook))
// handlers for first-time installation
m.Get("/install", handler.ErrorHandler(handler.Install))
m.Post("/install", handler.ErrorHandler(handler.InstallPost))
// handlers for repository, commits and build details
m.Get("/:host/:owner/:name/commit/:commit/build/:label/out.txt", handler.RepoHandler(handler.BuildOut))
m.Get("/:host/:owner/:name/commit/:commit/build/:label", handler.RepoHandler(handler.CommitShow))
m.Get("/:host/:owner/:name/commit/:commit", handler.RepoHandler(handler.CommitShow))
m.Get("/:host/:owner/:name/tree/:branch/status.png", handler.ErrorHandler(handler.Badge))
m.Get("/:host/:owner/:name/tree/:branch", handler.RepoHandler(handler.RepoDashboard))
m.Get("/:host/:owner/:name/status.png", handler.ErrorHandler(handler.Badge))
m.Get("/:host/:owner/:name/settings", handler.RepoAdminHandler(handler.RepoSettingsForm))
m.Get("/:host/:owner/:name/params", handler.RepoAdminHandler(handler.RepoParamsForm))
m.Get("/:host/:owner/:name/badges", handler.RepoAdminHandler(handler.RepoBadges))
m.Get("/:host/:owner/:name/keys", handler.RepoAdminHandler(handler.RepoKeys))
m.Get("/:host/:owner/:name/delete", handler.RepoAdminHandler(handler.RepoDeleteForm))
m.Post("/:host/:owner/:name/delete", handler.RepoAdminHandler(handler.RepoDelete))
m.Get("/:host/:owner/:name", handler.RepoHandler(handler.RepoDashboard))
m.Post("/:host/:owner/:name", handler.RepoHandler(handler.RepoUpdate))
http.Handle("/feed", websocket.Handler(channel.Read))
// no routes are served at the root URL. Instead we will
// redirect the user to his/her dashboard page.
m.Get("/", http.RedirectHandler("/dashboard", http.StatusSeeOther))
// the first time a page is requested we should record
// the scheme and hostname.
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
// get the hostname and scheme
// our multiplexer is a bit finnicky and therefore requires
// us to strip any trailing slashes in order to correctly
// find and match a route.
if r.URL.Path != "/" && strings.HasSuffix(r.URL.Path, "/") {
http.Redirect(w, r, r.URL.Path[:len(r.URL.Path)-1], http.StatusSeeOther)
return
}
// standard header variables that should be set, for good measure.
w.Header().Add("Cache-Control", "no-cache, no-store, max-age=0, must-revalidate")
w.Header().Add("X-Frame-Options", "DENY")
w.Header().Add("X-Content-Type-Options", "nosniff")
w.Header().Add("X-XSS-Protection", "1; mode=block")
// ok, now we're ready to serve the request.
m.ServeHTTP(w, r)
})
}

7
deb/drone/DEBIAN/control Normal file
View file

@ -0,0 +1,7 @@
Package: drone
Version: 0.1
Section: base
Priority: optional
Architecture: amd64
Maintainer: Brad Rydzewski <brad@drone.io>
Description: Drone continuous integration server

View file

@ -0,0 +1,8 @@
start on (filesystem and net-device-up)
chdir /var/lib/drone
console log
script
droned --port=":80"
end script

471
pkg/build/build.go Normal file
View file

@ -0,0 +1,471 @@
package build
import (
"fmt"
"io"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
"github.com/drone/drone/pkg/build/buildfile"
"github.com/drone/drone/pkg/build/docker"
"github.com/drone/drone/pkg/build/dockerfile"
"github.com/drone/drone/pkg/build/log"
"github.com/drone/drone/pkg/build/proxy"
"github.com/drone/drone/pkg/build/repo"
"github.com/drone/drone/pkg/build/script"
)
// instance of the Docker client
var client = docker.New()
// BuildState stores information about a build
// process including the Exit status and various
// Runtime statistics (coming soon).
type BuildState struct {
Started int64
Finished int64
ExitCode int
// we may eventually include detailed resource
// usage statistics, including including CPU time,
// Max RAM, Max Swap, Disk space, and more.
}
// Builder represents a build process being prepared
// to run.
type Builder struct {
// Image specifies the Docker Image that will be
// used to virtualize the Build process.
Build *script.Build
// Source specifies the Repository path of the code
// that we are testing.
//
// The source repository may be a local repository
// on the current filesystem, or a remote repository
// on GitHub, Bitbucket, etc.
Repo *repo.Repo
// Key is an identify file, such as an RSA private key, that
// will be copied into the environments ~/.ssh/id_rsa file.
Key []byte
// Timeout is the maximum amount of to will wait for a process
// to exit.
//
// The default is no timeout.
Timeout time.Duration
// Stdout specifies the builds's standard output.
//
// If stdout is nil, Run connects the corresponding file descriptor
// to the null device (os.DevNull).
Stdout io.Writer
// BuildState contains information about an exited build,
// available after a call to Run.
BuildState *BuildState
// Docker image that was created for
// this build.
image *docker.Image
// Docker container was that created
// for this build.
container *docker.Run
// Docker containers created for the
// specified services and linked to
// this build.
services []*docker.Container
}
func (b *Builder) Run() error {
// teardown will remove the Image and stop and
// remove the service containers after the
// build is done running.
defer b.teardown()
// setup will create the Image and supporting
// service containers.
if err := b.setup(); err != nil {
return err
}
// make sure build state is not nil
b.BuildState = &BuildState{}
b.BuildState.ExitCode = 0
b.BuildState.Started = time.Now().UTC().Unix()
c := make(chan error, 1)
go func() {
c <- b.run()
}()
// wait for either a) the job to complete or b) the job to timeout
select {
case err := <-c:
return err
case <-time.After(b.Timeout):
log.Errf("time limit exceeded for build %s", b.Build.Name)
b.BuildState.ExitCode = 124
b.BuildState.Finished = time.Now().UTC().Unix()
return nil
}
return nil
}
func (b *Builder) setup() error {
// temp directory to store all files required
// to generate the Docker image.
dir, err := ioutil.TempDir("", "drone-")
if err != nil {
return err
}
// clean up after our mess.
defer os.RemoveAll(dir)
// make sure the image isn't empty. this would be bad
if len(b.Build.Image) == 0 {
log.Err("Fatal Error, No Docker Image specified")
return fmt.Errorf("Error: missing Docker image")
}
// if we're using an alias for the build name we
// should substitute it now
if alias, ok := builders[b.Build.Image]; ok {
b.Build.Image = alias.Tag
}
// if this is a local repository we should symlink
// to the source code in our temp directory
if b.Repo.IsLocal() {
// this is where we used to use symlinks. We should
// talk to the docker team about this, since copying
// the entire repository is slow :(
//
// see https://github.com/dotcloud/docker/pull/3567
//src := filepath.Join(dir, "src")
//err = os.Symlink(b.Repo.Path, src)
//if err != nil {
// return err
//}
src := filepath.Join(dir, "src")
cmd := exec.Command("cp", "-a", b.Repo.Path, src)
if err := cmd.Run(); err != nil {
return err
}
}
// start all services required for the build
// that will get linked to the container.
for _, service := range b.Build.Services {
image, ok := services[service]
if !ok {
return fmt.Errorf("Error: Invalid or unknown service %s", service)
}
// debugging
log.Infof("starting service container %s", image.Tag)
// Run the contianer
run, err := client.Containers.RunDaemonPorts(image.Tag, image.Ports...)
if err != nil {
return err
}
// Get the container info
info, err := client.Containers.Inspect(run.ID)
if err != nil {
// on error kill the container since it hasn't yet been
// added to the array and would therefore not get
// removed in the defer statement.
client.Containers.Stop(run.ID, 10)
client.Containers.Remove(run.ID)
return err
}
// Add the running service to the list
b.services = append(b.services, info)
}
if err := b.writeIdentifyFile(dir); err != nil {
return err
}
if err := b.writeBuildScript(dir); err != nil {
return err
}
if err := b.writeProxyScript(dir); err != nil {
return err
}
if err := b.writeDockerfile(dir); err != nil {
return err
}
// debugging
log.Info("creating build image")
// check for build container (ie bradrydzewski/go:1.2)
// and download if it doesn't already exist
if _, err := client.Images.Inspect(b.Build.Image); err == docker.ErrNotFound {
// download the image if it doesn't exist
if err := client.Images.Pull(b.Build.Image); err != nil {
return err
}
}
// create the Docker image
id := createUID()
if err := client.Images.Build(id, dir); err != nil {
return err
}
// debugging
log.Infof("copying repository to %s", b.Repo.Dir)
// get the image details
b.image, err = client.Images.Inspect(id)
if err != nil {
// if we have problems with the image make sure
// we remove it before we exit
client.Images.Remove(id)
return err
}
return nil
}
// teardown is a helper function that we can use to
// stop and remove the build container, its supporting image,
// and the supporting service containers.
func (b *Builder) teardown() error {
// stop and destroy the container
if b.container != nil {
// debugging
log.Info("removing build container")
// stop the container, ignore error message
client.Containers.Stop(b.container.ID, 15)
// remove the container, ignore error message
if err := client.Containers.Remove(b.container.ID); err != nil {
log.Errf("failed to delete build container %s", b.container.ID)
}
}
// stop and destroy the container services
for i, container := range b.services {
// debugging
log.Infof("removing service container %s", b.Build.Services[i])
// stop the service container, ignore the error
client.Containers.Stop(container.ID, 15)
// remove the service container, ignore the error
if err := client.Containers.Remove(container.ID); err != nil {
log.Errf("failed to delete service container %s", container.ID)
}
}
// destroy the underlying image
if b.image != nil {
// debugging
log.Info("removing build image")
if _, err := client.Images.Remove(b.image.ID); err != nil {
log.Errf("failed to completely delete build image %s. %s", b.image.ID, err.Error())
}
}
return nil
}
func (b *Builder) run() error {
// create and run the container
conf := docker.Config{
Image: b.image.ID,
AttachStdin: false,
AttachStdout: true,
AttachStderr: true,
}
host := docker.HostConfig{
Privileged: false,
}
// debugging
log.Noticef("starting build %s", b.Build.Name)
// link service containers
for i, service := range b.services {
image, ok := services[b.Build.Services[i]]
if !ok {
continue // THIS SHOULD NEVER HAPPEN
}
// link the service container to our
// build container.
host.Links = append(host.Links, service.Name[1:]+":"+image.Name)
}
// create the container from the image
run, err := client.Containers.Create(&conf)
if err != nil {
return err
}
// cache instance of docker.Run
b.container = run
// attach to the container
go func() {
client.Containers.Attach(run.ID, &writer{b.Stdout})
}()
// start the container
if err := client.Containers.Start(run.ID, &host); err != nil {
b.BuildState.ExitCode = 1
b.BuildState.Finished = time.Now().UTC().Unix()
return err
}
// wait for the container to stop
wait, err := client.Containers.Wait(run.ID)
if err != nil {
b.BuildState.ExitCode = 1
b.BuildState.Finished = time.Now().UTC().Unix()
return err
}
// set completion time
b.BuildState.Finished = time.Now().UTC().Unix()
// get the exit code if possible
b.BuildState.ExitCode = wait.StatusCode //b.container.State.ExitCode
return nil
}
// writeDockerfile is a helper function that generates a
// Dockerfile and writes to the builds temporary directory
// so that it can be used to create the Image.
func (b *Builder) writeDockerfile(dir string) error {
var dockerfile = dockerfile.New(b.Build.Image)
dockerfile.WriteWorkdir(b.Repo.Dir)
dockerfile.WriteAdd("drone", "/usr/local/bin/")
// upload source code if repository is stored
// on the host machine
if b.Repo.IsRemote() == false {
dockerfile.WriteAdd("src", filepath.Join(b.Repo.Dir))
}
switch {
case strings.HasPrefix(b.Build.Image, "bradrydzewski/"),
strings.HasPrefix(b.Build.Image, "drone/"):
// the default user for all official Drone imnage
// is the "ubuntu" user, since all build images
// inherit from the ubuntu cloud ISO
dockerfile.WriteUser("ubuntu")
dockerfile.WriteEnv("HOME", "/home/ubuntu")
dockerfile.WriteEnv("LANG", "en_US.UTF-8")
dockerfile.WriteEnv("LANGUAGE", "en_US:en")
dockerfile.WriteEnv("LOGNAME", "ubuntu")
dockerfile.WriteEnv("TERM", "xterm")
dockerfile.WriteEnv("SHELL", "/bin/bash")
dockerfile.WriteAdd("id_rsa", "/home/ubuntu/.ssh/id_rsa")
dockerfile.WriteRun("sudo chown -R ubuntu:ubuntu /home/ubuntu/.ssh")
dockerfile.WriteRun("sudo chown -R ubuntu:ubuntu /var/cache/drone")
dockerfile.WriteRun("sudo chown -R ubuntu:ubuntu /usr/local/bin/drone")
dockerfile.WriteRun("sudo chmod 600 /home/ubuntu/.ssh/id_rsa")
default:
// all other images are assumed to use
// the root user.
dockerfile.WriteUser("root")
dockerfile.WriteEnv("HOME", "/root")
dockerfile.WriteEnv("LANG", "en_US.UTF-8")
dockerfile.WriteEnv("LANGUAGE", "en_US:en")
dockerfile.WriteEnv("LOGNAME", "root")
dockerfile.WriteEnv("TERM", "xterm")
dockerfile.WriteEnv("SHELL", "/bin/bash")
dockerfile.WriteEnv("GOPATH", "/var/cache/drone")
dockerfile.WriteAdd("id_rsa", "/root/.ssh/id_rsa")
dockerfile.WriteRun("chmod 600 /root/.ssh/id_rsa")
}
dockerfile.WriteAdd("proxy.sh", "/etc/drone.d/")
dockerfile.WriteEntrypoint("/bin/bash -e /usr/local/bin/drone")
// write the Dockerfile to the temporary directory
return ioutil.WriteFile(filepath.Join(dir, "Dockerfile"), dockerfile.Bytes(), 0700)
}
// writeBuildScript is a helper function that
// will generate the build script file in the builder's
// temp directory to be added to the Image.
func (b *Builder) writeBuildScript(dir string) error {
f := buildfile.New()
// if the repository is remote then we should
// add the commands to the build script to
// clone the repository
if b.Repo.IsRemote() {
for _, cmd := range b.Repo.Commands() {
f.WriteCmd(cmd)
}
}
// if the commit is for merging a pull request
// we should only execute the build commands,
// and omit the deploy and publish commands.
if len(b.Repo.PR) == 0 {
b.Build.Write(f)
} else {
// only write the build commands
b.Build.WriteBuild(f)
}
scriptfilePath := filepath.Join(dir, "drone")
return ioutil.WriteFile(scriptfilePath, f.Bytes(), 0700)
}
// writeProxyScript is a helper function that
// will generate the proxy.sh file in the builder's
// temp directory to be added to the Image.
func (b *Builder) writeProxyScript(dir string) error {
var proxyfile = proxy.Proxy{}
// loop through services so that we can
// map ip address to localhost
for _, container := range b.services {
// create an entry for each port
for port, _ := range container.NetworkSettings.Ports {
proxyfile.Set(port.Port(), container.NetworkSettings.IPAddress)
}
}
// write the proxyfile to the temp directory
proxyfilePath := filepath.Join(dir, "proxy.sh")
return ioutil.WriteFile(proxyfilePath, proxyfile.Bytes(), 0755)
}
// writeIdentifyFile is a helper function that
// will generate the id_rsa file in the builder's
// temp directory to be added to the Image.
func (b *Builder) writeIdentifyFile(dir string) error {
keyfilePath := filepath.Join(dir, "id_rsa")
return ioutil.WriteFile(keyfilePath, b.Key, 0700)
}

View file

@ -0,0 +1,72 @@
package buildfile
import (
"bytes"
"fmt"
)
type Buildfile struct {
bytes.Buffer
}
func New() *Buildfile {
b := Buildfile{}
b.WriteString(base)
return &b
}
// WriteCmd writes a command to the build file. The
// command will be echoed back as a base16 encoded
// command so that it can be parsed and appended to
// the build output
func (b *Buildfile) WriteCmd(command string) {
// echo the command as an encoded value
b.WriteString(fmt.Sprintf("echo '#DRONE:%x'\n", command))
// and then run the command
b.WriteString(fmt.Sprintf("%s\n", command))
}
// WriteCmdSilent writes a command to the build file
// but does not echo the command.
func (b *Buildfile) WriteCmdSilent(command string) {
b.WriteString(fmt.Sprintf("%s\n", command))
}
// WriteComment adds a comment to the build file. This
// is really only used internally for debugging purposes.
func (b *Buildfile) WriteComment(comment string) {
b.WriteString(fmt.Sprintf("#%s\n", comment))
}
// WriteEnv exports the environment variable as
// part of the script. The environment variables
// are not echoed back to the console, and are
// kept private by default.
func (b *Buildfile) WriteEnv(key, value string) {
b.WriteString(fmt.Sprintf("export %s=%s\n", key, value))
}
// every build script starts with the following
// code at the start.
var base = `
#!/bin/bash
# drone configuration files are stored in /etc/drone.d
# execute these files prior to our build to set global
# environment variables and initialize programs (like rbenv)
if [ -d /etc/drone.d ]; then
for i in /etc/drone.d/*.sh; do
if [ -r $i ]; then
. $i
fi
done
unset i
fi
# be sure to exit on error and print out
# our bash commands, so we can which commands
# are executing and troubleshoot failures.
set -e
# user-defined commands below ##############################
`

258
pkg/build/docker/client.go Normal file
View file

@ -0,0 +1,258 @@
package docker
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"net"
"net/http"
"net/http/httputil"
"os"
"strings"
"github.com/dotcloud/docker/pkg/term"
"github.com/dotcloud/docker/utils"
)
const (
APIVERSION = 1.9
DEFAULTHTTPPORT = 4243
DEFAULTUNIXSOCKET = "/var/run/docker.sock"
DEFAULTPROTOCOL = "unix"
DEFAULTTAG = "latest"
VERSION = "0.8.0"
)
// Enables verbose logging to the Terminal window
var Logging = true
// New creates an instance of the Docker Client
func New() *Client {
c := &Client{}
c.proto = DEFAULTPROTOCOL
c.addr = DEFAULTUNIXSOCKET
// if the default socket doesn't exist then
// we'll try to connect to the default tcp address
if _, err := os.Stat(DEFAULTUNIXSOCKET); err != nil {
c.proto = "tcp"
c.addr = "0.0.0.0:4243"
}
c.Images = &ImageService{c}
c.Containers = &ContainerService{c}
return c
}
type Client struct {
proto string
addr string
Images *ImageService
Containers *ContainerService
}
var (
// Returned if the specified resource does not exist.
ErrNotFound = errors.New("Not Found")
// Returned if the caller attempts to make a call or modify a resource
// for which the caller is not authorized.
//
// The request was a valid request, the caller's authentication credentials
// succeeded but those credentials do not grant the caller permission to
// access the resource.
ErrForbidden = errors.New("Forbidden")
// Returned if the call requires authentication and either the credentials
// provided failed or no credentials were provided.
ErrNotAuthorized = errors.New("Unauthorized")
// Returned if the caller submits a badly formed request. For example,
// the caller can receive this return if you forget a required parameter.
ErrBadRequest = errors.New("Bad Request")
)
// helper function used to make HTTP requests to the Docker daemon.
func (c *Client) do(method, path string, in, out interface{}) error {
// if data input is provided, serialize to JSON
var payload io.Reader
if in != nil {
buf, err := json.Marshal(in)
if err != nil {
return err
}
payload = bytes.NewBuffer(buf)
}
// create the request
req, err := http.NewRequest(method, fmt.Sprintf("/v%g%s", APIVERSION, path), payload)
if err != nil {
return err
}
// set the appropariate headers
req.Header = http.Header{}
req.Header.Set("User-Agent", "Docker-Client/"+VERSION)
req.Header.Set("Content-Type", "application/json")
// dial the host server
req.Host = c.addr
dial, err := net.Dial(c.proto, c.addr)
if err != nil {
return err
}
// make the request
conn := httputil.NewClientConn(dial, nil)
resp, err := conn.Do(req)
defer conn.Close()
if err != nil {
return err
}
// Read the bytes from the body (make sure we defer close the body)
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
}
// Check for an http error status (ie not 200 StatusOK)
switch resp.StatusCode {
case 404:
return ErrNotFound
case 403:
return ErrForbidden
case 401:
return ErrNotAuthorized
case 400:
return ErrBadRequest
}
// Unmarshall the JSON response
if out != nil {
return json.Unmarshal(body, out)
}
return nil
}
func (c *Client) hijack(method, path string, setRawTerminal bool, out io.Writer) error {
req, err := http.NewRequest(method, fmt.Sprintf("/v%g%s", APIVERSION, path), nil)
if err != nil {
return err
}
req.Header.Set("User-Agent", "Docker-Client/"+VERSION)
req.Header.Set("Content-Type", "plain/text")
req.Host = c.addr
dial, err := net.Dial(c.proto, c.addr)
if err != nil {
if strings.Contains(err.Error(), "connection refused") {
return fmt.Errorf("Can't connect to docker daemon. Is 'docker -d' running on this host?")
}
return err
}
clientconn := httputil.NewClientConn(dial, nil)
defer clientconn.Close()
// Server hijacks the connection, error 'connection closed' expected
clientconn.Do(req)
// Hijack the connection to read / write
rwc, br := clientconn.Hijack()
defer rwc.Close()
// launch a goroutine to copy the stream
// of build output to the writer.
errStdout := make(chan error, 1)
go func() {
var err error
if setRawTerminal {
_, err = io.Copy(out, br)
} else {
_, err = utils.StdCopy(out, out, br)
}
errStdout <- err
}()
// wait for a response
if err := <-errStdout; err != nil {
return err
}
return nil
}
func (c *Client) stream(method, path string, in io.Reader, out io.Writer, headers http.Header) error {
if (method == "POST" || method == "PUT") && in == nil {
in = bytes.NewReader(nil)
}
// setup the request
req, err := http.NewRequest(method, fmt.Sprintf("/v%g%s", APIVERSION, path), in)
if err != nil {
return err
}
// set default headers
req.Header = headers
req.Header.Set("User-Agent", "Docker-Client/0.6.4")
req.Header.Set("Content-Type", "plain/text")
// dial the host server
req.Host = c.addr
dial, err := net.Dial(c.proto, c.addr)
if err != nil {
return err
}
// make the request
conn := httputil.NewClientConn(dial, nil)
resp, err := conn.Do(req)
defer conn.Close()
if err != nil {
return err
}
// make sure we defer close the body
defer resp.Body.Close()
// Check for an http error status (ie not 200 StatusOK)
switch resp.StatusCode {
case 404:
return ErrNotFound
case 403:
return ErrForbidden
case 401:
return ErrNotAuthorized
case 400:
return ErrBadRequest
}
// If no output we exit now with no errors
if out == nil {
return nil
}
// copy the output stream to the writer
if resp.Header.Get("Content-Type") == "application/json" {
var terminalFd = os.Stdin.Fd()
var isTerminal = term.IsTerminal(terminalFd)
// it may not make sense to put this code here, but it works for
// us at the moment, and I don't feel like refactoring
return utils.DisplayJSONMessagesStream(resp.Body, out, terminalFd, isTerminal)
}
// otherwise plain text
if _, err := io.Copy(out, resp.Body); err != nil {
return err
}
return nil
}

View file

@ -0,0 +1,147 @@
package docker
import (
"fmt"
"io"
)
type ContainerService struct {
*Client
}
// List only running containers.
func (c *ContainerService) List() ([]*Containers, error) {
containers := []*Containers{}
err := c.do("GET", "/containers/json?all=0", nil, &containers)
return containers, err
}
// List all containers
func (c *ContainerService) ListAll() ([]*Containers, error) {
containers := []*Containers{}
err := c.do("GET", "/containers/json?all=1", nil, &containers)
return containers, err
}
// Create a Container
func (c *ContainerService) Create(conf *Config) (*Run, error) {
run, err := c.create(conf)
switch {
// if no error, exit immediately
case err == nil:
return run, nil
// if error we exit, unless it is
// a NOT FOUND error, which means we just
// need to download the Image from the center
// image index
case err != nil && err != ErrNotFound:
return nil, err
}
// attempt to pull the image
if err := c.Images.Pull(conf.Image); err != nil {
return nil, err
}
// now that we have the image, re-try creation
return c.create(conf)
}
func (c *ContainerService) create(conf *Config) (*Run, error) {
run := Run{}
err := c.do("POST", "/containers/create", conf, &run)
return &run, err
}
// Start the container id
func (c *ContainerService) Start(id string, conf *HostConfig) error {
return c.do("POST", fmt.Sprintf("/containers/%s/start", id), &conf, nil)
}
// Stop the container id
func (c *ContainerService) Stop(id string, timeout int) error {
return c.do("POST", fmt.Sprintf("/containers/%s/stop?t=%v", id, timeout), nil, nil)
}
// Remove the container id from the filesystem.
func (c *ContainerService) Remove(id string) error {
return c.do("DELETE", fmt.Sprintf("/containers/%s", id), nil, nil)
}
// Block until container id stops, then returns the exit code
func (c *ContainerService) Wait(id string) (*Wait, error) {
wait := Wait{}
err := c.do("POST", fmt.Sprintf("/containers/%s/wait", id), nil, &wait)
return &wait, err
}
// Attach to the container to stream the stdout and stderr
func (c *ContainerService) Attach(id string, out io.Writer) error {
path := fmt.Sprintf("/containers/%s/attach?&stream=1&stdout=1&stderr=1", id)
return c.hijack("POST", path, false, out)
}
// Stop the container id
func (c *ContainerService) Inspect(id string) (*Container, error) {
container := Container{}
err := c.do("GET", fmt.Sprintf("/containers/%s/json", id), nil, &container)
return &container, err
}
// Run the container
func (c *ContainerService) Run(conf *Config, host *HostConfig, out io.Writer) (*Wait, error) {
// create the container from the image
run, err := c.Create(conf)
if err != nil {
return nil, err
}
// attach to the container
go func() {
c.Attach(run.ID, out)
}()
// start the container
if err := c.Start(run.ID, host); err != nil {
return nil, err
}
// wait for the container to stop
wait, err := c.Wait(run.ID)
if err != nil {
return nil, err
}
return wait, nil
}
// Run the container as a Daemon
func (c *ContainerService) RunDaemon(conf *Config, host *HostConfig) (*Run, error) {
run, err := c.Create(conf)
if err != nil {
return nil, err
}
// start the container
err = c.Start(run.ID, host)
return run, err
}
func (c *ContainerService) RunDaemonPorts(image string, ports ...string) (*Run, error) {
// setup configuration
config := Config{Image: image}
config.ExposedPorts = make(map[Port]struct{})
// host configuration
host := HostConfig{}
host.PortBindings = make(map[Port][]PortBinding)
// loop through and add ports
for _, port := range ports {
config.ExposedPorts[Port(port+"/tcp")] = struct{}{}
host.PortBindings[Port(port+"/tcp")] = []PortBinding{{HostIp: "127.0.0.1", HostPort: ""}}
}
//127.0.0.1::%s
//map[3306/tcp:{}] map[3306/tcp:[{127.0.0.1 }]]
return c.RunDaemon(&config, &host)
}

124
pkg/build/docker/image.go Normal file
View file

@ -0,0 +1,124 @@
package docker
import (
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"os"
"time"
"github.com/dotcloud/docker/archive"
"github.com/dotcloud/docker/utils"
)
type Images struct {
ID string `json:"Id"`
RepoTags []string `json:",omitempty"`
Created int64
Size int64
VirtualSize int64
ParentId string `json:",omitempty"`
// DEPRECATED
Repository string `json:",omitempty"`
Tag string `json:",omitempty"`
}
type Image struct {
ID string `json:"id"`
Parent string `json:"parent,omitempty"`
Comment string `json:"comment,omitempty"`
Created time.Time `json:"created"`
Container string `json:"container,omitempty"`
ContainerConfig Config `json:"container_config,omitempty"`
DockerVersion string `json:"docker_version,omitempty"`
Author string `json:"author,omitempty"`
Config *Config `json:"config,omitempty"`
Architecture string `json:"architecture,omitempty"`
OS string `json:"os,omitempty"`
Size int64
}
type Delete struct {
Deleted string `json:",omitempty"`
Untagged string `json:",omitempty"`
}
type ImageService struct {
*Client
}
// List Images
func (c *ImageService) List() ([]*Images, error) {
images := []*Images{}
err := c.do("GET", "/images/json?all=0", nil, &images)
return images, err
}
// Create an image, either by pull it from the registry or by importing it.
func (c *ImageService) Create(image string) error {
return c.do("POST", fmt.Sprintf("/images/create?fromImage=%s"), nil, nil)
}
func (c *ImageService) Pull(image string) error {
name, tag := utils.ParseRepositoryTag(image)
if len(tag) == 0 {
tag = DEFAULTTAG
}
return c.PullTag(name, tag)
}
func (c *ImageService) PullTag(name, tag string) error {
var out io.Writer
if Logging {
out = os.Stdout
}
path := fmt.Sprintf("/images/create?fromImage=%s&tag=%s", name, tag)
return c.stream("POST", path, nil, out, http.Header{})
}
// Remove the image name from the filesystem
func (c *ImageService) Remove(image string) ([]*Delete, error) {
resp := []*Delete{}
err := c.do("DELETE", fmt.Sprintf("/images/%s", image), nil, &resp)
return resp, err
}
// Inspect the image
func (c *ImageService) Inspect(name string) (*Image, error) {
image := Image{}
err := c.do("GET", fmt.Sprintf("/images/%s/json", name), nil, &image)
return &image, err
}
// Build the Image
func (c *ImageService) Build(tag, dir string) error {
// tar the file
context, err := archive.Tar(dir, archive.Uncompressed)
if err != nil {
return err
}
var body io.Reader
body = ioutil.NopCloser(context)
// Upload the build context
v := url.Values{}
v.Set("t", tag)
v.Set("q", "1")
//v.Set("rm", "1")
// url path
path := fmt.Sprintf("/build?%s", v.Encode())
// set content type to tar file
headers := http.Header{}
headers.Set("Content-Type", "application/tar")
// make the request
return c.stream("POST", path, body, nil, headers)
}

166
pkg/build/docker/structs.go Normal file
View file

@ -0,0 +1,166 @@
package docker
import (
"fmt"
"strconv"
"strings"
"time"
)
// These are structures copied from the Docker project.
// We avoid importing the libraries due to a CGO
// depenency on libdevmapper that we'd like to avoid.
type KeyValuePair struct {
Key string
Value string
}
type HostConfig struct {
Binds []string
ContainerIDFile string
LxcConf []KeyValuePair
Privileged bool
PortBindings map[Port][]PortBinding
Links []string
PublishAllPorts bool
}
type Top struct {
Titles []string
Processes [][]string
}
type Containers struct {
ID string `json:"Id"`
Image string
Command string
Created int64
Status string
Ports []Port
SizeRw int64
SizeRootFs int64
Names []string
}
type Run struct {
ID string `json:"Id"`
Warnings []string `json:",omitempty"`
}
type Wait struct {
StatusCode int
}
type State struct {
Running bool
Pid int
ExitCode int
StartedAt time.Time
FinishedAt time.Time
Ghost bool
}
type PortBinding struct {
HostIp string
HostPort string
}
// 80/tcp
type Port string
func (p Port) Proto() string {
parts := strings.Split(string(p), "/")
if len(parts) == 1 {
return "tcp"
}
return parts[1]
}
func (p Port) Port() string {
return strings.Split(string(p), "/")[0]
}
func (p Port) Int() int {
i, err := parsePort(p.Port())
if err != nil {
panic(err)
}
return i
}
func parsePort(rawPort string) (int, error) {
port, err := strconv.ParseUint(rawPort, 10, 16)
if err != nil {
return 0, err
}
return int(port), nil
}
func NewPort(proto, port string) Port {
return Port(fmt.Sprintf("%s/%s", port, proto))
}
type PortMapping map[string]string // Deprecated
type NetworkSettings struct {
IPAddress string
IPPrefixLen int
Gateway string
Bridge string
PortMapping map[string]PortMapping // Deprecated
Ports map[Port][]PortBinding
}
type Config struct {
Hostname string
Domainname string
User string
Memory int64 // Memory limit (in bytes)
MemorySwap int64 // Total memory usage (memory + swap); set `-1' to disable swap
CpuShares int64 // CPU shares (relative weight vs. other containers)
AttachStdin bool
AttachStdout bool
AttachStderr bool
PortSpecs []string // Deprecated - Can be in the format of 8080/tcp
ExposedPorts map[Port]struct{}
Tty bool // Attach standard streams to a tty, including stdin if it is not closed.
OpenStdin bool // Open stdin
StdinOnce bool // If true, close stdin after the 1 attached client disconnects.
Env []string
Cmd []string
Dns []string
Image string // Name of the image as it was passed by the operator (eg. could be symbolic)
Volumes map[string]struct{}
VolumesFrom string
WorkingDir string
Entrypoint []string
NetworkDisabled bool
}
type Container struct {
ID string
Created time.Time
Path string
Args []string
Config *Config
State State
Image string
NetworkSettings *NetworkSettings
SysInitPath string
ResolvConfPath string
HostnamePath string
HostsPath string
Name string
Driver string
Volumes map[string]string
// Store rw/ro in a separate structure to preserve reverse-compatibility on-disk.
// Easier than migrating older container configs :)
VolumesRW map[string]bool
}

View file

@ -0,0 +1,44 @@
package dockerfile
import (
"bytes"
"fmt"
)
type Dockerfile struct {
bytes.Buffer
}
func New(from string) *Dockerfile {
d := Dockerfile{}
d.WriteFrom(from)
return &d
}
func (d *Dockerfile) WriteAdd(from, to string) {
d.WriteString(fmt.Sprintf("ADD %s %s\n", from, to))
}
func (d *Dockerfile) WriteFrom(from string) {
d.WriteString(fmt.Sprintf("FROM %s\n", from))
}
func (d *Dockerfile) WriteRun(cmd string) {
d.WriteString(fmt.Sprintf("RUN %s\n", cmd))
}
func (d *Dockerfile) WriteUser(user string) {
d.WriteString(fmt.Sprintf("USER %s\n", user))
}
func (d *Dockerfile) WriteEnv(key, val string) {
d.WriteString(fmt.Sprintf("ENV %s %s\n", key, val))
}
func (d *Dockerfile) WriteWorkdir(workdir string) {
d.WriteString(fmt.Sprintf("WORKDIR %s\n", workdir))
}
func (d *Dockerfile) WriteEntrypoint(entrypoint string) {
d.WriteString(fmt.Sprintf("ENTRYPOINT %s\n", entrypoint))
}

238
pkg/build/images.go Normal file
View file

@ -0,0 +1,238 @@
package build
type image struct {
// default ports the service will run on.
// for example, 3306 for mysql. Note that a service
// may expose multiple prots, for example, Riak
// exposes 8087 and 8089.
Ports []string
// tag of the docker image to pull in order
// to run this service.
Tag string
// display name of the image type
Name string
}
// List of 3rd party services (database, queue, etc) that
// are known to work with this Build utility.
var services = map[string]*image{
// neo4j
"neo4j": {
Ports: []string{"7474"},
Tag: "bradrydzewski/neo4j:1.9",
Name: "neo4j",
},
"neo4j:1.9": {
Ports: []string{"7474"},
Tag: "bradrydzewski/neo4j:1.9",
Name: "neo4j",
},
// elasticsearch servers
"elasticsearch": {
Ports: []string{"9200"},
Tag: "bradrydzewski/elasticsearch:0.90",
Name: "elasticsearch",
},
"elasticsearch:0.20": {
Ports: []string{"9200"},
Tag: "bradrydzewski/elasticsearch:0.20",
Name: "elasticsearch",
},
"elasticsearch:0.90": {
Ports: []string{"9200"},
Tag: "bradrydzewski/elasticsearch:0.90",
Name: "elasticsearch",
},
// redis servers
"redis": {
Ports: []string{"6379"},
Tag: "bradrydzewski/redis:2.8",
Name: "redis",
},
"redis:2.8": {
Ports: []string{"6379"},
Tag: "bradrydzewski/redis:2.8",
Name: "redis",
},
"redis:2.6": {
Ports: []string{"6379"},
Tag: "bradrydzewski/redis:2.6",
Name: "redis",
},
// mysql servers
"mysql": {
Tag: "bradrydzewski/mysql:5.5",
Ports: []string{"3306"},
Name: "mysql",
},
"mysql:5.5": {
Tag: "bradrydzewski/mysql:5.5",
Ports: []string{"3306"},
Name: "mysql",
},
// memcached
"memcached": {
Ports: []string{"11211"},
Tag: "bradrydzewski/memcached",
Name: "memcached",
},
// mongodb
"mongodb": {
Ports: []string{"27017"},
Tag: "bradrydzewski/mongodb:2.4",
Name: "mongodb",
},
"mongodb:2.4": {
Ports: []string{"27017"},
Tag: "bradrydzewski/mongodb:2.4",
Name: "mongodb",
},
"mongodb:2.2": {
Ports: []string{"27017"},
Tag: "bradrydzewski/mongodb:2.2",
Name: "mongodb",
},
// postgres
"postgres": {
Ports: []string{"5432"},
Tag: "bradrydzewski/postgres:9.1",
Name: "postgres",
},
"postgres:9.1": {
Ports: []string{"5432"},
Tag: "bradrydzewski/postgres:9.1",
Name: "postgres",
},
// couchdb
"couchdb": {
Ports: []string{"5984"},
Tag: "bradrydzewski/couchdb:1.0",
Name: "couchdb",
},
"couchdb:1.0": {
Ports: []string{"5984"},
Tag: "bradrydzewski/couchdb:1.0",
Name: "couchdb",
},
"couchdb:1.4": {
Ports: []string{"5984"},
Tag: "bradrydzewski/couchdb:1.4",
Name: "couchdb",
},
"couchdb:1.5": {
Ports: []string{"5984"},
Tag: "bradrydzewski/couchdb:1.5",
Name: "couchdb",
},
// rabbitmq
"rabbitmq": {
Ports: []string{"5672", "15672"},
Tag: "bradrydzewski/rabbitmq:3.2",
Name: "rabbitmq",
},
"rabbitmq:3.2": {
Ports: []string{"5672", "15672"},
Tag: "bradrydzewski/rabbitmq:3.2",
Name: "rabbitmq",
},
// experimental images from 3rd parties
"zookeeper": {
Ports: []string{"2181"},
Tag: "jplock/zookeeper:3.4.5",
Name: "zookeeper",
},
// cassandra
"cassandra": {
Ports: []string{"9042", "7000", "7001", "7199", "9160", "49183"},
Tag: "relateiq/cassandra",
Name: "cassandra",
},
// riak - TESTED
"riak": {
Ports: []string{"8087", "8098"},
Tag: "guillermo/riak",
Name: "riak",
},
}
// List of official Drone build images.
var builders = map[string]*image{
// Clojure build images
"lein": {Tag: "bradrydzewski/lein"},
// Dart build images
"dart": {Tag: "bradrydzewski/dart:stable"},
"dart_stable": {Tag: "bradrydzewski/dart:stable"},
"dart_dev": {Tag: "bradrydzewski/dart:dev"},
// Erlang build images
"erlang": {Tag: "bradrydzewski/erlang:R16B02"},
"erlangR16B": {Tag: "bradrydzewski/erlang:R16B"},
"erlangR16B02": {Tag: "bradrydzewski/erlang:R16B02"},
"erlangR16B01": {Tag: "bradrydzewski/erlang:R16B01"},
// GCC build images
"gcc": {Tag: "bradrydzewski/gcc:4.6"},
"gcc4.6": {Tag: "bradrydzewski/gcc:4.6"},
"gcc4.8": {Tag: "bradrydzewski/gcc:4.8"},
// Golang build images
"go": {Tag: "bradrydzewski/go:1.2"},
"go1": {Tag: "bradrydzewski/go:1.0"},
"go1.1": {Tag: "bradrydzewski/go:1.1"},
"go1.2": {Tag: "bradrydzewski/go:1.2"},
// Haskell build images
"haskell": {Tag: "bradrydzewski/haskell:7.4"},
"haskell7.4": {Tag: "bradrydzewski/haskell:7.4"},
// Java build images
"java": {Tag: "bradrydzewski/java:openjdk7"},
"openjdk6": {Tag: "bradrydzewski/java:openjdk6"},
"openjdk7": {Tag: "bradrydzewski/java:openjdk7"},
"oraclejdk7": {Tag: "bradrydzewski/java:oraclejdk7"},
"oraclejdk8": {Tag: "bradrydzewski/java:oraclejdk8"},
// Node build images
"node": {Tag: "bradrydzewski/node:0.10"},
"node0.10": {Tag: "bradrydzewski/node:0.10"},
"node0.8": {Tag: "bradrydzewski/node:0.8"},
// PHP build images
"php": {Tag: "bradrydzewski/php:5.5"},
"php5.5": {Tag: "bradrydzewski/php:5.5"},
"php5.4": {Tag: "bradrydzewski/php:5.4"},
// Python build images
"python": {Tag: "bradrydzewski/python:2.7"},
"python2.7": {Tag: "bradrydzewski/python:2.7"},
"python3.2": {Tag: "bradrydzewski/python:3.2"},
"python3.3": {Tag: "bradrydzewski/python:3.3"},
"pypy": {Tag: "bradrydzewski/python:pypy"},
// Ruby build images
"ruby": {Tag: "bradrydzewski/ruby:2.0.0"},
"ruby2.0.0": {Tag: "bradrydzewski/ruby:2.0.0"},
"ruby1.9.3": {Tag: "bradrydzewski/ruby:1.9.3"},
// Scala build images
"scala": {Tag: "bradrydzewski/scala:2.10.3"},
"scala2.10.3": {Tag: "bradrydzewski/scala:2.10.3"},
"scala2.9.3": {Tag: "bradrydzewski/scala:2.9.3"},
}

105
pkg/build/log/log.go Normal file
View file

@ -0,0 +1,105 @@
package log
import (
"fmt"
"io"
"os"
"sync"
)
const (
LOG_EMERG = iota
LOG_ALERT
LOG_CRIT
LOG_ERR
LOG_WARNING
LOG_NOTICE
LOG_INFO
LOG_DEBUG
)
var mu sync.Mutex
// the default Log priority
var priority int = LOG_DEBUG
// the default Log output destination
var output io.Writer = os.Stdout
// the log prefix
var prefix string
// the log suffix
var suffix string = "/n"
// SetPriority sets the default log level.
func SetPriority(level int) {
mu.Lock()
defer mu.Unlock()
priority = level
}
// SetOutput sets the output destination.
func SetOutput(w io.Writer) {
mu.Lock()
defer mu.Unlock()
output = w
}
// SetPrefix sets the prefix for the log message.
func SetPrefix(pre string) {
mu.Lock()
defer mu.Unlock()
prefix = pre
}
// SetSuffix sets the suffix for the log message.
func SetSuffix(suf string) {
mu.Lock()
defer mu.Unlock()
suffix = suf
}
func Write(out string, level int) {
mu.Lock()
defer mu.Unlock()
// append the prefix and suffix
out = prefix + out + suffix
if priority >= level {
output.Write([]byte(out))
}
}
func Debug(out string) {
Write(out, LOG_DEBUG)
}
func Debugf(format string, a ...interface{}) {
Debug(fmt.Sprintf(format, a...))
}
func Info(out string) {
Write(out, LOG_INFO)
}
func Infof(format string, a ...interface{}) {
Info(fmt.Sprintf(format, a...))
}
func Err(out string) {
Write(out, LOG_ERR)
}
func Errf(format string, a ...interface{}) {
Err(fmt.Sprintf(format, a...))
}
func Notice(out string) {
Write(out, LOG_NOTICE)
}
func Noticef(format string, a ...interface{}) {
Notice(fmt.Sprintf(format, a...))
}

41
pkg/build/proxy/proxy.go Normal file
View file

@ -0,0 +1,41 @@
package proxy
import (
"bytes"
"fmt"
)
// bash header
const header = "#!/bin/bash\n"
// this command string will check if the socat utility
// exists, and if it does, will proxy connections to
// the external IP address.
const command = "[ -x /usr/bin/socat ] && socat TCP-LISTEN:%s,fork TCP:%s:%s &\n"
// Proxy stores proxy configuration details mapping
// a local port to an external IP address with the
// same port number.
type Proxy map[string]string
func (p Proxy) Set(port, ip string) {
p[port] = ip
}
// String converts the proxy configuration details
// to a bash script.
func (p Proxy) String() string {
var buf bytes.Buffer
buf.WriteString(header)
for port, ip := range p {
buf.WriteString(fmt.Sprintf(command, port, ip, port))
}
return buf.String()
}
// Bytes converts the proxy configuration details
// to a bash script in byte array format.
func (p Proxy) Bytes() []byte {
return []byte(p.String())
}

View file

@ -0,0 +1,32 @@
package proxy
import (
"testing"
)
func TestProxy(t *testing.T) {
// test creating a proxy with a few different
// addresses, and our ability to create the
// proxy shell script.
p := Proxy{}
p.Set("8080", "172.1.4.5")
p.Set("8000", "172.1.3.1")
b := p.Bytes()
expected := `#!/bin/bash
[ -x /usr/bin/socat ] && socat TCP-LISTEN:8080,fork TCP:172.1.4.5:8080 &
[ -x /usr/bin/socat ] && socat TCP-LISTEN:8000,fork TCP:172.1.3.1:8000 &
`
if string(b) != expected {
t.Errorf("Invalid proxy \n%s", expected)
}
// test creating a proxy script when there
// are no proxy addresses added to the map
p = Proxy{}
b = p.Bytes()
expected = "#!/bin/bash\n"
if string(b) != expected {
t.Errorf("Invalid proxy \n%s", expected)
}
}

118
pkg/build/repo/repo.go Normal file
View file

@ -0,0 +1,118 @@
package repo
import (
"fmt"
"strings"
)
type Repo struct {
// The path of the Repoisotry. This could be
// the remote path of a Git repository or the path of
// of the repository on the local file system.
//
// A remote path must start with http://, https://,
// git://, ssh:// or git@. Otherwise we'll assume
// the repository is located on the local filesystem.
Path string
// (optional) Specific Branch that we should checkout
// when the Repository is cloned. If no value is
// provided we'll assume the default, master branch.
Branch string
// (optional) Specific Commit Hash that we should
// checkout when the Repository is cloned. If no
// value is provided we'll assume HEAD.
Commit string
// (optional) Pull Request number that we should
// checkout when the Repository is cloned.
PR string
// (optional) The filesystem path that the repository
// will be cloned into (or copied to) inside the
// host system (Docker Container).
Dir string
}
// IsRemote returns true if the Repository is located
// on a remote server (ie Github, Bitbucket)
func (r *Repo) IsRemote() bool {
switch {
case strings.HasPrefix(r.Path, "git://"):
return true
case strings.HasPrefix(r.Path, "git@"):
return true
case strings.HasPrefix(r.Path, "http://"):
return true
case strings.HasPrefix(r.Path, "https://"):
return true
case strings.HasPrefix(r.Path, "ssh://"):
return true
}
return false
}
// IsLocal returns true if the Repository is located
// on the local filesystem.
func (r *Repo) IsLocal() bool {
return !r.IsRemote()
}
// IsGit returns true if the Repository is
// a Git repoisitory.
func (r *Repo) IsGit() bool {
switch {
case strings.HasPrefix(r.Path, "git://"):
return true
case strings.HasPrefix(r.Path, "git@"):
return true
case strings.HasPrefix(r.Path, "ssh://git@"):
return true
case strings.HasPrefix(r.Path, "https://github.com/"):
return true
case strings.HasPrefix(r.Path, "http://github.com"):
return true
case strings.HasSuffix(r.Path, ".git"):
return true
}
// we could also ping the repository to check
return false
}
// returns commands that can be used in a Dockerfile
// to clone the repository.
//
// TODO we should also enable Mercurial projects and SVN projects
func (r *Repo) Commands() []string {
// get the branch. default to master
// if no branch exists.
branch := r.Branch
if len(branch) == 0 {
branch = "master"
}
cmds := []string{}
cmds = append(cmds, fmt.Sprintf("git clone --branch=%s %s %s", branch, r.Path, r.Dir))
switch {
// if a specific commit is provided then we'll
// need to clone it.
case len(r.PR) > 0:
cmds = append(cmds, fmt.Sprintf("git fetch origin +refs/pull/%s/head:refs/remotes/origin/pr/%s", r.PR, r.PR))
cmds = append(cmds, fmt.Sprintf("git checkout -qf pr/%s", r.PR))
//cmds = append(cmds, fmt.Sprintf("git fetch origin +refs/pull/%s/merge:", r.PR))
//cmds = append(cmds, fmt.Sprintf("git checkout -qf %s", "FETCH_HEAD"))
// if a specific commit is provided then we'll
// need to clone it.
case len(r.Commit) > 0:
cmds = append(cmds, fmt.Sprintf("git checkout -qf %s", r.Commit))
}
return cmds
}

View file

@ -0,0 +1,54 @@
package repo
import (
"testing"
)
func TestIsRemote(t *testing.T) {
repos := []struct {
path string
remote bool
}{
{"git://github.com/foo/far", true},
{"git://github.com/foo/far.git", true},
{"git@github.com:foo/far", true},
{"git@github.com:foo/far.git", true},
{"http://github.com/foo/far.git", true},
{"https://github.com/foo/far.git", true},
{"ssh://baz.com/foo/far.git", true},
{"/var/lib/src", false},
{"/home/ubuntu/src", false},
{"src", false},
}
for _, r := range repos {
repo := Repo{Path: r.path}
if remote := repo.IsRemote(); remote != r.remote {
t.Errorf("IsRemote %s was %v, expected %v", r.path, remote, r.remote)
}
}
}
func TestIsGit(t *testing.T) {
repos := []struct {
path string
remote bool
}{
{"git://github.com/foo/far", true},
{"git://github.com/foo/far.git", true},
{"git@github.com:foo/far", true},
{"git@github.com:foo/far.git", true},
{"http://github.com/foo/far.git", true},
{"https://github.com/foo/far.git", true},
{"ssh://baz.com/foo/far.git", true},
{"svn://gcc.gnu.org/svn/gcc/branches/gccgo", false},
{"https://code.google.com/p/go", false},
}
for _, r := range repos {
repo := Repo{Path: r.path}
if remote := repo.IsGit(); remote != r.remote {
t.Errorf("IsGit %s was %v, expected %v", r.path, remote, r.remote)
}
}
}

View file

@ -0,0 +1,12 @@
package deployment
import (
"github.com/drone/drone/pkg/build/buildfile"
)
type AppFog struct {
}
func (a *AppFog) Write(f *buildfile.Buildfile) {
}

View file

@ -0,0 +1,12 @@
package deployment
import (
"github.com/drone/drone/pkg/build/buildfile"
)
type CloudControl struct {
}
func (c *CloudControl) Write(f *buildfile.Buildfile) {
}

View file

@ -0,0 +1,12 @@
package deployment
import (
"github.com/drone/drone/pkg/build/buildfile"
)
type CloudFoundry struct {
}
func (c *CloudFoundry) Write(f *buildfile.Buildfile) {
}

View file

@ -0,0 +1,42 @@
package deployment
import (
"github.com/drone/drone/pkg/build/buildfile"
)
// Deploy stores the configuration details
// for deploying build artifacts when
// a Build has succeeded
type Deploy struct {
AppFog *AppFog `yaml:"appfog,omitempty"`
CloudControl *CloudControl `yaml:"cloudcontrol,omitempty"`
CloudFoundry *CloudFoundry `yaml:"cloudfoundry,omitempty"`
EngineYard *EngineYard `yaml:"engineyard,omitempty"`
Heroku *Heroku `yaml:"heroku,omitempty"`
Nodejitsu *Nodejitsu `yaml:"nodejitsu,omitempty"`
Openshift *Openshift `yaml:"openshift,omitempty"`
}
func (d *Deploy) Write(f *buildfile.Buildfile) {
if d.AppFog != nil {
d.AppFog.Write(f)
}
if d.CloudControl != nil {
d.CloudControl.Write(f)
}
if d.CloudFoundry != nil {
d.CloudFoundry.Write(f)
}
if d.EngineYard != nil {
d.EngineYard.Write(f)
}
if d.Heroku != nil {
d.Heroku.Write(f)
}
if d.Nodejitsu != nil {
d.Nodejitsu.Write(f)
}
if d.Openshift != nil {
d.Openshift.Write(f)
}
}

View file

@ -0,0 +1,12 @@
package deployment
import (
"github.com/drone/drone/pkg/build/buildfile"
)
type EngineYard struct {
}
func (e *EngineYard) Write(f *buildfile.Buildfile) {
}

View file

@ -0,0 +1 @@
package deployment

View file

@ -0,0 +1,38 @@
package deployment
import (
"fmt"
"github.com/drone/drone/pkg/build/buildfile"
)
type Heroku struct {
App string `yaml:"app,omitempty"`
Force bool `yaml:"force,omitempty"`
Branch string `yaml:"branch,omitempty"`
}
func (h *Heroku) Write(f *buildfile.Buildfile) {
// get the current commit hash
f.WriteCmdSilent("COMMIT=$(git rev-parse HEAD)")
// set the git user and email based on the individual
// that made the commit.
f.WriteCmdSilent("git config --global user.name $(git --no-pager log -1 --pretty=format:'%an')")
f.WriteCmdSilent("git config --global user.email $(git --no-pager log -1 --pretty=format:'%ae')")
// add heroku as a git remote
f.WriteCmd(fmt.Sprintf("git remote add heroku git@heroku.com:%s.git", h.App))
switch h.Force {
case true:
// this is useful when the there are artifacts generated
// by the build script, such as less files converted to css,
// that need to be deployed to Heroku.
f.WriteCmd(fmt.Sprintf("git add -A"))
f.WriteCmd(fmt.Sprintf("git commit -m 'adding build artifacts'"))
f.WriteCmd(fmt.Sprintf("git push heroku $COMMIT:master --force"))
case false:
// otherwise we just do a standard git push
f.WriteCmd(fmt.Sprintf("git push heroku $COMMIT:master"))
}
}

View file

@ -0,0 +1,12 @@
package deployment
import (
"github.com/drone/drone/pkg/build/buildfile"
)
type Nodejitsu struct {
}
func (n *Nodejitsu) Write(f *buildfile.Buildfile) {
}

View file

@ -0,0 +1,12 @@
package deployment
import (
"github.com/drone/drone/pkg/build/buildfile"
)
type Openshift struct {
}
func (o *Openshift) Write(f *buildfile.Buildfile) {
}

View file

@ -0,0 +1 @@
package deployment

View file

@ -0,0 +1,85 @@
package notification
import (
"fmt"
"net/smtp"
)
type Email struct {
Recipients []string `yaml:"recipients,omitempty"`
Success string `yaml:"on_success"`
Failure string `yaml:"on_failure"`
host string // smtp host address
port string // smtp host port
user string // smtp username for authentication
pass string // smtp password for authentication
from string // smtp email address. send from this address
}
// SetServer is a function that will set the SMTP
// server location and credentials
func (e *Email) SetServer(host, port, user, pass, from string) {
e.host = host
e.port = port
e.user = user
e.pass = pass
e.from = from
}
// Send will send an email, either success or failure,
// based on the Commit Status.
func (e *Email) Send(context *Context) error {
switch {
case context.Commit.Status == "Success" && e.Success != "never":
return e.sendSuccess(context)
case context.Commit.Status == "Failure" && e.Failure != "never":
return e.sendFailure(context)
}
return nil
}
// sendFailure sends email notifications to the list of
// recipients indicating the build failed.
func (e *Email) sendFailure(context *Context) error {
// loop through and email recipients
/*for _, email := range e.Recipients {
if err := mail.SendFailure(context.Repo.Slug, email, context); err != nil {
return err
}
}*/
return nil
}
// sendSuccess sends email notifications to the list of
// recipients indicating the build was a success.
func (e *Email) sendSuccess(context *Context) error {
// loop through and email recipients
/*for _, email := range e.Recipients {
if err := mail.SendSuccess(context.Repo.Slug, email, context); err != nil {
return err
}
}*/
return nil
}
// send is a simple helper function to format and
// send an email message.
func (e *Email) send(to, subject, body string) error {
// Format the raw email message body
raw := fmt.Sprintf(emailTemplate, e.from, to, subject, body)
auth := smtp.PlainAuth("", e.user, e.pass, e.host)
addr := fmt.Sprintf("%s:%s", e.host, e.port)
return smtp.SendMail(addr, auth, e.from, []string{to}, []byte(raw))
}
// text-template used to generate a raw Email message
var emailTemplate = `From: %s
To: %s
Subject: %s
MIME-version: 1.0
Content-Type: text/html; charset="UTF-8"
%s`

View file

@ -0,0 +1,64 @@
package notification
import (
"fmt"
"github.com/andybons/hipchat"
)
const (
startedMessage = "Building %s, commit %s, author %s"
successMessage = "<b>Success</b> %s, commit %s, author %s"
failureMessage = "<b>Failed</b> %s, commit %s, author %s"
)
type Hipchat struct {
Room string `yaml:"room,omitempty"`
Token string `yaml:"token,omitempty"`
Started bool `yaml:"on_started,omitempty"`
Success bool `yaml:"on_success,omitempty"`
Failure bool `yaml:"on_failure,omitempty"`
}
func (h *Hipchat) Send(context *Context) error {
switch {
case context.Commit.Status == "Started" && h.Started:
return h.sendStarted(context)
case context.Commit.Status == "Success" && h.Success:
return h.sendSuccess(context)
case context.Commit.Status == "Failure" && h.Failure:
return h.sendFailure(context)
}
return nil
}
func (h *Hipchat) sendStarted(context *Context) error {
msg := fmt.Sprintf(startedMessage, context.Repo.Name, context.Commit.HashShort(), context.Commit.Author)
return h.send(hipchat.ColorYellow, hipchat.FormatHTML, msg)
}
func (h *Hipchat) sendFailure(context *Context) error {
msg := fmt.Sprintf(failureMessage, context.Repo.Name, context.Commit.HashShort(), context.Commit.Author)
return h.send(hipchat.ColorRed, hipchat.FormatHTML, msg)
}
func (h *Hipchat) sendSuccess(context *Context) error {
msg := fmt.Sprintf(successMessage, context.Repo.Name, context.Commit.HashShort(), context.Commit.Author)
return h.send(hipchat.ColorGreen, hipchat.FormatHTML, msg)
}
// helper function to send Hipchat requests
func (h *Hipchat) send(color, format, message string) error {
c := hipchat.Client{AuthToken: h.Token}
req := hipchat.MessageRequest{
RoomId: h.Room,
From: "Drone",
Message: message,
Color: color,
MessageFormat: format,
Notify: true,
}
return c.PostMessage(req)
}

View file

@ -0,0 +1 @@
package notification

View file

@ -0,0 +1,53 @@
package notification
import (
"github.com/drone/drone/pkg/model"
)
// Context represents the context of an
// in-progress build request.
type Context struct {
// Global settings
Host string
// User that owns the repository
User *model.User
// Repository being built.
Repo *model.Repo
// Commit being built
Commit *model.Commit
}
type Sender interface {
Send(context *Context) error
}
// Notification stores the configuration details
// for notifying a user, or group of users,
// when their Build has completed.
type Notification struct {
Email *Email `yaml:"email,omitempty"`
Webhook *Webhook `yaml:"webhook,omitempty"`
Hipchat *Hipchat `yaml:"hipchat,omitempty"`
}
func (n *Notification) Send(context *Context) error {
// send email notifications
//if n.Email != nil && n.Email.Enabled {
// n.Email.Send(context)
//}
// send email notifications
if n.Webhook != nil {
n.Webhook.Send(context)
}
// send email notifications
if n.Hipchat != nil {
n.Hipchat.Send(context)
}
return nil
}

View file

@ -0,0 +1,59 @@
package notification
import (
"bytes"
"encoding/json"
"net/http"
"github.com/drone/drone/pkg/model"
)
type Webhook struct {
URL []string `yaml:"urls,omitempty"`
Success bool `yaml:"on_success,omitempty"`
Failure bool `yaml:"on_failure,omitempty"`
}
func (w *Webhook) Send(context *Context) error {
switch {
case context.Commit.Status == "Success" && w.Success:
return w.send(context)
case context.Commit.Status == "Failure" && w.Failure:
return w.send(context)
}
return nil
}
// helper function to send HTTP requests
func (w *Webhook) send(context *Context) error {
// data will get posted in this format
data := struct {
Owner *model.User `json:"owner"`
Repo *model.Repo `json:"repository"`
Commit *model.Commit `json:"commit"`
}{context.User, context.Repo, context.Commit}
// data json encoded
payload, err := json.Marshal(data)
if err != nil {
return err
}
// loop through and email recipients
for _, url := range w.URL {
go sendJson(url, payload)
}
return nil
}
// helper fuction to sent HTTP Post requests
// with JSON data as the payload.
func sendJson(url string, payload []byte) {
buf := bytes.NewBuffer(payload)
resp, err := http.Post(url, "application/json", buf)
if err != nil {
return
}
resp.Body.Close()
}

View file

@ -0,0 +1 @@
package notification

View file

@ -0,0 +1 @@
package publish

View file

@ -0,0 +1 @@
package publish

View file

@ -0,0 +1 @@
package publish

View file

@ -0,0 +1 @@
package publish

View file

@ -0,0 +1 @@
package publish

View file

@ -0,0 +1 @@
package publish

View file

@ -0,0 +1,18 @@
package publish
import (
"github.com/drone/drone/pkg/build/buildfile"
)
// Publish stores the configuration details
// for publishing build artifacts when
// a Build has succeeded
type Publish struct {
S3 *S3 `yaml:"s3,omitempty"`
}
func (p *Publish) Write(f *buildfile.Buildfile) {
if p.S3 != nil {
p.S3.Write(f)
}
}

View file

@ -0,0 +1,2 @@
package publish

View file

@ -0,0 +1,85 @@
package publish
import (
"fmt"
"strings"
"github.com/drone/drone/pkg/build/buildfile"
)
type S3 struct {
Key string `yaml:"access_key,omitempty"`
Secret string `yaml:"secret_key,omitempty"`
Bucket string `yaml:"bucket,omitempty"`
// us-east-1
// us-west-1
// us-west-2
// eu-west-1
// ap-southeast-1
// ap-southeast-2
// ap-northeast-1
// sa-east-1
Region string `yaml:"region,omitempty"`
// Indicates the files ACL, which should be one
// of the following:
// private
// public-read
// public-read-write
// authenticated-read
// bucket-owner-read
// bucket-owner-full-control
Access string `yaml:"acl,omitempty"`
// Copies the files from the specified directory.
// Regexp matching will apply to match multiple
// files
//
// Examples:
// /path/to/file
// /path/to/*.txt
// /path/to/*/*.txt
// /path/to/**
Source string `yaml:"source,omitempty"`
Target string `yaml:"target,omitempty"`
// Recursive uploads
Recursive bool `yaml:"recursive"`
Branch string `yaml:"branch,omitempty"`
}
func (s *S3) Write(f *buildfile.Buildfile) {
// install the AWS cli using PIP
f.WriteCmdSilent("[ -f /usr/bin/sudo ] || pip install awscli 1> /dev/null 2> /dev/null")
f.WriteCmdSilent("[ -f /usr/bin/sudo ] && sudo pip install awscli 1> /dev/null 2> /dev/null")
f.WriteEnv("AWS_ACCESS_KEY_ID", s.Key)
f.WriteEnv("AWS_SECRET_ACCESS_KEY", s.Secret)
// make sure a default region is set
if len(s.Region) == 0 {
s.Region = "us-east-1"
}
// make sure a default access is set
// let's be conservative and assume private
if len(s.Region) == 0 {
s.Region = "private"
}
// if the target starts with a "/" we need
// to remove it, otherwise we might adding
// a 3rd slash to s3://
if strings.HasPrefix(s.Target, "/") {
s.Target = s.Target[1:]
}
switch s.Recursive {
case true:
f.WriteCmd(fmt.Sprintf(`aws s3 cp %s s3://%s/%s --recursive --acl %s --region %s`, s.Source, s.Bucket, s.Target, s.Access, s.Region))
case false:
f.WriteCmd(fmt.Sprintf(`aws s3 cp %s s3://%s/%s --acl %s --region %s`, s.Source, s.Bucket, s.Target, s.Access, s.Region))
}
}

View file

@ -0,0 +1,5 @@
cobertura.go
coveralls.go
gocov.go
junit.go
phpunit.go

123
pkg/build/script/script.go Normal file
View file

@ -0,0 +1,123 @@
package script
import (
"io/ioutil"
"strings"
"launchpad.net/goyaml"
"github.com/drone/drone/pkg/build/buildfile"
"github.com/drone/drone/pkg/build/script/deployment"
"github.com/drone/drone/pkg/build/script/notification"
"github.com/drone/drone/pkg/build/script/publish"
)
func ParseBuild(data []byte) (*Build, error) {
build := Build{}
// parse the build configuration file
err := goyaml.Unmarshal(data, &build)
return &build, err
}
func ParseBuildFile(filename string) (*Build, error) {
data, err := ioutil.ReadFile(filename)
if err != nil {
return nil, err
}
return ParseBuild(data)
}
// Build stores the configuration details for
// building, testing and deploying code.
type Build struct {
// Image specifies the Docker Image that will be
// used to virtualize the Build process.
Image string
// Name specifies a user-defined label used
// to identify the build.
Name string
// Script specifies the build and test commands.
Script []string
// Env specifies the environment of the build.
Env []string
// Services specifies external services, such as
// database or messaging queues, that should be
// linked to the build environment.
Services []string
Deploy *deployment.Deploy `yaml:"deploy,omitempty"`
Publish *publish.Publish `yaml:"publish,omitempty"`
Notifications *notification.Notification `yaml:"notify,omitempty"`
}
// Write adds all the steps to the build script, including
// build commands, deploy and publish commands.
func (b *Build) Write(f *buildfile.Buildfile) {
// append build commands
b.WriteBuild(f)
// write publish commands
if b.Publish != nil {
b.Publish.Write(f)
}
// write deployment commands
if b.Deploy != nil {
b.Deploy.Write(f)
}
}
// WriteBuild adds only the build steps to the build script,
// omitting publish and deploy steps. This is important for
// pull requests, where deployment would be undesirable.
func (b *Build) WriteBuild(f *buildfile.Buildfile) {
// append environment variables
for _, env := range b.Env {
parts := strings.Split(env, "=")
if len(parts) != 2 {
continue
}
f.WriteEnv(parts[0], parts[1])
}
// append build commands
for _, cmd := range b.Script {
f.WriteCmd(cmd)
}
}
type Publish interface {
Write(f *buildfile.Buildfile)
}
type Deployment interface {
Write(f *buildfile.Buildfile)
}
type Notification interface {
Set(c Context)
}
type Context interface {
Host() string
Owner() string
Name() string
Branch() string
Hash() string
Status() string
Message() string
Author() string
Gravatar() string
Duration() int64
HumanDuration() string
//Settings
}

28
pkg/build/util.go Normal file
View file

@ -0,0 +1,28 @@
package build
import (
"crypto/rand"
"crypto/sha1"
"fmt"
"io"
)
// createUID is a helper function that will
// create a random, unique identifier.
func createUID() string {
c := sha1.New()
r := createRandom()
io.WriteString(c, string(r))
s := fmt.Sprintf("%x", c.Sum(nil))
return "drone-" + s[0:10]
}
// createRandom creates a random block of bytes
// that we can use to generate unique identifiers.
func createRandom() []byte {
k := make([]byte, sha1.BlockSize)
if _, err := io.ReadFull(rand.Reader, k); err != nil {
return nil
}
return k
}

57
pkg/build/writer.go Normal file
View file

@ -0,0 +1,57 @@
package build
import (
//"bytes"
"fmt"
"io"
"strings"
)
var (
// the prefix used to determine if this is
// data that should be stripped from the output
prefix = []byte("#DRONE:")
)
// custom writer to intercept the build
// output
type writer struct {
io.Writer
}
// Write appends the contents of p to the buffer. It will
// scan for DRONE special formatting codes embedded in the
// output, and will alter the output accordingly.
func (w *writer) Write(p []byte) (n int, err error) {
lines := strings.Split(string(p), "\n")
for i, line := range lines {
if strings.HasPrefix(line, "#DRONE:") {
var cmd string
// extract the command (base16 encoded)
// from the output
fmt.Sscanf(line[7:], "%x", &cmd)
// echo the decoded command
cmd = fmt.Sprintf("$ %s", cmd)
w.Writer.Write([]byte(cmd))
} else {
w.Writer.Write([]byte(line))
}
if i < len(lines)-1 {
w.Writer.Write([]byte("\n"))
}
}
return len(p), nil
}
// WriteString appends the contents of s to the buffer.
func (w *writer) WriteString(s string) (n int, err error) {
return w.Write([]byte(s))
}

27
pkg/build/writer_test.go Normal file
View file

@ -0,0 +1,27 @@
package build
import (
"bytes"
"testing"
)
func TestSetupDockerfile(t *testing.T) {
var buf bytes.Buffer
// wrap the buffer so we can analyze output
w := writer{&buf}
w.WriteString("#DRONE:676f206275696c64\n")
w.WriteString("#DRONE:676f2074657374202d76\n")
w.WriteString("PASS\n")
w.WriteString("ok github.com/garyburd/redigo/redis 0.113s\n")
expected := `$ go build
$ go test -v
PASS
ok github.com/garyburd/redigo/redis 0.113s
`
if expected != buf.String() {
t.Errorf("Expected commands decoded and echoed correctly. got \n%s", buf.String())
}
}

157
pkg/channel/channel.go Normal file
View file

@ -0,0 +1,157 @@
package channel
import (
"crypto/rand"
"encoding/json"
"fmt"
"io"
"time"
"code.google.com/p/go.net/websocket"
"github.com/dchest/authcookie"
)
// secret key used to generate tokens
var secret = make([]byte, 32)
func init() {
// generate the secret key by reading
// from crypto/random
if _, err := io.ReadFull(rand.Reader, secret); err != nil {
panic(err)
}
}
// Create will generate a token and create a new
// channel over which messages will be sent.
func Create(name string) string {
mu.Lock()
defer mu.Unlock()
if _, ok := hubs[name]; !ok {
hub := newHub(false, true)
hubs[name] = hub
go hub.run()
}
return authcookie.NewSinceNow(name, 24*time.Hour, secret)
}
// CreateStream will generate a token and create a new
// channel over which messages streams (ie build output)
// are sent.
func CreateStream(name string) string {
mu.Lock()
defer mu.Unlock()
if _, ok := hubs[name]; !ok {
hub := newHub(true, false)
hubs[name] = hub
go hub.run()
}
return authcookie.NewSinceNow(name, 24*time.Hour, secret)
}
// Token will generate a token, but will not create
// a new channel.
func Token(name string) string {
return authcookie.NewSinceNow(name, 24*time.Hour, secret)
}
// Send sends a message on the named channel.
func Send(name string, message string) error {
return SendBytes(name, []byte(message))
}
// SendJSON sends a JSON-encoded value on
// the named channel.
func SendJSON(name string, value interface{}) error {
m, err := json.Marshal(value)
if err != nil {
return err
}
return SendBytes(name, m)
}
// SendBytes send a message in byte format on
// the named channel.
func SendBytes(name string, value []byte) error {
// get the hub for the specified channel name
mu.RLock()
hub, ok := hubs[name]
mu.RUnlock()
if !ok {
return fmt.Errorf("channel does not exist")
}
go hub.Write(value)
return nil
}
func Read(ws *websocket.Conn) {
// get the name from the request
hash := ws.Request().FormValue("token")
// get the hash of the token
name := authcookie.Login(hash, secret)
// get the hub for the specified channel name
mu.RLock()
hub, ok := hubs[name]
mu.RUnlock()
// if hub not found, exit
if !ok {
ws.Close()
return
}
// internal representation of a connection
// maximum queue of 100000 messages
conn := &connection{
send: make(chan string, 100000),
ws: ws,
}
// register the connection with the hub
hub.register <- conn
defer func() {
go func() {
hub.unregister <- conn
}()
closed := <-hub.closed
// this will remove the hub when the connection is
// closed if the
if hub.autoClose && closed {
mu.Lock()
delete(hubs, name)
mu.Unlock()
}
}()
go conn.writer()
conn.reader()
}
func Close(name string) {
// get the hub for the specified channel name
mu.RLock()
hub, ok := hubs[name]
mu.RUnlock()
if !ok {
return
}
// close hub connections
hub.Close()
// remove the hub
mu.Lock()
delete(hubs, name)
mu.Unlock()
}

36
pkg/channel/conn.go Normal file
View file

@ -0,0 +1,36 @@
package channel
import (
"code.google.com/p/go.net/websocket"
)
type connection struct {
// The websocket connection.
ws *websocket.Conn
// Buffered channel of outbound messages.
send chan string
}
func (c *connection) reader() {
for {
var message string
err := websocket.Message.Receive(c.ws, &message)
if err != nil {
break
}
}
c.ws.Close()
}
func (c *connection) writer() {
for message := range c.send {
err := websocket.Message.Send(c.ws, message)
if err != nil {
break
}
}
c.ws.Close()
}

133
pkg/channel/hub.go Normal file
View file

@ -0,0 +1,133 @@
package channel
import (
"sync"
)
// mutex to lock access to the
// internal map of hubs.
var mu sync.RWMutex
// a map of hubs. each hub represents a different
// channel that a set of users can listen on. For
// example, we may have a hub to stream build output
// for github.com/foo/bar or a channel to post
// updates for user octocat.
var hubs = map[string]*hub{}
type hub struct {
// Registered connections
connections map[*connection]bool
// Inbound messages from the connections.
broadcast chan string
// Register requests from the connections.
register chan *connection
// Unregister requests from connections.
unregister chan *connection
// Buffer of sent data. This is used mostly
// for build output. A client may connect after
// the build has already started, in which case
// we need to stream them the build history.
history []string
// Send a "shutdown" signal
close chan bool
// Hub responds on this channel letting you know
// if it's active
closed chan bool
// Auto shutdown when last connection removed
autoClose bool
// Send history
sendHistory bool
}
func newHub(sendHistory, autoClose bool) *hub {
h := hub{
broadcast: make(chan string),
register: make(chan *connection),
unregister: make(chan *connection),
connections: make(map[*connection]bool),
history: make([]string, 0), // This should be pre-allocated, but it's not
close: make(chan bool),
autoClose: autoClose,
closed: make(chan bool),
sendHistory: sendHistory,
}
return &h
}
func sendHistory(c *connection, history []string) {
if len(history) > 0 {
for i := range history {
c.send <- history[i]
}
}
}
func (h *hub) run() {
// make sure we don't bring down the application
// if somehow we encounter a nil pointer or some
// other unexpected behavior.
defer func() {
recover()
}()
for {
select {
case c := <-h.register:
h.connections[c] = true
if len(h.history) > 0 {
b := make([]string, len(h.history))
copy(b, h.history)
go sendHistory(c, b)
}
case c := <-h.unregister:
delete(h.connections, c)
close(c.send)
shutdown := h.autoClose && (len(h.connections) == 0)
if shutdown {
h.closed <- shutdown
return
}
h.closed <- shutdown
case m := <-h.broadcast:
if h.sendHistory {
h.history = append(h.history, m)
}
for c := range h.connections {
select {
case c.send <- m:
// do nothing
default:
delete(h.connections, c)
go c.ws.Close()
}
}
case <-h.close:
for c := range h.connections {
delete(h.connections, c)
close(c.send)
}
h.closed <- true
return
}
}
}
func (h *hub) Close() {
h.close <- true
}
func (h *hub) Write(p []byte) (n int, err error) {
h.broadcast <- string(p)
return len(p), nil
}

71
pkg/database/builds.go Normal file
View file

@ -0,0 +1,71 @@
package database
import (
. "github.com/drone/drone/pkg/model"
"github.com/russross/meddler"
)
// Name of the Build table in the database
const buildTable = "builds"
// SQL Queries to retrieve a list of all Commits belonging to a Repo.
const buildStmt = `
SELECT id, commit_id, slug, status, started, finished, duration, created, updated, stdout
FROM builds
WHERE commit_id = ?
ORDER BY slug ASC
`
// SQL Queries to retrieve a Build by id.
const buildFindStmt = `
SELECT id, commit_id, slug, status, started, finished, duration, created, updated, stdout
FROM builds
WHERE id = ?
LIMIT 1
`
// SQL Queries to retrieve a Commit by name and repo id.
const buildFindSlugStmt = `
SELECT id, commit_id, slug, status, started, finished, duration, created, updated, stdout
FROM builds
WHERE slug = ? AND commit_id = ?
LIMIT 1
`
// SQL Queries to delete a Commit.
const buildDeleteStmt = `
DELETE FROM builds WHERE id = ?
`
// Returns the Build with the given ID.
func GetBuild(id int64) (*Build, error) {
build := Build{}
err := meddler.QueryRow(db, &build, buildFindStmt, id)
return &build, err
}
// Returns the Build with the given slug.
func GetBuildSlug(slug string, commit int64) (*Build, error) {
build := Build{}
err := meddler.QueryRow(db, &build, buildFindSlugStmt, slug, commit)
return &build, err
}
// Creates a new Build.
func SaveBuild(build *Build) error {
return meddler.Save(db, buildTable, build)
}
// Deletes an existing Build.
func DeleteBuild(id int64) error {
_, err := db.Exec(buildDeleteStmt, id)
return err
}
// Returns a list of all Builds associated
// with the specified Commit ID and branch.
func ListBuilds(id int64) ([]*Build, error) {
var builds []*Build
err := meddler.QueryAll(db, &builds, buildStmt, id)
return builds, err
}

174
pkg/database/commits.go Normal file
View file

@ -0,0 +1,174 @@
package database
import (
"time"
. "github.com/drone/drone/pkg/model"
"github.com/russross/meddler"
)
// Name of the Commit table in the database
const commitTable = "commits"
// SQL Queries to retrieve a list of all Commits belonging to a Repo.
const commitStmt = `
SELECT id, repo_id, status, started, finished, duration,
hash, branch, pull_request, author, gravatar, timestamp, message, created, updated
FROM commits
WHERE repo_id = ? AND branch = ?
ORDER BY created DESC
LIMIT 10
`
// SQL Queries to retrieve the latest Commit.
const commitLatestStmt = `
SELECT id, repo_id, status, started, finished, duration,
hash, branch, pull_request, author, gravatar, timestamp, message, created, updated
FROM commits
WHERE repo_id = ? AND branch = ?
ORDER BY created DESC
LIMIT 1
`
// SQL Queries to retrieve a Commit by id.
const commitFindStmt = `
SELECT id, repo_id, status, started, finished, duration,
hash, branch, pull_request, author, gravatar, timestamp, message, created, updated
FROM commits
WHERE id = ?
`
// SQL Queries to retrieve a Commit by name and repo id.
const commitFindHashStmt = `
SELECT id, repo_id, status, started, finished, duration,
hash, branch, pull_request, author, gravatar, timestamp, message, created, updated
FROM commits
WHERE hash = ? AND repo_id = ?
LIMIT 1
`
// SQL Query to retrieve a list of recent commits by user.
const userCommitRecentStmt = `
SELECT r.slug, r.host, r.owner, r.name,
c.status, c.started, c.finished, c.duration, c.hash, c.branch, c.pull_request,
c.author, c.gravatar, c.timestamp, c.message, c.created, c.updated
FROM repos r, commits c
WHERE r.user_id = ?
AND r.team_id = 0
AND r.id = c.repo_id
AND c.status IN ('Success', 'Failure')
ORDER BY c.created desc
LIMIT 10
`
// SQL Query to retrieve a list of recent commits by team.
const teamCommitRecentStmt = `
SELECT r.slug, r.host, r.owner, r.name,
c.status, c.started, c.finished, c.duration, c.hash, c.branch, c.pull_request,
c.author, c.gravatar, c.timestamp, c.message, c.created, c.updated
FROM repos r, commits c
WHERE r.team_id = ?
AND r.id = c.repo_id
AND c.status IN ('Success', 'Failure')
ORDER BY c.created desc
LIMIT 10
`
// SQL Queries to delete a Commit.
const commitDeleteStmt = `
DELETE FROM commits WHERE id = ?
`
// SQL Queries to retrieve the latest Commits for each branch.
const commitBranchesStmt = `
SELECT id, repo_id, status, started, finished, duration,
hash, branch, pull_request, author, gravatar, timestamp, message, created, updated
FROM commits
WHERE id IN (
SELECT MAX(id)
FROM commits
WHERE repo_id = ?
GROUP BY branch)
ORDER BY branch ASC
`
// SQL Queries to retrieve the latest Commits for each branch.
const commitBranchStmt = `
SELECT id, repo_id, status, started, finished, duration,
hash, branch, pull_request, author, gravatar, timestamp, message, created, updated
FROM commits
WHERE id IN (
SELECT MAX(id)
FROM commits
WHERE repo_id = ?
AND branch = ?
GROUP BY branch)
LIMIT 1
`
// Returns the Commit with the given ID.
func GetCommit(id int64) (*Commit, error) {
commit := Commit{}
err := meddler.QueryRow(db, &commit, commitFindStmt, id)
return &commit, err
}
// Returns the Commit with the given hash.
func GetCommitHash(hash string, repo int64) (*Commit, error) {
commit := Commit{}
err := meddler.QueryRow(db, &commit, commitFindHashStmt, hash, repo)
return &commit, err
}
// Returns the most recent Commit for the given branch.
func GetBranch(repo int64, branch string) (*Commit, error) {
commit := Commit{}
err := meddler.QueryRow(db, &commit, commitBranchStmt, repo, branch)
return &commit, err
}
// Creates a new Commit.
func SaveCommit(commit *Commit) error {
if commit.ID == 0 {
commit.Created = time.Now().UTC()
}
commit.Updated = time.Now().UTC()
return meddler.Save(db, commitTable, commit)
}
// Deletes an existing Commit.
func DeleteCommit(id int64) error {
_, err := db.Exec(commitDeleteStmt, id)
return err
}
// Returns a list of all Commits associated
// with the specified Repo ID.
func ListCommits(repo int64, branch string) ([]*Commit, error) {
var commits []*Commit
err := meddler.QueryAll(db, &commits, commitStmt, repo, branch)
return commits, err
}
// Returns a list of recent Commits associated
// with the specified User ID
func ListCommitsUser(user int64) ([]*RepoCommit, error) {
var commits []*RepoCommit
err := meddler.QueryAll(db, &commits, userCommitRecentStmt, user)
return commits, err
}
// Returns a list of recent Commits associated
// with the specified Team ID
func ListCommitsTeam(team int64) ([]*RepoCommit, error) {
var commits []*RepoCommit
err := meddler.QueryAll(db, &commits, teamCommitRecentStmt, team)
return commits, err
}
// Returns a list of the most recent commits for each branch.
func ListBranches(repo int64) ([]*Commit, error) {
var commits []*Commit
err := meddler.QueryAll(db, &commits, commitBranchesStmt, repo)
return commits, err
}

24
pkg/database/database.go Normal file
View file

@ -0,0 +1,24 @@
package database
import (
"database/sql"
"log"
"github.com/drone/drone/pkg/database/schema"
)
// global instance of our database connection.
var db *sql.DB
// Set sets the default database.
func Set(database *sql.DB) {
// set the global database
db = database
// load the database schema. If this is
// a new database all the tables and
// indexes will be created.
if err := schema.Load(db); err != nil {
log.Fatal(err)
}
}

View file

@ -0,0 +1,133 @@
package encrypt
import (
"bytes"
"crypto/cipher"
"crypto/rand"
"encoding/gob"
"fmt"
"io"
)
// EncryptedField handles encrypted and decryption of
// values to and from database columns.
type EncryptedField struct {
Cipher cipher.Block
}
// PreRead is called before a Scan operation. It is given a pointer to
// the raw struct field, and returns the value that will be given to
// the database driver.
func (e *EncryptedField) PreRead(fieldAddr interface{}) (scanTarget interface{}, err error) {
// give a pointer to a byte buffer to grab the raw data
return new([]byte), nil
}
// PostRead is called after a Scan operation. It is given the value returned
// by PreRead and a pointer to the raw struct field. It is expected to fill
// in the struct field if the two are different.
func (e *EncryptedField) PostRead(fieldAddr interface{}, scanTarget interface{}) error {
ptr := scanTarget.(*[]byte)
if ptr == nil {
return fmt.Errorf("encrypter.PostRead: nil pointer")
}
raw := *ptr
// ignore fields that aren't set at all
if len(raw) == 0 {
return nil
}
// decrypt value for gob decoding
var err error
raw, err = decrypt(e.Cipher, raw)
if err != nil {
return fmt.Errorf("Gob decryption error: %v", err)
}
// decode gob
gobDecoder := gob.NewDecoder(bytes.NewReader(raw))
if err := gobDecoder.Decode(fieldAddr); err != nil {
return fmt.Errorf("Gob decode error: %v", err)
}
return nil
}
// PreWrite is called before an Insert or Update operation. It is given
// a pointer to the raw struct field, and returns the value that will be
// given to the database driver.
func (e *EncryptedField) PreWrite(field interface{}) (saveValue interface{}, err error) {
buffer := new(bytes.Buffer)
// gob encode
gobEncoder := gob.NewEncoder(buffer)
if err := gobEncoder.Encode(field); err != nil {
return nil, fmt.Errorf("Gob encoding error: %v", err)
}
// and then ecrypt
encrypted, err := encrypt(e.Cipher, buffer.Bytes())
if err != nil {
return nil, fmt.Errorf("Gob decryption error: %v", err)
}
return encrypted, nil
}
// encrypt is a helper function to encrypt a slice
// of bytes using the specified block cipher.
func encrypt(block cipher.Block, v []byte) ([]byte, error) {
// if no block cipher value exists we'll assume
// the database is running in non-ecrypted mode.
if block == nil {
return v, nil
}
value := make([]byte, len(v))
copy(value, v)
// Generate a random initialization vector
iv := generateRandomKey(block.BlockSize())
if len(iv) != block.BlockSize() {
return nil, fmt.Errorf("Could not generate a valid initialization vector for encryption")
}
// Encrypt it.
stream := cipher.NewCTR(block, iv)
stream.XORKeyStream(value, value)
// Return iv + ciphertext.
return append(iv, value...), nil
}
// decrypt is a helper function to decrypt a slice
// using the specified block cipher.
func decrypt(block cipher.Block, value []byte) ([]byte, error) {
// if no block cipher value exists we'll assume
// the database is running in non-ecrypted mode.
if block == nil {
return value, nil
}
size := block.BlockSize()
if len(value) > size {
// Extract iv.
iv := value[:size]
// Extract ciphertext.
value = value[size:]
// Decrypt it.
stream := cipher.NewCTR(block, iv)
stream.XORKeyStream(value, value)
return value, nil
}
return nil, fmt.Errorf("Could not decrypt the value")
}
// GenerateRandomKey creates a random key of size length bytes
func generateRandomKey(strength int) []byte {
k := make([]byte, strength)
if _, err := io.ReadFull(rand.Reader, k); err != nil {
return nil
}
return k
}

86
pkg/database/members.go Normal file
View file

@ -0,0 +1,86 @@
package database
import (
. "github.com/drone/drone/pkg/model"
"github.com/russross/meddler"
)
// Name of the Member table in the database
const memberTable = "members"
// SQL Queries to retrieve a list of all members belonging to a team.
const memberStmt = `
SELECT user_id, name, email, gravatar, role
FROM members, users
WHERE users.id = members.user_id
AND team_id = ?
`
// SQL Queries to retrieve a team by id and user.
const memberFindStmt = `
SELECT user_id, name, email, gravatar, role
FROM members, users
WHERE users.id = members.user_id
AND user_id = ?
AND team_id = ?
`
// SQL Queries to retrieve a team by name .
const memberDeleteStmt = `
DELETE FROM members
WHERE user_id = ? AND team_id = ?
`
// SQL Queries to retrieve a member's role by id and user.
const roleFindStmt = `
SELECT role FROM members
WHERE user_id = ? AND team_id = ?
`
// Returns the Member with the given user and team IDs.
func GetMember(user, team int64) (*Member, error) {
member := Member{}
err := meddler.QueryRow(db, &member, memberFindStmt, user, team)
return &member, err
}
// Returns true if the user is a member of the team
func IsMember(user, team int64) (bool, error) {
role := Role{}
err := meddler.QueryRow(db, &role, roleFindStmt, user, team)
return len(role.Role) > 0, err
}
// Returns true is the user is an admin member of the team.
func IsMemberAdmin(user, team int64) (bool, error) {
role := Role{}
err := meddler.QueryRow(db, &role, roleFindStmt, user, team)
return role.Role == RoleAdmin || role.Role == RoleOwner, err
}
// Creates a new Member.
func SaveMember(user, team int64, role string) error {
r := Role{}
if err := meddler.QueryRow(db, &r, roleFindStmt, user, team); err == nil {
r.Role = role
return meddler.Save(db, memberTable, &r)
}
r.UserID = user
r.TeamID = team
r.Role = role
return meddler.Save(db, memberTable, &r)
}
// Deletes an existing Member.
func DeleteMember(user, team int64) error {
_, err := db.Exec(memberDeleteStmt, user, team)
return err
}
// Returns a list of all Team members.
func ListMembers(team int64) ([]*Member, error) {
var members []*Member
err := meddler.QueryAll(db, &members, memberStmt, team)
return members, err
}

92
pkg/database/repos.go Normal file
View file

@ -0,0 +1,92 @@
package database
import (
"time"
. "github.com/drone/drone/pkg/model"
"github.com/russross/meddler"
)
// Name of the Repos table in the database
const repoTable = "repos"
// SQL Queries to retrieve a list of all repos belonging to a User.
const repoStmt = `
SELECT id, slug, host, owner, name, private, disabled, disabled_pr, scm, url, username, password,
public_key, private_key, params, timeout, priveleged, created, updated, user_id, team_id
FROM repos
WHERE user_id = ? AND team_id = 0
ORDER BY slug ASC
`
// SQL Queries to retrieve a list of all repos belonging to a Team.
const repoTeamStmt = `
SELECT id, slug, host, owner, name, private, disabled, disabled_pr, scm, url, username, password,
public_key, private_key, params, timeout, priveleged, created, updated, user_id, team_id
FROM repos
WHERE team_id = ?
ORDER BY slug ASC
`
// SQL Queries to retrieve a repo by id.
const repoFindStmt = `
SELECT id, slug, host, owner, name, private, disabled, disabled_pr, scm, url, username, password,
public_key, private_key, params, timeout, priveleged, created, updated, user_id, team_id
FROM repos
WHERE id = ?
`
// SQL Queries to retrieve a repo by name.
const repoFindSlugStmt = `
SELECT id, slug, host, owner, name, private, disabled, disabled_pr, scm, url, username, password,
public_key, private_key, params, timeout, priveleged, created, updated, user_id, team_id
FROM repos
WHERE slug = ?
`
// Returns the Repo with the given ID.
func GetRepo(id int64) (*Repo, error) {
repo := Repo{}
err := meddler.QueryRow(db, &repo, repoFindStmt, id)
return &repo, err
}
// Returns the Repo with the given slug.
func GetRepoSlug(slug string) (*Repo, error) {
repo := Repo{}
err := meddler.QueryRow(db, &repo, repoFindSlugStmt, slug)
return &repo, err
}
// Creates a new Repository.
func SaveRepo(repo *Repo) error {
if repo.ID == 0 {
repo.Created = time.Now().UTC()
}
repo.Updated = time.Now().UTC()
return meddler.Save(db, repoTable, repo)
}
// Deletes an existing Repository.
// TODO need to delete builds too.
func DeleteRepo(id int64) error {
_, err := db.Exec("DELETE FROM repos WHERE id = ?", id)
db.Exec("DELETE FROM commits WHERE repo_id = ?", id)
return err
}
// Returns a list of all Repos associated
// with the specified User ID.
func ListRepos(id int64) ([]*Repo, error) {
var repos []*Repo
err := meddler.QueryAll(db, &repos, repoStmt, id)
return repos, err
}
// Returns a list of all Repos associated
// with the specified Team ID.
func ListReposTeam(id int64) ([]*Repo, error) {
var repos []*Repo
err := meddler.QueryAll(db, &repos, repoTeamStmt, id)
return repos, err
}

View file

@ -0,0 +1,126 @@
DELETE FROM builds;
DELETE FROM commits;
DELETE FROM repos;
DELETE FROM members;
DELETE FROM teams;
DELETE FROM users;
DELETE FROM settings;
-- insert users (default password is "password")
INSERT INTO users values (1, 'brad.rydzewski@gmail.com' , '$2a$10$b8d63QsTL38vx7lj0HEHfOdbu1PCAg6Gfca74UavkXooIBx9YxopS', 'nPmsbl6YNLUIUo0I7gkMcQ' ,'Brad Rydzewski', '8c58a0be77ee441bb8f8595b7f1b4e87', '2013-09-16 00:00:00', '2013-09-16 00:00:00', 1, '', '', '', '', '');
INSERT INTO users values (2, 'thomas.d.burke@gmail.com' , '$2a$10$b8d63QsTL38vx7lj0HEHfOdbu1PCAg6Gfca74UavkXooIBx9YxopS', 'sal5Tzy6S10yZCaE0jl6QA', 'Thomas Burke', 'c62f7126273f7fa786274274a5dec8ce', '2013-09-16 00:00:00', '2013-09-16 00:00:00', 1, '', '', '', '', '');
INSERT INTO users values (3, 'carlos.morales.duran@gmail.com', '$2a$10$b8d63QsTL38vx7lj0HEHfOdbu1PCAg6Gfca74UavkXooIBx9YxopS', 'bq87o8AmDUOahKApEy2tVQ', 'Carlos Morales', 'c2180a539620d90d68eaeb848364f1c2', '2013-09-16 00:00:00', '2013-09-17 00:00:00', 1, '', '', '', '', '');
-- insert teams
insert into teams values (1, 'drone', 'Drone' , 'brad@drone.io' , '0057e90a8036c29b1ddb22d0fd08b72c', '2013-09-16 00:00:00', '2013-09-16 00:00:00');
insert into teams values (2, 'google', 'Google', 'dev@google.com' , '24ba30616d2a20673f54c2aee36d159e', '2013-09-16 00:00:00', '2013-09-16 00:00:00');
insert into teams values (3, 'gradle', 'Gradle', 'dev@gradle.com' , '5cc3b557e3a3978d52036da9a5be2a08', '2013-09-16 00:00:00', '2013-09-16 00:00:00');
insert into teams values (4, 'dart', 'Dart' , 'dev@dartlang.org', 'f41fe13f979f2f93cc8b971e1875bdf8', '2013-09-16 00:00:00', '2013-09-16 00:00:00');
-- insert team members
insert into members values (1, 1, 1, 'Owner');
insert into members values (2, 1, 2, 'Admin');
insert into members values (3, 1, 3, 'Write');
-- insert repository
insert into repos values (1, 'github.com/drone/jkl', 'github.com', 'drone', 'jkl', 0, 0, 0, 0, 900, 'git', 'git://github.com/drone/jkl.git', '', '', '', '', '', '2013-09-16 00:00:00', '2013-09-16 00:00:00', 1, 1);
insert into repos values (2, 'github.com/drone/drone', 'github.com', 'drone', 'drone', 1, 0, 0, 0, 900, 'git', 'git@github.com:drone/drone.git', '', '', '', '', '', '2013-09-16 00:00:00', '2013-09-16 00:00:00', 1, 1);
insert into repos values (3, 'github.com/bradrydzewski/drone', 'github.com', 'bradrydzewski', 'drone', 1, 0, 0, 0, 900, 'git', 'git@github.com:bradrydzewski/drone.git', '', '', '', '', '', '2013-09-16 00:00:00', '2013-09-16 00:00:00', 1, 1);
insert into repos values (4, 'github.com/bradrydzewski/blog', 'github.com', 'bradrydzewski', 'blog', 0, 0, 0, 0, 900, 'git', 'git://github.com/bradrydzewski/blog.git', '', '', '', '', '', '2013-09-16 00:00:00', '2013-09-16 00:00:00', 1, 0);
-- insert commits
insert into commits values (1, 1, 'Success', '2013-09-16 00:00:00','2013-09-16 00:00:00', 60, 'ef2221722e6f07a6eaf8af8907b45324428a891d', 'master', '','brad.rydzewski@gmail.com', '8c58a0be77ee441bb8f8595b7f1b4e87', '2013-09-16 00:00:00', 'Fixed mock db class for entity', '2013-09-16 00:00:00', '2013-09-16 00:00:00');
insert into commits values (2, 1, 'Success', '2013-09-16 00:00:00','2013-09-16 00:00:00', 60, '867477aa487d01df28522cee84cd06f5aa154e53', 'master', '','brad.rydzewski@gmail.com', '8c58a0be77ee441bb8f8595b7f1b4e87', '2013-09-16 00:00:00', 'Fixed mock db class for entity', '2013-09-16 00:00:00', '2013-09-16 00:00:00');
insert into commits values (3, 1, 'Success', '2013-09-16 00:00:00','2013-09-16 00:00:00', 60, 'e43427ab462417cb3d53b8702c298c1675deb926', 'master', '','brad.rydzewski@gmail.com', '8c58a0be77ee441bb8f8595b7f1b4e87', '2013-09-16 00:00:00', 'Save deleted entity data to database', '2013-09-16 00:00:00', '2013-09-16 00:00:00');
insert into commits values (4, 1, 'Success', '2013-09-16 00:00:00','2013-09-16 00:00:00', 60, 'a43427ab462417cb3d53b8702c298c1675deb926', 'dev', '','brad.rydzewski@gmail.com', '8c58a0be77ee441bb8f8595b7f1b4e87', '2013-09-16 00:00:00', 'Save deleted entity data to database', '2013-09-16 00:00:00', '2013-09-16 00:00:00');
-- insert builds
insert into builds values (1, 1, 'node_0.10', 'Success', '2013-09-16 00:00:00','2013-09-16 00:00:00', 60, '2013-09-16 00:00:00','2013-09-16 00:00:00', '');
insert into builds values (2, 1, 'node_0.90', 'Success', '2013-09-16 00:00:00','2013-09-16 00:00:00', 60, '2013-09-16 00:00:00','2013-09-16 00:00:00', '');
insert into builds values (3, 1, 'node_0.80', 'Success', '2013-09-16 00:00:00','2013-09-16 00:00:00', 60, '2013-09-16 00:00:00','2013-09-16 00:00:00', '');
insert into builds values (4, 2, 'node_0.10', 'Success', '2013-09-16 00:00:00','2013-09-16 00:00:00', 60, '2013-09-16 00:00:00','2013-09-16 00:00:00', '');
insert into builds values (5, 2, 'node_0.90', 'Success', '2013-09-16 00:00:00','2013-09-16 00:00:00', 60, '2013-09-16 00:00:00','2013-09-16 00:00:00', '');
insert into builds values (6, 2, 'node_0.80', 'Success', '2013-09-16 00:00:00','2013-09-16 00:00:00', 60, '2013-09-16 00:00:00','2013-09-16 00:00:00', '');
insert into builds values (7, 3, 'node_0.10', 'Success', '2013-09-16 00:00:00','2013-09-16 00:00:00', 60, '2013-09-16 00:00:00','2013-09-16 00:00:00', '');
insert into builds values (8, 3, 'node_0.90', 'Success', '2013-09-16 00:00:00','2013-09-16 00:00:00', 60, '2013-09-16 00:00:00','2013-09-16 00:00:00', '');
insert into builds values (9, 3, 'node_0.80', 'Success', '2013-09-16 00:00:00','2013-09-16 00:00:00', 60, '2013-09-16 00:00:00','2013-09-16 00:00:00', '');
-- insert default, dummy settings
insert into settings values (1,'','','','','','','','','','localhost:8080','http');
-- add public & private keys to all repositories
update repos set public_key = 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQCybgl9+Y0VY0mKng3AB3CwCMAOVvg+Xh4X/4lP7SR815GaeEJQusaA0p33HkZfS/2XREWYMtiopHP0bZuBIht76JdhrJlHh1AcLoPQvWJROFvRGol6igVEVZzs9sUdZaPrexFz1CS/j6BJFzPsHnL4gXT3s4PYYST9++pThI90Aw==';
update repos set private_key = '-----BEGIN RSA PRIVATE KEY-----
MIICWwIBAAKBgQCybgl9+Y0VY0mKng3AB3CwCMAOVvg+Xh4X/4lP7SR815GaeEJQ
usaA0p33HkZfS/2XREWYMtiopHP0bZuBIht76JdhrJlHh1AcLoPQvWJROFvRGol6
igVEVZzs9sUdZaPrexFz1CS/j6BJFzPsHnL4gXT3s4PYYST9++pThI90AwIDAQAB
AoGAaxvs7MdaLsWcRu7cGDMfLT0DdVg1ytKaxBMsrWMQrTSGfjDEtkt4j6pfExIE
cn5ea2ibUmLrdkjKJqeJWrpLvlOZGhahBcL/SueFOfr6Lm+m8LvlTrX6JhyLXpx5
NbeEFr0mN16PC6JqkN0xRCN9BfV9m6gnpuP/ojD3RKYMZtkCQQDFbSX/ddEfp9ME
vRNAYif+bFxI6PEgMmwrCIjJGHOsq7zba3Z7KWjW034x2rJ3Cbhs8xtyTcA5qy9F
OzL3pFs3AkEA514SUXowIiqjh6ypnSvUBaQZsWjexDxTXN09DTYPt+Ck1qdzTHWP
9nerg2G3B6bTOWZBftHMaZ/plZ/eyV0LlQJACU1rTO4wPF2cA80k6xO07rgMYSMY
uXumvSBZ0Z/lU22EKJKXspXw6q5sc8zqO9GpbvjFgk1HkXAPeiOf8ys7YQJAD1CI
wd/mo7xSyr5BE+g8xorQMJASfsbHddQnIGK9s5wpDRRUa3E0sEnHjpC/PsBqJth/
6VcVwsAVBBRq+MUx6QJAS9KKxKcMf8JpnDheV7jh+WJKckabA1L2bq8sN6kXfPn0
o7deiE1FKJizXKJ6gd6anfuG3m7VAs7wJhzc685yMg==
-----END RSA PRIVATE KEY-----';
-- add standard output to all builds
update builds set stdout = '$ mvn test
-------------------------------------------------------
T E S T S
-------------------------------------------------------
Running brooklyn.qa.longevity.MonitorUtilsTest
Configuring TestNG with: TestNG652Configurator
[GC 69952K->6701K(253440K), 0.0505760 secs]
2013-08-21 21:12:58,327 INFO TESTNG RUNNING: Suite: "Command line test" containing "7" Tests (config: null)
2013-08-21 21:12:58,342 INFO BrooklynLeakListener.onStart attempting to terminate all extant ManagementContexts: name=Command line test; includedGroups=[]; excludedGroups=[Integration, Acceptance, Live, WIP]; suiteName=brooklyn.qa.longevity.MonitorUtilsTest; outDir=/scratch/jenkins/workspace/brooklyncentral/brooklyn/usage/qa/target/surefire-reports/brooklyn.qa.longevity.MonitorUtilsTest
2013-08-21 21:12:58,473 INFO TESTNG INVOKING: "Command line test" - brooklyn.qa.longevity.MonitorUtilsTest.testFindOwnPid()
2013-08-21 21:12:58,939 INFO executing cmd: ps -p 7484
2013-08-21 21:12:59,030 INFO TESTNG PASSED: "Command line test" - brooklyn.qa.longevity.MonitorUtilsTest.testFindOwnPid() finished in 595 ms
2013-08-21 21:12:59,033 INFO TESTNG INVOKING: "Command line test" - brooklyn.qa.longevity.MonitorUtilsTest.testGetRunningPids()
2013-08-21 21:12:59,035 INFO executing cmd: ps ax
2013-08-21 21:12:59,137 INFO TESTNG PASSED: "Command line test" - brooklyn.qa.longevity.MonitorUtilsTest.testGetRunningPids() finished in 104 ms
2013-08-21 21:12:59,139 INFO TESTNG INVOKING: "Command line test" - brooklyn.qa.longevity.MonitorUtilsTest.testGroovyExecuteAndWaitForConsumingOutputStream()
2013-08-21 21:12:59,295 INFO TESTNG PASSED: "Command line test" - brooklyn.qa.longevity.MonitorUtilsTest.testGroovyExecuteAndWaitForConsumingOutputStream() finished in 155 ms
2013-08-21 21:12:59,298 INFO TESTNG INVOKING: "Command line test" - brooklyn.qa.longevity.MonitorUtilsTest.testIsPidRunning()
2013-08-21 21:12:59,300 INFO executing cmd: ps ax
2013-08-21 21:12:59,384 INFO executing cmd: ps -p 7484
2013-08-21 21:12:59,391 INFO executing cmd: ps -p 10000
2013-08-21 21:12:59,443 INFO pid 10000 not running:
2013-08-21 21:12:59,446 INFO executing cmd: ps -p 1234567
2013-08-21 21:12:59,455 INFO pid 1234567 not running:
2013-08-21 21:12:59,456 INFO TESTNG PASSED: "Command line test" - brooklyn.qa.longevity.MonitorUtilsTest.testIsPidRunning() finished in 158 ms
2013-08-21 21:12:59,481 INFO TESTNG INVOKING: "Command line test" - brooklyn.qa.longevity.MonitorUtilsTest.testIsUrlUp()
[GC 76653K->7013K(253440K), 0.0729880 secs]
2013-08-21 21:13:00,726 INFO Error reading URL http://localhost/thispathdoesnotexist: org.apache.http.conn.HttpHostConnectException: Connection to http://localhost refused
2013-08-21 21:13:00,727 INFO TESTNG PASSED: "Command line test" - brooklyn.qa.longevity.MonitorUtilsTest.testIsUrlUp() finished in 1246 ms
2013-08-21 21:13:00,760 INFO TESTNG INVOKING: "Command line test" - brooklyn.qa.longevity.MonitorUtilsTest.testMemoryUsage()
2013-08-21 21:13:00,762 INFO executing cmd: jmap -histo 7484
2013-08-21 21:13:02,275 INFO executing cmd: jmap -histo 7484
2013-08-21 21:13:03,690 INFO executing cmd: jmap -histo 7484
2013-08-21 21:13:04,725 INFO TESTNG PASSED: "Command line test" - brooklyn.qa.longevity.MonitorUtilsTest.testMemoryUsage() finished in 3965 ms
2013-08-21 21:13:04,752 INFO TESTNG INVOKING: "Command line test" - brooklyn.qa.longevity.MonitorUtilsTest.testSearchLog()
2013-08-21 21:13:04,816 INFO executing cmd: grep -E line1 /tmp/monitorUtilsTest.testSearchLog2369184699231420767.txt
2013-08-21 21:13:04,848 INFO executing cmd: grep -E line1|line2 /tmp/monitorUtilsTest.testSearchLog2369184699231420767.txt
2013-08-21 21:13:04,854 INFO executing cmd: grep -E textnotthere /tmp/monitorUtilsTest.testSearchLog2369184699231420767.txt
2013-08-21 21:13:04,858 INFO executing cmd: grep -E line /tmp/monitorUtilsTest.testSearchLog2369184699231420767.txt
2013-08-21 21:13:04,897 INFO TESTNG PASSED: "Command line test" - brooklyn.qa.longevity.MonitorUtilsTest.testSearchLog() finished in 145 ms
2013-08-21 21:13:04,917 INFO TESTNG
===============================================
Command line test
Tests run: 7, Failures: 0, Skips: 0
===============================================
2013-08-21 21:13:04,944 INFO BrooklynLeakListener.onFinish attempting to terminate all extant ManagementContexts: name=Command line test; includedGroups=[]; excludedGroups=[Integration, Acceptance, Live, WIP]; suiteName=brooklyn.qa.longevity.MonitorUtilsTest; outDir=/scratch/jenkins/workspace/brooklyncentral/brooklyn/usage/qa/target/surefire-reports/brooklyn.qa.longevity.MonitorUtilsTest
Tests run: 7, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 10.849 sec
Results :
Tests run: 7, Failures: 0, Errors: 0, Skipped: 0';

View file

@ -0,0 +1,198 @@
package schema
import (
"database/sql"
)
// SQL statement to create the User Table.
var userTableStmt = `
CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT
,email VARCHAR(255) UNIQUE
,password VARCHAR(255)
,token VARCHAR(255) UNIQUE
,name VARCHAR(255)
,gravatar VARCHAR(255)
,created TIMESTAMP
,updated TIMESTAMP
,admin BOOLEAN
,github_login VARCHAR(255)
,github_token VARCHAR(255)
,bitbucket_login VARCHAR(255)
,bitbucket_token VARCHAR(255)
,bitbucket_secret VARCHAR(255)
);
`
// SQL statement to create the Team Table.
var teamTableStmt = `
CREATE TABLE teams (
id INTEGER PRIMARY KEY AUTOINCREMENT
,slug VARCHAR(255) UNIQUE
,name VARCHAR(255)
,email VARCHAR(255)
,gravatar VARCHAR(255)
,created TIMESTAMP
,updated TIMESTAMP
);
`
// SQL statement to create the Member Table.
var memberTableStmt = `
CREATE TABLE members (
id INTEGER PRIMARY KEY AUTOINCREMENT
,team_id INTEGER
,user_id INTEGER
,role INTEGER
);
`
// SQL statement to create the Repo Table.
var repoTableStmt = `
CREATE TABLE repos (
id INTEGER PRIMARY KEY AUTOINCREMENT
,slug VARCHAR(1024) UNIQUE
,host VARCHAR(255)
,owner VARCHAR(255)
,name VARCHAR(255)
,private BOOLEAN
,disabled BOOLEAN
,disabled_pr BOOLEAN
,priveleged BOOLEAN
,timeout INTEGER
,scm VARCHAR(25)
,url VARCHAR(1024)
,username VARCHAR(255)
,password VARCHAR(255)
,public_key VARCHAR(1024)
,private_key VARCHAR(1024)
,params VARCHAR(2000)
,created TIMESTAMP
,updated TIMESTAMP
,user_id INTEGER
,team_id INTEGER
);
`
// SQL statement to create the Commit Table.
var commitTableStmt = `
CREATE TABLE commits (
id INTEGER PRIMARY KEY AUTOINCREMENT
,repo_id INTEGER
,status VARCHAR(255)
,started TIMESTAMP
,finished TIMESTAMP
,duration INTEGER
,attempts INTEGER
,hash VARCHAR(255)
,branch VARCHAR(255)
,pull_request VARCHAR(255)
,author VARCHAR(255)
,gravatar VARCHAR(255)
,timestamp VARCHAR(255)
,message VARCHAR(255)
,created TIMESTAMP
,updated TIMESTAMP
);
`
// SQL statement to create the Build Table.
var buildTableStmt = `
CREATE TABLE builds (
id INTEGER PRIMARY KEY AUTOINCREMENT
,commit_id INTEGER
,slug VARCHAR(255)
,status VARCHAR(255)
,started TIMESTAMP
,finished TIMESTAMP
,duration INTEGER
,created TIMESTAMP
,updated TIMESTAMP
,stdout BLOB
);
`
// SQL statement to create the Settings
var settingsTableStmt = `
CREATE TABLE settings (
id INTEGER PRIMARY KEY
,github_key VARCHAR(255)
,github_secret VARCHAR(255)
,bitbucket_key VARCHAR(255)
,bitbucket_secret VARCHAR(255)
,smtp_server VARCHAR(1024)
,smtp_port VARCHAR(5)
,smtp_address VARCHAR(1024)
,smtp_username VARCHAR(1024)
,smtp_password VARCHAR(1024)
,hostname VARCHAR(1024)
,scheme VARCHAR(5)
);
`
var memberUniqueIndex = `
CREATE UNIQUE INDEX member_uix ON members (team_id, user_id);
`
var memberTeamIndex = `
CREATE INDEX member_team_ix ON members (team_id);
`
var memberUserIndex = `
CREATE INDEX member_user_ix ON members (user_id);
`
var commitUniqueIndex = `
CREATE UNIQUE INDEX commits_uix ON commits (repo_id, hash, branch);
`
var commitRepoIndex = `
CREATE INDEX commits_repo_ix ON commits (repo_id);
`
var commitBranchIndex = `
CREATE INDEX commits_repo_ix ON commits (repo_id, branch);
`
var repoTeamIndex = `
CREATE INDEX repo_team_ix ON repos (team_id);
`
var repoUserIndex = `
CREATE INDEX repo_user_ix ON repos (user_id);
`
var buildCommitIndex = `
CREATE INDEX builds_commit_ix ON builds (commit_id);
`
var buildSlugIndex = `
CREATE INDEX builds_commit_slug_ix ON builds (commit_id, slug);
`
// Load will apply the DDL commands to
// the provided database.
func Load(db *sql.DB) error {
// created tables
db.Exec(userTableStmt)
db.Exec(teamTableStmt)
db.Exec(memberTableStmt)
db.Exec(repoTableStmt)
db.Exec(commitTableStmt)
db.Exec(buildTableStmt)
db.Exec(settingsTableStmt)
db.Exec(memberUniqueIndex)
db.Exec(memberTeamIndex)
db.Exec(memberUserIndex)
db.Exec(commitUniqueIndex)
db.Exec(commitRepoIndex)
db.Exec(commitBranchIndex)
db.Exec(repoTeamIndex)
db.Exec(repoUserIndex)
db.Exec(buildCommitIndex)
db.Exec(buildSlugIndex)
return nil
}

View file

@ -0,0 +1,127 @@
DROP TABLE IF EXISTS builds;
DROP TABLE IF EXISTS commits;
DROP TABLE IF EXISTS repos;
DROP TABLE IF EXISTS members;
DROP TABLE IF EXISTS teams;
DROP TABLE IF EXISTS users;
DROP TABLE IF EXISTS settings;
CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT
,email VARCHAR(255) UNIQUE
,password VARCHAR(255)
,token VARCHAR(255) UNIQUE
,name VARCHAR(255)
,gravatar VARCHAR(255)
,created TIMESTAMP
,updated TIMESTAMP
,admin BOOLEAN
,github_login VARCHAR(255)
,github_token VARCHAR(255)
,bitbucket_login VARCHAR(255)
,bitbucket_token VARCHAR(255)
,bitbucket_secret VARCHAR(255)
);
CREATE TABLE teams (
id INTEGER PRIMARY KEY AUTOINCREMENT
,slug VARCHAR(255) UNIQUE
,name VARCHAR(255) UNIQUE
,email VARCHAR(255)
,gravatar VARCHAR(255)
,created TIMESTAMP
,updated TIMESTAMP
);
CREATE TABLE members (
id INTEGER PRIMARY KEY AUTOINCREMENT
,team_id INTEGER
,user_id INTEGER
,role INTEGER
);
CREATE TABLE repos (
id INTEGER PRIMARY KEY AUTOINCREMENT
,slug VARCHAR(1024) UNIQUE
,host VARCHAR(255)
,owner VARCHAR(255)
,name VARCHAR(255)
,private BOOLEAN
,disabled BOOLEAN
,disabled_pr BOOLEAN
,priveleged BOOLEAN
,timeout INTEGER
,scm VARCHAR(25)
,url VARCHAR(1024)
,username VARCHAR(255)
,password VARCHAR(255)
,public_key VARCHAR(1024)
,private_key VARCHAR(1024)
,params VARCHAR(2000)
,created TIMESTAMP
,updated TIMESTAMP
,user_id INTEGER
,team_id INTEGER
);
CREATE TABLE commits (
id INTEGER PRIMARY KEY AUTOINCREMENT
,repo_id INTEGER
,status VARCHAR(255)
,started TIMESTAMP
,finished TIMESTAMP
,duration INTEGER
,hash VARCHAR(255)
,branch VARCHAR(255)
,pull_request VARCHAR(255)
,author VARCHAR(255)
,gravatar VARCHAR(255)
,timestamp VARCHAR(255)
,message VARCHAR(255)
,created TIMESTAMP
,updated TIMESTAMP
);
CREATE TABLE builds (
id INTEGER PRIMARY KEY AUTOINCREMENT
,commit_id INTEGER
,slug VARCHAR(255)
,status VARCHAR(255)
,started TIMESTAMP
,finished TIMESTAMP
,duration INTEGER
,created TIMESTAMP
,updated TIMESTAMP
,stdout BLOB
);
CREATE TABLE settings (
id INTEGER PRIMARY KEY
,github_key VARCHAR(255)
,github_secret VARCHAR(255)
,bitbucket_key VARCHAR(255)
,bitbucket_secret VARCHAR(255)
,smtp_server VARCHAR(1024)
,smtp_port VARCHAR(5)
,smtp_address VARCHAR(1024)
,smtp_username VARCHAR(1024)
,smtp_password VARCHAR(1024)
,hostname VARCHAR(1024)
,scheme VARCHAR(5)
);
CREATE UNIQUE INDEX member_uix ON members (team_id, user_id);
CREATE UNIQUE INDEX commits_uix ON commits (repo_id, hash, branch);
CREATE INDEX member_team_ix ON members (team_id);
CREATE INDEX member_user_ix ON members (user_id);
CREATE INDEX repo_team_ix ON repos (team_id);
CREATE INDEX repo_user_ix ON repos (user_id);
CREATE INDEX commits_repo_ix ON commits (repo_id);
CREATE INDEX commits_repo_branch_ix ON commits (repo_id, branch);
CREATE INDEX builds_commit_ix ON builds (commit_id);
CREATE INDEX builds_commit_slug_ix ON builds (commit_id, slug);

72
pkg/database/settings.go Normal file
View file

@ -0,0 +1,72 @@
package database
import (
. "github.com/drone/drone/pkg/model"
"github.com/russross/meddler"
)
// Name of the Settings table in the database
const settingsTable = "settings"
// SQL Queries to retrieve the system settings
const settingsStmt = `
SELECT id, github_key, github_secret, bitbucket_key, bitbucket_secret,
smtp_server, smtp_port, smtp_address, smtp_username, smtp_password, hostname, scheme
FROM settings WHERE id = 1
`
//var (
// // mutex for locking the local settings cache
// settingsLock sync.Mutex
//
// // cached settings
// settingsCache = &Settings{}
//)
// Returns the system Settings.
func GetSettings() (*Settings, error) {
//settingsLock.Lock()
//defer settingsLock.Unlock()
// return a copy of the settings
//if settingsCache.ID == 0 {
/// settingsCopy := &Settings{}
// *settingsCopy = *settingsCache
// return settingsCopy, nil
//}
settings := Settings{}
err := meddler.QueryRow(db, &settings, settingsStmt)
//if err == sql.ErrNoRows {
// // we ignore the NoRows error in case this
// // is the first time the system is being used
// err = nil
//}
return &settings, err
}
// Returns the system Settings. This is expected
// always pass, and will panic on failure.
func SettingsMust() *Settings {
settings, err := GetSettings()
if err != nil {
panic(err)
}
return settings
}
// Saves the system Settings.
func SaveSettings(settings *Settings) error {
//settingsLock.Lock()
//defer settingsLock.Unlock()
// persist changes to settings
err := meddler.Save(db, settingsTable, settings)
if err != nil {
return err
}
// store updated settings in cache
//*settingsCache = *settings
return nil
}

73
pkg/database/teams.go Normal file
View file

@ -0,0 +1,73 @@
package database
import (
"time"
. "github.com/drone/drone/pkg/model"
"github.com/russross/meddler"
)
// Name of the Team table in the database
const teamTable = "teams"
// SQL Queries to retrieve a list of all teams belonging to a user.
const teamStmt = `
SELECT id, slug, name, email, gravatar, created, updated
FROM teams
WHERE id IN (select team_id from members where user_id = ?)
`
// SQL Queries to retrieve a team by id and user.
const teamFindStmt = `
SELECT id, slug, name, email, gravatar, created, updated
FROM teams
WHERE id = ?
`
// SQL Queries to retrieve a team by slug.
const teamFindSlugStmt = `
SELECT id, slug, name, email, gravatar, created, updated
FROM teams
WHERE slug = ?
`
// Returns the Team with the given ID.
func GetTeam(id int64) (*Team, error) {
team := Team{}
err := meddler.QueryRow(db, &team, teamFindStmt, id)
return &team, err
}
// Returns the Team with the given slug.
func GetTeamSlug(slug string) (*Team, error) {
team := Team{}
err := meddler.QueryRow(db, &team, teamFindSlugStmt, slug)
return &team, err
}
// Saves a Team.
func SaveTeam(team *Team) error {
if team.ID == 0 {
team.Created = time.Now().UTC()
}
team.Updated = time.Now().UTC()
return meddler.Save(db, teamTable, team)
}
// Deletes an existing Team account.
func DeleteTeam(id int64) error {
// disassociate all repos with this team
db.Exec("UPDATE repos SET team_id = 0 WHERE team_id = ?", id)
// delete the team memberships and the team itself
db.Exec("DELETE FROM members WHERE team_id = ?", id)
db.Exec("DELETE FROM teams WHERE id = ?", id)
return nil
}
// Returns a list of all Teams associated
// with the specified User ID.
func ListTeams(id int64) ([]*Team, error) {
var teams []*Team
err := meddler.QueryAll(db, &teams, teamStmt, id)
return teams, err
}

View file

@ -0,0 +1,136 @@
package database
import (
"testing"
"github.com/drone/drone/pkg/database"
)
func TestGetBuild(t *testing.T) {
Setup()
defer Teardown()
build, err := database.GetBuild(1)
if err != nil {
t.Error(err)
}
if build.ID != 1 {
t.Errorf("Exepected ID %d, got %d", 1, build.ID)
}
if build.Slug != "node_0.10" {
t.Errorf("Exepected Slug %s, got %s", "node_0.10", build.Slug)
}
if build.Status != "Success" {
t.Errorf("Exepected Status %s, got %s", "Success", build.Status)
}
}
func TestGetBuildSlug(t *testing.T) {
Setup()
defer Teardown()
build, err := database.GetBuildSlug("node_0.10", 1)
if err != nil {
t.Error(err)
}
if build.ID != 1 {
t.Errorf("Exepected ID %d, got %d", 1, build.ID)
}
if build.Slug != "node_0.10" {
t.Errorf("Exepected Slug %s, got %s", "node_0.10", build.Slug)
}
if build.Status != "Success" {
t.Errorf("Exepected Status %s, got %s", "Success", build.Status)
}
}
func TestSaveBbuild(t *testing.T) {
Setup()
defer Teardown()
// get the build we plan to update
build, err := database.GetBuild(1)
if err != nil {
t.Error(err)
}
// update fields
build.Status = "Failing"
// update the database
if err := database.SaveBuild(build); err != nil {
t.Error(err)
}
// get the updated build
updatedBuild, err := database.GetBuild(1)
if err != nil {
t.Error(err)
}
if build.ID != updatedBuild.ID {
t.Errorf("Exepected ID %d, got %d", updatedBuild.ID, build.ID)
}
if build.Slug != updatedBuild.Slug {
t.Errorf("Exepected Slug %s, got %s", updatedBuild.Slug, build.Slug)
}
if build.Status != updatedBuild.Status {
t.Errorf("Exepected Status %s, got %s", updatedBuild.Status, build.Status)
}
}
func TestDeleteBuild(t *testing.T) {
Setup()
defer Teardown()
if err := database.DeleteBuild(1); err != nil {
t.Error(err)
}
// try to get the deleted row
_, err := database.GetBuild(1)
if err == nil {
t.Fail()
}
}
func TestListBuilds(t *testing.T) {
Setup()
defer Teardown()
// builds for commit_id = 1
builds, err := database.ListBuilds(1)
if err != nil {
t.Error(err)
}
// verify user count
if len(builds) != 2 {
t.Errorf("Exepected %d builds in database, got %d", 2, len(builds))
return
}
// get the first user in the list and verify
// fields are being populated correctly
build := builds[1]
if build.ID != 1 {
t.Errorf("Exepected ID %d, got %d", 1, build.ID)
}
if build.Slug != "node_0.10" {
t.Errorf("Exepected Slug %s, got %s", "node_0.10", build.Slug)
}
if build.Status != "Success" {
t.Errorf("Exepected Status %s, got %s", "Success", build.Status)
}
}

View file

@ -0,0 +1,164 @@
package database
import (
"testing"
"github.com/drone/drone/pkg/database"
)
func TestGetCommit(t *testing.T) {
Setup()
defer Teardown()
commit, err := database.GetCommit(1)
if err != nil {
t.Error(err)
}
if commit.ID != 1 {
t.Errorf("Exepected ID %d, got %d", 1, commit.ID)
}
if commit.Status != "Success" {
t.Errorf("Exepected Status %s, got %s", "Success", commit.Status)
}
if commit.Hash != "4f4c4594be6d6ddbc1c0dd521334f7ecba92b608" {
t.Errorf("Exepected Hash %s, got %s", "4f4c4594be6d6ddbc1c0dd521334f7ecba92b608", commit.Hash)
}
if commit.Branch != "master" {
t.Errorf("Exepected Branch %s, got %s", "master", commit.Branch)
}
if commit.Author != "brad.rydzewski@gmail.com" {
t.Errorf("Exepected Author %s, got %s", "master", commit.Author)
}
if commit.Message != "commit message" {
t.Errorf("Exepected Message %s, got %s", "master", commit.Message)
}
if commit.Gravatar != "8c58a0be77ee441bb8f8595b7f1b4e87" {
t.Errorf("Exepected Gravatar %s, got %s", "8c58a0be77ee441bb8f8595b7f1b4e87", commit.Gravatar)
}
}
func TestGetCommitHash(t *testing.T) {
Setup()
defer Teardown()
commit, err := database.GetCommitHash("4f4c4594be6d6ddbc1c0dd521334f7ecba92b608", 1)
if err != nil {
t.Error(err)
}
if commit.ID != 1 {
t.Errorf("Exepected ID %d, got %d", 1, commit.ID)
}
if commit.Hash != "4f4c4594be6d6ddbc1c0dd521334f7ecba92b608" {
t.Errorf("Exepected Hash %s, got %s", "4f4c4594be6d6ddbc1c0dd521334f7ecba92b608", commit.Hash)
}
if commit.Status != "Success" {
t.Errorf("Exepected Status %s, got %s", "Success", commit.Status)
}
}
func TestSaveCommit(t *testing.T) {
Setup()
defer Teardown()
// get the commit we plan to update
commit, err := database.GetCommit(1)
if err != nil {
t.Error(err)
}
// update fields
commit.Status = "Failing"
// update the database
if err := database.SaveCommit(commit); err != nil {
t.Error(err)
}
// get the updated commit
updatedCommit, err := database.GetCommit(1)
if err != nil {
t.Error(err)
}
if commit.Hash != updatedCommit.Hash {
t.Errorf("Exepected Hash %s, got %s", updatedCommit.Hash, commit.Hash)
}
if commit.Status != "Failing" {
t.Errorf("Exepected Status %s, got %s", updatedCommit.Status, commit.Status)
}
}
func TestDeleteCommit(t *testing.T) {
Setup()
defer Teardown()
if err := database.DeleteCommit(1); err != nil {
t.Error(err)
}
// try to get the deleted row
_, err := database.GetCommit(1)
if err == nil {
t.Fail()
}
}
func TestListCommits(t *testing.T) {
Setup()
defer Teardown()
// commits for repo_id = 1
commits, err := database.ListCommits(1, "master")
if err != nil {
t.Error(err)
}
// verify commit count
if len(commits) != 2 {
t.Errorf("Exepected %d commits in database, got %d", 2, len(commits))
return
}
// get the first user in the list and verify
// fields are being populated correctly
commit := commits[1] // TODO something strange is happening with ordering here
if commit.ID != 1 {
t.Errorf("Exepected ID %d, got %d", 1, commit.ID)
}
if commit.Status != "Success" {
t.Errorf("Exepected Status %s, got %s", "Success", commit.Status)
}
if commit.Hash != "4f4c4594be6d6ddbc1c0dd521334f7ecba92b608" {
t.Errorf("Exepected Hash %s, got %s", "4f4c4594be6d6ddbc1c0dd521334f7ecba92b608", commit.Hash)
}
if commit.Branch != "master" {
t.Errorf("Exepected Branch %s, got %s", "master", commit.Branch)
}
if commit.Author != "brad.rydzewski@gmail.com" {
t.Errorf("Exepected Author %s, got %s", "master", commit.Author)
}
if commit.Message != "commit message" {
t.Errorf("Exepected Message %s, got %s", "master", commit.Message)
}
if commit.Gravatar != "8c58a0be77ee441bb8f8595b7f1b4e87" {
t.Errorf("Exepected Gravatar %s, got %s", "8c58a0be77ee441bb8f8595b7f1b4e87", commit.Gravatar)
}
}

View file

@ -0,0 +1,140 @@
package database
import (
"testing"
"github.com/drone/drone/pkg/database"
"github.com/drone/drone/pkg/model"
)
// TODO unit test to verify unique constraint on Team.Name
// TestGetMember tests the ability to retrieve a Team
// Member from the database by Unique ID.
func TestGetMember(t *testing.T) {
Setup()
defer Teardown()
// get member by user_id and team_id
member, err := database.GetMember(1, 1)
if err != nil {
t.Error(err)
}
if member.ID != 1 {
t.Errorf("Exepected ID %d, got %d", 1, member.ID)
}
if member.Name != "Brad Rydzewski" {
t.Errorf("Exepected Name %s, got %s", "Brad Rydzewski", member.Name)
}
if member.Email != "brad.rydzewski@gmail.com" {
t.Errorf("Exepected Email %s, got %s", "brad.rydzewski@gmail.com", member.Email)
}
if member.Gravatar != "8c58a0be77ee441bb8f8595b7f1b4e87" {
t.Errorf("Exepected Gravatar %s, got %s", "8c58a0be77ee441bb8f8595b7f1b4e87", member.Gravatar)
}
if member.Role != model.RoleOwner {
t.Errorf("Exepected Role %s, got %s", model.RoleOwner, member.Role)
}
}
func TestIsMember(t *testing.T) {
Setup()
defer Teardown()
ok, err := database.IsMember(1, 1)
if err != nil {
t.Error(err)
}
if !ok {
t.Errorf("Expected IsMember to return true, returned false")
}
}
func TestIsMemberAdmin(t *testing.T) {
Setup()
defer Teardown()
// expecting user is Owner
if ok, err := database.IsMemberAdmin(1, 1); err != nil {
t.Error(err)
} else if !ok {
t.Errorf("Expected IsMemberAdmin to return true, returned false")
}
// expecting user is Admin
if ok, err := database.IsMemberAdmin(2, 1); err != nil {
t.Error(err)
} else if !ok {
t.Errorf("Expected IsMemberAdmin to return true, returned false")
}
// expecting user is NOT Admin (Write role)
if ok, err := database.IsMemberAdmin(3, 1); err != nil {
t.Error(err)
} else if ok {
t.Errorf("Expected IsMemberAdmin to return false, returned true")
}
}
func TestDeleteMember(t *testing.T) {
Setup()
defer Teardown()
// delete member by user_id and team_id
if err := database.DeleteMember(1, 1); err != nil {
t.Error(err)
}
// get member by user_id and team_id
if _, err := database.GetMember(1, 1); err == nil {
t.Error(err)
}
}
func TestListMembers(t *testing.T) {
Setup()
defer Teardown()
// list members by team_id
members, err := database.ListMembers(1)
if err != nil {
t.Error(err)
}
// verify team count
if len(members) != 3 {
t.Errorf("Exepected %d Team Members in database, got %d", 3, len(members))
return
}
// get the first member in the list and verify
// fields are being populated correctly
member := members[0]
if member.ID != 1 {
t.Errorf("Exepected ID %d, got %d", 1, member.ID)
}
if member.Name != "Brad Rydzewski" {
t.Errorf("Exepected Name %s, got %s", "Brad Rydzewski", member.Name)
}
if member.Email != "brad.rydzewski@gmail.com" {
t.Errorf("Exepected Email %s, got %s", "brad.rydzewski@gmail.com", member.Email)
}
if member.Gravatar != "8c58a0be77ee441bb8f8595b7f1b4e87" {
t.Errorf("Exepected Gravatar %s, got %s", "8c58a0be77ee441bb8f8595b7f1b4e87", member.Gravatar)
}
if member.Role != model.RoleOwner {
t.Errorf("Exepected Role %s, got %s", model.RoleOwner, member.Role)
}
}

View file

@ -0,0 +1,403 @@
package database
import (
"testing"
"github.com/drone/drone/pkg/database"
)
// TODO unit test to verify unique constraint on Member.UserID and Member.TeamID
// TestGetRepo tests the ability to retrieve a Repo
// from the database by Unique ID.
func TestGetRepo(t *testing.T) {
Setup()
defer Teardown()
repo, err := database.GetRepo(1)
if err != nil {
t.Error(err)
}
if repo.ID != 1 {
t.Errorf("Exepected ID %d, got %d", 1, repo.ID)
}
if repo.Slug != "github.com/drone/drone" {
t.Errorf("Exepected Slug %s, got %s", "github.com/drone/drone", repo.Slug)
}
if repo.Host != "github.com" {
t.Errorf("Exepected Host %s, got %s", "github.com", repo.Host)
}
if repo.Owner != "drone" {
t.Errorf("Exepected Owner %s, got %s", "drone", repo.Owner)
}
if repo.Name != "drone" {
t.Errorf("Exepected Name %s, got %s", "drone", repo.Name)
}
if repo.Private != true {
t.Errorf("Exepected Private %v, got %v", true, repo.Private)
}
if repo.Disabled != false {
t.Errorf("Exepected Private %v, got %v", false, repo.Disabled)
}
if repo.SCM != "git" {
t.Errorf("Exepected Type %s, got %s", "git", repo.SCM)
}
if repo.URL != "git@github.com:drone/drone.git" {
t.Errorf("Exepected URL %s, got %s", "git@github.com:drone/drone.git", repo.URL)
}
if repo.Username != "no username" {
t.Errorf("Exepected Username %s, got %s", "no username", repo.Username)
}
if repo.Password != "no password" {
t.Errorf("Exepected Password %s, got %s", "no password", repo.Password)
}
if repo.PublicKey != "public key" {
t.Errorf("Exepected PublicKey %s, got %s", "public key", repo.PublicKey)
}
if repo.PrivateKey != "private key" {
t.Errorf("Exepected PrivateKey %s, got %s", "private key", repo.PrivateKey)
}
if repo.UserID != 1 {
t.Errorf("Exepected ID %d, got %d", 1, repo.UserID)
}
if repo.TeamID != 1 {
t.Errorf("Exepected ID %d, got %d", 1, repo.TeamID)
}
}
// TestGetRepoSlug tests the ability to retrieve a Repo
// from the database by it's Canonical Name.
func TestGetRepoSlug(t *testing.T) {
Setup()
defer Teardown()
repo, err := database.GetRepoSlug("github.com/drone/drone")
if err != nil {
t.Error(err)
}
if repo.ID != 1 {
t.Errorf("Exepected ID %d, got %d", 1, repo.ID)
}
if repo.Slug != "github.com/drone/drone" {
t.Errorf("Exepected Slug %s, got %s", "github.com/drone/drone", repo.Slug)
}
if repo.Host != "github.com" {
t.Errorf("Exepected Host %s, got %s", "github.com", repo.Host)
}
if repo.Owner != "drone" {
t.Errorf("Exepected Owner %s, got %s", "drone", repo.Owner)
}
if repo.Name != "drone" {
t.Errorf("Exepected Name %s, got %s", "drone", repo.Name)
}
if repo.Private != true {
t.Errorf("Exepected Private %v, got %v", true, repo.Private)
}
if repo.Disabled != false {
t.Errorf("Exepected Private %v, got %v", false, repo.Disabled)
}
if repo.SCM != "git" {
t.Errorf("Exepected Type %s, got %s", "git", repo.SCM)
}
if repo.URL != "git@github.com:drone/drone.git" {
t.Errorf("Exepected URL %s, got %s", "git@github.com:drone/drone.git", repo.URL)
}
if repo.Username != "no username" {
t.Errorf("Exepected Username %s, got %s", "no username", repo.Username)
}
if repo.Password != "no password" {
t.Errorf("Exepected Password %s, got %s", "no password", repo.Password)
}
if repo.PublicKey != "public key" {
t.Errorf("Exepected PublicKey %s, got %s", "public key", repo.PublicKey)
}
if repo.PrivateKey != "private key" {
t.Errorf("Exepected PrivateKey %s, got %s", "private key", repo.PrivateKey)
}
if repo.UserID != 1 {
t.Errorf("Exepected ID %d, got %d", 1, repo.UserID)
}
if repo.TeamID != 1 {
t.Errorf("Exepected ID %d, got %d", 1, repo.TeamID)
}
}
func TestSaveRepo(t *testing.T) {
Setup()
defer Teardown()
// get the repo we plan to update
repo, err := database.GetRepo(1)
if err != nil {
t.Error(err)
}
// update fields
repo.Slug = "bitbucket.org/drone/drone"
repo.Host = "bitbucket.org"
repo.Private = false
repo.Disabled = true
repo.SCM = "hg"
repo.URL = "https://bitbucket.org/drone/drone"
repo.Username = "brad"
repo.Password = "password"
repo.TeamID = 0
// update the database
if err := database.SaveRepo(repo); err != nil {
t.Error(err)
}
// get the updated repo
updatedRepo, err := database.GetRepo(1)
if err != nil {
t.Error(err)
}
if updatedRepo.Slug != repo.Slug {
t.Errorf("Exepected Slug %s, got %s", updatedRepo.Slug, repo.Slug)
}
if updatedRepo.Host != repo.Host {
t.Errorf("Exepected Host %s, got %s", updatedRepo.Host, repo.Host)
}
if updatedRepo.Private != repo.Private {
t.Errorf("Exepected Private %v, got %v", updatedRepo.Private, repo.Private)
}
if updatedRepo.Disabled != repo.Disabled {
t.Errorf("Exepected Private %v, got %v", updatedRepo.Disabled, repo.Disabled)
}
if updatedRepo.SCM != repo.SCM {
t.Errorf("Exepected Type %s, got %s", true, repo.SCM)
}
if updatedRepo.URL != repo.URL {
t.Errorf("Exepected URL %s, got %s", updatedRepo.URL, repo.URL)
}
if updatedRepo.Username != repo.Username {
t.Errorf("Exepected Username %s, got %s", updatedRepo.Username, repo.Username)
}
if updatedRepo.Password != repo.Password {
t.Errorf("Exepected Password %s, got %s", updatedRepo.Password, repo.Password)
}
if updatedRepo.TeamID != repo.TeamID {
t.Errorf("Exepected TeamID %d, got %d", updatedRepo.TeamID, repo.TeamID)
}
}
func TestDeleteRepo(t *testing.T) {
Setup()
defer Teardown()
if err := database.DeleteRepo(1); err != nil {
t.Error(err)
}
// try to get the deleted row
_, err := database.GetRepo(1)
if err == nil {
t.Fail()
}
}
/*
func TestListRepos(t *testing.T) {
Setup()
defer Teardown()
// repos for user_id = 1
repos, err := database.ListRepos(1)
if err != nil {
t.Error(err)
}
// verify user count
if len(repos) != 2 {
t.Errorf("Exepected %d repos in database, got %d", 2, len(repos))
return
}
// get the second repo in the list and verify
// fields are being populated correctly
// NOTE: we get the 2nd repo due to sorting
repo := repos[1]
if repo.ID != 1 {
t.Errorf("Exepected ID %d, got %d", 1, repo.ID)
}
if repo.Name != "github.com/drone/drone" {
t.Errorf("Exepected Name %s, got %s", "github.com/drone/drone", repo.Name)
}
if repo.Host != "github.com" {
t.Errorf("Exepected Host %s, got %s", "github.com", repo.Host)
}
if repo.Owner != "drone" {
t.Errorf("Exepected Owner %s, got %s", "drone", repo.Owner)
}
if repo.Slug != "drone" {
t.Errorf("Exepected Slug %s, got %s", "drone", repo.Slug)
}
if repo.Private != true {
t.Errorf("Exepected Private %v, got %v", true, repo.Private)
}
if repo.Disabled != false {
t.Errorf("Exepected Private %v, got %v", false, repo.Disabled)
}
if repo.SCM != "git" {
t.Errorf("Exepected Type %s, got %s", "git", repo.SCM)
}
if repo.URL != "git@github.com:drone/drone.git" {
t.Errorf("Exepected URL %s, got %s", "git@github.com:drone/drone.git", repo.URL)
}
if repo.Username != "no username" {
t.Errorf("Exepected Username %s, got %s", "no username", repo.Username)
}
if repo.Password != "no password" {
t.Errorf("Exepected Password %s, got %s", "no password", repo.Password)
}
if repo.PublicKey != "public key" {
t.Errorf("Exepected PublicKey %s, got %s", "public key", repo.PublicKey)
}
if repo.PrivateKey != "private key" {
t.Errorf("Exepected PrivateKey %s, got %s", "private key", repo.PrivateKey)
}
if repo.UserID != 1 {
t.Errorf("Exepected ID %d, got %d", 1, repo.UserID)
}
if repo.TeamID != 1 {
t.Errorf("Exepected ID %d, got %d", 1, repo.TeamID)
}
}
*/
func TestListReposTeam(t *testing.T) {
Setup()
defer Teardown()
// repos for team_id = 1
repos, err := database.ListReposTeam(1)
if err != nil {
t.Error(err)
}
// verify user count
if len(repos) != 2 {
t.Errorf("Exepected %d repos in database, got %d", 2, len(repos))
return
}
// get the second repo in the list and verify
// fields are being populated correctly
// NOTE: we get the 2nd repo due to sorting
repo := repos[1]
if repo.ID != 1 {
t.Errorf("Exepected ID %d, got %d", 1, repo.ID)
}
if repo.Slug != "github.com/drone/drone" {
t.Errorf("Exepected Slug %s, got %s", "github.com/drone/drone", repo.Slug)
}
if repo.Host != "github.com" {
t.Errorf("Exepected Host %s, got %s", "github.com", repo.Host)
}
if repo.Owner != "drone" {
t.Errorf("Exepected Owner %s, got %s", "drone", repo.Owner)
}
if repo.Name != "drone" {
t.Errorf("Exepected Name %s, got %s", "drone", repo.Name)
}
if repo.Private != true {
t.Errorf("Exepected Private %v, got %v", true, repo.Private)
}
if repo.Disabled != false {
t.Errorf("Exepected Private %v, got %v", false, repo.Disabled)
}
if repo.SCM != "git" {
t.Errorf("Exepected Type %s, got %s", "git", repo.SCM)
}
if repo.URL != "git@github.com:drone/drone.git" {
t.Errorf("Exepected URL %s, got %s", "git@github.com:drone/drone.git", repo.URL)
}
if repo.Username != "no username" {
t.Errorf("Exepected Username %s, got %s", "no username", repo.Username)
}
if repo.Password != "no password" {
t.Errorf("Exepected Password %s, got %s", "no password", repo.Password)
}
if repo.PublicKey != "public key" {
t.Errorf("Exepected PublicKey %s, got %s", "public key", repo.PublicKey)
}
if repo.PrivateKey != "private key" {
t.Errorf("Exepected PrivateKey %s, got %s", "private key", repo.PrivateKey)
}
if repo.UserID != 1 {
t.Errorf("Exepected ID %d, got %d", 1, repo.UserID)
}
if repo.TeamID != 1 {
t.Errorf("Exepected ID %d, got %d", 1, repo.TeamID)
}
}

View file

@ -0,0 +1,63 @@
package database
import (
"testing"
"github.com/drone/drone/pkg/database"
)
func TestGetSettings(t *testing.T) {
Setup()
defer Teardown()
// even though no settings exist yet, we should
// not see an error since we supress the msg
settings, err := database.GetSettings()
//if err != nil {
// t.Error(err)
//}
// add some settings
//settings := &modelSettings{}
settings.Scheme = "https"
settings.Domain = "foo.com"
settings.BitbucketKey = "bitbucketkey"
settings.BitbucketSecret = "bitbucketsecret"
settings.GitHubKey = "githubkey"
settings.GitHubSecret = "githubsecret"
settings.SmtpAddress = "noreply@foo.bar"
settings.SmtpServer = "0.0.0.0"
settings.SmtpUsername = "username"
settings.SmtpPassword = "password"
// save the updated settings
if err := database.SaveSettings(settings); err != nil {
t.Error(err)
}
// re-retrieve the settings post-save
settings, err = database.GetSettings()
if err != nil {
t.Error(err)
}
if settings.ID != 1 {
t.Errorf("Exepected ID %d, got %d", 1, settings.ID)
}
if settings.Scheme != "https" {
t.Errorf("Exepected Scheme %s, got %s", "https", settings.Scheme)
}
if settings.Domain != "foo.com" {
t.Errorf("Exepected Domain %s, got %s", "foo.com", settings.Domain)
}
// Verify caching works and is threadsafe
settingsA, _ := database.GetSettings()
settingsB, _ := database.GetSettings()
settingsA.Domain = "foo.bar.baz"
if settingsA.Domain == settingsB.Domain {
t.Errorf("Exepected Domain ThreadSafe and unchanged")
}
}

View file

@ -0,0 +1,169 @@
package database
import (
"testing"
"github.com/drone/drone/pkg/database"
)
// TODO unit test to verify unique constraint on Member.UserID and Member.TeamID
// TestGetTeam tests the ability to retrieve a Team
// from the database by Unique ID.
func TestGetTeam(t *testing.T) {
Setup()
defer Teardown()
team, err := database.GetTeam(1)
if err != nil {
t.Error(err)
}
if team.ID != 1 {
t.Errorf("Exepected ID %d, got %d", 1, team.ID)
}
if team.Name != "Drone" {
t.Errorf("Exepected Name %s, got %s", "Drone", team.Name)
}
if team.Slug != "drone" {
t.Errorf("Exepected Slug %s, got %s", "drone", team.Slug)
}
if team.Email != "support@drone.io" {
t.Errorf("Exepected Email %s, got %s", "brad@drone.io", team.Email)
}
if team.Gravatar != "8c58a0be77ee441bb8f8595b7f1b4e87" {
t.Errorf("Exepected Gravatar %s, got %s", "8c58a0be77ee441bb8f8595b7f1b4e87", team.Gravatar)
}
}
// TestGetTeamName tests the ability to retrieve a Team
// from the database by Unique Team Name (aka Slug).
func TestGetTeamSlug(t *testing.T) {
Setup()
defer Teardown()
team, err := database.GetTeamSlug("drone")
if err != nil {
t.Error(err)
}
if team.ID != 1 {
t.Errorf("Exepected ID %d, got %d", 1, team.ID)
}
if team.Name != "Drone" {
t.Errorf("Exepected Name %s, got %s", "Drone", team.Name)
}
if team.Slug != "drone" {
t.Errorf("Exepected Slug %s, got %s", "drone", team.Slug)
}
if team.Email != "support@drone.io" {
t.Errorf("Exepected Email %s, got %s", "brad@drone.io", team.Email)
}
if team.Gravatar != "8c58a0be77ee441bb8f8595b7f1b4e87" {
t.Errorf("Exepected Gravatar %s, got %s", "8c58a0be77ee441bb8f8595b7f1b4e87", team.Gravatar)
}
}
// TestUpdateTeam tests the ability to updatee an
// existing Team in the database.
func TestUpdateTeam(t *testing.T) {
Setup()
defer Teardown()
// get the user we plan to update
team, err := database.GetTeam(1)
if err != nil {
t.Error(err)
}
// update fields
team.Email = "brad@drone.io"
team.Gravatar = "61024896f291303615bcd4f7a0dcfb74"
// update the database
if err := database.SaveTeam(team); err != nil {
t.Error(err)
}
// get the updated team
updatedTeam, err := database.GetTeam(1)
if err != nil {
t.Error(err)
}
// verify the updated fields
if team.Email != updatedTeam.Email {
t.Errorf("Exepected Email %s, got %s", team.Email, updatedTeam.Email)
}
if team.Gravatar != updatedTeam.Gravatar {
t.Errorf("Exepected Gravatar %s, got %s", team.Gravatar, updatedTeam.Gravatar)
}
}
// Test the ability to delete a Team.
func TestDeleteTeam(t *testing.T) {
Setup()
defer Teardown()
// get the team we plan to update
if err := database.DeleteTeam(1); err != nil {
t.Error(err)
}
// now try to get the team from the database
_, err := database.GetTeam(1)
if err == nil {
t.Fail()
}
}
// Test the ability to get a list of Teams
// to which a User belongs.
func TestListTeam(t *testing.T) {
Setup()
defer Teardown()
teams, err := database.ListTeams(1)
if err != nil {
t.Error(err)
}
// verify team count
if len(teams) != 3 {
t.Errorf("Exepected %d teams in database, got %d", 3, len(teams))
return
}
// get the first user in the list and verify
// fields are being populated correctly
team := teams[0]
if team.ID != 1 {
t.Errorf("Exepected ID %d, got %d", 1, team.ID)
}
if team.Name != "Drone" {
t.Errorf("Exepected Name %s, got %s", "Drone", team.Name)
}
if team.Slug != "drone" {
t.Errorf("Exepected Slug %s, got %s", "drone", team.Slug)
}
if team.Email != "support@drone.io" {
t.Errorf("Exepected Email %s, got %s", "brad@drone.io", team.Email)
}
if team.Gravatar != "8c58a0be77ee441bb8f8595b7f1b4e87" {
t.Errorf("Exepected Gravatar %s, got %s", "8c58a0be77ee441bb8f8595b7f1b4e87", team.Gravatar)
}
}

View file

@ -0,0 +1,207 @@
package database
import (
"crypto/aes"
"database/sql"
"log"
"github.com/drone/drone/pkg/database"
"github.com/drone/drone/pkg/database/encrypt"
. "github.com/drone/drone/pkg/model"
_ "github.com/mattn/go-sqlite3"
"github.com/russross/meddler"
)
// in-memory database used for
// unit testing purposes.
var db *sql.DB
func init() {
// create a cipher for ecnrypting and decrypting
// database fields
cipher, err := aes.NewCipher([]byte("38B241096B8DA08131563770F4CDDFAC"))
if err != nil {
log.Fatal(err)
}
// register function with meddler to encrypt and
// decrypt database fields.
meddler.Register("gobencrypt", &encrypt.EncryptedField{cipher})
// notify meddler that we are working with sqlite
meddler.Default = meddler.SQLite
}
func Setup() {
// create an in-memory database
db, _ = sql.Open("sqlite3", ":memory:")
// make sure all the tables and indexes are created
database.Set(db)
// create dummy user data
user1 := User{
Password: "$2a$10$b8d63QsTL38vx7lj0HEHfOdbu1PCAg6Gfca74UavkXooIBx9YxopS",
Name: "Brad Rydzewski",
Email: "brad.rydzewski@gmail.com",
Gravatar: "8c58a0be77ee441bb8f8595b7f1b4e87",
Token: "123",
Admin: true}
user2 := User{
Password: "$2a$10$b8d63QsTL38vx7lj0HEHfOdbu1PCAg6Gfca74UavkXooIBx9YxopS",
Name: "Thomas Burke",
Email: "cavepig@gmail.com",
Gravatar: "c62f7126273f7fa786274274a5dec8ce",
Token: "456",
Admin: false}
user3 := User{
Password: "$2a$10$b8d63QsTL38vx7lj0HEHfOdbu1PCAg6Gfca74UavkXooIBx9YxopS",
Name: "Carlos Morales",
Email: "ytsejammer@gmail.com",
Gravatar: "c2180a539620d90d68eaeb848364f1c2",
Token: "789",
Admin: false}
database.SaveUser(&user1)
database.SaveUser(&user2)
database.SaveUser(&user3)
// create dummy team data
team1 := Team{
Slug: "drone",
Name: "Drone",
Email: "support@drone.io",
Gravatar: "8c58a0be77ee441bb8f8595b7f1b4e87"}
team2 := Team{
Slug: "github",
Name: "Github",
Email: "support@github.com",
Gravatar: "61024896f291303615bcd4f7a0dcfb74"}
team3 := Team{
Slug: "golang",
Name: "Golang",
Email: "support@golang.org",
Gravatar: "991695cc770c6b8354b68cd18c280b95"}
database.SaveTeam(&team1)
database.SaveTeam(&team2)
database.SaveTeam(&team3)
// create team membership data
database.SaveMember(user1.ID, team1.ID, RoleOwner)
database.SaveMember(user2.ID, team1.ID, RoleAdmin)
database.SaveMember(user3.ID, team1.ID, RoleWrite)
database.SaveMember(user1.ID, team2.ID, RoleOwner)
database.SaveMember(user2.ID, team2.ID, RoleAdmin)
database.SaveMember(user3.ID, team2.ID, RoleWrite)
database.SaveMember(user1.ID, team3.ID, RoleOwner)
// create dummy repo data
repo1 := Repo{
Slug: "github.com/drone/drone",
Host: "github.com",
Owner: "drone",
Name: "drone",
Private: true,
Disabled: false,
SCM: "git",
URL: "git@github.com:drone/drone.git",
Username: "no username",
Password: "no password",
PublicKey: "public key",
PrivateKey: "private key",
UserID: user1.ID,
TeamID: team1.ID,
}
repo2 := Repo{
Slug: "bitbucket.org/drone/test",
Host: "bitbucket.org",
Owner: "drone",
Name: "test",
Private: false,
Disabled: false,
SCM: "hg",
URL: "https://bitbucket.org/drone/test",
Username: "no username",
Password: "no password",
PublicKey: "public key",
PrivateKey: "private key",
UserID: user1.ID,
TeamID: team1.ID,
}
repo3 := Repo{
Slug: "bitbucket.org/brydzewski/test",
Host: "bitbucket.org",
Owner: "brydzewski",
Name: "test",
Private: false,
Disabled: false,
SCM: "hg",
URL: "https://bitbucket.org/brydzewski/test",
Username: "no username",
Password: "no password",
PublicKey: "public key",
PrivateKey: "private key",
UserID: user2.ID,
}
database.SaveRepo(&repo1)
database.SaveRepo(&repo2)
database.SaveRepo(&repo3)
commit1 := Commit{
RepoID: repo1.ID,
Status: "Success",
Hash: "4f4c4594be6d6ddbc1c0dd521334f7ecba92b608",
Branch: "master",
Author: user1.Email,
Gravatar: user1.Gravatar,
Message: "commit message",
}
commit2 := Commit{
RepoID: repo1.ID,
Status: "Failure",
Hash: "0eb2fa13e9f4139e803b6ad37831708d4786c74a",
Branch: "master",
Author: user1.Email,
Gravatar: user1.Gravatar,
Message: "commit message",
}
commit3 := Commit{
RepoID: repo1.ID,
Status: "Failure",
Hash: "60a7fe87ccf01d0152e53242528399e05acaf047",
Branch: "dev",
Author: user1.Email,
Gravatar: user1.Gravatar,
Message: "commit message",
}
commit4 := Commit{
RepoID: repo2.ID,
Status: "Success",
Hash: "a4078d1e9a0842cdd214adbf0512578799a4f2ba",
Branch: "master",
Author: user1.Email,
Gravatar: user1.Gravatar,
Message: "commit message",
}
// create dummy commit data
database.SaveCommit(&commit1)
database.SaveCommit(&commit2)
database.SaveCommit(&commit3)
database.SaveCommit(&commit4)
// create dummy build data
database.SaveBuild(&Build{CommitID: commit1.ID, Slug: "node_0.10", Status: "Success", Duration: 60})
database.SaveBuild(&Build{CommitID: commit1.ID, Slug: "node_0.09", Status: "Success", Duration: 70})
database.SaveBuild(&Build{CommitID: commit2.ID, Slug: "node_0.10", Status: "Success", Duration: 10})
database.SaveBuild(&Build{CommitID: commit2.ID, Slug: "node_0.09", Status: "Failure", Duration: 65})
database.SaveBuild(&Build{CommitID: commit3.ID, Slug: "node_0.10", Status: "Failure", Duration: 50})
database.SaveBuild(&Build{CommitID: commit3.ID, Slug: "node_0.09", Status: "Failure", Duration: 55})
}
func Teardown() {
db.Close()
}

View file

@ -0,0 +1,169 @@
package database
import (
"testing"
"github.com/drone/drone/pkg/database"
)
// TODO unit test to verify unique constraint on User.Username
// TODO unit test to verify unique constraint on User.Email
// TestGetUser tests the ability to retrieve a User
// from the database by Unique ID.
func TestGetUser(t *testing.T) {
Setup()
defer Teardown()
u, err := database.GetUser(1)
if err != nil {
t.Error(err)
}
if u.ID != 1 {
t.Errorf("Exepected ID %d, got %d", 1, u.ID)
}
if u.Password != "$2a$10$b8d63QsTL38vx7lj0HEHfOdbu1PCAg6Gfca74UavkXooIBx9YxopS" {
t.Errorf("Exepected Password %s, got %s", "$2a$10$b8d63QsTL38vx7lj0HEHfOdbu1PCAg6Gfca74UavkXooIBx9YxopS", u.Password)
}
if u.Name != "Brad Rydzewski" {
t.Errorf("Exepected Name %s, got %s", "Brad Rydzewski", u.Name)
}
if u.Email != "brad.rydzewski@gmail.com" {
t.Errorf("Exepected Email %s, got %s", "brad.rydzewski@gmail.com", u.Email)
}
if u.Gravatar != "8c58a0be77ee441bb8f8595b7f1b4e87" {
t.Errorf("Exepected Gravatar %s, got %s", "8c58a0be77ee441bb8f8595b7f1b4e87", u.Gravatar)
}
}
// TestGetUseEmail tests the ability to retrieve a User
// from the database by Email address.
func TestGetUserEmail(t *testing.T) {
Setup()
defer Teardown()
u, err := database.GetUserEmail("brad.rydzewski@gmail.com")
if err != nil {
t.Error(err)
}
if u.ID != 1 {
t.Errorf("Exepected ID %d, got %d", 1, u.ID)
}
if u.Password != "$2a$10$b8d63QsTL38vx7lj0HEHfOdbu1PCAg6Gfca74UavkXooIBx9YxopS" {
t.Errorf("Exepected Password %s, got %s", "$2a$10$b8d63QsTL38vx7lj0HEHfOdbu1PCAg6Gfca74UavkXooIBx9YxopS", u.Password)
}
if u.Name != "Brad Rydzewski" {
t.Errorf("Exepected Name %s, got %s", "Brad Rydzewski", u.Name)
}
if u.Email != "brad.rydzewski@gmail.com" {
t.Errorf("Exepected Email %s, got %s", "brad.rydzewski@gmail.com", u.Email)
}
if u.Gravatar != "8c58a0be77ee441bb8f8595b7f1b4e87" {
t.Errorf("Exepected Gravatar %s, got %s", "8c58a0be77ee441bb8f8595b7f1b4e87", u.Gravatar)
}
}
// TestUpdateUser tests the ability to updatee an
// existing User in the database.
func TestUpdateUser(t *testing.T) {
Setup()
defer Teardown()
// get the user we plan to update
user, err := database.GetUser(1)
if err != nil {
t.Error(err)
}
// update fields
user.Email = "brad@drone.io"
user.Password = "password"
// update the database
if err := database.SaveUser(user); err != nil {
t.Error(err)
}
// get the updated user
updatedUser, err := database.GetUser(1)
if err != nil {
t.Error(err)
}
// verify the updated fields
if user.Email != updatedUser.Email {
t.Errorf("Exepected Email %s, got %s", user.Email, updatedUser.Email)
}
if user.Password != updatedUser.Password {
t.Errorf("Exepected Password %s, got %s", user.Email, updatedUser.Password)
}
}
// Deletes an existing User account.
func TestDeleteUser(t *testing.T) {
Setup()
defer Teardown()
// get the user we plan to update
if err := database.DeleteUser(1); err != nil {
t.Error(err)
}
// now try to get the user from the database
_, err := database.GetUser(1)
if err == nil {
t.Fail()
}
}
// Returns a list of all Users.
func TestListUsers(t *testing.T) {
Setup()
defer Teardown()
users, err := database.ListUsers()
if err != nil {
t.Error(err)
}
// verify user count
if len(users) != 3 {
t.Errorf("Exepected %d users in database, got %d", 3, len(users))
return
}
// get the first user in the list and verify
// fields are being populated correctly
u := users[0]
if u.ID != 1 {
t.Errorf("Exepected ID %d, got %d", 1, u.ID)
}
if u.Password != "$2a$10$b8d63QsTL38vx7lj0HEHfOdbu1PCAg6Gfca74UavkXooIBx9YxopS" {
t.Errorf("Exepected Password %s, got %s", "$2a$10$b8d63QsTL38vx7lj0HEHfOdbu1PCAg6Gfca74UavkXooIBx9YxopS", u.Password)
}
if u.Name != "Brad Rydzewski" {
t.Errorf("Exepected Name %s, got %s", "Brad Rydzewski", u.Name)
}
if u.Email != "brad.rydzewski@gmail.com" {
t.Errorf("Exepected Email %s, got %s", "brad.rydzewski@gmail.com", u.Email)
}
if u.Gravatar != "8c58a0be77ee441bb8f8595b7f1b4e87" {
t.Errorf("Exepected Gravatar %s, got %s", "8c58a0be77ee441bb8f8595b7f1b4e87", u.Gravatar)
}
}

90
pkg/database/users.go Normal file
View file

@ -0,0 +1,90 @@
package database
import (
"time"
. "github.com/drone/drone/pkg/model"
"github.com/russross/meddler"
)
// Name of the User table in the database
const userTable = "users"
// SQL Queries to retrieve a user by their unique database key
const userFindIdStmt = `
SELECT id, email, password, name, gravatar, created, updated, admin,
github_login, github_token, bitbucket_login, bitbucket_token, bitbucket_secret
FROM users WHERE id = ?
`
// SQL Queries to retrieve a user by their email address
const userFindEmailStmt = `
SELECT id, email, password, name, gravatar, created, updated, admin,
github_login, github_token, bitbucket_login, bitbucket_token, bitbucket_secret
FROM users WHERE email = ?
`
// SQL Queries to retrieve a list of all users
const userStmt = `
SELECT id, email, password, name, gravatar, created, updated, admin,
github_login, github_token, bitbucket_login, bitbucket_token, bitbucket_secret
FROM users
ORDER BY name ASC
`
// Returns the User with the given ID.
func GetUser(id int64) (*User, error) {
user := User{}
err := meddler.QueryRow(db, &user, userFindIdStmt, id)
return &user, err
}
// Returns the User with the given email address.
func GetUserEmail(email string) (*User, error) {
user := User{}
err := meddler.QueryRow(db, &user, userFindEmailStmt, email)
return &user, err
}
// Returns the User Password Hash for the given
// email address.
func GetPassEmail(email string) ([]byte, error) {
user, err := GetUserEmail(email)
if err != nil {
return nil, err
}
return []byte(user.Password), nil
}
// Saves the User account.
func SaveUser(user *User) error {
if user.ID == 0 {
user.Created = time.Now().UTC()
}
user.Updated = time.Now().UTC()
return meddler.Save(db, userTable, user)
}
// Deletes an existing User account.
func DeleteUser(id int64) error {
db.Exec("DELETE FROM members WHERE user_id = ?", id)
db.Exec("DELETE FROM users WHERE id = ?", id)
// TODO delete all projects
return nil
}
// Returns a list of all Users.
func ListUsers() ([]*User, error) {
var users []*User
err := meddler.QueryAll(db, &users, userStmt)
return users, err
}
// Returns a list of Users within the specified
// range (for pagination purposes).
func ListUsersRange(limit, offset int) ([]*User, error) {
var users []*User
err := meddler.QueryAll(db, &users, userStmt)
return users, err
}

256
pkg/handler/admin.go Normal file
View file

@ -0,0 +1,256 @@
package handler
import (
"fmt"
"log"
"net/http"
"strconv"
"time"
"github.com/dchest/authcookie"
"github.com/drone/drone/pkg/database"
"github.com/drone/drone/pkg/mail"
. "github.com/drone/drone/pkg/model"
)
// Display a list of ALL users in the system
func AdminUserList(w http.ResponseWriter, r *http.Request, u *User) error {
users, err := database.ListUsers()
if err != nil {
return err
}
data := struct {
User *User
Users []*User
}{u, users}
return RenderTemplate(w, "admin_users.html", &data)
}
// Invite a user to join the system
func AdminUserAdd(w http.ResponseWriter, r *http.Request, u *User) error {
return RenderTemplate(w, "admin_users_add.html", &struct{ User *User }{u})
}
// Invite a user to join the system
func AdminUserInvite(w http.ResponseWriter, r *http.Request, u *User) error {
// generate the password reset token
email := r.FormValue("email")
token := authcookie.New(email, time.Now().Add(12*time.Hour), secret)
// get settings
hostname := database.SettingsMust().URL().String()
emailEnabled := database.SettingsMust().SmtpServer != ""
if !emailEnabled {
// Email is not enabled, so must let the user know the signup link
link := fmt.Sprintf("%v/register?token=%v", hostname, token)
return RenderText(w, link, http.StatusOK)
}
// send data to template
data := struct {
Host string
Email string
Token string
}{hostname, email, token}
// send the email message async
go func() {
if err := mail.SendActivation(email, data); err != nil {
log.Printf("error sending account activation email to %s. %s", email, err)
}
}()
return RenderText(w, http.StatusText(http.StatusOK), http.StatusOK)
}
// Form to edit a user
func AdminUserEdit(w http.ResponseWriter, r *http.Request, u *User) error {
idstr := r.FormValue("id")
id, err := strconv.Atoi(idstr)
if err != nil {
return err
}
// get the user from the database
user, err := database.GetUser(int64(id))
if err != nil {
return err
}
data := struct {
User *User
EditUser *User
}{u, user}
return RenderTemplate(w, "admin_users_edit.html", &data)
}
func AdminUserUpdate(w http.ResponseWriter, r *http.Request, u *User) error {
// get the ID from the URL parameter
idstr := r.FormValue("id")
id, err := strconv.Atoi(idstr)
if err != nil {
return err
}
// get the user from the database
user, err := database.GetUser(int64(id))
if err != nil {
return err
}
// update if user is administrator or not
switch r.FormValue("Admin") {
case "true":
user.Admin = true
case "false":
user.Admin = false
}
// saving user
if err := database.SaveUser(user); err != nil {
return err
}
return RenderText(w, http.StatusText(http.StatusOK), http.StatusOK)
}
func AdminUserDelete(w http.ResponseWriter, r *http.Request, u *User) error {
// get the ID from the URL parameter
idstr := r.FormValue("id")
id, err := strconv.Atoi(idstr)
if err != nil {
return err
}
// cannot delete self
if u.ID == int64(id) {
return RenderForbidden(w)
}
// delete the user
if err := database.DeleteUser(int64(id)); err != nil {
return err
}
http.Redirect(w, r, "/account/admin/users", http.StatusSeeOther)
return nil
}
// Display a list of ALL users in the system
func AdminSettings(w http.ResponseWriter, r *http.Request, u *User) error {
// get settings from database
settings := database.SettingsMust()
data := struct {
User *User
Settings *Settings
}{u, settings}
return RenderTemplate(w, "admin_settings.html", &data)
}
// Display a list of ALL users in the system
func AdminSettingsUpdate(w http.ResponseWriter, r *http.Request, u *User) error {
// get settings from database
settings := database.SettingsMust()
// update smtp settings
settings.Domain = r.FormValue("Domain")
settings.Scheme = r.FormValue("Scheme")
// update bitbucket settings
settings.BitbucketKey = r.FormValue("BitbucketKey")
settings.BitbucketSecret = r.FormValue("BitbucketSecret")
// update github settings
settings.GitHubKey = r.FormValue("GitHubKey")
settings.GitHubSecret = r.FormValue("GitHubSecret")
// update smtp settings
settings.SmtpServer = r.FormValue("SmtpServer")
settings.SmtpPort = r.FormValue("SmtpPort")
settings.SmtpAddress = r.FormValue("SmtpAddress")
settings.SmtpUsername = r.FormValue("SmtpUsername")
settings.SmtpPassword = r.FormValue("SmtpPassword")
// persist changes
if err := database.SaveSettings(settings); err != nil {
return RenderError(w, err, http.StatusBadRequest)
}
// make sure the mail package is updated with the
// latest client information.
//mail.SetClient(&mail.SMTPClient{
// Host: settings.SmtpServer,
// Port: settings.SmtpPort,
// User: settings.SmtpUsername,
// Pass: settings.SmtpPassword,
// From: settings.SmtpAddress,
//})
return RenderText(w, http.StatusText(http.StatusOK), http.StatusOK)
}
func Install(w http.ResponseWriter, r *http.Request) error {
// we can only perform the inital installation if no
// users exist in the system
if users, err := database.ListUsers(); err != nil {
return RenderError(w, err, http.StatusBadRequest)
} else if len(users) != 0 {
// if users exist in the systsem
// we should render a NotFound page
return RenderNotFound(w)
}
return RenderTemplate(w, "install.html", true)
}
func InstallPost(w http.ResponseWriter, r *http.Request) error {
// we can only perform the inital installation if no
// users exist in the system
if users, err := database.ListUsers(); err != nil {
return RenderError(w, err, http.StatusBadRequest)
} else if len(users) != 0 {
// if users exist in the systsem
// we should render a NotFound page
return RenderNotFound(w)
}
// set the email and name
user := NewUser(r.FormValue("name"), r.FormValue("email"))
user.Admin = true
// set the new password
if err := user.SetPassword(r.FormValue("password")); err != nil {
return RenderError(w, err, http.StatusBadRequest)
}
// verify fields are correct
if err := user.Validate(); err != nil {
return RenderError(w, err, http.StatusBadRequest)
}
// save to the database
if err := database.SaveUser(user); err != nil {
return RenderError(w, err, http.StatusBadRequest)
}
// update settings
settings := Settings{}
settings.Domain = r.FormValue("Domain")
settings.Scheme = r.FormValue("Scheme")
database.SaveSettings(&settings)
// add the user to the session object
// so that he/she is loggedin
SetCookie(w, r, "_sess", user.Email)
// send the user to the settings page
// to complete the configuration.
http.Redirect(w, r, "/account/admin/settings", http.StatusSeeOther)
return nil
}

185
pkg/handler/app.go Normal file
View file

@ -0,0 +1,185 @@
package handler
import (
"crypto/rand"
"io"
"log"
"net/http"
"time"
"github.com/dchest/authcookie"
"github.com/dchest/passwordreset"
"github.com/drone/drone/pkg/database"
"github.com/drone/drone/pkg/mail"
. "github.com/drone/drone/pkg/model"
)
var (
// Secret key used to sign auth cookies,
// password reset tokens, etc.
secret = generateRandomKey(256)
)
// GenerateRandomKey creates a random key of size length bytes
func generateRandomKey(strength int) []byte {
k := make([]byte, strength)
if _, err := io.ReadFull(rand.Reader, k); err != nil {
return nil
}
return k
}
// Returns an HTML index.html page if the user is
// not currently authenticated, otherwise redirects
// the user to their personal dashboard screen
func Index(w http.ResponseWriter, r *http.Request) error {
// is the user already authenticated then
// redirect to the dashboard page
if _, err := r.Cookie("_sess"); err == nil {
http.Redirect(w, r, "/dashboard", http.StatusSeeOther)
return nil
}
// otherwise redirect to the login page
http.Redirect(w, r, "/login", http.StatusSeeOther)
return nil
}
// Return an HTML form for the User to login.
func Login(w http.ResponseWriter, r *http.Request) error {
return RenderTemplate(w, "login.html", nil)
}
// Terminate the User session.
func Logout(w http.ResponseWriter, r *http.Request) error {
DelCookie(w, r, "_sess")
http.Redirect(w, r, "/login", http.StatusSeeOther)
return nil
}
// Return an HTML form for the User to request a password reset.
func Forgot(w http.ResponseWriter, r *http.Request) error {
return RenderTemplate(w, "forgot.html", nil)
}
// Return an HTML form for the User to perform a password reset.
// This page must be visited from a Password Reset email that
// contains a hash to verify the User's identity.
func Reset(w http.ResponseWriter, r *http.Request) error {
return RenderTemplate(w, "reset.html", &struct{ Error string }{""})
}
// Return an HTML form to register for a new account. This
// page must be visited from a Signup email that contains
// a hash to verify the Email address is correct.
func Register(w http.ResponseWriter, r *http.Request) error {
return RenderTemplate(w, "register.html", &struct{ Error string }{""})
}
func ForgotPost(w http.ResponseWriter, r *http.Request) error {
email := r.FormValue("email")
// attempt to retrieve the user by email address
user, err := database.GetUserEmail(email)
if err != nil {
log.Printf("could not find user %s to reset password. %s", email, err)
// if we can't find the email, we still display
// the template to the user. This prevents someone
// from trying to guess passwords through trial & error
return RenderTemplate(w, "forgot_sent.html", nil)
}
// hostname from settings
hostname := database.SettingsMust().URL().String()
// generate the password reset token
token := passwordreset.NewToken(user.Email, 12*time.Hour, []byte(user.Password), secret)
data := struct {
Host string
User *User
Token string
}{hostname, user, token}
// send the email message async
go func() {
if err := mail.SendPassword(email, data); err != nil {
log.Printf("error sending password reset email to %s. %s", email, err)
}
}()
// render the template indicating a success
return RenderTemplate(w, "forgot_sent.html", nil)
}
func ResetPost(w http.ResponseWriter, r *http.Request) error {
// verify the token and extract the username
token := r.FormValue("token")
email, err := passwordreset.VerifyToken(token, database.GetPassEmail, secret)
if err != nil {
return RenderTemplate(w, "reset.html", &struct{ Error string }{"Your password reset request is expired."})
}
// get the user from the database
user, err := database.GetUserEmail(email)
if err != nil {
return RenderTemplate(w, "reset.html", &struct{ Error string }{"Unable to locate user account."})
}
// get the new password
password := r.FormValue("password")
if err := user.SetPassword(password); err != nil {
return RenderTemplate(w, "reset.html", &struct{ Error string }{err.Error()})
}
// save to the database
if err := database.SaveUser(user); err != nil {
return RenderTemplate(w, "reset.html", &struct{ Error string }{"Unable to update password. Please try again"})
}
// add the user to the session object
//session, _ := store.Get(r, "_sess")
//session.Values["username"] = user.Email
//session.Save(r, w)
SetCookie(w, r, "_sess", user.Email)
http.Redirect(w, r, "/dashboard", http.StatusSeeOther)
return nil
}
func RegisterPost(w http.ResponseWriter, r *http.Request) error {
// verify the token and extract the username
token := r.FormValue("token")
email := authcookie.Login(token, secret)
if len(email) == 0 {
return RenderTemplate(w, "register.html", &struct{ Error string }{"Your registration email is expired."})
}
// set the email and name
user := User{}
user.SetEmail(email)
user.Name = r.FormValue("name")
// set the new password
password := r.FormValue("password")
if err := user.SetPassword(password); err != nil {
return RenderTemplate(w, "register.html", &struct{ Error string }{err.Error()})
}
// verify fields are correct
if err := user.Validate(); err != nil {
return RenderTemplate(w, "register.html", &struct{ Error string }{err.Error()})
}
// save to the database
if err := database.SaveUser(&user); err != nil {
return err
}
// add the user to the session object
SetCookie(w, r, "_sess", user.Email)
// redirect the user to their dashboard
http.Redirect(w, r, "/dashboard", http.StatusSeeOther)
return nil
}

91
pkg/handler/auth.go Normal file
View file

@ -0,0 +1,91 @@
package handler
import (
"net/http"
"github.com/drone/drone/pkg/database"
. "github.com/drone/drone/pkg/model"
"github.com/drone/go-github/github"
"github.com/drone/go-github/oauth2"
)
// Create the User session.
func Authorize(w http.ResponseWriter, r *http.Request) error {
// extract form data
username := r.FormValue("username")
password := r.FormValue("password")
returnTo := r.FormValue("return_to")
// get the user from the database
user, err := database.GetUserEmail(username)
if err != nil {
return RenderTemplate(w, "login_error.html", nil)
}
// verify the password
if err := user.ComparePassword(password); err != nil {
return RenderTemplate(w, "login_error.html", nil)
}
// add the user to the session object
SetCookie(w, r, "_sess", username)
// where should we send the user to?
if len(returnTo) == 0 {
returnTo = "/dashboard"
}
// redirect to the homepage
http.Redirect(w, r, returnTo, http.StatusSeeOther)
return nil
}
func LinkGithub(w http.ResponseWriter, r *http.Request, u *User) error {
// get settings from database
settings := database.SettingsMust()
// github OAuth2 Data
var oauth = oauth2.Client{
RedirectURL: settings.URL().String() + "/auth/login/github",
AccessTokenURL: "https://github.com/login/oauth/access_token",
AuthorizationURL: "https://github.com/login/oauth/authorize",
ClientId: settings.GitHubKey,
ClientSecret: settings.GitHubSecret,
}
// get the OAuth code
code := r.FormValue("code")
if len(code) == 0 {
scope := "repo,repo:status,user:email"
state := "FqB4EbagQ2o"
redirect := oauth.AuthorizeRedirect(scope, state)
http.Redirect(w, r, redirect, http.StatusSeeOther)
return nil
}
// exchange code for an auth token
token, err := oauth.GrantToken(code)
if err != nil {
return err
}
// create the client
client := github.New(token.AccessToken)
// get the user information
githubUser, err := client.Users.Current()
if err != nil {
return err
}
// save the github token to the user account
u.GithubToken = token.AccessToken
u.GithubLogin = githubUser.Login
if err := database.SaveUser(u); err != nil {
return err
}
http.Redirect(w, r, "/new/github.com", http.StatusSeeOther)
return nil
}

50
pkg/handler/badges.go Normal file
View file

@ -0,0 +1,50 @@
package handler
import (
"fmt"
"net/http"
"github.com/drone/drone/pkg/database"
)
// Display a static badge (png format) for a specific
// repository and an optional branch.
// TODO this needs to implement basic caching
func Badge(w http.ResponseWriter, r *http.Request) error {
branchParam := r.FormValue(":branch")
hostParam := r.FormValue(":host")
ownerParam := r.FormValue(":owner")
nameParam := r.FormValue(":name")
repoSlug := fmt.Sprintf("%s/%s/%s", hostParam, ownerParam, nameParam)
// get the repo from the database
repo, err := database.GetRepoSlug(repoSlug)
if err != nil {
http.NotFound(w, r)
return nil
}
// get the default branch for the repository
// if no branch is provided.
if len(branchParam) == 0 {
branchParam = repo.DefaultBranch()
}
// default badge of "unknown"
badge := "/img/build_unknown.png"
// get the latest commit from the database
// for the requested branch
commit, err := database.GetBranch(repo.ID, branchParam)
if err == nil {
switch commit.Status {
case "Success":
badge = "/img/build_success.png"
case "Failing", "Failure":
badge = "/img/build_failing.png"
}
}
http.Redirect(w, r, badge, http.StatusSeeOther)
return nil
}

34
pkg/handler/builds.go Normal file
View file

@ -0,0 +1,34 @@
package handler
import (
"net/http"
"github.com/drone/drone/pkg/database"
. "github.com/drone/drone/pkg/model"
)
// Returns the combined stdout / stderr for an individual Build.
func BuildOut(w http.ResponseWriter, r *http.Request, u *User, repo *Repo) error {
hash := r.FormValue(":commit")
labl := r.FormValue(":label")
// get the commit from the database
commit, err := database.GetCommitHash(hash, repo.ID)
if err != nil {
return err
}
// get the build from the database
build, err := database.GetBuildSlug(labl, commit.ID)
if err != nil {
return err
}
return RenderText(w, build.Stdout, http.StatusOK)
}
// Returns the gzipped stdout / stderr for an individual Build
func BuildOutGzip(w http.ResponseWriter, r *http.Request, u *User) error {
// TODO
return nil
}

56
pkg/handler/commits.go Normal file
View file

@ -0,0 +1,56 @@
package handler
import (
"fmt"
"net/http"
"github.com/drone/drone/pkg/channel"
"github.com/drone/drone/pkg/database"
. "github.com/drone/drone/pkg/model"
)
// Display a specific Commit.
func CommitShow(w http.ResponseWriter, r *http.Request, u *User, repo *Repo) error {
hash := r.FormValue(":commit")
labl := r.FormValue(":label")
// get the commit from the database
commit, err := database.GetCommitHash(hash, repo.ID)
if err != nil {
return err
}
// get the builds from the database. a commit can have
// multiple sub-builds (or matrix builds)
builds, err := database.ListBuilds(commit.ID)
if err != nil {
return err
}
data := struct {
User *User
Repo *Repo
Commit *Commit
Build *Build
Builds []*Build
Token string
}{u, repo, commit, builds[0], builds, ""}
// get the specific build requested by the user. instead
// of a database round trip, we can just loop through the
// list and extract the requested build.
for _, b := range builds {
if b.Slug == labl {
data.Build = b
break
}
}
// generate a token to connect with the websocket
// handler and stream output, if the build is running.
data.Token = channel.Token(fmt.Sprintf(
"%s/commit/%s/builds/%s", repo.Slug, commit.Hash, builds[0].Slug))
// render the repository template.
return RenderTemplate(w, "repo_commit.html", &data)
}

192
pkg/handler/handler.go Normal file
View file

@ -0,0 +1,192 @@
package handler
import (
"fmt"
"log"
"net/http"
"net/url"
"github.com/drone/drone/pkg/database"
. "github.com/drone/drone/pkg/model"
)
// ErrorHandler wraps the default http.HandleFunc to handle an
// error as the return value.
type ErrorHandler func(w http.ResponseWriter, r *http.Request) error
func (h ErrorHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if err := h(w, r); err != nil {
log.Print(err)
}
}
// UserHandler wraps the default http.HandlerFunc to include
// the currently authenticated User in the method signature,
// in addition to handling an error as the return value.
type UserHandler func(w http.ResponseWriter, r *http.Request, user *User) error
func (h UserHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
user, err := readUser(r)
if err != nil {
redirectLogin(w, r)
return
}
if err = h(w, r, user); err != nil {
log.Print(err)
RenderError(w, err, http.StatusBadRequest)
}
}
// AdminHandler wraps the default http.HandlerFunc to include
// the currently authenticated User in the method signature,
// in addition to handling an error as the return value. It also
// verifies the user has Administrative priveleges.
type AdminHandler func(w http.ResponseWriter, r *http.Request, user *User) error
func (h AdminHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
user, err := readUser(r)
if err != nil {
redirectLogin(w, r)
return
}
// User MUST have administrative priveleges in order
// to execute the handler.
if user.Admin == false {
RenderNotFound(w)
return
}
if err = h(w, r, user); err != nil {
log.Print(err)
RenderError(w, err, http.StatusBadRequest)
}
}
// RepoHandler wraps the default http.HandlerFunc to include
// the currently authenticated User and requested Repository
// in the method signature, in addition to handling an error
// as the return value.
type RepoHandler func(w http.ResponseWriter, r *http.Request, user *User, repo *Repo) error
func (h RepoHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
user, err := readUser(r)
if err != nil {
redirectLogin(w, r)
return
}
// repository name from the URL parameters
hostParam := r.FormValue(":host")
userParam := r.FormValue(":owner")
nameParam := r.FormValue(":name")
repoName := fmt.Sprintf("%s/%s/%s", hostParam, userParam, nameParam)
repo, err := database.GetRepoSlug(repoName)
if err != nil {
RenderNotFound(w)
return
}
// The User must own the repository OR be a member
// of the Team that owns the repository.
if user.ID != repo.UserID {
if member, _ := database.IsMember(user.ID, repo.TeamID); !member {
RenderNotFound(w)
return
}
}
if err = h(w, r, user, repo); err != nil {
log.Print(err)
RenderError(w, err, http.StatusBadRequest)
}
}
// RepoHandler wraps the default http.HandlerFunc to include
// the currently authenticated User and requested Repository
// in the method signature, in addition to handling an error
// as the return value.
type RepoAdminHandler func(w http.ResponseWriter, r *http.Request, user *User, repo *Repo) error
func (h RepoAdminHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
user, err := readUser(r)
if err != nil {
redirectLogin(w, r)
return
}
// repository name from the URL parameters
hostParam := r.FormValue(":host")
userParam := r.FormValue(":owner")
nameParam := r.FormValue(":name")
repoName := fmt.Sprintf("%s/%s/%s", hostParam, userParam, nameParam)
repo, err := database.GetRepoSlug(repoName)
if err != nil {
RenderNotFound(w)
return
}
// The User must own the repository OR be a member
// of the Team that owns the repository.
if user.ID != repo.UserID {
if admin, _ := database.IsMemberAdmin(user.ID, repo.TeamID); admin == false {
RenderNotFound(w)
return
}
}
if err = h(w, r, user, repo); err != nil {
log.Print(err)
RenderError(w, err, http.StatusBadRequest)
}
}
// helper function that reads the currently authenticated
// user from the given http.Request.
func readUser(r *http.Request) (*User, error) {
username := GetCookie(r, "_sess")
if len(username) == 0 {
return nil, fmt.Errorf("No user session")
}
// get the user from the database
user, err := database.GetUserEmail(username)
if err != nil || user == nil || user.ID == 0 {
return nil, err
}
return user, nil
}
// helper function that retrieves the repository based
// on the URL parameters
func readRepo(r *http.Request) (*Repo, error) {
// get the repo data from the URL parameters
hostParam := r.FormValue(":host")
userParam := r.FormValue(":owner")
nameParam := r.FormValue(":slug")
repoSlug := fmt.Sprintf("%s/%s/%s", hostParam, userParam, nameParam)
// get the repo from the database
return database.GetRepoSlug(repoSlug)
}
// helper function that sends the user to the login page.
func redirectLogin(w http.ResponseWriter, r *http.Request) {
v := url.Values{}
v.Add("return_to", r.URL.String())
http.Redirect(w, r, "/login?"+v.Encode(), http.StatusSeeOther)
}
func renderNotFound(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound)
RenderTemplate(w, "404.amber", nil)
}
func renderBadRequest(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusBadRequest)
RenderTemplate(w, "500.amber", nil)
}

302
pkg/handler/hooks.go Normal file
View file

@ -0,0 +1,302 @@
package handler
import (
"database/sql"
"net/http"
"strconv"
"time"
"github.com/drone/drone/pkg/build/script"
"github.com/drone/drone/pkg/database"
. "github.com/drone/drone/pkg/model"
"github.com/drone/drone/pkg/queue"
"github.com/drone/go-github/github"
)
// Processes a generic POST-RECEIVE hook and
// attempts to trigger a build.
func Hook(w http.ResponseWriter, r *http.Request) error {
// if this is a pull request route
// to a different handler
if r.Header.Get("X-Github-Event") == "pull_request" {
PullRequestHook(w, r)
return nil
}
// get the payload of the message
// this should contain a json representation of the
// repository and commit details
payload := r.FormValue("payload")
// parse the github Hook payload
hook, err := github.ParseHook([]byte(payload))
if err != nil {
println("could not parse hook")
return RenderText(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
}
// make sure this is being triggered because of a commit
// and not something like a tag deletion or whatever
if hook.IsTag() || hook.IsGithubPages() ||
hook.IsHead() == false || hook.IsDeleted() {
return RenderText(w, http.StatusText(http.StatusOK), http.StatusOK)
}
// get the repo from the URL
repoId := r.FormValue("id")
// get the repo from the database, return error if not found
repo, err := database.GetRepoSlug(repoId)
if err != nil {
return RenderText(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
}
// Get the user that owns the repository
user, err := database.GetUser(repo.UserID)
if err != nil {
return RenderText(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
}
// Verify that the commit doesn't already exist.
// We should never build the same commit twice.
_, err = database.GetCommitHash(hook.Head.Id, repo.ID)
if err != nil && err != sql.ErrNoRows {
println("commit already exists")
return RenderText(w, http.StatusText(http.StatusBadGateway), http.StatusBadGateway)
}
// we really only need:
// * repo owner
// * repo name
// * repo host (github)
// * commit hash
// * commit timestamp
// * commit branch
// * commit message
// * commit author
// * pull request
// once we have this data we could just send directly to the queue
// and let it handle everything else
commit := &Commit{}
commit.RepoID = repo.ID
commit.Branch = hook.Branch()
commit.Hash = hook.Head.Id
commit.Status = "Pending"
commit.Created = time.Now().UTC()
// extract the author and message from the commit
// this is kind of experimental, since I don't know
// what I'm doing here.
if hook.Head != nil && hook.Head.Author != nil {
commit.Message = hook.Head.Message
commit.Timestamp = hook.Head.Timestamp
commit.SetAuthor(hook.Head.Author.Email)
} else if hook.Commits != nil && len(hook.Commits) > 0 && hook.Commits[0].Author != nil {
commit.Message = hook.Commits[0].Message
commit.Timestamp = hook.Commits[0].Timestamp
commit.SetAuthor(hook.Commits[0].Author.Email)
}
// get the drone.yml file from GitHub
client := github.New(user.GithubToken)
content, err := client.Contents.FindRef(repo.Owner, repo.Slug, ".drone.yml", commit.Branch) // TODO should this really be the hash??
if err != nil {
msg := "No .drone.yml was found in this repository. You need to add one.\n"
if err := saveFailedBuild(commit, msg); err != nil {
return RenderText(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}
return RenderText(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
}
// decode the content. Note: Not sure this will ever happen...it basically means a GitHub API issue
raw, err := content.DecodeContent()
if err != nil {
msg := "Could not decode the yaml from GitHub. Check that your .drone.yml is a valid yaml file.\n"
if err := saveFailedBuild(commit, msg); err != nil {
return RenderText(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}
return RenderText(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
}
// parse the build script
buildscript, err := script.ParseBuild(raw)
if err != nil {
msg := "Could not parse your .drone.yml file. It needs to be a valid drone yaml file.\n\n" + err.Error() + "\n"
if err := saveFailedBuild(commit, msg); err != nil {
return RenderText(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}
return RenderText(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
}
// save the commit to the database
if err := database.SaveCommit(commit); err != nil {
return RenderText(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}
// save the build to the database
build := &Build{}
build.Slug = "1" // TODO
build.CommitID = commit.ID
build.Created = time.Now().UTC()
build.Status = "Pending"
if err := database.SaveBuild(build); err != nil {
return RenderText(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}
// notify websocket that a new build is pending
//realtime.CommitPending(repo.UserID, repo.TeamID, repo.ID, commit.ID, repo.Private)
//realtime.BuildPending(repo.UserID, repo.TeamID, repo.ID, commit.ID, build.ID, repo.Private)
queue.Add(&queue.BuildTask{Repo: repo, Commit: commit, Build: build, Script: buildscript}) //Push(repo, commit, build, buildscript)
// OK!
return RenderText(w, http.StatusText(http.StatusOK), http.StatusOK)
}
func PullRequestHook(w http.ResponseWriter, r *http.Request) {
// get the payload of the message
// this should contain a json representation of the
// repository and commit details
payload := r.FormValue("payload")
println("GOT PR HOOK")
println(payload)
hook, err := github.ParsePullRequestHook([]byte(payload))
if err != nil {
RenderText(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
// ignore these
if hook.Action != "opened" && hook.Action != "synchronize" {
RenderText(w, http.StatusText(http.StatusOK), http.StatusOK)
return
}
// get the repo from the URL
repoId := r.FormValue("id")
// get the repo from the database, return error if not found
repo, err := database.GetRepoSlug(repoId)
if err != nil {
RenderText(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
return
}
// Get the user that owns the repository
user, err := database.GetUser(repo.UserID)
if err != nil {
RenderText(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
// Verify that the commit doesn't already exist.
// We should enver build the same commit twice.
_, err = database.GetCommitHash(hook.PullRequest.Head.Sha, repo.ID)
if err != nil && err != sql.ErrNoRows {
RenderText(w, http.StatusText(http.StatusBadGateway), http.StatusBadGateway)
return
}
///////////////////////////////////////////////////////
commit := &Commit{}
commit.RepoID = repo.ID
commit.Branch = hook.PullRequest.Head.Ref
commit.Hash = hook.PullRequest.Head.Sha
commit.Status = "Pending"
commit.Created = time.Now().UTC()
commit.Gravatar = hook.PullRequest.User.GravatarId
commit.PullRequest = strconv.Itoa(hook.Number)
commit.Message = hook.PullRequest.Title
// label := p.PullRequest.Head.Labe
// get the drone.yml file from GitHub
client := github.New(user.GithubToken)
content, err := client.Contents.FindRef(repo.Owner, repo.Slug, ".drone.yml", commit.Hash) // TODO should this really be the hash??
if err != nil {
println(err.Error())
RenderText(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
// decode the content
raw, err := content.DecodeContent()
if err != nil {
RenderText(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
// parse the build script
buildscript, err := script.ParseBuild(raw)
if err != nil {
// TODO if the YAML is invalid we should create a commit record
// with an ERROR status so that the user knows why a build wasn't
// triggered in the system
RenderText(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
// save the commit to the database
if err := database.SaveCommit(commit); err != nil {
RenderText(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
// save the build to the database
build := &Build{}
build.Slug = "1" // TODO
build.CommitID = commit.ID
build.Created = time.Now().UTC()
build.Status = "Pending"
if err := database.SaveBuild(build); err != nil {
RenderText(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
// notify websocket that a new build is pending
// TODO we should, for consistency, just put this inside Queue.Add()
queue.Add(&queue.BuildTask{Repo: repo, Commit: commit, Build: build, Script: buildscript})
// OK!
RenderText(w, http.StatusText(http.StatusOK), http.StatusOK)
}
// Helper method for saving a failed build or commit in the case where it never starts to build.
// This can happen if the yaml is bad or doesn't exist.
func saveFailedBuild(commit *Commit, msg string) error {
// Set the commit to failed
commit.Status = "Failure"
commit.Created = time.Now().UTC()
commit.Finished = commit.Created
commit.Duration = 0
if err := database.SaveCommit(commit); err != nil {
return err
}
// save the build to the database
build := &Build{}
build.Slug = "1" // TODO: This should not be hardcoded
build.CommitID = commit.ID
build.Created = time.Now().UTC()
build.Finished = build.Created
commit.Duration = 0
build.Status = "Failure"
build.Stdout = msg
if err := database.SaveBuild(build); err != nil {
return err
}
// TODO: Should the status be Error instead of Failure?
// TODO: Do we need to update the branch table too?
return nil
}

227
pkg/handler/members.go Normal file
View file

@ -0,0 +1,227 @@
package handler
import (
"fmt"
"net/http"
"strconv"
"time"
"github.com/dchest/authcookie"
"github.com/drone/drone/pkg/database"
"github.com/drone/drone/pkg/mail"
. "github.com/drone/drone/pkg/model"
)
// Display a list of Team Members.
func TeamMembers(w http.ResponseWriter, r *http.Request, u *User) error {
teamParam := r.FormValue(":team")
team, err := database.GetTeamSlug(teamParam)
if err != nil {
return err
}
// user must be a team member admin
if member, _ := database.IsMemberAdmin(u.ID, team.ID); !member {
return fmt.Errorf("Forbidden")
}
members, err := database.ListMembers(team.ID)
if err != nil {
return err
}
data := struct {
User *User
Team *Team
Members []*Member
}{u, team, members}
return RenderTemplate(w, "team_members.html", &data)
}
// Return an HTML form for creating a new Team Member.
func TeamMemberAdd(w http.ResponseWriter, r *http.Request, u *User) error {
teamParam := r.FormValue(":team")
team, err := database.GetTeamSlug(teamParam)
if err != nil {
return err
}
if member, _ := database.IsMemberAdmin(u.ID, team.ID); !member {
return fmt.Errorf("Forbidden")
}
data := struct {
User *User
Team *Team
}{u, team}
return RenderTemplate(w, "members_add.html", &data)
}
// Return an HTML form for editing a Team Member.
func TeamMemberEdit(w http.ResponseWriter, r *http.Request, u *User) error {
teamParam := r.FormValue(":team")
team, err := database.GetTeamSlug(teamParam)
if err != nil {
return err
}
if member, _ := database.IsMemberAdmin(u.ID, team.ID); !member {
return fmt.Errorf("Forbidden")
}
// get the ID from the URL parameter
idstr := r.FormValue("id")
id, err := strconv.Atoi(idstr)
if err != nil {
return err
}
user, err := database.GetUser(int64(id))
if err != nil {
return err
}
member, err := database.GetMember(user.ID, team.ID)
if err != nil {
return err
}
data := struct {
User *User
Team *Team
Member *Member
}{u, team, member}
return RenderTemplate(w, "members_edit.html", &data)
}
// Update a specific Team Member.
func TeamMemberUpdate(w http.ResponseWriter, r *http.Request, u *User) error {
roleParam := r.FormValue("Role")
teamParam := r.FormValue(":team")
// get the team from the database
team, err := database.GetTeamSlug(teamParam)
if err != nil {
return RenderError(w, err, http.StatusNotFound)
}
// verify the user is a admin member of the team
if member, _ := database.IsMemberAdmin(u.ID, team.ID); !member {
return fmt.Errorf("Forbidden")
}
// get the ID from the URL parameter
idstr := r.FormValue("id")
id, err := strconv.Atoi(idstr)
if err != nil {
return err
}
// get the user from the database
user, err := database.GetUser(int64(id))
if err != nil {
return RenderError(w, err, http.StatusNotFound)
}
// add the user to the team
if err := database.SaveMember(user.ID, team.ID, roleParam); err != nil {
return RenderError(w, err, http.StatusInternalServerError)
}
return RenderText(w, http.StatusText(http.StatusOK), http.StatusOK)
}
// Delete a specific Team Member.
func TeamMemberDelete(w http.ResponseWriter, r *http.Request, u *User) error {
// get the team from the database
teamParam := r.FormValue(":team")
team, err := database.GetTeamSlug(teamParam)
if err != nil {
return RenderNotFound(w)
}
if member, _ := database.IsMemberAdmin(u.ID, team.ID); !member {
return fmt.Errorf("Forbidden")
}
// get the ID from the URL parameter
idstr := r.FormValue("id")
id, err := strconv.Atoi(idstr)
if err != nil {
return err
}
// get the user from the database
user, err := database.GetUser(int64(id))
if err != nil {
return RenderNotFound(w)
}
// must be at least 1 member
members, err := database.ListMembers(team.ID)
if err != nil {
return err
} else if len(members) == 1 {
return fmt.Errorf("There must be at least 1 member per team")
}
// delete the member
database.DeleteMember(user.ID, team.ID)
http.Redirect(w, r, fmt.Sprintf("/account/team/%s/members", team.Name), http.StatusSeeOther)
return nil
}
// Invite a new Team Member.
func TeamMemberInvite(w http.ResponseWriter, r *http.Request, u *User) error {
teamParam := r.FormValue(":team")
mailParam := r.FormValue("email")
team, err := database.GetTeamSlug(teamParam)
if err != nil {
return RenderError(w, err, http.StatusNotFound)
}
if member, _ := database.IsMemberAdmin(u.ID, team.ID); !member {
return fmt.Errorf("Forbidden")
}
// generate a token that is valid for 3 days to join the team
token := authcookie.New(team.Name, time.Now().Add(72*time.Hour), secret)
// hostname from settings
hostname := database.SettingsMust().URL().String()
emailEnabled := database.SettingsMust().SmtpServer != ""
if !emailEnabled {
// Email is not enabled, so must let the user know the signup link
link := fmt.Sprintf("%v/accept?token=%v", hostname, token)
return RenderText(w, link, http.StatusOK)
}
// send the invitation
data := struct {
User *User
Team *Team
Token string
Host string
}{u, team, token, hostname}
// send email async
go mail.SendInvitation(team.Name, mailParam, &data)
return RenderText(w, http.StatusText(http.StatusOK), http.StatusOK)
}
func TeamMemberAccept(w http.ResponseWriter, r *http.Request, u *User) error {
// get the team name from the token
token := r.FormValue("token")
teamName := authcookie.Login(token, secret)
if len(teamName) == 0 {
return ErrInvalidTeamName
}
// get the team from the database
// TODO it might make more sense to use the ID in case the Slug changes
team, err := database.GetTeamSlug(teamName)
if err != nil {
return RenderError(w, err, http.StatusNotFound)
}
// add the user to the team.
// by default the user has write access to the team, which means
// they can add and manage new repositories.
if err := database.SaveMember(u.ID, team.ID, RoleWrite); err != nil {
return RenderError(w, err, http.StatusInternalServerError)
}
// send the user to the dashboard
http.Redirect(w, r, "/dashboard/team/"+team.Name, http.StatusSeeOther)
return nil
}

281
pkg/handler/repos.go Normal file
View file

@ -0,0 +1,281 @@
package handler
import (
"fmt"
"net/http"
"github.com/drone/drone/pkg/channel"
"github.com/drone/drone/pkg/database"
. "github.com/drone/drone/pkg/model"
"github.com/drone/go-github/github"
"launchpad.net/goyaml"
)
// Display a Repository dashboard.
func RepoDashboard(w http.ResponseWriter, r *http.Request, u *User, repo *Repo) error {
branch := r.FormValue(":branch")
// get a list of all branches
branches, err := database.ListBranches(repo.ID)
if err != nil {
return err
}
// if no branch is provided then we'll
// want to use a default value.
if len(branch) == 0 {
branch = repo.DefaultBranch()
}
// get a list of recent commits for the
// repository and specific branch
commits, err := database.ListCommits(repo.ID, branch)
if err != nil {
return err
}
// get a token that can be exchanged with the
// websocket handler to authorize listening
// for a stream of changes for this repository
token := channel.Create(repo.Slug)
data := struct {
User *User
Repo *Repo
Branches []*Commit
Commits []*Commit
Branch string
Token string
}{u, repo, branches, commits, branch, token}
return RenderTemplate(w, "repo_dashboard.html", &data)
}
func RepoAdd(w http.ResponseWriter, r *http.Request, u *User) error {
teams, err := database.ListTeams(u.ID)
if err != nil {
return err
}
data := struct {
User *User
Teams []*Team
}{u, teams}
// if the user hasn't linked their GitHub account
// render a different template
if len(u.GithubToken) == 0 {
return RenderTemplate(w, "github_link.html", &data)
}
// otherwise display the template for adding
// a new GitHub repository.
return RenderTemplate(w, "github_add.html", &data)
}
func RepoCreateGithub(w http.ResponseWriter, r *http.Request, u *User) error {
teamName := r.FormValue("team")
owner := r.FormValue("owner")
name := r.FormValue("name")
// get the github settings from the database
settings := database.SettingsMust()
// create the GitHub client
client := github.New(u.GithubToken)
githubRepo, err := client.Repos.Find(owner, name)
if err != nil {
return err
}
repo, err := NewGitHubRepo(owner, name, githubRepo.Private)
if err != nil {
return err
}
repo.UserID = u.ID
// if the user chose to assign to a team account
// we need to retrieve the team, verify the user
// has access, and then set the team id.
if len(teamName) > 0 {
team, err := database.GetTeamSlug(teamName)
if err != nil {
return err
}
// user must be an admin member of the team
if ok, _ := database.IsMemberAdmin(u.ID, team.ID); !ok {
return fmt.Errorf("Forbidden")
}
repo.TeamID = team.ID
}
// if the repository is private we'll need
// to upload a github key to the repository
if repo.Private {
// name the key
keyName := fmt.Sprintf("%s@%s", repo.Owner, settings.Domain)
// create the github key, or update if one already exists
_, err := client.RepoKeys.CreateUpdate(owner, name, repo.PublicKey, keyName)
if err != nil {
return fmt.Errorf("Unable to add Private Key to your GitHub repository")
}
}
// create a hook so that we get notified when code
// is pushed to the repository and can execute a build.
link := fmt.Sprintf("%s://%s/hook/github.com?id=%s", settings.Scheme, settings.Domain, repo.Slug)
// add the hook
if _, err := client.Hooks.CreateUpdate(owner, name, link); err != nil {
return fmt.Errorf("Unable to add Hook to your GitHub repository. %s", err.Error())
}
// Save to the database
if err := database.SaveRepo(repo); err != nil {
return err
}
return RenderText(w, http.StatusText(http.StatusOK), http.StatusOK)
}
// Repository Settings
func RepoSettingsForm(w http.ResponseWriter, r *http.Request, u *User, repo *Repo) error {
// get the list of teams
teams, err := database.ListTeams(u.ID)
if err != nil {
return err
}
data := struct {
Repo *Repo
User *User
Teams []*Team
Owner *User
Team *Team
}{Repo: repo, User: u, Teams: teams}
// get the repo owner
if repo.TeamID > 0 {
data.Team, err = database.GetTeam(repo.TeamID)
if err != nil {
return err
}
}
// get the team owner
data.Owner, err = database.GetUser(repo.UserID)
if err != nil {
return err
}
return RenderTemplate(w, "repo_settings.html", &data)
}
// Repository Params (YAML parameters) Form
func RepoParamsForm(w http.ResponseWriter, r *http.Request, u *User, repo *Repo) error {
data := struct {
Repo *Repo
User *User
Textarea string
}{repo, u, ""}
if repo.Params != nil && len(repo.Params) != 0 {
raw, _ := goyaml.Marshal(&repo.Params)
data.Textarea = string(raw)
}
return RenderTemplate(w, "repo_params.html", &data)
}
func RepoBadges(w http.ResponseWriter, r *http.Request, u *User, repo *Repo) error {
// hostname from settings
hostname := database.SettingsMust().URL().String()
data := struct {
Repo *Repo
User *User
Host string
}{repo, u, hostname}
return RenderTemplate(w, "repo_badges.html", &data)
}
func RepoKeys(w http.ResponseWriter, r *http.Request, u *User, repo *Repo) error {
data := struct {
Repo *Repo
User *User
}{repo, u}
return RenderTemplate(w, "repo_keys.html", &data)
}
// Updates an existing repository.
func RepoUpdate(w http.ResponseWriter, r *http.Request, u *User, repo *Repo) error {
switch r.FormValue("action") {
case "params":
repo.Params = map[string]string{}
if err := goyaml.Unmarshal([]byte(r.FormValue("params")), &repo.Params); err != nil {
return err
}
default:
repo.Disabled = len(r.FormValue("Disabled")) == 0
repo.DisabledPullRequest = len(r.FormValue("DisabledPullRequest")) == 0
// value of "" indicates the currently authenticated user
// should be set as the administrator.
if len(r.FormValue("Owner")) == 0 {
repo.UserID = u.ID
repo.TeamID = 0
} else {
// else the user has chosen a team
team, err := database.GetTeamSlug(r.FormValue("Owner"))
if err != nil {
return err
}
// verify the user is a member of the team
if member, _ := database.IsMemberAdmin(u.ID, team.ID); !member {
return fmt.Errorf("Forbidden")
}
// set the team ID
repo.TeamID = team.ID
}
}
// save the page
if err := database.SaveRepo(repo); err != nil {
return err
}
http.Redirect(w, r, r.URL.Path, http.StatusSeeOther)
return nil
}
// Deletes a specific repository.
func RepoDeleteForm(w http.ResponseWriter, r *http.Request, u *User, repo *Repo) error {
data := struct {
Repo *Repo
User *User
}{repo, u}
return RenderTemplate(w, "repo_delete.html", &data)
}
// Deletes a specific repository.
func RepoDelete(w http.ResponseWriter, r *http.Request, u *User, repo *Repo) error {
// the user must confirm their password before deleting
password := r.FormValue("password")
if err := u.ComparePassword(password); err != nil {
return err
}
// delete the repo
if err := database.DeleteRepo(repo.ID); err != nil {
return err
}
http.Redirect(w, r, "/dashboard", http.StatusSeeOther)
return nil
}

152
pkg/handler/teams.go Normal file
View file

@ -0,0 +1,152 @@
package handler
import (
"fmt"
"net/http"
"github.com/drone/drone/pkg/database"
. "github.com/drone/drone/pkg/model"
)
// Display a specific Team.
func TeamShow(w http.ResponseWriter, r *http.Request, u *User) error {
teamParam := r.FormValue(":team")
team, err := database.GetTeamSlug(teamParam)
if err != nil {
return err
}
if member, _ := database.IsMember(u.ID, team.ID); !member {
return fmt.Errorf("Forbidden")
}
// list of repositories owned by Team
repos, err := database.ListReposTeam(team.ID)
if err != nil {
return err
}
// list all user teams
teams, err := database.ListTeams(u.ID)
if err != nil {
return err
}
// list of recent commits
commits, err := database.ListCommitsTeam(team.ID)
if err != nil {
return err
}
data := struct {
User *User
Team *Team
Teams []*Team
Repos []*Repo
Commits []*RepoCommit
}{u, team, teams, repos, commits}
return RenderTemplate(w, "team_dashboard.html", &data)
}
// Return an HTML form for editing a Team.
func TeamEdit(w http.ResponseWriter, r *http.Request, u *User) error {
teamParam := r.FormValue(":team")
team, err := database.GetTeamSlug(teamParam)
if err != nil {
return err
}
if member, _ := database.IsMemberAdmin(u.ID, team.ID); !member {
return fmt.Errorf("Forbidden")
}
data := struct {
User *User
Team *Team
}{u, team}
return RenderTemplate(w, "team_profile.html", &data)
}
// Return an HTML form for creating a Team.
func TeamAdd(w http.ResponseWriter, r *http.Request, u *User) error {
return RenderTemplate(w, "user_teams_add.html", struct{ User *User }{u})
}
// Create a new Team.
func TeamCreate(w http.ResponseWriter, r *http.Request, u *User) error {
// set the name and email from the form data
team := Team{}
team.SetName(r.FormValue("name"))
team.SetEmail(r.FormValue("email"))
if err := team.Validate(); err != nil {
return RenderError(w, err, http.StatusBadRequest)
}
if err := database.SaveTeam(&team); err != nil {
return RenderError(w, err, http.StatusBadRequest)
}
// add default member to the team (me)
if err := database.SaveMember(u.ID, team.ID, RoleOwner); err != nil {
return RenderError(w, err, http.StatusInternalServerError)
}
return RenderText(w, http.StatusText(http.StatusOK), http.StatusOK)
}
// Update a specific Team.
func TeamUpdate(w http.ResponseWriter, r *http.Request, u *User) error {
// get team from the database
teamName := r.FormValue(":team")
team, err := database.GetTeamSlug(teamName)
if err != nil {
return fmt.Errorf("Forbidden")
}
if member, _ := database.IsMemberAdmin(u.ID, team.ID); !member {
return fmt.Errorf("Forbidden")
}
team.Name = r.FormValue("name")
team.SetEmail(r.FormValue("email"))
if err := team.Validate(); err != nil {
return RenderError(w, err, http.StatusBadRequest)
}
if err := database.SaveTeam(team); err != nil {
return RenderError(w, err, http.StatusBadRequest)
}
return RenderText(w, http.StatusText(http.StatusOK), http.StatusOK)
}
// Delete Confirmation Page
func TeamDeleteConfirm(w http.ResponseWriter, r *http.Request, u *User) error {
teamParam := r.FormValue(":team")
team, err := database.GetTeamSlug(teamParam)
if err != nil {
return err
}
if member, _ := database.IsMemberAdmin(u.ID, team.ID); !member {
return fmt.Errorf("Forbidden")
}
data := struct {
User *User
Team *Team
}{u, team}
return RenderTemplate(w, "team_delete.html", &data)
}
// Delete a specific Team.
func TeamDelete(w http.ResponseWriter, r *http.Request, u *User) error {
// get the team from the database
teamParam := r.FormValue(":team")
team, err := database.GetTeamSlug(teamParam)
if err != nil {
return RenderNotFound(w)
}
if member, _ := database.IsMemberAdmin(u.ID, team.ID); !member {
return fmt.Errorf("Forbidden")
}
// the user must confirm their password before deleting
password := r.FormValue("password")
if err := u.ComparePassword(password); err != nil {
return RenderError(w, err, http.StatusBadRequest)
}
database.DeleteTeam(team.ID)
http.Redirect(w, r, "/account/user/teams", http.StatusSeeOther)
return nil
}

View file

@ -0,0 +1,173 @@
package testing
import (
//"net/http"
//"net/http/httptest"
//"net/url"
"testing"
//"github.com/drone/drone/database"
. "github.com/drone/drone/database/testing"
//"github.com/drone/drone/handler"
. "github.com/smartystreets/goconvey/convey"
)
func TestTeamProfilePage(t *testing.T) {
// seed the database with values
Setup()
defer Teardown()
// dummy request
//req := http.Request{}
//req.Form = url.Values{}
Convey("Team Profile Page", t, func() {
Convey("View Profile Information", func() {
SkipConvey("Email Address is correct", func() {
})
SkipConvey("Team Name is correct", func() {
})
SkipConvey("GitHub Login is correct", func() {
})
SkipConvey("Bitbucket Login is correct", func() {
})
})
Convey("Update Email Address", func() {
SkipConvey("With a Valid Email Address", func() {
})
SkipConvey("With an Invalid Email Address", func() {
})
SkipConvey("With an Empty Email Address", func() {
})
})
Convey("Update Team Name", func() {
SkipConvey("With a Valid Name", func() {
})
SkipConvey("With an Invalid Name", func() {
})
SkipConvey("With an Empty Name", func() {
})
})
Convey("Delete the Team", func() {
SkipConvey("Providing an Invalid Password", func() {
})
SkipConvey("Providing a Valid Password", func() {
})
})
})
}
func TestTeamMembersPage(t *testing.T) {
// seed the database with values
Setup()
defer Teardown()
// dummy request
//req := http.Request{}
//req.Form = url.Values{}
Convey("Team Members Page", t, func() {
SkipConvey("View List of Team Members", func() {
})
SkipConvey("Add a New Team Member", func() {
})
Convey("Edit a Team Member", func() {
SkipConvey("Modify the Role", func() {
})
SkipConvey("Change to an Invalid Role", func() {
})
SkipConvey("Change from Owner to Read", func() {
})
})
Convey("Delete a Team Member", func() {
SkipConvey("Delete a Read-only Member", func() {
})
SkipConvey("Delete the Last Member", func() {
})
SkipConvey("Delete the Owner", func() {
})
})
Convey("Accept Membership", func() {
SkipConvey("Valid Invitation", func() {
})
SkipConvey("Expired Invitation", func() {
})
SkipConvey("Invalid or Forged Invitation", func() {
})
})
})
}
func TestDashboardPage(t *testing.T) {
// seed the database with values
Setup()
defer Teardown()
// dummy request
//req := http.Request{}
//req.Form = url.Values{}
SkipConvey("Team Dashboard", t, func() {
})
SkipConvey("User Dashboard", t, func() {
})
SkipConvey("Repo Dashboard", t, func() {
})
SkipConvey("Repo Settings", t, func() {
})
SkipConvey("Commit Dashboard", t, func() {
})
Convey("User Account", t, func() {
SkipConvey("Login", func() {
})
SkipConvey("Logout", func() {
})
SkipConvey("Register", func() {
})
SkipConvey("Sign Up", func() {
})
})
}

View file

@ -0,0 +1,172 @@
package testing
import (
"net/http"
"net/http/httptest"
"net/url"
"testing"
"github.com/drone/drone/database"
. "github.com/drone/drone/database/testing"
"github.com/drone/drone/handler"
. "github.com/drone/drone/model"
. "github.com/smartystreets/goconvey/convey"
)
func TestUserProfilePage(t *testing.T) {
// seed the database with values
Setup()
defer Teardown()
// dummy request
req := http.Request{}
req.Form = url.Values{}
Convey("User Profile", t, func() {
SkipConvey("View Profile Information", func() {
user, _ := database.GetUser(1)
res := httptest.NewRecorder()
handler.UserUpdate(res, &req, user)
Convey("Email Address is correct", func() {
})
Convey("User Name is correct", func() {
})
})
Convey("Update Email Address", func() {
Convey("With a Valid Email Address", func() {
user, _ := database.GetUser(1)
req.Form.Set("name", "John Smith")
req.Form.Set("email", "John.Smith@gmail.com")
res := httptest.NewRecorder()
handler.UserUpdate(res, &req, user)
So(res.Code, ShouldEqual, http.StatusOK)
})
Convey("With an Invalid Email Address", func() {
user, _ := database.GetUser(1)
req.Form.Set("name", "John Smith")
req.Form.Set("email", "John.Smith")
res := httptest.NewRecorder()
handler.UserUpdate(res, &req, user)
So(res.Code, ShouldEqual, http.StatusBadRequest)
So(res.Body.String(), ShouldContainSubstring, ErrInvalidEmail.Error())
})
Convey("With an Empty Email Address", func() {
user, _ := database.GetUser(1)
req.Form.Set("name", "John Smith")
req.Form.Set("email", "")
res := httptest.NewRecorder()
handler.UserUpdate(res, &req, user)
So(res.Code, ShouldEqual, http.StatusBadRequest)
So(res.Body.String(), ShouldContainSubstring, ErrInvalidEmail.Error())
})
Convey("With a Duplicate Email Address", func() {
user, _ := database.GetUser(1)
req.Form.Set("name", "John Smith")
req.Form.Set("email", "cavepig@gmail.com")
res := httptest.NewRecorder()
handler.UserUpdate(res, &req, user)
So(res.Code, ShouldEqual, http.StatusBadRequest)
})
})
Convey("Update User Name", func() {
Convey("With a Valid Name", func() {
user, _ := database.GetUser(1)
req.Form.Set("name", "John Smith")
req.Form.Set("email", "John.Smith@gmail.com")
res := httptest.NewRecorder()
handler.UserUpdate(res, &req, user)
So(res.Code, ShouldEqual, http.StatusOK)
})
Convey("With an Empty Name", func() {
user, _ := database.GetUser(1)
req.Form.Set("name", "")
req.Form.Set("email", "John.Smith@gmail.com")
res := httptest.NewRecorder()
handler.UserUpdate(res, &req, user)
So(res.Code, ShouldEqual, http.StatusBadRequest)
So(res.Body.String(), ShouldContainSubstring, ErrInvalidUserName.Error())
})
})
Convey("Change Password", func() {
Convey("To a Valid Password", func() {
user, _ := database.GetUser(1)
req.Form.Set("password", "password123")
res := httptest.NewRecorder()
handler.UserPassUpdate(res, &req, user)
So(res.Code, ShouldEqual, http.StatusOK)
So(user.ComparePassword("password123"), ShouldBeNil)
})
Convey("To an Invalid Password, too short", func() {
user, _ := database.GetUser(1)
req.Form.Set("password", "123")
res := httptest.NewRecorder()
handler.UserPassUpdate(res, &req, user)
So(res.Code, ShouldEqual, http.StatusBadRequest)
})
})
Convey("Delete the Account", func() {
Convey("Providing an Invalid Password", func() {
user, _ := database.GetUser(1)
req.Form.Set("password", "password111")
res := httptest.NewRecorder()
handler.UserDelete(res, &req, user)
So(res.Code, ShouldEqual, http.StatusBadRequest)
})
SkipConvey("Providing a Valid Password", func() {
// TODO Skipping because there are no teampltes
// loaded which will cause a panic
user, _ := database.GetUser(2)
req.Form.Set("password", "password")
res := httptest.NewRecorder()
handler.UserDelete(res, &req, user)
So(res.Code, ShouldEqual, http.StatusOK)
})
})
})
}
func TestUserTeamPage(t *testing.T) {
// seed the database with values
Setup()
defer Teardown()
// dummy request
//req := http.Request{}
//req.Form = url.Values{}
Convey("User Team Page", t, func() {
SkipConvey("View List of Teams", func() {
})
SkipConvey("View Empty List of Teams", func() {
})
})
Convey("Create a Team", t, func() {
SkipConvey("With an Invalid Name", func() {
})
SkipConvey("With an Invalid Email", func() {
})
SkipConvey("With a Valid Name and Email", func() {
})
})
}

111
pkg/handler/users.go Normal file
View file

@ -0,0 +1,111 @@
package handler
import (
"net/http"
"github.com/drone/drone/pkg/database"
. "github.com/drone/drone/pkg/model"
)
// Display the dashboard for a specific user
func UserShow(w http.ResponseWriter, r *http.Request, u *User) error {
// list of repositories owned by User
repos, err := database.ListRepos(u.ID)
if err != nil {
return err
}
// list of user team accounts
teams, err := database.ListTeams(u.ID)
if err != nil {
return err
}
// list of recent commits
commits, err := database.ListCommitsUser(u.ID)
if err != nil {
return err
}
data := struct {
User *User
Repos []*Repo
Teams []*Team
Commits []*RepoCommit
}{u, repos, teams, commits}
return RenderTemplate(w, "user_dashboard.html", &data)
}
// return an HTML form for editing a user
func UserEdit(w http.ResponseWriter, r *http.Request, u *User) error {
return RenderTemplate(w, "user_profile.html", struct{ User *User }{u})
}
// return an HTML form for editing a user password
func UserPass(w http.ResponseWriter, r *http.Request, u *User) error {
return RenderTemplate(w, "user_password.html", struct{ User *User }{u})
}
// return an HTML form for deleting a user.
func UserDeleteConfirm(w http.ResponseWriter, r *http.Request, u *User) error {
return RenderTemplate(w, "user_delete.html", struct{ User *User }{u})
}
// update a specific user
func UserUpdate(w http.ResponseWriter, r *http.Request, u *User) error {
// set the name and email from the form data
u.Name = r.FormValue("name")
u.SetEmail(r.FormValue("email"))
if err := u.Validate(); err != nil {
return RenderError(w, err, http.StatusBadRequest)
}
if err := database.SaveUser(u); err != nil {
return RenderError(w, err, http.StatusBadRequest)
}
return RenderText(w, http.StatusText(http.StatusOK), http.StatusOK)
}
// update a specific user's password
func UserPassUpdate(w http.ResponseWriter, r *http.Request, u *User) error {
// set the name and email from the form data
pass := r.FormValue("password")
if err := u.SetPassword(pass); err != nil {
return RenderError(w, err, http.StatusBadRequest)
}
// save the updated password to the database
if err := database.SaveUser(u); err != nil {
return RenderError(w, err, http.StatusBadRequest)
}
return RenderText(w, http.StatusText(http.StatusOK), http.StatusOK)
}
// delete a specific user.
func UserDelete(w http.ResponseWriter, r *http.Request, u *User) error {
// the user must confirm their password before deleting
password := r.FormValue("password")
if err := u.ComparePassword(password); err != nil {
return RenderError(w, err, http.StatusBadRequest)
}
// TODO we need to delete all repos, builds, commits, branches, etc
// TODO we should transfer ownership of all team-owned projects to the team owner
// delete the account
if err := database.DeleteUser(u.ID); err != nil {
return RenderError(w, err, http.StatusBadRequest)
}
Logout(w, r)
return nil
}
// Display a list of all Teams for the currently authenticated User.
func UserTeams(w http.ResponseWriter, r *http.Request, u *User) error {
teams, err := database.ListTeams(u.ID)
if err != nil {
return err
}
data := struct {
User *User
Teams []*Team
}{u, teams}
return RenderTemplate(w, "user_teams.html", &data)
}

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