mirror of
https://github.com/woodpecker-ci/woodpecker.git
synced 2024-12-24 10:07:21 +02:00
initial public commit
This commit is contained in:
commit
d5e5797934
24
.drone.yml
Normal file
24
.drone.yml
Normal 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
14
.gitignore
vendored
Normal 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
5
AUTHORS
Normal 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
202
LICENSE
Normal 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
78
Makefile
Normal 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
128
README.md
Normal 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
1
bin/README.md
Normal 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
288
cmd/drone/drone.go
Normal 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
90
cmd/drone/util.go
Normal 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)
|
||||
}
|
1076
cmd/droned/assets/css/drone.css
Normal file
1076
cmd/droned/assets/css/drone.css
Normal file
File diff suppressed because it is too large
Load Diff
1263
cmd/droned/assets/css/drone.less
Normal file
1263
cmd/droned/assets/css/drone.less
Normal file
File diff suppressed because it is too large
Load Diff
BIN
cmd/droned/assets/img/build_failing.png
Normal file
BIN
cmd/droned/assets/img/build_failing.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.2 KiB |
BIN
cmd/droned/assets/img/build_none.png
Normal file
BIN
cmd/droned/assets/img/build_none.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.2 KiB |
BIN
cmd/droned/assets/img/build_success.png
Normal file
BIN
cmd/droned/assets/img/build_success.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.4 KiB |
BIN
cmd/droned/assets/img/build_unknown.png
Normal file
BIN
cmd/droned/assets/img/build_unknown.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.1 KiB |
BIN
cmd/droned/assets/img/favicon.ico
Normal file
BIN
cmd/droned/assets/img/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.4 KiB |
BIN
cmd/droned/assets/img/favicon.png
Normal file
BIN
cmd/droned/assets/img/favicon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 442 B |
187
cmd/droned/drone.go
Normal file
187
cmd/droned/drone.go
Normal 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
7
deb/drone/DEBIAN/control
Normal 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
|
8
deb/drone/etc/init/drone.conf
Normal file
8
deb/drone/etc/init/drone.conf
Normal 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
471
pkg/build/build.go
Normal 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)
|
||||
}
|
72
pkg/build/buildfile/buildfile.go
Normal file
72
pkg/build/buildfile/buildfile.go
Normal 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
258
pkg/build/docker/client.go
Normal 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
|
||||
}
|
147
pkg/build/docker/container.go
Normal file
147
pkg/build/docker/container.go
Normal 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
124
pkg/build/docker/image.go
Normal 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
166
pkg/build/docker/structs.go
Normal 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
|
||||
}
|
44
pkg/build/dockerfile/dockerfile.go
Normal file
44
pkg/build/dockerfile/dockerfile.go
Normal 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
238
pkg/build/images.go
Normal 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
105
pkg/build/log/log.go
Normal 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
41
pkg/build/proxy/proxy.go
Normal 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())
|
||||
}
|
32
pkg/build/proxy/proxy_test.go
Normal file
32
pkg/build/proxy/proxy_test.go
Normal 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
118
pkg/build/repo/repo.go
Normal 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
|
||||
}
|
54
pkg/build/repo/repo_test.go
Normal file
54
pkg/build/repo/repo_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
12
pkg/build/script/deployment/appfog.go
Normal file
12
pkg/build/script/deployment/appfog.go
Normal 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) {
|
||||
|
||||
}
|
12
pkg/build/script/deployment/cloudcontrol.go
Normal file
12
pkg/build/script/deployment/cloudcontrol.go
Normal 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) {
|
||||
|
||||
}
|
12
pkg/build/script/deployment/cloudfoundry.go
Normal file
12
pkg/build/script/deployment/cloudfoundry.go
Normal 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) {
|
||||
|
||||
}
|
42
pkg/build/script/deployment/deployment.go
Normal file
42
pkg/build/script/deployment/deployment.go
Normal 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)
|
||||
}
|
||||
}
|
12
pkg/build/script/deployment/engineyard.go
Normal file
12
pkg/build/script/deployment/engineyard.go
Normal 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) {
|
||||
|
||||
}
|
1
pkg/build/script/deployment/git.go
Normal file
1
pkg/build/script/deployment/git.go
Normal file
@ -0,0 +1 @@
|
||||
package deployment
|
38
pkg/build/script/deployment/heroku.go
Normal file
38
pkg/build/script/deployment/heroku.go
Normal 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"))
|
||||
}
|
||||
}
|
12
pkg/build/script/deployment/nodejitsu.go
Normal file
12
pkg/build/script/deployment/nodejitsu.go
Normal 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) {
|
||||
|
||||
}
|
12
pkg/build/script/deployment/openshift.go
Normal file
12
pkg/build/script/deployment/openshift.go
Normal 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) {
|
||||
|
||||
}
|
1
pkg/build/script/deployment/ssh.go
Normal file
1
pkg/build/script/deployment/ssh.go
Normal file
@ -0,0 +1 @@
|
||||
package deployment
|
85
pkg/build/script/notification/email.go
Normal file
85
pkg/build/script/notification/email.go
Normal 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`
|
64
pkg/build/script/notification/hipchat.go
Normal file
64
pkg/build/script/notification/hipchat.go
Normal 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)
|
||||
}
|
1
pkg/build/script/notification/irc.go
Normal file
1
pkg/build/script/notification/irc.go
Normal file
@ -0,0 +1 @@
|
||||
package notification
|
53
pkg/build/script/notification/notification.go
Normal file
53
pkg/build/script/notification/notification.go
Normal 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
|
||||
}
|
59
pkg/build/script/notification/webhook.go
Normal file
59
pkg/build/script/notification/webhook.go
Normal 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()
|
||||
}
|
1
pkg/build/script/notification/zapier.go
Normal file
1
pkg/build/script/notification/zapier.go
Normal file
@ -0,0 +1 @@
|
||||
package notification
|
1
pkg/build/script/publish/bintray.go
Normal file
1
pkg/build/script/publish/bintray.go
Normal file
@ -0,0 +1 @@
|
||||
package publish
|
1
pkg/build/script/publish/dropbox.go
Normal file
1
pkg/build/script/publish/dropbox.go
Normal file
@ -0,0 +1 @@
|
||||
package publish
|
1
pkg/build/script/publish/gems.go
Normal file
1
pkg/build/script/publish/gems.go
Normal file
@ -0,0 +1 @@
|
||||
package publish
|
1
pkg/build/script/publish/maven.go
Normal file
1
pkg/build/script/publish/maven.go
Normal file
@ -0,0 +1 @@
|
||||
package publish
|
1
pkg/build/script/publish/npm.go
Normal file
1
pkg/build/script/publish/npm.go
Normal file
@ -0,0 +1 @@
|
||||
package publish
|
1
pkg/build/script/publish/pub.go
Normal file
1
pkg/build/script/publish/pub.go
Normal file
@ -0,0 +1 @@
|
||||
package publish
|
18
pkg/build/script/publish/publish.go
Normal file
18
pkg/build/script/publish/publish.go
Normal 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)
|
||||
}
|
||||
}
|
2
pkg/build/script/publish/pypi.go
Normal file
2
pkg/build/script/publish/pypi.go
Normal file
@ -0,0 +1,2 @@
|
||||
package publish
|
||||
|
85
pkg/build/script/publish/s3.go
Normal file
85
pkg/build/script/publish/s3.go
Normal 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))
|
||||
}
|
||||
}
|
5
pkg/build/script/report/README.md
Normal file
5
pkg/build/script/report/README.md
Normal file
@ -0,0 +1,5 @@
|
||||
cobertura.go
|
||||
coveralls.go
|
||||
gocov.go
|
||||
junit.go
|
||||
phpunit.go
|
123
pkg/build/script/script.go
Normal file
123
pkg/build/script/script.go
Normal 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
28
pkg/build/util.go
Normal 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
57
pkg/build/writer.go
Normal 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
27
pkg/build/writer_test.go
Normal 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
157
pkg/channel/channel.go
Normal 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
36
pkg/channel/conn.go
Normal 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
133
pkg/channel/hub.go
Normal 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
71
pkg/database/builds.go
Normal 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
174
pkg/database/commits.go
Normal 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
24
pkg/database/database.go
Normal 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)
|
||||
}
|
||||
}
|
133
pkg/database/encrypt/encrypt.go
Normal file
133
pkg/database/encrypt/encrypt.go
Normal 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
86
pkg/database/members.go
Normal 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
92
pkg/database/repos.go
Normal 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
|
||||
}
|
126
pkg/database/schema/sample.sql
Normal file
126
pkg/database/schema/sample.sql
Normal 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';
|
198
pkg/database/schema/schema.go
Normal file
198
pkg/database/schema/schema.go
Normal 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
|
||||
}
|
127
pkg/database/schema/schema.sql
Normal file
127
pkg/database/schema/schema.sql
Normal 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
72
pkg/database/settings.go
Normal 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
73
pkg/database/teams.go
Normal 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
|
||||
}
|
136
pkg/database/testing/builds_test.go
Normal file
136
pkg/database/testing/builds_test.go
Normal 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)
|
||||
}
|
||||
}
|
164
pkg/database/testing/commits_test.go
Normal file
164
pkg/database/testing/commits_test.go
Normal 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)
|
||||
}
|
||||
}
|
140
pkg/database/testing/members_test.go
Normal file
140
pkg/database/testing/members_test.go
Normal 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)
|
||||
}
|
||||
}
|
403
pkg/database/testing/repos_test.go
Normal file
403
pkg/database/testing/repos_test.go
Normal 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)
|
||||
}
|
||||
}
|
63
pkg/database/testing/settings_test.go
Normal file
63
pkg/database/testing/settings_test.go
Normal 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")
|
||||
}
|
||||
}
|
169
pkg/database/testing/teams_test.go
Normal file
169
pkg/database/testing/teams_test.go
Normal 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)
|
||||
}
|
||||
}
|
207
pkg/database/testing/testing.go
Normal file
207
pkg/database/testing/testing.go
Normal 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()
|
||||
}
|
169
pkg/database/testing/users_test.go
Normal file
169
pkg/database/testing/users_test.go
Normal 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
90
pkg/database/users.go
Normal 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
256
pkg/handler/admin.go
Normal 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
185
pkg/handler/app.go
Normal 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
91
pkg/handler/auth.go
Normal 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
50
pkg/handler/badges.go
Normal 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
34
pkg/handler/builds.go
Normal 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
56
pkg/handler/commits.go
Normal 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
192
pkg/handler/handler.go
Normal 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
302
pkg/handler/hooks.go
Normal 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
227
pkg/handler/members.go
Normal 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
281
pkg/handler/repos.go
Normal 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
152
pkg/handler/teams.go
Normal 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
|
||||
}
|
173
pkg/handler/testing/team_test.go
Normal file
173
pkg/handler/testing/team_test.go
Normal 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() {
|
||||
|
||||
})
|
||||
})
|
||||
}
|
172
pkg/handler/testing/users_test.go
Normal file
172
pkg/handler/testing/users_test.go
Normal 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
111
pkg/handler/users.go
Normal 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
Loading…
Reference in New Issue
Block a user