mirror of
https://github.com/harness/drone.git
synced 2025-05-13 23:50:47 +08: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