You've already forked woodpecker
							
							
				mirror of
				https://github.com/woodpecker-ci/woodpecker.git
				synced 2025-10-30 23:27:39 +02:00 
			
		
		
		
	initial public commit
This commit is contained in:
		
							
								
								
									
										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
		Reference in New Issue
	
	Block a user